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