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