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