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