1<?php 2 3/* 4 * This file is part of the TYPO3 CMS project. 5 * 6 * It is free software; you can redistribute it and/or modify it under 7 * the terms of the GNU General Public License, either version 2 8 * of the License, or any later version. 9 * 10 * For the full copyright and license information, please read the 11 * LICENSE.txt file that was distributed with this source code. 12 * 13 * The TYPO3 project - inspiring people to share! 14 */ 15 16namespace TYPO3\CMS\Backend\Utility; 17 18use Psr\EventDispatcher\EventDispatcherInterface; 19use Psr\Log\LoggerInterface; 20use TYPO3\CMS\Backend\Configuration\TypoScript\ConditionMatching\ConditionMatcher; 21use TYPO3\CMS\Backend\Routing\UriBuilder; 22use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 23use TYPO3\CMS\Core\Cache\CacheManager; 24use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface; 25use TYPO3\CMS\Core\Configuration\Event\ModifyLoadedPageTsConfigEvent; 26use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader; 27use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser; 28use TYPO3\CMS\Core\Context\Context; 29use TYPO3\CMS\Core\Context\DateTimeAspect; 30use TYPO3\CMS\Core\Core\Environment; 31use TYPO3\CMS\Core\Database\Connection; 32use TYPO3\CMS\Core\Database\ConnectionPool; 33use TYPO3\CMS\Core\Database\Query\QueryBuilder; 34use TYPO3\CMS\Core\Database\Query\QueryHelper; 35use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 36use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; 37use TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction; 38use TYPO3\CMS\Core\Database\RelationHandler; 39use TYPO3\CMS\Core\Domain\Repository\PageRepository; 40use TYPO3\CMS\Core\Exception\SiteNotFoundException; 41use TYPO3\CMS\Core\Http\Uri; 42use TYPO3\CMS\Core\Imaging\Icon; 43use TYPO3\CMS\Core\Imaging\IconFactory; 44use TYPO3\CMS\Core\Imaging\ImageDimension; 45use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection; 46use TYPO3\CMS\Core\Information\Typo3Information; 47use TYPO3\CMS\Core\Localization\LanguageService; 48use TYPO3\CMS\Core\Log\LogManager; 49use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; 50use TYPO3\CMS\Core\Resource\ProcessedFile; 51use TYPO3\CMS\Core\Resource\ResourceFactory; 52use TYPO3\CMS\Core\Routing\InvalidRouteArgumentsException; 53use TYPO3\CMS\Core\Routing\RouterInterface; 54use TYPO3\CMS\Core\Routing\UnableToLinkToPageException; 55use TYPO3\CMS\Core\Site\SiteFinder; 56use TYPO3\CMS\Core\Type\Bitmask\Permission; 57use TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser; 58use TYPO3\CMS\Core\Utility\ArrayUtility; 59use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 60use TYPO3\CMS\Core\Utility\GeneralUtility; 61use TYPO3\CMS\Core\Utility\HttpUtility; 62use TYPO3\CMS\Core\Utility\MathUtility; 63use TYPO3\CMS\Core\Utility\PathUtility; 64use TYPO3\CMS\Core\Versioning\VersionState; 65 66/** 67 * Standard functions available for the TYPO3 backend. 68 * You are encouraged to use this class in your own applications (Backend Modules) 69 * Don't instantiate - call functions with "\TYPO3\CMS\Backend\Utility\BackendUtility::" prefixed the function name. 70 * 71 * Call ALL methods without making an object! 72 * Eg. to get a page-record 51 do this: '\TYPO3\CMS\Backend\Utility\BackendUtility::getRecord('pages',51)' 73 */ 74class BackendUtility 75{ 76 /******************************************* 77 * 78 * SQL-related, selecting records, searching 79 * 80 *******************************************/ 81 /** 82 * Gets record with uid = $uid from $table 83 * You can set $field to a list of fields (default is '*') 84 * Additional WHERE clauses can be added by $where (fx. ' AND some_field = 1') 85 * Will automatically check if records has been deleted and if so, not return anything. 86 * $table must be found in $GLOBALS['TCA'] 87 * 88 * @param string $table Table name present in $GLOBALS['TCA'] 89 * @param int $uid UID of record 90 * @param string $fields List of fields to select 91 * @param string $where Additional WHERE clause, eg. ' AND some_field = 0' 92 * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE) 93 * @return array|null Returns the row if found, otherwise NULL 94 */ 95 public static function getRecord($table, $uid, $fields = '*', $where = '', $useDeleteClause = true) 96 { 97 // Ensure we have a valid uid (not 0 and not NEWxxxx) and a valid TCA 98 if ((int)$uid && !empty($GLOBALS['TCA'][$table])) { 99 $queryBuilder = static::getQueryBuilderForTable($table); 100 101 // do not use enabled fields here 102 $queryBuilder->getRestrictions()->removeAll(); 103 104 // should the delete clause be used 105 if ($useDeleteClause) { 106 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 107 } 108 109 // set table and where clause 110 $queryBuilder 111 ->select(...GeneralUtility::trimExplode(',', $fields, true)) 112 ->from($table) 113 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT))); 114 115 // add custom where clause 116 if ($where) { 117 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where)); 118 } 119 120 $row = $queryBuilder->execute()->fetch(); 121 if ($row) { 122 return $row; 123 } 124 } 125 return null; 126 } 127 128 /** 129 * Like getRecord(), but overlays workspace version if any. 130 * 131 * @param string $table Table name present in $GLOBALS['TCA'] 132 * @param int $uid UID of record 133 * @param string $fields List of fields to select 134 * @param string $where Additional WHERE clause, eg. ' AND some_field = 0' 135 * @param bool $useDeleteClause Use the deleteClause to check if a record is deleted (default TRUE) 136 * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace 137 * @return array Returns the row if found, otherwise nothing 138 */ 139 public static function getRecordWSOL( 140 $table, 141 $uid, 142 $fields = '*', 143 $where = '', 144 $useDeleteClause = true, 145 $unsetMovePointers = false 146 ) { 147 if ($fields !== '*') { 148 $internalFields = GeneralUtility::uniqueList($fields . ',uid,pid'); 149 $row = self::getRecord($table, $uid, $internalFields, $where, $useDeleteClause); 150 self::workspaceOL($table, $row, -99, $unsetMovePointers); 151 if (is_array($row)) { 152 foreach ($row as $key => $_) { 153 if (!GeneralUtility::inList($fields, $key) && $key[0] !== '_') { 154 unset($row[$key]); 155 } 156 } 157 } 158 } else { 159 $row = self::getRecord($table, $uid, $fields, $where, $useDeleteClause); 160 self::workspaceOL($table, $row, -99, $unsetMovePointers); 161 } 162 return $row; 163 } 164 165 /** 166 * Purges computed properties starting with underscore character ('_'). 167 * 168 * @param array<string,mixed> $record 169 * @return array<string,mixed> 170 * @internal should only be used from within TYPO3 Core 171 */ 172 public static function purgeComputedPropertiesFromRecord(array $record): array 173 { 174 return array_filter( 175 $record, 176 function (string $propertyName): bool { 177 return $propertyName[0] !== '_'; 178 }, 179 ARRAY_FILTER_USE_KEY 180 ); 181 } 182 183 /** 184 * Purges computed property names starting with underscore character ('_'). 185 * 186 * @param array $propertyNames 187 * @return array 188 * @internal should only be used from within TYPO3 Core 189 */ 190 public static function purgeComputedPropertyNames(array $propertyNames): array 191 { 192 return array_filter( 193 $propertyNames, 194 function (string $propertyName): bool { 195 return $propertyName[0] !== '_'; 196 } 197 ); 198 } 199 200 /** 201 * Makes a backwards explode on the $str and returns an array with ($table, $uid). 202 * Example: tt_content_45 => ['tt_content', 45] 203 * 204 * @param string $str [tablename]_[uid] string to explode 205 * @return array 206 * @internal should only be used from within TYPO3 Core 207 */ 208 public static function splitTable_Uid($str) 209 { 210 [$uid, $table] = explode('_', strrev($str), 2); 211 return [strrev($table), strrev($uid)]; 212 } 213 214 /** 215 * Backend implementation of enableFields() 216 * Notice that "fe_groups" is not selected for - only disabled, starttime and endtime. 217 * Notice that deleted-fields are NOT filtered - you must ALSO call deleteClause in addition. 218 * $GLOBALS["SIM_ACCESS_TIME"] is used for date. 219 * 220 * @param string $table The table from which to return enableFields WHERE clause. Table name must have a 'ctrl' section in $GLOBALS['TCA']. 221 * @param bool $inv Means that the query will select all records NOT VISIBLE records (inverted selection) 222 * @return string WHERE clause part 223 * @internal should only be used from within TYPO3 Core, but DefaultRestrictionHandler is recommended as alternative 224 */ 225 public static function BEenableFields($table, $inv = false) 226 { 227 $ctrl = $GLOBALS['TCA'][$table]['ctrl']; 228 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 229 ->getConnectionForTable($table) 230 ->getExpressionBuilder(); 231 $query = $expressionBuilder->andX(); 232 $invQuery = $expressionBuilder->orX(); 233 234 if (is_array($ctrl)) { 235 if (is_array($ctrl['enablecolumns'])) { 236 if ($ctrl['enablecolumns']['disabled'] ?? false) { 237 $field = $table . '.' . $ctrl['enablecolumns']['disabled']; 238 $query->add($expressionBuilder->eq($field, 0)); 239 $invQuery->add($expressionBuilder->neq($field, 0)); 240 } 241 if ($ctrl['enablecolumns']['starttime'] ?? false) { 242 $field = $table . '.' . $ctrl['enablecolumns']['starttime']; 243 $query->add($expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME'])); 244 $invQuery->add( 245 $expressionBuilder->andX( 246 $expressionBuilder->neq($field, 0), 247 $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME']) 248 ) 249 ); 250 } 251 if ($ctrl['enablecolumns']['endtime'] ?? false) { 252 $field = $table . '.' . $ctrl['enablecolumns']['endtime']; 253 $query->add( 254 $expressionBuilder->orX( 255 $expressionBuilder->eq($field, 0), 256 $expressionBuilder->gt($field, (int)$GLOBALS['SIM_ACCESS_TIME']) 257 ) 258 ); 259 $invQuery->add( 260 $expressionBuilder->andX( 261 $expressionBuilder->neq($field, 0), 262 $expressionBuilder->lte($field, (int)$GLOBALS['SIM_ACCESS_TIME']) 263 ) 264 ); 265 } 266 } 267 } 268 269 if ($query->count() === 0) { 270 return ''; 271 } 272 273 return ' AND ' . ($inv ? $invQuery : $query); 274 } 275 276 /** 277 * Fetches the localization for a given record. 278 * 279 * @param string $table Table name present in $GLOBALS['TCA'] 280 * @param int $uid The uid of the record 281 * @param int $language The uid of the language record in sys_language 282 * @param string $andWhereClause Optional additional WHERE clause (default: '') 283 * @return mixed Multidimensional array with selected records, empty array if none exists and FALSE if table is not localizable 284 */ 285 public static function getRecordLocalization($table, $uid, $language, $andWhereClause = '') 286 { 287 $recordLocalization = false; 288 289 if (self::isTableLocalizable($table)) { 290 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl']; 291 292 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 293 ->getQueryBuilderForTable($table); 294 $queryBuilder->getRestrictions() 295 ->removeAll() 296 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 297 ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace ?? 0)); 298 299 $queryBuilder->select('*') 300 ->from($table) 301 ->where( 302 $queryBuilder->expr()->eq( 303 $tcaCtrl['translationSource'] ?? $tcaCtrl['transOrigPointerField'], 304 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 305 ), 306 $queryBuilder->expr()->eq( 307 $tcaCtrl['languageField'], 308 $queryBuilder->createNamedParameter((int)$language, \PDO::PARAM_INT) 309 ) 310 ) 311 ->setMaxResults(1); 312 313 if ($andWhereClause) { 314 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($andWhereClause)); 315 } 316 317 $recordLocalization = $queryBuilder->execute()->fetchAll(); 318 } 319 320 return $recordLocalization; 321 } 322 323 /******************************************* 324 * 325 * Page tree, TCA related 326 * 327 *******************************************/ 328 /** 329 * Returns what is called the 'RootLine'. That is an array with information about the page records from a page id 330 * ($uid) and back to the root. 331 * By default deleted pages are filtered. 332 * This RootLine will follow the tree all the way to the root. This is opposite to another kind of root line known 333 * from the frontend where the rootline stops when a root-template is found. 334 * 335 * @param int $uid Page id for which to create the root line. 336 * @param string $clause Clause can be used to select other criteria. It would typically be where-clauses that 337 * stops the process if we meet a page, the user has no reading access to. 338 * @param bool $workspaceOL If TRUE, version overlay is applied. This must be requested specifically because it is 339 * usually only wanted when the rootline is used for visual output while for permission checking you want the raw thing! 340 * @param string[] $additionalFields Additional Fields to select for rootline records 341 * @return array Root line array, all the way to the page tree root uid=0 (or as far as $clause allows!), including the page given as $uid 342 */ 343 public static function BEgetRootLine($uid, $clause = '', $workspaceOL = false, array $additionalFields = []) 344 { 345 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); 346 $beGetRootLineCache = $runtimeCache->get('backendUtilityBeGetRootLine') ?: []; 347 $output = []; 348 $pid = $uid; 349 $ident = $pid . '-' . $clause . '-' . $workspaceOL . ($additionalFields ? '-' . md5(implode(',', $additionalFields)) : ''); 350 if (is_array($beGetRootLineCache[$ident] ?? false)) { 351 $output = $beGetRootLineCache[$ident]; 352 } else { 353 $loopCheck = 100; 354 $theRowArray = []; 355 while ($uid != 0 && $loopCheck) { 356 $loopCheck--; 357 $row = self::getPageForRootline($uid, $clause, $workspaceOL, $additionalFields); 358 if (is_array($row)) { 359 $uid = $row['pid']; 360 $theRowArray[] = $row; 361 } else { 362 break; 363 } 364 } 365 $fields = [ 366 'uid', 367 'pid', 368 'title', 369 'doktype', 370 'slug', 371 'tsconfig_includes', 372 'TSconfig', 373 'is_siteroot', 374 't3ver_oid', 375 't3ver_wsid', 376 't3ver_state', 377 't3ver_stage', 378 'backend_layout_next_level', 379 'hidden', 380 'starttime', 381 'endtime', 382 'fe_group', 383 'nav_hide', 384 'content_from_pid', 385 'module', 386 'extendToSubpages' 387 ]; 388 $fields = array_merge($fields, $additionalFields); 389 $rootPage = array_fill_keys($fields, null); 390 if ($uid == 0) { 391 $rootPage['uid'] = 0; 392 $theRowArray[] = $rootPage; 393 } 394 $c = count($theRowArray); 395 foreach ($theRowArray as $val) { 396 $c--; 397 $output[$c] = array_intersect_key($val, $rootPage); 398 if (isset($val['_ORIG_pid'])) { 399 $output[$c]['_ORIG_pid'] = $val['_ORIG_pid']; 400 } 401 } 402 $beGetRootLineCache[$ident] = $output; 403 $runtimeCache->set('backendUtilityBeGetRootLine', $beGetRootLineCache); 404 } 405 return $output; 406 } 407 408 /** 409 * Gets the cached page record for the rootline 410 * 411 * @param int $uid Page id for which to create the root line. 412 * @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. 413 * @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! 414 * @param string[] $additionalFields AdditionalFields to fetch from the root line 415 * @return array Cached page record for the rootline 416 * @see BEgetRootLine 417 */ 418 protected static function getPageForRootline($uid, $clause, $workspaceOL, array $additionalFields = []) 419 { 420 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); 421 $pageForRootlineCache = $runtimeCache->get('backendUtilityPageForRootLine') ?: []; 422 $statementCacheIdent = md5($clause . ($additionalFields ? '-' . implode(',', $additionalFields) : '')); 423 $ident = $uid . '-' . $workspaceOL . '-' . $statementCacheIdent; 424 if (is_array($pageForRootlineCache[$ident] ?? false)) { 425 $row = $pageForRootlineCache[$ident]; 426 } else { 427 $statement = $runtimeCache->get('getPageForRootlineStatement-' . $statementCacheIdent); 428 if (!$statement) { 429 $queryBuilder = static::getQueryBuilderForTable('pages'); 430 $queryBuilder->getRestrictions() 431 ->removeAll() 432 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 433 434 $queryBuilder 435 ->select( 436 'pid', 437 'uid', 438 'title', 439 'doktype', 440 'slug', 441 'tsconfig_includes', 442 'TSconfig', 443 'is_siteroot', 444 't3ver_oid', 445 't3ver_wsid', 446 't3ver_state', 447 't3ver_stage', 448 'backend_layout_next_level', 449 'hidden', 450 'starttime', 451 'endtime', 452 'fe_group', 453 'nav_hide', 454 'content_from_pid', 455 'module', 456 'extendToSubpages', 457 ...$additionalFields 458 ) 459 ->from('pages') 460 ->where( 461 $queryBuilder->expr()->eq('uid', $queryBuilder->createPositionalParameter($uid, \PDO::PARAM_INT)), 462 QueryHelper::stripLogicalOperatorPrefix($clause) 463 ); 464 $statement = $queryBuilder->execute(); 465 if (class_exists(\Doctrine\DBAL\ForwardCompatibility\Result::class) && $statement instanceof \Doctrine\DBAL\ForwardCompatibility\Result) { 466 $statement = $statement->getIterator(); 467 } 468 $runtimeCache->set('getPageForRootlineStatement-' . $statementCacheIdent, $statement); 469 } else { 470 $statement->bindValue(1, (int)$uid); 471 $statement->execute(); 472 } 473 $row = $statement->fetch(); 474 $statement->closeCursor(); 475 476 if ($row) { 477 $newLocation = false; 478 if ($workspaceOL) { 479 self::workspaceOL('pages', $row); 480 if (is_array($row) && (int)$row['t3ver_state'] === VersionState::MOVE_POINTER) { 481 $newLocation = self::getMovePlaceholder('pages', $row['uid'], 'pid'); 482 } 483 } 484 if (is_array($row)) { 485 if ($newLocation !== false) { 486 $row['pid'] = $newLocation['pid']; 487 } else { 488 self::fixVersioningPid('pages', $row); 489 } 490 $pageForRootlineCache[$ident] = $row; 491 $runtimeCache->set('backendUtilityPageForRootLine', $pageForRootlineCache); 492 } 493 } 494 } 495 return $row; 496 } 497 498 /** 499 * Opens the page tree to the specified page id 500 * 501 * @param int $pid Page id. 502 * @param bool $clearExpansion If set, then other open branches are closed. 503 * @internal should only be used from within TYPO3 Core 504 */ 505 public static function openPageTree($pid, $clearExpansion) 506 { 507 $beUser = static::getBackendUserAuthentication(); 508 // Get current expansion data: 509 if ($clearExpansion) { 510 $expandedPages = []; 511 } else { 512 $expandedPages = $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash']; 513 } 514 // Get rootline: 515 $rL = self::BEgetRootLine($pid); 516 // First, find out what mount index to use (if more than one DB mount exists): 517 $mountIndex = 0; 518 $mountKeys = $beUser->returnWebmounts(); 519 520 foreach ($rL as $rLDat) { 521 if (isset($mountKeys[$rLDat['uid']])) { 522 $mountIndex = $mountKeys[$rLDat['uid']]; 523 break; 524 } 525 } 526 // Traverse rootline and open paths: 527 foreach ($rL as $rLDat) { 528 $expandedPages[$mountIndex . '_' . $rLDat['uid']] = '1'; 529 } 530 // Write back: 531 $beUser->uc['BackendComponents']['States']['Pagetree']['stateHash'] = $expandedPages; 532 $beUser->writeUC(); 533 } 534 535 /** 536 * Returns the path (visually) of a page $uid, fx. "/First page/Second page/Another subpage" 537 * Each part of the path will be limited to $titleLimit characters 538 * Deleted pages are filtered out. 539 * 540 * @param int $uid Page uid for which to create record path 541 * @param string $clause Clause is additional where clauses, eg. 542 * @param int $titleLimit Title limit 543 * @param int $fullTitleLimit Title limit of Full title (typ. set to 1000 or so) 544 * @return mixed Path of record (string) OR array with short/long title if $fullTitleLimit is set. 545 */ 546 public static function getRecordPath($uid, $clause, $titleLimit, $fullTitleLimit = 0) 547 { 548 if (!$titleLimit) { 549 $titleLimit = 1000; 550 } 551 $output = $fullOutput = '/'; 552 $clause = trim($clause); 553 if ($clause !== '' && strpos($clause, 'AND') !== 0) { 554 $clause = 'AND ' . $clause; 555 } 556 $data = self::BEgetRootLine($uid, $clause, true); 557 foreach ($data as $record) { 558 if ($record['uid'] === 0) { 559 continue; 560 } 561 $output = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $titleLimit) . $output; 562 if ($fullTitleLimit) { 563 $fullOutput = '/' . GeneralUtility::fixed_lgd_cs(strip_tags($record['title']), $fullTitleLimit) . $fullOutput; 564 } 565 } 566 if ($fullTitleLimit) { 567 return [$output, $fullOutput]; 568 } 569 return $output; 570 } 571 572 /** 573 * Determines whether a table is localizable and has the languageField and transOrigPointerField set in $GLOBALS['TCA']. 574 * 575 * @param string $table The table to check 576 * @return bool Whether a table is localizable 577 */ 578 public static function isTableLocalizable($table) 579 { 580 $isLocalizable = false; 581 if (isset($GLOBALS['TCA'][$table]['ctrl']) && is_array($GLOBALS['TCA'][$table]['ctrl'])) { 582 $tcaCtrl = $GLOBALS['TCA'][$table]['ctrl']; 583 $isLocalizable = isset($tcaCtrl['languageField']) && $tcaCtrl['languageField'] && isset($tcaCtrl['transOrigPointerField']) && $tcaCtrl['transOrigPointerField']; 584 } 585 return $isLocalizable; 586 } 587 588 /** 589 * 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. 590 * If $id is zero a pseudo root-page with "_thePath" set is returned IF the current BE_USER is admin. 591 * In any case ->isInWebMount must return TRUE for the user (regardless of $perms_clause) 592 * 593 * @param int $id Page uid for which to check read-access 594 * @param string $perms_clause This is typically a value generated with static::getBackendUserAuthentication()->getPagePermsClause(1); 595 * @return array|false Returns page record if OK, otherwise FALSE. 596 */ 597 public static function readPageAccess($id, $perms_clause) 598 { 599 if ((string)$id !== '') { 600 $id = (int)$id; 601 if (!$id) { 602 if (static::getBackendUserAuthentication()->isAdmin()) { 603 return ['_thePath' => '/']; 604 } 605 } else { 606 $pageinfo = self::getRecord('pages', $id, '*', $perms_clause); 607 if ($pageinfo['uid'] && static::getBackendUserAuthentication()->isInWebMount($pageinfo, $perms_clause)) { 608 self::workspaceOL('pages', $pageinfo); 609 if (is_array($pageinfo)) { 610 self::fixVersioningPid('pages', $pageinfo); 611 [$pageinfo['_thePath'], $pageinfo['_thePathFull']] = self::getRecordPath((int)$pageinfo['uid'], $perms_clause, 15, 1000); 612 return $pageinfo; 613 } 614 } 615 } 616 } 617 return false; 618 } 619 620 /** 621 * Returns the "type" value of $rec from $table which can be used to look up the correct "types" rendering section in $GLOBALS['TCA'] 622 * If no "type" field is configured in the "ctrl"-section of the $GLOBALS['TCA'] for the table, zero is used. 623 * 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) 624 * 625 * Note: This method is very similar to the type determination of FormDataProvider/DatabaseRecordTypeValue, 626 * however, it has two differences: 627 * 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). 628 * 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" 629 * and "group" field values are stored, which makes different processing of the "foreign pointer field" type field variant necessary. 630 * 631 * @param string $table Table name present in TCA 632 * @param array $row Record from $table 633 * @throws \RuntimeException 634 * @return string Field value 635 */ 636 public static function getTCAtypeValue($table, $row) 637 { 638 $typeNum = 0; 639 if ($GLOBALS['TCA'][$table]) { 640 $field = $GLOBALS['TCA'][$table]['ctrl']['type']; 641 if (strpos($field, ':') !== false) { 642 [$pointerField, $foreignTableTypeField] = explode(':', $field); 643 // Get field value from database if field is not in the $row array 644 if (!isset($row[$pointerField])) { 645 $localRow = self::getRecord($table, $row['uid'], $pointerField); 646 $foreignUid = $localRow[$pointerField]; 647 } else { 648 $foreignUid = $row[$pointerField]; 649 } 650 if ($foreignUid) { 651 $fieldConfig = $GLOBALS['TCA'][$table]['columns'][$pointerField]['config']; 652 $relationType = $fieldConfig['type']; 653 if ($relationType === 'select') { 654 $foreignTable = $fieldConfig['foreign_table']; 655 } elseif ($relationType === 'group') { 656 $allowedTables = explode(',', $fieldConfig['allowed']); 657 $foreignTable = $allowedTables[0]; 658 } else { 659 throw new \RuntimeException( 660 'TCA foreign field pointer fields are only allowed to be used with group or select field types.', 661 1325862240 662 ); 663 } 664 $foreignRow = self::getRecord($foreignTable, $foreignUid, $foreignTableTypeField); 665 if ($foreignRow[$foreignTableTypeField]) { 666 $typeNum = $foreignRow[$foreignTableTypeField]; 667 } 668 } 669 } else { 670 $typeNum = $row[$field]; 671 } 672 // If that value is an empty string, set it to "0" (zero) 673 if (empty($typeNum)) { 674 $typeNum = 0; 675 } 676 } 677 // If current typeNum doesn't exist, set it to 0 (or to 1 for historical reasons, if 0 doesn't exist) 678 if (!isset($GLOBALS['TCA'][$table]['types'][$typeNum]) || !$GLOBALS['TCA'][$table]['types'][$typeNum]) { 679 $typeNum = isset($GLOBALS['TCA'][$table]['types']['0']) ? 0 : 1; 680 } 681 // Force to string. Necessary for eg '-1' to be recognized as a type value. 682 $typeNum = (string)$typeNum; 683 return $typeNum; 684 } 685 686 /******************************************* 687 * 688 * TypoScript related 689 * 690 *******************************************/ 691 /** 692 * Returns the Page TSconfig for page with id, $id 693 * 694 * @param int $id Page uid for which to create Page TSconfig 695 * @return array Page TSconfig 696 * @see \TYPO3\CMS\Core\TypoScript\Parser\TypoScriptParser 697 */ 698 public static function getPagesTSconfig($id) 699 { 700 $id = (int)$id; 701 702 $cache = self::getRuntimeCache(); 703 $pagesTsConfigIdToHash = $cache->get('pagesTsConfigIdToHash' . $id); 704 if ($pagesTsConfigIdToHash !== false) { 705 return $cache->get('pagesTsConfigHashToContent' . $pagesTsConfigIdToHash); 706 } 707 708 $rootLine = self::BEgetRootLine($id, '', true); 709 // Order correctly 710 ksort($rootLine); 711 712 try { 713 $site = GeneralUtility::makeInstance(SiteFinder::class)->getSiteByPageId($id); 714 } catch (SiteNotFoundException $exception) { 715 $site = null; 716 } 717 718 // Load PageTS from all pages of the rootLine 719 $pageTs = GeneralUtility::makeInstance(PageTsConfigLoader::class)->load($rootLine); 720 721 // Parse the PageTS into an array, also applying conditions 722 $parser = GeneralUtility::makeInstance( 723 PageTsConfigParser::class, 724 GeneralUtility::makeInstance(TypoScriptParser::class), 725 GeneralUtility::makeInstance(CacheManager::class)->getCache('hash') 726 ); 727 $matcher = GeneralUtility::makeInstance(ConditionMatcher::class, null, $id, $rootLine); 728 $tsConfig = $parser->parse($pageTs, $matcher, $site); 729 $cacheHash = md5((string)json_encode($tsConfig)); 730 731 // Get User TSconfig overlay, if no backend user is logged-in, this needs to be checked as well 732 if (static::getBackendUserAuthentication()) { 733 $userTSconfig = static::getBackendUserAuthentication()->getTSConfig() ?? []; 734 } else { 735 $userTSconfig = []; 736 } 737 738 if (is_array($userTSconfig['page.'] ?? null)) { 739 // Override page TSconfig with user TSconfig 740 ArrayUtility::mergeRecursiveWithOverrule($tsConfig, $userTSconfig['page.']); 741 $cacheHash .= '_user' . static::getBackendUserAuthentication()->user['uid']; 742 } 743 744 // Many pages end up with the same ts config. To reduce memory usage, the cache 745 // entries are a linked list: One or more pids point to content hashes which then 746 // contain the cached content. 747 $cache->set('pagesTsConfigHashToContent' . $cacheHash, $tsConfig, ['pagesTsConfig']); 748 $cache->set('pagesTsConfigIdToHash' . $id, $cacheHash, ['pagesTsConfig']); 749 750 return $tsConfig; 751 } 752 753 /** 754 * Returns the non-parsed Page TSconfig for page with id, $id 755 * 756 * @param int $id Page uid for which to create Page TSconfig 757 * @param array $rootLine If $rootLine is an array, that is used as rootline, otherwise rootline is just calculated 758 * @return array Non-parsed Page TSconfig 759 */ 760 public static function getRawPagesTSconfig($id, array $rootLine = null) 761 { 762 trigger_error('BackendUtility::getRawPagesTSconfig will be removed in TYPO3 v11.0. Use PageTsConfigLoader instead.', E_USER_DEPRECATED); 763 if (!is_array($rootLine)) { 764 $rootLine = self::BEgetRootLine($id, '', true); 765 } 766 767 // Order correctly 768 ksort($rootLine); 769 $tsDataArray = []; 770 // Setting default configuration 771 $tsDataArray['defaultPageTSconfig'] = $GLOBALS['TYPO3_CONF_VARS']['BE']['defaultPageTSconfig']; 772 foreach ($rootLine as $k => $v) { 773 if (trim($v['tsconfig_includes'])) { 774 $includeTsConfigFileList = GeneralUtility::trimExplode(',', $v['tsconfig_includes'], true); 775 // Traversing list 776 foreach ($includeTsConfigFileList as $key => $includeTsConfigFile) { 777 if (strpos($includeTsConfigFile, 'EXT:') === 0) { 778 [$includeTsConfigFileExtensionKey, $includeTsConfigFilename] = explode( 779 '/', 780 substr($includeTsConfigFile, 4), 781 2 782 ); 783 if ((string)$includeTsConfigFileExtensionKey !== '' 784 && ExtensionManagementUtility::isLoaded($includeTsConfigFileExtensionKey) 785 && (string)$includeTsConfigFilename !== '' 786 ) { 787 $extensionPath = ExtensionManagementUtility::extPath($includeTsConfigFileExtensionKey); 788 $includeTsConfigFileAndPath = PathUtility::getCanonicalPath($extensionPath . $includeTsConfigFilename); 789 if (strpos($includeTsConfigFileAndPath, $extensionPath) === 0 && file_exists($includeTsConfigFileAndPath)) { 790 $tsDataArray['uid_' . $v['uid'] . '_static_' . $key] = file_get_contents($includeTsConfigFileAndPath); 791 } 792 } 793 } 794 } 795 } 796 $tsDataArray['uid_' . $v['uid']] = $v['TSconfig']; 797 } 798 799 $eventDispatcher = GeneralUtility::getContainer()->get(EventDispatcherInterface::class); 800 $event = $eventDispatcher->dispatch(new ModifyLoadedPageTsConfigEvent($tsDataArray, $rootLine)); 801 return TypoScriptParser::checkIncludeLines_array($event->getTsConfig()); 802 } 803 804 /******************************************* 805 * 806 * Users / Groups related 807 * 808 *******************************************/ 809 /** 810 * Returns an array with be_users records of all user NOT DELETED sorted by their username 811 * Keys in the array is the be_users uid 812 * 813 * @param string $fields Optional $fields list (default: username,usergroup,usergroup_cached_list,uid) can be used to set the selected fields 814 * @param string $where Optional $where clause (fx. "AND username='pete'") can be used to limit query 815 * @return array 816 * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements 817 */ 818 public static function getUserNames($fields = 'username,usergroup,usergroup_cached_list,uid', $where = '') 819 { 820 return self::getRecordsSortedByTitle( 821 GeneralUtility::trimExplode(',', $fields, true), 822 'be_users', 823 'username', 824 'AND pid=0 ' . $where 825 ); 826 } 827 828 /** 829 * Returns an array with be_groups records (title, uid) of all groups NOT DELETED sorted by their title 830 * 831 * @param string $fields Field list 832 * @param string $where WHERE clause 833 * @return array 834 * @internal should only be used from within TYPO3 Core, use a direct SQL query instead to ensure proper DBAL where statements 835 */ 836 public static function getGroupNames($fields = 'title,uid', $where = '') 837 { 838 return self::getRecordsSortedByTitle( 839 GeneralUtility::trimExplode(',', $fields, true), 840 'be_groups', 841 'title', 842 'AND pid=0 ' . $where 843 ); 844 } 845 846 /** 847 * Returns an array of all non-deleted records of a table sorted by a given title field. 848 * The value of the title field will be replaced by the return value 849 * of self::getRecordTitle() before the sorting is performed. 850 * 851 * @param array $fields Fields to select 852 * @param string $table Table name 853 * @param string $titleField Field that will contain the record title 854 * @param string $where Additional where clause 855 * @return array Array of sorted records 856 */ 857 protected static function getRecordsSortedByTitle(array $fields, $table, $titleField, $where = '') 858 { 859 $fieldsIndex = array_flip($fields); 860 // Make sure the titleField is amongst the fields when getting sorted 861 $fieldsIndex[$titleField] = 1; 862 863 $result = []; 864 865 $queryBuilder = static::getQueryBuilderForTable($table); 866 $queryBuilder->getRestrictions() 867 ->removeAll() 868 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 869 870 $res = $queryBuilder 871 ->select('*') 872 ->from($table) 873 ->where(QueryHelper::stripLogicalOperatorPrefix($where)) 874 ->execute(); 875 876 while ($record = $res->fetch()) { 877 // store the uid, because it might be unset if it's not among the requested $fields 878 $recordId = $record['uid']; 879 $record[$titleField] = self::getRecordTitle($table, $record); 880 881 // include only the requested fields in the result 882 $result[$recordId] = array_intersect_key($record, $fieldsIndex); 883 } 884 885 // sort records by $sortField. This is not done in the query because the title might have been overwritten by 886 // self::getRecordTitle(); 887 return ArrayUtility::sortArraysByKey($result, $titleField); 888 } 889 890 /** 891 * Returns the array $usernames with the names of all users NOT IN $groupArray changed to the uid (hides the usernames!). 892 * If $excludeBlindedFlag is set, then these records are unset from the array $usernames 893 * 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 894 * 895 * @param array $usernames User names 896 * @param array $groupArray Group names 897 * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames 898 * @return array User names, blinded 899 * @internal 900 */ 901 public static function blindUserNames($usernames, $groupArray, $excludeBlindedFlag = false) 902 { 903 if (is_array($usernames) && is_array($groupArray)) { 904 foreach ($usernames as $uid => $row) { 905 $userN = $uid; 906 $set = 0; 907 if ($row['uid'] != static::getBackendUserAuthentication()->user['uid']) { 908 foreach ($groupArray as $v) { 909 if ($v && GeneralUtility::inList($row['usergroup_cached_list'], $v)) { 910 $userN = $row['username']; 911 $set = 1; 912 } 913 } 914 } else { 915 $userN = $row['username']; 916 $set = 1; 917 } 918 $usernames[$uid]['username'] = $userN; 919 if ($excludeBlindedFlag && !$set) { 920 unset($usernames[$uid]); 921 } 922 } 923 } 924 return $usernames; 925 } 926 927 /** 928 * Corresponds to blindUserNames but works for groups instead 929 * 930 * @param array $groups Group names 931 * @param array $groupArray Group names (reference) 932 * @param bool $excludeBlindedFlag If $excludeBlindedFlag is set, then these records are unset from the array $usernames 933 * @return array 934 * @internal 935 */ 936 public static function blindGroupNames($groups, $groupArray, $excludeBlindedFlag = false) 937 { 938 if (is_array($groups) && is_array($groupArray)) { 939 foreach ($groups as $uid => $row) { 940 $groupN = $uid; 941 $set = 0; 942 if (in_array($uid, $groupArray, false)) { 943 $groupN = $row['title']; 944 $set = 1; 945 } 946 $groups[$uid]['title'] = $groupN; 947 if ($excludeBlindedFlag && !$set) { 948 unset($groups[$uid]); 949 } 950 } 951 } 952 return $groups; 953 } 954 955 /******************************************* 956 * 957 * Output related 958 * 959 *******************************************/ 960 /** 961 * Returns the difference in days between input $tstamp and $EXEC_TIME 962 * 963 * @param int $tstamp Time stamp, seconds 964 * @return int 965 */ 966 public static function daysUntil($tstamp) 967 { 968 $delta_t = $tstamp - $GLOBALS['EXEC_TIME']; 969 return ceil($delta_t / (3600 * 24)); 970 } 971 972 /** 973 * Returns $tstamp formatted as "ddmmyy" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy']) 974 * 975 * @param int $tstamp Time stamp, seconds 976 * @return string Formatted time 977 */ 978 public static function date($tstamp) 979 { 980 return date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], (int)$tstamp); 981 } 982 983 /** 984 * Returns $tstamp formatted as "ddmmyy hhmm" (According to $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] AND $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']) 985 * 986 * @param int $value Time stamp, seconds 987 * @return string Formatted time 988 */ 989 public static function datetime($value) 990 { 991 return date( 992 $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], 993 $value 994 ); 995 } 996 997 /** 998 * Returns $value (in seconds) formatted as hh:mm:ss 999 * For instance $value = 3600 + 60*2 + 3 should return "01:02:03" 1000 * 1001 * @param int $value Time stamp, seconds 1002 * @param bool $withSeconds Output hh:mm:ss. If FALSE: hh:mm 1003 * @return string Formatted time 1004 */ 1005 public static function time($value, $withSeconds = true) 1006 { 1007 return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value); 1008 } 1009 1010 /** 1011 * Returns the "age" in minutes / hours / days / years of the number of $seconds inputted. 1012 * 1013 * @param int $seconds Seconds could be the difference of a certain timestamp and time() 1014 * @param string $labels Labels should be something like ' min| hrs| days| yrs| min| hour| day| year'. This value is typically delivered by this function call: $GLOBALS["LANG"]->sL("LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears") 1015 * @return string Formatted time 1016 */ 1017 public static function calcAge($seconds, $labels = 'min|hrs|days|yrs|min|hour|day|year') 1018 { 1019 $labelArr = GeneralUtility::trimExplode('|', $labels, true); 1020 $absSeconds = abs($seconds); 1021 $sign = $seconds < 0 ? -1 : 1; 1022 if ($absSeconds < 3600) { 1023 $val = round($absSeconds / 60); 1024 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[4] : $labelArr[0]); 1025 } elseif ($absSeconds < 24 * 3600) { 1026 $val = round($absSeconds / 3600); 1027 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[5] : $labelArr[1]); 1028 } elseif ($absSeconds < 365 * 24 * 3600) { 1029 $val = round($absSeconds / (24 * 3600)); 1030 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[6] : $labelArr[2]); 1031 } else { 1032 $val = round($absSeconds / (365 * 24 * 3600)); 1033 $seconds = $sign * $val . ' ' . ($val == 1 ? $labelArr[7] : $labelArr[3]); 1034 } 1035 return $seconds; 1036 } 1037 1038 /** 1039 * Returns a formatted timestamp if $tstamp is set. 1040 * The date/datetime will be followed by the age in parenthesis. 1041 * 1042 * @param int $tstamp Time stamp, seconds 1043 * @param int $prefix 1/-1 depending on polarity of age. 1044 * @param string $date $date=="date" will yield "dd:mm:yy" formatting, otherwise "dd:mm:yy hh:mm 1045 * @return string 1046 */ 1047 public static function dateTimeAge($tstamp, $prefix = 1, $date = '') 1048 { 1049 if (!$tstamp) { 1050 return ''; 1051 } 1052 $label = static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears'); 1053 $age = ' (' . self::calcAge($prefix * ($GLOBALS['EXEC_TIME'] - $tstamp), $label) . ')'; 1054 return ($date === 'date' ? self::date($tstamp) : self::datetime($tstamp)) . $age; 1055 } 1056 1057 /** 1058 * Resolves file references for a given record. 1059 * 1060 * @param string $tableName Name of the table of the record 1061 * @param string $fieldName Name of the field of the record 1062 * @param array $element Record data 1063 * @param int|null $workspaceId Workspace to fetch data for 1064 * @return \TYPO3\CMS\Core\Resource\FileReference[]|null 1065 */ 1066 public static function resolveFileReferences($tableName, $fieldName, $element, $workspaceId = null) 1067 { 1068 if (empty($GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config'])) { 1069 return null; 1070 } 1071 $configuration = $GLOBALS['TCA'][$tableName]['columns'][$fieldName]['config']; 1072 if (empty($configuration['type']) || $configuration['type'] !== 'inline' 1073 || empty($configuration['foreign_table']) || $configuration['foreign_table'] !== 'sys_file_reference' 1074 ) { 1075 return null; 1076 } 1077 1078 $fileReferences = []; 1079 /** @var RelationHandler $relationHandler */ 1080 $relationHandler = GeneralUtility::makeInstance(RelationHandler::class); 1081 if ($workspaceId !== null) { 1082 $relationHandler->setWorkspaceId($workspaceId); 1083 } 1084 $relationHandler->start( 1085 $element[$fieldName], 1086 $configuration['foreign_table'], 1087 $configuration['MM'] ?? '', 1088 $element['uid'], 1089 $tableName, 1090 $configuration 1091 ); 1092 $relationHandler->processDeletePlaceholder(); 1093 $referenceUids = $relationHandler->tableArray[$configuration['foreign_table']]; 1094 1095 foreach ($referenceUids as $referenceUid) { 1096 try { 1097 $fileReference = GeneralUtility::makeInstance(ResourceFactory::class)->getFileReferenceObject( 1098 $referenceUid, 1099 [], 1100 $workspaceId === 0 1101 ); 1102 $fileReferences[$fileReference->getUid()] = $fileReference; 1103 } catch (FileDoesNotExistException $e) { 1104 /** 1105 * We just catch the exception here 1106 * Reasoning: There is nothing an editor or even admin could do 1107 */ 1108 } catch (\InvalidArgumentException $e) { 1109 /** 1110 * The storage does not exist anymore 1111 * Log the exception message for admins as they maybe can restore the storage 1112 */ 1113 self::getLogger()->error($e->getMessage(), ['table' => $tableName, 'fieldName' => $fieldName, 'referenceUid' => $referenceUid, 'exception' => $e]); 1114 } 1115 } 1116 1117 return $fileReferences; 1118 } 1119 1120 /** 1121 * Returns a linked image-tag for thumbnail(s)/fileicons/truetype-font-previews from a database row with sys_file_references 1122 * All $GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext'] extension are made to thumbnails + ttf file (renders font-example) 1123 * Thumbnails are linked to ShowItemController (/thumbnails route) 1124 * 1125 * @param array $row Row is the database row from the table, $table. 1126 * @param string $table Table name for $row (present in TCA) 1127 * @param string $field Field is pointing to the connecting field of sys_file_references 1128 * @param string $backPath Back path prefix for image tag src="" field 1129 * @param string $thumbScript UNUSED since FAL 1130 * @param string $uploaddir UNUSED since FAL 1131 * @param int $abs UNUSED 1132 * @param string $tparams Optional: $tparams is additional attributes for the image tags 1133 * @param int|string $size Optional: $size is [w]x[h] of the thumbnail. 64 is default. 1134 * @param bool $linkInfoPopup Whether to wrap with a link opening the info popup 1135 * @return string Thumbnail image tag. 1136 */ 1137 public static function thumbCode( 1138 $row, 1139 $table, 1140 $field, 1141 $backPath = '', 1142 $thumbScript = '', 1143 $uploaddir = null, 1144 $abs = 0, 1145 $tparams = '', 1146 $size = '', 1147 $linkInfoPopup = true 1148 ) { 1149 $size = (int)(trim((string)$size) ?: 64); 1150 $targetDimension = new ImageDimension($size, $size); 1151 $thumbData = ''; 1152 $fileReferences = static::resolveFileReferences($table, $field, $row); 1153 // FAL references 1154 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 1155 if ($fileReferences !== null) { 1156 foreach ($fileReferences as $fileReferenceObject) { 1157 // Do not show previews of hidden references 1158 if ($fileReferenceObject->getProperty('hidden')) { 1159 continue; 1160 } 1161 $fileObject = $fileReferenceObject->getOriginalFile(); 1162 1163 if ($fileObject->isMissing()) { 1164 $thumbData .= '<span class="label label-danger">' 1165 . htmlspecialchars( 1166 static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:warning.file_missing') 1167 ) 1168 . '</span> ' . htmlspecialchars($fileObject->getName()) . '<br />'; 1169 continue; 1170 } 1171 1172 // Preview web image or media elements 1173 if ($GLOBALS['TYPO3_CONF_VARS']['GFX']['thumbnails'] 1174 && $fileReferenceObject->getOriginalFile()->isImage() 1175 ) { 1176 $cropVariantCollection = CropVariantCollection::create((string)$fileReferenceObject->getProperty('crop')); 1177 $cropArea = $cropVariantCollection->getCropArea(); 1178 $taskType = ProcessedFile::CONTEXT_IMAGEPREVIEW; 1179 $processingConfiguration = [ 1180 'width' => $targetDimension->getWidth(), 1181 'height' => $targetDimension->getHeight(), 1182 ]; 1183 if (!$cropArea->isEmpty()) { 1184 $taskType = ProcessedFile::CONTEXT_IMAGECROPSCALEMASK; 1185 $processingConfiguration = [ 1186 'maxWidth' => $targetDimension->getWidth(), 1187 'maxHeight' => $targetDimension->getHeight(), 1188 'crop' => $cropArea->makeAbsoluteBasedOnFile($fileReferenceObject), 1189 ]; 1190 } 1191 $processedImage = $fileObject->process($taskType, $processingConfiguration); 1192 $attributes = [ 1193 'src' => $processedImage->getPublicUrl(true), 1194 'width' => $processedImage->getProperty('width'), 1195 'height' => $processedImage->getProperty('height'), 1196 'alt' => $fileReferenceObject->getName(), 1197 ]; 1198 $imgTag = '<img ' . GeneralUtility::implodeAttributes($attributes, true) . $tparams . '/>'; 1199 } else { 1200 // Icon 1201 $imgTag = '<span title="' . htmlspecialchars($fileObject->getName()) . '">' 1202 . $iconFactory->getIconForResource($fileObject, Icon::SIZE_SMALL)->render() 1203 . '</span>'; 1204 } 1205 if ($linkInfoPopup) { 1206 // relies on module 'TYPO3/CMS/Backend/ActionDispatcher' 1207 $attributes = GeneralUtility::implodeAttributes([ 1208 'data-dispatch-action' => 'TYPO3.InfoWindow.showItem', 1209 'data-dispatch-args-list' => '_FILE,' . (int)$fileObject->getUid(), 1210 ], true); 1211 $thumbData .= '<a href="#" ' . $attributes . '>' . $imgTag . '</a> '; 1212 } else { 1213 $thumbData .= $imgTag; 1214 } 1215 } 1216 } 1217 return $thumbData; 1218 } 1219 1220 /** 1221 * @param int $fileId 1222 * @param array $configuration 1223 * @return string 1224 */ 1225 public static function getThumbnailUrl(int $fileId, array $configuration): string 1226 { 1227 $taskType = $configuration['_context'] ?? ProcessedFile::CONTEXT_IMAGEPREVIEW; 1228 unset($configuration['_context']); 1229 1230 return GeneralUtility::makeInstance(ResourceFactory::class) 1231 ->getFileObject($fileId) 1232 ->process($taskType, $configuration) 1233 ->getPublicUrl(true); 1234 } 1235 1236 /** 1237 * Returns title-attribute information for a page-record informing about id, doktype, hidden, starttime, endtime, fe_group etc. 1238 * 1239 * @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) 1240 * @param string $perms_clause This is used to get the record path of the shortcut page, if any (and doktype==4) 1241 * @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 1242 * @return string 1243 */ 1244 public static function titleAttribForPages($row, $perms_clause = '', $includeAttrib = true) 1245 { 1246 $lang = static::getLanguageService(); 1247 $parts = []; 1248 $parts[] = 'id=' . $row['uid']; 1249 if ($row['uid'] === 0) { 1250 $out = htmlspecialchars($parts[0]); 1251 return $includeAttrib ? 'title="' . $out . '"' : $out; 1252 } 1253 switch (VersionState::cast($row['t3ver_state'])) { 1254 case new VersionState(VersionState::NEW_PLACEHOLDER): 1255 $parts[] = 'PLH WSID#' . $row['t3ver_wsid']; 1256 break; 1257 case new VersionState(VersionState::DELETE_PLACEHOLDER): 1258 $parts[] = 'Deleted element!'; 1259 break; 1260 case new VersionState(VersionState::MOVE_PLACEHOLDER): 1261 $parts[] = 'OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid']; 1262 break; 1263 case new VersionState(VersionState::MOVE_POINTER): 1264 $parts[] = 'NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid']; 1265 break; 1266 case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION): 1267 $parts[] = 'New element!'; 1268 break; 1269 } 1270 if ($row['doktype'] == PageRepository::DOKTYPE_LINK) { 1271 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['url']['label']) . ' ' . $row['url']; 1272 } elseif ($row['doktype'] == PageRepository::DOKTYPE_SHORTCUT) { 1273 if ($perms_clause) { 1274 $label = self::getRecordPath((int)$row['shortcut'], $perms_clause, 20); 1275 } else { 1276 $row['shortcut'] = (int)$row['shortcut']; 1277 $lRec = self::getRecordWSOL('pages', $row['shortcut'], 'title'); 1278 $label = $lRec['title'] . ' (id=' . $row['shortcut'] . ')'; 1279 } 1280 if ($row['shortcut_mode'] != PageRepository::SHORTCUT_MODE_NONE) { 1281 $label .= ', ' . $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut_mode']['label']) . ' ' 1282 . $lang->sL(self::getLabelFromItemlist('pages', 'shortcut_mode', $row['shortcut_mode'])); 1283 } 1284 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['shortcut']['label']) . ' ' . $label; 1285 } elseif ($row['doktype'] == PageRepository::DOKTYPE_MOUNTPOINT) { 1286 if ((int)$row['mount_pid'] > 0) { 1287 if ($perms_clause) { 1288 $label = self::getRecordPath((int)$row['mount_pid'], $perms_clause, 20); 1289 } else { 1290 $lRec = self::getRecordWSOL('pages', (int)$row['mount_pid'], 'title'); 1291 $label = $lRec['title'] . ' (id=' . $row['mount_pid'] . ')'; 1292 } 1293 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid']['label']) . ' ' . $label; 1294 if ($row['mount_pid_ol']) { 1295 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['mount_pid_ol']['label']); 1296 } 1297 } else { 1298 $parts[] = $lang->sL('LLL:EXT:frontend/Resources/Private/Language/locallang_tca.xlf:no_mount_pid'); 1299 } 1300 } 1301 if ($row['nav_hide']) { 1302 $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_tca.xlf:pages.nav_hide'); 1303 } 1304 if ($row['hidden']) { 1305 $parts[] = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden'); 1306 } 1307 if ($row['starttime']) { 1308 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['starttime']['label']) 1309 . ' ' . self::dateTimeAge($row['starttime'], -1, 'date'); 1310 } 1311 if ($row['endtime']) { 1312 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['endtime']['label']) . ' ' 1313 . self::dateTimeAge($row['endtime'], -1, 'date'); 1314 } 1315 if ($row['fe_group']) { 1316 $fe_groups = []; 1317 foreach (GeneralUtility::intExplode(',', $row['fe_group']) as $fe_group) { 1318 if ($fe_group < 0) { 1319 $fe_groups[] = $lang->sL(self::getLabelFromItemlist('pages', 'fe_group', (string)$fe_group)); 1320 } else { 1321 $lRec = self::getRecordWSOL('fe_groups', $fe_group, 'title'); 1322 $fe_groups[] = $lRec['title']; 1323 } 1324 } 1325 $label = implode(', ', $fe_groups); 1326 $parts[] = $lang->sL($GLOBALS['TCA']['pages']['columns']['fe_group']['label']) . ' ' . $label; 1327 } 1328 $out = htmlspecialchars(implode(' - ', $parts)); 1329 return $includeAttrib ? 'title="' . $out . '"' : $out; 1330 } 1331 1332 /** 1333 * Returns the combined markup for Bootstraps tooltips 1334 * 1335 * @param array $row 1336 * @param string $table 1337 * @return string 1338 */ 1339 public static function getRecordToolTip(array $row, $table = 'pages') 1340 { 1341 $toolTipText = self::getRecordIconAltText($row, $table); 1342 $toolTipCode = 'data-toggle="tooltip" data-title=" ' 1343 . str_replace(' - ', '<br>', $toolTipText) 1344 . '" data-html="true" data-placement="right"'; 1345 return $toolTipCode; 1346 } 1347 1348 /** 1349 * Returns title-attribute information for ANY record (from a table defined in TCA of course) 1350 * 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. 1351 * "pages" table can be used as well and will return the result of ->titleAttribForPages() for that page. 1352 * 1353 * @param array $row Table row; $row is a row from the table, $table 1354 * @param string $table Table name 1355 * @return string 1356 */ 1357 public static function getRecordIconAltText($row, $table = 'pages') 1358 { 1359 if ($table === 'pages') { 1360 $out = self::titleAttribForPages($row, '', false); 1361 } else { 1362 $out = !empty(trim($GLOBALS['TCA'][$table]['ctrl']['descriptionColumn'])) ? $row[$GLOBALS['TCA'][$table]['ctrl']['descriptionColumn']] . ' ' : ''; 1363 $ctrl = $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']; 1364 // Uid is added 1365 $out .= 'id=' . $row['uid']; 1366 if (static::isTableWorkspaceEnabled($table)) { 1367 switch (VersionState::cast($row['t3ver_state'])) { 1368 case new VersionState(VersionState::NEW_PLACEHOLDER): 1369 $out .= ' - PLH WSID#' . $row['t3ver_wsid']; 1370 break; 1371 case new VersionState(VersionState::DELETE_PLACEHOLDER): 1372 $out .= ' - Deleted element!'; 1373 break; 1374 case new VersionState(VersionState::MOVE_PLACEHOLDER): 1375 $out .= ' - OLD LOCATION (Move Placeholder) WSID#' . $row['t3ver_wsid']; 1376 break; 1377 case new VersionState(VersionState::MOVE_POINTER): 1378 $out .= ' - NEW LOCATION (Move-to Pointer) WSID#' . $row['t3ver_wsid']; 1379 break; 1380 case new VersionState(VersionState::NEW_PLACEHOLDER_VERSION): 1381 $out .= ' - New element!'; 1382 break; 1383 } 1384 } 1385 // Hidden 1386 $lang = static::getLanguageService(); 1387 if ($ctrl['disabled']) { 1388 $out .= $row[$ctrl['disabled']] ? ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.hidden') : ''; 1389 } 1390 if ($ctrl['starttime']) { 1391 if ($row[$ctrl['starttime']] > $GLOBALS['EXEC_TIME']) { 1392 $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.starttime') . ':' . self::date($row[$ctrl['starttime']]) . ' (' . self::daysUntil($row[$ctrl['starttime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')'; 1393 } 1394 } 1395 if ($row[$ctrl['endtime']]) { 1396 $out .= ' - ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.endtime') . ': ' . self::date($row[$ctrl['endtime']]) . ' (' . self::daysUntil($row[$ctrl['endtime']]) . ' ' . $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.days') . ')'; 1397 } 1398 } 1399 return htmlspecialchars($out); 1400 } 1401 1402 /** 1403 * Returns the label of the first found entry in an "items" array from $GLOBALS['TCA'] (tablename = $table/fieldname = $col) where the value is $key 1404 * 1405 * @param string $table Table name, present in $GLOBALS['TCA'] 1406 * @param string $col Field name, present in $GLOBALS['TCA'] 1407 * @param string $key items-array value to match 1408 * @return string Label for item entry 1409 */ 1410 public static function getLabelFromItemlist($table, $col, $key) 1411 { 1412 // Check, if there is an "items" array: 1413 if (is_array($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] ?? false)) { 1414 // Traverse the items-array... 1415 foreach ($GLOBALS['TCA'][$table]['columns'][$col]['config']['items'] as $v) { 1416 // ... and return the first found label where the value was equal to $key 1417 if ((string)$v[1] === (string)$key) { 1418 return $v[0]; 1419 } 1420 } 1421 } 1422 return ''; 1423 } 1424 1425 /** 1426 * Return the label of a field by additionally checking TsConfig values 1427 * 1428 * @param int $pageId Page id 1429 * @param string $table Table name 1430 * @param string $column Field Name 1431 * @param string $key item value 1432 * @return string Label for item entry 1433 */ 1434 public static function getLabelFromItemListMerged($pageId, $table, $column, $key) 1435 { 1436 $pageTsConfig = static::getPagesTSconfig($pageId); 1437 $label = ''; 1438 if (isset($pageTsConfig['TCEFORM.']) 1439 && \is_array($pageTsConfig['TCEFORM.']) 1440 && \is_array($pageTsConfig['TCEFORM.'][$table . '.']) 1441 && \is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']) 1442 ) { 1443 if (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.']) 1444 && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key]) 1445 ) { 1446 $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['addItems.'][$key]; 1447 } elseif (\is_array($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.']) 1448 && isset($pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key]) 1449 ) { 1450 $label = $pageTsConfig['TCEFORM.'][$table . '.'][$column . '.']['altLabels.'][$key]; 1451 } 1452 } 1453 if (empty($label)) { 1454 $tcaValue = self::getLabelFromItemlist($table, $column, $key); 1455 if (!empty($tcaValue)) { 1456 $label = $tcaValue; 1457 } 1458 } 1459 return $label; 1460 } 1461 1462 /** 1463 * Splits the given key with commas and returns the list of all the localized items labels, separated by a comma. 1464 * NOTE: this does not take itemsProcFunc into account 1465 * 1466 * @param string $table Table name, present in TCA 1467 * @param string $column Field name 1468 * @param string $keyList Key or comma-separated list of keys. 1469 * @param array $columnTsConfig page TSConfig for $column (TCEMAIN.<table>.<column>) 1470 * @return string Comma-separated list of localized labels 1471 */ 1472 public static function getLabelsFromItemsList($table, $column, $keyList, array $columnTsConfig = []) 1473 { 1474 // Check if there is an "items" array 1475 if ( 1476 !isset($GLOBALS['TCA'][$table]['columns'][$column]['config']['items']) 1477 || !is_array($GLOBALS['TCA'][$table]['columns'][$column]['config']['items']) 1478 || $keyList === '' 1479 ) { 1480 return ''; 1481 } 1482 1483 $keys = GeneralUtility::trimExplode(',', $keyList, true); 1484 $labels = []; 1485 // Loop on all selected values 1486 foreach ($keys as $key) { 1487 $label = null; 1488 if ($columnTsConfig) { 1489 // Check if label has been defined or redefined via pageTsConfig 1490 if (isset($columnTsConfig['addItems.'][$key])) { 1491 $label = $columnTsConfig['addItems.'][$key]; 1492 } elseif (isset($columnTsConfig['altLabels.'][$key])) { 1493 $label = $columnTsConfig['altLabels.'][$key]; 1494 } 1495 } 1496 if ($label === null) { 1497 // Otherwise lookup the label in TCA items list 1498 foreach ($GLOBALS['TCA'][$table]['columns'][$column]['config']['items'] as $itemConfiguration) { 1499 [$currentLabel, $currentKey] = $itemConfiguration; 1500 if ((string)$key === (string)$currentKey) { 1501 $label = $currentLabel; 1502 break; 1503 } 1504 } 1505 } 1506 if ($label !== null) { 1507 $labels[] = static::getLanguageService()->sL($label); 1508 } 1509 } 1510 return implode(', ', $labels); 1511 } 1512 1513 /** 1514 * Returns the label-value for fieldname $col in table, $table 1515 * 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>' 1516 * 1517 * @param string $table Table name, present in $GLOBALS['TCA'] 1518 * @param string $col Field name 1519 * @return string or NULL if $col is not found in the TCA table 1520 */ 1521 public static function getItemLabel($table, $col) 1522 { 1523 // Check if column exists 1524 if (is_array($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]['columns'][$col])) { 1525 return $GLOBALS['TCA'][$table]['columns'][$col]['label']; 1526 } 1527 1528 return null; 1529 } 1530 1531 /** 1532 * Returns the "title"-value in record, $row, from table, $table 1533 * The field(s) from which the value is taken is determined by the "ctrl"-entries 'label', 'label_alt' and 'label_alt_force' 1534 * 1535 * @param string $table Table name, present in TCA 1536 * @param array $row Row from table 1537 * @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 1538 * @param bool $forceResult If set, the function always returns an output. If no value is found for the title, '[No title]' is returned (localized). 1539 * @return string 1540 */ 1541 public static function getRecordTitle($table, $row, $prep = false, $forceResult = true) 1542 { 1543 $params = []; 1544 $recordTitle = ''; 1545 if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table])) { 1546 // If configured, call userFunc 1547 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'])) { 1548 $params['table'] = $table; 1549 $params['row'] = $row; 1550 $params['title'] = ''; 1551 $params['options'] = $GLOBALS['TCA'][$table]['ctrl']['label_userFunc_options'] ?? []; 1552 1553 // Create NULL-reference 1554 $null = null; 1555 GeneralUtility::callUserFunction($GLOBALS['TCA'][$table]['ctrl']['label_userFunc'], $params, $null); 1556 $recordTitle = $params['title']; 1557 } else { 1558 // No userFunc: Build label 1559 $recordTitle = self::getProcessedValue( 1560 $table, 1561 $GLOBALS['TCA'][$table]['ctrl']['label'], 1562 $row[$GLOBALS['TCA'][$table]['ctrl']['label']], 1563 0, 1564 false, 1565 false, 1566 $row['uid'], 1567 $forceResult 1568 ) ?? ''; 1569 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt']) 1570 && (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) || (string)$recordTitle === '') 1571 ) { 1572 $altFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true); 1573 $tA = []; 1574 if (!empty($recordTitle)) { 1575 $tA[] = $recordTitle; 1576 } 1577 foreach ($altFields as $fN) { 1578 $recordTitle = trim(strip_tags($row[$fN])); 1579 if ((string)$recordTitle !== '') { 1580 $recordTitle = self::getProcessedValue($table, $fN, $recordTitle, 0, false, false, $row['uid']); 1581 if (!$GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) { 1582 break; 1583 } 1584 $tA[] = $recordTitle; 1585 } 1586 } 1587 if ($GLOBALS['TCA'][$table]['ctrl']['label_alt_force']) { 1588 $recordTitle = implode(', ', $tA); 1589 } 1590 } 1591 } 1592 // If the current result is empty, set it to '[No title]' (localized) and prepare for output if requested 1593 if ($prep || $forceResult) { 1594 if ($prep) { 1595 $recordTitle = self::getRecordTitlePrep($recordTitle); 1596 } 1597 if (trim($recordTitle) === '') { 1598 $recordTitle = self::getNoRecordTitle($prep); 1599 } 1600 } 1601 } 1602 1603 return $recordTitle; 1604 } 1605 1606 /** 1607 * Crops a title string to a limited length and if it really was cropped, wrap it in a <span title="...">|</span>, 1608 * which offers a tooltip with the original title when moving mouse over it. 1609 * 1610 * @param string $title The title string to be cropped 1611 * @param int $titleLength Crop title after this length - if not set, BE_USER->uc['titleLen'] is used 1612 * @return string The processed title string, wrapped in <span title="...">|</span> if cropped 1613 */ 1614 public static function getRecordTitlePrep($title, $titleLength = 0) 1615 { 1616 // If $titleLength is not a valid positive integer, use BE_USER->uc['titleLen']: 1617 if (!$titleLength || !MathUtility::canBeInterpretedAsInteger($titleLength) || $titleLength < 0) { 1618 $titleLength = static::getBackendUserAuthentication()->uc['titleLen']; 1619 } 1620 $titleOrig = htmlspecialchars($title); 1621 $title = htmlspecialchars(GeneralUtility::fixed_lgd_cs($title, $titleLength)); 1622 // If title was cropped, offer a tooltip: 1623 if ($titleOrig != $title) { 1624 $title = '<span title="' . $titleOrig . '">' . $title . '</span>'; 1625 } 1626 return $title; 1627 } 1628 1629 /** 1630 * Get a localized [No title] string, wrapped in <em>|</em> if $prep is TRUE. 1631 * 1632 * @param bool $prep Wrap result in <em>|</em> 1633 * @return string Localized [No title] string 1634 */ 1635 public static function getNoRecordTitle($prep = false) 1636 { 1637 $noTitle = '[' . 1638 htmlspecialchars(static::getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.no_title')) 1639 . ']'; 1640 if ($prep) { 1641 $noTitle = '<em>' . $noTitle . '</em>'; 1642 } 1643 return $noTitle; 1644 } 1645 1646 /** 1647 * Returns a human readable output of a value from a record 1648 * 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. 1649 * $table/$col is tablename and fieldname 1650 * REMEMBER to pass the output through htmlspecialchars() if you output it to the browser! (To protect it from XSS attacks and be XHTML compliant) 1651 * 1652 * @param string $table Table name, present in TCA 1653 * @param string $col Field name, present in TCA 1654 * @param string $value The value of that field from a selected record 1655 * @param int $fixed_lgd_chars The max amount of characters the value may occupy 1656 * @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") 1657 * @param bool $noRecordLookup If set, no records will be looked up, UIDs are just shown. 1658 * @param int $uid Uid of the current record 1659 * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded. 1660 * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field 1661 * @throws \InvalidArgumentException 1662 * @return string|null 1663 */ 1664 public static function getProcessedValue( 1665 $table, 1666 $col, 1667 $value, 1668 $fixed_lgd_chars = 0, 1669 $defaultPassthrough = false, 1670 $noRecordLookup = false, 1671 $uid = 0, 1672 $forceResult = true, 1673 $pid = 0 1674 ) { 1675 if ($col === 'uid') { 1676 // uid is not in TCA-array 1677 return $value; 1678 } 1679 // Check if table and field is configured 1680 if (!isset($GLOBALS['TCA'][$table]['columns'][$col]) || !is_array($GLOBALS['TCA'][$table]['columns'][$col])) { 1681 return null; 1682 } 1683 // Depending on the fields configuration, make a meaningful output value. 1684 $theColConf = $GLOBALS['TCA'][$table]['columns'][$col]['config'] ?? []; 1685 /***************** 1686 *HOOK: pre-processing the human readable output from a record 1687 ****************/ 1688 $referenceObject = new \stdClass(); 1689 $referenceObject->table = $table; 1690 $referenceObject->fieldName = $col; 1691 $referenceObject->uid = $uid; 1692 $referenceObject->value = &$value; 1693 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['preProcessValue'] ?? [] as $_funcRef) { 1694 GeneralUtility::callUserFunction($_funcRef, $theColConf, $referenceObject); 1695 } 1696 1697 $l = ''; 1698 $lang = static::getLanguageService(); 1699 switch ((string)($theColConf['type'] ?? '')) { 1700 case 'radio': 1701 $l = self::getLabelFromItemlist($table, $col, $value); 1702 $l = $lang->sL($l); 1703 break; 1704 case 'inline': 1705 case 'select': 1706 if (!empty($theColConf['MM'])) { 1707 if ($uid) { 1708 // Display the title of MM related records in lists 1709 if ($noRecordLookup) { 1710 $MMfields = []; 1711 $MMfields[] = $theColConf['foreign_table'] . '.uid'; 1712 } else { 1713 $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']]; 1714 if (isset($GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'])) { 1715 foreach (GeneralUtility::trimExplode( 1716 ',', 1717 $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'], 1718 true 1719 ) as $f) { 1720 $MMfields[] = $theColConf['foreign_table'] . '.' . $f; 1721 } 1722 } 1723 } 1724 /** @var RelationHandler $dbGroup */ 1725 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class); 1726 $dbGroup->start( 1727 $value, 1728 $theColConf['foreign_table'], 1729 $theColConf['MM'], 1730 $uid, 1731 $table, 1732 $theColConf 1733 ); 1734 $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']]; 1735 if (is_array($selectUids) && !empty($selectUids)) { 1736 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']); 1737 $queryBuilder->getRestrictions() 1738 ->removeAll() 1739 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1740 1741 $result = $queryBuilder 1742 ->select('uid', ...$MMfields) 1743 ->from($theColConf['foreign_table']) 1744 ->where( 1745 $queryBuilder->expr()->in( 1746 'uid', 1747 $queryBuilder->createNamedParameter($selectUids, Connection::PARAM_INT_ARRAY) 1748 ) 1749 ) 1750 ->execute(); 1751 1752 $mmlA = []; 1753 while ($MMrow = $result->fetch()) { 1754 // Keep sorting of $selectUids 1755 $selectedUid = array_search($MMrow['uid'], $selectUids); 1756 $mmlA[$selectedUid] = $MMrow['uid']; 1757 if (!$noRecordLookup) { 1758 $mmlA[$selectedUid] = static::getRecordTitle( 1759 $theColConf['foreign_table'], 1760 $MMrow, 1761 false, 1762 $forceResult 1763 ); 1764 } 1765 } 1766 1767 if (!empty($mmlA)) { 1768 ksort($mmlA); 1769 $l = implode('; ', $mmlA); 1770 } else { 1771 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1772 } 1773 } else { 1774 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1775 } 1776 } else { 1777 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1778 } 1779 } else { 1780 $columnTsConfig = []; 1781 if ($pid) { 1782 $pageTsConfig = self::getPagesTSconfig($pid); 1783 if (isset($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']) && is_array($pageTsConfig['TCEFORM.'][$table . '.'][$col . '.'])) { 1784 $columnTsConfig = $pageTsConfig['TCEFORM.'][$table . '.'][$col . '.']; 1785 } 1786 } 1787 $l = self::getLabelsFromItemsList($table, $col, $value, $columnTsConfig); 1788 if (!empty($theColConf['foreign_table']) && !$l && !empty($GLOBALS['TCA'][$theColConf['foreign_table']])) { 1789 if ($noRecordLookup) { 1790 $l = $value; 1791 } else { 1792 $rParts = []; 1793 if ($uid && isset($theColConf['foreign_field']) && $theColConf['foreign_field'] !== '') { 1794 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']); 1795 $queryBuilder->getRestrictions() 1796 ->removeAll() 1797 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 1798 ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, static::getBackendUserAuthentication()->workspace)); 1799 $constraints = [ 1800 $queryBuilder->expr()->eq( 1801 $theColConf['foreign_field'], 1802 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 1803 ) 1804 ]; 1805 1806 if (!empty($theColConf['foreign_table_field'])) { 1807 $constraints[] = $queryBuilder->expr()->eq( 1808 $theColConf['foreign_table_field'], 1809 $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR) 1810 ); 1811 } 1812 1813 // Add additional where clause if foreign_match_fields are defined 1814 $foreignMatchFields = []; 1815 if (is_array($theColConf['foreign_match_fields'])) { 1816 $foreignMatchFields = $theColConf['foreign_match_fields']; 1817 } 1818 1819 foreach ($foreignMatchFields as $matchField => $matchValue) { 1820 $constraints[] = $queryBuilder->expr()->eq( 1821 $matchField, 1822 $queryBuilder->createNamedParameter($matchValue) 1823 ); 1824 } 1825 1826 $result = $queryBuilder 1827 ->select('*') 1828 ->from($theColConf['foreign_table']) 1829 ->where(...$constraints) 1830 ->execute(); 1831 1832 while ($record = $result->fetch()) { 1833 $rParts[] = $record['uid']; 1834 } 1835 } 1836 if (empty($rParts)) { 1837 $rParts = GeneralUtility::trimExplode(',', $value, true); 1838 } 1839 $lA = []; 1840 foreach ($rParts as $rVal) { 1841 $rVal = (int)$rVal; 1842 $r = self::getRecordWSOL($theColConf['foreign_table'], $rVal); 1843 if (is_array($r)) { 1844 $lA[] = $lang->sL($theColConf['foreign_table_prefix']) 1845 . self::getRecordTitle($theColConf['foreign_table'], $r, false, $forceResult); 1846 } else { 1847 $lA[] = $rVal ? '[' . $rVal . '!]' : ''; 1848 } 1849 } 1850 $l = implode(', ', $lA); 1851 } 1852 } 1853 if (empty($l) && !empty($value)) { 1854 // Use plain database value when label is empty 1855 $l = $value; 1856 } 1857 } 1858 break; 1859 case 'group': 1860 // resolve the titles for DB records 1861 if (isset($theColConf['internal_type']) && $theColConf['internal_type'] === 'db') { 1862 if (isset($theColConf['MM']) && $theColConf['MM']) { 1863 if ($uid) { 1864 // Display the title of MM related records in lists 1865 if ($noRecordLookup) { 1866 $MMfields = []; 1867 $MMfields[] = $theColConf['foreign_table'] . '.uid'; 1868 } else { 1869 $MMfields = [$theColConf['foreign_table'] . '.' . $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label']]; 1870 $altLabelFields = explode( 1871 ',', 1872 $GLOBALS['TCA'][$theColConf['foreign_table']]['ctrl']['label_alt'] 1873 ); 1874 foreach ($altLabelFields as $f) { 1875 $f = trim($f); 1876 if ($f !== '') { 1877 $MMfields[] = $theColConf['foreign_table'] . '.' . $f; 1878 } 1879 } 1880 } 1881 /** @var RelationHandler $dbGroup */ 1882 $dbGroup = GeneralUtility::makeInstance(RelationHandler::class); 1883 $dbGroup->start( 1884 $value, 1885 $theColConf['foreign_table'], 1886 $theColConf['MM'], 1887 $uid, 1888 $table, 1889 $theColConf 1890 ); 1891 $selectUids = $dbGroup->tableArray[$theColConf['foreign_table']]; 1892 if (!empty($selectUids) && is_array($selectUids)) { 1893 $queryBuilder = static::getQueryBuilderForTable($theColConf['foreign_table']); 1894 $queryBuilder->getRestrictions() 1895 ->removeAll() 1896 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1897 1898 $result = $queryBuilder 1899 ->select('uid', ...$MMfields) 1900 ->from($theColConf['foreign_table']) 1901 ->where( 1902 $queryBuilder->expr()->in( 1903 'uid', 1904 $queryBuilder->createNamedParameter( 1905 $selectUids, 1906 Connection::PARAM_INT_ARRAY 1907 ) 1908 ) 1909 ) 1910 ->execute(); 1911 1912 $mmlA = []; 1913 while ($MMrow = $result->fetch()) { 1914 // Keep sorting of $selectUids 1915 $selectedUid = array_search($MMrow['uid'], $selectUids); 1916 $mmlA[$selectedUid] = $MMrow['uid']; 1917 if (!$noRecordLookup) { 1918 $mmlA[$selectedUid] = static::getRecordTitle( 1919 $theColConf['foreign_table'], 1920 $MMrow, 1921 false, 1922 $forceResult 1923 ); 1924 } 1925 } 1926 1927 if (!empty($mmlA)) { 1928 ksort($mmlA); 1929 $l = implode('; ', $mmlA); 1930 } else { 1931 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1932 } 1933 } else { 1934 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1935 } 1936 } else { 1937 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 1938 } 1939 } else { 1940 $finalValues = []; 1941 $relationTableName = $theColConf['allowed']; 1942 $explodedValues = GeneralUtility::trimExplode(',', $value, true); 1943 1944 foreach ($explodedValues as $explodedValue) { 1945 if (MathUtility::canBeInterpretedAsInteger($explodedValue)) { 1946 $relationTableNameForField = $relationTableName; 1947 } else { 1948 [$relationTableNameForField, $explodedValue] = self::splitTable_Uid($explodedValue); 1949 } 1950 1951 $relationRecord = static::getRecordWSOL($relationTableNameForField, $explodedValue); 1952 $finalValues[] = static::getRecordTitle($relationTableNameForField, $relationRecord); 1953 } 1954 $l = implode(', ', $finalValues); 1955 } 1956 } else { 1957 $l = implode(', ', GeneralUtility::trimExplode(',', $value, true)); 1958 } 1959 break; 1960 case 'check': 1961 if (!is_array($theColConf['items'])) { 1962 $l = $value ? $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no'); 1963 } elseif (count($theColConf['items']) === 1) { 1964 reset($theColConf['items']); 1965 $invertStateDisplay = current($theColConf['items'])['invertStateDisplay'] ?? false; 1966 if ($invertStateDisplay) { 1967 $value = !$value; 1968 } 1969 $l = $value ? $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes') : $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no'); 1970 } else { 1971 $lA = []; 1972 foreach ($theColConf['items'] as $key => $val) { 1973 if ($value & 2 ** $key) { 1974 $lA[] = $lang->sL($val[0]); 1975 } 1976 } 1977 $l = implode(', ', $lA); 1978 } 1979 break; 1980 case 'input': 1981 // Hide value 0 for dates, but show it for everything else 1982 // todo: phpstan states that $value always exists and is not nullable. At the moment, this is a false 1983 // positive as null can be passed into this method via $value. As soon as more strict types are 1984 // used, this isset check must be replaced with a more appropriate check. 1985 if (isset($value)) { 1986 $dateTimeFormats = QueryHelper::getDateTimeFormats(); 1987 1988 if (GeneralUtility::inList($theColConf['eval'] ?? '', 'date')) { 1989 // Handle native date field 1990 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'date') { 1991 $value = $value === $dateTimeFormats['date']['empty'] ? 0 : (int)strtotime($value); 1992 } else { 1993 $value = (int)$value; 1994 } 1995 if (!empty($value)) { 1996 $ageSuffix = ''; 1997 $dateColumnConfiguration = $GLOBALS['TCA'][$table]['columns'][$col]['config']; 1998 $ageDisplayKey = 'disableAgeDisplay'; 1999 2000 // generate age suffix as long as not explicitly suppressed 2001 if (!isset($dateColumnConfiguration[$ageDisplayKey]) 2002 // non typesafe comparison on intention 2003 || $dateColumnConfiguration[$ageDisplayKey] == false 2004 ) { 2005 $ageSuffix = ' (' . ($GLOBALS['EXEC_TIME'] - $value > 0 ? '-' : '') 2006 . self::calcAge( 2007 (int)abs($GLOBALS['EXEC_TIME'] - $value), 2008 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears') 2009 ) 2010 . ')'; 2011 } 2012 2013 $l = self::date($value) . $ageSuffix; 2014 } 2015 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'time')) { 2016 // Handle native time field 2017 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') { 2018 $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC'); 2019 } else { 2020 $value = (int)$value; 2021 } 2022 if (!empty($value)) { 2023 $l = gmdate('H:i', (int)$value); 2024 } 2025 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'timesec')) { 2026 // Handle native time field 2027 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'time') { 2028 $value = $value === $dateTimeFormats['time']['empty'] ? 0 : (int)strtotime('1970-01-01 ' . $value . ' UTC'); 2029 } else { 2030 $value = (int)$value; 2031 } 2032 if (!empty($value)) { 2033 $l = gmdate('H:i:s', (int)$value); 2034 } 2035 } elseif (GeneralUtility::inList($theColConf['eval'] ?? '', 'datetime')) { 2036 // Handle native datetime field 2037 if (isset($theColConf['dbType']) && $theColConf['dbType'] === 'datetime') { 2038 $value = $value === $dateTimeFormats['datetime']['empty'] ? 0 : (int)strtotime($value); 2039 } else { 2040 $value = (int)$value; 2041 } 2042 if (!empty($value)) { 2043 $l = self::datetime($value); 2044 } 2045 } else { 2046 $l = $value; 2047 } 2048 } 2049 break; 2050 case 'flex': 2051 $l = strip_tags($value); 2052 break; 2053 default: 2054 if ($defaultPassthrough) { 2055 $l = $value; 2056 } elseif (isset($theColConf['MM'])) { 2057 $l = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:notAvailableAbbreviation'); 2058 } elseif ($value) { 2059 $l = GeneralUtility::fixed_lgd_cs(strip_tags($value), 200); 2060 } 2061 } 2062 // If this field is a password field, then hide the password by changing it to a random number of asterisk (*) 2063 if (!empty($theColConf['eval']) && stripos($theColConf['eval'], 'password') !== false) { 2064 $l = ''; 2065 $randomNumber = random_int(5, 12); 2066 for ($i = 0; $i < $randomNumber; $i++) { 2067 $l .= '*'; 2068 } 2069 } 2070 /***************** 2071 *HOOK: post-processing the human readable output from a record 2072 ****************/ 2073 $null = null; 2074 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['postProcessValue'] ?? [] as $_funcRef) { 2075 $params = [ 2076 'value' => $l, 2077 'colConf' => $theColConf 2078 ]; 2079 $l = GeneralUtility::callUserFunction($_funcRef, $params, $null); 2080 } 2081 if ($fixed_lgd_chars) { 2082 return GeneralUtility::fixed_lgd_cs($l, $fixed_lgd_chars); 2083 } 2084 return $l; 2085 } 2086 2087 /** 2088 * 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. 2089 * 2090 * @param string $table Table name, present in TCA 2091 * @param string $fN Field name 2092 * @param string $fV Field value 2093 * @param int $fixed_lgd_chars The max amount of characters the value may occupy 2094 * @param int $uid Uid of the current record 2095 * @param bool $forceResult If BackendUtility::getRecordTitle is used to process the value, this parameter is forwarded. 2096 * @param int $pid Optional page uid is used to evaluate page TSConfig for the given field 2097 * @return string 2098 * @see getProcessedValue() 2099 */ 2100 public static function getProcessedValueExtra( 2101 $table, 2102 $fN, 2103 $fV, 2104 $fixed_lgd_chars = 0, 2105 $uid = 0, 2106 $forceResult = true, 2107 $pid = 0 2108 ) { 2109 $fVnew = self::getProcessedValue($table, $fN, $fV, $fixed_lgd_chars, true, false, $uid, $forceResult, $pid); 2110 if (!isset($fVnew)) { 2111 if (is_array($GLOBALS['TCA'][$table])) { 2112 if ($fN == $GLOBALS['TCA'][$table]['ctrl']['tstamp'] || $fN == $GLOBALS['TCA'][$table]['ctrl']['crdate']) { 2113 $fVnew = self::datetime((int)$fV); 2114 } elseif ($fN === 'pid') { 2115 // Fetches the path with no regard to the users permissions to select pages. 2116 $fVnew = self::getRecordPath((int)$fV, '1=1', 20); 2117 } else { 2118 $fVnew = $fV; 2119 } 2120 } 2121 } 2122 return $fVnew; 2123 } 2124 2125 /** 2126 * Returns fields for a table, $table, which would typically be interesting to select 2127 * This includes uid, the fields defined for title, icon-field. 2128 * 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) 2129 * 2130 * @param string $table Table name, present in $GLOBALS['TCA'] 2131 * @param string $prefix Table prefix 2132 * @param array $fields Preset fields (must include prefix if that is used) 2133 * @return string List of fields. 2134 * @internal should only be used from within TYPO3 Core 2135 */ 2136 public static function getCommonSelectFields($table, $prefix = '', $fields = []) 2137 { 2138 $fields[] = $prefix . 'uid'; 2139 if (isset($GLOBALS['TCA'][$table]['ctrl']['label']) && $GLOBALS['TCA'][$table]['ctrl']['label'] != '') { 2140 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['label']; 2141 } 2142 if (!empty($GLOBALS['TCA'][$table]['ctrl']['label_alt'])) { 2143 $secondFields = GeneralUtility::trimExplode(',', $GLOBALS['TCA'][$table]['ctrl']['label_alt'], true); 2144 foreach ($secondFields as $fieldN) { 2145 $fields[] = $prefix . $fieldN; 2146 } 2147 } 2148 if (static::isTableWorkspaceEnabled($table)) { 2149 $fields[] = $prefix . 't3ver_state'; 2150 $fields[] = $prefix . 't3ver_wsid'; 2151 $fields[] = $prefix . 't3ver_count'; 2152 } 2153 if (!empty($GLOBALS['TCA'][$table]['ctrl']['selicon_field'])) { 2154 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['selicon_field']; 2155 } 2156 if (!empty($GLOBALS['TCA'][$table]['ctrl']['typeicon_column'])) { 2157 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['typeicon_column']; 2158 } 2159 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled'])) { 2160 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['disabled']; 2161 } 2162 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime'])) { 2163 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['starttime']; 2164 } 2165 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime'])) { 2166 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['endtime']; 2167 } 2168 if (!empty($GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group'])) { 2169 $fields[] = $prefix . $GLOBALS['TCA'][$table]['ctrl']['enablecolumns']['fe_group']; 2170 } 2171 return implode(',', array_unique($fields)); 2172 } 2173 2174 /******************************************* 2175 * 2176 * Backend Modules API functions 2177 * 2178 *******************************************/ 2179 2180 /** 2181 * Returns CSH help text (description), if configured for, as an array (title, description) 2182 * 2183 * @param string $table Table name 2184 * @param string $field Field name 2185 * @return array With keys 'description' (raw, as available in locallang), 'title' (optional), 'moreInfo' 2186 * @internal should only be used from within TYPO3 Core 2187 */ 2188 public static function helpTextArray($table, $field) 2189 { 2190 if (!isset($GLOBALS['TCA_DESCR'][$table]['columns'])) { 2191 static::getLanguageService()->loadSingleTableDescription($table); 2192 } 2193 $output = [ 2194 'description' => null, 2195 'title' => null, 2196 'moreInfo' => false 2197 ]; 2198 if (isset($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field])) { 2199 $data = $GLOBALS['TCA_DESCR'][$table]['columns'][$field]; 2200 // Add alternative title, if defined 2201 if ($data['alttitle']) { 2202 $output['title'] = $data['alttitle']; 2203 } 2204 // If we have more information to show and access to the cshmanual 2205 if (($data['image_descr'] || $data['seeAlso'] || $data['details'] || $data['syntax']) 2206 && static::getBackendUserAuthentication()->check('modules', 'help_cshmanual') 2207 ) { 2208 $output['moreInfo'] = true; 2209 } 2210 // Add description 2211 if ($data['description']) { 2212 $output['description'] = $data['description']; 2213 } 2214 } 2215 return $output; 2216 } 2217 2218 /** 2219 * Returns CSH help text 2220 * 2221 * @param string $table Table name 2222 * @param string $field Field name 2223 * @return string HTML content for help text 2224 * @see cshItem() 2225 * @internal should only be used from within TYPO3 Core 2226 */ 2227 public static function helpText($table, $field) 2228 { 2229 $helpTextArray = self::helpTextArray($table, $field); 2230 $output = ''; 2231 $arrow = ''; 2232 // Put header before the rest of the text 2233 if ($helpTextArray['title'] !== null) { 2234 $output .= '<h2>' . $helpTextArray['title'] . '</h2>'; 2235 } 2236 // Add see also arrow if we have more info 2237 if ($helpTextArray['moreInfo']) { 2238 /** @var IconFactory $iconFactory */ 2239 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 2240 $arrow = $iconFactory->getIcon('actions-view-go-forward', Icon::SIZE_SMALL)->render(); 2241 } 2242 // Wrap description and arrow in p tag 2243 if ($helpTextArray['description'] !== null || $arrow) { 2244 $output .= '<p class="help-short">' . nl2br(htmlspecialchars($helpTextArray['description'])) . $arrow . '</p>'; 2245 } 2246 return $output; 2247 } 2248 2249 /** 2250 * API function that wraps the text / html in help text, so if a user hovers over it 2251 * the help text will show up 2252 * 2253 * @param string $table The table name for which the help should be shown 2254 * @param string $field The field name for which the help should be shown 2255 * @param string $text The text which should be wrapped with the help text 2256 * @param array $overloadHelpText Array with text to overload help text 2257 * @return string the HTML code ready to render 2258 * @internal should only be used from within TYPO3 Core 2259 */ 2260 public static function wrapInHelp($table, $field, $text = '', array $overloadHelpText = []) 2261 { 2262 // Initialize some variables 2263 $helpText = ''; 2264 $abbrClassAdd = ''; 2265 $hasHelpTextOverload = !empty($overloadHelpText); 2266 // Get the help text that should be shown on hover 2267 if (!$hasHelpTextOverload) { 2268 $helpText = self::helpText($table, $field); 2269 } 2270 // If there's a help text or some overload information, proceed with preparing an output 2271 if (!empty($helpText) || $hasHelpTextOverload) { 2272 // If no text was given, just use the regular help icon 2273 if ($text == '') { 2274 $iconFactory = GeneralUtility::makeInstance(IconFactory::class); 2275 $text = $iconFactory->getIcon('actions-system-help-open', Icon::SIZE_SMALL)->render(); 2276 $abbrClassAdd = ' help-teaser-icon'; 2277 } 2278 $text = '<abbr class="help-teaser' . $abbrClassAdd . '">' . $text . '</abbr>'; 2279 $wrappedText = '<span class="help-link" data-table="' . $table . '" data-field="' . $field . '"'; 2280 // The overload array may provide a title and a description 2281 // If either one is defined, add them to the "data" attributes 2282 if ($hasHelpTextOverload) { 2283 if (isset($overloadHelpText['title'])) { 2284 $wrappedText .= ' data-title="' . htmlspecialchars($overloadHelpText['title']) . '"'; 2285 } 2286 if (isset($overloadHelpText['description'])) { 2287 $wrappedText .= ' data-description="' . htmlspecialchars($overloadHelpText['description']) . '"'; 2288 } 2289 } 2290 $wrappedText .= '>' . $text . '</span>'; 2291 return $wrappedText; 2292 } 2293 return $text; 2294 } 2295 2296 /** 2297 * API for getting CSH icons/text for use in backend modules. 2298 * TCA_DESCR will be loaded if it isn't already 2299 * 2300 * @param string $table Table name ('_MOD_'+module name) 2301 * @param string $field Field name (CSH locallang main key) 2302 * @param string $_ (unused) 2303 * @param string $wrap Wrap code for icon-mode, splitted by "|". Not used for full-text mode. 2304 * @return string HTML content for help text 2305 */ 2306 public static function cshItem($table, $field, $_ = '', $wrap = '') 2307 { 2308 static::getLanguageService()->loadSingleTableDescription($table); 2309 if (is_array($GLOBALS['TCA_DESCR'][$table]) 2310 && is_array($GLOBALS['TCA_DESCR'][$table]['columns'][$field]) 2311 ) { 2312 // Creating short description 2313 $output = self::wrapInHelp($table, $field); 2314 if ($output && $wrap) { 2315 $wrParts = explode('|', $wrap); 2316 $output = $wrParts[0] . $output . $wrParts[1]; 2317 } 2318 return $output; 2319 } 2320 return ''; 2321 } 2322 2323 /** 2324 * Returns a JavaScript string (for an onClick handler) which will load the EditDocumentController script that shows the form for editing of the record(s) you have send as params. 2325 * REMEMBER to always htmlspecialchar() content in href-properties to ampersands get converted to entities (XHTML requirement and XSS precaution) 2326 * 2327 * @param string $params Parameters sent along to EditDocumentController. This requires a much more details description which you must seek in Inside TYPO3s documentation of the FormEngine API. And example could be '&edit[pages][123] = edit' which will show edit form for page record 123. 2328 * @param string $_ (unused) 2329 * @param string $requestUri An optional returnUrl you can set - automatically set to REQUEST_URI. 2330 * 2331 * @return string 2332 * @deprecated will be removed in TYPO3 v11. 2333 */ 2334 public static function editOnClick($params, $_ = '', $requestUri = '') 2335 { 2336 trigger_error(__METHOD__ . ' has been marked as deprecated and will be removed in TYPO3 v11. Consider using regular links and use the UriBuilder API instead.', E_USER_DEPRECATED); 2337 if ($requestUri == -1) { 2338 $returnUrl = 'T3_THIS_LOCATION'; 2339 } else { 2340 $returnUrl = GeneralUtility::quoteJSvalue(rawurlencode($requestUri ?: GeneralUtility::getIndpEnv('REQUEST_URI'))); 2341 } 2342 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 2343 return 'window.location.href=' . GeneralUtility::quoteJSvalue((string)$uriBuilder->buildUriFromRoute('record_edit') . $params . '&returnUrl=') . '+' . $returnUrl . '; return false;'; 2344 } 2345 2346 /** 2347 * Returns a JavaScript string for viewing the page id, $id 2348 * It will re-use any window already open. 2349 * 2350 * @param int $pageUid Page UID 2351 * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above) 2352 * @param array|null $rootLine If root line is supplied the function will look for the first found domain record and use that URL instead (if found) 2353 * @param string $anchorSection Optional anchor to the URL 2354 * @param string $alternativeUrl An alternative URL that, if set, will ignore other parameters except $switchFocus: It will return the window.open command wrapped around this URL! 2355 * @param string $additionalGetVars Additional GET variables. 2356 * @param bool $switchFocus If TRUE, then the preview window will gain the focus. 2357 * @return string 2358 */ 2359 public static function viewOnClick( 2360 $pageUid, 2361 $backPath = '', 2362 $rootLine = null, 2363 $anchorSection = '', 2364 $alternativeUrl = '', 2365 $additionalGetVars = '', 2366 $switchFocus = true 2367 ) { 2368 try { 2369 $previewUrl = self::getPreviewUrl( 2370 $pageUid, 2371 $backPath, 2372 $rootLine, 2373 $anchorSection, 2374 $alternativeUrl, 2375 $additionalGetVars, 2376 $switchFocus 2377 ); 2378 } catch (UnableToLinkToPageException $e) { 2379 return ''; 2380 } 2381 2382 $onclickCode = 'var previewWin = window.open(' . GeneralUtility::quoteJSvalue($previewUrl) . ',\'newTYPO3frontendWindow\');' 2383 . ($switchFocus ? 'previewWin.focus();' : '') . LF 2384 . 'if (previewWin.location.href === ' . GeneralUtility::quoteJSvalue($previewUrl) . ') { previewWin.location.reload(); };'; 2385 2386 return $onclickCode; 2387 } 2388 2389 /** 2390 * Returns the preview url 2391 * 2392 * It will detect the correct domain name if needed and provide the link with the right back path. 2393 * 2394 * @param int $pageUid Page UID 2395 * @param string $backPath Must point back to TYPO3_mainDir (where the site is assumed to be one level above) 2396 * @param array|null $rootLine If root line is supplied the function will look for the first found domain record and use that URL instead (if found) 2397 * @param string $anchorSection Optional anchor to the URL 2398 * @param string $alternativeUrl An alternative URL that, if set, will ignore other parameters except $switchFocus: It will return the window.open command wrapped around this URL! 2399 * @param string $additionalGetVars Additional GET variables. 2400 * @param bool $switchFocus If TRUE, then the preview window will gain the focus. 2401 * @return string 2402 */ 2403 public static function getPreviewUrl( 2404 $pageUid, 2405 $backPath = '', 2406 $rootLine = null, 2407 $anchorSection = '', 2408 $alternativeUrl = '', 2409 $additionalGetVars = '', 2410 &$switchFocus = true 2411 ): string { 2412 $viewScript = '/index.php?id='; 2413 if ($alternativeUrl) { 2414 $viewScript = $alternativeUrl; 2415 } 2416 2417 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) { 2418 $hookObj = GeneralUtility::makeInstance($className); 2419 if (method_exists($hookObj, 'preProcess')) { 2420 $hookObj->preProcess( 2421 $pageUid, 2422 $backPath, 2423 $rootLine, 2424 $anchorSection, 2425 $viewScript, 2426 $additionalGetVars, 2427 $switchFocus 2428 ); 2429 } 2430 } 2431 2432 // If there is an alternative URL or the URL has been modified by a hook, use that one. 2433 if ($alternativeUrl || $viewScript !== '/index.php?id=') { 2434 $previewUrl = $viewScript; 2435 } else { 2436 $permissionClause = $GLOBALS['BE_USER']->getPagePermsClause(Permission::PAGE_SHOW); 2437 $pageInfo = self::readPageAccess($pageUid, $permissionClause) ?: []; 2438 // prepare custom context for link generation (to allow for example time based previews) 2439 $context = clone GeneralUtility::makeInstance(Context::class); 2440 $additionalGetVars .= self::ADMCMD_previewCmds($pageInfo, $context); 2441 2442 // Build the URL with a site as prefix, if configured 2443 $siteFinder = GeneralUtility::makeInstance(SiteFinder::class); 2444 // Check if the page (= its rootline) has a site attached, otherwise just keep the URL as is 2445 $rootLine = $rootLine ?? BackendUtility::BEgetRootLine($pageUid); 2446 try { 2447 $site = $siteFinder->getSiteByPageId((int)$pageUid, $rootLine); 2448 } catch (SiteNotFoundException $e) { 2449 throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794919); 2450 } 2451 // Create a multi-dimensional array out of the additional get vars 2452 $additionalQueryParams = []; 2453 parse_str($additionalGetVars, $additionalQueryParams); 2454 if (isset($additionalQueryParams['L'])) { 2455 $additionalQueryParams['_language'] = $additionalQueryParams['_language'] ?? $additionalQueryParams['L']; 2456 unset($additionalQueryParams['L']); 2457 } 2458 try { 2459 $previewUrl = (string)$site->getRouter($context)->generateUri( 2460 $pageUid, 2461 $additionalQueryParams, 2462 $anchorSection, 2463 RouterInterface::ABSOLUTE_URL 2464 ); 2465 } catch (\InvalidArgumentException | InvalidRouteArgumentsException $e) { 2466 throw new UnableToLinkToPageException('The page ' . $pageUid . ' had no proper connection to a site, no link could be built.', 1559794914); 2467 } 2468 } 2469 2470 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['viewOnClickClass'] ?? [] as $className) { 2471 $hookObj = GeneralUtility::makeInstance($className); 2472 if (method_exists($hookObj, 'postProcess')) { 2473 $previewUrl = $hookObj->postProcess( 2474 $previewUrl, 2475 $pageUid, 2476 $rootLine, 2477 $anchorSection, 2478 $viewScript, 2479 $additionalGetVars, 2480 $switchFocus 2481 ); 2482 } 2483 } 2484 2485 return $previewUrl; 2486 } 2487 2488 /** 2489 * Makes click menu link (context sensitive menu) 2490 * 2491 * Returns $str wrapped in a link which will activate the context sensitive 2492 * menu for the record ($table/$uid) or file ($table = file) 2493 * The link will load the top frame with the parameter "&item" which is the table, uid 2494 * and context arguments imploded by "|": rawurlencode($table.'|'.$uid.'|'.$context) 2495 * 2496 * @param string $content String to be wrapped in link, typ. image tag. 2497 * @param string $table Table name/File path. If the icon is for a database 2498 * record, enter the tablename from $GLOBALS['TCA']. If a file then enter 2499 * the absolute filepath 2500 * @param int|string $uid If icon is for database record this is the UID for the 2501 * record from $table or identifier for sys_file record 2502 * @param string $context Set tree if menu is called from tree view 2503 * @param string $_addParams NOT IN USE 2504 * @param string $_enDisItems NOT IN USE 2505 * @param bool $returnTagParameters If set, will return only the onclick 2506 * JavaScript, not the whole link. 2507 * 2508 * @return string The link wrapped input string. 2509 */ 2510 public static function wrapClickMenuOnIcon( 2511 $content, 2512 $table, 2513 $uid = 0, 2514 $context = '', 2515 $_addParams = '', 2516 $_enDisItems = '', 2517 $returnTagParameters = false 2518 ) { 2519 $tagParameters = [ 2520 'class' => 't3js-contextmenutrigger', 2521 'data-table' => $table, 2522 'data-uid' => (string)$uid, 2523 'data-context' => $context 2524 ]; 2525 2526 if ($returnTagParameters) { 2527 return $tagParameters; 2528 } 2529 return '<a href="#" ' . GeneralUtility::implodeAttributes($tagParameters, true) . '>' . $content . '</a>'; 2530 } 2531 2532 /** 2533 * Returns a URL with a command to TYPO3 Datahandler 2534 * 2535 * @param string $parameters Set of GET params to send. Example: "&cmd[tt_content][123][move]=456" or "&data[tt_content][123][hidden]=1&data[tt_content][123][title]=Hello%20World 2536 * @param string|int $redirectUrl Redirect URL, default is to use GeneralUtility::getIndpEnv('REQUEST_URI'), -1 means to generate an URL for JavaScript using T3_THIS_LOCATION 2537 * @return string 2538 */ 2539 public static function getLinkToDataHandlerAction($parameters, $redirectUrl = '') 2540 { 2541 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 2542 $url = (string)$uriBuilder->buildUriFromRoute('tce_db') . $parameters . '&redirect='; 2543 if ((int)$redirectUrl === -1) { 2544 trigger_error('Generating URLs to DataHandler for JavaScript click handlers is deprecated. Consider using the href attribute instead.', E_USER_DEPRECATED); 2545 $url = GeneralUtility::quoteJSvalue($url) . '+T3_THIS_LOCATION'; 2546 } else { 2547 $url .= rawurlencode((string)($redirectUrl ?: GeneralUtility::getIndpEnv('REQUEST_URI'))); 2548 } 2549 return $url; 2550 } 2551 2552 /** 2553 * Builds the frontend view domain for a given page ID with a given root 2554 * line. 2555 * 2556 * @param int $pageId The page ID to use, must be > 0 2557 * @param array|null $rootLine The root line structure to use 2558 * @return string The full domain including the protocol http:// or https://, but without the trailing '/' 2559 * @deprecated since TYPO3 v10.0, will be removed in TYPO3 v11.0. Use PageRouter instead. 2560 */ 2561 public static function getViewDomain($pageId, $rootLine = null) 2562 { 2563 trigger_error('BackendUtility::getViewDomain() will be removed in TYPO3 v11.0. Use a Site and its PageRouter to link to a page directly', E_USER_DEPRECATED); 2564 $domain = rtrim(GeneralUtility::getIndpEnv('TYPO3_SITE_URL'), '/'); 2565 if (!is_array($rootLine)) { 2566 $rootLine = self::BEgetRootLine($pageId); 2567 } 2568 // Checks alternate domains 2569 if (!empty($rootLine)) { 2570 try { 2571 $site = GeneralUtility::makeInstance(SiteFinder::class) 2572 ->getSiteByPageId((int)$pageId, $rootLine); 2573 $uri = $site->getBase(); 2574 } catch (SiteNotFoundException $e) { 2575 // Just use the current domain 2576 $uri = new Uri($domain); 2577 // Append port number if lockSSLPort is not the standard port 443 2578 $portNumber = (int)$GLOBALS['TYPO3_CONF_VARS']['BE']['lockSSLPort']; 2579 if ($portNumber > 0 && $portNumber !== 443 && $portNumber < 65536 && $uri->getScheme() === 'https') { 2580 $uri = $uri->withPort((int)$portNumber); 2581 } 2582 } 2583 return (string)$uri; 2584 } 2585 return $domain; 2586 } 2587 2588 /** 2589 * Returns a selector box "function menu" for a module 2590 * See Inside TYPO3 for details about how to use / make Function menus 2591 * 2592 * @param mixed $mainParams The "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... 2593 * @param string $elementName The form elements name, probably something like "SET[...] 2594 * @param string $currentValue The value to be selected currently. 2595 * @param array $menuItems An array with the menu items for the selector box 2596 * @param string $script The script to send the &id to, if empty it's automatically found 2597 * @param string $addParams Additional parameters to pass to the script. 2598 * @return string HTML code for selector box 2599 */ 2600 public static function getFuncMenu( 2601 $mainParams, 2602 $elementName, 2603 $currentValue, 2604 $menuItems, 2605 $script = '', 2606 $addParams = '' 2607 ) { 2608 if (!is_array($menuItems) || count($menuItems) <= 1) { 2609 return ''; 2610 } 2611 $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script); 2612 $options = []; 2613 foreach ($menuItems as $value => $label) { 2614 $options[] = '<option value="' 2615 . htmlspecialchars($value) . '"' 2616 . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>' 2617 . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>'; 2618 } 2619 $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName); 2620 $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier); 2621 $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier); 2622 if (!empty($options)) { 2623 // relies on module 'TYPO3/CMS/Backend/ActionDispatcher' 2624 $attributes = GeneralUtility::implodeAttributes([ 2625 'name' => $elementName, 2626 'class' => 'form-control', 2627 'data-menu-identifier' => $dataMenuIdentifier, 2628 'data-global-event' => 'change', 2629 'data-action-navigate' => '$data=~s/$value/', 2630 'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}', 2631 ], true); 2632 return sprintf( 2633 '<select %s>%s</select>', 2634 $attributes, 2635 implode('', $options) 2636 ); 2637 } 2638 return ''; 2639 } 2640 2641 /** 2642 * Returns a selector box to switch the view 2643 * Based on BackendUtility::getFuncMenu() but done as new function because it has another purpose. 2644 * Mingling with getFuncMenu would harm the docHeader Menu. 2645 * 2646 * @param mixed $mainParams The "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... 2647 * @param string $elementName The form elements name, probably something like "SET[...] 2648 * @param string $currentValue The value to be selected currently. 2649 * @param array $menuItems An array with the menu items for the selector box 2650 * @param string $script The script to send the &id to, if empty it's automatically found 2651 * @param string $addParams Additional parameters to pass to the script. 2652 * @return string HTML code for selector box 2653 */ 2654 public static function getDropdownMenu( 2655 $mainParams, 2656 $elementName, 2657 $currentValue, 2658 $menuItems, 2659 $script = '', 2660 $addParams = '' 2661 ) { 2662 if (!is_array($menuItems) || count($menuItems) <= 1) { 2663 return ''; 2664 } 2665 $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script); 2666 $options = []; 2667 foreach ($menuItems as $value => $label) { 2668 $options[] = '<option value="' 2669 . htmlspecialchars($value) . '"' 2670 . ((string)$currentValue === (string)$value ? ' selected="selected"' : '') . '>' 2671 . htmlspecialchars($label, ENT_COMPAT, 'UTF-8', false) . '</option>'; 2672 } 2673 $dataMenuIdentifier = str_replace(['SET[', ']'], '', $elementName); 2674 $dataMenuIdentifier = GeneralUtility::camelCaseToLowerCaseUnderscored($dataMenuIdentifier); 2675 $dataMenuIdentifier = str_replace('_', '-', $dataMenuIdentifier); 2676 if (!empty($options)) { 2677 // relies on module 'TYPO3/CMS/Backend/ActionDispatcher' 2678 $attributes = GeneralUtility::implodeAttributes([ 2679 'name' => $elementName, 2680 'data-menu-identifier' => $dataMenuIdentifier, 2681 'data-global-event' => 'change', 2682 'data-action-navigate' => '$data=~s/$value/', 2683 'data-navigate-value' => $scriptUrl . '&' . $elementName . '=${value}', 2684 ], true); 2685 return ' 2686 <div class="form-group"> 2687 <!-- Function Menu of module --> 2688 <select class="form-control input-sm" ' . $attributes . '> 2689 ' . implode(LF, $options) . ' 2690 </select> 2691 </div> 2692 '; 2693 } 2694 return ''; 2695 } 2696 2697 /** 2698 * Checkbox function menu. 2699 * Works like ->getFuncMenu() but takes no $menuItem array since this is a simple checkbox. 2700 * 2701 * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... 2702 * @param string $elementName The form elements name, probably something like "SET[...] 2703 * @param string $currentValue The value to be selected currently. 2704 * @param string $script The script to send the &id to, if empty it's automatically found 2705 * @param string $addParams Additional parameters to pass to the script. 2706 * @param string $tagParams Additional attributes for the checkbox input tag 2707 * @return string HTML code for checkbox 2708 * @see getFuncMenu() 2709 */ 2710 public static function getFuncCheck( 2711 $mainParams, 2712 $elementName, 2713 $currentValue, 2714 $script = '', 2715 $addParams = '', 2716 $tagParams = '' 2717 ) { 2718 // relies on module 'TYPO3/CMS/Backend/ActionDispatcher' 2719 $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script); 2720 $attributes = GeneralUtility::implodeAttributes([ 2721 'type' => 'checkbox', 2722 'class' => 'checkbox', 2723 'name' => $elementName, 2724 'value' => '1', 2725 'data-global-event' => 'change', 2726 'data-action-navigate' => '$data=~s/$value/', 2727 'data-navigate-value' => sprintf('%s&%s=${value}', $scriptUrl, $elementName), 2728 'data-empty-value' => '0', 2729 ], true); 2730 return 2731 '<input ' . $attributes . 2732 ($currentValue ? ' checked="checked"' : '') . 2733 ($tagParams ? ' ' . $tagParams : '') . 2734 ' />'; 2735 } 2736 2737 /** 2738 * Input field function menu 2739 * Works like ->getFuncMenu() / ->getFuncCheck() but displays an input field instead which updates the script "onchange" 2740 * 2741 * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... 2742 * @param string $elementName The form elements name, probably something like "SET[...] 2743 * @param string $currentValue The value to be selected currently. 2744 * @param int $size Relative size of input field, max is 48 2745 * @param string $script The script to send the &id to, if empty it's automatically found 2746 * @param string $addParams Additional parameters to pass to the script. 2747 * @return string HTML code for input text field. 2748 * @see getFuncMenu() 2749 */ 2750 public static function getFuncInput( 2751 $mainParams, 2752 $elementName, 2753 $currentValue, 2754 $size = 10, 2755 $script = '', 2756 $addParams = '' 2757 ) { 2758 $scriptUrl = self::buildScriptUrl($mainParams, $addParams, $script); 2759 $onChange = 'window.location.href = ' . GeneralUtility::quoteJSvalue($scriptUrl . '&' . $elementName . '=') . '+escape(this.value);'; 2760 return '<input type="text" class="form-control" name="' . $elementName . '" value="' . htmlspecialchars($currentValue) . '" onchange="' . htmlspecialchars($onChange) . '" />'; 2761 } 2762 2763 /** 2764 * Builds the URL to the current script with given arguments 2765 * 2766 * @param mixed $mainParams $id is the "&id=" parameter value to be sent to the module, but it can be also a parameter array which will be passed instead of the &id=... 2767 * @param string $addParams Additional parameters to pass to the script. 2768 * @param string $script The script to send the &id to, if empty it's automatically found 2769 * @return string The complete script URL 2770 */ 2771 protected static function buildScriptUrl($mainParams, $addParams, $script = '') 2772 { 2773 if (!is_array($mainParams)) { 2774 $mainParams = ['id' => $mainParams]; 2775 } 2776 if (!$script) { 2777 $script = PathUtility::basename(Environment::getCurrentScript()); 2778 } 2779 2780 if ($routePath = GeneralUtility::_GP('route')) { 2781 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 2782 $scriptUrl = (string)$uriBuilder->buildUriFromRoutePath($routePath, $mainParams); 2783 $scriptUrl .= $addParams; 2784 } else { 2785 $scriptUrl = $script . HttpUtility::buildQueryString($mainParams, '?') . $addParams; 2786 } 2787 2788 return $scriptUrl; 2789 } 2790 2791 /** 2792 * Call to update the page tree frame (or something else..?) after 2793 * use 'updatePageTree' as a first parameter will set the page tree to be updated. 2794 * 2795 * @param string $set Key to set the update signal. When setting, this value contains strings telling WHAT to set. At this point it seems that the value "updatePageTree" is the only one it makes sense to set. If empty, all update signals will be removed. 2796 * @param mixed $params Additional information for the update signal, used to only refresh a branch of the tree 2797 * @see BackendUtility::getUpdateSignalCode() 2798 */ 2799 public static function setUpdateSignal($set = '', $params = '') 2800 { 2801 $beUser = static::getBackendUserAuthentication(); 2802 $modData = $beUser->getModuleData( 2803 \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', 2804 'ses' 2805 ); 2806 if ($set) { 2807 $modData[$set] = [ 2808 'set' => $set, 2809 'parameter' => $params 2810 ]; 2811 } else { 2812 // clear the module data 2813 $modData = []; 2814 } 2815 $beUser->pushModuleData(\TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', $modData); 2816 } 2817 2818 /** 2819 * Call to update the page tree frame (or something else..?) if this is set by the function 2820 * setUpdateSignal(). It will return some JavaScript that does the update 2821 * 2822 * @return string HTML javascript code 2823 * @see BackendUtility::setUpdateSignal() 2824 */ 2825 public static function getUpdateSignalCode() 2826 { 2827 $signals = []; 2828 $modData = static::getBackendUserAuthentication()->getModuleData( 2829 \TYPO3\CMS\Backend\Utility\BackendUtility::class . '::getUpdateSignal', 2830 'ses' 2831 ); 2832 if (empty($modData)) { 2833 return ''; 2834 } 2835 // Hook: Allows to let TYPO3 execute your JS code 2836 $updateSignals = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_befunc.php']['updateSignalHook'] ?? []; 2837 // Loop through all setUpdateSignals and get the JS code 2838 foreach ($modData as $set => $val) { 2839 if (isset($updateSignals[$set])) { 2840 $params = ['set' => $set, 'parameter' => $val['parameter'], 'JScode' => '']; 2841 $ref = null; 2842 GeneralUtility::callUserFunction($updateSignals[$set], $params, $ref); 2843 $signals[] = $params['JScode']; 2844 } else { 2845 switch ($set) { 2846 case 'updatePageTree': 2847 $signals[] = ' 2848 if (top && top.TYPO3.Backend && top.TYPO3.Backend.NavigationContainer.PageTree) { 2849 top.TYPO3.Backend.NavigationContainer.PageTree.refreshTree(); 2850 } 2851 '; 2852 break; 2853 case 'updateFolderTree': 2854 $signals[] = ' 2855 if (top && top.nav_frame && top.nav_frame.location) { 2856 top.nav_frame.location.reload(true); 2857 }'; 2858 break; 2859 case 'updateModuleMenu': 2860 $signals[] = ' 2861 if (top && top.TYPO3.ModuleMenu && top.TYPO3.ModuleMenu.App) { 2862 top.TYPO3.ModuleMenu.App.refreshMenu(); 2863 }'; 2864 break; 2865 case 'updateTopbar': 2866 $signals[] = ' 2867 if (top && top.TYPO3.Backend && top.TYPO3.Backend.Topbar) { 2868 top.TYPO3.Backend.Topbar.refresh(); 2869 }'; 2870 break; 2871 } 2872 } 2873 } 2874 $content = implode(LF, $signals); 2875 // For backwards compatibility, should be replaced 2876 self::setUpdateSignal(); 2877 return $content; 2878 } 2879 2880 /** 2881 * Returns an array which is most backend modules becomes MOD_SETTINGS containing values from function menus etc. determining the function of the module. 2882 * This is kind of session variable management framework for the backend users. 2883 * If a key from MOD_MENU is set in the CHANGED_SETTINGS array (eg. a value is passed to the script from the outside), this value is put into the settings-array 2884 * Ultimately, see Inside TYPO3 for how to use this function in relation to your modules. 2885 * 2886 * @param array $MOD_MENU MOD_MENU is an array that defines the options in menus. 2887 * @param array $CHANGED_SETTINGS CHANGED_SETTINGS represents the array used when passing values to the script from the menus. 2888 * @param string $modName modName is the name of this module. Used to get the correct module data. 2889 * @param string $type If type is 'ses' then the data is stored as session-lasting data. This means that it'll be wiped out the next time the user logs in. 2890 * @param string $dontValidateList dontValidateList can be used to list variables that should not be checked if their value is found in the MOD_MENU array. Used for dynamically generated menus. 2891 * @param string $setDefaultList List of default values from $MOD_MENU to set in the output array (only if the values from MOD_MENU are not arrays) 2892 * @throws \RuntimeException 2893 * @return array The array $settings, which holds a key for each MOD_MENU key and the values of each key will be within the range of values for each menuitem 2894 */ 2895 public static function getModuleData( 2896 $MOD_MENU, 2897 $CHANGED_SETTINGS, 2898 $modName, 2899 $type = '', 2900 $dontValidateList = '', 2901 $setDefaultList = '' 2902 ) { 2903 if ($modName && is_string($modName)) { 2904 // Getting stored user-data from this module: 2905 $beUser = static::getBackendUserAuthentication(); 2906 $settings = $beUser->getModuleData($modName, $type); 2907 $changed = 0; 2908 if (!is_array($settings)) { 2909 $changed = 1; 2910 $settings = []; 2911 } 2912 if (is_array($MOD_MENU)) { 2913 foreach ($MOD_MENU as $key => $var) { 2914 // If a global var is set before entering here. eg if submitted, then it's substituting the current value the array. 2915 if (is_array($CHANGED_SETTINGS) && isset($CHANGED_SETTINGS[$key])) { 2916 if (is_array($CHANGED_SETTINGS[$key])) { 2917 $serializedSettings = serialize($CHANGED_SETTINGS[$key]); 2918 if ((string)$settings[$key] !== $serializedSettings) { 2919 $settings[$key] = $serializedSettings; 2920 $changed = 1; 2921 } 2922 } else { 2923 if ((string)$settings[$key] !== (string)$CHANGED_SETTINGS[$key]) { 2924 $settings[$key] = $CHANGED_SETTINGS[$key]; 2925 $changed = 1; 2926 } 2927 } 2928 } 2929 // If the $var is an array, which denotes the existence of a menu, we check if the value is permitted 2930 if (is_array($var) && (!$dontValidateList || !GeneralUtility::inList($dontValidateList, $key))) { 2931 // If the setting is an array or not present in the menu-array, MOD_MENU, then the default value is inserted. 2932 if (is_array($settings[$key]) || !isset($MOD_MENU[$key][$settings[$key]])) { 2933 $settings[$key] = (string)key($var); 2934 $changed = 1; 2935 } 2936 } 2937 // Sets default values (only strings/checkboxes, not menus) 2938 if ($setDefaultList && !is_array($var)) { 2939 if (GeneralUtility::inList($setDefaultList, $key) && !isset($settings[$key])) { 2940 $settings[$key] = (string)$var; 2941 } 2942 } 2943 } 2944 } else { 2945 throw new \RuntimeException('No menu', 1568119229); 2946 } 2947 if ($changed) { 2948 $beUser->pushModuleData($modName, $settings); 2949 } 2950 return $settings; 2951 } 2952 throw new \RuntimeException('Wrong module name "' . $modName . '"', 1568119221); 2953 } 2954 2955 /******************************************* 2956 * 2957 * Core 2958 * 2959 *******************************************/ 2960 /** 2961 * Unlock or Lock a record from $table with $uid 2962 * If $table and $uid is not set, then all locking for the current BE_USER is removed! 2963 * 2964 * @param string $table Table name 2965 * @param int $uid Record uid 2966 * @param int $pid Record pid 2967 * @internal 2968 */ 2969 public static function lockRecords($table = '', $uid = 0, $pid = 0) 2970 { 2971 $beUser = static::getBackendUserAuthentication(); 2972 if (isset($beUser->user['uid'])) { 2973 $userId = (int)$beUser->user['uid']; 2974 if ($table && $uid) { 2975 $fieldsValues = [ 2976 'userid' => $userId, 2977 'feuserid' => 0, 2978 'tstamp' => $GLOBALS['EXEC_TIME'], 2979 'record_table' => $table, 2980 'record_uid' => $uid, 2981 'username' => $beUser->user['username'], 2982 'record_pid' => $pid 2983 ]; 2984 GeneralUtility::makeInstance(ConnectionPool::class) 2985 ->getConnectionForTable('sys_lockedrecords') 2986 ->insert( 2987 'sys_lockedrecords', 2988 $fieldsValues 2989 ); 2990 } else { 2991 GeneralUtility::makeInstance(ConnectionPool::class) 2992 ->getConnectionForTable('sys_lockedrecords') 2993 ->delete( 2994 'sys_lockedrecords', 2995 ['userid' => (int)$userId] 2996 ); 2997 } 2998 } 2999 } 3000 3001 /** 3002 * Returns information about whether the record from table, $table, with uid, $uid is currently locked 3003 * (edited by another user - which should issue a warning). 3004 * Notice: Locking is not strictly carried out since locking is abandoned when other backend scripts 3005 * are activated - which means that a user CAN have a record "open" without having it locked. 3006 * So this just serves as a warning that counts well in 90% of the cases, which should be sufficient. 3007 * 3008 * @param string $table Table name 3009 * @param int $uid Record uid 3010 * @return array|bool 3011 * @internal 3012 */ 3013 public static function isRecordLocked($table, $uid) 3014 { 3015 $runtimeCache = self::getRuntimeCache(); 3016 $cacheId = 'backend-recordLocked'; 3017 $recordLockedCache = $runtimeCache->get($cacheId); 3018 if ($recordLockedCache !== false) { 3019 $lockedRecords = $recordLockedCache; 3020 } else { 3021 $lockedRecords = []; 3022 3023 $queryBuilder = static::getQueryBuilderForTable('sys_lockedrecords'); 3024 $result = $queryBuilder 3025 ->select('*') 3026 ->from('sys_lockedrecords') 3027 ->where( 3028 $queryBuilder->expr()->neq( 3029 'sys_lockedrecords.userid', 3030 $queryBuilder->createNamedParameter( 3031 static::getBackendUserAuthentication()->user['uid'], 3032 \PDO::PARAM_INT 3033 ) 3034 ), 3035 $queryBuilder->expr()->gt( 3036 'sys_lockedrecords.tstamp', 3037 $queryBuilder->createNamedParameter( 3038 $GLOBALS['EXEC_TIME'] - 2 * 3600, 3039 \PDO::PARAM_INT 3040 ) 3041 ) 3042 ) 3043 ->execute(); 3044 3045 $lang = static::getLanguageService(); 3046 while ($row = $result->fetch()) { 3047 // Get the type of the user that locked this record: 3048 if ($row['userid']) { 3049 $userTypeLabel = 'beUser'; 3050 } elseif ($row['feuserid']) { 3051 $userTypeLabel = 'feUser'; 3052 } else { 3053 $userTypeLabel = 'user'; 3054 } 3055 $userType = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.' . $userTypeLabel); 3056 // Get the username (if available): 3057 if ($row['username']) { 3058 $userName = $row['username']; 3059 } else { 3060 $userName = $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.unknownUser'); 3061 } 3062 $lockedRecords[$row['record_table'] . ':' . $row['record_uid']] = $row; 3063 $lockedRecords[$row['record_table'] . ':' . $row['record_uid']]['msg'] = sprintf( 3064 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser'), 3065 $userType, 3066 $userName, 3067 self::calcAge( 3068 $GLOBALS['EXEC_TIME'] - $row['tstamp'], 3069 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears') 3070 ) 3071 ); 3072 if ($row['record_pid'] && !isset($lockedRecords[$row['record_table'] . ':' . $row['record_pid']])) { 3073 $lockedRecords['pages:' . $row['record_pid']]['msg'] = sprintf( 3074 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.lockedRecordUser_content'), 3075 $userType, 3076 $userName, 3077 self::calcAge( 3078 $GLOBALS['EXEC_TIME'] - $row['tstamp'], 3079 $lang->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.minutesHoursDaysYears') 3080 ) 3081 ); 3082 } 3083 } 3084 $runtimeCache->set($cacheId, $lockedRecords); 3085 } 3086 3087 return $lockedRecords[$table . ':' . $uid] ?? false; 3088 } 3089 3090 /** 3091 * Returns TSConfig for the TCEFORM object in Page TSconfig. 3092 * Used in TCEFORMs 3093 * 3094 * @param string $table Table name present in TCA 3095 * @param array $row Row from table 3096 * @return array 3097 */ 3098 public static function getTCEFORM_TSconfig($table, $row) 3099 { 3100 self::fixVersioningPid($table, $row); 3101 $res = []; 3102 // Get main config for the table 3103 [$TScID, $cPid] = self::getTSCpid($table, $row['uid'], $row['pid']); 3104 if ($TScID >= 0) { 3105 $tsConfig = static::getPagesTSconfig($TScID)['TCEFORM.'][$table . '.'] ?? []; 3106 $typeVal = self::getTCAtypeValue($table, $row); 3107 foreach ($tsConfig as $key => $val) { 3108 if (is_array($val)) { 3109 $fieldN = substr($key, 0, -1); 3110 $res[$fieldN] = $val; 3111 unset($res[$fieldN]['types.']); 3112 if ((string)$typeVal !== '' && is_array($val['types.'][$typeVal . '.'])) { 3113 ArrayUtility::mergeRecursiveWithOverrule($res[$fieldN], $val['types.'][$typeVal . '.']); 3114 } 3115 } 3116 } 3117 } 3118 $res['_CURRENT_PID'] = $cPid; 3119 $res['_THIS_UID'] = $row['uid']; 3120 // So the row will be passed to foreign_table_where_query() 3121 $res['_THIS_ROW'] = $row; 3122 return $res; 3123 } 3124 3125 /** 3126 * Find the real PID of the record (with $uid from $table). 3127 * This MAY be impossible if the pid is set as a reference to the former record or a page (if two records are created at one time). 3128 * NOTICE: Make sure that the input PID is never negative because the record was an offline version! 3129 * Therefore, you should always use BackendUtility::fixVersioningPid($table,$row); on the data you input before calling this function! 3130 * 3131 * @param string $table Table name 3132 * @param int $uid Record uid 3133 * @param int|string $pid Record pid, could be negative then pointing to a record from same table whose pid to find and return 3134 * @return int 3135 * @internal 3136 * @see \TYPO3\CMS\Core\DataHandling\DataHandler::copyRecord() 3137 * @see \TYPO3\CMS\Backend\Utility\BackendUtility::getTSCpid() 3138 */ 3139 public static function getTSconfig_pidValue($table, $uid, $pid) 3140 { 3141 // If pid is an integer this takes precedence in our lookup. 3142 if (MathUtility::canBeInterpretedAsInteger($pid)) { 3143 $thePidValue = (int)$pid; 3144 // If ref to another record, look that record up. 3145 if ($thePidValue < 0) { 3146 $pidRec = self::getRecord($table, abs($thePidValue), 'pid'); 3147 $thePidValue = is_array($pidRec) ? $pidRec['pid'] : -2; 3148 } 3149 } else { 3150 // Try to fetch the record pid from uid. If the uid is 'NEW...' then this will of course return nothing 3151 $rr = self::getRecord($table, $uid); 3152 $thePidValue = null; 3153 if (is_array($rr)) { 3154 // First check if the t3ver_oid value is greater 0, which means 3155 // it is a workspace element. If so, get the "real" record: 3156 if ((int)($rr['t3ver_oid'] ?? 0) > 0) { 3157 $rr = self::getRecord($table, $rr['t3ver_oid'], 'pid'); 3158 if (is_array($rr)) { 3159 $thePidValue = $rr['pid']; 3160 } 3161 } else { 3162 // Returning the "pid" of the record 3163 $thePidValue = $rr['pid']; 3164 } 3165 } 3166 if (!$thePidValue) { 3167 // Returns -1 if the record with this pid was not found. 3168 $thePidValue = -1; 3169 } 3170 } 3171 return $thePidValue; 3172 } 3173 3174 /** 3175 * Return the real pid of a record and caches the result. 3176 * The non-cached method needs database queries to do the job, so this method 3177 * can be used if code sometimes calls the same record multiple times to save 3178 * some queries. This should not be done if the calling code may change the 3179 * same record meanwhile. 3180 * 3181 * @param string $table Tablename 3182 * @param string $uid UID value 3183 * @param string $pid PID value 3184 * @return array Array of two integers; first is the real PID of a record, second is the PID value for TSconfig. 3185 */ 3186 public static function getTSCpidCached($table, $uid, $pid) 3187 { 3188 $runtimeCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); 3189 $firstLevelCache = $runtimeCache->get('backendUtilityTscPidCached') ?: []; 3190 $key = $table . ':' . $uid . ':' . $pid; 3191 if (!isset($firstLevelCache[$key])) { 3192 $firstLevelCache[$key] = static::getTSCpid($table, (int)$uid, (int)$pid); 3193 $runtimeCache->set('backendUtilityTscPidCached', $firstLevelCache); 3194 } 3195 return $firstLevelCache[$key]; 3196 } 3197 3198 /** 3199 * Returns the REAL pid of the record, if possible. If both $uid and $pid is strings, then pid=-1 is returned as an error indication. 3200 * 3201 * @param string $table Table name 3202 * @param int $uid Record uid 3203 * @param int|string $pid Record pid 3204 * @return array Array of two integers; first is the REAL PID of a record and if its a new record negative values are resolved to the true PID, 3205 * second value is the PID value for TSconfig (uid if table is pages, otherwise the pid) 3206 * @internal 3207 * @see \TYPO3\CMS\Core\DataHandling\DataHandler::setHistory() 3208 * @see \TYPO3\CMS\Core\DataHandling\DataHandler::process_datamap() 3209 */ 3210 public static function getTSCpid($table, $uid, $pid) 3211 { 3212 // If pid is negative (referring to another record) the pid of the other record is fetched and returned. 3213 $cPid = self::getTSconfig_pidValue($table, $uid, $pid); 3214 // $TScID is the id of $table = pages, else it's the pid of the record. 3215 $TScID = $table === 'pages' && MathUtility::canBeInterpretedAsInteger($uid) ? $uid : $cPid; 3216 return [$TScID, $cPid]; 3217 } 3218 3219 /** 3220 * Returns soft-reference parser for the softRef processing type 3221 * Usage: $softRefObj = BackendUtility::softRefParserObj('[parser key]'); 3222 * 3223 * @param string $spKey softRef parser key 3224 * @return mixed If available, returns Soft link parser object, otherwise false. 3225 * @internal should only be used from within TYPO3 Core 3226 */ 3227 public static function softRefParserObj($spKey) 3228 { 3229 $className = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['GLOBAL']['softRefParser'][$spKey] ?? false; 3230 if ($className) { 3231 return GeneralUtility::makeInstance($className); 3232 } 3233 return false; 3234 } 3235 3236 /** 3237 * Gets an instance of the runtime cache. 3238 * 3239 * @return FrontendInterface 3240 */ 3241 protected static function getRuntimeCache() 3242 { 3243 return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime'); 3244 } 3245 3246 /** 3247 * Returns array of soft parser references 3248 * 3249 * @param string $parserList softRef parser list 3250 * @return array|bool Array where the parser key is the key and the value is the parameter string, FALSE if no parsers were found 3251 * @throws \InvalidArgumentException 3252 * @internal should only be used from within TYPO3 Core 3253 */ 3254 public static function explodeSoftRefParserList($parserList) 3255 { 3256 // Return immediately if list is blank: 3257 if ((string)$parserList === '') { 3258 return false; 3259 } 3260 3261 $runtimeCache = self::getRuntimeCache(); 3262 $cacheId = 'backend-softRefList-' . md5($parserList); 3263 $parserListCache = $runtimeCache->get($cacheId); 3264 if ($parserListCache !== false) { 3265 return $parserListCache; 3266 } 3267 3268 // Otherwise parse the list: 3269 $keyList = GeneralUtility::trimExplode(',', $parserList, true); 3270 $output = []; 3271 foreach ($keyList as $val) { 3272 $reg = []; 3273 if (preg_match('/^([[:alnum:]_-]+)\\[(.*)\\]$/', $val, $reg)) { 3274 $output[$reg[1]] = GeneralUtility::trimExplode(';', $reg[2], true); 3275 } else { 3276 $output[$val] = ''; 3277 } 3278 } 3279 $runtimeCache->set($cacheId, $output); 3280 return $output; 3281 } 3282 3283 /** 3284 * Returns TRUE if $modName is set and is found as a main- or submodule in $TBE_MODULES array 3285 * 3286 * @param string $modName Module name 3287 * @return bool 3288 */ 3289 public static function isModuleSetInTBE_MODULES($modName) 3290 { 3291 $loaded = []; 3292 foreach ($GLOBALS['TBE_MODULES'] as $mkey => $list) { 3293 $loaded[$mkey] = 1; 3294 if (!is_array($list) && trim($list)) { 3295 $subList = GeneralUtility::trimExplode(',', $list, true); 3296 foreach ($subList as $skey) { 3297 $loaded[$mkey . '_' . $skey] = 1; 3298 } 3299 } 3300 } 3301 return $modName && isset($loaded[$modName]); 3302 } 3303 3304 /** 3305 * Counting references to a record/file 3306 * 3307 * @param string $table Table name (or "_FILE" if its a file) 3308 * @param string $ref Reference: If table, then int-uid, if _FILE, then file reference (relative to Environment::getPublicPath()) 3309 * @param string $msg Message with %s, eg. "There were %s records pointing to this file! 3310 * @param string|int|null $count Reference count 3311 * @return string|int Output string (or int count value if no msg string specified) 3312 */ 3313 public static function referenceCount($table, $ref, $msg = '', $count = null) 3314 { 3315 if ($count === null) { 3316 3317 // Build base query 3318 $queryBuilder = static::getQueryBuilderForTable('sys_refindex'); 3319 $queryBuilder 3320 ->count('*') 3321 ->from('sys_refindex') 3322 ->where( 3323 $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter($table, \PDO::PARAM_STR)), 3324 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)) 3325 ); 3326 3327 // Look up the path: 3328 if ($table === '_FILE') { 3329 if (!GeneralUtility::isFirstPartOfStr($ref, Environment::getPublicPath())) { 3330 return ''; 3331 } 3332 3333 $ref = PathUtility::stripPathSitePrefix($ref); 3334 $queryBuilder->andWhere( 3335 $queryBuilder->expr()->eq('ref_string', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_STR)) 3336 ); 3337 } else { 3338 $queryBuilder->andWhere( 3339 $queryBuilder->expr()->eq('ref_uid', $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT)) 3340 ); 3341 if ($table === 'sys_file') { 3342 $queryBuilder->andWhere($queryBuilder->expr()->neq('tablename', $queryBuilder->quote('sys_file_metadata'))); 3343 } 3344 } 3345 3346 $count = $queryBuilder->execute()->fetchColumn(0); 3347 } 3348 3349 if ($count) { 3350 return $msg ? sprintf($msg, $count) : $count; 3351 } 3352 return $msg ? '' : 0; 3353 } 3354 3355 /** 3356 * Counting translations of records 3357 * 3358 * @param string $table Table name 3359 * @param string $ref Reference: the record's uid 3360 * @param string $msg Message with %s, eg. "This record has %s translation(s) which will be deleted, too! 3361 * @return string Output string (or int count value if no msg string specified) 3362 */ 3363 public static function translationCount($table, $ref, $msg = '') 3364 { 3365 $count = null; 3366 if ($GLOBALS['TCA'][$table]['ctrl']['languageField'] 3367 && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'] 3368 ) { 3369 $queryBuilder = static::getQueryBuilderForTable($table); 3370 $queryBuilder->getRestrictions() 3371 ->removeAll() 3372 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 3373 3374 $count = (int)$queryBuilder 3375 ->count('*') 3376 ->from($table) 3377 ->where( 3378 $queryBuilder->expr()->eq( 3379 $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'], 3380 $queryBuilder->createNamedParameter($ref, \PDO::PARAM_INT) 3381 ), 3382 $queryBuilder->expr()->neq( 3383 $GLOBALS['TCA'][$table]['ctrl']['languageField'], 3384 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 3385 ) 3386 ) 3387 ->execute() 3388 ->fetchColumn(0); 3389 } 3390 3391 if ($count && $msg) { 3392 return sprintf($msg, $count); 3393 } 3394 3395 if ($count) { 3396 return $msg ? sprintf($msg, $count) : $count; 3397 } 3398 return $msg ? '' : 0; 3399 } 3400 3401 /******************************************* 3402 * 3403 * Workspaces / Versioning 3404 * 3405 *******************************************/ 3406 /** 3407 * Select all versions of a record, ordered by latest created version (uid DESC) 3408 * 3409 * @param string $table Table name to select from 3410 * @param int $uid Record uid for which to find versions. 3411 * @param string $fields Field list to select 3412 * @param int|null $workspace Search in workspace ID and Live WS, if 0 search only in LiveWS, if NULL search in all WS. 3413 * @param bool $includeDeletedRecords If set, deleted-flagged versions are included! (Only for clean-up script!) 3414 * @param array $row The current record 3415 * @return array|null Array of versions of table/uid 3416 * @internal should only be used from within TYPO3 Core 3417 */ 3418 public static function selectVersionsOfRecord( 3419 $table, 3420 $uid, 3421 $fields = '*', 3422 $workspace = 0, 3423 $includeDeletedRecords = false, 3424 $row = null 3425 ) { 3426 $realPid = 0; 3427 $outputRows = []; 3428 if (static::isTableWorkspaceEnabled($table)) { 3429 if (is_array($row) && !$includeDeletedRecords) { 3430 $row['_CURRENT_VERSION'] = true; 3431 $realPid = $row['pid']; 3432 $outputRows[] = $row; 3433 } else { 3434 // Select UID version: 3435 $row = self::getRecord($table, $uid, $fields, '', !$includeDeletedRecords); 3436 // Add rows to output array: 3437 if ($row) { 3438 $row['_CURRENT_VERSION'] = true; 3439 $realPid = $row['pid']; 3440 $outputRows[] = $row; 3441 } 3442 } 3443 3444 $queryBuilder = static::getQueryBuilderForTable($table); 3445 $queryBuilder->getRestrictions()->removeAll(); 3446 3447 // build fields to select 3448 $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields)); 3449 3450 $queryBuilder 3451 ->from($table) 3452 ->where( 3453 $queryBuilder->expr()->neq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)), 3454 $queryBuilder->expr()->eq('t3ver_oid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) 3455 ) 3456 ->orderBy('uid', 'DESC'); 3457 3458 if (!$includeDeletedRecords) { 3459 $queryBuilder->getRestrictions()->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 3460 } 3461 3462 if ($workspace === 0) { 3463 // Only in Live WS 3464 $queryBuilder->andWhere( 3465 $queryBuilder->expr()->eq( 3466 't3ver_wsid', 3467 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 3468 ) 3469 ); 3470 } elseif ($workspace !== null) { 3471 // In Live WS and Workspace with given ID 3472 $queryBuilder->andWhere( 3473 $queryBuilder->expr()->in( 3474 't3ver_wsid', 3475 $queryBuilder->createNamedParameter([0, (int)$workspace], Connection::PARAM_INT_ARRAY) 3476 ) 3477 ); 3478 } 3479 3480 $rows = $queryBuilder->execute()->fetchAll(); 3481 3482 // Add rows to output array: 3483 if (is_array($rows)) { 3484 $outputRows = array_merge($outputRows, $rows); 3485 } 3486 // Set real-pid: 3487 foreach ($outputRows as $idx => $oRow) { 3488 $outputRows[$idx]['_REAL_PID'] = $realPid; 3489 } 3490 return $outputRows; 3491 } 3492 return null; 3493 } 3494 3495 /** 3496 * Find page-tree PID for versionized record 3497 * Will look if the "pid" value of the input record is -1 and if the table supports versioning - if so, 3498 * it will translate the -1 PID into the PID of the original record 3499 * Used whenever you are tracking something back, like making the root line. 3500 * Will only translate if the workspace of the input record matches that of the current user (unless flag set) 3501 * Principle; Record offline! => Find online? 3502 * 3503 * If the record had its pid corrected to the online versions pid, then "_ORIG_pid" is set 3504 * to the original pid value (-1 of course). The field "_ORIG_pid" is used by various other functions 3505 * to detect if a record was in fact in a versionized branch. 3506 * 3507 * @param string $table Table name 3508 * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid", "t3ver_state" and "t3ver_wsid" is nice and will save you a DB query. 3509 * @param bool $ignoreWorkspaceMatch Ignore workspace match 3510 * @see PageRepository::fixVersioningPid() 3511 * @internal should only be used from within TYPO3 Core 3512 */ 3513 public static function fixVersioningPid($table, &$rr, $ignoreWorkspaceMatch = false) 3514 { 3515 if (!ExtensionManagementUtility::isLoaded('workspaces')) { 3516 return; 3517 } 3518 if (!static::isTableWorkspaceEnabled($table)) { 3519 return; 3520 } 3521 // Check that the input record is an offline version from a table that supports versioning 3522 if (!is_array($rr)) { 3523 return; 3524 } 3525 $incomingPid = $rr['pid'] ?? null; 3526 // Check values for t3ver_oid and t3ver_wsid: 3527 if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid']) && isset($rr['t3ver_state'])) { 3528 // If "t3ver_oid" is already a field, just set this: 3529 $oid = $rr['t3ver_oid']; 3530 $workspaceId = (int)$rr['t3ver_wsid']; 3531 $versionState = (int)$rr['t3ver_state']; 3532 } else { 3533 $oid = 0; 3534 $workspaceId = 0; 3535 $versionState = 0; 3536 // Otherwise we have to expect "uid" to be in the record and look up based on this: 3537 $newPidRec = self::getRecord($table, $rr['uid'], 'pid,t3ver_oid,t3ver_wsid,t3ver_state'); 3538 if (is_array($newPidRec)) { 3539 $incomingPid = $newPidRec['pid']; 3540 $oid = $newPidRec['t3ver_oid']; 3541 $workspaceId = $newPidRec['t3ver_wsid']; 3542 $versionState = $newPidRec['t3ver_state']; 3543 } 3544 } 3545 if ($oid && ($ignoreWorkspaceMatch || (static::getBackendUserAuthentication() instanceof BackendUserAuthentication && $workspaceId === (int)static::getBackendUserAuthentication()->workspace))) { 3546 if ($incomingPid === null) { 3547 // This can be removed, as this is the same for all versioned records 3548 $onlineRecord = self::getRecord($table, $oid, 'pid'); 3549 if (is_array($onlineRecord)) { 3550 $rr['_ORIG_pid'] = $onlineRecord['pid']; 3551 $rr['pid'] = $onlineRecord['pid']; 3552 } 3553 } else { 3554 // This can be removed, as this is the same for all versioned records (clearly obvious here) 3555 $rr['_ORIG_pid'] = $incomingPid; 3556 $rr['pid'] = $incomingPid; 3557 } 3558 // Use moved PID in case of move pointer 3559 if ($versionState === VersionState::MOVE_POINTER) { 3560 if ($incomingPid !== null) { 3561 $movedPageIdInWorkspace = $incomingPid; 3562 } else { 3563 $versionedMovePointer = self::getRecord($table, $rr['uid'], 'pid'); 3564 $movedPageIdInWorkspace = $versionedMovePointer['pid']; 3565 } 3566 $rr['_ORIG_pid'] = $incomingPid; 3567 $rr['pid'] = $movedPageIdInWorkspace; 3568 } 3569 } 3570 } 3571 3572 /** 3573 * Workspace Preview Overlay 3574 * Generally ALWAYS used when records are selected based on uid or pid. 3575 * If records are selected on other fields than uid or pid (eg. "email = ....") 3576 * then usage might produce undesired results and that should be evaluated on individual basis. 3577 * Principle; Record online! => Find offline? 3578 * Recently, this function has been modified so it MAY set $row to FALSE. 3579 * This happens if a version overlay with the move-id pointer is found in which case we would like a backend preview. 3580 * In other words, you should check if the input record is still an array afterwards when using this function. 3581 * 3582 * @param string $table Table name 3583 * @param array $row Record array passed by reference. As minimum, the "uid" and "pid" fields must exist! Fake fields cannot exist since the fields in the array is used as field names in the SQL look up. It would be nice to have fields like "t3ver_state" and "t3ver_mode_id" as well to avoid a new lookup inside movePlhOL(). 3584 * @param int $wsid Workspace ID, if not specified will use static::getBackendUserAuthentication()->workspace 3585 * @param bool $unsetMovePointers If TRUE the function does not return a "pointer" row for moved records in a workspace 3586 * @see fixVersioningPid() 3587 */ 3588 public static function workspaceOL($table, &$row, $wsid = -99, $unsetMovePointers = false) 3589 { 3590 if (!ExtensionManagementUtility::isLoaded('workspaces')) { 3591 return; 3592 } 3593 // If this is FALSE the placeholder is shown raw in the backend. 3594 // I don't know if this move can be useful for users to toggle. Technically it can help debugging. 3595 $previewMovePlaceholders = true; 3596 // Initialize workspace ID 3597 if ($wsid == -99 && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) { 3598 $wsid = static::getBackendUserAuthentication()->workspace; 3599 } 3600 // Check if workspace is different from zero and record is set: 3601 if ($wsid !== 0 && is_array($row)) { 3602 // Check if input record is a move-placeholder and if so, find the pointed-to live record: 3603 $movePldSwap = null; 3604 $orig_uid = 0; 3605 $orig_pid = 0; 3606 if ($previewMovePlaceholders) { 3607 $orig_uid = $row['uid']; 3608 $orig_pid = $row['pid']; 3609 $movePldSwap = self::movePlhOL($table, $row); 3610 } 3611 $wsAlt = self::getWorkspaceVersionOfRecord( 3612 $wsid, 3613 $table, 3614 $row['uid'], 3615 implode(',', static::purgeComputedPropertyNames(array_keys($row))) 3616 ); 3617 // If version was found, swap the default record with that one. 3618 if (is_array($wsAlt)) { 3619 // Check if this is in move-state: 3620 if ($previewMovePlaceholders && !$movePldSwap && static::isTableWorkspaceEnabled($table) && $unsetMovePointers) { 3621 // Only for WS ver 2... (moving) 3622 // If t3ver_state is not found, then find it... (but we like best if it is here...) 3623 if (!isset($wsAlt['t3ver_state'])) { 3624 $stateRec = self::getRecord($table, $wsAlt['uid'], 't3ver_state'); 3625 $versionState = VersionState::cast($stateRec['t3ver_state']); 3626 } else { 3627 $versionState = VersionState::cast($wsAlt['t3ver_state']); 3628 } 3629 if ($versionState->equals(VersionState::MOVE_POINTER)) { 3630 // @todo Same problem as frontend in versionOL(). See TODO point there. 3631 $row = false; 3632 return; 3633 } 3634 } 3635 // Always correct PID from -1 to what it should be 3636 if (isset($wsAlt['pid'])) { 3637 // Keep the old (-1) - indicates it was a version. 3638 $wsAlt['_ORIG_pid'] = $wsAlt['pid']; 3639 // Set in the online versions PID. 3640 $wsAlt['pid'] = $row['pid']; 3641 } 3642 // For versions of single elements or page+content, swap UID and PID 3643 $wsAlt['_ORIG_uid'] = $wsAlt['uid']; 3644 $wsAlt['uid'] = $row['uid']; 3645 // Backend css class: 3646 $wsAlt['_CSSCLASS'] = 'ver-element'; 3647 // Changing input record to the workspace version alternative: 3648 $row = $wsAlt; 3649 } 3650 // If the original record was a move placeholder, the uid and pid of that is preserved here: 3651 if ($movePldSwap) { 3652 $row['_MOVE_PLH'] = true; 3653 $row['_MOVE_PLH_uid'] = $orig_uid; 3654 $row['_MOVE_PLH_pid'] = $orig_pid; 3655 // For display; To make the icon right for the placeholder vs. the original 3656 $row['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER); 3657 } 3658 } 3659 } 3660 3661 /** 3662 * Checks if record is a move-placeholder (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so 3663 * it will set $row to be the pointed-to live record (and return TRUE) 3664 * 3665 * @param string $table Table name 3666 * @param array $row Row (passed by reference) - must be online record! 3667 * @return bool TRUE if overlay is made. 3668 * @see PageRepository::movePlhOl() 3669 * @internal should only be used from within TYPO3 Core 3670 */ 3671 public static function movePlhOL($table, &$row) 3672 { 3673 if (static::isTableWorkspaceEnabled($table)) { 3674 // If t3ver_move_id or t3ver_state is not found, then find it... (but we like best if it is here...) 3675 if (!isset($row['t3ver_move_id']) || !isset($row['t3ver_state'])) { 3676 $moveIDRec = self::getRecord($table, $row['uid'], 't3ver_move_id, t3ver_state'); 3677 $moveID = $moveIDRec['t3ver_move_id']; 3678 $versionState = VersionState::cast($moveIDRec['t3ver_state']); 3679 } else { 3680 $moveID = $row['t3ver_move_id']; 3681 $versionState = VersionState::cast($row['t3ver_state']); 3682 } 3683 // Find pointed-to record. 3684 if ($versionState->equals(VersionState::MOVE_PLACEHOLDER) && $moveID) { 3685 if ($origRow = self::getRecord( 3686 $table, 3687 $moveID, 3688 implode(',', static::purgeComputedPropertyNames(array_keys($row))) 3689 )) { 3690 $row = $origRow; 3691 return true; 3692 } 3693 } 3694 } 3695 return false; 3696 } 3697 3698 /** 3699 * Select the workspace version of a record, if exists 3700 * 3701 * @param int $workspace Workspace ID 3702 * @param string $table Table name to select from 3703 * @param int $uid Record uid for which to find workspace version. 3704 * @param string $fields Field list to select 3705 * @return array|bool If found, return record, otherwise false 3706 */ 3707 public static function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*') 3708 { 3709 if (ExtensionManagementUtility::isLoaded('workspaces')) { 3710 if ($workspace !== 0 && self::isTableWorkspaceEnabled($table)) { 3711 3712 // Select workspace version of record: 3713 $queryBuilder = static::getQueryBuilderForTable($table); 3714 $queryBuilder->getRestrictions() 3715 ->removeAll() 3716 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 3717 3718 // build fields to select 3719 $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields)); 3720 3721 $row = $queryBuilder 3722 ->from($table) 3723 ->where( 3724 $queryBuilder->expr()->eq( 3725 't3ver_oid', 3726 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 3727 ), 3728 $queryBuilder->expr()->eq( 3729 't3ver_wsid', 3730 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT) 3731 ) 3732 ) 3733 ->execute() 3734 ->fetch(); 3735 3736 return $row; 3737 } 3738 } 3739 return false; 3740 } 3741 3742 /** 3743 * Returns live version of record 3744 * 3745 * @param string $table Table name 3746 * @param int $uid Record UID of draft, offline version 3747 * @param string $fields Field list, default is * 3748 * @return array|null If found, the record, otherwise NULL 3749 */ 3750 public static function getLiveVersionOfRecord($table, $uid, $fields = '*') 3751 { 3752 $liveVersionId = self::getLiveVersionIdOfRecord($table, $uid); 3753 if ($liveVersionId !== null) { 3754 return self::getRecord($table, $liveVersionId, $fields); 3755 } 3756 return null; 3757 } 3758 3759 /** 3760 * Gets the id of the live version of a record. 3761 * 3762 * @param string $table Name of the table 3763 * @param int $uid Uid of the offline/draft record 3764 * @return int|null The id of the live version of the record (or NULL if nothing was found) 3765 * @internal should only be used from within TYPO3 Core 3766 */ 3767 public static function getLiveVersionIdOfRecord($table, $uid) 3768 { 3769 if (!ExtensionManagementUtility::isLoaded('workspaces')) { 3770 return null; 3771 } 3772 $liveVersionId = null; 3773 if (self::isTableWorkspaceEnabled($table)) { 3774 $currentRecord = self::getRecord($table, $uid, 'pid,t3ver_oid'); 3775 if (is_array($currentRecord) && (int)$currentRecord['t3ver_oid'] > 0) { 3776 $liveVersionId = $currentRecord['t3ver_oid']; 3777 } 3778 } 3779 return $liveVersionId; 3780 } 3781 3782 /** 3783 * Will return where clause de-selecting new(/deleted)-versions from other workspaces. 3784 * If in live-workspace, don't show "MOVE-TO-PLACEHOLDERS" records if versioningWS is 2 (allows moving) 3785 * 3786 * @param string $table Table name 3787 * @return string Where clause if applicable. 3788 * @internal should only be used from within TYPO3 Core 3789 */ 3790 public static function versioningPlaceholderClause($table) 3791 { 3792 if (static::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) { 3793 $currentWorkspace = (int)static::getBackendUserAuthentication()->workspace; 3794 return ' AND (' . $table . '.t3ver_state <= ' . new VersionState(VersionState::DEFAULT_STATE) . ' OR ' . $table . '.t3ver_wsid = ' . $currentWorkspace . ')'; 3795 } 3796 return ''; 3797 } 3798 3799 /** 3800 * Get additional where clause to select records of a specific workspace (includes live as well). 3801 * 3802 * @param string $table Table name 3803 * @param int $workspaceId Workspace ID 3804 * @return string Workspace where clause 3805 * @internal should only be used from within TYPO3 Core 3806 */ 3807 public static function getWorkspaceWhereClause($table, $workspaceId = null) 3808 { 3809 $whereClause = ''; 3810 if (self::isTableWorkspaceEnabled($table) && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) { 3811 if ($workspaceId === null) { 3812 $workspaceId = static::getBackendUserAuthentication()->workspace; 3813 } 3814 $workspaceId = (int)$workspaceId; 3815 $comparison = $workspaceId === 0 ? '=' : '>'; 3816 $whereClause = ' AND ' . $table . '.t3ver_wsid=' . $workspaceId . ' AND ' . $table . '.t3ver_oid' . $comparison . '0'; 3817 } 3818 return $whereClause; 3819 } 3820 3821 /** 3822 * Performs mapping of new uids to new versions UID in case of import inside a workspace. 3823 * 3824 * @param string $table Table name 3825 * @param int $uid Record uid (of live record placeholder) 3826 * @return int Uid of offline version if any, otherwise live uid. 3827 * @internal should only be used from within TYPO3 Core 3828 */ 3829 public static function wsMapId($table, $uid) 3830 { 3831 $wsRec = null; 3832 if (static::getBackendUserAuthentication() instanceof BackendUserAuthentication) { 3833 $wsRec = self::getWorkspaceVersionOfRecord( 3834 static::getBackendUserAuthentication()->workspace, 3835 $table, 3836 $uid, 3837 'uid' 3838 ); 3839 } 3840 return is_array($wsRec) ? $wsRec['uid'] : $uid; 3841 } 3842 3843 /** 3844 * Returns move placeholder of online (live) version 3845 * 3846 * @param string $table Table name 3847 * @param int $uid Record UID of online version 3848 * @param string $fields Field list, default is * 3849 * @param int|null $workspace The workspace to be used 3850 * @return array|bool If found, the record, otherwise false 3851 * @internal should only be used from within TYPO3 Core 3852 */ 3853 public static function getMovePlaceholder($table, $uid, $fields = '*', $workspace = null) 3854 { 3855 if ($workspace === null && static::getBackendUserAuthentication() instanceof BackendUserAuthentication) { 3856 $workspace = static::getBackendUserAuthentication()->workspace; 3857 } 3858 if ((int)$workspace !== 0 && static::isTableWorkspaceEnabled($table)) { 3859 // Select workspace version of record: 3860 $queryBuilder = static::getQueryBuilderForTable($table); 3861 $queryBuilder->getRestrictions() 3862 ->removeAll() 3863 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 3864 3865 $row = $queryBuilder 3866 ->select(...GeneralUtility::trimExplode(',', $fields, true)) 3867 ->from($table) 3868 ->where( 3869 $queryBuilder->expr()->eq( 3870 't3ver_state', 3871 $queryBuilder->createNamedParameter( 3872 (string)new VersionState(VersionState::MOVE_PLACEHOLDER), 3873 \PDO::PARAM_INT 3874 ) 3875 ), 3876 $queryBuilder->expr()->eq( 3877 't3ver_move_id', 3878 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 3879 ), 3880 $queryBuilder->expr()->eq( 3881 't3ver_wsid', 3882 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT) 3883 ) 3884 ) 3885 ->execute() 3886 ->fetch(); 3887 3888 return $row ?: false; 3889 } 3890 return false; 3891 } 3892 3893 /******************************************* 3894 * 3895 * Miscellaneous 3896 * 3897 *******************************************/ 3898 /** 3899 * Prints TYPO3 Copyright notice for About Modules etc. modules. 3900 * 3901 * Warning: 3902 * DO NOT prevent this notice from being shown in ANY WAY. 3903 * According to the GPL license an interactive application must show such a notice on start-up ('If the program is interactive, make it output a short notice... ' - see GPL.txt) 3904 * Therefore preventing this notice from being properly shown is a violation of the license, regardless of whether you remove it or use a stylesheet to obstruct the display. 3905 * 3906 * @return string Text/Image (HTML) for copyright notice. 3907 * @deprecated since TYPO3 v10.2, will be removed in TYPO3 v11.0 3908 */ 3909 public static function TYPO3_copyRightNotice() 3910 { 3911 trigger_error('BackendUtility::TYPO3_copyRightNotice() will be removed in TYPO3 v11.0, use the Typo3Information PHP class instead.', E_USER_DEPRECATED); 3912 $copyrightGenerator = GeneralUtility::makeInstance(Typo3Information::class, static::getLanguageService()); 3913 return $copyrightGenerator->getCopyrightNotice(); 3914 } 3915 3916 /** 3917 * Creates ADMCMD parameters for the "viewpage" extension / frontend 3918 * 3919 * @param array $pageInfo Page record 3920 * @param \TYPO3\CMS\Core\Context\Context $context 3921 * @return string Query-parameters 3922 * @internal 3923 */ 3924 public static function ADMCMD_previewCmds($pageInfo, Context $context) 3925 { 3926 if ($pageInfo === []) { 3927 return ''; 3928 } 3929 // Initialize access restriction values from current page 3930 $access = [ 3931 'fe_group' => (string)($pageInfo['fe_group'] ?? ''), 3932 'starttime' => (int)($pageInfo['starttime'] ?? 0), 3933 'endtime' => (int)($pageInfo['endtime'] ?? 0) 3934 ]; 3935 // Only check rootline if the current page has not set extendToSubpages itself 3936 if (!(bool)($pageInfo['extendToSubpages'] ?? false)) { 3937 $rootline = self::BEgetRootLine((int)($pageInfo['uid'] ?? 0)); 3938 // remove the current page from the rootline 3939 array_shift($rootline); 3940 foreach ($rootline as $page) { 3941 // Skip root node, invalid pages and pages which do not define extendToSubpages 3942 if ((int)($page['uid'] ?? 0) <= 0 || !(bool)($page['extendToSubpages'] ?? false)) { 3943 continue; 3944 } 3945 $access['fe_group'] = (string)($page['fe_group'] ?? ''); 3946 $access['starttime'] = (int)($page['starttime'] ?? 0); 3947 $access['endtime'] = (int)($page['endtime'] ?? 0); 3948 // Stop as soon as a page in the rootline has extendToSubpages set 3949 break; 3950 } 3951 } 3952 $simUser = ''; 3953 $simTime = ''; 3954 if ((int)$access['fe_group'] === -2) { 3955 // -2 means "show at any login". We simulate first available fe_group. 3956 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 3957 ->getQueryBuilderForTable('fe_groups'); 3958 $queryBuilder->getRestrictions() 3959 ->removeAll() 3960 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)) 3961 ->add(GeneralUtility::makeInstance(HiddenRestriction::class)); 3962 3963 $activeFeGroupId = $queryBuilder->select('uid') 3964 ->from('fe_groups') 3965 ->execute() 3966 ->fetchColumn(); 3967 3968 if ($activeFeGroupId) { 3969 $simUser = '&ADMCMD_simUser=' . $activeFeGroupId; 3970 } 3971 } elseif (!empty($access['fe_group'])) { 3972 $simUser = '&ADMCMD_simUser=' . $access['fe_group']; 3973 } 3974 if ($access['starttime'] > $GLOBALS['EXEC_TIME']) { 3975 // simulate access time to ensure PageRepository will find the page and in turn PageRouter will generate 3976 // an URL for it 3977 $dateAspect = GeneralUtility::makeInstance(DateTimeAspect::class, new \DateTimeImmutable('@' . $access['starttime'])); 3978 $context->setAspect('date', $dateAspect); 3979 $simTime = '&ADMCMD_simTime=' . $access['starttime']; 3980 } 3981 if ($access['endtime'] < $GLOBALS['EXEC_TIME'] && $access['endtime'] !== 0) { 3982 // Set access time to page's endtime subtracted one second to ensure PageRepository will find the page and 3983 // in turn PageRouter will generate an URL for it 3984 $dateAspect = GeneralUtility::makeInstance( 3985 DateTimeAspect::class, 3986 new \DateTimeImmutable('@' . ($access['endtime'] - 1)) 3987 ); 3988 $context->setAspect('date', $dateAspect); 3989 $simTime = '&ADMCMD_simTime=' . ($access['endtime'] - 1); 3990 } 3991 return $simUser . $simTime; 3992 } 3993 3994 /** 3995 * Returns the name of the backend script relative to the TYPO3 main directory. 3996 * 3997 * @param string $interface Name of the backend interface (backend, frontend) to look up the script name for. If no interface is given, the interface for the current backend user is used. 3998 * @return string The name of the backend script relative to the TYPO3 main directory. 3999 * @internal should only be used from within TYPO3 Core 4000 */ 4001 public static function getBackendScript($interface = '') 4002 { 4003 if (!$interface) { 4004 $interface = static::getBackendUserAuthentication()->uc['interfaceSetup']; 4005 } 4006 switch ($interface) { 4007 case 'frontend': 4008 $script = '../.'; 4009 break; 4010 case 'backend': 4011 default: 4012 $script = (string)GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute('main'); 4013 } 4014 return $script; 4015 } 4016 4017 /** 4018 * Determines whether a table is enabled for workspaces. 4019 * 4020 * @param string $table Name of the table to be checked 4021 * @return bool 4022 */ 4023 public static function isTableWorkspaceEnabled($table) 4024 { 4025 return !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']); 4026 } 4027 4028 /** 4029 * Gets the TCA configuration of a field. 4030 * 4031 * @param string $table Name of the table 4032 * @param string $field Name of the field 4033 * @return array 4034 */ 4035 public static function getTcaFieldConfiguration($table, $field) 4036 { 4037 $configuration = []; 4038 if (isset($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { 4039 $configuration = $GLOBALS['TCA'][$table]['columns'][$field]['config']; 4040 } 4041 return $configuration; 4042 } 4043 4044 /** 4045 * Whether to ignore restrictions on a web-mount of a table. 4046 * The regular behaviour is that records to be accessed need to be 4047 * in a valid user's web-mount. 4048 * 4049 * @param string $table Name of the table 4050 * @return bool 4051 */ 4052 public static function isWebMountRestrictionIgnored($table) 4053 { 4054 return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreWebMountRestriction']); 4055 } 4056 4057 /** 4058 * Whether to ignore restrictions on root-level records. 4059 * The regular behaviour is that records on the root-level (page-id 0) 4060 * only can be accessed by admin users. 4061 * 4062 * @param string $table Name of the table 4063 * @return bool 4064 */ 4065 public static function isRootLevelRestrictionIgnored($table) 4066 { 4067 return !empty($GLOBALS['TCA'][$table]['ctrl']['security']['ignoreRootLevelRestriction']); 4068 } 4069 4070 /** 4071 * @param string $table 4072 * @return Connection 4073 */ 4074 protected static function getConnectionForTable($table) 4075 { 4076 return GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); 4077 } 4078 4079 /** 4080 * @param string $table 4081 * @return QueryBuilder 4082 */ 4083 protected static function getQueryBuilderForTable($table) 4084 { 4085 return GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 4086 } 4087 4088 /** 4089 * @return LoggerInterface 4090 */ 4091 protected static function getLogger() 4092 { 4093 return GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); 4094 } 4095 4096 /** 4097 * @return LanguageService 4098 */ 4099 protected static function getLanguageService() 4100 { 4101 return $GLOBALS['LANG']; 4102 } 4103 4104 /** 4105 * @return BackendUserAuthentication|null 4106 */ 4107 protected static function getBackendUserAuthentication() 4108 { 4109 return $GLOBALS['BE_USER'] ?? null; 4110 } 4111} 4112