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