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