1<?php 2namespace TYPO3\CMS\Frontend\Page; 3 4/* 5 * This file is part of the TYPO3 CMS project. 6 * 7 * It is free software; you can redistribute it and/or modify it under 8 * the terms of the GNU General Public License, either version 2 9 * of the License, or any later version. 10 * 11 * For the full copyright and license information, please read the 12 * LICENSE.txt file that was distributed with this source code. 13 * 14 * The TYPO3 project - inspiring people to share! 15 */ 16 17use Psr\Log\LoggerAwareInterface; 18use Psr\Log\LoggerAwareTrait; 19use TYPO3\CMS\Core\Cache\CacheManager; 20use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend; 21use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait; 22use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait; 23use TYPO3\CMS\Core\Context\Context; 24use TYPO3\CMS\Core\Context\Exception\AspectNotFoundException; 25use TYPO3\CMS\Core\Context\LanguageAspect; 26use TYPO3\CMS\Core\Context\UserAspect; 27use TYPO3\CMS\Core\Database\Connection; 28use TYPO3\CMS\Core\Database\ConnectionPool; 29use TYPO3\CMS\Core\Database\Query\QueryHelper; 30use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction; 31use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction; 32use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer; 33use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction; 34use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction; 35use TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException; 36use TYPO3\CMS\Core\Resource\Exception\FileDoesNotExistException; 37use TYPO3\CMS\Core\Resource\FileRepository; 38use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 39use TYPO3\CMS\Core\Utility\GeneralUtility; 40use TYPO3\CMS\Core\Utility\MathUtility; 41use TYPO3\CMS\Core\Utility\RootlineUtility; 42use TYPO3\CMS\Core\Versioning\VersionState; 43 44/** 45 * Page functions, a lot of sql/pages-related functions 46 * 47 * Mainly used in the frontend but also in some cases in the backend. It's 48 * important to set the right $where_hid_del in the object so that the 49 * functions operate properly 50 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::fetch_the_id() 51 */ 52class PageRepository implements LoggerAwareInterface 53{ 54 use LoggerAwareTrait; 55 use PublicPropertyDeprecationTrait; 56 use PublicMethodDeprecationTrait; 57 58 /** 59 * List of all deprecated public properties 60 * @var array 61 */ 62 protected $deprecatedPublicProperties = [ 63 'versioningPreview' => 'Using $versioningPreview of class PageRepository is discouraged, just use versioningWorkspaceId to determine if a workspace should be previewed.', 64 'workspaceCache' => 'Using $workspaceCache of class PageRepository from the outside is discouraged, as this only reflects a local runtime cache.', 65 'error_getRootLine' => 'Using $error_getRootLine of class PageRepository from the outside is deprecated as this property only exists for legacy reasons.', 66 'error_getRootLine_failPid' => 'Using $error_getRootLine_failPid of class PageRepository from the outside is deprecated as this property only exists for legacy reasons.', 67 'sys_language_uid' => 'Using $sys_language_uid of class PageRepository from the outside is deprecated as this information is now stored within the Context Language Aspect given by the constructor.', 68 'versioningWorkspaceId' => 'Using $versioningWorkspaceId of class PageRepository from the outside is deprecated as this information is now stored within the Context Workspace Aspect given by the constructor.', 69 ]; 70 71 /** 72 * List of all deprecated public methods 73 * @var array 74 */ 75 protected $deprecatedPublicMethods = [ 76 'init' => 'init() is now called implicitly on object creation, and is not necessary anymore to be called explicitly. Calling init() will throw an error in TYPO3 v10.0.', 77 'movePlhOL' => 'Using movePlhOL is deprecated and will not be possible anymore in TYPO3 v10.0.', 78 'getMovePlaceholder' => 'Using getMovePlaceholder is deprecated and will not be possible anymore in TYPO3 v10.0.' 79 ]; 80 81 /** 82 * This is not the final clauses. There will normally be conditions for the 83 * hidden, starttime and endtime fields as well. You MUST initialize the object 84 * by the init() function 85 * 86 * @var string 87 */ 88 public $where_hid_del = ' AND pages.deleted=0'; 89 90 /** 91 * Clause for fe_group access 92 * 93 * @var string 94 */ 95 public $where_groupAccess = ''; 96 97 /** 98 * @var int 99 * @deprecated will be removed in TYPO3 v10.0, all occurrences should be replaced with the language->id() aspect property in TYPO3 v10.0 100 * However, the usage within the class is kept as the property could be overwritten by third-party classes 101 */ 102 protected $sys_language_uid = 0; 103 104 /** 105 * If TRUE, versioning preview of other record versions is allowed. THIS MUST 106 * ONLY BE SET IF the page is not cached and truly previewed by a backend 107 * user!!! 108 * 109 * @var bool 110 * @deprecated since TYPO3 v9.3, will be removed in TYPO3 v10.0. As $versioningWorkspaceId now indicates what records to fetch. 111 */ 112 protected $versioningPreview = false; 113 114 /** 115 * Workspace ID for preview 116 * If > 0, versioning preview of other record versions is allowed. THIS MUST 117 * ONLY BE SET IF the page is not cached and truly previewed by a backend 118 * user! 119 * 120 * @var int 121 * @deprecated This method will be kept protected from TYPO3 v10.0 on, instantiate a new pageRepository object with a custom workspace aspect to override this setting. 122 */ 123 protected $versioningWorkspaceId = 0; 124 125 /** 126 * @var array 127 */ 128 protected $workspaceCache = []; 129 130 /** 131 * Error string set by getRootLine() 132 * 133 * @var string 134 */ 135 protected $error_getRootLine = ''; 136 137 /** 138 * Error uid set by getRootLine() 139 * 140 * @var int 141 */ 142 protected $error_getRootLine_failPid = 0; 143 144 /** 145 * @var array 146 */ 147 protected $cache_getPage = []; 148 149 /** 150 * @var array 151 */ 152 protected $cache_getPage_noCheck = []; 153 154 /** 155 * @var array 156 */ 157 protected $cache_getPageIdFromAlias = []; 158 159 /** 160 * @var array 161 */ 162 protected $cache_getMountPointInfo = []; 163 164 /** 165 * Computed properties that are added to database rows. 166 * 167 * @var array 168 */ 169 protected $computedPropertyNames = [ 170 '_LOCALIZED_UID', 171 '_MP_PARAM', 172 '_ORIG_uid', 173 '_ORIG_pid', 174 '_PAGES_OVERLAY', 175 '_PAGES_OVERLAY_UID', 176 '_PAGES_OVERLAY_LANGUAGE', 177 '_PAGES_OVERLAY_REQUESTEDLANGUAGE', 178 ]; 179 180 /** 181 * Named constants for "magic numbers" of the field doktype 182 */ 183 const DOKTYPE_DEFAULT = 1; 184 const DOKTYPE_LINK = 3; 185 const DOKTYPE_SHORTCUT = 4; 186 const DOKTYPE_BE_USER_SECTION = 6; 187 const DOKTYPE_MOUNTPOINT = 7; 188 const DOKTYPE_SPACER = 199; 189 const DOKTYPE_SYSFOLDER = 254; 190 const DOKTYPE_RECYCLER = 255; 191 192 /** 193 * Named constants for "magic numbers" of the field shortcut_mode 194 */ 195 const SHORTCUT_MODE_NONE = 0; 196 const SHORTCUT_MODE_FIRST_SUBPAGE = 1; 197 const SHORTCUT_MODE_RANDOM_SUBPAGE = 2; 198 const SHORTCUT_MODE_PARENT_PAGE = 3; 199 200 /** 201 * @var Context 202 */ 203 protected $context; 204 205 /** 206 * PageRepository constructor to set the base context, this will effectively remove the necessity for 207 * setting properties from the outside. 208 * 209 * @param Context $context 210 */ 211 public function __construct(Context $context = null) 212 { 213 $this->context = $context ?? GeneralUtility::makeInstance(Context::class); 214 $this->versioningWorkspaceId = $this->context->getPropertyFromAspect('workspace', 'id'); 215 // Only set up the where clauses for pages when TCA is set. This usually happens only in tests. 216 // Once all tests are written very well, this can be removed again 217 if (isset($GLOBALS['TCA']['pages'])) { 218 $this->init($this->context->getPropertyFromAspect('visibility', 'includeHiddenPages')); 219 $this->where_groupAccess = $this->getMultipleGroupsWhereClause('pages.fe_group', 'pages'); 220 $this->sys_language_uid = (int)$this->context->getPropertyFromAspect('language', 'id', 0); 221 } 222 } 223 224 /** 225 * init() MUST be run directly after creating a new template-object 226 * This sets the internal variable $this->where_hid_del to the correct where 227 * clause for page records taking deleted/hidden/starttime/endtime/t3ver_state 228 * into account 229 * 230 * @param bool $show_hidden If $show_hidden is TRUE, the hidden-field is ignored!! Normally this should be FALSE. Is used for previewing. 231 * @internal 232 */ 233 protected function init($show_hidden) 234 { 235 $this->where_groupAccess = ''; 236 // As PageRepository may be used multiple times during the frontend request, and may 237 // actually be used before the usergroups have been resolved, self::getMultipleGroupsWhereClause() 238 // and the hook in ->enableFields() need to be reconsidered when the usergroup state changes. 239 // When something changes in the context, a second runtime cache entry is built. 240 // However, the PageRepository is generally in use for generating e.g. hundreds of links, so they would all use 241 // the same cache identifier. 242 $userAspect = $this->context->getAspect('frontend.user'); 243 $frontendUserIdentifier = 'user_' . (int)$userAspect->get('id') . '_groups_' . md5(implode(',', $userAspect->getGroupIds())); 244 245 // We need to respect the date aspect as we might have subrequests with a different time (e.g. backend preview links) 246 $dateTimeIdentifier = $this->context->getAspect('date')->get('timestamp'); 247 248 $cache = $this->getRuntimeCache(); 249 $cacheIdentifier = 'PageRepository_hidDelWhere' . ($show_hidden ? 'ShowHidden' : '') . '_' . (int)$this->versioningWorkspaceId . '_' . $frontendUserIdentifier . '_' . $dateTimeIdentifier; 250 $cacheEntry = $cache->get($cacheIdentifier); 251 if ($cacheEntry) { 252 $this->where_hid_del = $cacheEntry; 253 } else { 254 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 255 ->getQueryBuilderForTable('pages') 256 ->expr(); 257 if ($this->versioningWorkspaceId > 0) { 258 // For version previewing, make sure that enable-fields are not 259 // de-selecting hidden pages - we need versionOL() to unset them only 260 // if the overlay record instructs us to. 261 // Clear where_hid_del and restrict to live and current workspaces 262 $this->where_hid_del = ' AND ' . $expressionBuilder->andX( 263 $expressionBuilder->eq('pages.deleted', 0), 264 $expressionBuilder->orX( 265 $expressionBuilder->eq('pages.t3ver_wsid', 0), 266 $expressionBuilder->eq('pages.t3ver_wsid', (int)$this->versioningWorkspaceId) 267 ), 268 $expressionBuilder->lt('pages.doktype', 200) 269 ); 270 } else { 271 // add starttime / endtime, and check for hidden/deleted 272 // Filter out new/deleted place-holder pages in case we are NOT in a 273 // versioning preview (that means we are online!) 274 $this->where_hid_del = ' AND ' . (string)$expressionBuilder->andX( 275 QueryHelper::stripLogicalOperatorPrefix( 276 $this->enableFields('pages', $show_hidden, ['fe_group' => true], true) 277 ), 278 $expressionBuilder->lt('pages.doktype', 200) 279 ); 280 } 281 $cache->set($cacheIdentifier, $this->where_hid_del); 282 } 283 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] ?? false)) { 284 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] as $classRef) { 285 $hookObject = GeneralUtility::makeInstance($classRef); 286 if (!$hookObject instanceof PageRepositoryInitHookInterface) { 287 throw new \UnexpectedValueException($classRef . ' must implement interface ' . PageRepositoryInitHookInterface::class, 1379579812); 288 } 289 $hookObject->init_postProcess($this); 290 } 291 } 292 } 293 294 /************************** 295 * 296 * Selecting page records 297 * 298 **************************/ 299 300 /** 301 * Loads the full page record for the given page ID. 302 * 303 * The page record is either served from a first-level cache or loaded from the 304 * database. If no page can be found, an empty array is returned. 305 * 306 * Language overlay and versioning overlay are applied. Mount Point 307 * handling is not done, an overlaid Mount Point is not replaced. 308 * 309 * The result is conditioned by the public properties where_groupAccess 310 * and where_hid_del that are preset by the init() method. 311 * 312 * @see PageRepository::where_groupAccess 313 * @see PageRepository::where_hid_del 314 * 315 * By default the usergroup access check is enabled. Use the second method argument 316 * to disable the usergroup access check. 317 * 318 * The given UID can be preprocessed by registering a hook class that is 319 * implementing the PageRepositoryGetPageHookInterface into the configuration array 320 * $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage']. 321 * 322 * @param int $uid The page id to look up 323 * @param bool $disableGroupAccessCheck set to true to disable group access check 324 * @return array The resulting page record with overlays or empty array 325 * @throws \UnexpectedValueException 326 * @see PageRepository::getPage_noCheck() 327 */ 328 public function getPage($uid, $disableGroupAccessCheck = false) 329 { 330 // Hook to manipulate the page uid for special overlay handling 331 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'] ?? [] as $className) { 332 $hookObject = GeneralUtility::makeInstance($className); 333 if (!$hookObject instanceof PageRepositoryGetPageHookInterface) { 334 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetPageHookInterface::class, 1251476766); 335 } 336 $hookObject->getPage_preProcess($uid, $disableGroupAccessCheck, $this); 337 } 338 $cacheIdentifier = 'PageRepository_getPage_' . md5( 339 implode( 340 '-', 341 [ 342 $uid, 343 $disableGroupAccessCheck ? '' : $this->where_groupAccess, 344 $this->where_hid_del, 345 $this->sys_language_uid 346 ] 347 ) 348 ); 349 $cache = $this->getRuntimeCache(); 350 $cacheEntry = $cache->get($cacheIdentifier); 351 if (is_array($cacheEntry)) { 352 return $cacheEntry; 353 } 354 $result = []; 355 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 356 $queryBuilder->getRestrictions()->removeAll(); 357 $queryBuilder->select('*') 358 ->from('pages') 359 ->where( 360 $queryBuilder->expr()->eq('uid', (int)$uid), 361 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del) 362 ); 363 364 $originalWhereGroupAccess = ''; 365 if (!$disableGroupAccessCheck) { 366 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess)); 367 } else { 368 $originalWhereGroupAccess = $this->where_groupAccess; 369 $this->where_groupAccess = ''; 370 } 371 372 $row = $queryBuilder->execute()->fetch(); 373 if ($row) { 374 $this->versionOL('pages', $row); 375 if (is_array($row)) { 376 $result = $this->getPageOverlay($row); 377 } 378 } 379 380 if ($disableGroupAccessCheck) { 381 $this->where_groupAccess = $originalWhereGroupAccess; 382 } 383 384 $cache->set($cacheIdentifier, $result); 385 return $result; 386 } 387 388 /** 389 * Return the $row for the page with uid = $uid WITHOUT checking for 390 * ->where_hid_del (start- and endtime or hidden). Only "deleted" is checked! 391 * 392 * @param int $uid The page id to look up 393 * @return array The page row with overlaid localized fields. Empty array if no page. 394 * @see getPage() 395 */ 396 public function getPage_noCheck($uid) 397 { 398 $cache = $this->getRuntimeCache(); 399 $cacheIdentifier = 'PageRepository_getPage_noCheck_' . $uid . '_' . $this->sys_language_uid . '_' . $this->versioningWorkspaceId; 400 $cacheEntry = $cache->get($cacheIdentifier); 401 if ($cacheEntry !== false) { 402 return $cacheEntry; 403 } 404 405 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 406 $queryBuilder->getRestrictions() 407 ->removeAll() 408 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 409 $row = $queryBuilder->select('*') 410 ->from('pages') 411 ->where($queryBuilder->expr()->eq('uid', (int)$uid)) 412 ->execute() 413 ->fetch(); 414 415 $result = []; 416 if ($row) { 417 $this->versionOL('pages', $row); 418 if (is_array($row)) { 419 $result = $this->getPageOverlay($row); 420 } 421 } 422 $cache->set($cacheIdentifier, $result); 423 return $result; 424 } 425 426 /** 427 * Returns the $row of the first web-page in the tree (for the default menu...) 428 * 429 * @param int $uid The page id for which to fetch first subpages (PID) 430 * @return mixed If found: The page record (with overlaid localized fields, if any). If NOT found: blank value (not array!) 431 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::fetch_the_id() 432 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. Use getMenu() to fetch all subpages of a page. 433 */ 434 public function getFirstWebPage($uid) 435 { 436 trigger_error('PageRepository->getFirstWebPage() will be removed in TYPO3 v10.0. Use "getMenu()" instead.', E_USER_DEPRECATED); 437 $output = ''; 438 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 439 $queryBuilder->getRestrictions()->removeAll(); 440 $row = $queryBuilder->select('*') 441 ->from('pages') 442 ->where( 443 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)), 444 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del), 445 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess) 446 ) 447 ->orderBy('sorting') 448 ->setMaxResults(1) 449 ->execute() 450 ->fetch(); 451 452 if ($row) { 453 $this->versionOL('pages', $row); 454 if (is_array($row)) { 455 $output = $this->getPageOverlay($row); 456 } 457 } 458 return $output; 459 } 460 461 /** 462 * Returns a pagerow for the page with alias $alias 463 * 464 * @param string $alias The alias to look up the page uid for. 465 * @return int Returns page uid (int) if found, otherwise 0 (zero) 466 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::checkAndSetAlias(), ContentObjectRenderer::typoLink() 467 */ 468 public function getPageIdFromAlias($alias) 469 { 470 $alias = strtolower($alias); 471 $cacheIdentifier = 'PageRepository_getPageIdFromAlias_' . md5($alias); 472 $cache = $this->getRuntimeCache(); 473 $cacheEntry = $cache->get($cacheIdentifier); 474 if ($cacheEntry !== false) { 475 return $cacheEntry; 476 } 477 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 478 $queryBuilder->getRestrictions() 479 ->removeAll() 480 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 481 482 $row = $queryBuilder->select('uid') 483 ->from('pages') 484 ->where( 485 $queryBuilder->expr()->eq('alias', $queryBuilder->createNamedParameter($alias, \PDO::PARAM_STR)), 486 // "AND pid>=0" because of versioning (means that aliases sent MUST be online!) 487 $queryBuilder->expr()->gte('pid', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)) 488 ) 489 ->setMaxResults(1) 490 ->execute() 491 ->fetch(); 492 493 if ($row) { 494 $cache->set($cacheIdentifier, $row['uid']); 495 return $row['uid']; 496 } 497 $cache->set($cacheIdentifier, 0); 498 return 0; 499 } 500 501 /** 502 * Master helper method to overlay a record to a language. 503 * 504 * Be aware that for pages the languageId is taken, and for all other records the contentId. 505 * This might change through a feature switch in the future. 506 * 507 * @param string $table the name of the table, should be a TCA table with localization enabled 508 * @param array $row the current (full-fletched) record. 509 * @return array|null 510 */ 511 public function getLanguageOverlay(string $table, array $row) 512 { 513 // table is not localizable, so return directly 514 if (!isset($GLOBALS['TCA'][$table]['ctrl']['languageField'])) { 515 return $row; 516 } 517 try { 518 /** @var LanguageAspect $languageAspect */ 519 $languageAspect = $this->context->getAspect('language'); 520 if ($languageAspect->doOverlays()) { 521 if ($table === 'pages') { 522 return $this->getPageOverlay($row, $languageAspect->getId()); 523 } 524 return $this->getRecordOverlay( 525 $table, 526 $row, 527 $languageAspect->getContentId(), 528 $languageAspect->getOverlayType() === $languageAspect::OVERLAYS_MIXED ? '1' : 'hideNonTranslated' 529 ); 530 } 531 } catch (AspectNotFoundException $e) { 532 // no overlays 533 } 534 return $row; 535 } 536 537 /** 538 * Returns the relevant page overlay record fields 539 * 540 * @param mixed $pageInput If $pageInput is an integer, it's the pid of the pageOverlay record and thus the page overlay record is returned. If $pageInput is an array, it's a page-record and based on this page record the language record is found and OVERLAID before the page record is returned. 541 * @param int $languageUid Language UID if you want to set an alternative value to $this->sys_language_uid which is default. Should be >=0 542 * @throws \UnexpectedValueException 543 * @return array Page row which is overlaid with language_overlay record (or the overlay record alone) 544 */ 545 public function getPageOverlay($pageInput, $languageUid = null) 546 { 547 if ($languageUid === -1) { 548 trigger_error('Calling getPageOverlay() with "-1" as languageId is discouraged and will be unsupported in TYPO3 v10.0. Omit the parameter or use "null" instead.', E_USER_DEPRECATED); 549 $languageUid = null; 550 } 551 $rows = $this->getPagesOverlay([$pageInput], $languageUid); 552 // Always an array in return 553 return $rows[0] ?? []; 554 } 555 556 /** 557 * Returns the relevant page overlay record fields 558 * 559 * @param array $pagesInput Array of integers or array of arrays. If each value is an integer, it's the pids of the pageOverlay records and thus the page overlay records are returned. If each value is an array, it's page-records and based on this page records the language records are found and OVERLAID before the page records are returned. 560 * @param int $languageUid Language UID if you want to set an alternative value to $this->sys_language_uid which is default. Should be >=0 561 * @throws \UnexpectedValueException 562 * @return array Page rows which are overlaid with language_overlay record. 563 * If the input was an array of integers, missing records are not 564 * included. If the input were page rows, untranslated pages 565 * are returned. 566 */ 567 public function getPagesOverlay(array $pagesInput, $languageUid = null) 568 { 569 if (empty($pagesInput)) { 570 return []; 571 } 572 if ($languageUid === null) { 573 $languageUid = $this->sys_language_uid; 574 } elseif ($languageUid < 0) { 575 trigger_error('Calling getPagesOverlay() with "-1" as languageId is discouraged and will be unsupported in TYPO3 v10.0. Omit the parameter or use "null" instead.', E_USER_DEPRECATED); 576 $languageUid = $this->sys_language_uid; 577 } 578 foreach ($pagesInput as &$origPage) { 579 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPageOverlay'] ?? [] as $className) { 580 $hookObject = GeneralUtility::makeInstance($className); 581 if (!$hookObject instanceof PageRepositoryGetPageOverlayHookInterface) { 582 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetPageOverlayHookInterface::class, 1269878881); 583 } 584 $hookObject->getPageOverlay_preProcess($origPage, $languageUid, $this); 585 } 586 } 587 unset($origPage); 588 589 $overlays = []; 590 // If language UID is different from zero, do overlay: 591 if ($languageUid) { 592 $languageUids = array_merge([$languageUid], $this->getLanguageFallbackChain(null)); 593 594 $pageIds = []; 595 foreach ($pagesInput as $origPage) { 596 if (is_array($origPage)) { 597 // Was the whole record 598 $pageIds[] = (int)$origPage['uid']; 599 } else { 600 // Was the id 601 $pageIds[] = (int)$origPage; 602 } 603 } 604 $overlays = $this->getPageOverlaysForLanguageUids($pageIds, $languageUids); 605 } 606 607 // Create output: 608 $pagesOutput = []; 609 foreach ($pagesInput as $key => $origPage) { 610 if (is_array($origPage)) { 611 $pagesOutput[$key] = $origPage; 612 if (isset($overlays[$origPage['uid']])) { 613 // Overwrite the original field with the overlay 614 foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) { 615 if ($fieldName !== 'uid' && $fieldName !== 'pid') { 616 $pagesOutput[$key][$fieldName] = $fieldValue; 617 } 618 } 619 } 620 } else { 621 if (isset($overlays[$origPage])) { 622 $pagesOutput[$key] = $overlays[$origPage]; 623 } 624 } 625 } 626 return $pagesOutput; 627 } 628 629 /** 630 * Checks whether the passed (translated or default language) page is accessible with the given language settings. 631 * 632 * @param array $page the page translation record or the page in the default language 633 * @param LanguageAspect $languageAspect 634 * @return bool true if the given page translation record is suited for the given language ID 635 * @internal 636 */ 637 public function isPageSuitableForLanguage(array $page, LanguageAspect $languageAspect): bool 638 { 639 $languageUid = $languageAspect->getId(); 640 // Checks if the default language version can be shown 641 // Block page is set, if l18n_cfg allows plus: 1) Either default language or 2) another language but NO overlay record set for page! 642 if (GeneralUtility::hideIfDefaultLanguage($page['l18n_cfg']) && (!$languageUid || $languageUid && !$page['_PAGES_OVERLAY'])) { 643 return false; 644 } 645 if ($languageUid > 0 && GeneralUtility::hideIfNotTranslated($page['l18n_cfg'])) { 646 if (!$page['_PAGES_OVERLAY'] || (int)$page['_PAGES_OVERLAY_LANGUAGE'] !== $languageUid) { 647 return false; 648 } 649 } elseif ($languageUid > 0) { 650 $languageUids = array_merge([$languageUid], $this->getLanguageFallbackChain($languageAspect)); 651 return in_array((int)$page['sys_language_uid'], $languageUids, true); 652 } 653 return true; 654 } 655 656 /** 657 * Returns the cleaned fallback chain from the current language aspect, if there is one. 658 * 659 * @param LanguageAspect|null $languageAspect 660 * @return int[] 661 */ 662 protected function getLanguageFallbackChain(?LanguageAspect $languageAspect): array 663 { 664 $languageAspect = $languageAspect ?? $this->context->getAspect('language'); 665 return array_filter($languageAspect->getFallbackChain(), function ($item) { 666 return MathUtility::canBeInterpretedAsInteger($item); 667 }); 668 } 669 670 /** 671 * Returns the first match of overlays for pages in the passed languages. 672 * 673 * NOTE regarding the query restrictions: 674 * Currently the visibility aspect within the FrontendRestrictionContainer will allow 675 * page translation records to be selected as they are child-records of a page. 676 * However you may argue that the visibility flag should determine this. 677 * But that's not how it's done right now. 678 * 679 * @param array $pageUids 680 * @param array $languageUids uid of sys_language, please note that the order is important here. 681 * @return array 682 */ 683 protected function getPageOverlaysForLanguageUids(array $pageUids, array $languageUids): array 684 { 685 // Remove default language ("0") 686 $languageUids = array_filter($languageUids); 687 $languageField = $GLOBALS['TCA']['pages']['ctrl']['languageField']; 688 $overlays = []; 689 690 foreach ($pageUids as $pageId) { 691 // Create a map based on the order of values in $languageUids. Those entries reflect the order of the language + fallback chain. 692 // We can't work with database ordering since there is no common SQL clause to order by e.g. [7, 1, 2]. 693 $orderedListByLanguages = array_flip($languageUids); 694 695 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 696 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)); 697 // Because "fe_group" is an exclude field, so it is synced between overlays, the group restriction is removed for language overlays of pages 698 $queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class); 699 $result = $queryBuilder->select('*') 700 ->from('pages') 701 ->where( 702 $queryBuilder->expr()->eq( 703 $GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'], 704 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) 705 ), 706 $queryBuilder->expr()->in( 707 $GLOBALS['TCA']['pages']['ctrl']['languageField'], 708 $queryBuilder->createNamedParameter($languageUids, Connection::PARAM_INT_ARRAY) 709 ) 710 ) 711 ->execute(); 712 713 // Create a list of rows ordered by values in $languageUids 714 while ($row = $result->fetch()) { 715 $orderedListByLanguages[$row[$languageField]] = $row; 716 } 717 718 foreach ($orderedListByLanguages as $languageUid => $row) { 719 if (!is_array($row)) { 720 continue; 721 } 722 723 // Found a result for the current language id 724 $this->versionOL('pages', $row); 725 if (is_array($row)) { 726 $row['_PAGES_OVERLAY'] = true; 727 $row['_PAGES_OVERLAY_UID'] = $row['uid']; 728 $row['_PAGES_OVERLAY_LANGUAGE'] = $languageUid; 729 $row['_PAGES_OVERLAY_REQUESTEDLANGUAGE'] = $languageUids[0]; 730 // Unset vital fields that are NOT allowed to be overlaid: 731 unset($row['uid'], $row['pid'], $row['alias']); 732 $overlays[$pageId] = $row; 733 734 // Language fallback found, stop querying further languages 735 break; 736 } 737 } 738 } 739 740 return $overlays; 741 } 742 743 /** 744 * Creates language-overlay for records in general (where translation is found 745 * in records from the same table) 746 * 747 * @param string $table Table name 748 * @param array $row Record to overlay. Must contain uid, pid and $table]['ctrl']['languageField'] 749 * @param int $sys_language_content Pointer to the sys_language uid for content on the site. 750 * @param string $OLmode Overlay mode. If "hideNonTranslated" then records without translation will not be returned un-translated but unset (and return value is FALSE) 751 * @throws \UnexpectedValueException 752 * @return mixed Returns the input record, possibly overlaid with a translation. But if $OLmode is "hideNonTranslated" then it will return FALSE if no translation is found. 753 */ 754 public function getRecordOverlay($table, $row, $sys_language_content, $OLmode = '') 755 { 756 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] ?? [] as $className) { 757 $hookObject = GeneralUtility::makeInstance($className); 758 if (!$hookObject instanceof PageRepositoryGetRecordOverlayHookInterface) { 759 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetRecordOverlayHookInterface::class, 1269881658); 760 } 761 $hookObject->getRecordOverlay_preProcess($table, $row, $sys_language_content, $OLmode, $this); 762 } 763 764 $tableControl = $GLOBALS['TCA'][$table]['ctrl'] ?? []; 765 766 if (!empty($tableControl['languageField']) 767 // Return record for ALL languages untouched 768 // TODO: Fix call stack to prevent this situation in the first place 769 && (int)$row[$tableControl['languageField']] !== -1 770 && !empty($tableControl['transOrigPointerField']) 771 && $row['uid'] > 0 772 && ($row['pid'] > 0 || in_array($tableControl['rootLevel'] ?? false, [true, 1, -1], true))) { 773 // Will try to overlay a record only if the sys_language_content value is larger than zero. 774 if ($sys_language_content > 0) { 775 // Must be default language, otherwise no overlaying 776 if ((int)$row[$tableControl['languageField']] === 0) { 777 // Select overlay record: 778 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 779 ->getQueryBuilderForTable($table); 780 $queryBuilder->setRestrictions( 781 GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context) 782 ); 783 $olrow = $queryBuilder->select('*') 784 ->from($table) 785 ->where( 786 $queryBuilder->expr()->eq( 787 'pid', 788 $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT) 789 ), 790 $queryBuilder->expr()->eq( 791 $tableControl['languageField'], 792 $queryBuilder->createNamedParameter($sys_language_content, \PDO::PARAM_INT) 793 ), 794 $queryBuilder->expr()->eq( 795 $tableControl['transOrigPointerField'], 796 $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT) 797 ) 798 ) 799 ->setMaxResults(1) 800 ->execute() 801 ->fetch(); 802 803 $this->versionOL($table, $olrow); 804 // Merge record content by traversing all fields: 805 if (is_array($olrow)) { 806 if (isset($olrow['_ORIG_uid'])) { 807 $row['_ORIG_uid'] = $olrow['_ORIG_uid']; 808 } 809 if (isset($olrow['_ORIG_pid'])) { 810 $row['_ORIG_pid'] = $olrow['_ORIG_pid']; 811 } 812 foreach ($row as $fN => $fV) { 813 if ($fN !== 'uid' && $fN !== 'pid' && isset($olrow[$fN])) { 814 $row[$fN] = $olrow[$fN]; 815 } elseif ($fN === 'uid') { 816 $row['_LOCALIZED_UID'] = $olrow['uid']; 817 } 818 } 819 } elseif ($OLmode === 'hideNonTranslated' && (int)$row[$tableControl['languageField']] === 0) { 820 // Unset, if non-translated records should be hidden. ONLY done if the source 821 // record really is default language and not [All] in which case it is allowed. 822 unset($row); 823 } 824 } elseif ($sys_language_content != $row[$tableControl['languageField']]) { 825 unset($row); 826 } 827 } else { 828 // When default language is displayed, we never want to return a record carrying 829 // another language! 830 if ($row[$tableControl['languageField']] > 0) { 831 unset($row); 832 } 833 } 834 } 835 836 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getRecordOverlay'] ?? [] as $className) { 837 $hookObject = GeneralUtility::makeInstance($className); 838 if (!$hookObject instanceof PageRepositoryGetRecordOverlayHookInterface) { 839 throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetRecordOverlayHookInterface::class, 1269881659); 840 } 841 $hookObject->getRecordOverlay_postProcess($table, $row, $sys_language_content, $OLmode, $this); 842 } 843 844 return $row; 845 } 846 847 /************************************************ 848 * 849 * Page related: Menu, Domain record, Root line 850 * 851 ************************************************/ 852 853 /** 854 * Returns an array with page rows for subpages of a certain page ID. This is used for menus in the frontend. 855 * If there are mount points in overlay mode the _MP_PARAM field is set to the correct MPvar. 856 * 857 * If the $pageId being input does in itself require MPvars to define a correct 858 * rootline these must be handled externally to this function. 859 * 860 * @param int|int[] $pageId The page id (or array of page ids) for which to fetch subpages (PID) 861 * @param string $fields List of fields to select. Default is "*" = all 862 * @param string $sortField The field to sort by. Default is "sorting 863 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%some text%'" for instance. 864 * @param bool $checkShortcuts Check if shortcuts exist, checks by default 865 * @return array Array with key/value pairs; keys are page-uid numbers. values are the corresponding page records (with overlaid localized fields, if any) 866 * @see self::getPageShortcut(), \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::makeMenu() 867 */ 868 public function getMenu($pageId, $fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true) 869 { 870 return $this->getSubpagesForPages((array)$pageId, $fields, $sortField, $additionalWhereClause, $checkShortcuts); 871 } 872 873 /** 874 * Returns an array with page-rows for pages with uid in $pageIds. 875 * 876 * This is used for menus. If there are mount points in overlay mode 877 * the _MP_PARAM field is set to the correct MPvar. 878 * 879 * @param int[] $pageIds Array of page ids to fetch 880 * @param string $fields List of fields to select. Default is "*" = all 881 * @param string $sortField The field to sort by. Default is "sorting" 882 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%some text%'" for instance. 883 * @param bool $checkShortcuts Check if shortcuts exist, checks by default 884 * @return array Array with key/value pairs; keys are page-uid numbers. values are the corresponding page records (with overlaid localized fields, if any) 885 */ 886 public function getMenuForPages(array $pageIds, $fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true) 887 { 888 return $this->getSubpagesForPages($pageIds, $fields, $sortField, $additionalWhereClause, $checkShortcuts, false); 889 } 890 891 /** 892 * Loads page records either by PIDs or by UIDs. 893 * 894 * By default the subpages of the given page IDs are loaded (as the method name suggests). If $parentPages is set 895 * to FALSE, the page records for the given page IDs are loaded directly. 896 * 897 * Concerning the rationale, please see these two other methods: 898 * 899 * @see PageRepository::getMenu() 900 * @see PageRepository::getMenuForPages() 901 * 902 * Version and language overlay are applied to the loaded records. 903 * 904 * If a record is a mount point in overlay mode, the the overlaying page record is returned in place of the 905 * record. The record is enriched by the field _MP_PARAM containing the mount point mapping for the mount 906 * point. 907 * 908 * The query can be customized by setting fields, sorting and additional WHERE clauses. If additional WHERE 909 * clauses are given, the clause must start with an operator, i.e: "AND title like '%some text%'". 910 * 911 * The keys of the returned page records are the page UIDs. 912 * 913 * CAUTION: In case of an overlaid mount point, it is the original UID. 914 * 915 * @param int[] $pageIds PIDs or UIDs to load records for 916 * @param string $fields fields to select 917 * @param string $sortField the field to sort by 918 * @param string $additionalWhereClause optional additional WHERE clause 919 * @param bool $checkShortcuts whether to check if shortcuts exist 920 * @param bool $parentPages Switch to load pages (false) or child pages (true). 921 * @return array page records 922 * 923 * @see self::getPageShortcut() 924 * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject::makeMenu() 925 */ 926 protected function getSubpagesForPages( 927 array $pageIds, 928 string $fields = '*', 929 string $sortField = 'sorting', 930 string $additionalWhereClause = '', 931 bool $checkShortcuts = true, 932 bool $parentPages = true 933 ): array { 934 $relationField = $parentPages ? 'pid' : 'uid'; 935 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 936 $queryBuilder->getRestrictions()->removeAll(); 937 938 $res = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true)) 939 ->from('pages') 940 ->where( 941 $queryBuilder->expr()->in( 942 $relationField, 943 $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY) 944 ), 945 $queryBuilder->expr()->eq( 946 $GLOBALS['TCA']['pages']['ctrl']['languageField'], 947 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 948 ), 949 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del), 950 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess), 951 QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause) 952 ); 953 954 if (!empty($sortField)) { 955 $orderBy = QueryHelper::parseOrderBy($sortField); 956 foreach ($orderBy as $order) { 957 $res->addOrderBy($order[0], $order[1] ?? 'ASC'); 958 } 959 } 960 $result = $res->execute(); 961 962 $pages = []; 963 while ($page = $result->fetch()) { 964 $originalUid = $page['uid']; 965 966 // Versioning Preview Overlay 967 $this->versionOL('pages', $page, true); 968 // Skip if page got disabled due to version overlay 969 // (might be delete or move placeholder) 970 if (empty($page)) { 971 continue; 972 } 973 974 // Add a mount point parameter if needed 975 $page = $this->addMountPointParameterToPage((array)$page); 976 977 // If shortcut, look up if the target exists and is currently visible 978 if ($checkShortcuts) { 979 $page = $this->checkValidShortcutOfPage((array)$page, $additionalWhereClause); 980 } 981 982 // If the page still is there, we add it to the output 983 if (!empty($page)) { 984 $pages[$originalUid] = $page; 985 } 986 } 987 988 // Finally load language overlays 989 return $this->getPagesOverlay($pages); 990 } 991 992 /** 993 * Replaces the given page record with mounted page if required 994 * 995 * If the given page record is a mount point in overlay mode, the page 996 * record is replaced by the record of the overlaying page. The overlay 997 * record is enriched by setting the mount point mapping into the field 998 * _MP_PARAM as string for example '23-14'. 999 * 1000 * In all other cases the given page record is returned as is. 1001 * 1002 * @todo Find a better name. The current doesn't hit the point. 1003 * 1004 * @param array $page The page record to handle. 1005 * @return array The given page record or it's replacement. 1006 */ 1007 protected function addMountPointParameterToPage(array $page): array 1008 { 1009 if (empty($page)) { 1010 return []; 1011 } 1012 1013 // $page MUST have "uid", "pid", "doktype", "mount_pid", "mount_pid_ol" fields in it 1014 $mountPointInfo = $this->getMountPointInfo($page['uid'], $page); 1015 1016 // There is a valid mount point in overlay mode. 1017 if (is_array($mountPointInfo) && $mountPointInfo['overlay']) { 1018 1019 // Using "getPage" is OK since we need the check for enableFields AND for type 2 1020 // of mount pids we DO require a doktype < 200! 1021 $mountPointPage = $this->getPage($mountPointInfo['mount_pid']); 1022 1023 if (!empty($mountPointPage)) { 1024 $page = $mountPointPage; 1025 $page['_MP_PARAM'] = $mountPointInfo['MPvar']; 1026 } else { 1027 $page = []; 1028 } 1029 } 1030 return $page; 1031 } 1032 1033 /** 1034 * If shortcut, look up if the target exists and is currently visible 1035 * 1036 * @param array $page The page to check 1037 * @param string $additionalWhereClause Optional additional where clauses. Like "AND title like '%some text%'" for instance. 1038 * @return array 1039 */ 1040 protected function checkValidShortcutOfPage(array $page, $additionalWhereClause) 1041 { 1042 if (empty($page)) { 1043 return []; 1044 } 1045 1046 $dokType = (int)$page['doktype']; 1047 $shortcutMode = (int)$page['shortcut_mode']; 1048 1049 if ($dokType === self::DOKTYPE_SHORTCUT && ($page['shortcut'] || $shortcutMode)) { 1050 if ($shortcutMode === self::SHORTCUT_MODE_NONE) { 1051 // No shortcut_mode set, so target is directly set in $page['shortcut'] 1052 $searchField = 'uid'; 1053 $searchUid = (int)$page['shortcut']; 1054 } elseif ($shortcutMode === self::SHORTCUT_MODE_FIRST_SUBPAGE || $shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) { 1055 // Check subpages - first subpage or random subpage 1056 $searchField = 'pid'; 1057 // If a shortcut mode is set and no valid page is given to select subpags 1058 // from use the actual page. 1059 $searchUid = (int)$page['shortcut'] ?: $page['uid']; 1060 } elseif ($shortcutMode === self::SHORTCUT_MODE_PARENT_PAGE) { 1061 // Shortcut to parent page 1062 $searchField = 'uid'; 1063 $searchUid = $page['pid']; 1064 } else { 1065 $searchField = ''; 1066 $searchUid = 0; 1067 } 1068 1069 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 1070 $queryBuilder->getRestrictions()->removeAll(); 1071 $count = $queryBuilder->count('uid') 1072 ->from('pages') 1073 ->where( 1074 $queryBuilder->expr()->eq( 1075 $searchField, 1076 $queryBuilder->createNamedParameter($searchUid, \PDO::PARAM_INT) 1077 ), 1078 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del), 1079 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess), 1080 QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause) 1081 ) 1082 ->execute() 1083 ->fetchColumn(); 1084 1085 if (!$count) { 1086 $page = []; 1087 } 1088 } elseif ($dokType === self::DOKTYPE_SHORTCUT) { 1089 // Neither shortcut target nor mode is set. Remove the page from the menu. 1090 $page = []; 1091 } 1092 return $page; 1093 } 1094 1095 /** 1096 * Get page shortcut; Finds the records pointed to by input value $SC (the shortcut value) 1097 * 1098 * @param int $shortcutFieldValue The value of the "shortcut" field from the pages record 1099 * @param int $shortcutMode The shortcut mode: 1 will select first subpage, 2 a random subpage, 3 the parent page; default is the page pointed to by $SC 1100 * @param int $thisUid The current page UID of the page which is a shortcut 1101 * @param int $iteration Safety feature which makes sure that the function is calling itself recursively max 20 times (since this function can find shortcuts to other shortcuts to other shortcuts...) 1102 * @param array $pageLog An array filled with previous page uids tested by the function - new page uids are evaluated against this to avoid going in circles. 1103 * @param bool $disableGroupCheck If true, the group check is disabled when fetching the target page (needed e.g. for menu generation) 1104 * 1105 * @throws \RuntimeException 1106 * @throws ShortcutTargetPageNotFoundException 1107 * @return mixed Returns the page record of the page that the shortcut pointed to. 1108 * @internal 1109 * @see getPageAndRootline() 1110 */ 1111 public function getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration = 20, $pageLog = [], $disableGroupCheck = false) 1112 { 1113 $idArray = GeneralUtility::intExplode(',', $shortcutFieldValue); 1114 // Find $page record depending on shortcut mode: 1115 switch ($shortcutMode) { 1116 case self::SHORTCUT_MODE_FIRST_SUBPAGE: 1117 case self::SHORTCUT_MODE_RANDOM_SUBPAGE: 1118 $pageArray = $this->getMenu($idArray[0] ?: $thisUid, '*', 'sorting', 'AND pages.doktype<199 AND pages.doktype!=' . self::DOKTYPE_BE_USER_SECTION); 1119 $pO = 0; 1120 if ($shortcutMode == self::SHORTCUT_MODE_RANDOM_SUBPAGE && !empty($pageArray)) { 1121 $pO = (int)rand(0, count($pageArray) - 1); 1122 } 1123 $c = 0; 1124 $page = []; 1125 foreach ($pageArray as $pV) { 1126 if ($c === $pO) { 1127 $page = $pV; 1128 break; 1129 } 1130 $c++; 1131 } 1132 if (empty($page)) { 1133 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a subpage. However, this page has no accessible subpages.'; 1134 throw new ShortcutTargetPageNotFoundException($message, 1301648328); 1135 } 1136 break; 1137 case self::SHORTCUT_MODE_PARENT_PAGE: 1138 $parent = $this->getPage($idArray[0] ?: $thisUid, $disableGroupCheck); 1139 $page = $this->getPage($parent['pid'], $disableGroupCheck); 1140 if (empty($page)) { 1141 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to its parent page. However, the parent page is not accessible.'; 1142 throw new ShortcutTargetPageNotFoundException($message, 1301648358); 1143 } 1144 break; 1145 default: 1146 $page = $this->getPage($idArray[0], $disableGroupCheck); 1147 if (empty($page)) { 1148 $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a page, which is not accessible (ID ' . $idArray[0] . ').'; 1149 throw new ShortcutTargetPageNotFoundException($message, 1301648404); 1150 } 1151 } 1152 // Check if short cut page was a shortcut itself, if so look up recursively: 1153 if ($page['doktype'] == self::DOKTYPE_SHORTCUT) { 1154 if (!in_array($page['uid'], $pageLog) && $iteration > 0) { 1155 $pageLog[] = $page['uid']; 1156 $page = $this->getPageShortcut($page['shortcut'], $page['shortcut_mode'], $page['uid'], $iteration - 1, $pageLog, $disableGroupCheck); 1157 } else { 1158 $pageLog[] = $page['uid']; 1159 $message = 'Page shortcuts were looping in uids ' . implode(',', $pageLog) . '...!'; 1160 $this->logger->error($message); 1161 throw new \RuntimeException($message, 1294587212); 1162 } 1163 } 1164 // Return resulting page: 1165 return $page; 1166 } 1167 1168 /** 1169 * Will find the page carrying the domain record matching the input domain. 1170 * 1171 * @param string $domain Domain name to search for. Eg. "www.typo3.com". Typical the HTTP_HOST value. 1172 * @param string $path Path for the current script in domain. Eg. "/somedir/subdir". Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('SCRIPT_NAME') 1173 * @param string $request_uri Request URI: Used to get parameters from if they should be appended. Typ. supplied by \TYPO3\CMS\Core\Utility\GeneralUtility::getIndpEnv('REQUEST_URI') 1174 * @return mixed If found, returns integer with page UID where found. Otherwise blank. Might exit if location-header is sent, see description. 1175 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::findDomainRecord() 1176 * @deprecated will be removed in TYPO3 v10.0. 1177 */ 1178 public function getDomainStartPage($domain, $path = '', $request_uri = '') 1179 { 1180 trigger_error('This method will be removed in TYPO3 v10.0. As the SiteResolver middleware resolves the domain start page.', E_USER_DEPRECATED); 1181 $domain = explode(':', $domain); 1182 $domain = strtolower(preg_replace('/\\.$/', '', $domain[0])); 1183 // Removing extra trailing slashes 1184 $path = trim(preg_replace('/\\/[^\\/]*$/', '', $path)); 1185 // Appending to domain string 1186 $domain .= $path; 1187 $domain = preg_replace('/\\/*$/', '', $domain); 1188 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 1189 $queryBuilder->getRestrictions()->removeAll(); 1190 $row = $queryBuilder 1191 ->select( 1192 'pages.uid' 1193 ) 1194 ->from('pages') 1195 ->from('sys_domain') 1196 ->where( 1197 $queryBuilder->expr()->eq('pages.uid', $queryBuilder->quoteIdentifier('sys_domain.pid')), 1198 $queryBuilder->expr()->eq( 1199 'sys_domain.hidden', 1200 $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) 1201 ), 1202 $queryBuilder->expr()->orX( 1203 $queryBuilder->expr()->eq( 1204 'sys_domain.domainName', 1205 $queryBuilder->createNamedParameter($domain, \PDO::PARAM_STR) 1206 ), 1207 $queryBuilder->expr()->eq( 1208 'sys_domain.domainName', 1209 $queryBuilder->createNamedParameter($domain . '/', \PDO::PARAM_STR) 1210 ) 1211 ), 1212 QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del), 1213 QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess) 1214 ) 1215 ->setMaxResults(1) 1216 ->execute() 1217 ->fetch(); 1218 1219 if (!$row) { 1220 return ''; 1221 } 1222 return $row['uid']; 1223 } 1224 1225 /** 1226 * Returns array with fields of the pages from here ($uid) and back to the root 1227 * 1228 * NOTICE: This function only takes deleted pages into account! So hidden, 1229 * starttime and endtime restricted pages are included no matter what. 1230 * 1231 * Further: If any "recycler" page is found (doktype=255) then it will also block 1232 * for the rootline) 1233 * 1234 * If you want more fields in the rootline records than default such can be added 1235 * by listing them in $GLOBALS['TYPO3_CONF_VARS']['FE']['addRootLineFields'] 1236 * 1237 * @param int $uid The page uid for which to seek back to the page tree root. 1238 * @param string $MP Commalist of MountPoint parameters, eg. "1-2,3-4" etc. Normally this value comes from the GET var, MP 1239 * @param bool $ignoreMPerrors If set, some errors related to Mount Points in root line are ignored. 1240 * @throws \Exception 1241 * @throws \RuntimeException 1242 * @return array Array with page records from the root line as values. The array is ordered with the outer records first and root record in the bottom. The keys are numeric but in reverse order. So if you traverse/sort the array by the numeric keys order you will get the order from root and out. If an error is found (like eternal looping or invalid mountpoint) it will return an empty array. 1243 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::getPageAndRootline() 1244 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0. 1245 */ 1246 public function getRootLine($uid, $MP = '', $ignoreMPerrors = null) 1247 { 1248 trigger_error('Calling PageRepository->getRootLine() will be removed in TYPO3 v10.0. Use RootlineUtility directly.', E_USER_DEPRECATED); 1249 $rootline = GeneralUtility::makeInstance(RootlineUtility::class, $uid, $MP, $this->context); 1250 try { 1251 return $rootline->get(); 1252 } catch (\RuntimeException $ex) { 1253 if ($ignoreMPerrors) { 1254 $this->error_getRootLine = $ex->getMessage(); 1255 if (substr($this->error_getRootLine, -7) === 'uid -1.') { 1256 $this->error_getRootLine_failPid = -1; 1257 } 1258 return []; 1259 } 1260 if ($ex->getCode() === 1343589451) { 1261 /** @see \TYPO3\CMS\Core\Utility\RootlineUtility::getRecordArray */ 1262 return []; 1263 } 1264 throw $ex; 1265 } 1266 } 1267 1268 /** 1269 * Returns the redirect URL for the input page row IF the doktype is set to 3. 1270 * 1271 * @param array $pagerow The page row to return URL type for 1272 * @return string|bool The URL from based on the data from "pages:url". False if not found. 1273 * @see \TYPO3\CMS\Frontend\Controller\TypoScriptFrontendController::initializeRedirectUrlHandlers() 1274 */ 1275 public function getExtURL($pagerow) 1276 { 1277 if ((int)$pagerow['doktype'] === self::DOKTYPE_LINK) { 1278 $redirectTo = $pagerow['url']; 1279 $uI = parse_url($redirectTo); 1280 // If relative path, prefix Site URL 1281 // If it's a valid email without protocol, add "mailto:" 1282 if (!($uI['scheme'] ?? false)) { 1283 if (GeneralUtility::validEmail($redirectTo)) { 1284 $redirectTo = 'mailto:' . $redirectTo; 1285 } elseif ($redirectTo[0] !== '/') { 1286 $redirectTo = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . $redirectTo; 1287 } 1288 } 1289 return $redirectTo; 1290 } 1291 return false; 1292 } 1293 1294 /** 1295 * Returns a MountPoint array for the specified page 1296 * 1297 * Does a recursive search if the mounted page should be a mount page 1298 * itself. 1299 * 1300 * Note: 1301 * 1302 * Recursive mount points are not supported by all parts of the core. 1303 * The usage is discouraged. They may be removed from this method. 1304 * 1305 * @see: https://decisions.typo3.org/t/supporting-or-prohibiting-recursive-mount-points/165/3 1306 * 1307 * An array will be returned if mount pages are enabled, the correct 1308 * doktype (7) is set for page and there IS a mount_pid with a valid 1309 * record. 1310 * 1311 * The optional page record must contain at least uid, pid, doktype, 1312 * mount_pid,mount_pid_ol. If it is not supplied it will be looked up by 1313 * the system at additional costs for the lookup. 1314 * 1315 * Returns FALSE if no mount point was found, "-1" if there should have been 1316 * one, but no connection to it, otherwise an array with information 1317 * about mount pid and modes. 1318 * 1319 * @param int $pageId Page id to do the lookup for. 1320 * @param array|bool $pageRec Optional page record for the given page. 1321 * @param array $prevMountPids Internal register to prevent lookup cycles. 1322 * @param int $firstPageUid The first page id. 1323 * @return mixed Mount point array or failure flags (-1, false). 1324 * @see \TYPO3\CMS\Frontend\ContentObject\Menu\AbstractMenuContentObject 1325 */ 1326 public function getMountPointInfo($pageId, $pageRec = false, $prevMountPids = [], $firstPageUid = 0) 1327 { 1328 if (!$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) { 1329 return false; 1330 } 1331 $cacheIdentifier = 'PageRepository_getMountPointInfo_' . $pageId; 1332 $cache = $this->getRuntimeCache(); 1333 if ($cache->has($cacheIdentifier)) { 1334 return $cache->get($cacheIdentifier); 1335 } 1336 $result = false; 1337 // Get pageRec if not supplied: 1338 if (!is_array($pageRec)) { 1339 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 1340 $queryBuilder->getRestrictions() 1341 ->removeAll() 1342 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1343 1344 $pageRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state') 1345 ->from('pages') 1346 ->where( 1347 $queryBuilder->expr()->eq( 1348 'uid', 1349 $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) 1350 ), 1351 $queryBuilder->expr()->neq( 1352 'doktype', 1353 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT) 1354 ) 1355 ) 1356 ->execute() 1357 ->fetch(); 1358 1359 // Only look for version overlay if page record is not supplied; This assumes 1360 // that the input record is overlaid with preview version, if any! 1361 $this->versionOL('pages', $pageRec); 1362 } 1363 // Set first Page uid: 1364 if (!$firstPageUid) { 1365 $firstPageUid = $pageRec['uid']; 1366 } 1367 // Look for mount pid value plus other required circumstances: 1368 $mount_pid = (int)$pageRec['mount_pid']; 1369 if (is_array($pageRec) && (int)$pageRec['doktype'] === self::DOKTYPE_MOUNTPOINT && $mount_pid > 0 && !in_array($mount_pid, $prevMountPids, true)) { 1370 // Get the mount point record (to verify its general existence): 1371 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages'); 1372 $queryBuilder->getRestrictions() 1373 ->removeAll() 1374 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1375 1376 $mountRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state') 1377 ->from('pages') 1378 ->where( 1379 $queryBuilder->expr()->eq( 1380 'uid', 1381 $queryBuilder->createNamedParameter($mount_pid, \PDO::PARAM_INT) 1382 ), 1383 $queryBuilder->expr()->neq( 1384 'doktype', 1385 $queryBuilder->createNamedParameter(255, \PDO::PARAM_INT) 1386 ) 1387 ) 1388 ->execute() 1389 ->fetch(); 1390 1391 $this->versionOL('pages', $mountRec); 1392 if (is_array($mountRec)) { 1393 // Look for recursive mount point: 1394 $prevMountPids[] = $mount_pid; 1395 $recursiveMountPid = $this->getMountPointInfo($mount_pid, $mountRec, $prevMountPids, $firstPageUid); 1396 // Return mount point information: 1397 $result = $recursiveMountPid ?: [ 1398 'mount_pid' => $mount_pid, 1399 'overlay' => $pageRec['mount_pid_ol'], 1400 'MPvar' => $mount_pid . '-' . $firstPageUid, 1401 'mount_point_rec' => $pageRec, 1402 'mount_pid_rec' => $mountRec 1403 ]; 1404 } else { 1405 // Means, there SHOULD have been a mount point, but there was none! 1406 $result = -1; 1407 } 1408 } 1409 $cache->set($cacheIdentifier, $result); 1410 return $result; 1411 } 1412 1413 /******************************** 1414 * 1415 * Selecting records in general 1416 * 1417 ********************************/ 1418 1419 /** 1420 * Checks if a record exists and is accessible. 1421 * The row is returned if everything's OK. 1422 * 1423 * @param string $table The table name to search 1424 * @param int $uid The uid to look up in $table 1425 * @param bool|int $checkPage If checkPage is set, it's also required that the page on which the record resides is accessible 1426 * @return array|int Returns array (the record) if OK, otherwise blank/0 (zero) 1427 */ 1428 public function checkRecord($table, $uid, $checkPage = 0) 1429 { 1430 $uid = (int)$uid; 1431 if (is_array($GLOBALS['TCA'][$table]) && $uid > 0) { 1432 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1433 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)); 1434 $row = $queryBuilder->select('*') 1435 ->from($table) 1436 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))) 1437 ->execute() 1438 ->fetch(); 1439 1440 if ($row) { 1441 $this->versionOL($table, $row); 1442 if (is_array($row)) { 1443 if ($checkPage) { 1444 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1445 ->getQueryBuilderForTable('pages'); 1446 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)); 1447 $numRows = (int)$queryBuilder->count('*') 1448 ->from('pages') 1449 ->where( 1450 $queryBuilder->expr()->eq( 1451 'uid', 1452 $queryBuilder->createNamedParameter($row['pid'], \PDO::PARAM_INT) 1453 ) 1454 ) 1455 ->execute() 1456 ->fetchColumn(); 1457 if ($numRows > 0) { 1458 return $row; 1459 } 1460 return 0; 1461 } 1462 return $row; 1463 } 1464 } 1465 } 1466 return 0; 1467 } 1468 1469 /** 1470 * Returns record no matter what - except if record is deleted 1471 * 1472 * @param string $table The table name to search 1473 * @param int $uid The uid to look up in $table 1474 * @param string $fields The fields to select, default is "* 1475 * @param bool $noWSOL If set, no version overlay is applied 1476 * @return mixed Returns array (the record) if found, otherwise blank/0 (zero) 1477 * @see getPage_noCheck() 1478 */ 1479 public function getRawRecord($table, $uid, $fields = '*', $noWSOL = null) 1480 { 1481 $uid = (int)$uid; 1482 if (isset($GLOBALS['TCA'][$table]) && is_array($GLOBALS['TCA'][$table]) && $uid > 0) { 1483 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1484 $queryBuilder->getRestrictions() 1485 ->removeAll() 1486 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1487 $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true)) 1488 ->from($table) 1489 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))) 1490 ->execute() 1491 ->fetch(); 1492 1493 if ($row) { 1494 if ($noWSOL !== null) { 1495 trigger_error('The fourth parameter of PageRepository->getRawRecord() will be removed in TYPO3 v10.0. Use a SQL statement directly.', E_USER_DEPRECATED); 1496 } 1497 // @deprecated - remove this if-clause in TYPO3 v10.0 1498 if (!$noWSOL) { 1499 $this->versionOL($table, $row); 1500 } 1501 if (is_array($row)) { 1502 return $row; 1503 } 1504 } 1505 } 1506 return 0; 1507 } 1508 1509 /** 1510 * Selects records based on matching a field (ei. other than UID) with a value 1511 * 1512 * @param string $theTable The table name to search, eg. "pages" or "tt_content 1513 * @param string $theField The fieldname to match, eg. "uid" or "alias 1514 * @param string $theValue The value that fieldname must match, eg. "123" or "frontpage 1515 * @param string $whereClause Optional additional WHERE clauses put in the end of the query. DO NOT PUT IN GROUP BY, ORDER BY or LIMIT! 1516 * @param string $groupBy Optional GROUP BY field(s). If none, supply blank string. 1517 * @param string $orderBy Optional ORDER BY field(s). If none, supply blank string. 1518 * @param string $limit Optional LIMIT value ([begin,]max). If none, supply blank string. 1519 * @return mixed Returns array (the record) if found, otherwise nothing (void) 1520 * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0 1521 */ 1522 public function getRecordsByField($theTable, $theField, $theValue, $whereClause = '', $groupBy = '', $orderBy = '', $limit = '') 1523 { 1524 trigger_error('PageRepository->getRecordsByField() should not be used any longer, this method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 1525 if (is_array($GLOBALS['TCA'][$theTable])) { 1526 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($theTable); 1527 $queryBuilder->getRestrictions() 1528 ->removeAll() 1529 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1530 1531 $queryBuilder->select('*') 1532 ->from($theTable) 1533 ->where($queryBuilder->expr()->eq($theField, $queryBuilder->createNamedParameter($theValue))); 1534 1535 if ($whereClause !== '') { 1536 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($whereClause)); 1537 } 1538 1539 if ($groupBy !== '') { 1540 $queryBuilder->groupBy(QueryHelper::parseGroupBy($groupBy)); 1541 } 1542 1543 if ($orderBy !== '') { 1544 foreach (QueryHelper::parseOrderBy($orderBy) as $orderPair) { 1545 [$fieldName, $order] = $orderPair; 1546 $queryBuilder->addOrderBy($fieldName, $order); 1547 } 1548 } 1549 1550 if ($limit !== '') { 1551 if (strpos($limit, ',')) { 1552 $limitOffsetAndMax = GeneralUtility::intExplode(',', $limit); 1553 $queryBuilder->setFirstResult((int)$limitOffsetAndMax[0]); 1554 $queryBuilder->setMaxResults((int)$limitOffsetAndMax[1]); 1555 } else { 1556 $queryBuilder->setMaxResults((int)$limit); 1557 } 1558 } 1559 1560 $rows = $queryBuilder->execute()->fetchAll(); 1561 1562 if (!empty($rows)) { 1563 return $rows; 1564 } 1565 } 1566 return null; 1567 } 1568 1569 /******************************** 1570 * 1571 * Standard clauses 1572 * 1573 ********************************/ 1574 1575 /** 1576 * Returns the "AND NOT deleted" clause for the tablename given IF 1577 * $GLOBALS['TCA'] configuration points to such a field. 1578 * 1579 * @param string $table Tablename 1580 * @return string 1581 * @see enableFields() 1582 * @deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use QueryBuilders' Restrictions directly instead. 1583 */ 1584 public function deleteClause($table) 1585 { 1586 trigger_error('PageRepository->deleteClause() will be removed in TYPO3 v10.0. The delete clause can be applied via the DeletedRestrictions via QueryBuilder.', E_USER_DEPRECATED); 1587 return $GLOBALS['TCA'][$table]['ctrl']['delete'] ? ' AND ' . $table . '.' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0' : ''; 1588 } 1589 1590 /** 1591 * Returns a part of a WHERE clause which will filter out records with start/end 1592 * times or hidden/fe_groups fields set to values that should de-select them 1593 * according to the current time, preview settings or user login. Definitely a 1594 * frontend function. 1595 * 1596 * Is using the $GLOBALS['TCA'] arrays "ctrl" part where the key "enablefields" 1597 * determines for each table which of these features applies to that table. 1598 * 1599 * @param string $table Table name found in the $GLOBALS['TCA'] array 1600 * @param int $show_hidden If $show_hidden is set (0/1), any hidden-fields in records are ignored. NOTICE: If you call this function, consider what to do with the show_hidden parameter. Maybe it should be set? See ContentObjectRenderer->enableFields where it's implemented correctly. 1601 * @param array $ignore_array Array you can pass where keys can be "disabled", "starttime", "endtime", "fe_group" (keys from "enablefields" in TCA) and if set they will make sure that part of the clause is not added. Thus disables the specific part of the clause. For previewing etc. 1602 * @param bool $noVersionPreview If set, enableFields will be applied regardless of any versioning preview settings which might otherwise disable enableFields 1603 * @throws \InvalidArgumentException 1604 * @return string The clause starting like " AND ...=... AND ...=... 1605 * @see \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::enableFields() 1606 */ 1607 public function enableFields($table, $show_hidden = -1, $ignore_array = [], $noVersionPreview = false) 1608 { 1609 if ($show_hidden === -1) { 1610 // If show_hidden was not set from outside, use the current context 1611 $show_hidden = (int)$this->context->getPropertyFromAspect('visibility', $table === 'pages' ? 'includeHiddenPages' : 'includeHiddenContent', false); 1612 } 1613 // If show_hidden was not changed during the previous evaluation, do it here. 1614 $ctrl = $GLOBALS['TCA'][$table]['ctrl'] ?? null; 1615 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1616 ->getQueryBuilderForTable($table) 1617 ->expr(); 1618 $constraints = []; 1619 if (is_array($ctrl)) { 1620 // Delete field check: 1621 if ($ctrl['delete']) { 1622 $constraints[] = $expressionBuilder->eq($table . '.' . $ctrl['delete'], 0); 1623 } 1624 if ($ctrl['versioningWS'] ?? false) { 1625 if (!$this->versioningWorkspaceId > 0) { 1626 // Filter out placeholder records (new/moved/deleted items) 1627 // in case we are NOT in a versioning preview (that means we are online!) 1628 $constraints[] = $expressionBuilder->lte( 1629 $table . '.t3ver_state', 1630 new VersionState(VersionState::DEFAULT_STATE) 1631 ); 1632 } elseif ($table !== 'pages') { 1633 // show only records of live and of the current workspace 1634 // in case we are in a versioning preview 1635 $constraints[] = $expressionBuilder->orX( 1636 $expressionBuilder->eq($table . '.t3ver_wsid', 0), 1637 $expressionBuilder->eq($table . '.t3ver_wsid', (int)$this->versioningWorkspaceId) 1638 ); 1639 } 1640 1641 // Filter out versioned records 1642 if (!$noVersionPreview && empty($ignore_array['pid'])) { 1643 $constraints[] = $expressionBuilder->neq($table . '.pid', -1); 1644 } 1645 } 1646 1647 // Enable fields: 1648 if (is_array($ctrl['enablecolumns'])) { 1649 // In case of versioning-preview, enableFields are ignored (checked in 1650 // versionOL()) 1651 if ($this->versioningWorkspaceId <= 0 || !$ctrl['versioningWS'] || $noVersionPreview) { 1652 if (($ctrl['enablecolumns']['disabled'] ?? false) && !$show_hidden && !($ignore_array['disabled'] ?? false)) { 1653 $field = $table . '.' . $ctrl['enablecolumns']['disabled']; 1654 $constraints[] = $expressionBuilder->eq($field, 0); 1655 } 1656 if (($ctrl['enablecolumns']['starttime'] ?? false) && !($ignore_array['starttime'] ?? false)) { 1657 $field = $table . '.' . $ctrl['enablecolumns']['starttime']; 1658 $constraints[] = $expressionBuilder->lte( 1659 $field, 1660 $this->context->getPropertyFromAspect('date', 'accessTime', 0) 1661 ); 1662 } 1663 if (($ctrl['enablecolumns']['endtime'] ?? false) && !($ignore_array['endtime'] ?? false)) { 1664 $field = $table . '.' . $ctrl['enablecolumns']['endtime']; 1665 $constraints[] = $expressionBuilder->orX( 1666 $expressionBuilder->eq($field, 0), 1667 $expressionBuilder->gt( 1668 $field, 1669 $this->context->getPropertyFromAspect('date', 'accessTime', 0) 1670 ) 1671 ); 1672 } 1673 if (($ctrl['enablecolumns']['fe_group'] ?? false) && !($ignore_array['fe_group'] ?? false)) { 1674 $field = $table . '.' . $ctrl['enablecolumns']['fe_group']; 1675 $constraints[] = QueryHelper::stripLogicalOperatorPrefix( 1676 $this->getMultipleGroupsWhereClause($field, $table) 1677 ); 1678 } 1679 // Call hook functions for additional enableColumns 1680 // It is used by the extension ingmar_accessctrl which enables assigning more 1681 // than one usergroup to content and page records 1682 $_params = [ 1683 'table' => $table, 1684 'show_hidden' => $show_hidden, 1685 'ignore_array' => $ignore_array, 1686 'ctrl' => $ctrl 1687 ]; 1688 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'] ?? [] as $_funcRef) { 1689 $constraints[] = QueryHelper::stripLogicalOperatorPrefix( 1690 GeneralUtility::callUserFunction($_funcRef, $_params, $this) 1691 ); 1692 } 1693 } 1694 } 1695 } else { 1696 throw new \InvalidArgumentException('There is no entry in the $TCA array for the table "' . $table . '". This means that the function enableFields() is ' . 'called with an invalid table name as argument.', 1283790586); 1697 } 1698 1699 return empty($constraints) ? '' : ' AND ' . $expressionBuilder->andX(...$constraints); 1700 } 1701 1702 /** 1703 * Creating where-clause for checking group access to elements in enableFields 1704 * function 1705 * 1706 * @param string $field Field with group list 1707 * @param string $table Table name 1708 * @return string AND sql-clause 1709 * @see enableFields() 1710 */ 1711 public function getMultipleGroupsWhereClause($field, $table) 1712 { 1713 if (!$this->context->hasAspect('frontend.user')) { 1714 return ''; 1715 } 1716 /** @var UserAspect $userAspect */ 1717 $userAspect = $this->context->getAspect('frontend.user'); 1718 $memberGroups = $userAspect->getGroupIds(); 1719 $cache = $this->getRuntimeCache(); 1720 $cacheIdentifier = 'PageRepository_groupAccessWhere_' . md5($field . '_' . $table . '_' . implode('_', $memberGroups)); 1721 $cacheEntry = $cache->get($cacheIdentifier); 1722 if ($cacheEntry) { 1723 return $cacheEntry; 1724 } 1725 $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1726 ->getQueryBuilderForTable($table) 1727 ->expr(); 1728 $orChecks = []; 1729 // If the field is empty, then OK 1730 $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal('')); 1731 // If the field is NULL, then OK 1732 $orChecks[] = $expressionBuilder->isNull($field); 1733 // If the field contains zero, then OK 1734 $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal('0')); 1735 foreach ($memberGroups as $value) { 1736 $orChecks[] = $expressionBuilder->inSet($field, $expressionBuilder->literal($value)); 1737 } 1738 1739 $accessGroupWhere = ' AND (' . $expressionBuilder->orX(...$orChecks) . ')'; 1740 $cache->set($cacheIdentifier, $accessGroupWhere); 1741 return $accessGroupWhere; 1742 } 1743 1744 /********************** 1745 * 1746 * Versioning Preview 1747 * 1748 **********************/ 1749 1750 /** 1751 * Finding online PID for offline version record 1752 * 1753 * ONLY active when backend user is previewing records. MUST NEVER affect a site 1754 * served which is not previewed by backend users!!! 1755 * 1756 * Will look if the "pid" value of the input record is -1 (it is an offline 1757 * version) and if the table supports versioning; if so, it will translate the -1 1758 * PID into the PID of the original record. 1759 * 1760 * Used whenever you are tracking something back, like making the root line. 1761 * 1762 * Principle; Record offline! => Find online? 1763 * 1764 * @param string $table Table name 1765 * @param array $rr Record array passed by reference. As minimum, "pid" and "uid" fields must exist! "t3ver_oid" and "t3ver_wsid" is nice and will save you a DB query. 1766 * @see BackendUtility::fixVersioningPid(), versionOL(), getRootLine() 1767 */ 1768 public function fixVersioningPid($table, &$rr) 1769 { 1770 if ($this->versioningWorkspaceId > 0 && is_array($rr) && (int)$rr['pid'] === -1 && $GLOBALS['TCA'][$table]['ctrl']['versioningWS']) { 1771 $oid = 0; 1772 $wsid = 0; 1773 // Check values for t3ver_oid and t3ver_wsid: 1774 if (isset($rr['t3ver_oid']) && isset($rr['t3ver_wsid'])) { 1775 // If "t3ver_oid" is already a field, just set this: 1776 $oid = $rr['t3ver_oid']; 1777 $wsid = $rr['t3ver_wsid']; 1778 } else { 1779 // Otherwise we have to expect "uid" to be in the record and look up based 1780 // on this: 1781 $uid = (int)$rr['uid']; 1782 if ($uid > 0) { 1783 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1784 $queryBuilder->getRestrictions() 1785 ->removeAll() 1786 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1787 $newPidRec = $queryBuilder->select('t3ver_oid', 't3ver_wsid') 1788 ->from($table) 1789 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT))) 1790 ->execute() 1791 ->fetch(); 1792 1793 if (is_array($newPidRec)) { 1794 $oid = $newPidRec['t3ver_oid']; 1795 $wsid = $newPidRec['t3ver_wsid']; 1796 } 1797 } 1798 } 1799 // If workspace ids matches and ID of current online version is found, look up 1800 // the PID value of that: 1801 if ($oid && (int)$wsid === (int)$this->versioningWorkspaceId) { 1802 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1803 $queryBuilder->getRestrictions() 1804 ->removeAll() 1805 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1806 $oidRec = $queryBuilder->select('pid') 1807 ->from($table) 1808 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($oid, \PDO::PARAM_INT))) 1809 ->execute() 1810 ->fetch(); 1811 1812 if (is_array($oidRec)) { 1813 // SWAP uid as well? Well no, because when fixing a versioning PID happens it is 1814 // assumed that this is a "branch" type page and therefore the uid should be 1815 // kept (like in versionOL()). However if the page is NOT a branch version it 1816 // should not happen - but then again, direct access to that uid should not 1817 // happen! 1818 $rr['_ORIG_pid'] = $rr['pid']; 1819 $rr['pid'] = $oidRec['pid']; 1820 } 1821 } 1822 } 1823 // Changing PID in case of moving pointer: 1824 if ($movePlhRec = $this->getMovePlaceholder($table, $rr['uid'], 'pid')) { 1825 $rr['pid'] = $movePlhRec['pid']; 1826 } 1827 } 1828 1829 /** 1830 * Versioning Preview Overlay 1831 * 1832 * ONLY active when backend user is previewing records. MUST NEVER affect a site 1833 * served which is not previewed by backend users!!! 1834 * 1835 * Generally ALWAYS used when records are selected based on uid or pid. If 1836 * records are selected on other fields than uid or pid (eg. "email = ....") then 1837 * usage might produce undesired results and that should be evaluated on 1838 * individual basis. 1839 * 1840 * Principle; Record online! => Find offline? 1841 * 1842 * @param string $table Table name 1843 * @param array $row Record array passed by reference. As minimum, the "uid", "pid" and "t3ver_state" fields must exist! The record MAY be set to FALSE in which case the calling function should act as if the record is forbidden to access! 1844 * @param bool $unsetMovePointers If set, the $row is cleared in case it is a move-pointer. This is only for preview of moved records (to remove the record from the original location so it appears only in the new location) 1845 * @param bool $bypassEnableFieldsCheck Unless this option is TRUE, the $row is unset if enablefields for BOTH the version AND the online record deselects it. This is because when versionOL() is called it is assumed that the online record is already selected with no regards to it's enablefields. However, after looking for a new version the online record enablefields must ALSO be evaluated of course. This is done all by this function! 1846 * @see fixVersioningPid(), BackendUtility::workspaceOL() 1847 */ 1848 public function versionOL($table, &$row, $unsetMovePointers = false, $bypassEnableFieldsCheck = false) 1849 { 1850 if ($this->versioningWorkspaceId > 0 && is_array($row)) { 1851 // will overlay any movePlhOL found with the real record, which in turn 1852 // will be overlaid with its workspace version if any. 1853 $movePldSwap = $this->movePlhOL($table, $row); 1854 // implode(',',array_keys($row)) = Using fields from original record to make 1855 // sure no additional fields are selected. This is best for eg. getPageOverlay() 1856 // Computed properties are excluded since those would lead to SQL errors. 1857 $fieldNames = implode(',', array_keys($this->purgeComputedProperties($row))); 1858 if ($wsAlt = $this->getWorkspaceVersionOfRecord($this->versioningWorkspaceId, $table, $row['uid'], $fieldNames, $bypassEnableFieldsCheck)) { 1859 if (is_array($wsAlt)) { 1860 // Always fix PID (like in fixVersioningPid() above). [This is usually not 1861 // the important factor for versioning OL] 1862 // Keep the old (-1) - indicates it was a version... 1863 $wsAlt['_ORIG_pid'] = $wsAlt['pid']; 1864 // Set in the online versions PID. 1865 $wsAlt['pid'] = $row['pid']; 1866 // For versions of single elements or page+content, preserve online UID and PID 1867 // (this will produce true "overlay" of element _content_, not any references) 1868 // For page+content the "_ORIG_uid" should actually be used as PID for selection. 1869 $wsAlt['_ORIG_uid'] = $wsAlt['uid']; 1870 $wsAlt['uid'] = $row['uid']; 1871 // Translate page alias as well so links are pointing to the _online_ page: 1872 if ($table === 'pages') { 1873 $wsAlt['alias'] = $row['alias']; 1874 } 1875 // Changing input record to the workspace version alternative: 1876 $row = $wsAlt; 1877 // Check if it is deleted/new 1878 $rowVersionState = VersionState::cast($row['t3ver_state'] ?? null); 1879 if ( 1880 $rowVersionState->equals(VersionState::NEW_PLACEHOLDER) 1881 || $rowVersionState->equals(VersionState::DELETE_PLACEHOLDER) 1882 ) { 1883 // Unset record if it turned out to be deleted in workspace 1884 $row = false; 1885 } 1886 // Check if move-pointer in workspace (unless if a move-placeholder is the 1887 // reason why it appears!): 1888 // You have to specifically set $unsetMovePointers in order to clear these 1889 // because it is normally a display issue if it should be shown or not. 1890 if ( 1891 ( 1892 $rowVersionState->equals(VersionState::MOVE_POINTER) 1893 && !$movePldSwap 1894 ) && $unsetMovePointers 1895 ) { 1896 // Unset record if it turned out to be deleted in workspace 1897 $row = false; 1898 } 1899 } else { 1900 // No version found, then check if t3ver_state = VersionState::NEW_PLACEHOLDER 1901 // (online version is dummy-representation) 1902 // Notice, that unless $bypassEnableFieldsCheck is TRUE, the $row is unset if 1903 // enablefields for BOTH the version AND the online record deselects it. See 1904 // note for $bypassEnableFieldsCheck 1905 /** @var \TYPO3\CMS\Core\Versioning\VersionState $versionState */ 1906 $versionState = VersionState::cast($row['t3ver_state']); 1907 if ($wsAlt <= -1 || $versionState->indicatesPlaceholder()) { 1908 // Unset record if it turned out to be "hidden" 1909 $row = false; 1910 } 1911 } 1912 } 1913 } 1914 } 1915 1916 /** 1917 * Checks if record is a move-placeholder 1918 * (t3ver_state==VersionState::MOVE_PLACEHOLDER) and if so it will set $row to be 1919 * the pointed-to live record (and return TRUE) Used from versionOL 1920 * 1921 * @param string $table Table name 1922 * @param array $row Row (passed by reference) - only online records... 1923 * @return bool TRUE if overlay is made. 1924 * @see BackendUtility::movePlhOl() 1925 */ 1926 protected function movePlhOL($table, &$row) 1927 { 1928 if (!empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) 1929 && (int)VersionState::cast($row['t3ver_state'])->equals(VersionState::MOVE_PLACEHOLDER) 1930 ) { 1931 $moveID = 0; 1932 // If t3ver_move_id is not found, then find it (but we like best if it is here) 1933 if (!isset($row['t3ver_move_id'])) { 1934 if ((int)$row['uid'] > 0) { 1935 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1936 $queryBuilder->getRestrictions() 1937 ->removeAll() 1938 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1939 $moveIDRec = $queryBuilder->select('t3ver_move_id') 1940 ->from($table) 1941 ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT))) 1942 ->execute() 1943 ->fetch(); 1944 1945 if (is_array($moveIDRec)) { 1946 $moveID = $moveIDRec['t3ver_move_id']; 1947 } 1948 } 1949 } else { 1950 $moveID = $row['t3ver_move_id']; 1951 } 1952 // Find pointed-to record. 1953 if ($moveID) { 1954 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1955 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)); 1956 $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class); 1957 $origRow = $queryBuilder->select(...array_keys($this->purgeComputedProperties($row))) 1958 ->from($table) 1959 ->where( 1960 $queryBuilder->expr()->eq( 1961 'uid', 1962 $queryBuilder->createNamedParameter($moveID, \PDO::PARAM_INT) 1963 ) 1964 ) 1965 ->setMaxResults(1) 1966 ->execute() 1967 ->fetch(); 1968 1969 if ($origRow) { 1970 $row = $origRow; 1971 return true; 1972 } 1973 } 1974 } 1975 return false; 1976 } 1977 1978 /** 1979 * Returns move placeholder of online (live) version 1980 * 1981 * @param string $table Table name 1982 * @param int $uid Record UID of online version 1983 * @param string $fields Field list, default is * 1984 * @return array If found, the record, otherwise nothing. 1985 * @see BackendUtility::getMovePlaceholder() 1986 */ 1987 protected function getMovePlaceholder($table, $uid, $fields = '*') 1988 { 1989 $workspace = (int)$this->versioningWorkspaceId; 1990 if (!empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) && $workspace > 0) { 1991 // Select workspace version of record: 1992 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 1993 $queryBuilder->getRestrictions() 1994 ->removeAll() 1995 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 1996 1997 $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true)) 1998 ->from($table) 1999 ->where( 2000 $queryBuilder->expr()->neq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)), 2001 $queryBuilder->expr()->eq( 2002 't3ver_state', 2003 $queryBuilder->createNamedParameter( 2004 (string)VersionState::cast(VersionState::MOVE_PLACEHOLDER), 2005 \PDO::PARAM_INT 2006 ) 2007 ), 2008 $queryBuilder->expr()->eq( 2009 't3ver_move_id', 2010 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 2011 ), 2012 $queryBuilder->expr()->eq( 2013 't3ver_wsid', 2014 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT) 2015 ) 2016 ) 2017 ->setMaxResults(1) 2018 ->execute() 2019 ->fetch(); 2020 2021 if (is_array($row)) { 2022 return $row; 2023 } 2024 } 2025 return false; 2026 } 2027 2028 /** 2029 * Select the version of a record for a workspace 2030 * 2031 * @param int $workspace Workspace ID 2032 * @param string $table Table name to select from 2033 * @param int $uid Record uid for which to find workspace version. 2034 * @param string $fields Field list to select 2035 * @param bool $bypassEnableFieldsCheck If TRUE, enablefields are not checked for. 2036 * @return mixed If found, return record, otherwise other value: Returns 1 if version was sought for but not found, returns -1/-2 if record (offline/online) existed but had enableFields that would disable it. Returns FALSE if not in workspace or no versioning for record. Notice, that the enablefields of the online record is also tested. 2037 * @see BackendUtility::getWorkspaceVersionOfRecord() 2038 */ 2039 public function getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields = '*', $bypassEnableFieldsCheck = false) 2040 { 2041 if ($workspace !== 0 && !empty($GLOBALS['TCA'][$table]['ctrl']['versioningWS'])) { 2042 $workspace = (int)$workspace; 2043 $uid = (int)$uid; 2044 // Select workspace version of record, only testing for deleted. 2045 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 2046 $queryBuilder->getRestrictions() 2047 ->removeAll() 2048 ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); 2049 2050 $newrow = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fields, true)) 2051 ->from($table) 2052 ->where( 2053 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)), 2054 $queryBuilder->expr()->eq( 2055 't3ver_oid', 2056 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 2057 ), 2058 $queryBuilder->expr()->eq( 2059 't3ver_wsid', 2060 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT) 2061 ) 2062 ) 2063 ->setMaxResults(1) 2064 ->execute() 2065 ->fetch(); 2066 2067 // If version found, check if it could have been selected with enableFields on 2068 // as well: 2069 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table); 2070 $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)); 2071 // Remove the frontend workspace restriction because we are testing a version record 2072 $queryBuilder->getRestrictions()->removeByType(FrontendWorkspaceRestriction::class); 2073 $queryBuilder->select('uid') 2074 ->from($table) 2075 ->setMaxResults(1); 2076 2077 if (is_array($newrow)) { 2078 $queryBuilder->where( 2079 $queryBuilder->expr()->eq('pid', $queryBuilder->createNamedParameter(-1, \PDO::PARAM_INT)), 2080 $queryBuilder->expr()->eq( 2081 't3ver_oid', 2082 $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT) 2083 ), 2084 $queryBuilder->expr()->eq( 2085 't3ver_wsid', 2086 $queryBuilder->createNamedParameter($workspace, \PDO::PARAM_INT) 2087 ) 2088 ); 2089 if ($bypassEnableFieldsCheck || $queryBuilder->execute()->fetchColumn()) { 2090 // Return offline version, tested for its enableFields. 2091 return $newrow; 2092 } 2093 // Return -1 because offline version was de-selected due to its enableFields. 2094 return -1; 2095 } 2096 // OK, so no workspace version was found. Then check if online version can be 2097 // selected with full enable fields and if so, return 1: 2098 $queryBuilder->where( 2099 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)) 2100 ); 2101 if ($bypassEnableFieldsCheck || $queryBuilder->execute()->fetchColumn()) { 2102 // Means search was done, but no version found. 2103 return 1; 2104 } 2105 // Return -2 because the online record was de-selected due to its enableFields. 2106 return -2; 2107 } 2108 // No look up in database because versioning not enabled / or workspace not 2109 // offline 2110 return false; 2111 } 2112 2113 /** 2114 * Checks if user has access to workspace. 2115 * 2116 * @param int $wsid Workspace ID 2117 * @return bool true if the backend user has access to a certain workspace 2118 * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0. Use $BE_USER->checkWorkspace() directly if necessary. 2119 */ 2120 public function checkWorkspaceAccess($wsid) 2121 { 2122 trigger_error('PageRepository->checkWorkspaceAccess() will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 2123 if (!$this->getBackendUser() || !ExtensionManagementUtility::isLoaded('workspaces')) { 2124 return false; 2125 } 2126 if (!isset($this->workspaceCache[$wsid])) { 2127 $this->workspaceCache[$wsid] = $this->getBackendUser()->checkWorkspace($wsid); 2128 } 2129 return (string)$this->workspaceCache[$wsid]['_ACCESS'] !== ''; 2130 } 2131 2132 /** 2133 * Gets file references for a given record field. 2134 * 2135 * @param string $tableName Name of the table 2136 * @param string $fieldName Name of the field 2137 * @param array $element The parent element referencing to files 2138 * @return array 2139 * @deprecated since TYPO3 v9.4, will be removed in TYPO3 v10.0 2140 */ 2141 public function getFileReferences($tableName, $fieldName, array $element) 2142 { 2143 trigger_error('PageRepository->getFileReferences() should not be used any longer, this method will be removed in TYPO3 v10.0.', E_USER_DEPRECATED); 2144 /** @var FileRepository $fileRepository */ 2145 $fileRepository = GeneralUtility::makeInstance(FileRepository::class); 2146 $currentId = !empty($element['uid']) ? $element['uid'] : 0; 2147 2148 // Fetch the references of the default element 2149 try { 2150 $references = $fileRepository->findByRelation($tableName, $fieldName, $currentId); 2151 } catch (FileDoesNotExistException $e) { 2152 /** 2153 * We just catch the exception here 2154 * Reasoning: There is nothing an editor or even admin could do 2155 */ 2156 return []; 2157 } catch (\InvalidArgumentException $e) { 2158 /** 2159 * The storage does not exist anymore 2160 * Log the exception message for admins as they maybe can restore the storage 2161 */ 2162 $logMessage = $e->getMessage() . ' (table: "' . $tableName . '", fieldName: "' . $fieldName . '", currentId: ' . $currentId . ')'; 2163 $this->logger->error($logMessage, ['exception' => $e]); 2164 return []; 2165 } 2166 2167 $localizedId = null; 2168 if (isset($element['_LOCALIZED_UID'])) { 2169 $localizedId = $element['_LOCALIZED_UID']; 2170 } elseif (isset($element['_PAGES_OVERLAY_UID'])) { 2171 $localizedId = $element['_PAGES_OVERLAY_UID']; 2172 } 2173 2174 $isTableLocalizable = ( 2175 !empty($GLOBALS['TCA'][$tableName]['ctrl']['languageField']) 2176 && !empty($GLOBALS['TCA'][$tableName]['ctrl']['transOrigPointerField']) 2177 ); 2178 if ($isTableLocalizable && $localizedId !== null) { 2179 $localizedReferences = $fileRepository->findByRelation($tableName, $fieldName, $localizedId); 2180 $references = $localizedReferences; 2181 } 2182 2183 return $references; 2184 } 2185 2186 /** 2187 * Purges computed properties from database rows, 2188 * such as _ORIG_uid or _ORIG_pid for instance. 2189 * 2190 * @param array $row 2191 * @return array 2192 */ 2193 protected function purgeComputedProperties(array $row) 2194 { 2195 foreach ($this->computedPropertyNames as $computedPropertyName) { 2196 if (array_key_exists($computedPropertyName, $row)) { 2197 unset($row[$computedPropertyName]); 2198 } 2199 } 2200 return $row; 2201 } 2202 2203 /** 2204 * Returns the current BE user. 2205 * 2206 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication 2207 * @deprecated will be removed in TYPO3 v10.0 as will not be used anymore then because checkWorkspaceAccess() will be removed. 2208 */ 2209 protected function getBackendUser() 2210 { 2211 return $GLOBALS['BE_USER']; 2212 } 2213 2214 /** 2215 * @return VariableFrontend 2216 */ 2217 protected function getRuntimeCache(): VariableFrontend 2218 { 2219 return GeneralUtility::makeInstance(CacheManager::class)->getCache('cache_runtime'); 2220 } 2221} 2222