[FEATURE] Add "Pseudo" Site functionality
[Packages/TYPO3.CMS.git] / typo3 / sysext / backend / Classes / Utility / BackendUtility.php
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\Backend\Shortcut\ShortcutRepository;
19 use TYPO3\CMS\Backend\Routing\UriBuilder;
20 use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
21 use TYPO3\CMS\Core\Cache\CacheManager;
22 use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
23 use TYPO3\CMS\Core\Core\Environment;
24 use TYPO3\CMS\Core\Database\Connection;
25 use TYPO3\CMS\Core\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Database\Query\QueryBuilder;
27 use TYPO3\CMS\Core\Database\Query\QueryHelper;
28 use TYPO3\CMS\Core\Database\Query\Restriction\BackendWorkspaceRestriction;
29 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
30 use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
31 use TYPO3\CMS\Core\Database\RelationHandler;
32 use TYPO3\CMS\Core\Exception\SiteNotFoundException;
33 use TYPO3\CMS\Core\Http\ServerRequest;
34 use TYPO3\CMS\Core\Imaging\Icon;
35 use TYPO3\CMS\Core\Imaging\IconFactory;
36 use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
37 use TYPO3\CMS\Core\Localization\LanguageService;
38 use TYPO3\CMS\Core\Log\LogManager;
39 use TYPO3\CMS\Core\Resource\AbstractFile;
40 use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
41 use TYPO3\CMS\Core\Resource\File;
42 use TYPO3\CMS\Core\Resource\ProcessedFile;
43 use TYPO3\CMS\Core\Resource\ResourceFactory;
44 use TYPO3\CMS\Core\Routing\PageUriBuilder;
45 use TYPO3\CMS\Core\Routing\SiteMatcher;
46 use TYPO3\CMS\Core\Site\Entity\PseudoSite;
47 use TYPO3\CMS\Core\Site\SiteFinder;
48 use TYPO3\CMS\Core\Type\Bitmask\Permission;
49 use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser;
50 use TYPO3\CMS\Core\Utility\ArrayUtility;
51 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
52 use TYPO3\CMS\Core\Utility\GeneralUtility;
53 use TYPO3\CMS\Core\Utility\MathUtility;
54 use TYPO3\CMS\Core\Utility\PathUtility;
55 use TYPO3\CMS\Core\Versioning\VersionState;
56 use TYPO3\CMS\Frontend\Compatibility\LegacyDomainResolver;
57 use TYPO3\CMS\Frontend\Page\PageRepository;
58
59 /**
60 * Standard functions available for the TYPO3 backend.
61 * You are encouraged to use this class in your own applications (Backend Modules)
62 * Don't instantiate - call functions with "\TYPO3\CMS\Backend\Utility\BackendUtility::" prefixed the function name.
63 *
64 * Call ALL methods without making an object!
65 * Eg. to get a page-record 51 do this: '\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages',51)'
66 */
67 class BackendUtility
68 {
69 /**
70 * Cache the TCA configuration of tables with their types during runtime
71 *
72 * @var array
73 * @see self::getTCAtypes()
74 * @deprecated since TYPO3 v9.4 will be removed in TYPO3 v10.0.
75 */
76 protected static $tcaTableTypeConfigurationCache = [];
77
78 /*******************************************
79 *
80 * SQL-related, selecting records, searching
81 *
82 *******************************************/
83 /**
84 * Returns the WHERE clause " AND NOT [tablename].[deleted-field]" if a deleted-field
85 * is configured in $GLOBALS['TCA'] for the tablename, $table
86 * This function should ALWAYS be called in the backend for selection on tables which
87 * are configured in $GLOBALS['TCA'] since it will ensure consistent selection of records,
88 * even if they are marked deleted (in which case the system must always treat them as non-existent!)
89 * In the frontend a function, ->enableFields(), is known to filter hidden-field, start- and endtime
90 * and fe_groups as well. But that is a job of the frontend, not the backend. If you need filtering
91 * on those fields as well in the backend you can use ->BEenableFields() though.
92 *
93 * @param string $table Table name present in $GLOBALS['TCA']
94 * @param string $tableAlias Table alias if any
95 * @return string WHERE clause for filtering out deleted records, eg " AND tablename.deleted=0
96 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10, the DeletedRestriction functionality should be used instead.
97 */
98 public static function deleteClause($table, $tableAlias = '')
99 {
100 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);
101 if (empty($GLOBALS['TCA'][$table]['ctrl']['delete'])) {
102 return '';
103 }
104 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
105 ->getQueryBuilderForTable($table)
106 ->expr();
107 return ' AND ' . $expressionBuilder->eq(
108 ($tableAlias ?: $table) . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'],
109 0
110 );
111 }
112
113 /**
114 * Gets record with uid = $uid from $table
115 * You can set $field to a list of fields (default is '*')
116 * Additional WHERE clauses can be added by $where (fx. ' AND blabla = 1')
117 * Will automatically check if records has been deleted and if so, not return anything.
118 * $table must be found in $GLOBALS['TCA']
119 *
120 * @param string $table Table name present in $GLOBALS['TCA']
121 * @param int $uid UID of record
122 * @param string $fields List of fields to select
123 * @param string $where Additional WHERE clause, eg. " AND blablabla = 0
124 * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
125 * @return array|null Returns the row if found, otherwise NULL
126 */
127 public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true)
128 {
129 // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA
130 if ((int)$uid && !empty($GLOBALS['TCA'][$table])) {
131 $queryBuilder = static::getQueryBuilderForTable($table);
132
133 // do not use enabled fields here
134 $queryBuilder->getRestrictions()->removeAll();
135
136 // should the delete clause be used
137 if ($useDeleteClause) {
138 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class));
139 }
140
141 // set table and where clause
142 $queryBuilder
143 ->select(...GeneralUtility::trimExplode(',', $fields, true))
144 ->from($table)
145 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)));
146
147 // add custom where clause
148 if ($where) {
149 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
150 }
151
152 $row = $queryBuilder->execute()->fetch();
153 if ($row) {
154 return $row;
155 }
156 }
157 return null;
158 }
159
160 /**
161 * Like getRecord(), but overlays workspace version if any.
162 *
163 * @param string $table Table name present in $GLOBALS['TCA']
164 * @param int $uid UID of record
165 * @param string $fields List of fields to select
166 * @param string $where Additional WHERE clause, eg. " AND blablabla = 0
167 * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE)
168 * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace
169 * @return array Returns the row if found, otherwise nothing
170 */
171 public static function getRecordWSOL(
172 $table,
173 $uid,
174 $fields = '*',
175 $where = '',
176 $useDeleteClause = true,
177 $unsetMovePointers = false
178 ) {
179 if ($fields !== '*') {
180 $internalFields = GeneralUtility::uniqueList($fields . ',uid,pid');
181 $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause);
182 self::workspaceOL($table, $row, -99, $unsetMovePointers);
183 if (is_array($row)) {
184 foreach ($row as $key => $_) {
185 if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') {
186 unset($row[$key]);
187 }
188 }
189 }
190 } else {
191 $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause);
192 self::workspaceOL($table, $row, -99, $unsetMovePointers);
193 }
194 return $row;
195 }
196
197 /**
198 * Purges computed properties starting with underscore character ('_').
199 *
200 * @param array $record
201 * @return array
202 */
203 public static function purgeComputedPropertiesFromRecord(array $record): array
204 {
205 return array_filter(
206 $record,
207 function (string $propertyName): bool {
208 return $propertyName[0] !== '_';
209 },
210 ARRAY_FILTER_USE_KEY
211 );
212 }
213
214 /**
215 * Purges computed property names starting with underscore character ('_').
216 *
217 * @param array $propertyNames
218 * @return array
219 */
220 public static function purgeComputedPropertyNames(array $propertyNames): array
221 {
222 return array_filter(
223 $propertyNames,
224 function (string $propertyName): bool {
225 return $propertyName[0] !== '_';
226 }
227 );
228 }
229
230 /**
231 * Makes an backwards explode on the $str and returns an array with ($table, $uid).
232 * Example: tt_content_45 => ['tt_content', 45]
233 *
234 * @param string $str [tablename]_[uid] string to explode
235 * @return array
236 */
237 public static function splitTable_Uid($str)
238 {
239 list($uid, $table) = explode('_', strrev($str), 2);
240 return [strrev($table), strrev($uid)];
241 }
242
243 /**
244 * Backend implementation of enableFields()
245 * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime.
246 * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition.
247 * $GLOBALS["SIM_ACCESS_TIME"] is used for date.
248 *
249 * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA'].
250 * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection)
251 * @return string WHERE clause part
252 */
253 public static function BEenableFields($table, $inv = false)
254 {
255 $ctrl = $GLOBALS['TCA'][$table]['ctrl'];
256 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
257 ->getConnectionForTable($table)
258 ->getExpressionBuilder();
259 $query = $expressionBuilder->andX();
260 $invQuery = $expressionBuilder->orX();
261
262 if (is_array($ctrl)) {
263 if (is_array($ctrl['enablecolumns'])) {
264 if ($ctrl['enablecolumns']['disabled'] ?? false) {
265 $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
266 $query->add($expressionBuilder->eq($field, 0));
267 $invQuery->add($expressionBuilder->neq($field, 0));
268 }
269 if ($ctrl['enablecolumns']['starttime'] ?? false) {
270 $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
271 $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']));
272 $invQuery->add(
273 $expressionBuilder->andX(
274 $expressionBuilder->neq($field, 0),
275 $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
276 )
277 );
278 }
279 if ($ctrl['enablecolumns']['endtime'] ?? false) {
280 $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
281 $query->add(
282 $expressionBuilder->orX(
283 $expressionBuilder->eq($field, 0),
284 $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
285 )
286 );
287 $invQuery->add(
288 $expressionBuilder->andX(
289 $expressionBuilder->neq($field, 0),
290 $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])
291 )
292 );
293 }
294 }
295 }
296
297 if ($query->count() === 0) {
298 return '';
299 }
300
301 return ' AND ' . ($inv ? $invQuery : $query);
302 }
303
304 /**
305 * Fetches the localization for a given record.
306 *
307 * @param string $table Table name present in $GLOBALS['TCA']
308 * @param int $uid The uid of the record
309 * @param int $language The uid of the language record in sys_language
310 * @param string $andWhereClause Optional additional WHERE clause (default: '')
311 * @return mixed Multidimensional array with selected records; if none exist, FALSE is returned
312 */
313 public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '')
314 {
315 $recordLocalization = false;
316
317 if (self::isTableLocalizable($table)) {
318 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
319
320 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
321 ->getQueryBuilderForTable($table);
322 $queryBuilder->getRestrictions()
323 ->removeAll()
324 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
325 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
326
327 $queryBuilder->select('*')
328 ->from($table)
329 ->where(
330 $queryBuilder->expr()->eq(
331 $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'],
332 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
333 ),
334 $queryBuilder->expr()->eq(
335 $tcaCtrl['languageField'],
336 $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT)
337 )
338 )
339 ->setMaxResults(1);
340
341 if ($andWhereClause) {
342 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause));
343 }
344
345 $recordLocalization = $queryBuilder->execute()->fetchAll();
346 }
347
348 return $recordLocalization;
349 }
350
351 /*******************************************
352 *
353 * Page tree, TCA related
354 *
355 *******************************************/
356 /**
357 * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id
358 * ($uid) and back to the root.
359 * By default deleted pages are filtered.
360 * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known
361 * from the frontend where the rootline stops when a root-template is found.
362 *
363 * @param int $uid Page id for which to create the root line.
364 * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that
365 * stops the process if we meet a page, the user has no reading access to.
366 * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is
367 * usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing!
368 * @param string[] $additionalFields Additional Fields to select for rootline records
369 * @return array Root line array, all the way to the page tree root (or as far as $clause allows!)
370 */
371 public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = [])
372 {
373 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
374 $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: [];
375 $output = [];
376 $pid = $uid;
377 $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . implode(',', $additionalFields) : '');
378 if (is_array($beGetRootLineCache[$ident] ?? false)) {
379 $output = $beGetRootLineCache[$ident];
380 } else {
381 $loopCheck = 100;
382 $theRowArray = [];
383 while ($uid != 0 && $loopCheck) {
384 $loopCheck--;
385 $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields);
386 if (is_array($row)) {
387 $uid = $row['pid'];
388 $theRowArray[] = $row;
389 } else {
390 break;
391 }
392 }
393 if ($uid == 0) {
394 $theRowArray[] = [
395 'uid' => 0,
396 'pid' => null,
397 'title' => '',
398 'doktype' => null,
399 'tsconfig_includes' => null,
400 'TSconfig' => null,
401 'is_siteroot' => null,
402 't3ver_oid' => null,
403 't3ver_wsid' => null,
404 't3ver_state' => null,
405 't3ver_stage' => null,
406 'backend_layout_next_level' => null
407 ];
408 }
409 $c = count($theRowArray);
410 foreach ($theRowArray as $val) {
411 $c--;
412 $fields = [
413 'uid',
414 'pid',
415 'title',
416 'doktype',
417 'tsconfig_includes',
418 'TSconfig',
419 'is_siteroot',
420 't3ver_oid',
421 't3ver_wsid',
422 't3ver_state',
423 't3ver_stage',
424 'backend_layout_next_level',
425 ];
426 $fields = array_merge($fields, $additionalFields);
427 $output[$c] = array_intersect_key($val, array_combine($fields, $fields));
428 if (isset($val['_ORIG_pid'])) {
429 $output[$c]['_ORIG_pid'] = $val['_ORIG_pid'];
430 }
431 }
432 $beGetRootLineCache[$ident] = $output;
433 $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache);
434 }
435 return $output;
436 }
437
438 /**
439 * Gets the cached page record for the rootline
440 *
441 * @param int $uid Page id for which to create the root line.
442 * @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.
443 * @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!
444 * @param string[] $additionalFields AdditionalFields to fetch from the root line
445 * @return array Cached page record for the rootline
446 * @see BEgetRootLine
447 */
448 protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = [])
449 {
450 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime');
451 $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: [];
452 $ident = $uid . '-' . $clause . '-' . $workspaceOL;
453 if (is_array($pageForRootlineCache[$ident] ?? false)) {
454 $row = $pageForRootlineCache[$ident];
455 } else {
456 $queryBuilder = static::getQueryBuilderForTable('pages');
457 $queryBuilder->getRestrictions()
458 ->removeAll()
459 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
460
461 $row = $queryBuilder
462 ->select(
463 'pid',
464 'uid',
465 'title',
466 'doktype',
467 'tsconfig_includes',
468 'TSconfig',
469 'is_siteroot',
470 't3ver_oid',
471 't3ver_wsid',
472 't3ver_state',
473 't3ver_stage',
474 'backend_layout_next_level',
475 ...$additionalFields
476 )
477 ->from('pages')
478 ->where(
479 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
480 QueryHelper::stripLogicalOperatorPrefix($clause)
481 )
482 ->execute()
483 ->fetch();
484
485 if ($row) {
486 $newLocation = false;
487 if ($workspaceOL) {
488 self::workspaceOL('pages', $row);
489 $newLocation = self::getMovePlaceholder('pages', $row['uid'], 'pid');
490 }
491 if (is_array($row)) {
492 if ($newLocation !== false) {
493 $row['pid'] = $newLocation['pid'];
494 } else {
495 self::fixVersioningPid('pages', $row);
496 }
497 $pageForRootlineCache[$ident] = $row;
498 $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache);
499 }
500 }
501 }
502 return $row;
503 }
504
505 /**
506 * Opens the page tree to the specified page id
507 *
508 * @param int $pid Page id.
509 * @param bool $clearExpansion If set, then other open branches are closed.
510 */
511 public static function openPageTree($pid, $clearExpansion)
512 {
513 $beUser = static::getBackendUserAuthentication();
514 // Get current expansion data:
515 if ($clearExpansion) {
516 $expandedPages = [];
517 } else {
518 $expandedPages = unserialize($beUser->uc['browseTrees']['browsePages']);
519 }
520 // Get rootline:
521 $rL = self::BEgetRootLine($pid);
522 // First, find out what mount index to use (if more than one DB mount exists):
523 $mountIndex = 0;
524 $mountKeys = array_flip($beUser->returnWebmounts());
525 foreach ($rL as $rLDat) {
526 if (isset($mountKeys[$rLDat['uid']])) {
527 $mountIndex = $mountKeys[$rLDat['uid']];
528 break;
529 }
530 }
531 // Traverse rootline and open paths:
532 foreach ($rL as $rLDat) {
533 $expandedPages[$mountIndex][$rLDat['uid']] = 1;
534 }
535 // Write back:
536 $beUser->uc['browseTrees']['browsePages'] = serialize($expandedPages);
537 $beUser->writeUC();
538 }
539
540 /**
541 * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage"
542 * Each part of the path will be limited to $titleLimit characters
543 * Deleted pages are filtered out.
544 *
545 * @param int $uid Page uid for which to create record path
546 * @param string $clause Clause is additional where clauses, eg.
547 * @param int $titleLimit Title limit
548 * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so)
549 * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set.
550 */
551 public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0)
552 {
553 if (!$titleLimit) {
554 $titleLimit = 1000;
555 }
556 $output = $fullOutput = '/';
557 $clause = trim($clause);
558 if ($clause !== '' && substr($clause, 0, 3) !== 'AND') {
559 $clause = 'AND ' . $clause;
560 }
561 $data = self::BEgetRootLine($uid, $clause);
562 foreach ($data as $record) {
563 if ($record['uid'] === 0) {
564 continue;
565 }
566 $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output;
567 if ($fullTitleLimit) {
568 $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput;
569 }
570 }
571 if ($fullTitleLimit) {
572 return [$output, $fullOutput];
573 }
574 return $output;
575 }
576
577 /**
578 * Gets the original translation pointer table, which is always the same table
579 *
580 * @param string $table Name of the table
581 * @return string Pointer table (if any)
582 */
583 public static function getOriginalTranslationTable($table)
584 {
585 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);
586 return $table;
587 }
588
589 /**
590 * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA'].
591 *
592 * @param string $table The table to check
593 * @return bool Whether a table is localizable
594 */
595 public static function isTableLocalizable($table)
596 {
597 $isLocalizable = false;
598 if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) {
599 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl'];
600 $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField'];
601 }
602 return $isLocalizable;
603 }
604
605 /**
606 * 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.
607 * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin.
608 * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause)
609 *
610 * @param int $id Page uid for which to check read-access
611 * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1);
612 * @return array|bool Returns page record if OK, otherwise FALSE.
613 */
614 public static function readPageAccess($id, $perms_clause)
615 {
616 if ((string)$id !== '') {
617 $id = (int)$id;
618 if (!$id) {
619 if (static::getBackendUserAuthentication()->isAdmin()) {
620 return ['_thePath' => '/'];
621 }
622 } else {
623 $pageinfo = self::getRecord('pages', $id, '*', $perms_clause);
624 if ($pageinfo['uid'] && static::getBackendUserAuthentication()->isInWebMount($id, $perms_clause)) {
625 self::workspaceOL('pages', $pageinfo);
626 if (is_array($pageinfo)) {
627 self::fixVersioningPid('pages', $pageinfo);
628 list($pageinfo['_thePath'], $pageinfo['_thePathFull']) = self::getRecordPath((int)$pageinfo['uid'], $perms_clause, 15, 1000);
629 return $pageinfo;
630 }
631 }
632 }
633 }
634 return false;
635 }
636
637 /**
638 * Returns the "types" configuration parsed into an array for the record, $rec, from table, $table
639 *
640 * @param string $table Table name (present in TCA)
641 * @param array $rec Record from $table
642 * @param bool $useFieldNameAsKey If $useFieldNameAsKey is set, then the fieldname is associative keys in the return array, otherwise just numeric keys.
643 * @return array|null
644 * @deprecated since TYPO3 v9.4 will be removed in TYPO3 v10.0.
645 */
646 public static function getTCAtypes($table, $rec, $useFieldNameAsKey = false)
647 {
648 trigger_error('BackendUtility::getTCAtypes will be removed in TYPO3 v10. The method is not in use anymore.', E_USER_DEPRECATED);
649 if (isset($GLOBALS['TCA'][$table])) {
650 // Get type value:
651 $fieldValue = self::getTCAtypeValue($table, $rec);
652 $cacheIdentifier = $table . '-type-' . $fieldValue . '-fnk-' . $useFieldNameAsKey;
653
654 // Fetch from first-level-cache if available
655 if (isset(self::$tcaTableTypeConfigurationCache[$cacheIdentifier])) {
656 return self::$tcaTableTypeConfigurationCache[$cacheIdentifier];
657 }
658
659 // Get typesConf
660 $typesConf = $GLOBALS['TCA'][$table]['types'][$fieldValue] ?? null;
661 // Get fields list and traverse it
662 $fieldList = explode(',', $typesConf['showitem']);
663
664 // Add subtype fields e.g. for a valid RTE transformation
665 // The RTE runs the DB -> RTE transformation only, if the RTE field is part of the getTCAtypes array
666 if (isset($typesConf['subtype_value_field'])) {
667 $subType = $rec[$typesConf['subtype_value_field']];
668 if (isset($typesConf['subtypes_addlist'][$subType])) {
669 $subFields = GeneralUtility::trimExplode(',', $typesConf['subtypes_addlist'][$subType], true);
670 $fieldList = array_merge($fieldList, $subFields);
671 }
672 }
673
674 // Add palette fields e.g. for a valid RTE transformation
675 $paletteFieldList = [];
676 foreach ($fieldList as $fieldData) {
677 $fieldDataArray = GeneralUtility::trimExplode(';', $fieldData);
678 // first two entries would be fieldname and altTitle, they are not used here.
679 $pPalette = $fieldDataArray[2] ?? null;
680 if ($pPalette
681 && isset($GLOBALS['TCA'][$table]['palettes'][$pPalette])
682 && is_array($GLOBALS['TCA'][$table]['palettes'][$pPalette])
683 && isset($GLOBALS['TCA'][$table]['palettes'][$pPalette]['showitem'])
684 ) {
685 $paletteFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['palettes'][$pPalette]['showitem'], true);
686 foreach ($paletteFields as $paletteField) {
687 if ($paletteField !== '--linebreak--') {
688 $paletteFieldList[] = $paletteField;
689 }
690 }
691 }
692 }
693 $fieldList = array_merge($fieldList, $paletteFieldList);
694 $altFieldList = [];
695 // Traverse fields in types config and parse the configuration into a nice array:
696 foreach ($fieldList as $k => $v) {
697 $vArray = GeneralUtility::trimExplode(';', $v);
698 $fieldList[$k] = [
699 'field' => $vArray[0],
700 'title' => $vArray[1] ?? null,
701 'palette' => $vArray[2] ?? null,
702 'spec' => [],
703 'origString' => $v
704 ];
705 if ($useFieldNameAsKey) {
706 $altFieldList[$fieldList[$k]['field']] = $fieldList[$k];
707 }
708 }
709 if ($useFieldNameAsKey) {
710 $fieldList = $altFieldList;
711 }
712
713 // Add to first-level-cache
714 self::$tcaTableTypeConfigurationCache[$cacheIdentifier] = $fieldList;
715
716 // Return array:
717 return $fieldList;
718 }
719 return null;
720 }
721
722 /**
723 * Returns the "type" value of $rec from $table which can be used to look up the correct "types" rendering section in $GLOBALS['TCA']
724 * If no "type" field is configured in the "ctrl"-section of the $GLOBALS['TCA'] for the table, zero is used.
725 * 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)
726 *
727 * Note: This method is very similar to the type determination of FormDataProvider/DatabaseRecordTypeValue,
728 * however, it has two differences:
729 * 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).
730 * 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"
731 * and "group" field values are stored, which makes different processing of the "foreign pointer field" type field variant necessary.
732 *
733 * @param string $table Table name present in TCA
734 * @param array $row Record from $table
735 * @throws \RuntimeException
736 * @return string Field value
737 */
738 public static function getTCAtypeValue($table, $row)
739 {
740 $typeNum = 0;
741 if ($GLOBALS['TCA'][$table]) {
742 $field = $GLOBALS['TCA'][$table]['ctrl']['type'];
743 if (strpos($field, ':') !== false) {
744 list($pointerField, $foreignTableTypeField) = explode(':', $field);
745 // Get field value from database if field is not in the $row array
746 if (!isset($row[$pointerField])) {
747 $localRow = self::getRecord($table, $row['uid'], $pointerField);
748 $foreignUid = $localRow[$pointerField];
749 } else {
750 $foreignUid = $row[$pointerField];
751 }
752 if ($foreignUid) {
753 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config'];
754 $relationType = $fieldConfig['type'];
755 if ($relationType === 'select') {
756 $foreignTable = $fieldConfig['foreign_table'];
757 } elseif ($relationType === 'group') {
758 $allowedTables = explode(',', $fieldConfig['allowed']);
759 $foreignTable = $allowedTables[0];
760 } else {
761 throw new \RuntimeException(
762 'TCA foreign field pointer fields are only allowed to be used with group or select field types.',
763 1325862240
764 );
765 }
766 $foreignRow = self::getRecord($foreignTable, $foreignUid, $foreignTableTypeField);
767 if ($foreignRow[$foreignTableTypeField]) {
768 $typeNum = $foreignRow[$foreignTableTypeField];
769 }
770 }
771 } else {
772 $typeNum = $row[$field];
773 }
774 // If that value is an empty string, set it to "0" (zero)
775 if (empty($typeNum)) {
776 $typeNum = 0;
777 }
778 }
779 // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist)
780 if (!isset($GLOBALS['TCA'][$table]['types'][$typeNum]) || !$GLOBALS['TCA'][$table]['types'][$typeNum]) {
781 $typeNum = isset($GLOBALS['TCA'][$table]['types']['0']) ? 0 : 1;
782 }
783 // Force to string. Necessary for eg '-1' to be recognized as a type value.
784 $typeNum = (string)$typeNum;
785 return $typeNum;
786 }
787
788 /*******************************************
789 *
790 * Caching related
791 *
792 *******************************************/
793 /**
794 * Stores $data in the 'cache_hash' cache with the hash key, $hash
795 * and visual/symbolic identification, $ident
796 *
797 * @param string $hash 32 bit hash string (eg. a md5 hash of a serialized array identifying the data being stored)
798 * @param mixed $data The data to store
799 * @param string $ident $ident is just a textual identification in order to inform about the content!
800 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10, use the Caching Framework directly
801 */
802 public static function storeHash($hash, $data, $ident)
803 {
804 trigger_error('This method will be removed in TYPO3 v10.0, use the Caching Framework directly.', E_USER_DEPRECATED);
805 $cacheManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
806 $cacheManager->getCache('cache_hash')->set($hash, $data, ['ident_' . $ident], 0);
807 }
808
809 /**
810 * Returns data stored for the hash string in the cache "cache_hash"
811 * Can be used to retrieved a cached value, array or object
812 *
813 * @param string $hash The hash-string which was used to store the data value
814 * @return mixed The "data" from the cache
815 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10, use the Caching Framework directly
816 */
817 public static function getHash($hash)
818 {
819 trigger_error('This method will be removed in TYPO3 v10.0, use the Caching Framework directly.', E_USER_DEPRECATED);
820 $cacheManager = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Cache\CacheManager::class);
821 $cacheEntry = $cacheManager->getCache('cache_hash')->get($hash);
822 $hashContent = null;
823 if ($cacheEntry) {
824 $hashContent = $cacheEntry;
825 }
826 return $hashContent;
827 }
828
829 /*******************************************
830 *
831 * TypoScript related
832 *
833 *******************************************/
834 /**
835 * Returns the Page TSconfig for page with id, $id
836 *
837 * @param int $id Page uid for which to create Page TSconfig
838 * @param array $rootLine @deprecated
839 * @param bool $returnPartArray @deprecated
840 * @return array Page TSconfig
841 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser
842 */
843 public static function getPagesTSconfig($id, $rootLine = null, $returnPartArray = false)
844 {
845 $id = (int)$id;
846
847 $cache = self::getRuntimeCache();
848 if ($returnPartArray === false
849 && $rootLine === null
850 && $cache->has('pagesTsConfigIdToHash' . $id)
851 ) {
852 return $cache->get('pagesTsConfigHashToContent' . $cache->get('pagesTsConfigIdToHash' . $id));
853 }
854 $tsConfig = [];
855 // No custom rootline, so the results can be cached
856 if (!is_array($rootLine)) {
857 $rootLine = self::BEgetRootLine($id, '', true);
858 $useCacheForCurrentPageId = true;
859 } else {
860 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);
861 $useCacheForCurrentPageId = false;
862 }
863
864 $TSdataArray = static::getRawPagesTSconfig($id, $rootLine);
865 if ($returnPartArray) {
866 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);
867 return $TSdataArray;
868 }
869 // Parsing the page TS-Config
870 $pageTs = implode(LF . '[GLOBAL]' . LF, $TSdataArray);
871 /* @var $parseObj \TYPO3\CMS\Backend\Configuration\TsConfigParser */
872 $parseObj = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Configuration\TsConfigParser::class);
873 $res = $parseObj->parseTSconfig($pageTs, 'PAGES', $id, $rootLine);
874 if ($res) {
875 $tsConfig = $res['TSconfig'];
876 }
877 $cacheHash = $res['hash'];
878 // Get User TSconfig overlay
879
880 $userTSconfig = static::getBackendUserAuthentication()->getTSConfig() ?? [];
881 $isCacheHashExtendedWithUserUid = false;
882 if (is_array($userTSconfig['page.'])) {
883 ArrayUtility::mergeRecursiveWithOverrule($tsConfig, $userTSconfig['page.']);
884 $isCacheHashExtendedWithUserUid = true;
885 $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid'];
886 }
887
888 // Overlay page "mod." ts with user ts in a special and deprecated way
889 if (is_array($userTSconfig['mod.'])) {
890 // @deprecated This entire "if" and variable $isCacheHashExtendedWithUserUid can be deleted in v10
891 trigger_error(
892 'Overriding page TSconfig "mod." with user TSconfig "mod." is deprecated. Use user TSconfig "page.mod." instead',
893 E_USER_DEPRECATED
894 );
895 if (!is_array($tsConfig['mod.'])) {
896 $tsConfig['mod.'] = [];
897 }
898 ArrayUtility::mergeRecursiveWithOverrule($tsConfig['mod.'], $userTSconfig['mod.']);
899 if (!$isCacheHashExtendedWithUserUid) {
900 $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid'];
901 }
902 }
903
904 if ($useCacheForCurrentPageId) {
905 // Many pages end up with the same ts config. To reduce memory usage, the cache
906 // entries are a linked list: One or more pids point to content hashes which then
907 // contain the cached content.
908 $cache->set('pagesTsConfigHashToContent' . $cacheHash, $tsConfig, ['pagesTsConfig']);
909 $cache->set('pagesTsConfigIdToHash' . $id, $cacheHash, ['pagesTsConfig']);
910 }
911
912 return $tsConfig;
913 }
914
915 /**
916 * Returns the non-parsed Page TSconfig for page with id, $id
917 *
918 * @param int $id Page uid for which to create Page TSconfig
919 * @param array $rootLine If $rootLine is an array, that is used as rootline, otherwise rootline is just calculated
920 * @return array Non-parsed Page TSconfig
921 */
922 public static function getRawPagesTSconfig($id, array $rootLine = null)
923 {
924 if (!is_array($rootLine)) {
925 $rootLine = self::BEgetRootLine($id, '', true);
926 }
927
928 // Order correctly
929 ksort($rootLine);
930 $tsDataArray = [];
931 // Setting default configuration
932 $tsDataArray['defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig'];
933 foreach ($rootLine as $k => $v) {
934 if (trim($v['tsconfig_includes'])) {
935 $includeTsConfigFileList = GeneralUtility::trimExplode(',', $v['tsconfig_includes'], true);
936 // Traversing list
937 foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) {
938 if (strpos($includeTsConfigFile, 'EXT:') === 0) {
939 list($includeTsConfigFileExtensionKey, $includeTsConfigFilename) = explode(
940 '/',
941 substr($includeTsConfigFile, 4),
942 2
943 );
944 if ((string)$includeTsConfigFileExtensionKey !== ''
945 && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey)
946 && (string)$includeTsConfigFilename !== ''
947 ) {
948 $includeTsConfigFileAndPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey) .
949 $includeTsConfigFilename;
950 if (file_exists($includeTsConfigFileAndPath)) {
951 $tsDataArray['uid_' . $v['uid'] . '_static_' . $key] = file_get_contents($includeTsConfigFileAndPath);
952 }
953 }
954 }
955 }
956 }
957 $tsDataArray['uid_' . $v['uid']] = $v['TSconfig'];
958 }
959
960 $tsDataArray = static::emitGetPagesTSconfigPreIncludeSignal($tsDataArray, $id, $rootLine);
961 $tsDataArray = TypoScriptParser::checkIncludeLines_array($tsDataArray);
962
963 return $tsDataArray;
964 }
965
966 /*******************************************
967 *
968 * Users / Groups related
969 *
970 *******************************************/
971 /**
972 * Returns an array with be_users records of all user NOT DELETED sorted by their username
973 * Keys in the array is the be_users uid
974 *
975 * @param string $fields Optional $fields list (default: username,usergroup,usergroup_cached_list,uid) can be used to set the selected fields
976 * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query
977 * @return array
978 */
979 public static function getUserNames($fields = 'username,usergroup,usergroup_cached_list,uid', $where = '')
980 {
981 return self::getRecordsSortedByTitle(
982 GeneralUtility::trimExplode(',', $fields, true),
983 'be_users',
984 'username',
985 'AND pid=0 ' . $where
986 );
987 }
988
989 /**
990 * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title
991 *
992 * @param string $fields Field list
993 * @param string $where WHERE clause
994 * @return array
995 */
996 public static function getGroupNames($fields = 'title,uid', $where = '')
997 {
998 return self::getRecordsSortedByTitle(
999 GeneralUtility::trimExplode(',', $fields, true),
1000 'be_groups',
1001 'title',
1002 'AND pid=0 ' . $where
1003 );
1004 }
1005
1006 /**
1007 * Returns an array of all non-deleted records of a table sorted by a given title field.
1008 * The value of the title field will be replaced by the return value
1009 * of self::getRecordTitle() before the sorting is performed.
1010 *
1011 * @param array $fields Fields to select
1012 * @param string $table Table name
1013 * @param string $titleField Field that will contain the record title
1014 * @param string $where Additional where clause
1015 * @return array Array of sorted records
1016 */
1017 protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '')
1018 {
1019 $fieldsIndex = array_flip($fields);
1020 // Make sure the titleField is amongst the fields when getting sorted
1021 $fieldsIndex[$titleField] = 1;
1022
1023 $result = [];
1024
1025 $queryBuilder = static::getQueryBuilderForTable($table);
1026 $queryBuilder->getRestrictions()
1027 ->removeAll()
1028 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1029
1030 $res = $queryBuilder
1031 ->select('*')
1032 ->from($table)
1033 ->where(QueryHelper::stripLogicalOperatorPrefix($where))
1034 ->execute();
1035
1036 while ($record = $res->fetch()) {
1037 // store the uid, because it might be unset if it's not among the requested $fields
1038 $recordId = $record['uid'];
1039 $record[$titleField] = self::getRecordTitle($table, $record);
1040
1041 // include only the requested fields in the result
1042 $result[$recordId] = array_intersect_key($record, $fieldsIndex);
1043 }
1044
1045 // sort records by $sortField. This is not done in the query because the title might have been overwritten by
1046 // self::getRecordTitle();
1047 return ArrayUtility::sortArraysByKey($result, $titleField);
1048 }
1049
1050 /**
1051 * Returns an array with be_groups records (like ->getGroupNames) but:
1052 * - 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.
1053 *
1054 * @param string $fields Field list; $fields specify the fields selected (default: title,uid)
1055 * @return array
1056 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.
1057 */
1058 public static function getListGroupNames($fields = 'title, uid')
1059 {
1060 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);
1061 $beUser = static::getBackendUserAuthentication();
1062 $exQ = '';
1063 if (!$beUser->isAdmin()) {
1064 $exQ = ' AND uid IN (' . ($beUser->user['usergroup_cached_list'] ?: 0) . ')';
1065 }
1066 return self::getGroupNames($fields, $exQ);
1067 }
1068
1069 /**
1070 * Returns the array $usernames with the names of all users NOT IN $groupArray changed to the uid (hides the usernames!).
1071 * If $excludeBlindedFlag is set, then these records are unset from the array $usernames
1072 * 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
1073 *
1074 * @param array $usernames User names
1075 * @param array $groupArray Group names
1076 * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
1077 * @return array User names, blinded
1078 */
1079 public static function blindUserNames($usernames, $groupArray, $excludeBlindedFlag = false)
1080 {
1081 if (is_array($usernames) && is_array($groupArray)) {
1082 foreach ($usernames as $uid => $row) {
1083 $userN = $uid;
1084 $set = 0;
1085 if ($row['uid'] != static::getBackendUserAuthentication()->user['uid']) {
1086 foreach ($groupArray as $v) {
1087 if ($v && GeneralUtility::inList($row['usergroup_cached_list'], $v)) {
1088 $userN = $row['username'];
1089 $set = 1;
1090 }
1091 }
1092 } else {
1093 $userN = $row['username'];
1094 $set = 1;
1095 }
1096 $usernames[$uid]['username'] = $userN;
1097 if ($excludeBlindedFlag && !$set) {
1098 unset($usernames[$uid]);
1099 }
1100 }
1101 }
1102 return $usernames;
1103 }
1104
1105 /**
1106 * Corresponds to blindUserNames but works for groups instead
1107 *
1108 * @param array $groups Group names
1109 * @param array $groupArray Group names (reference)
1110 * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames
1111 * @return array
1112 */
1113 public static function blindGroupNames($groups, $groupArray, $excludeBlindedFlag = false)
1114 {
1115 if (is_array($groups) && is_array($groupArray)) {
1116 foreach ($groups as $uid => $row) {
1117 $groupN = $uid;
1118 $set = 0;
1119 if (in_array($uid, $groupArray, false)) {
1120 $groupN = $row['title'];
1121 $set = 1;
1122 }
1123 $groups[$uid]['title'] = $groupN;
1124 if ($excludeBlindedFlag && !$set) {
1125 unset($groups[$uid]);
1126 }
1127 }
1128 }
1129 return $groups;
1130 }
1131
1132 /*******************************************
1133 *
1134 * Output related
1135 *
1136 *******************************************/
1137 /**
1138 * Returns the difference in days between input $tstamp and $EXEC_TIME
1139 *
1140 * @param int $tstamp Time stamp, seconds
1141 * @return int
1142 */
1143 public static function daysUntil($tstamp)
1144 {
1145 $delta_t = $tstamp - $GLOBALS['EXEC_TIME'];
1146 return ceil($delta_t / (3600 * 24));
1147 }
1148
1149 /**
1150 * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'])
1151 *
1152 * @param int $tstamp Time stamp, seconds
1153 * @return string Formatted time
1154 */
1155 public static function date($tstamp)
1156 {
1157 return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp);
1158 }
1159
1160 /**
1161 * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'])
1162 *
1163 * @param int $value Time stamp, seconds
1164 * @return string Formatted time
1165 */
1166 public static function datetime($value)
1167 {
1168 return date(
1169 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'],
1170 $value
1171 );
1172 }
1173
1174 /**
1175 * Returns $value (in seconds) formatted as hh:mm:ss
1176 * For instance $value = 3600 + 60*2 + 3 should return "01:02:03"
1177 *
1178 * @param int $value Time stamp, seconds
1179 * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm
1180 * @return string Formatted time
1181 */
1182 public static function time($value, $withSeconds = true)
1183 {
1184 return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
1185 }
1186
1187 /**
1188 * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted.
1189 *
1190 * @param int $seconds Seconds could be the difference of a certain timestamp and time()
1191 * @param string $labels Labels should be something like ' min| hrs| days| yrs| min| hour| day| year'. This value is typically delivered by this function call: $GLOBALS["LANG"]->sL("LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears")
1192 * @return string Formatted time
1193 */
1194 public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year')
1195 {
1196 $labelArr = GeneralUtility::trimExplode('|', $labels, true);
1197 $absSeconds = abs($seconds);
1198 $sign = $seconds < 0 ? -1 : 1;
1199 if ($absSeconds < 3600) {
1200 $val = round($absSeconds / 60);
1201 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]);
1202 } elseif ($absSeconds < 24 * 3600) {
1203 $val = round($absSeconds / 3600);
1204 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]);
1205 } elseif ($absSeconds < 365 * 24 * 3600) {
1206 $val = round($absSeconds / (24 * 3600));
1207 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]);
1208 } else {
1209 $val = round($absSeconds / (365 * 24 * 3600));
1210 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]);
1211 }
1212 return $seconds;
1213 }
1214
1215 /**
1216 * Returns a formatted timestamp if $tstamp is set.
1217 * The date/datetime will be followed by the age in parenthesis.
1218 *
1219 * @param int $tstamp Time stamp, seconds
1220 * @param int $prefix 1/-1 depending on polarity of age.
1221 * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm
1222 * @return string
1223 */
1224 public static function dateTimeAge($tstamp, $prefix = 1, $date = '')
1225 {
1226 if (!$tstamp) {
1227 return '';
1228 }
1229 $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears');
1230 $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')';
1231 return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age;
1232 }
1233
1234 /**
1235 * Resolves file references for a given record.
1236 *
1237 * @param string $tableName Name of the table of the record
1238 * @param string $fieldName Name of the field of the record
1239 * @param array $element Record data
1240 * @param int|null $workspaceId Workspace to fetch data for
1241 * @return \TYPO3\CMS\Core\Resource\FileReference[]|null
1242 */
1243 public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null)
1244 {
1245 if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) {
1246 return null;
1247 }
1248 $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'];
1249 if (empty($configuration['type']) || $configuration['type'] !== 'inline'
1250 || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference'
1251 ) {
1252 return null;
1253 }
1254
1255 $fileReferences = [];
1256 /** @var $relationHandler RelationHandler */
1257 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class);
1258 if ($workspaceId !== null) {
1259 $relationHandler->setWorkspaceId($workspaceId);
1260 }
1261 $relationHandler->start(
1262 $element[$fieldName],
1263 $configuration['foreign_table'],
1264 $configuration['MM'],
1265 $element['uid'],
1266 $tableName,
1267 $configuration
1268 );
1269 $relationHandler->processDeletePlaceholder();
1270 $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']];
1271
1272 foreach ($referenceUids as $referenceUid) {
1273 try {
1274 $fileReference = ResourceFactory::getInstance()->getFileReferenceObject(
1275 $referenceUid,
1276 [],
1277 $workspaceId === 0
1278 );
1279 $fileReferences[$fileReference->getUid()] = $fileReference;
1280 } catch (\TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException $e) {
1281 /**
1282 * We just catch the exception here
1283 * Reasoning: There is nothing an editor or even admin could do
1284 */
1285 } catch (\InvalidArgumentException $e) {
1286 /**
1287 * The storage does not exist anymore
1288 * Log the exception message for admins as they maybe can restore the storage
1289 */
1290 self::getLogger()->error($e->getMessage(), ['table' => $tableName, 'fieldName' => $fieldName, 'referenceUid' => $referenceUid, 'exception' => $e]);
1291 }
1292 }
1293
1294 return $fileReferences;
1295 }
1296
1297 /**
1298 * 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
1299 * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example)
1300 * Thumbsnails are linked to the show_item.php script which will display further details.
1301 *
1302 * @param array $row Row is the database row from the table, $table.
1303 * @param string $table Table name for $row (present in TCA)
1304 * @param string $field Field is pointing to the list of image files
1305 * @param string $backPath Back path prefix for image tag src="" field
1306 * @param string $thumbScript UNUSED since FAL
1307 * @param string $uploaddir Optional: $uploaddir is the directory relative to Environment::getPublicPath() 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!)
1308 * @param int $abs UNUSED
1309 * @param string $tparams Optional: $tparams is additional attributes for the image tags
1310 * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default.
1311 * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup
1312 * @return string Thumbnail image tag.
1313 */
1314 public static function thumbCode(
1315 $row,
1316 $table,
1317 $field,
1318 $backPath = '',
1319 $thumbScript = '',
1320 $uploaddir = null,
1321 $abs = 0,
1322 $tparams = '',
1323 $size = '',
1324 $linkInfoPopup = true
1325 ) {
1326 // Check and parse the size parameter
1327 $size = trim($size);
1328 $sizeParts = [64, 64];
1329 if ($size) {
1330 $sizeParts = explode('x', $size . 'x' . $size);
1331 }
1332 $thumbData = '';
1333 $fileReferences = static::resolveFileReferences($table, $field, $row);
1334 // FAL references
1335 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
1336 if ($fileReferences !== null) {
1337 foreach ($fileReferences as $fileReferenceObject) {
1338 // Do not show previews of hidden references
1339 if ($fileReferenceObject->getProperty('hidden')) {
1340 continue;
1341 }
1342 $fileObject = $fileReferenceObject->getOriginalFile();
1343
1344 if ($fileObject->isMissing()) {
1345 $thumbData .= '<span class="label label-danger">'
1346 . htmlspecialchars(
1347 static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing')
1348 )
1349 . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1350 continue;
1351 }
1352
1353 // Preview web image or media elements
1354 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails']
1355 && GeneralUtility::inList(
1356 $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'],
1357 $fileReferenceObject->getExtension()
1358 )
1359 ) {
1360 $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop'));
1361 $cropArea = $cropVariantCollection->getCropArea();
1362 $parameters = json_encode([
1363 'fileId' => $fileObject->getUid(),
1364 'configuration' => [
1365 'width' => $sizeParts[0],
1366 'height' => $sizeParts[1] . 'c',
1367 'crop' => $cropArea->isEmpty() ? null : $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject),
1368 ]
1369 ]);
1370 $uriParameters = [
1371 'parameters' => $parameters,
1372 'hmac' => GeneralUtility::hmac(
1373 $parameters,
1374 \TYPO3\CMS\Backend\Controller\File\ThumbnailController::class
1375 ),
1376 ];
1377 $imageUrl = (string)GeneralUtility::makeInstance(UriBuilder::class)
1378 ->buildUriFromRoute('thumbnails', $uriParameters);
1379 $attributes = [
1380 'src' => $imageUrl,
1381 'width' => (int)$sizeParts[0],
1382 'height' => (int)$sizeParts[1],
1383 'alt' => $fileReferenceObject->getName(),
1384 ];
1385 $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . '/>';
1386 } else {
1387 // Icon
1388 $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1389 . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1390 . '</span>';
1391 }
1392 if ($linkInfoPopup) {
1393 $onClick = 'top.TYPO3.InfoWindow.showItem(\'_FILE\',\'' . (int)$fileObject->getUid() . '\'); return false;';
1394 $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $imgTag . '</a> ';
1395 } else {
1396 $thumbData .= $imgTag;
1397 }
1398 }
1399 } else {
1400 // Find uploaddir automatically
1401 if ($uploaddir === null) {
1402 $uploaddir = $GLOBALS['TCA'][$table]['columns'][$field]['config']['uploadfolder'];
1403 }
1404 $uploaddir = rtrim($uploaddir, '/');
1405 // Traverse files:
1406 $thumbs = GeneralUtility::trimExplode(',', $row[$field], true);
1407 $thumbData = '';
1408 foreach ($thumbs as $theFile) {
1409 if ($theFile) {
1410 $fileName = trim($uploaddir . '/' . $theFile, '/');
1411 try {
1412 /** @var File $fileObject */
1413 $fileObject = ResourceFactory::getInstance()->retrieveFileOrFolderObject($fileName);
1414 // Skip the resource if it's not of type AbstractFile. One case where this can happen if the
1415 // storage has been externally modified and the field value now points to a folder
1416 // instead of a file.
1417 if (!$fileObject instanceof AbstractFile) {
1418 continue;
1419 }
1420 if ($fileObject->isMissing()) {
1421 $thumbData .= '<span class="label label-danger">'
1422 . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
1423 . '</span>&nbsp;' . htmlspecialchars($fileObject->getName()) . '<br />';
1424 continue;
1425 }
1426 } catch (ResourceDoesNotExistException $exception) {
1427 $thumbData .= '<span class="label label-danger">'
1428 . htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing'))
1429 . '</span>&nbsp;' . htmlspecialchars($fileName) . '<br />';
1430 continue;
1431 }
1432
1433 $fileExtension = $fileObject->getExtension();
1434 if ($fileExtension === 'ttf'
1435 || GeneralUtility::inList($GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'], $fileExtension)
1436 ) {
1437 $imageUrl = $fileObject->process(
1438 ProcessedFile::CONTEXT_IMAGEPREVIEW,
1439 [
1440 'width' => $sizeParts[0],
1441 'height' => $sizeParts[1]
1442 ]
1443 )->getPublicUrl(true);
1444
1445 $image = '<img src="' . htmlspecialchars($imageUrl) . '" hspace="2" border="0" title="' . htmlspecialchars($fileObject->getName()) . '"' . $tparams . ' alt="" />';
1446 if ($linkInfoPopup) {
1447 $onClick = 'top.TYPO3.InfoWindow.showItem(\'_FILE\', ' . GeneralUtility::quoteJSvalue($fileName) . ',\'\');return false;';
1448 $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $image . '</a> ';
1449 } else {
1450 $thumbData .= $image;
1451 }
1452 } else {
1453 // Gets the icon
1454 $fileIcon = '<span title="' . htmlspecialchars($fileObject->getName()) . '">'
1455 . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render()
1456 . '</span>';
1457 if ($linkInfoPopup) {
1458 $onClick = 'top.TYPO3.InfoWindow.showItem(\'_FILE\', ' . GeneralUtility::quoteJSvalue($fileName) . ',\'\'); return false;';
1459 $thumbData .= '<a href="#" onclick="' . htmlspecialchars($onClick) . '">' . $fileIcon . '</a> ';
1460 } else {
1461 $thumbData .= $fileIcon;
1462 }
1463 }
1464 }
1465 }
1466 }
1467 return $thumbData;
1468 }
1469
1470 /**
1471 * Returns title-attribute information for a page-record informing about id, alias, doktype, hidden, starttime, endtime, fe_group etc.
1472 *
1473 * @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)
1474 * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4)
1475 * @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
1476 * @return string
1477 */
1478 public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true)
1479 {
1480 $lang = static::getLanguageService();
1481 $parts = [];
1482 $parts[] = 'id=' . $row['uid'];
1483 if ($row['alias']) {
1484 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['alias']['label']) . ' ' . $row['alias'];
1485 }
1486 if ($row['pid'] < 0) {
1487 $parts[] = 'v#1.' . $row['t3ver_id'];
1488 }
1489 switch (VersionState::cast($row['t3ver_state'])) {
1490 case new VersionState(VersionState::NEW_PLACEHOLDER):
1491 $parts[] = 'PLH WSID#' . $row['t3ver_wsid'];
1492 break;
1493 case new VersionState(VersionState::DELETE_PLACEHOLDER):
1494 $parts[] = 'Deleted element!';
1495 break;
1496 case new VersionState(VersionState::MOVE_PLACEHOLDER):
1497 $parts[] = 'NEW LOCATION (PLH) WSID#' . $row['t3ver_wsid'];
1498 break;
1499 case new VersionState(VersionState::MOVE_POINTER):
1500 $parts[] = 'OLD LOCATION (PNT) WSID#' . $row['t3ver_wsid'];
1501 break;
1502 case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1503 $parts[] = 'New element!';
1504 break;
1505 }
1506 if ($row['doktype'] == PageRepository::DOKTYPE_LINK) {
1507 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url'];
1508 } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) {
1509 if ($perms_clause) {
1510 $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20);
1511 } else {
1512 $row['shortcut'] = (int)$row['shortcut'];
1513 $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title');
1514 $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')';
1515 }
1516 if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) {
1517 $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' '
1518 . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode']));
1519 }
1520 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . $label;
1521 } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) {
1522 if ($perms_clause) {
1523 $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20);
1524 } else {
1525 $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title');
1526 $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')';
1527 }
1528 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label;
1529 if ($row['mount_pid_ol']) {
1530 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']);
1531 }
1532 }
1533 if ($row['nav_hide']) {
1534 $parts[] = rtrim($lang->sL($GLOBALS['TCA']['pages']['columns']['nav_hide']['label']), ':');
1535 }
1536 if ($row['hidden']) {
1537 $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden');
1538 }
1539 if ($row['starttime']) {
1540 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label'])
1541 . ' ' . self::dateTimeAge($row['starttime'], -1, 'date');
1542 }
1543 if ($row['endtime']) {
1544 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' '
1545 . self::dateTimeAge($row['endtime'], -1, 'date');
1546 }
1547 if ($row['fe_group']) {
1548 $fe_groups = [];
1549 foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) {
1550 if ($fe_group < 0) {
1551 $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', $fe_group));
1552 } else {
1553 $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title');
1554 $fe_groups[] = $lRec['title'];
1555 }
1556 }
1557 $label = implode(', ', $fe_groups);
1558 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label;
1559 }
1560 $out = htmlspecialchars(implode(' - ', $parts));
1561 return $includeAttrib ? 'title="' . $out . '"' : $out;
1562 }
1563
1564 /**
1565 * Returns the combined markup for Bootstraps tooltips
1566 *
1567 * @param array $row
1568 * @param string $table
1569 * @return string
1570 */
1571 public static function getRecordToolTip(array $row, $table = 'pages')
1572 {
1573 $toolTipText = self::getRecordIconAltText($row, $table);
1574 $toolTipCode = 'data-toggle="tooltip" data-title=" '
1575 . str_replace(' - ', '<br>', $toolTipText)
1576 . '" data-html="true" data-placement="right"';
1577 return $toolTipCode;
1578 }
1579
1580 /**
1581 * Returns title-attribute information for ANY record (from a table defined in TCA of course)
1582 * 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.
1583 * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page.
1584 *
1585 * @param array $row Table row; $row is a row from the table, $table
1586 * @param string $table Table name
1587 * @return string
1588 */
1589 public static function getRecordIconAltText($row, $table = 'pages')
1590 {
1591 if ($table === 'pages') {
1592 $out = self::titleAttribForPages($row, '', 0);
1593 } else {
1594 $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'])) ? $row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] . ' ' : '';
1595 $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns'];
1596 // Uid is added
1597 $out .= 'id=' . $row['uid'];
1598 if ($table === 'pages' && $row['alias']) {
1599 $out .= ' / ' . $row['alias'];
1600 }
1601 if (static::isTableWorkspaceEnabled($table) && $row['pid'] < 0) {
1602 $out .= ' - v#1.' . $row['t3ver_id'];
1603 }
1604 if (static::isTableWorkspaceEnabled($table)) {
1605 switch (VersionState::cast($row['t3ver_state'])) {
1606 case new VersionState(VersionState::NEW_PLACEHOLDER):
1607 $out .= ' - PLH WSID#' . $row['t3ver_wsid'];
1608 break;
1609 case new VersionState(VersionState::DELETE_PLACEHOLDER):
1610 $out .= ' - Deleted element!';
1611 break;
1612 case new VersionState(VersionState::MOVE_PLACEHOLDER):
1613 $out .= ' - NEW LOCATION (PLH) WSID#' . $row['t3ver_wsid'];
1614 break;
1615 case new VersionState(VersionState::MOVE_POINTER):
1616 $out .= ' - OLD LOCATION (PNT) WSID#' . $row['t3ver_wsid'];
1617 break;
1618 case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION):
1619 $out .= ' - New element!';
1620 break;
1621 }
1622 }
1623 // Hidden
1624 $lang = static::getLanguageService();
1625 if ($ctrl['disabled']) {
1626 $out .= $row[$ctrl['disabled']] ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : '';
1627 }
1628 if ($ctrl['starttime']) {
1629 if ($row[$ctrl['starttime']] > $GLOBALS['EXEC_TIME']) {
1630 $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.starttime') . ':' . self::date($row[$ctrl['starttime']]) . ' (' . self::daysUntil($row[$ctrl['starttime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1631 }
1632 }
1633 if ($row[$ctrl['endtime']]) {
1634 $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.endtime') . ': ' . self::date($row[$ctrl['endtime']]) . ' (' . self::daysUntil($row[$ctrl['endtime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')';
1635 }
1636 }
1637 return htmlspecialchars($out);
1638 }
1639
1640 /**
1641 * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key
1642 *
1643 * @param string $table Table name, present in $GLOBALS['TCA']
1644 * @param string $col Field name, present in $GLOBALS['TCA']
1645 * @param string $key items-array value to match
1646 * @return string Label for item entry
1647 */
1648 public static function getLabelFromItemlist($table, $col, $key)
1649 {
1650 // Check, if there is an "items" array:
1651 if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) {
1652 // Traverse the items-array...
1653 foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) {
1654 // ... and return the first found label where the value was equal to $key
1655 if ((string)$v[1] === (string)$key) {
1656 return $v[0];
1657 }
1658 }
1659 }
1660 return '';
1661 }
1662
1663 /**
1664 * Return the label of a field by additionally checking TsConfig values
1665 *
1666 * @param int $pageId Page id
1667 * @param string $table Table name
1668 * @param string $column Field Name
1669 * @param string $key item value
1670 * @return string Label for item entry
1671 */
1672 public static function getLabelFromItemListMerged($pageId, $table, $column, $key)
1673 {
1674 $pageTsConfig = static::getPagesTSconfig($pageId);
1675 $label = '';
1676 if (isset($pageTsConfig['TCEFORM.'])
1677 && \is_array($pageTsConfig['TCEFORM.'])
1678 && \is_array($pageTsConfig['TCEFORM.'][$table . '.'])
1679 && \is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.'])
1680 ) {
1681 if (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'])
1682 && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key])
1683 ) {
1684 $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key];
1685 } elseif (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'])
1686 && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key])
1687 ) {
1688 $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key];
1689 }
1690 }
1691 if (empty($label)) {
1692 $tcaValue = self::getLabelFromItemlist($table, $column, $key);
1693 if (!empty($tcaValue)) {
1694 $label = $tcaValue;
1695 }
1696 }
1697 return $label;
1698 }
1699
1700 /**
1701 * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma.
1702 * NOTE: this does not take itemsProcFunc into account
1703 *
1704 * @param string $table Table name, present in TCA
1705 * @param string $column Field name
1706 * @param string $keyList Key or comma-separated list of keys.
1707 * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>)
1708 * @return string Comma-separated list of localized labels
1709 */
1710 public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = [])
1711 {
1712 // Check if there is an "items" array
1713 if (
1714 !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1715 || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'])
1716 || $keyList === ''
1717 ) {
1718 return '';
1719 }
1720
1721 $keys = GeneralUtility::trimExplode(',', $keyList, true);
1722 $labels = [];
1723 // Loop on all selected values
1724 foreach ($keys as $key) {
1725 $label = null;
1726 if ($columnTsConfig) {
1727 // Check if label has been defined or redefined via pageTsConfig
1728 if (isset($columnTsConfig['addItems.'][$key])) {
1729 $label = $columnTsConfig['addItems.'][$key];
1730 } elseif (isset($columnTsConfig['altLabels.'][$key])) {
1731 $label = $columnTsConfig['altLabels.'][$key];
1732 }
1733 }
1734 if ($label === null) {
1735 // Otherwise lookup the label in TCA items list
1736 foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) {
1737 list($currentLabel, $currentKey) = $itemConfiguration;
1738 if ((string)$key === (string)$currentKey) {
1739 $label = $currentLabel;
1740 break;
1741 }
1742 }
1743 }
1744 if ($label !== null) {
1745 $labels[] = static::getLanguageService()->sL($label);
1746 }
1747 }
1748 return implode(', ', $labels);
1749 }
1750
1751 /**
1752 * Returns the label-value for fieldname $col in table, $table
1753 * 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>'
1754 *
1755 * @param string $table Table name, present in $GLOBALS['TCA']
1756 * @param string $col Field name
1757 * @return string or NULL if $col is not found in the TCA table
1758 */
1759 public static function getItemLabel($table, $col)
1760 {
1761 // Check if column exists
1762 if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1763 return $GLOBALS['TCA'][$table]['columns'][$col]['label'];
1764 }
1765
1766 return null;
1767 }
1768
1769 /**
1770 * Returns the "title"-value in record, $row, from table, $table
1771 * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force'
1772 *
1773 * @param string $table Table name, present in TCA
1774 * @param array $row Row from table
1775 * @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
1776 * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized).
1777 * @return string
1778 */
1779 public static function getRecordTitle($table, $row, $prep = false, $forceResult = true)
1780 {
1781 $recordTitle = '';
1782 if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table])) {
1783 // If configured, call userFunc
1784 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) {
1785 $params['table'] = $table;
1786 $params['row'] = $row;
1787 $params['title'] = '';
1788 $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? [];
1789
1790 // Create NULL-reference
1791 $null = null;
1792 GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null);
1793 $recordTitle = $params['title'];
1794 } else {
1795 // No userFunc: Build label
1796 $recordTitle = self::getProcessedValue(
1797 $table,
1798 $GLOBALS['TCA'][$table]['ctrl']['label'],
1799 $row[$GLOBALS['TCA'][$table]['ctrl']['label']],
1800 0,
1801 0,
1802 false,
1803 $row['uid'],
1804 $forceResult
1805 );
1806 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])
1807 && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || (string)$recordTitle === '')
1808 ) {
1809 $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
1810 $tA = [];
1811 if (!empty($recordTitle)) {
1812 $tA[] = $recordTitle;
1813 }
1814 foreach ($altFields as $fN) {
1815 $recordTitle = trim(strip_tags($row[$fN]));
1816 if ((string)$recordTitle !== '') {
1817 $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, 0, false, $row['uid']);
1818 if (!$GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1819 break;
1820 }
1821 $tA[] = $recordTitle;
1822 }
1823 }
1824 if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) {
1825 $recordTitle = implode(', ', $tA);
1826 }
1827 }
1828 }
1829 // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested
1830 if ($prep || $forceResult) {
1831 if ($prep) {
1832 $recordTitle = self::getRecordTitlePrep($recordTitle);
1833 }
1834 if (trim($recordTitle) === '') {
1835 $recordTitle = self::getNoRecordTitle($prep);
1836 }
1837 }
1838 }
1839
1840 return $recordTitle;
1841 }
1842
1843 /**
1844 * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>,
1845 * which offers a tooltip with the original title when moving mouse over it.
1846 *
1847 * @param string $title The title string to be cropped
1848 * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used
1849 * @return string The processed title string, wrapped in <span title="...">|</span> if cropped
1850 */
1851 public static function getRecordTitlePrep($title, $titleLength = 0)
1852 {
1853 // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']:
1854 if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) {
1855 $titleLength = static::getBackendUserAuthentication()->uc['titleLen'];
1856 }
1857 $titleOrig = htmlspecialchars($title);
1858 $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength));
1859 // If title was cropped, offer a tooltip:
1860 if ($titleOrig != $title) {
1861 $title = '<span title="' . $titleOrig . '">' . $title . '</span>';
1862 }
1863 return $title;
1864 }
1865
1866 /**
1867 * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE.
1868 *
1869 * @param bool $prep Wrap result in <em>|</em>
1870 * @return string Localized [No title] string
1871 */
1872 public static function getNoRecordTitle($prep = false)
1873 {
1874 $noTitle = '[' .
1875 htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title'))
1876 . ']';
1877 if ($prep) {
1878 $noTitle = '<em>' . $noTitle . '</em>';
1879 }
1880 return $noTitle;
1881 }
1882
1883 /**
1884 * Returns a human readable output of a value from a record
1885 * 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.
1886 * $table/$col is tablename and fieldname
1887 * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant)
1888 *
1889 * @param string $table Table name, present in TCA
1890 * @param string $col Field name, present in TCA
1891 * @param string $value The value of that field from a selected record
1892 * @param int $fixed_lgd_chars The max amount of characters the value may occupy
1893 * @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")
1894 * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown.
1895 * @param int $uid Uid of the current record
1896 * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
1897 * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
1898 * @throws \InvalidArgumentException
1899 * @return string|null
1900 */
1901 public static function getProcessedValue(
1902 $table,
1903 $col,
1904 $value,
1905 $fixed_lgd_chars = 0,
1906 $defaultPassthrough = false,
1907 $noRecordLookup = false,
1908 $uid = 0,
1909 $forceResult = true,
1910 $pid = 0
1911 ) {
1912 if ($col === 'uid') {
1913 // uid is not in TCA-array
1914 return $value;
1915 }
1916 // Check if table and field is configured
1917 if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) {
1918 return null;
1919 }
1920 // Depending on the fields configuration, make a meaningful output value.
1921 $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? [];
1922 /*****************
1923 *HOOK: pre-processing the human readable output from a record
1924 ****************/
1925 $null = null;
1926 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) {
1927 GeneralUtility::callUserFunction($_funcRef, $theColConf, $null);
1928 }
1929
1930 $l = '';
1931 $lang = static::getLanguageService();
1932 switch ((string)($theColConf['type'] ?? '')) {
1933 case 'radio':
1934 $l = self::getLabelFromItemlist($table, $col, $value);
1935 $l = $lang->sL($l);
1936 break;
1937 case 'inline':
1938 case 'select':
1939 if (!empty($theColConf['MM'])) {
1940 if ($uid) {
1941 // Display the title of MM related records in lists
1942 if ($noRecordLookup) {
1943 $MMfields = [];
1944 $MMfields[] = $theColConf['foreign_table'] . '.uid';
1945 } else {
1946 $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
1947 if (isset($GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'])) {
1948 foreach (GeneralUtility::trimExplode(
1949 ',',
1950 $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'],
1951 true
1952 ) as $f) {
1953 $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
1954 }
1955 }
1956 }
1957 /** @var $dbGroup RelationHandler */
1958 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
1959 $dbGroup->start(
1960 $value,
1961 $theColConf['foreign_table'],
1962 $theColConf['MM'],
1963 $uid,
1964 $table,
1965 $theColConf
1966 );
1967 $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
1968 if (is_array($selectUids) && !empty($selectUids)) {
1969 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
1970 $queryBuilder->getRestrictions()
1971 ->removeAll()
1972 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1973
1974 $result = $queryBuilder
1975 ->select('uid', ...$MMfields)
1976 ->from($theColConf['foreign_table'])
1977 ->where(
1978 $queryBuilder->expr()->in(
1979 'uid',
1980 $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY)
1981 )
1982 )
1983 ->execute();
1984
1985 $mmlA = [];
1986 while ($MMrow = $result->fetch()) {
1987 // Keep sorting of $selectUids
1988 $selectedUid = array_search($MMrow['uid'], $selectUids);
1989 $mmlA[$selectedUid] = $MMrow['uid'];
1990 if (!$noRecordLookup) {
1991 $mmlA[$selectedUid] = static::getRecordTitle(
1992 $theColConf['foreign_table'],
1993 $MMrow,
1994 false,
1995 $forceResult
1996 );
1997 }
1998 }
1999
2000 if (!empty($mmlA)) {
2001 ksort($mmlA);
2002 $l = implode('; ', $mmlA);
2003 } else {
2004 $l = 'N/A';
2005 }
2006 } else {
2007 $l = 'N/A';
2008 }
2009 } else {
2010 $l = 'N/A';
2011 }
2012 } else {
2013 $columnTsConfig = [];
2014 if ($pid) {
2015 $pageTsConfig = self::getPagesTSconfig($pid);
2016 if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) {
2017 $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'];
2018 }
2019 }
2020 $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig);
2021 if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) {
2022 if ($noRecordLookup) {
2023 $l = $value;
2024 } else {
2025 $rParts = [];
2026 if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') {
2027 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
2028 $queryBuilder->getRestrictions()
2029 ->removeAll()
2030 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
2031 ->add(GeneralUtility::makeInstance(BackendWorkspaceRestriction::class));
2032 $constraints = [
2033 $queryBuilder->expr()->eq(
2034 $theColConf['foreign_field'],
2035 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)
2036 )
2037 ];
2038
2039 if (!empty($theColConf['foreign_table_field'])) {
2040 $constraints[] = $queryBuilder->expr()->eq(
2041 $theColConf['foreign_table_field'],
2042 $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)
2043 );
2044 }
2045
2046 // Add additional where clause if foreign_match_fields are defined
2047 $foreignMatchFields = [];
2048 if (is_array($theColConf['foreign_match_fields'])) {
2049 $foreignMatchFields = $theColConf['foreign_match_fields'];
2050 }
2051
2052 foreach ($foreignMatchFields as $matchField => $matchValue) {
2053 $constraints[] = $queryBuilder->expr()->eq(
2054 $matchField,
2055 $queryBuilder->createNamedParameter($matchValue)
2056 );
2057 }
2058
2059 $result = $queryBuilder
2060 ->select('*')
2061 ->from($theColConf['foreign_table'])
2062 ->where(...$constraints)
2063 ->execute();
2064
2065 while ($record = $result->fetch()) {
2066 $rParts[] = $record['uid'];
2067 }
2068 }
2069 if (empty($rParts)) {
2070 $rParts = GeneralUtility::trimExplode(',', $value, true);
2071 }
2072 $lA = [];
2073 foreach ($rParts as $rVal) {
2074 $rVal = (int)$rVal;
2075 $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal);
2076 if (is_array($r)) {
2077 $lA[] = $lang->sL($theColConf['foreign_table_prefix'])
2078 . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult);
2079 } else {
2080 $lA[] = $rVal ? '[' . $rVal . '!]' : '';
2081 }
2082 }
2083 $l = implode(', ', $lA);
2084 }
2085 }
2086 if (empty($l) && !empty($value)) {
2087 // Use plain database value when label is empty
2088 $l = $value;
2089 }
2090 }
2091 break;
2092 case 'group':
2093 // resolve the titles for DB records
2094 if (isset($theColConf['internal_type']) && $theColConf['internal_type'] === 'db') {
2095 if (isset($theColConf['MM']) && $theColConf['MM']) {
2096 if ($uid) {
2097 // Display the title of MM related records in lists
2098 if ($noRecordLookup) {
2099 $MMfields = [];
2100 $MMfields[] = $theColConf['foreign_table'] . '.uid';
2101 } else {
2102 $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']];
2103 $altLabelFields = explode(
2104 ',',
2105 $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt']
2106 );
2107 foreach ($altLabelFields as $f) {
2108 $f = trim($f);
2109 if ($f !== '') {
2110 $MMfields[] = $theColConf['foreign_table'] . '.' . $f;
2111 }
2112 }
2113 }
2114 /** @var $dbGroup RelationHandler */
2115 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class);
2116 $dbGroup->start(
2117 $value,
2118 $theColConf['foreign_table'],
2119 $theColConf['MM'],
2120 $uid,
2121 $table,
2122 $theColConf
2123 );
2124 $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']];
2125 if (!empty($selectUids) && is_array($selectUids)) {
2126 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']);
2127 $queryBuilder->getRestrictions()
2128 ->removeAll()
2129 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
2130
2131 $result = $queryBuilder
2132 ->select('uid', ...$MMfields)
2133 ->from($theColConf['foreign_table'])
2134 ->where(
2135 $queryBuilder->expr()->in(
2136 'uid',
2137 $queryBuilder->createNamedParameter(
2138 $selectUids,
2139 Connection::PARAM_INT_ARRAY
2140 )
2141 )
2142 )
2143 ->execute();
2144
2145 $mmlA = [];
2146 while ($MMrow = $result->fetch()) {
2147 // Keep sorting of $selectUids
2148 $selectedUid = array_search($MMrow['uid'], $selectUids);
2149 $mmlA[$selectedUid] = $MMrow['uid'];
2150 if (!$noRecordLookup) {
2151 $mmlA[$selectedUid] = static::getRecordTitle(
2152 $theColConf['foreign_table'],
2153 $MMrow,
2154 false,
2155 $forceResult
2156 );
2157 }
2158 }
2159
2160 if (!empty($mmlA)) {
2161 ksort($mmlA);
2162 $l = implode('; ', $mmlA);
2163 } else {
2164 $l = 'N/A';
2165 }
2166 } else {
2167 $l = 'N/A';
2168 }
2169 } else {
2170 $l = 'N/A';
2171 }
2172 } else {
2173 $finalValues = [];
2174 $relationTableName = $theColConf['allowed'];
2175 $explodedValues = GeneralUtility::trimExplode(',', $value, true);
2176
2177 foreach ($explodedValues as $explodedValue) {
2178 if (MathUtility::canBeInterpretedAsInteger($explodedValue)) {
2179 $relationTableNameForField = $relationTableName;
2180 } else {
2181 list($relationTableNameForField, $explodedValue) = self::splitTable_Uid($explodedValue);
2182 }
2183
2184 $relationRecord = static::getRecordWSOL($relationTableNameForField, $explodedValue);
2185 $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord);
2186 }
2187 $l = implode(', ', $finalValues);
2188 }
2189 } else {
2190 $l = implode(', ', GeneralUtility::trimExplode(',', $value, true));
2191 }
2192 break;
2193 case 'check':
2194 if (!is_array($theColConf['items']) || count($theColConf['items']) === 1) {
2195 $l = $value ? $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
2196 } else {
2197 $lA = [];
2198 foreach ($theColConf['items'] as $key => $val) {
2199 if ($value & pow(2, $key)) {
2200 $lA[] = $lang->sL($val[0]);
2201 }
2202 }
2203 $l = implode(', ', $lA);
2204 }
2205 break;
2206 case 'input':
2207 // Hide value 0 for dates, but show it for everything else
2208 if (isset($value)) {
2209 $dateTimeFormats = QueryHelper::getDateTimeFormats();
2210
2211 if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) {
2212 // Handle native date field
2213 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') {
2214 $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value);
2215 } else {
2216 $value = (int)$value;
2217 }
2218 if (!empty($value)) {
2219 $ageSuffix = '';
2220 $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config'];
2221 $ageDisplayKey = 'disableAgeDisplay';
2222
2223 // generate age suffix as long as not explicitly suppressed
2224 if (!isset($dateColumnConfiguration[$ageDisplayKey])
2225 // non typesafe comparison on intention
2226 || $dateColumnConfiguration[$ageDisplayKey] == false
2227 ) {
2228 $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '')
2229 . self::calcAge(
2230 abs($GLOBALS['EXEC_TIME'] - $value),
2231 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears')
2232 )
2233 . ')';
2234 }
2235
2236 $l = self::date($value) . $ageSuffix;
2237 }
2238 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) {
2239 // Handle native time field
2240 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2241 $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2242 } else {
2243 $value = (int)$value;
2244 }
2245 if (!empty($value)) {
2246 $l = gmdate('H:i', (int)$value);
2247 }
2248 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) {
2249 // Handle native time field
2250 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') {
2251 $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value);
2252 } else {
2253 $value = (int)$value;
2254 }
2255 if (!empty($value)) {
2256 $l = gmdate('H:i:s', (int)$value);
2257 }
2258 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) {
2259 // Handle native datetime field
2260 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') {
2261 $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value);
2262 } else {
2263 $value = (int)$value;
2264 }
2265 if (!empty($value)) {
2266 $l = self::datetime($value);
2267 }
2268 } else {
2269 $l = $value;
2270 }
2271 }
2272 break;
2273 case 'flex':
2274 $l = strip_tags($value);
2275 break;
2276 default:
2277 if ($defaultPassthrough) {
2278 $l = $value;
2279 } elseif (isset($theColConf['MM'])) {
2280 $l = 'N/A';
2281 } elseif ($value) {
2282 $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200);
2283 }
2284 }
2285 // If this field is a password field, then hide the password by changing it to a random number of asterisk (*)
2286 if (!empty($theColConf['eval']) && stristr($theColConf['eval'], 'password')) {
2287 $l = '';
2288 $randomNumber = rand(5, 12);
2289 for ($i = 0; $i < $randomNumber; $i++) {
2290 $l .= '*';
2291 }
2292 }
2293 /*****************
2294 *HOOK: post-processing the human readable output from a record
2295 ****************/
2296 $null = null;
2297 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) {
2298 $params = [
2299 'value' => $l,
2300 'colConf' => $theColConf
2301 ];
2302 $l = GeneralUtility::callUserFunction($_funcRef, $params, $null);
2303 }
2304 if ($fixed_lgd_chars) {
2305 return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars);
2306 }
2307 return $l;
2308 }
2309
2310 /**
2311 * 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.
2312 *
2313 * @param string $table Table name, present in TCA
2314 * @param string $fN Field name
2315 * @param string $fV Field value
2316 * @param int $fixed_lgd_chars The max amount of characters the value may occupy
2317 * @param int $uid Uid of the current record
2318 * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded.
2319 * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field
2320 * @return string
2321 * @see getProcessedValue()
2322 */
2323 public static function getProcessedValueExtra(
2324 $table,
2325 $fN,
2326 $fV,
2327 $fixed_lgd_chars = 0,
2328 $uid = 0,
2329 $forceResult = true,
2330 $pid = 0
2331 ) {
2332 $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, 1, 0, $uid, $forceResult, $pid);
2333 if (!isset($fVnew)) {
2334 if (is_array($GLOBALS['TCA'][$table])) {
2335 if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) {
2336 $fVnew = self::datetime($fV);
2337 } elseif ($fN === 'pid') {
2338 // Fetches the path with no regard to the users permissions to select pages.
2339 $fVnew = self::getRecordPath($fV, '1=1', 20);
2340 } else {
2341 $fVnew = $fV;
2342 }
2343 }
2344 }
2345 return $fVnew;
2346 }
2347
2348 /**
2349 * Returns fields for a table, $table, which would typically be interesting to select
2350 * This includes uid, the fields defined for title, icon-field.
2351 * 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)
2352 *
2353 * @param string $table Table name, present in $GLOBALS['TCA']
2354 * @param string $prefix Table prefix
2355 * @param array $fields Preset fields (must include prefix if that is used)
2356 * @return string List of fields.
2357 */
2358 public static function getCommonSelectFields($table, $prefix = '', $fields = [])
2359 {
2360 $fields[] = $prefix . 'uid';
2361 if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') {
2362 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label'];
2363 }
2364 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) {
2365 $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true);
2366 foreach ($secondFields as $fieldN) {
2367 $fields[] = $prefix . $fieldN;
2368 }
2369 }
2370 if (static::isTableWorkspaceEnabled($table)) {
2371 $fields[] = $prefix . 't3ver_id';
2372 $fields[] = $prefix . 't3ver_state';
2373 $fields[] = $prefix . 't3ver_wsid';
2374 $fields[] = $prefix . 't3ver_count';
2375 }
2376 if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) {
2377 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field'];
2378 }
2379 if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) {
2380 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column'];
2381 }
2382 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) {
2383 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'];
2384 }
2385 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) {
2386 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'];
2387 }
2388 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) {
2389 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'];
2390 }
2391 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) {
2392 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'];
2393 }
2394 return implode(',', array_unique($fields));
2395 }
2396
2397 /*******************************************
2398 *
2399 * Backend Modules API functions
2400 *
2401 *******************************************/
2402
2403 /**
2404 * Returns CSH help text (description), if configured for, as an array (title, description)
2405 *
2406 * @param string $table Table name
2407 * @param string $field Field name
2408 * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo'
2409 */
2410 public static function helpTextArray($table, $field)
2411 {
2412 if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) {
2413 static::getLanguageService()->loadSingleTableDescription($table);
2414 }
2415 $output = [
2416 'description' => null,
2417 'title' => null,
2418 'moreInfo' => false
2419 ];
2420 if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) {
2421 $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field];
2422 // Add alternative title, if defined
2423 if ($data['alttitle']) {
2424 $output['title'] = $data['alttitle'];
2425 }
2426 // If we have more information to show and access to the cshmanual
2427 if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax'])
2428 && static::getBackendUserAuthentication()->check('modules', 'help_CshmanualCshmanual')
2429 ) {
2430 $output['moreInfo'] = true;
2431 }
2432 // Add description
2433 if ($data['description']) {
2434 $output['description'] = $data['description'];
2435 }
2436 }
2437 return $output;
2438 }
2439
2440 /**
2441 * Returns CSH help text
2442 *
2443 * @param string $table Table name
2444 * @param string $field Field name
2445 * @return string HTML content for help text
2446 * @see cshItem()
2447 */
2448 public static function helpText($table, $field)
2449 {
2450 $helpTextArray = self::helpTextArray($table, $field);
2451 $output = '';
2452 $arrow = '';
2453 // Put header before the rest of the text
2454 if ($helpTextArray['title'] !== null) {
2455 $output .= '<h2>' . $helpTextArray['title'] . '</h2>';
2456 }
2457 // Add see also arrow if we have more info
2458 if ($helpTextArray['moreInfo']) {
2459 /** @var IconFactory $iconFactory */
2460 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2461 $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render();
2462 }
2463 // Wrap description and arrow in p tag
2464 if ($helpTextArray['description'] !== null || $arrow) {
2465 $output .= '<p class="t3-help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>';
2466 }
2467 return $output;
2468 }
2469
2470 /**
2471 * API function that wraps the text / html in help text, so if a user hovers over it
2472 * the help text will show up
2473 *
2474 * @param string $table The table name for which the help should be shown
2475 * @param string $field The field name for which the help should be shown
2476 * @param string $text The text which should be wrapped with the help text
2477 * @param array $overloadHelpText Array with text to overload help text
2478 * @return string the HTML code ready to render
2479 */
2480 public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = [])
2481 {
2482 // Initialize some variables
2483 $helpText = '';
2484 $abbrClassAdd = '';
2485 $hasHelpTextOverload = !empty($overloadHelpText);
2486 // Get the help text that should be shown on hover
2487 if (!$hasHelpTextOverload) {
2488 $helpText = self::helpText($table, $field);
2489 }
2490 // If there's a help text or some overload information, proceed with preparing an output
2491 if ((!empty($helpText) || $hasHelpTextOverload)) {
2492 // If no text was given, just use the regular help icon
2493 if ($text == '') {
2494 $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
2495 $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render();
2496 $abbrClassAdd = '-icon';
2497 }
2498 $text = '<abbr class="t3-help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>';
2499 $wrappedText = '<span class="t3-help-link" href="#" data-table="' . $table . '" data-field="' . $field . '"';
2500 // The overload array may provide a title and a description
2501 // If either one is defined, add them to the "data" attributes
2502 if ($hasHelpTextOverload) {
2503 if (isset($overloadHelpText['title'])) {
2504 $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"';
2505 }
2506 if (isset($overloadHelpText['description'])) {
2507 $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"';
2508 }
2509 }
2510 $wrappedText .= '>' . $text . '</span>';
2511 return $wrappedText;
2512 }
2513 return $text;
2514 }
2515
2516 /**
2517 * API for getting CSH icons/text for use in backend modules.
2518 * TCA_DESCR will be loaded if it isn't already
2519 *
2520 * @param string $table Table name ('_MOD_'+module name)
2521 * @param string $field Field name (CSH locallang main key)
2522 * @param string $_ (unused)
2523 * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode.
2524 * @return string HTML content for help text
2525 */
2526 public static function cshItem($table, $field, $_ = '', $wrap = '')
2527 {
2528 static::getLanguageService()->loadSingleTableDescription($table);
2529 if (is_array($GLOBALS['TCA_DESCR'][$table])
2530 && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])
2531 ) {
2532 // Creating short description
2533 $output = self::wrapInHelp($table, $field);
2534 if ($output && $wrap) {
2535 $wrParts = explode('|', $wrap);
2536 $output = $wrParts[0] . $output . $wrParts[1];
2537 }
2538 return $output;
2539 }
2540 return '';
2541 }
2542
2543 /**
2544 * 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.
2545 * REMEMBER to always htmlspecialchar() content in href-properties to ampersands get converted to entities (XHTML requirement and XSS precaution)
2546 *
2547 * @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.
2548 * @param string $_ (unused)
2549 * @param string $requestUri An optional returnUrl you can set - automatically set to REQUEST_URI.
2550 *
2551 * @return string
2552 */
2553 public static function editOnClick($params, $_ = '', $requestUri = '')
2554 {
2555 if ($requestUri == -1) {
2556 $returnUrl = 'T3_THIS_LOCATION';
2557 } else {
2558 $returnUrl = GeneralUtility::quoteJSvalue(rawurlencode($requestUri ?: GeneralUtility::getIndpEnv('REQUEST_URI')));
2559 }
2560 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2561 return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('record_edit') . $params . '&returnUrl=') . '+' . $returnUrl . '; return false;';
2562 }
2563
2564 /**
2565 * Returns a JavaScript string for viewing the page id, $id
2566 * It will re-use any window already open.
2567 *
2568 * @param int $pageUid Page UID
2569 * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2570 * @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)
2571 * @param string $anchorSection Optional anchor to the URL
2572 * @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!
2573 * @param string $additionalGetVars Additional GET variables.
2574 * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2575 * @return string
2576 */
2577 public static function viewOnClick(
2578 $pageUid,
2579 $backPath = '',
2580 $rootLine = null,
2581 $anchorSection = '',
2582 $alternativeUrl = '',
2583 $additionalGetVars = '',
2584 $switchFocus = true
2585 ) {
2586 $previewUrl = self::getPreviewUrl(
2587 $pageUid,
2588 $backPath,
2589 $rootLine,
2590 $anchorSection,
2591 $alternativeUrl,
2592 $additionalGetVars,
2593 $switchFocus
2594 );
2595
2596 $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');'
2597 . ($switchFocus ? 'previewWin.focus();' : '') . LF
2598 . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };';
2599
2600 return $onclickCode;
2601 }
2602
2603 /**
2604 * Returns the preview url
2605 *
2606 * It will detect the correct domain name if needed and provide the link with the right back path.
2607 *
2608 * @param int $pageUid Page UID
2609 * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above)
2610 * @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)
2611 * @param string $anchorSection Optional anchor to the URL
2612 * @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!
2613 * @param string $additionalGetVars Additional GET variables.
2614 * @param bool $switchFocus If TRUE, then the preview window will gain the focus.
2615 * @return string
2616 */
2617 public static function getPreviewUrl(
2618 $pageUid,
2619 $backPath = '',
2620 $rootLine = null,
2621 $anchorSection = '',
2622 $alternativeUrl = '',
2623 $additionalGetVars = '',
2624 &$switchFocus = true
2625 ): string {
2626 $viewScript = '/index.php?id=';
2627 if ($alternativeUrl) {
2628 $viewScript = $alternativeUrl;
2629 }
2630
2631 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2632 $hookObj = GeneralUtility::makeInstance($className);
2633 if (method_exists($hookObj, 'preProcess')) {
2634 $hookObj->preProcess(
2635 $pageUid,
2636 $backPath,
2637 $rootLine,
2638 $anchorSection,
2639 $viewScript,
2640 $additionalGetVars,
2641 $switchFocus
2642 );
2643 }
2644 }
2645
2646 if ($alternativeUrl) {
2647 $previewUrl = $viewScript;
2648 } else {
2649 $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW);
2650 $pageInfo = self::readPageAccess($pageUid, $permissionClause);
2651 $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo);
2652
2653 // Build the URL with a site as prefix, if configured
2654 $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
2655 // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is
2656 $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid);
2657 try {
2658 $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine);
2659 // Create a multi-dimensional array out of the additional get vars
2660 $additionalQueryParams = [];
2661 parse_str($additionalGetVars, $additionalQueryParams);
2662 $uriBuilder = GeneralUtility::makeInstance(PageUriBuilder::class);
2663 $previewUrl = (string)$uriBuilder->buildUri($pageUid, $additionalQueryParams, $anchorSection, ['rootLine' => $rootLine], $uriBuilder::ABSOLUTE_URL);
2664 } catch (SiteNotFoundException $e) {
2665 $previewUrl = self::createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript);
2666 }
2667 }
2668
2669 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) {
2670 $hookObj = GeneralUtility::makeInstance($className);
2671 if (method_exists($hookObj, 'postProcess')) {
2672 $previewUrl = $hookObj->postProcess(
2673 $previewUrl,
2674 $pageUid,
2675 $rootLine,
2676 $anchorSection,
2677 $viewScript,
2678 $additionalGetVars,
2679 $switchFocus
2680 );
2681 }
2682 }
2683
2684 return $previewUrl;
2685 }
2686
2687 /**
2688 * Makes click menu link (context sensitive menu)
2689 *
2690 * Returns $str wrapped in a link which will activate the context sensitive
2691 * menu for the record ($table/$uid) or file ($table = file)
2692 * The link will load the top frame with the parameter "&item" which is the table, uid
2693 * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context)
2694 *
2695 * @param string $content String to be wrapped in link, typ. image tag.
2696 * @param string $table Table name/File path. If the icon is for a database
2697 * record, enter the tablename from $GLOBALS['TCA']. If a file then enter
2698 * the absolute filepath
2699 * @param int|string $uid If icon is for database record this is the UID for the
2700 * record from $table or identifier for sys_file record
2701 * @param string $context Set tree if menu is called from tree view
2702 * @param string $_addParams NOT IN USE
2703 * @param string $_enDisItems NOT IN USE
2704 * @param bool $returnTagParameters If set, will return only the onclick
2705 * JavaScript, not the whole link.
2706 *
2707 * @return string The link wrapped input string.
2708 */
2709 public static function wrapClickMenuOnIcon(
2710 $content,
2711 $table,
2712 $uid = 0,
2713 $context = '',
2714 $_addParams = '',
2715 $_enDisItems = '',
2716 $returnTagParameters = false
2717 ) {
2718 $tagParameters = [
2719 'class' => 't3js-contextmenutrigger',
2720 'data-table' => $table,
2721 'data-uid' => $uid,
2722 'data-context' => $context
2723 ];
2724
2725 if ($returnTagParameters) {
2726 return $tagParameters;
2727 }
2728 return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>';
2729 }
2730
2731 /**
2732 * Returns a URL with a command to TYPO3 Datahandler
2733 *
2734 * @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
2735 * @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
2736 * @return string URL to BackendUtility::getModuleUrl('tce_db') + parameters
2737 */
2738 public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '')
2739 {
2740 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
2741 $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect=';
2742 if ((int)$redirectUrl === -1) {
2743 $url = GeneralUtility::quoteJSvalue($url) . '+T3_THIS_LOCATION';
2744 } else {
2745 $url .= rawurlencode($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'));
2746 }
2747 return $url;
2748 }
2749
2750 /**
2751 * Creates the view-on-click preview URL without any alternative URL.
2752 *
2753 * @param int $pageUid Page UID
2754 * @param array $rootLine If rootline is supplied, the function will look for the first found domain record and use that URL instead
2755 * @param string $anchorSection Optional anchor to the URL
2756 * @param string $additionalGetVars Additional GET variables.
2757 * @param string $viewScript The path to the script used to view the page
2758 *
2759 * @return string The preview URL
2760 */
2761 protected static function createPreviewUrl($pageUid, $rootLine, $anchorSection, $additionalGetVars, $viewScript)
2762 {
2763 // Look if a fixed preview language should be added:
2764 $beUser = static::getBackendUserAuthentication();
2765 $viewLanguageOrder = (string)($beUser->getTSConfig()['options.']['view.']['languageOrder'] ?? '');
2766
2767 if (!empty($viewLanguageOrder)) {
2768 $suffix = '';
2769 // Find allowed languages (if none, all are allowed!)
2770 $allowedLanguages = null;
2771 if (!$beUser->isAdmin() && $beUser->groupData['allowed_languages'] !== '') {
2772 $allowedLanguages = array_flip(explode(',', $beUser->groupData['allowed_languages']));
2773 }
2774 // Traverse the view order, match first occurrence:
2775 $languageOrder = GeneralUtility::intExplode(',', $viewLanguageOrder);
2776 foreach ($languageOrder as $langUid) {
2777 if (is_array($allowedLanguages) && !empty($allowedLanguages)) {
2778 // Choose if set.
2779 if (isset($allowedLanguages[$langUid])) {
2780 $suffix = '&L=' . $langUid;
2781 break;
2782 }
2783 } else {
2784 // All allowed since no lang. are listed.
2785 $suffix = '&L=' . $langUid;
2786 break;
2787 }
2788 }
2789 // Add it
2790 $additionalGetVars .= $suffix;
2791 }
2792
2793 // Check a mount point needs to be previewed
2794 $pageRepository = GeneralUtility::makeInstance(PageRepository::class);
2795 $mountPointInfo = $pageRepository->getMountPointInfo($pageUid);
2796
2797 if ($mountPointInfo && $mountPointInfo['overlay']) {
2798 $pageUid = $mountPointInfo['mount_pid'];
2799 $additionalGetVars .= '&MP=' . $mountPointInfo['MPvar'];
2800 }
2801 $viewDomain = self::getViewDomain($pageUid, $rootLine);
2802
2803 return $viewDomain . $viewScript . $pageUid . $additionalGetVars . $anchorSection;
2804 }
2805
2806 /**
2807 * Builds the frontend view domain for a given page ID with a given root
2808 * line.
2809 *
2810 * @param int $pageId The page ID to use, must be > 0
2811 * @param array|null $rootLine The root line structure to use
2812 * @return string The full domain including the protocol http:// or https://, but without the trailing '/'
2813 */
2814 public static function getViewDomain($pageId, $rootLine = null)
2815 {
2816 $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/');
2817 if (!is_array($rootLine)) {
2818 $rootLine = self::BEgetRootLine($pageId);
2819 }
2820 // Checks alternate domains
2821 if (!empty($rootLine)) {
2822 $protocol = GeneralUtility::getIndpEnv('TYPO3_SSL') ? 'https' : 'http';
2823 $previewDomainConfig = self::getPagesTSconfig($pageId)['TCEMAIN.']['previewDomain'] ?? '';
2824 if (!empty($previewDomainConfig)) {
2825 if (strpos($previewDomainConfig, '://') !== false) {
2826 list($protocol, $domainName) = explode('://', $previewDomainConfig);
2827 } else {
2828 $domainName = $previewDomainConfig;
2829 }
2830 } else {
2831 $domainResolver = GeneralUtility::makeInstance(LegacyDomainResolver::class);
2832 foreach ($rootLine as $row) {
2833 $domainRecord = $domainResolver->matchRootPageId((int)$row['uid']);
2834 if (is_array($domainRecord)) {
2835 $domainName