‪TYPO3CMS  ‪main
PageRepository.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
17 
18 use Psr\EventDispatcher\EventDispatcherInterface;
19 use Psr\Log\LoggerAwareInterface;
20 use Psr\Log\LoggerAwareTrait;
51 
60 class ‪PageRepository implements LoggerAwareInterface
61 {
62  use LoggerAwareTrait;
63 
69  protected string ‪$where_hid_del = ' AND pages.deleted=0';
70 
74  protected string ‪$where_groupAccess = '';
75 
79  protected int ‪$sys_language_uid = 0;
80 
88  protected int ‪$versioningWorkspaceId = 0;
89 
93  protected array ‪$computedPropertyNames = [
94  '_LOCALIZED_UID',
95  '_MP_PARAM',
96  '_ORIG_uid',
97  '_ORIG_pid',
98  '_SHORTCUT_ORIGINAL_PAGE_UID',
99  '_PAGES_OVERLAY',
100  '_PAGES_OVERLAY_UID',
101  '_PAGES_OVERLAY_LANGUAGE',
102  '_PAGES_OVERLAY_REQUESTEDLANGUAGE',
103  ];
104 
108  public const ‪DOKTYPE_DEFAULT = 1;
109  public const ‪DOKTYPE_LINK = 3;
110  public const ‪DOKTYPE_SHORTCUT = 4;
111  public const ‪DOKTYPE_BE_USER_SECTION = 6;
112  public const ‪DOKTYPE_MOUNTPOINT = 7;
113  public const ‪DOKTYPE_SPACER = 199;
114  public const ‪DOKTYPE_SYSFOLDER = 254;
115 
119  public const ‪SHORTCUT_MODE_NONE = 0;
123 
125 
130  public function ‪__construct(‪Context ‪$context = null)
131  {
132  $this->context = ‪$context ?? GeneralUtility::makeInstance(Context::class);
133  $this->versioningWorkspaceId = $this->context->getPropertyFromAspect('workspace', 'id');
134  // Only set up the where clauses for pages when TCA is set. This usually happens only in tests.
135  // Once all tests are written very well, this can be removed again
136  if (isset(‪$GLOBALS['TCA']['pages'])) {
137  $this->‪init($this->context->getPropertyFromAspect('visibility', 'includeHiddenPages'));
138  $this->where_groupAccess = $this->‪getMultipleGroupsWhereClause('pages.fe_group', 'pages');
139  $this->sys_language_uid = (int)$this->context->getPropertyFromAspect('language', 'id', 0);
140  }
141  }
142 
152  protected function ‪init($show_hidden)
153  {
154  $this->where_groupAccess = '';
155  // As PageRepository may be used multiple times during the frontend request, and may
156  // actually be used before the usergroups have been resolved, self::getMultipleGroupsWhereClause()
157  // and the hook in ->enableFields() need to be reconsidered when the usergroup state changes.
158  // When something changes in the context, a second runtime cache entry is built.
159  // However, the PageRepository is generally in use for generating e.g. hundreds of links, so they would all use
160  // the same cache identifier.
161  $userAspect = $this->context->getAspect('frontend.user');
162  $frontendUserIdentifier = 'user_' . (int)$userAspect->get('id') . '_groups_' . md5(implode(',', $userAspect->getGroupIds()));
163 
164  // We need to respect the date aspect as we might have subrequests with a different time (e.g. backend preview links)
165  $dateTimeIdentifier = $this->context->getAspect('date')->get('timestamp');
166 
167  $cache = $this->‪getRuntimeCache();
168  $cacheIdentifier = 'PageRepository_hidDelWhere' . ($show_hidden ? 'ShowHidden' : '') . '_' . (int)$this->versioningWorkspaceId . '_' . $frontendUserIdentifier . '_' . $dateTimeIdentifier;
169  $cacheEntry = $cache->get($cacheIdentifier);
170  if ($cacheEntry) {
171  $this->where_hid_del = $cacheEntry;
172  } else {
173  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
174  ->getQueryBuilderForTable('pages')
175  ->expr();
176  if ($this->versioningWorkspaceId > 0) {
177  // For version previewing, make sure that enable-fields are not
178  // de-selecting hidden pages - we need versionOL() to unset them only
179  // if the overlay record instructs us to.
180  // Clear where_hid_del and restrict to live and current workspaces
181  $this->where_hid_del = ' AND ' . $expressionBuilder->and(
182  $expressionBuilder->eq('pages.deleted', 0),
183  $expressionBuilder->or(
184  $expressionBuilder->eq('pages.t3ver_wsid', 0),
185  $expressionBuilder->eq('pages.t3ver_wsid', (int)$this->versioningWorkspaceId)
186  )
187  );
188  } else {
189  // add starttime / endtime, and check for hidden/deleted
190  // Filter out new/deleted place-holder pages in case we are NOT in a
191  // versioning preview (that means we are online!)
192  $this->where_hid_del = ' AND ' . (string)$expressionBuilder->and(
194  $this->enableFields('pages', (int)$show_hidden, ['fe_group' => true])
195  )
196  );
197  }
198  $cache->set($cacheIdentifier, $this->where_hid_del);
199  }
200 
201  if (is_array(‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] ?? false)) {
202  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS'][self::class]['init'] as $classRef) {
203  $hookObject = GeneralUtility::makeInstance($classRef);
204  if (!$hookObject instanceof ‪PageRepositoryInitHookInterface) {
205  throw new \UnexpectedValueException($classRef . ' must implement interface ' . PageRepositoryInitHookInterface::class, 1379579812);
206  }
207  $hookObject->init_postProcess($this);
208  }
209  }
210  }
211 
212  /**************************
213  *
214  * Selecting page records
215  *
216  **************************/
217 
246  public function ‪getPage(‪$uid, $disableGroupAccessCheck = false)
247  {
248  // Hook to manipulate the page uid for special overlay handling
249  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['getPage'] ?? [] as $className) {
250  $hookObject = GeneralUtility::makeInstance($className);
251  if (!$hookObject instanceof ‪PageRepositoryGetPageHookInterface) {
252  throw new \UnexpectedValueException($className . ' must implement interface ' . PageRepositoryGetPageHookInterface::class, 1251476766);
253  }
254  $hookObject->getPage_preProcess(‪$uid, $disableGroupAccessCheck, $this);
255  }
256  $cacheIdentifier = 'PageRepository_getPage_' . md5(
257  implode(
258  '-',
259  [
260  ‪$uid,
261  $disableGroupAccessCheck ? '' : $this->where_groupAccess,
262  $this->where_hid_del,
263  $this->sys_language_uid,
264  ]
265  )
266  );
267  $cache = $this->‪getRuntimeCache();
268  $cacheEntry = $cache->get($cacheIdentifier);
269  if (is_array($cacheEntry)) {
270  return $cacheEntry;
271  }
272  $result = [];
273  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
274  $queryBuilder->getRestrictions()->removeAll();
275  $queryBuilder->select('*')
276  ->from('pages')
277  ->where(
278  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)‪$uid, ‪Connection::PARAM_INT)),
279  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del)
280  );
281 
282  $originalWhereGroupAccess = '';
283  if (!$disableGroupAccessCheck) {
284  $queryBuilder->andWhere(‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess));
285  } else {
286  $originalWhereGroupAccess = ‪$this->where_groupAccess;
287  $this->where_groupAccess = '';
288  }
289 
290  $row = $queryBuilder->executeQuery()->fetchAssociative();
291  if ($row) {
292  $this->‪versionOL('pages', $row);
293  if (is_array($row)) {
294  $result = $this->‪getLanguageOverlay('pages', $row);
295  }
296  }
297 
298  if ($disableGroupAccessCheck) {
299  $this->where_groupAccess = $originalWhereGroupAccess;
300  }
301 
302  $cache->set($cacheIdentifier, $result);
303  return $result;
304  }
305 
314  public function ‪getPage_noCheck(‪$uid)
315  {
316  $cache = $this->‪getRuntimeCache();
317  $cacheIdentifier = 'PageRepository_getPage_noCheck_' . ‪$uid . '_' . $this->sys_language_uid . '_' . ‪$this->versioningWorkspaceId;
318  $cacheEntry = $cache->get($cacheIdentifier);
319  if ($cacheEntry !== false) {
320  return $cacheEntry;
321  }
322 
323  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
324  $queryBuilder->getRestrictions()
325  ->removeAll()
326  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
327  $row = $queryBuilder->select('*')
328  ->from('pages')
329  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)‪$uid, ‪Connection::PARAM_INT)))
330  ->executeQuery()
331  ->fetchAssociative();
332 
333  $result = [];
334  if ($row) {
335  $this->‪versionOL('pages', $row);
336  if (is_array($row)) {
337  $result = $this->‪getLanguageOverlay('pages', $row);
338  }
339  }
340  $cache->set($cacheIdentifier, $result);
341  return $result;
342  }
343 
354  public function ‪getLanguageOverlay(string $table, array $originalRow, ‪LanguageAspect $languageAspect = null): ?array
355  {
356  // table is not localizable, so return directly
357  if (!isset(‪$GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
358  return $originalRow;
359  }
360 
361  try {
363  $languageAspect = $languageAspect ?? $this->context->getAspect('language');
364  } catch (‪AspectNotFoundException $e) {
365  // no overlays
366  return $originalRow;
367  }
368 
369  $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
370 
371  $event = $eventDispatcher->dispatch(new ‪BeforeRecordLanguageOverlayEvent($table, $originalRow, $languageAspect));
372  $languageAspect = $event->getLanguageAspect();
373  $originalRow = $event->getRecord();
374 
375  $attempted = false;
376  $localizedRecord = null;
377  if ($languageAspect->doOverlays()) {
378  $attempted = true;
379  // Mixed = if nothing is available in the selected language, try the fallbacks
380  // Fallbacks work as follows:
381  // 1. We have a default language record and then start doing overlays (= the basis for fallbacks)
382  // 2. Check if the actual requested language version is available in the DB (language=3 = canadian-french)
383  // 3. If not, we check the next language version in the chain (e.g. language=2 = french) and so forth until we find a record
384  if ($languageAspect->getOverlayType() === ‪LanguageAspect::OVERLAYS_MIXED) {
385  $languageChain = $this->‪getLanguageFallbackChain($languageAspect);
386  $languageChain = array_reverse($languageChain);
387  if ($table === 'pages') {
388  $result = $this->‪getPageOverlay(
389  $originalRow,
390  new ‪LanguageAspect($languageAspect->getId(), $languageAspect->getId(), ‪LanguageAspect::OVERLAYS_MIXED, $languageChain)
391  );
392  if (!empty($result)) {
393  $localizedRecord = $result;
394  }
395  } else {
396  $languageChain = array_merge($languageChain, [$languageAspect->getContentId()]);
397  // Loop through each (fallback) language and see if there is a record
398  // However, we do not want to preserve the "originalRow", that's why we set the option to "OVERLAYS_ON"
399  while (($languageId = array_pop($languageChain)) !== null) {
400  $result = $this->‪getRecordOverlay(
401  $table,
402  $originalRow,
403  new ‪LanguageAspect($languageId, $languageId, ‪LanguageAspect::OVERLAYS_ON)
404  );
405  // If an overlay is found, return it
406  if (is_array($result)) {
407  $localizedRecord = $result;
408  break;
409  }
410  }
411  if ($localizedRecord === null) {
412  // If nothing was found, we set the localized record to the originalRow to simulate
413  // that the default language is "kept" (we want fallback to default language).
414  // Note: Most installations might have "type=fallback" set but do not set the default language
415  // as fallback. In the future - once we want to get rid of the magic "default language",
416  // this needs to behave different, and the "pageNotFound" special handling within fallbacks should be removed
417  // and we need to check explicitly on in_array(0, $languageAspect->getFallbackChain())
418  // However, getPageOverlay() a few lines above also returns the "default language page" as well.
419  $localizedRecord = $originalRow;
420  }
421  }
422  } else {
423  // The option to hide records if they were not explicitly selected, was chosen (OVERLAYS_ON/WITH_FLOATING)
424  // in the language configuration. So, here no changes are done.
425  if ($table === 'pages') {
426  $localizedRecord = $this->‪getPageOverlay($originalRow, $languageAspect);
427  } else {
428  $localizedRecord = $this->‪getRecordOverlay($table, $originalRow, $languageAspect);
429  }
430  }
431  } else {
432  // Free mode.
433  // For "pages": Pages are usually retrieved by fetching the page record in the default language.
434  // However, the originalRow should still fetch the page in a specific language (with fallbacks).
435  // The method "getPageOverlay" should still be called in order to get the page record in the correct language.
436  if ($table === 'pages' && $languageAspect->getId() > 0) {
437  $attempted = true;
438  $localizedRecord = $this->‪getPageOverlay($originalRow, $languageAspect);
439  }
440  }
441 
442  $event = new ‪AfterRecordLanguageOverlayEvent($table, $originalRow, $localizedRecord, $attempted, $languageAspect);
443  $event = $eventDispatcher->dispatch($event);
444 
445  // Return localized record or the original row, if no overlays were done
446  return $event->overlayingWasAttempted() ? $event->getLocalizedRecord() : $originalRow;
447  }
448 
457  public function ‪getPageOverlay($pageInput, $language = null)
458  {
459  $rows = $this->‪getPagesOverlay([$pageInput], $language);
460  // Always an array in return
461  return $rows[0] ?? [];
462  }
463 
475  public function ‪getPagesOverlay(array $pagesInput, int|‪LanguageAspect $language = null)
476  {
477  if (empty($pagesInput)) {
478  return [];
479  }
480  if (is_int($language)) {
481  $languageAspect = new ‪LanguageAspect($language, $language);
482  } else {
483  $languageAspect = $language ?? $this->context->getAspect('language');
484  }
485 
486  $overlays = [];
487  // If language UID is different from zero, do overlay:
488  if ($languageAspect->getId() > 0) {
489  $pageIds = [];
490  foreach ($pagesInput as $origPage) {
491  if (is_array($origPage)) {
492  // Was the whole record
493  $pageIds[] = (int)($origPage['uid'] ?? 0);
494  } else {
495  // Was the id
496  $pageIds[] = (int)$origPage;
497  }
498  }
499 
500  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(
501  new ‪BeforePageLanguageOverlayEvent($pagesInput, $pageIds, $languageAspect)
502  );
503  $pagesInput = $event->getPageInput();
504  $overlays = $this->‪getPageOverlaysForLanguage($event->getPageIds(), $event->getLanguageAspect());
505  }
506 
507  // Create output:
508  $pagesOutput = [];
509  foreach ($pagesInput as $key => $origPage) {
510  if (is_array($origPage)) {
511  $pagesOutput[$key] = $origPage;
512  if (isset($origPage['uid'], $overlays[$origPage['uid']])) {
513  // Overwrite the original field with the overlay
514  foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) {
515  if ($fieldName !== 'uid' && $fieldName !== 'pid') {
516  $pagesOutput[$key][$fieldName] = $fieldValue;
517  }
518  }
519  $pagesOutput[$key]['_TRANSLATION_SOURCE'] = new ‪Page($origPage);
520  }
521  } elseif (isset($overlays[$origPage])) {
522  $pagesOutput[$key] = $overlays[$origPage];
523  }
524  }
525  return $pagesOutput;
526  }
527 
535  public function ‪isPageSuitableForLanguage(array $page, ‪LanguageAspect $languageAspect): bool
536  {
537  $languageUid = $languageAspect->‪getId();
538  // Checks if the default language version can be shown
539  // Block page is set, if l18n_cfg allows plus: 1) Either default language or 2) another language but NO overlay record set for page!
540  $pageTranslationVisibility = new ‪PageTranslationVisibility((int)($page['l18n_cfg'] ?? 0));
541  if ((!$languageUid || !($page['_PAGES_OVERLAY'] ?? false))
542  && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
543  ) {
544  return false;
545  }
546  if ($languageUid > 0 && $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
547  if (!($page['_PAGES_OVERLAY'] ?? false) || (int)($page['_PAGES_OVERLAY_LANGUAGE'] ?? 0) !== $languageUid) {
548  return false;
549  }
550  } elseif ($languageUid > 0) {
551  $languageUids = array_merge([$languageUid], $this->‪getLanguageFallbackChain($languageAspect));
552  return in_array((int)($page['sys_language_uid'] ?? 0), $languageUids, true);
553  }
554  return true;
555  }
556 
562  protected function ‪getLanguageFallbackChain(?‪LanguageAspect $languageAspect): array
563  {
564  $languageAspect = $languageAspect ?? $this->context->getAspect('language');
565  return array_filter($languageAspect->‪getFallbackChain(), static ‪function ($item) {
567  });
568  }
569 
582  protected function ‪getPageOverlaysForLanguage(array $pageUids, ‪LanguageAspect $languageAspect): array
583  {
584  if ($pageUids === []) {
585  return [];
586  }
587 
588  $languageUids = array_merge([$languageAspect->‪getId()], $this->getLanguageFallbackChain($languageAspect));
589  // Remove default language ("0")
590  $languageUids = array_filter($languageUids);
591  $languageField = ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'];
592  $transOrigPointerField = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
593 
594  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
595  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
596  // Because "fe_group" is an exclude field, so it is synced between overlays, the group restriction is removed for language overlays of pages
597  $queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class);
598 
599  $candidates = [];
600  $maxChunk = ‪PlatformInformation::getMaxBindParameters($queryBuilder->getConnection()->getDatabasePlatform());
601  foreach (array_chunk($pageUids, (int)floor($maxChunk / 3)) as $pageUidsChunk) {
602  $query = $queryBuilder
603  ->select('*')
604  ->from('pages')
605  ->where(
606  $queryBuilder->expr()->in(
607  $languageField,
608  $queryBuilder->createNamedParameter($languageUids, Connection::PARAM_INT_ARRAY)
609  ),
610  $queryBuilder->expr()->in(
611  $transOrigPointerField,
612  $queryBuilder->createNamedParameter($pageUidsChunk, Connection::PARAM_INT_ARRAY)
613  )
614  );
615 
616  // This has cache hits for the current page and for menus (little performance gain).
617  $cacheIdentifier = 'PageRepository_getPageOverlaysForLanguage_'
618  . hash('xxh3', $query->getSQL() . json_encode($query->getParameters()));
619  $rows = $this->‪getRuntimeCache()->get($cacheIdentifier);
620  if (!is_array($rows)) {
621  $rows = $query->executeQuery()->fetchAllAssociative();
622  $this->‪getRuntimeCache()->set($cacheIdentifier, $rows);
623  }
624 
625  foreach ($rows as $row) {
626  $pageId = $row[$transOrigPointerField];
627  $priority = array_search($row[$languageField], $languageUids);
628  $candidates[$pageId][$priority] = $row;
629  }
630  }
631 
632  $overlays = [];
633  foreach ($pageUids as $pageId) {
634  $languageRows = $candidates[$pageId] ?? [];
635  ksort($languageRows, SORT_NATURAL);
636  foreach ($languageRows as $row) {
637  // Found a result for the current language id
638  $this->‪versionOL('pages', $row);
639  if (is_array($row)) {
640  $row['_PAGES_OVERLAY'] = true;
641  $row['_PAGES_OVERLAY_UID'] = $row['uid'];
642  $row['_PAGES_OVERLAY_LANGUAGE'] = $row[$languageField];
643  $row['_PAGES_OVERLAY_REQUESTEDLANGUAGE'] = $languageUids[0];
644  // Unset vital fields that are NOT allowed to be overlaid:
645  unset($row['uid'], $row['pid']);
646  $overlays[$pageId] = $row;
647 
648  // Language fallback found, stop querying further languages
649  break;
650  }
651  }
652  }
653 
654  return $overlays;
655  }
656 
667  protected function ‪getRecordOverlay(string $table, array $row, ‪LanguageAspect $languageAspect)
668  {
669  // Early return when no overlays are needed
670  if ($languageAspect->‪getOverlayType() === $languageAspect::OVERLAYS_OFF) {
671  return $row;
672  }
673 
674  $tableControl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? [];
675 
676  if (!empty($tableControl['languageField'])
677  // Return record for ALL languages untouched
678  // @todo: Fix call stack to prevent this situation in the first place
679  && (int)($row[$tableControl['languageField']] ?? 0) !== -1
680  && !empty($tableControl['transOrigPointerField'])
681  && ($row['uid'] ?? 0) > 0
682  && (($row['pid'] ?? 0) > 0 || in_array($tableControl['rootLevel'] ?? false, [true, 1, -1], true))
683  ) {
684  // Will try to overlay a record only if the sys_language_content value is larger than zero.
685  if ($languageAspect->‪getContentId() > 0) {
686  // Must be default language, otherwise no overlaying
687  if ((int)($row[$tableControl['languageField']] ?? 0) === 0) {
688  // Select overlay record:
689  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
690  ->getQueryBuilderForTable($table);
691  $queryBuilder->setRestrictions(
692  GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)
693  );
694  if ($this->versioningWorkspaceId > 0) {
695  // If not in live workspace, remove query based "enable fields" checks, it will be done in versionOL()
696  // @see functional workspace test createLocalizedNotHiddenWorkspaceContentHiddenInLive()
697  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
698  $queryBuilder->getRestrictions()->removeByType(StartTimeRestriction::class);
699  $queryBuilder->getRestrictions()->removeByType(EndTimeRestriction::class);
700  // We keep the WorkspaceRestriction in this case, because we need to get the LIVE record
701  // of the language record before doing the version overlay of the language again. WorkspaceRestriction
702  // does this for us, PLUS we need to ensure to get a possible LIVE record first (that's why
703  // the "orderBy" query is there, so the LIVE record is found first), as there might only be a
704  // versioned record (e.g. new version) or both (common for modifying, moving etc).
705  if ($this->‪hasTableWorkspaceSupport($table)) {
706  $queryBuilder->orderBy('t3ver_wsid', 'ASC');
707  }
708  }
709 
710  $pid = $row['pid'];
711  // When inside a workspace, the already versioned $row of the default language is coming in
712  // For moved versioned records, the PID MIGHT be different. However, the idea of this function is
713  // to get the language overlay of the LIVE default record, and afterwards get the versioned record
714  // the found (live) language record again, see the versionOL() call a few lines below.
715  // This means, we need to modify the $pid value for moved records, as they might be on a different
716  // page and use the PID of the LIVE version.
717  if (isset($row['_ORIG_pid']) && $this->‪hasTableWorkspaceSupport($table) && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
718  $pid = $row['_ORIG_pid'];
719  }
720  $olrow = $queryBuilder->select('*')
721  ->from($table)
722  ->where(
723  $queryBuilder->expr()->eq(
724  'pid',
725  $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)
726  ),
727  $queryBuilder->expr()->eq(
728  $tableControl['languageField'],
729  $queryBuilder->createNamedParameter($languageAspect->‪getContentId(), ‪Connection::PARAM_INT)
730  ),
731  $queryBuilder->expr()->eq(
732  $tableControl['transOrigPointerField'],
733  $queryBuilder->createNamedParameter($row['uid'], ‪Connection::PARAM_INT)
734  )
735  )
736  ->setMaxResults(1)
737  ->executeQuery()
738  ->fetchAssociative();
739 
740  $this->‪versionOL($table, $olrow);
741  // Merge record content by traversing all fields:
742  if (is_array($olrow)) {
743  if (isset($olrow['_ORIG_uid'])) {
744  $row['_ORIG_uid'] = $olrow['_ORIG_uid'];
745  }
746  if (isset($olrow['_ORIG_pid'])) {
747  $row['_ORIG_pid'] = $olrow['_ORIG_pid'];
748  }
749  foreach ($row as $fN => $fV) {
750  if ($fN !== 'uid' && $fN !== 'pid' && array_key_exists($fN, $olrow)) {
751  $row[$fN] = $olrow[$fN];
752  } elseif ($fN === 'uid') {
753  $row['_LOCALIZED_UID'] = $olrow['uid'];
754  }
755  }
757  && (int)($row[$tableControl['languageField']] ?? 0) === 0
758  ) {
759  // Unset, if non-translated records should be hidden. ONLY done if the source
760  // record really is default language and not [All] in which case it is allowed.
761  $row = null;
762  }
763  } elseif ($languageAspect->‪getContentId() != ($row[$tableControl['languageField']] ?? null)) {
764  $row = null;
765  }
766  } else {
767  // When default language is displayed, we never want to return a record carrying
768  // another language!
769  if ((int)($row[$tableControl['languageField']] ?? 0) > 0) {
770  $row = null;
771  }
772  }
773  }
774  return is_array($row) ? $row : null;
775  }
776 
777  /************************************************
778  *
779  * Page related: Menu, Domain record, Root line
780  *
781  ************************************************/
782 
800  public function ‪getMenu($pageId, ‪$fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true, bool $disableGroupAccessCheck = false)
801  {
802  // @todo: Restricting $fields to a list like 'uid, title' here, leads to issues from methods like
803  // getSubpagesForPages() which access keys like 'doktype'. This is odd, select field list
804  // should be handled better here, probably at least containing fields that are used in the
805  // sub methods. In the end, it might be easier to drop argument $fields altogether and
806  // always select * ?
807  return $this->‪getSubpagesForPages((array)$pageId, ‪$fields, $sortField, $additionalWhereClause, $checkShortcuts, true, $disableGroupAccessCheck);
808  }
809 
824  public function ‪getMenuForPages(array $pageIds, ‪$fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true, bool $disableGroupAccessCheck = false)
825  {
826  return $this->‪getSubpagesForPages($pageIds, ‪$fields, $sortField, $additionalWhereClause, $checkShortcuts, false, $disableGroupAccessCheck);
827  }
828 
865  protected function ‪getSubpagesForPages(
866  array $pageIds,
867  string ‪$fields = '*',
868  string $sortField = 'sorting',
869  string $additionalWhereClause = '',
870  bool $checkShortcuts = true,
871  bool $parentPages = true,
872  bool $disableGroupAccessCheck = false
873  ): array {
874  $relationField = $parentPages ? 'pid' : 'uid';
875 
876  if ($disableGroupAccessCheck) {
877  $whereGroupAccessCheck = ‪$this->where_groupAccess;
878  $this->where_groupAccess = '';
879  }
880 
881  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
882  $queryBuilder->getRestrictions()
883  ->removeAll()
884  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->versioningWorkspaceId));
885 
886  $res = $queryBuilder->select(...GeneralUtility::trimExplode(',', ‪$fields, true))
887  ->from('pages')
888  ->where(
889  $queryBuilder->expr()->in(
890  $relationField,
891  $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
892  ),
893  $queryBuilder->expr()->eq(
894  ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'],
895  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
896  ),
897  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
898  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
899  ‪QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
900  );
901 
902  if (!empty($sortField)) {
903  $orderBy = ‪QueryHelper::parseOrderBy($sortField);
904  foreach ($orderBy as $order) {
905  $res->addOrderBy($order[0], $order[1] ?? 'ASC');
906  }
907  }
908  $result = $res->executeQuery();
909 
910  $pages = [];
911  while ($page = $result->fetchAssociative()) {
912  $originalUid = $page['uid'];
913 
914  // Versioning Preview Overlay
915  $this->‪versionOL('pages', $page, true);
916  // Skip if page got disabled due to version overlay (might be delete placeholder)
917  if (empty($page)) {
918  continue;
919  }
920 
921  // Add a mount point parameter if needed
922  $page = $this->‪addMountPointParameterToPage((array)$page);
923 
924  // If shortcut, look up if the target exists and is currently visible
925  if ($checkShortcuts) {
926  $page = $this->‪checkValidShortcutOfPage((array)$page, $additionalWhereClause);
927  }
928 
929  // If the page still is there, we add it to the output
930  if (!empty($page)) {
931  $pages[$originalUid] = $page;
932  }
933  }
934 
935  if ($disableGroupAccessCheck) {
936  $this->where_groupAccess = $whereGroupAccessCheck;
937  }
938 
939  // Finally load language overlays
940  return $this->‪getPagesOverlay($pages);
941  }
942 
958  protected function ‪addMountPointParameterToPage(array $page): array
959  {
960  if (empty($page)) {
961  return [];
962  }
963 
964  // $page MUST have "uid", "pid", "doktype", "mount_pid", "mount_pid_ol" fields in it
965  $mountPointInfo = $this->‪getMountPointInfo($page['uid'], $page);
966 
967  // There is a valid mount point in overlay mode.
968  if (is_array($mountPointInfo) && $mountPointInfo['overlay']) {
969  // Using "getPage" is OK since we need the check for enableFields AND for type 2
970  // of mount pids we DO require a doktype < 200!
971  $mountPointPage = $this->‪getPage($mountPointInfo['mount_pid']);
972 
973  if (!empty($mountPointPage)) {
974  $page = $mountPointPage;
975  $page['_MP_PARAM'] = $mountPointInfo['MPvar'];
976  } else {
977  $page = [];
978  }
979  }
980  return $page;
981  }
982 
990  protected function ‪checkValidShortcutOfPage(array $page, $additionalWhereClause)
991  {
992  if (empty($page)) {
993  return [];
994  }
995 
996  $dokType = (int)($page['doktype'] ?? 0);
997  $shortcutMode = (int)($page['shortcut_mode'] ?? 0);
998 
999  if ($dokType === self::DOKTYPE_SHORTCUT && (($shortcut = (int)($page['shortcut'] ?? 0)) || $shortcutMode)) {
1000  if ($shortcutMode === self::SHORTCUT_MODE_NONE) {
1001  // No shortcut_mode set, so target is directly set in $page['shortcut']
1002  $searchField = 'uid';
1003  $searchUid = $shortcut;
1004  } elseif ($shortcutMode === self::SHORTCUT_MODE_FIRST_SUBPAGE || $shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) {
1005  // Check subpages - first subpage or random subpage
1006  $searchField = 'pid';
1007  // If a shortcut mode is set and no valid page is given to select subpages
1008  // from use the actual page.
1009  $searchUid = $shortcut ?: $page['uid'];
1010  } elseif ($shortcutMode === self::SHORTCUT_MODE_PARENT_PAGE) {
1011  // Shortcut to parent page
1012  $searchField = 'uid';
1013  $searchUid = $page['pid'];
1014  } else {
1015  $searchField = '';
1016  $searchUid = 0;
1017  }
1018 
1019  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1020  $queryBuilder->getRestrictions()->removeAll();
1021  $count = $queryBuilder->count('uid')
1022  ->from('pages')
1023  ->where(
1024  $queryBuilder->expr()->eq(
1025  $searchField,
1026  $queryBuilder->createNamedParameter($searchUid, ‪Connection::PARAM_INT)
1027  ),
1028  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_hid_del),
1029  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
1030  ‪QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
1031  )
1032  ->executeQuery()
1033  ->fetchOne();
1034 
1035  if (!$count) {
1036  $page = [];
1037  }
1038  } elseif ($dokType === self::DOKTYPE_SHORTCUT) {
1039  // Neither shortcut target nor mode is set. Remove the page from the menu.
1040  $page = [];
1041  }
1042  return $page;
1043  }
1044 
1062  public function ‪getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration = 20, $pageLog = [], $disableGroupCheck = false, bool $resolveRandomPageShortcuts = true)
1063  {
1064  $idArray = GeneralUtility::intExplode(',', $shortcutFieldValue);
1065  if ($resolveRandomPageShortcuts === false && (int)$shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) {
1066  return [];
1067  }
1068  // Find $page record depending on shortcut mode:
1069  switch ($shortcutMode) {
1072  $excludedDoktypes = [
1076  ];
1077  $savedWhereGroupAccess = '';
1078  // "getMenu()" does not allow to hand over $disableGroupCheck, for this reason it is manually disabled and re-enabled afterwards.
1079  if ($disableGroupCheck) {
1080  $savedWhereGroupAccess = ‪$this->where_groupAccess;
1081  $this->where_groupAccess = '';
1082  }
1083  $pageArray = $this->‪getMenu($idArray[0] ?: $thisUid, '*', 'sorting', 'AND pages.doktype NOT IN (' . implode(', ', $excludedDoktypes) . ')');
1084  if ($disableGroupCheck) {
1085  $this->where_groupAccess = $savedWhereGroupAccess;
1086  }
1087  $pO = 0;
1088  if ($shortcutMode == self::SHORTCUT_MODE_RANDOM_SUBPAGE && !empty($pageArray)) {
1089  $pO = (int)random_int(0, count($pageArray) - 1);
1090  }
1091  $c = 0;
1092  $page = [];
1093  foreach ($pageArray as $pV) {
1094  if ($c === $pO) {
1095  $page = $pV;
1096  break;
1097  }
1098  $c++;
1099  }
1100  if (empty($page)) {
1101  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a subpage. However, this page has no accessible subpages.';
1102  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648328);
1103  }
1104  break;
1106  $parent = $this->‪getPage($idArray[0] ?: $thisUid, $disableGroupCheck);
1107  $page = $this->‪getPage($parent['pid'], $disableGroupCheck);
1108  if (empty($page)) {
1109  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to its parent page. However, the parent page is not accessible.';
1110  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648358);
1111  }
1112  break;
1113  default:
1114  $page = $this->‪getPage($idArray[0], $disableGroupCheck);
1115  if (empty($page)) {
1116  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a page, which is not accessible (ID ' . $idArray[0] . ').';
1117  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648404);
1118  }
1119  }
1120  // Check if short cut page was a shortcut itself, if so look up recursively:
1121  if ((int)$page['doktype'] === self::DOKTYPE_SHORTCUT) {
1122  if (!in_array($page['uid'], $pageLog) && $iteration > 0) {
1123  $pageLog[] = $page['uid'];
1124  $page = $this->‪getPageShortcut((string)$page['shortcut'], $page['shortcut_mode'], $page['uid'], $iteration - 1, $pageLog, $disableGroupCheck);
1125  } else {
1126  $pageLog[] = $page['uid'];
1127  $this->logger->error('Page shortcuts were looping in uids {uids}', ['uids' => implode(', ', array_values($pageLog))]);
1128  throw new \RuntimeException('Page shortcuts were looping in uids: ' . implode(', ', array_values($pageLog)), 1294587212);
1129  }
1130  }
1131  // Return resulting page:
1132  return $page;
1133  }
1134 
1145  public function ‪resolveShortcutPage(array $page, bool $resolveRandomSubpages = false, bool $disableGroupAccessCheck = false): array
1146  {
1147  if ((int)($page['doktype'] ?? 0) !== self::DOKTYPE_SHORTCUT) {
1148  return $page;
1149  }
1150  $shortcutMode = (int)($page['shortcut_mode'] ?? self::SHORTCUT_MODE_NONE);
1151  $shortcutTarget = (string)($page['shortcut'] ?? '');
1152 
1153  $cacheIdentifier = 'shortcuts_resolved_' . ($disableGroupAccessCheck ? '1' : '0') . '_' . $page['uid'] . '_' . $this->sys_language_uid . '_' . $page['sys_language_uid'];
1154  // Only use the runtime cache if we do not support the random subpages functionality
1155  if ($resolveRandomSubpages === false) {
1156  $cachedResult = $this->‪getRuntimeCache()->get($cacheIdentifier);
1157  if (is_array($cachedResult)) {
1158  return $cachedResult;
1159  }
1160  }
1161  $shortcut = $this->‪getPageShortcut(
1162  $shortcutTarget,
1163  $shortcutMode,
1164  $page['uid'],
1165  20,
1166  [],
1167  $disableGroupAccessCheck,
1168  $resolveRandomSubpages
1169  );
1170  if (!empty($shortcut)) {
1171  $shortcutOriginalPageUid = (int)$page['uid'];
1172  $page = $shortcut;
1173  $page['_SHORTCUT_ORIGINAL_PAGE_UID'] = $shortcutOriginalPageUid;
1174  }
1175 
1176  if ($resolveRandomSubpages === false) {
1177  $this->‪getRuntimeCache()->set($cacheIdentifier, $page);
1178  }
1179 
1180  return $page;
1181  }
1182 
1215  public function ‪getMountPointInfo($pageId, $pageRec = false, $prevMountPids = [], $firstPageUid = 0)
1216  {
1217  if (!‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1218  return false;
1219  }
1220  $cacheIdentifier = 'PageRepository_getMountPointInfo_' . $pageId;
1221  $cache = $this->‪getRuntimeCache();
1222  if ($cache->has($cacheIdentifier)) {
1223  return $cache->get($cacheIdentifier);
1224  }
1225  $result = false;
1226  // Get pageRec if not supplied:
1227  if (!is_array($pageRec)) {
1228  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1229  $queryBuilder->getRestrictions()
1230  ->removeAll()
1231  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1232 
1233  $pageRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
1234  ->from('pages')
1235  ->where(
1236  $queryBuilder->expr()->eq(
1237  'uid',
1238  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
1239  )
1240  )
1241  ->executeQuery()
1242  ->fetchAssociative();
1243 
1244  // Only look for version overlay if page record is not supplied; This assumes
1245  // that the input record is overlaid with preview version, if any!
1246  $this->‪versionOL('pages', $pageRec);
1247  }
1248  // Set first Page uid:
1249  if (!$firstPageUid) {
1250  $firstPageUid = (int)($pageRec['l10n_parent'] ?? false) ?: $pageRec['uid'] ?? 0;
1251  }
1252  // Look for mount pid value plus other required circumstances:
1253  $mount_pid = (int)($pageRec['mount_pid'] ?? 0);
1254  $doktype = (int)($pageRec['doktype'] ?? 0);
1255  if (is_array($pageRec) && $doktype === self::DOKTYPE_MOUNTPOINT && $mount_pid > 0 && !in_array($mount_pid, $prevMountPids, true)) {
1256  // Get the mount point record (to verify its general existence):
1257  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1258  $queryBuilder->getRestrictions()
1259  ->removeAll()
1260  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1261 
1262  $mountRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
1263  ->from('pages')
1264  ->where(
1265  $queryBuilder->expr()->eq(
1266  'uid',
1267  $queryBuilder->createNamedParameter($mount_pid, ‪Connection::PARAM_INT)
1268  )
1269  )
1270  ->executeQuery()
1271  ->fetchAssociative();
1272 
1273  $this->‪versionOL('pages', $mountRec);
1274  if (is_array($mountRec)) {
1275  // Look for recursive mount point:
1276  $prevMountPids[] = $mount_pid;
1277  $recursiveMountPid = $this->‪getMountPointInfo($mount_pid, $mountRec, $prevMountPids, $firstPageUid);
1278  // Return mount point information:
1279  $result = $recursiveMountPid ?: [
1280  'mount_pid' => $mount_pid,
1281  'overlay' => $pageRec['mount_pid_ol'],
1282  'MPvar' => $mount_pid . '-' . $firstPageUid,
1283  'mount_point_rec' => $pageRec,
1284  'mount_pid_rec' => $mountRec,
1285  ];
1286  } else {
1287  // Means, there SHOULD have been a mount point, but there was none!
1288  $result = -1;
1289  }
1290  }
1291  $cache->set($cacheIdentifier, $result);
1292  return $result;
1293  }
1294 
1303  public function ‪filterAccessiblePageIds(array $pageIds, ‪QueryRestrictionContainerInterface $restrictionContainer = null): array
1304  {
1305  if ($pageIds === []) {
1306  return [];
1307  }
1308  $validPageIds = [];
1309  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1310  $queryBuilder->setRestrictions(
1311  $restrictionContainer ?? GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)
1312  );
1313  $statement = $queryBuilder->select('uid')
1314  ->from('pages')
1315  ->where(
1316  $queryBuilder->expr()->in(
1317  'uid',
1318  $queryBuilder->createNamedParameter($pageIds, Connection::PARAM_INT_ARRAY)
1319  )
1320  )
1321  ->executeQuery();
1322  while ($row = $statement->fetchAssociative()) {
1323  $validPageIds[] = (int)$row['uid'];
1324  }
1325  return $validPageIds;
1326  }
1327  /********************************
1328  *
1329  * Selecting records in general
1330  *
1331  ********************************/
1332 
1342  public function ‪checkRecord($table, ‪$uid, $checkPage = 0)
1343  {
1344  ‪$uid = (int)‪$uid;
1345  if (is_array(‪$GLOBALS['TCA'][$table]) && ‪$uid > 0) {
1346  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1347  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1348  $row = $queryBuilder->select('*')
1349  ->from($table)
1350  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)))
1351  ->executeQuery()
1352  ->fetchAssociative();
1353 
1354  if ($row) {
1355  $this->‪versionOL($table, $row);
1356  if (is_array($row)) {
1357  if ($checkPage) {
1358  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1359  ->getQueryBuilderForTable('pages');
1360  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1361  $numRows = (int)$queryBuilder->count('*')
1362  ->from('pages')
1363  ->where(
1364  $queryBuilder->expr()->eq(
1365  'uid',
1366  $queryBuilder->createNamedParameter($row['pid'], ‪Connection::PARAM_INT)
1367  )
1368  )
1369  ->executeQuery()
1370  ->fetchOne();
1371  if ($numRows > 0) {
1372  return $row;
1373  }
1374  return 0;
1375  }
1376  return $row;
1377  }
1378  }
1379  }
1380  return 0;
1381  }
1382 
1393  public function ‪getRawRecord($table, ‪$uid, ‪$fields = '*')
1394  {
1395  ‪$uid = (int)‪$uid;
1396  if (isset(‪$GLOBALS['TCA'][$table]) && is_array(‪$GLOBALS['TCA'][$table]) && ‪$uid > 0) {
1397  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1398  $queryBuilder->getRestrictions()
1399  ->removeAll()
1400  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1401  $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', ‪$fields, true))
1402  ->from($table)
1403  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)))
1404  ->executeQuery()
1405  ->fetchAssociative();
1406 
1407  if ($row) {
1408  $this->‪versionOL($table, $row);
1409  if (is_array($row)) {
1410  return $row;
1411  }
1412  }
1413  }
1414  return 0;
1415  }
1416 
1417  /********************************
1418  *
1419  * Standard clauses
1420  *
1421  ********************************/
1422 
1438  public function ‪enableFields($table, $show_hidden = -1, $ignore_array = [])
1439  {
1440  $showInaccessible = $this->context->getPropertyFromAspect('visibility', 'includeScheduledRecords', false);
1441 
1442  if ($show_hidden === -1) {
1443  // If show_hidden was not set from outside, use the current context
1444  $show_hidden = (int)$this->context->getPropertyFromAspect('visibility', $table === 'pages' ? 'includeHiddenPages' : 'includeHiddenContent', false);
1445  }
1446  // If show_hidden was not changed during the previous evaluation, do it here.
1447  $ctrl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? null;
1448  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1449  ->getQueryBuilderForTable($table)
1450  ->expr();
1451  $constraints = [];
1452  if (is_array($ctrl)) {
1453  // Delete field check:
1454  if ($ctrl['delete'] ?? false) {
1455  $constraints[] = $expressionBuilder->eq($table . '.' . $ctrl['delete'], 0);
1456  }
1457  if ($this->‪hasTableWorkspaceSupport($table)) {
1458  // this should work exactly as WorkspaceRestriction and WorkspaceRestriction should be used instead
1459  if ($this->versioningWorkspaceId === 0) {
1460  // Filter out placeholder records (new/deleted items)
1461  // in case we are NOT in a version preview (that means we are online!)
1462  $constraints[] = $expressionBuilder->lte(
1463  $table . '.t3ver_state',
1464  VersionState::DEFAULT_STATE->value
1465  );
1466  $constraints[] = $expressionBuilder->eq($table . '.t3ver_wsid', 0);
1467  } else {
1468  // show only records of live and of the current workspace
1469  // in case we are in a versioning preview
1470  $constraints[] = $expressionBuilder->or(
1471  $expressionBuilder->eq($table . '.t3ver_wsid', 0),
1472  $expressionBuilder->eq($table . '.t3ver_wsid', (int)$this->versioningWorkspaceId)
1473  );
1474  }
1475 
1476  // Filter out versioned records
1477  if (empty($ignore_array['pid'])) {
1478  // Always filter out versioned records that have an "offline" record
1479  $constraints[] = $expressionBuilder->or(
1480  $expressionBuilder->eq($table . '.t3ver_oid', 0),
1481  $expressionBuilder->eq($table . '.t3ver_state', VersionState::MOVE_POINTER->value)
1482  );
1483  }
1484  }
1485 
1486  // Enable fields:
1487  if (is_array($ctrl['enablecolumns'] ?? false)) {
1488  // In case of versioning-preview, enableFields are ignored (checked in
1489  // versionOL())
1490  if ($this->versioningWorkspaceId === 0 || !$this->‪hasTableWorkspaceSupport($table)) {
1491  if (($ctrl['enablecolumns']['disabled'] ?? false) && !$show_hidden && !($ignore_array['disabled'] ?? false)) {
1492  $field = $table . '.' . $ctrl['enablecolumns']['disabled'];
1493  $constraints[] = $expressionBuilder->eq($field, 0);
1494  }
1495  if (($ctrl['enablecolumns']['starttime'] ?? false) && !$showInaccessible && !($ignore_array['starttime'] ?? false)) {
1496  $field = $table . '.' . $ctrl['enablecolumns']['starttime'];
1497  $constraints[] = $expressionBuilder->lte(
1498  $field,
1499  $this->context->getPropertyFromAspect('date', 'accessTime', 0)
1500  );
1501  }
1502  if (($ctrl['enablecolumns']['endtime'] ?? false) && !$showInaccessible && !($ignore_array['endtime'] ?? false)) {
1503  $field = $table . '.' . $ctrl['enablecolumns']['endtime'];
1504  $constraints[] = $expressionBuilder->or(
1505  $expressionBuilder->eq($field, 0),
1506  $expressionBuilder->gt(
1507  $field,
1508  $this->context->getPropertyFromAspect('date', 'accessTime', 0)
1509  )
1510  );
1511  }
1512  if (($ctrl['enablecolumns']['fe_group'] ?? false) && !($ignore_array['fe_group'] ?? false)) {
1513  $field = $table . '.' . $ctrl['enablecolumns']['fe_group'];
1515  $this->‪getMultipleGroupsWhereClause($field, $table)
1516  );
1517  }
1518  // Call hook functions for additional enableColumns
1519  // It is used by the extension ingmar_accessctrl which enables assigning more
1520  // than one usergroup to content and page records
1521  $_params = [
1522  'table' => $table,
1523  'show_hidden' => $show_hidden,
1524  'showInaccessible' => $showInaccessible,
1525  'ignore_array' => $ignore_array,
1526  'ctrl' => $ctrl,
1527  ];
1528  foreach (‪$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_page.php']['addEnableColumns'] ?? [] as $_funcRef) {
1530  GeneralUtility::callUserFunction($_funcRef, $_params, $this)
1531  );
1532  }
1533  }
1534  }
1535  } else {
1536  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);
1537  }
1538 
1539  return empty($constraints) ? '' : ' AND ' . $expressionBuilder->and(...$constraints);
1540  }
1541 
1551  public function ‪getMultipleGroupsWhereClause($field, $table)
1552  {
1553  if (!$this->context->hasAspect('frontend.user')) {
1554  return '';
1555  }
1557  $userAspect = $this->context->getAspect('frontend.user');
1558  $memberGroups = $userAspect->getGroupIds();
1559  $cache = $this->‪getRuntimeCache();
1560  $cacheIdentifier = 'PageRepository_groupAccessWhere_' . md5($field . '_' . $table . '_' . implode('_', $memberGroups));
1561  $cacheEntry = $cache->get($cacheIdentifier);
1562  if ($cacheEntry) {
1563  return $cacheEntry;
1564  }
1565 
1566  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1567  ->getQueryBuilderForTable($table)
1568  ->expr();
1569  $orChecks = [];
1570  // If the field is empty, then OK
1571  $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal(''));
1572  // If the field is NULL, then OK
1573  $orChecks[] = $expressionBuilder->isNull($field);
1574  // If the field contains zero, then OK
1575  $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal('0'));
1576  foreach ($memberGroups as $value) {
1577  $orChecks[] = $expressionBuilder->inSet($field, $expressionBuilder->literal($value));
1578  }
1579 
1580  $accessGroupWhere = ' AND (' . $expressionBuilder->or(...$orChecks) . ')';
1581  $cache->set($cacheIdentifier, $accessGroupWhere);
1582  return $accessGroupWhere;
1583  }
1584 
1585  /**********************
1586  *
1587  * Versioning Preview
1588  *
1589  **********************/
1590 
1610  public function ‪versionOL($table, &$row, $unsetMovePointers = false, $bypassEnableFieldsCheck = false)
1611  {
1612  if ($this->versioningWorkspaceId > 0 && is_array($row) && $row !== [] && isset($row['uid'], $row['t3ver_oid'])) {
1613  // implode(',',array_keys($row)) = Using fields from original record to make
1614  // sure no additional fields are selected. This is best for eg. getPageOverlay()
1615  // Computed properties are excluded since those would lead to SQL errors.
1616  $fieldNames = implode(',', array_keys($this->‪purgeComputedProperties($row)));
1617  // will overlay any incoming moved record with the live record, which in turn
1618  // will be overlaid with its workspace version again to fetch both PID fields.
1619  $incomingRecordIsAMoveVersion = (int)$row['t3ver_oid'] > 0 && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER;
1620  if ($incomingRecordIsAMoveVersion) {
1621  // Fetch the live version again if the given $row is a move pointer, so we know the original PID
1622  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1623  $queryBuilder->getRestrictions()
1624  ->removeAll()
1625  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1626  $row = $queryBuilder->select(...GeneralUtility::trimExplode(',', $fieldNames, true))
1627  ->from($table)
1628  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$row['t3ver_oid'], ‪Connection::PARAM_INT)))
1629  ->executeQuery()
1630  ->fetchAssociative();
1631  }
1632  if ($wsAlt = $this->‪getWorkspaceVersionOfRecord($this->versioningWorkspaceId, $table, $row['uid'], $fieldNames, $bypassEnableFieldsCheck)) {
1633  if (is_array($wsAlt)) {
1634  $rowVersionState = VersionState::tryFrom($wsAlt['t3ver_state'] ?? 0);
1635  if ($rowVersionState === VersionState::MOVE_POINTER) {
1636  // For move pointers, store the actual live PID in the _ORIG_pid
1637  // The only place where PID is actually different in a workspace
1638  $wsAlt['_ORIG_pid'] = $row['pid'];
1639  }
1640  // For versions of single elements or page+content, preserve online UID
1641  // (this will produce true "overlay" of element _content_, not any references)
1642  // For new versions there is no online counterpart
1643  if ($rowVersionState !== VersionState::NEW_PLACEHOLDER) {
1644  $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
1645  }
1646  $wsAlt['uid'] = $row['uid'];
1647  // Changing input record to the workspace version alternative:
1648  $row = $wsAlt;
1649  // Unset record if it turned out to be deleted in workspace
1650  if ($rowVersionState === VersionState::DELETE_PLACEHOLDER) {
1651  $row = false;
1652  }
1653  // Check if move-pointer in workspace (unless if a move-placeholder is the
1654  // reason why it appears!):
1655  // You have to specifically set $unsetMovePointers in order to clear these
1656  // because it is normally a display issue if it should be shown or not.
1657  if ($rowVersionState === VersionState::MOVE_POINTER && !$incomingRecordIsAMoveVersion && $unsetMovePointers) {
1658  // Unset record if it turned out to be deleted in workspace
1659  $row = false;
1660  }
1661  } else {
1662  // No version found, then check if online version is dummy-representation
1663  // Notice, that unless $bypassEnableFieldsCheck is TRUE, the $row is unset if
1664  // enablefields for BOTH the version AND the online record deselects it. See
1665  // note for $bypassEnableFieldsCheck
1666  if ($wsAlt <= -1 || VersionState::tryFrom($row['t3ver_state'] ?? 0)->‪indicatesPlaceholder()) {
1667  // Unset record if it turned out to be "hidden"
1668  $row = false;
1669  }
1670  }
1671  }
1672  }
1673  }
1674 
1688  public function ‪getWorkspaceVersionOfRecord($workspace, $table, ‪$uid, ‪$fields = '*', $bypassEnableFieldsCheck = false)
1689  {
1690  if ($workspace !== 0 && $this->‪hasTableWorkspaceSupport($table)) {
1691  $workspace = (int)$workspace;
1692  ‪$uid = (int)‪$uid;
1693  // Select workspace version of record, only testing for deleted.
1694  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1695  $queryBuilder->getRestrictions()
1696  ->removeAll()
1697  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1698 
1699  $newrow = $queryBuilder->select(...GeneralUtility::trimExplode(',', ‪$fields, true))
1700  ->from($table)
1701  ->where(
1702  $queryBuilder->expr()->eq(
1703  't3ver_wsid',
1704  $queryBuilder->createNamedParameter($workspace, ‪Connection::PARAM_INT)
1705  ),
1706  $queryBuilder->expr()->or(
1707  // t3ver_state=1 does not contain a t3ver_oid, and returns itself
1708  $queryBuilder->expr()->and(
1709  $queryBuilder->expr()->eq(
1710  'uid',
1711  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1712  ),
1713  $queryBuilder->expr()->eq(
1714  't3ver_state',
1715  $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)
1716  )
1717  ),
1718  $queryBuilder->expr()->eq(
1719  't3ver_oid',
1720  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1721  )
1722  )
1723  )
1724  ->setMaxResults(1)
1725  ->executeQuery()
1726  ->fetchAssociative();
1727 
1728  // If version found, check if it could have been selected with enableFields on
1729  // as well:
1730  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1731  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1732  // Remove the workspace restriction because we are testing a version record
1733  $queryBuilder->getRestrictions()->removeByType(WorkspaceRestriction::class);
1734  $queryBuilder->select('uid')
1735  ->from($table)
1736  ->setMaxResults(1);
1737 
1738  if (is_array($newrow)) {
1739  $queryBuilder->where(
1740  $queryBuilder->expr()->eq(
1741  't3ver_wsid',
1742  $queryBuilder->createNamedParameter($workspace, ‪Connection::PARAM_INT)
1743  ),
1744  $queryBuilder->expr()->or(
1745  // t3ver_state=1 does not contain a t3ver_oid, and returns itself
1746  $queryBuilder->expr()->and(
1747  $queryBuilder->expr()->eq(
1748  'uid',
1749  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1750  ),
1751  $queryBuilder->expr()->eq(
1752  't3ver_state',
1753  $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)
1754  )
1755  ),
1756  $queryBuilder->expr()->eq(
1757  't3ver_oid',
1758  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1759  )
1760  )
1761  );
1762  if ($bypassEnableFieldsCheck || $queryBuilder->executeQuery()->fetchOne()) {
1763  // Return offline version, tested for its enableFields.
1764  return $newrow;
1765  }
1766  // Return -1 because offline version was de-selected due to its enableFields.
1767  return -1;
1768  }
1769  // OK, so no workspace version was found. Then check if online version can be
1770  // selected with full enable fields and if so, return 1:
1771  $queryBuilder->where(
1772  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT))
1773  );
1774  if ($bypassEnableFieldsCheck || $queryBuilder->executeQuery()->fetchOne()) {
1775  // Means search was done, but no version found.
1776  return 1;
1777  }
1778  // Return -2 because the online record was de-selected due to its enableFields.
1779  return -2;
1780  }
1781  // No look up in database because versioning not enabled / or workspace not
1782  // offline
1783  return false;
1784  }
1785 
1796  public function ‪getPageIdsRecursive(array $pageIds, int $depth): array
1797  {
1798  if ($pageIds === []) {
1799  return [];
1800  }
1801  $pageIds = array_map(intval(...), $pageIds);
1802  if ($depth === 0) {
1803  return $pageIds;
1804  }
1805  $allPageIds = [];
1806  foreach ($pageIds as $pageId) {
1807  $allPageIds = array_merge($allPageIds, [$pageId], $this->‪getDescendantPageIdsRecursive($pageId, $depth));
1808  }
1809  return array_unique($allPageIds);
1810  }
1811 
1835  public function ‪getDescendantPageIdsRecursive(int $startPageId, int $depth, int $begin = 0, array $excludePageIds = [], bool $bypassEnableFieldsCheck = false): array
1836  {
1837  if (!$startPageId) {
1838  return [];
1839  }
1840 
1841  // Check the cache
1842  $parameters = [
1843  $startPageId,
1844  $depth,
1845  $begin,
1846  $excludePageIds,
1847  $bypassEnableFieldsCheck,
1848  $this->context->getPropertyFromAspect('frontend.user', 'groupIds', [0, -1]),
1849  ];
1850  $cacheIdentifier = md5(serialize($parameters));
1851  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1852  ->getQueryBuilderForTable('cache_treelist');
1853  $cacheEntry = $queryBuilder->select('treelist')
1854  ->from('cache_treelist')
1855  ->where(
1856  $queryBuilder->expr()->eq(
1857  'md5hash',
1858  $queryBuilder->createNamedParameter($cacheIdentifier)
1859  ),
1860  $queryBuilder->expr()->gt(
1861  'expires',
1862  $queryBuilder->createNamedParameter(‪$GLOBALS['EXEC_TIME'], ‪Connection::PARAM_INT)
1863  )
1864  )
1865  ->setMaxResults(1)
1866  ->executeQuery()
1867  ->fetchOne();
1868 
1869  // Cache hit
1870  if (!empty($cacheEntry)) {
1871  return GeneralUtility::intExplode(',', $cacheEntry);
1872  }
1873 
1874  // Check if the page actually exists
1875  if (!$this->‪getRawRecord('pages', $startPageId, 'uid')) {
1876  // Return blank if the start page was NOT found at all!
1877  return [];
1878  }
1879  // Find mount point if any
1880  $mount_info = $this->‪getMountPointInfo($startPageId);
1881  $includePageId = false;
1882  if (is_array($mount_info)) {
1883  $startPageId = (int)$mount_info['mount_pid'];
1884  // In Overlay mode, use the mounted page uid as added ID!
1885  if ($mount_info['overlay']) {
1886  $includePageId = true;
1887  }
1888  }
1889 
1890  $descendantPageIds = $this->‪getSubpagesRecursive($startPageId, $depth, $begin, $excludePageIds, $bypassEnableFieldsCheck);
1891  if ($includePageId) {
1892  $descendantPageIds = array_merge([$startPageId], $descendantPageIds);
1893  }
1894  // Only add to cache if not logged into TYPO3 Backend
1895  if (!$this->context->getPropertyFromAspect('backend.user', 'isLoggedIn', false)) {
1896  $cacheEntry = [
1897  'md5hash' => $cacheIdentifier,
1898  'pid' => $startPageId,
1899  'treelist' => implode(',', $descendantPageIds),
1900  'tstamp' => ‪$GLOBALS['EXEC_TIME'],
1901  ];
1902  $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('cache_treelist');
1903  try {
1904  $connection->transactional(static function ($connection) use ($cacheEntry) {
1905  $connection->insert('cache_treelist', $cacheEntry);
1906  });
1907  } catch (\Throwable $e) {
1908  }
1909  }
1910  return $descendantPageIds;
1911  }
1912 
1920  protected function ‪getSubpagesRecursive(int $pageId, int $depth, int $begin, array $excludePageIds, bool $bypassEnableFieldsCheck, array $prevId_array = []): array
1921  {
1922  $descendantPageIds = [];
1923  // if $depth is 0, then we do not fetch subpages
1924  if ($depth === 0) {
1925  return [];
1926  }
1927  // Add this ID to the array of IDs
1928  if ($begin <= 0) {
1929  $prevId_array[] = $pageId;
1930  }
1931  // Select subpages
1932  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1933  $queryBuilder->getRestrictions()
1934  ->removeAll()
1935  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1936  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->versioningWorkspaceId));
1937  $queryBuilder->select('*')
1938  ->from('pages')
1939  ->where(
1940  $queryBuilder->expr()->eq(
1941  'pid',
1942  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
1943  ),
1944  // tree is only built by language=0 pages
1945  $queryBuilder->expr()->eq('sys_language_uid', 0)
1946  )
1947  ->orderBy('sorting');
1948 
1949  if ($excludePageIds !== []) {
1950  $queryBuilder->andWhere(
1951  $queryBuilder->expr()->notIn('uid', $queryBuilder->createNamedParameter($excludePageIds, Connection::PARAM_INT_ARRAY))
1952  );
1953  }
1954 
1955  $result = $queryBuilder->executeQuery();
1956  while ($row = $result->fetchAssociative()) {
1957  $versionState = VersionState::tryFrom($row['t3ver_state'] ?? 0);
1958  $this->‪versionOL('pages', $row, false, $bypassEnableFieldsCheck);
1959  if ($row === false
1960  || (int)$row['doktype'] === self::DOKTYPE_BE_USER_SECTION
1961  || $versionState->indicatesPlaceholder()
1962  ) {
1963  // falsy row means Overlay prevents access to this page.
1964  // Doing this after the overlay to make sure changes
1965  // in the overlay are respected.
1966  // However, we do not process pages below of and
1967  // including of type BE user section
1968  continue;
1969  }
1970  // Find mount point if any:
1971  $next_id = (int)$row['uid'];
1972  $mount_info = $this->‪getMountPointInfo($next_id, $row);
1973  // Overlay mode:
1974  if (is_array($mount_info) && $mount_info['overlay']) {
1975  $next_id = (int)$mount_info['mount_pid'];
1976  // @todo: check if we could use $mount_info[mount_pid_rec] and check against $excludePageIds?
1977  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1978  ->getQueryBuilderForTable('pages');
1979  $queryBuilder->getRestrictions()
1980  ->removeAll()
1981  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1982  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $this->versioningWorkspaceId));
1983  $queryBuilder->select('*')
1984  ->from('pages')
1985  ->where(
1986  $queryBuilder->expr()->eq(
1987  'uid',
1988  $queryBuilder->createNamedParameter($next_id, ‪Connection::PARAM_INT)
1989  )
1990  )
1991  ->orderBy('sorting')
1992  ->setMaxResults(1);
1993 
1994  if ($excludePageIds !== []) {
1995  $queryBuilder->andWhere(
1996  $queryBuilder->expr()->notIn('uid', $queryBuilder->createNamedParameter($excludePageIds, Connection::PARAM_INT_ARRAY))
1997  );
1998  }
1999 
2000  $row = $queryBuilder->executeQuery()->fetchAssociative();
2001  $this->‪versionOL('pages', $row);
2002  $versionState = VersionState::tryFrom($row['t3ver_state'] ?? 0);
2003  if ($row === false
2004  || (int)$row['doktype'] === self::DOKTYPE_BE_USER_SECTION
2005  || $versionState->indicatesPlaceholder()
2006  ) {
2007  // Doing this after the overlay to make sure
2008  // changes in the overlay are respected.
2009  // see above
2010  continue;
2011  }
2012  }
2013  $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
2014  // Add record:
2015  if ($bypassEnableFieldsCheck || $accessVoter->accessGrantedForPageInRootLine($row, $this->context)) {
2016  // Add ID to list:
2017  if ($begin <= 0) {
2018  if ($bypassEnableFieldsCheck || $accessVoter->accessGranted('pages', $row, $this->context)) {
2019  $descendantPageIds[] = $next_id;
2020  }
2021  }
2022  // Next level
2023  if (!$row['php_tree_stop']) {
2024  // Normal mode:
2025  if (is_array($mount_info) && !$mount_info['overlay']) {
2026  $next_id = (int)$mount_info['mount_pid'];
2027  }
2028  // Call recursively, if the id is not in prevID_array:
2029  if (!in_array($next_id, $prevId_array, true)) {
2030  $descendantPageIds = array_merge(
2031  $descendantPageIds,
2032  $this->‪getSubpagesRecursive(
2033  $next_id,
2034  $depth - 1,
2035  $begin - 1,
2036  $excludePageIds,
2037  $bypassEnableFieldsCheck,
2038  $prevId_array
2039  )
2040  );
2041  }
2042  }
2043  }
2044  }
2045  return $descendantPageIds;
2046  }
2047 
2054  public function ‪checkIfPageIsHidden(int $pageId, ‪LanguageAspect $languageAspect): bool
2055  {
2056  if ($pageId === 0) {
2057  return false;
2058  }
2059  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2060  ->getQueryBuilderForTable('pages');
2061  $queryBuilder
2062  ->getRestrictions()
2063  ->removeAll()
2064  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
2065 
2066  $queryBuilder
2067  ->select('uid', 'hidden', 'starttime', 'endtime')
2068  ->from('pages')
2069  ->setMaxResults(1);
2070 
2071  // $pageId always points to the ID of the default language page, so we check
2072  // the current site language to determine if we need to fetch a translation but consider fallbacks
2073  if ($languageAspect->‪getId() > 0) {
2074  $languagesToCheck = [$languageAspect->‪getId()];
2075  // Remove fallback information like "pageNotFound"
2076  foreach ($languageAspect->‪getFallbackChain() as $languageToCheck) {
2077  if (is_numeric($languageToCheck)) {
2078  $languagesToCheck[] = $languageToCheck;
2079  }
2080  }
2081  // Check for the language and all its fallbacks (except for default language)
2082  $constraint = $queryBuilder->expr()->and(
2083  $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)),
2084  $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), Connection::PARAM_INT_ARRAY))
2085  );
2086  // If the fallback language Ids also contains the default language, this needs to be considered
2087  if (in_array(0, $languagesToCheck, true)) {
2088  $constraint = $queryBuilder->expr()->or(
2089  $constraint,
2090  // Ensure to also fetch the default record
2091  $queryBuilder->expr()->and(
2092  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)),
2093  $queryBuilder->expr()->eq('sys_language_uid', 0)
2094  )
2095  );
2096  }
2097  $queryBuilder->where($constraint);
2098  // Ensure that the translated records are shown first (maxResults is set to 1)
2099  $queryBuilder->orderBy('sys_language_uid', 'DESC');
2100  } else {
2101  $queryBuilder->where(
2102  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT))
2103  );
2104  }
2105  $page = $queryBuilder->executeQuery()->fetchAssociative();
2106  $workspaceId = ‪$this->versioningWorkspaceId;
2107  if ($this->versioningWorkspaceId > 0) {
2108  // Fetch overlay of page if in workspace and check if it is hidden
2109  $backupContext = clone ‪$this->context;
2110  $this->context->‪setAspect('workspace', GeneralUtility::makeInstance(WorkspaceAspect::class, $this->versioningWorkspaceId));
2111  $this->context->setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
2112  $targetPage = $this->‪getWorkspaceVersionOfRecord($this->versioningWorkspaceId, 'pages', (int)$page['uid']);
2113  // Also checks if the workspace version is NOT hidden but the live version is in fact still hidden
2114  $result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1);
2115  $this->context = $backupContext;
2116  } else {
2117  $result = is_array($page) && ($page['hidden'] || $page['starttime'] > ‪$GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= ‪$GLOBALS['SIM_EXEC_TIME']);
2118  }
2119  return $result;
2120  }
2121 
2128  protected function ‪purgeComputedProperties(array $row)
2129  {
2130  foreach ($this->computedPropertyNames as $computedPropertyName) {
2131  if (array_key_exists($computedPropertyName, $row)) {
2132  unset($row[$computedPropertyName]);
2133  }
2134  }
2135  return $row;
2136  }
2137 
2139  {
2140  return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
2141  }
2142 
2143  protected function ‪hasTableWorkspaceSupport(string $tableName): bool
2144  {
2145  return !empty(‪$GLOBALS['TCA'][$tableName]['ctrl']['versioningWS']);
2146  }
2147 }
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$versioningWorkspaceId
‪int $versioningWorkspaceId
Definition: PageRepository.php:88
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getSubpagesForPages
‪array getSubpagesForPages(array $pageIds, string $fields=' *', string $sortField='sorting', string $additionalWhereClause='', bool $checkShortcuts=true, bool $parentPages=true, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:865
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\isPageSuitableForLanguage
‪bool isPageSuitableForLanguage(array $page, LanguageAspect $languageAspect)
Definition: PageRepository.php:535
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getMultipleGroupsWhereClause
‪string getMultipleGroupsWhereClause($field, $table)
Definition: PageRepository.php:1551
‪TYPO3\CMS\Core\Database\Query\QueryHelper\parseOrderBy
‪static array array[] parseOrderBy(string $input)
Definition: QueryHelper.php:44
‪TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction
Definition: HiddenRestriction.php:27
‪TYPO3\CMS\Core\Domain\Page
Definition: Page.php:24
‪TYPO3\CMS\Core\Versioning\indicatesPlaceholder
‪@ indicatesPlaceholder
Definition: VersionState.php:59
‪TYPO3\CMS\Core\Context\VisibilityAspect
Definition: VisibilityAspect.php:31
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\versionOL
‪versionOL($table, &$row, $unsetMovePointers=false, $bypassEnableFieldsCheck=false)
Definition: PageRepository.php:1610
‪TYPO3\CMS\Core\Context\LanguageAspect\getOverlayType
‪getOverlayType()
Definition: LanguageAspect.php:94
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getMenu
‪array getMenu($pageId, $fields=' *', $sortField='sorting', $additionalWhereClause='', $checkShortcuts=true, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:800
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:50
‪TYPO3\CMS\Core\Context\WorkspaceAspect
Definition: WorkspaceAspect.php:31
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getRuntimeCache
‪getRuntimeCache()
Definition: PageRepository.php:2138
‪TYPO3\CMS\Core\Context\LanguageAspect\OVERLAYS_MIXED
‪const OVERLAYS_MIXED
Definition: LanguageAspect.php:75
‪TYPO3\CMS\Core\Context\LanguageAspect\getId
‪getId()
Definition: LanguageAspect.php:103
‪TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction
Definition: EndTimeRestriction.php:27
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation\getMaxBindParameters
‪static getMaxBindParameters(DoctrineAbstractPlatform $platform)
Definition: PlatformInformation.php:110
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageOverlay
‪array getPageOverlay($pageInput, $language=null)
Definition: PageRepository.php:457
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\resolveShortcutPage
‪resolveShortcutPage(array $page, bool $resolveRandomSubpages=false, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:1145
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_DEFAULT
‪const DOKTYPE_DEFAULT
Definition: PageRepository.php:108
‪TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
Definition: StartTimeRestriction.php:27
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$context
‪Context $context
Definition: PageRepository.php:124
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getLanguageFallbackChain
‪int[] getLanguageFallbackChain(?LanguageAspect $languageAspect)
Definition: PageRepository.php:562
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkRecord
‪array int checkRecord($table, $uid, $checkPage=0)
Definition: PageRepository.php:1342
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$where_hid_del
‪string $where_hid_del
Definition: PageRepository.php:69
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getWorkspaceVersionOfRecord
‪mixed getWorkspaceVersionOfRecord($workspace, $table, $uid, $fields=' *', $bypassEnableFieldsCheck=false)
Definition: PageRepository.php:1688
‪TYPO3\CMS\Core\Versioning\VersionState
‪VersionState
Definition: VersionState.php:22
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getDescendantPageIdsRecursive
‪int[] getDescendantPageIdsRecursive(int $startPageId, int $depth, int $begin=0, array $excludePageIds=[], bool $bypassEnableFieldsCheck=false)
Definition: PageRepository.php:1835
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPagesOverlay
‪array getPagesOverlay(array $pagesInput, int|LanguageAspect $language=null)
Definition: PageRepository.php:475
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPage_noCheck
‪array getPage_noCheck($uid)
Definition: PageRepository.php:314
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPage
‪array getPage($uid, $disableGroupAccessCheck=false)
Definition: PageRepository.php:246
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SHORTCUT
‪const DOKTYPE_SHORTCUT
Definition: PageRepository.php:110
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_LINK
‪const DOKTYPE_LINK
Definition: PageRepository.php:109
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$computedPropertyNames
‪array $computedPropertyNames
Definition: PageRepository.php:93
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$sys_language_uid
‪int $sys_language_uid
Definition: PageRepository.php:79
‪TYPO3\CMS\Core\Context\LanguageAspect\OVERLAYS_ON
‪const OVERLAYS_ON
Definition: LanguageAspect.php:76
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\filterAccessiblePageIds
‪int[] filterAccessiblePageIds(array $pageIds, QueryRestrictionContainerInterface $restrictionContainer=null)
Definition: PageRepository.php:1303
‪TYPO3\CMS\Core\Domain\Repository\PageRepositoryInitHookInterface
Definition: PageRepositoryInitHookInterface.php:22
‪TYPO3\CMS\Core\Database\Query\Restriction\QueryRestrictionContainerInterface
Definition: QueryRestrictionContainerInterface.php:25
‪TYPO3\CMS\Core\Type\Bitmask\PageTranslationVisibility
Definition: PageTranslationVisibility.php:30
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getRecordOverlay
‪array null getRecordOverlay(string $table, array $row, LanguageAspect $languageAspect)
Definition: PageRepository.php:667
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\addMountPointParameterToPage
‪array addMountPointParameterToPage(array $page)
Definition: PageRepository.php:958
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_RANDOM_SUBPAGE
‪const SHORTCUT_MODE_RANDOM_SUBPAGE
Definition: PageRepository.php:121
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\purgeComputedProperties
‪array purgeComputedProperties(array $row)
Definition: PageRepository.php:2128
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction
Definition: FrontendGroupRestriction.php:30
‪TYPO3\CMS\Core\Domain\Event\AfterRecordLanguageOverlayEvent
Definition: AfterRecordLanguageOverlayEvent.php:26
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Domain\Event\BeforePageLanguageOverlayEvent
Definition: BeforePageLanguageOverlayEvent.php:27
‪TYPO3\CMS\Core\Context\LanguageAspect\getContentId
‪getContentId()
Definition: LanguageAspect.php:113
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_FIRST_SUBPAGE
‪const SHORTCUT_MODE_FIRST_SUBPAGE
Definition: PageRepository.php:120
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:138
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getSubpagesRecursive
‪int[] getSubpagesRecursive(int $pageId, int $depth, int $begin, array $excludePageIds, bool $bypassEnableFieldsCheck, array $prevId_array=[])
Definition: PageRepository.php:1920
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageShortcut
‪mixed getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration=20, $pageLog=[], $disableGroupCheck=false, bool $resolveRandomPageShortcuts=true)
Definition: PageRepository.php:1062
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkIfPageIsHidden
‪checkIfPageIsHidden(int $pageId, LanguageAspect $languageAspect)
Definition: PageRepository.php:2054
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:112
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkValidShortcutOfPage
‪array checkValidShortcutOfPage(array $page, $additionalWhereClause)
Definition: PageRepository.php:990
‪TYPO3\CMS\Core\Context\LanguageAspect\getFallbackChain
‪getFallbackChain()
Definition: LanguageAspect.php:118
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\hasTableWorkspaceSupport
‪hasTableWorkspaceSupport(string $tableName)
Definition: PageRepository.php:2143
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\__construct
‪__construct(Context $context=null)
Definition: PageRepository.php:130
‪TYPO3\CMS\Core\Cache\Frontend\VariableFrontend
Definition: VariableFrontend.php:25
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SPACER
‪const DOKTYPE_SPACER
Definition: PageRepository.php:113
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_BE_USER_SECTION
‪const DOKTYPE_BE_USER_SECTION
Definition: PageRepository.php:111
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageOverlaysForLanguage
‪getPageOverlaysForLanguage(array $pageUids, LanguageAspect $languageAspect)
Definition: PageRepository.php:582
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getMenuForPages
‪array getMenuForPages(array $pageIds, $fields=' *', $sortField='sorting', $additionalWhereClause='', $checkShortcuts=true, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:824
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SYSFOLDER
‪const DOKTYPE_SYSFOLDER
Definition: PageRepository.php:114
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\init
‪init($show_hidden)
Definition: PageRepository.php:152
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:57
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\enableFields
‪string enableFields($table, $show_hidden=-1, $ignore_array=[])
Definition: PageRepository.php:1438
‪TYPO3\CMS\Core\Domain\Repository
Definition: PageRepository.php:16
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_PARENT_PAGE
‪const SHORTCUT_MODE_PARENT_PAGE
Definition: PageRepository.php:122
‪TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
Definition: AspectNotFoundException.php:25
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:39
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageIdsRecursive
‪int[] getPageIdsRecursive(array $pageIds, int $depth)
Definition: PageRepository.php:1796
‪TYPO3\CMS\Core\function
‪static return function(ContainerConfigurator $container, ContainerBuilder $containerBuilder)
Definition: Services.php:16
‪TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException
Definition: ShortcutTargetPageNotFoundException.php:23
‪TYPO3\CMS\Webhooks\Message\$uid
‪identifier readonly int $uid
Definition: PageModificationMessage.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper\stripLogicalOperatorPrefix
‪static string stripLogicalOperatorPrefix(string $constraint)
Definition: QueryHelper.php:171
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_NONE
‪const SHORTCUT_MODE_NONE
Definition: PageRepository.php:119
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$where_groupAccess
‪string $where_groupAccess
Definition: PageRepository.php:74
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Database\Platform\PlatformInformation
Definition: PlatformInformation.php:33
‪TYPO3\CMS\Core\Utility\MathUtility
Definition: MathUtility.php:24
‪TYPO3\CMS\Core\Domain\Event\BeforeRecordLanguageOverlayEvent
Definition: BeforeRecordLanguageOverlayEvent.php:27
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:61
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:48
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:52
‪TYPO3\CMS\Core\Context\LanguageAspect\OVERLAYS_ON_WITH_FLOATING
‪const OVERLAYS_ON_WITH_FLOATING
Definition: LanguageAspect.php:77
‪TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer
Definition: FrontendRestrictionContainer.php:30
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getRawRecord
‪mixed getRawRecord($table, $uid, $fields=' *')
Definition: PageRepository.php:1393
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getLanguageOverlay
‪array null getLanguageOverlay(string $table, array $originalRow, LanguageAspect $languageAspect=null)
Definition: PageRepository.php:354
‪TYPO3\CMS\Core\Domain\Repository\PageRepositoryGetPageHookInterface
Definition: PageRepositoryGetPageHookInterface.php:22
‪TYPO3\CMS\Core\Domain\Access\RecordAccessVoter
Definition: RecordAccessVoter.php:29
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getMountPointInfo
‪mixed getMountPointInfo($pageId, $pageRec=false, $prevMountPids=[], $firstPageUid=0)
Definition: PageRepository.php:1215
‪TYPO3\CMS\Core\Context\UserAspect
Definition: UserAspect.php:37
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39