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