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