‪TYPO3CMS  ‪main
PageRepository.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Psr\EventDispatcher\EventDispatcherInterface;
21 use Psr\Log\LoggerAwareInterface;
22 use Psr\Log\LoggerAwareTrait;
55 
68 class ‪PageRepository implements LoggerAwareInterface
69 {
70  use LoggerAwareTrait;
71 
76  protected string ‪$where_hid_del = 'pages.deleted=0';
77 
81  protected string ‪$where_groupAccess = '';
82 
86  protected array ‪$computedPropertyNames = [
87  '_LOCALIZED_UID',
88  '_REQUESTED_OVERLAY_LANGUAGE',
89  '_MP_PARAM',
90  '_ORIG_uid',
91  '_ORIG_pid',
92  '_SHORTCUT_ORIGINAL_PAGE_UID',
93  ];
94 
98  public const ‪DOKTYPE_DEFAULT = 1;
99  public const ‪DOKTYPE_LINK = 3;
100  public const ‪DOKTYPE_SHORTCUT = 4;
101  public const ‪DOKTYPE_BE_USER_SECTION = 6;
102  public const ‪DOKTYPE_MOUNTPOINT = 7;
103  public const ‪DOKTYPE_SPACER = 199;
104  public const ‪DOKTYPE_SYSFOLDER = 254;
105 
109  public const ‪SHORTCUT_MODE_NONE = 0;
113 
115 
120  public function ‪__construct(‪Context ‪$context = null)
121  {
122  $this->context = ‪$context ?? GeneralUtility::makeInstance(Context::class);
123  $this->‪init();
124  }
125 
133  protected function ‪init(): void
134  {
135  $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id');
136  // As PageRepository may be used multiple times during the frontend request, and may
137  // actually be used before the usergroups have been resolved, self::getMultipleGroupsWhereClause()
138  // and the Event in ->enableFields() need to be reconsidered when the usergroup state changes.
139  // When something changes in the context, a second runtime cache entry is built.
140  // However, the PageRepository is generally in use for generating e.g. hundreds of links, so they would all use
141  // the same cache identifier.
142  $userAspect = $this->context->getAspect('frontend.user');
143  $frontendUserIdentifier = 'user_' . (int)$userAspect->get('id') . '_groups_' . md5(implode(',', $userAspect->getGroupIds()));
144 
145  // We need to respect the date aspect as we might have subrequests with a different time (e.g. backend preview links)
146  $dateTimeIdentifier = $this->context->getAspect('date')->get('timestamp');
147 
148  // If TRUE, the hidden-field is ignored. Normally this should be FALSE. Is used for previewing.
149  $includeHiddenPages = $this->context->getPropertyFromAspect('visibility', 'includeHiddenPages');
150 
151  $cache = $this->‪getRuntimeCache();
152  $cacheIdentifier = 'PageRepository_hidDelWhere' . ($includeHiddenPages ? 'ShowHidden' : '') . '_' . $workspaceId . '_' . $frontendUserIdentifier . '_' . $dateTimeIdentifier;
153  $cacheEntry = $cache->get($cacheIdentifier);
154  if ($cacheEntry) {
155  $this->where_hid_del = $cacheEntry;
156  } else {
157  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
158  ->getQueryBuilderForTable('pages')
159  ->expr();
160  if ($workspaceId > 0) {
161  // For version previewing, make sure that enable-fields are not
162  // de-selecting hidden pages - we need versionOL() to unset them only
163  // if the overlay record instructs us to.
164  // Clear where_hid_del and restrict to live and current workspaces
165  $this->where_hid_del = (string)$expressionBuilder->and(
166  $expressionBuilder->eq('pages.deleted', 0),
167  $expressionBuilder->or(
168  $expressionBuilder->eq('pages.t3ver_wsid', 0),
169  $expressionBuilder->eq('pages.t3ver_wsid', $workspaceId)
170  )
171  );
172  } else {
173  // add starttime / endtime, and check for hidden/deleted
174  // Filter out new/deleted place-holder pages in case we are NOT in a
175  // versioning preview (that means we are online!)
176  $constraints = $this->‪getDefaultConstraints('pages', ['fe_group' => true]);
177  $this->where_hid_del = $constraints === [] ? '' : (string)$expressionBuilder->and(...$constraints);
178  }
179  $cache->set($cacheIdentifier, $this->where_hid_del);
180  }
181  $this->where_groupAccess = $this->‪getMultipleGroupsWhereClause('pages.fe_group', 'pages');
182  }
183 
184  /**************************
185  *
186  * Selecting page records
187  *
188  **************************/
189 
216  public function ‪getPage(int ‪$uid, bool $disableGroupAccessCheck = false): array
217  {
218  // Dispatch Event to manipulate the page uid for special overlay handling
219  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(
220  new ‪BeforePageIsRetrievedEvent(‪$uid, $disableGroupAccessCheck, $this->context)
221  );
222  if ($event->hasPage()) {
223  // In case an event listener resolved the page on its own, directly return it
224  return $event->getPage()->toArray(true);
225  }
226  ‪$uid = $event->getPageId();
227  $cacheIdentifier = 'PageRepository_getPage_' . md5(
228  implode(
229  '-',
230  [
231  ‪$uid,
232  $disableGroupAccessCheck ? '' : $this->where_groupAccess,
233  $this->where_hid_del,
234  $this->context->getPropertyFromAspect('language', 'id', 0),
235  ]
236  )
237  );
238  $cache = $this->‪getRuntimeCache();
239  $cacheEntry = $cache->get($cacheIdentifier);
240  if (is_array($cacheEntry)) {
241  return $cacheEntry;
242  }
243  $result = [];
244  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
245  $queryBuilder->getRestrictions()->removeAll();
246  $queryBuilder->select('*')
247  ->from('pages')
248  ->where(
249  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)‪$uid, ‪Connection::PARAM_INT)),
250  $this->where_hid_del
251  );
252 
253  $originalWhereGroupAccess = '';
254  if (!$disableGroupAccessCheck) {
255  $queryBuilder->andWhere(‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess));
256  } else {
257  $originalWhereGroupAccess = ‪$this->where_groupAccess;
258  $this->where_groupAccess = '';
259  }
260 
261  $row = $queryBuilder->executeQuery()->fetchAssociative();
262  if ($row) {
263  $this->‪versionOL('pages', $row);
264  if (is_array($row)) {
265  $result = $this->‪getLanguageOverlay('pages', $row);
266  }
267  }
268 
269  if ($disableGroupAccessCheck) {
270  $this->where_groupAccess = $originalWhereGroupAccess;
271  }
272 
273  $cache->set($cacheIdentifier, $result);
274  return $result;
275  }
276 
285  public function ‪getPage_noCheck(int ‪$uid): array
286  {
287  $cache = $this->‪getRuntimeCache();
288  $cacheIdentifier = 'PageRepository_getPage_noCheck_' . ‪$uid . '_' . $this->context->getPropertyFromAspect('language', 'id', 0) . '_' . (int)$this->context->getPropertyFromAspect('workspace', 'id');
289  $cacheEntry = $cache->get($cacheIdentifier);
290  if ($cacheEntry !== false) {
291  return $cacheEntry;
292  }
293 
294  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
295  $queryBuilder->getRestrictions()
296  ->removeAll()
297  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
298  $row = $queryBuilder->select('*')
299  ->from('pages')
300  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)))
301  ->executeQuery()
302  ->fetchAssociative();
303 
304  $result = [];
305  if ($row) {
306  $this->‪versionOL('pages', $row);
307  if (is_array($row)) {
308  $result = $this->‪getLanguageOverlay('pages', $row);
309  }
310  }
311  $cache->set($cacheIdentifier, $result);
312  return $result;
313  }
314 
325  public function ‪getLanguageOverlay(string $table, array $originalRow, ‪LanguageAspect $languageAspect = null): ?array
326  {
327  // table is not localizable, so return directly
328  if (!isset(‪$GLOBALS['TCA'][$table]['ctrl']['languageField'])) {
329  return $originalRow;
330  }
331 
332  try {
334  $languageAspect = $languageAspect ?? $this->context->getAspect('language');
335  } catch (‪AspectNotFoundException $e) {
336  // no overlays
337  return $originalRow;
338  }
339 
340  $eventDispatcher = GeneralUtility::makeInstance(EventDispatcherInterface::class);
341 
342  $event = $eventDispatcher->dispatch(new ‪BeforeRecordLanguageOverlayEvent($table, $originalRow, $languageAspect));
343  $languageAspect = $event->getLanguageAspect();
344  $originalRow = $event->getRecord();
345 
346  $attempted = false;
347  $localizedRecord = null;
348  if ($languageAspect->doOverlays()) {
349  $attempted = true;
350  // Mixed = if nothing is available in the selected language, try the fallbacks
351  // Fallbacks work as follows:
352  // 1. We have a default language record and then start doing overlays (= the basis for fallbacks)
353  // 2. Check if the actual requested language version is available in the DB (language=3 = canadian-french)
354  // 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
355  if ($languageAspect->getOverlayType() === ‪LanguageAspect::OVERLAYS_MIXED) {
356  $languageChain = $this->‪getLanguageFallbackChain($languageAspect);
357  $languageChain = array_reverse($languageChain);
358  if ($table === 'pages') {
359  $result = $this->‪getPageOverlay(
360  $originalRow,
361  new ‪LanguageAspect($languageAspect->getId(), $languageAspect->getId(), ‪LanguageAspect::OVERLAYS_MIXED, $languageChain)
362  );
363  if (!empty($result)) {
364  $localizedRecord = $result;
365  }
366  } else {
367  $languageChain = array_merge($languageChain, [$languageAspect->getContentId()]);
368  // Loop through each (fallback) language and see if there is a record
369  // However, we do not want to preserve the "originalRow", that's why we set the option to "OVERLAYS_ON"
370  while (($languageId = array_pop($languageChain)) !== null) {
371  $result = $this->‪getRecordOverlay(
372  $table,
373  $originalRow,
374  new ‪LanguageAspect($languageId, $languageId, ‪LanguageAspect::OVERLAYS_ON)
375  );
376  // If an overlay is found, return it
377  if (is_array($result)) {
378  $localizedRecord = $result;
379  $localizedRecord['_REQUESTED_OVERLAY_LANGUAGE'] = $languageAspect->getContentId();
380  break;
381  }
382  }
383  if ($localizedRecord === null) {
384  // If nothing was found, we set the localized record to the originalRow to simulate
385  // that the default language is "kept" (we want fallback to default language).
386  // Note: Most installations might have "type=fallback" set but do not set the default language
387  // as fallback. In the future - once we want to get rid of the magic "default language",
388  // this needs to behave different, and the "pageNotFound" special handling within fallbacks should be removed
389  // and we need to check explicitly on in_array(0, $languageAspect->getFallbackChain())
390  // However, getPageOverlay() a few lines above also returns the "default language page" as well.
391  $localizedRecord = $originalRow;
392  }
393  }
394  } else {
395  // The option to hide records if they were not explicitly selected, was chosen (OVERLAYS_ON/WITH_FLOATING)
396  // in the language configuration. So, here no changes are done.
397  if ($table === 'pages') {
398  $localizedRecord = $this->‪getPageOverlay($originalRow, $languageAspect);
399  } else {
400  $localizedRecord = $this->‪getRecordOverlay($table, $originalRow, $languageAspect);
401  }
402  }
403  } else {
404  // Free mode.
405  // For "pages": Pages are usually retrieved by fetching the page record in the default language.
406  // However, the originalRow should still fetch the page in a specific language (with fallbacks).
407  // The method "getPageOverlay" should still be called in order to get the page record in the correct language.
408  if ($table === 'pages' && $languageAspect->getId() > 0) {
409  $attempted = true;
410  $localizedRecord = $this->‪getPageOverlay($originalRow, $languageAspect);
411  }
412  }
413 
414  $event = new ‪AfterRecordLanguageOverlayEvent($table, $originalRow, $localizedRecord, $attempted, $languageAspect);
415  $event = $eventDispatcher->dispatch($event);
416 
417  // Return localized record or the original row, if no overlays were done
418  return $event->overlayingWasAttempted() ? $event->getLocalizedRecord() : $originalRow;
419  }
420 
429  public function ‪getPageOverlay(int|array $pageInput, ‪LanguageAspect|int $language = null): array
430  {
431  $rows = $this->‪getPagesOverlay([$pageInput], $language);
432  // Always an array in return
433  return $rows[0] ?? [];
434  }
435 
447  public function ‪getPagesOverlay(array $pagesInput, int|‪LanguageAspect $language = null): array
448  {
449  if (empty($pagesInput)) {
450  return [];
451  }
452  if (is_int($language)) {
453  $languageAspect = new ‪LanguageAspect($language, $language);
454  } else {
455  $languageAspect = $language ?? $this->context->getAspect('language');
456  }
457 
458  $overlays = [];
459  // If language UID is different from zero, do overlay:
460  if ($languageAspect->getId() > 0) {
461  $pageIds = [];
462  foreach ($pagesInput as $origPage) {
463  if (is_array($origPage)) {
464  // Was the whole record
465  $pageIds[] = (int)($origPage['uid'] ?? 0);
466  } else {
467  // Was the id
468  $pageIds[] = (int)$origPage;
469  }
470  }
471 
472  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch(
473  new ‪BeforePageLanguageOverlayEvent($pagesInput, $pageIds, $languageAspect)
474  );
475  $pagesInput = $event->getPageInput();
476  $overlays = $this->‪getPageOverlaysForLanguage($event->getPageIds(), $event->getLanguageAspect());
477  }
478 
479  // Create output:
480  $pagesOutput = [];
481  foreach ($pagesInput as $key => $origPage) {
482  if (is_array($origPage)) {
483  $pagesOutput[$key] = $origPage;
484  if (isset($origPage['uid'], $overlays[$origPage['uid']])) {
485  // Overwrite the original field with the overlay
486  foreach ($overlays[$origPage['uid']] as $fieldName => $fieldValue) {
487  if ($fieldName !== 'uid' && $fieldName !== 'pid') {
488  $pagesOutput[$key][$fieldName] = $fieldValue;
489  }
490  }
491  $pagesOutput[$key]['_TRANSLATION_SOURCE'] = new ‪Page($origPage);
492  }
493  } elseif (isset($overlays[$origPage])) {
494  $pagesOutput[$key] = $overlays[$origPage];
495  }
496  }
497  return $pagesOutput;
498  }
499 
507  public function ‪isPageSuitableForLanguage(array $page, ‪LanguageAspect $languageAspect): bool
508  {
509  $languageUid = $languageAspect->‪getId();
510  // Checks if the default language version can be shown
511  // Block page is set, if l18n_cfg allows plus: 1) Either default language or 2) another language but NO overlay record set for page!
512  $pageTranslationVisibility = new ‪PageTranslationVisibility((int)($page['l18n_cfg'] ?? 0));
513  if ((!$languageUid || !isset($page['_LOCALIZED_UID']))
514  && $pageTranslationVisibility->shouldBeHiddenInDefaultLanguage()
515  ) {
516  return false;
517  }
518  if ($languageUid > 0 && $pageTranslationVisibility->shouldHideTranslationIfNoTranslatedRecordExists()) {
519  if (!isset($page['_LOCALIZED_UID']) || (int)($page['sys_language_uid'] ?? 0) !== $languageUid) {
520  return false;
521  }
522  } elseif ($languageUid > 0) {
523  $languageUids = array_merge([$languageUid], $this->‪getLanguageFallbackChain($languageAspect));
524  return in_array((int)($page['sys_language_uid'] ?? 0), $languageUids, true);
525  }
526  return true;
527  }
528 
534  protected function ‪getLanguageFallbackChain(?‪LanguageAspect $languageAspect): array
535  {
536  $languageAspect = $languageAspect ?? $this->context->getAspect('language');
537  return array_filter($languageAspect->‪getFallbackChain(), ‪MathUtility::canBeInterpretedAsInteger(...));
538  }
539 
551  protected function ‪getPageOverlaysForLanguage(array $pageUids, ‪LanguageAspect $languageAspect): array
552  {
553  if ($pageUids === []) {
554  return [];
555  }
556 
557  $languageUids = array_merge([$languageAspect->‪getId()], $this->getLanguageFallbackChain($languageAspect));
558  // Remove default language ("0")
559  $languageUids = array_filter($languageUids);
560  $languageField = ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'];
561  $transOrigPointerField = ‪$GLOBALS['TCA']['pages']['ctrl']['transOrigPointerField'];
562 
563  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
564  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
565  // Because "fe_group" is an exclude field, so it is synced between overlays, the group restriction is removed for language overlays of pages
566  $queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class);
567 
568  $candidates = [];
569  $maxChunk = ‪PlatformInformation::getMaxBindParameters($queryBuilder->getConnection()->getDatabasePlatform());
570  foreach (array_chunk($pageUids, (int)floor($maxChunk / 3)) as $pageUidsChunk) {
571  $query = $queryBuilder
572  ->select('*')
573  ->from('pages')
574  ->where(
575  $queryBuilder->expr()->in(
576  $languageField,
577  $queryBuilder->createNamedParameter($languageUids, ‪Connection::PARAM_INT_ARRAY)
578  ),
579  $queryBuilder->expr()->in(
580  $transOrigPointerField,
581  $queryBuilder->createNamedParameter($pageUidsChunk, ‪Connection::PARAM_INT_ARRAY)
582  )
583  );
584 
585  // This has cache hits for the current page and for menus (little performance gain).
586  $cacheIdentifier = 'PageRepository_getPageOverlaysForLanguage_'
587  . hash('xxh3', $query->getSQL() . json_encode($query->getParameters()));
588  $rows = $this->‪getRuntimeCache()->get($cacheIdentifier);
589  if (!is_array($rows)) {
590  $rows = $query->executeQuery()->fetchAllAssociative();
591  $this->‪getRuntimeCache()->set($cacheIdentifier, $rows);
592  }
593 
594  foreach ($rows as $row) {
595  $pageId = $row[$transOrigPointerField];
596  $priority = array_search($row[$languageField], $languageUids);
597  $candidates[$pageId][$priority] = $row;
598  }
599  }
600 
601  $overlays = [];
602  foreach ($pageUids as $pageId) {
603  $languageRows = $candidates[$pageId] ?? [];
604  ksort($languageRows, SORT_NATURAL);
605  foreach ($languageRows as $row) {
606  // Found a result for the current language id
607  $this->‪versionOL('pages', $row);
608  if (is_array($row)) {
609  $row['_LOCALIZED_UID'] = (int)$row['uid'];
610  $row['_REQUESTED_OVERLAY_LANGUAGE'] = $languageUids[0];
611  // Unset vital fields that are NOT allowed to be overlaid:
612  unset($row['uid'], $row['pid']);
613  $overlays[$pageId] = $row;
614 
615  // Language fallback found, stop querying further languages
616  break;
617  }
618  }
619  }
620 
621  return $overlays;
622  }
623 
634  protected function ‪getRecordOverlay(string $table, array $row, ‪LanguageAspect $languageAspect): ?array
635  {
636  // Early return when no overlays are needed
637  if ($languageAspect->‪getOverlayType() === ‪LanguageAspect::OVERLAYS_OFF) {
638  return $row;
639  }
640 
641  $tableControl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? [];
642  $languageField = $tableControl['languageField'] ?? '';
643  $transOrigPointerField = $tableControl['transOrigPointerField'] ?? '';
644 
645  // Only try overlays for tables with localization support
646  if (empty($languageField)) {
647  return $row;
648  }
649  if (empty($transOrigPointerField)) {
650  return $row;
651  }
652  $incomingLanguageId = (int)($row[$languageField] ?? 0);
653 
654  // Return record for ALL languages untouched
655  if ($incomingLanguageId === -1) {
656  return $row;
657  }
658 
659  $recordUid = (int)($row['uid'] ?? 0);
660  $incomingRecordPid = (int)($row['pid'] ?? 0);
661 
662  // @todo: Fix call stack to prevent this situation in the first place
663  if ($recordUid <= 0) {
664  return $row;
665  }
666  if ($incomingRecordPid <= 0 && !in_array($tableControl['rootLevel'] ?? false, [true, 1, -1], true)) {
667  return $row;
668  }
669  // When default language is displayed, we never want to return a record carrying
670  // another language.
671  if ($languageAspect->‪getContentId() === 0 && $incomingLanguageId > 0) {
672  return null;
673  }
674 
675  // Will try to overlay a record only if the contentId value is larger than zero,
676  // contentId is used for regular records, whereas getId() is used for "pages" only.
677  if ($languageAspect->‪getContentId() === 0) {
678  return $row;
679  }
680  // Must be default language, otherwise no overlaying
681  if ($incomingLanguageId === 0) {
682  // Select overlay record:
683  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
684  ->getQueryBuilderForTable($table);
685  $queryBuilder->setRestrictions(
686  GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)
687  );
688  if ((int)$this->context->getPropertyFromAspect('workspace', 'id') > 0) {
689  // If not in live workspace, remove query based "enable fields" checks, it will be done in versionOL()
690  // @see functional workspace test createLocalizedNotHiddenWorkspaceContentHiddenInLive()
691  $queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
692  $queryBuilder->getRestrictions()->removeByType(StartTimeRestriction::class);
693  $queryBuilder->getRestrictions()->removeByType(EndTimeRestriction::class);
694  // We keep the WorkspaceRestriction in this case, because we need to get the LIVE record
695  // of the language record before doing the version overlay of the language again. WorkspaceRestriction
696  // does this for us, PLUS we need to ensure to get a possible LIVE record first (that's why
697  // the "orderBy" query is there, so the LIVE record is found first), as there might only be a
698  // versioned record (e.g. new version) or both (common for modifying, moving etc).
699  if ($this->‪hasTableWorkspaceSupport($table)) {
700  $queryBuilder->orderBy('t3ver_wsid', 'ASC');
701  }
702  }
703 
704  $pid = $incomingRecordPid;
705  // When inside a workspace, the already versioned $row of the default language is coming in
706  // For moved versioned records, the PID MIGHT be different. However, the idea of this function is
707  // to get the language overlay of the LIVE default record, and afterward get the versioned record
708  // the found (live) language record again, see the versionOL() call a few lines below.
709  // This means, we need to modify the $pid value for moved records, as they might be on a different
710  // page and use the PID of the LIVE version.
711  if (isset($row['_ORIG_pid']) && $this->‪hasTableWorkspaceSupport($table) && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER) {
712  $pid = $row['_ORIG_pid'];
713  }
714  $olrow = $queryBuilder->select('*')
715  ->from($table)
716  ->where(
717  $queryBuilder->expr()->eq(
718  'pid',
719  $queryBuilder->createNamedParameter($pid, ‪Connection::PARAM_INT)
720  ),
721  $queryBuilder->expr()->eq(
722  $languageField,
723  $queryBuilder->createNamedParameter($languageAspect->‪getContentId(), ‪Connection::PARAM_INT)
724  ),
725  $queryBuilder->expr()->eq(
726  $transOrigPointerField,
727  $queryBuilder->createNamedParameter($recordUid, ‪Connection::PARAM_INT)
728  )
729  )
730  ->setMaxResults(1)
731  ->executeQuery()
732  ->fetchAssociative();
733 
734  $this->‪versionOL($table, $olrow);
735  // Merge record content by traversing all fields:
736  if (is_array($olrow)) {
737  if (isset($olrow['_ORIG_uid'])) {
738  $row['_ORIG_uid'] = $olrow['_ORIG_uid'];
739  }
740  if (isset($olrow['_ORIG_pid'])) {
741  $row['_ORIG_pid'] = $olrow['_ORIG_pid'];
742  }
743  foreach ($row as $fN => $fV) {
744  if ($fN !== 'uid' && $fN !== 'pid' && array_key_exists($fN, $olrow)) {
745  $row[$fN] = $olrow[$fN];
746  } elseif ($fN === 'uid') {
747  $row['_LOCALIZED_UID'] = (int)$olrow['uid'];
748  // will be overridden again outside of this method if there is a multi-level chain
749  $row['_REQUESTED_OVERLAY_LANGUAGE'] = $olrow[$languageField];
750  }
751  }
752  return $row;
753  }
754  // No overlay found.
755  // Unset, if non-translated records should be hidden. ONLY done if the source
756  // record really is default language and not [All] in which case it is allowed.
758  return null;
759  }
760  } elseif ($languageAspect->‪getContentId() !== $incomingLanguageId) {
761  return null;
762  }
763  return $row;
764  }
765 
766  /************************************************
767  *
768  * Page related: Menu, Domain record, Root line
769  *
770  ************************************************/
771 
789  public function ‪getMenu($pageId, ‪$fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true, bool $disableGroupAccessCheck = false)
790  {
791  // @todo: Restricting $fields to a list like 'uid, title' here, leads to issues from methods like
792  // getSubpagesForPages() which access keys like 'doktype'. This is odd, select field list
793  // should be handled better here, probably at least containing fields that are used in the
794  // sub methods. In the end, it might be easier to drop argument $fields altogether and
795  // always select * ?
796  return $this->‪getSubpagesForPages((array)$pageId, ‪$fields, $sortField, $additionalWhereClause, $checkShortcuts, true, $disableGroupAccessCheck);
797  }
798 
813  public function ‪getMenuForPages(array $pageIds, ‪$fields = '*', $sortField = 'sorting', $additionalWhereClause = '', $checkShortcuts = true, bool $disableGroupAccessCheck = false)
814  {
815  return $this->‪getSubpagesForPages($pageIds, ‪$fields, $sortField, $additionalWhereClause, $checkShortcuts, false, $disableGroupAccessCheck);
816  }
817 
854  protected function ‪getSubpagesForPages(
855  array $pageIds,
856  string ‪$fields = '*',
857  string $sortField = 'sorting',
858  string $additionalWhereClause = '',
859  bool $checkShortcuts = true,
860  bool $parentPages = true,
861  bool $disableGroupAccessCheck = false
862  ): array {
863  $relationField = $parentPages ? 'pid' : 'uid';
864 
865  if ($disableGroupAccessCheck) {
866  $whereGroupAccessCheck = ‪$this->where_groupAccess;
867  $this->where_groupAccess = '';
868  }
869 
870  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
871  $queryBuilder->getRestrictions()
872  ->removeAll()
873  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->context->getPropertyFromAspect('workspace', 'id')));
874 
875  $res = $queryBuilder->select(...‪GeneralUtility::trimExplode(',', ‪$fields, true))
876  ->from('pages')
877  ->where(
878  $queryBuilder->expr()->in(
879  $relationField,
880  $queryBuilder->createNamedParameter($pageIds, ‪Connection::PARAM_INT_ARRAY)
881  ),
882  $queryBuilder->expr()->eq(
883  ‪$GLOBALS['TCA']['pages']['ctrl']['languageField'],
884  $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT)
885  ),
886  $this->where_hid_del,
887  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
888  ‪QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
889  );
890 
891  if (!empty($sortField)) {
892  $orderBy = ‪QueryHelper::parseOrderBy($sortField);
893  foreach ($orderBy as $order) {
894  $res->addOrderBy($order[0], $order[1] ?? 'ASC');
895  }
896  }
897  $result = $res->executeQuery();
898 
899  $pages = [];
900  while ($page = $result->fetchAssociative()) {
901  $originalUid = $page['uid'];
902 
903  // Versioning Preview Overlay
904  $this->‪versionOL('pages', $page, true);
905  // Skip if page got disabled due to version overlay (might be delete placeholder)
906  if (empty($page)) {
907  continue;
908  }
909 
910  // Add a mount point parameter if needed
911  $page = $this->‪addMountPointParameterToPage((array)$page);
912 
913  // If shortcut, look up if the target exists and is currently visible
914  if ($checkShortcuts) {
915  $page = $this->‪checkValidShortcutOfPage((array)$page, $additionalWhereClause);
916  }
917 
918  // If the page still is there, we add it to the output
919  if (!empty($page)) {
920  $pages[$originalUid] = $page;
921  }
922  }
923 
924  if ($disableGroupAccessCheck) {
925  $this->where_groupAccess = $whereGroupAccessCheck;
926  }
927 
928  // Finally load language overlays
929  return $this->‪getPagesOverlay($pages);
930  }
931 
947  protected function ‪addMountPointParameterToPage(array $page): array
948  {
949  if (empty($page)) {
950  return [];
951  }
952 
953  // $page MUST have "uid", "pid", "doktype", "mount_pid", "mount_pid_ol" fields in it
954  $mountPointInfo = $this->‪getMountPointInfo($page['uid'], $page);
955 
956  // There is a valid mount point in overlay mode.
957  if (is_array($mountPointInfo) && $mountPointInfo['overlay']) {
958  // Using "getPage" is OK since we need the check for enableFields AND for type 2
959  // of mount pids we DO require a doktype < 200!
960  $mountPointPage = $this->‪getPage((int)$mountPointInfo['mount_pid']);
961 
962  if (!empty($mountPointPage)) {
963  $page = $mountPointPage;
964  $page['_MP_PARAM'] = $mountPointInfo['MPvar'];
965  } else {
966  $page = [];
967  }
968  }
969  return $page;
970  }
971 
978  protected function ‪checkValidShortcutOfPage(array $page, string $additionalWhereClause): array
979  {
980  if (empty($page)) {
981  return [];
982  }
983 
984  $dokType = (int)($page['doktype'] ?? 0);
985  $shortcutMode = (int)($page['shortcut_mode'] ?? 0);
986 
987  if ($dokType === self::DOKTYPE_SHORTCUT && (($shortcut = (int)($page['shortcut'] ?? 0)) || $shortcutMode)) {
988  if ($shortcutMode === self::SHORTCUT_MODE_NONE) {
989  // No shortcut_mode set, so target is directly set in $page['shortcut']
990  $searchField = 'uid';
991  $searchUid = $shortcut;
992  } elseif ($shortcutMode === self::SHORTCUT_MODE_FIRST_SUBPAGE || $shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) {
993  // Check subpages - first subpage or random subpage
994  $searchField = 'pid';
995  // If a shortcut mode is set and no valid page is given to select subpages
996  // from use the actual page.
997  $searchUid = $shortcut ?: $page['uid'];
998  } elseif ($shortcutMode === self::SHORTCUT_MODE_PARENT_PAGE) {
999  // Shortcut to parent page
1000  $searchField = 'uid';
1001  $searchUid = $page['pid'];
1002  } else {
1003  $searchField = '';
1004  $searchUid = 0;
1005  }
1006 
1007  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1008  $queryBuilder->getRestrictions()->removeAll();
1009  $count = $queryBuilder->count('uid')
1010  ->from('pages')
1011  ->where(
1012  $queryBuilder->expr()->eq(
1013  $searchField,
1014  $queryBuilder->createNamedParameter($searchUid, ‪Connection::PARAM_INT)
1015  ),
1016  $this->where_hid_del,
1017  ‪QueryHelper::stripLogicalOperatorPrefix($this->where_groupAccess),
1018  ‪QueryHelper::stripLogicalOperatorPrefix($additionalWhereClause)
1019  )
1020  ->executeQuery()
1021  ->fetchOne();
1022 
1023  if (!$count) {
1024  $page = [];
1025  }
1026  } elseif ($dokType === self::DOKTYPE_SHORTCUT) {
1027  // Neither shortcut target nor mode is set. Remove the page from the menu.
1028  $page = [];
1029  }
1030  return $page;
1031  }
1032 
1049  protected function ‪getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration = 20, $pageLog = [], $disableGroupCheck = false, bool $resolveRandomPageShortcuts = true)
1050  {
1051  // @todo: Simplify! page['shortcut'] is maxitems 1 and not a comma separated list of values!
1052  $idArray = ‪GeneralUtility::intExplode(',', $shortcutFieldValue);
1053  if ($resolveRandomPageShortcuts === false && (int)$shortcutMode === self::SHORTCUT_MODE_RANDOM_SUBPAGE) {
1054  return [];
1055  }
1056  // Find $page record depending on shortcut mode:
1057  switch ($shortcutMode) {
1060  $excludedDoktypes = [
1064  ];
1065  $savedWhereGroupAccess = '';
1066  // "getMenu()" does not allow to hand over $disableGroupCheck, for this reason it is manually disabled and re-enabled afterwards.
1067  if ($disableGroupCheck) {
1068  $savedWhereGroupAccess = ‪$this->where_groupAccess;
1069  $this->where_groupAccess = '';
1070  }
1071  $pageArray = $this->‪getMenu($idArray[0] ?: (int)$thisUid, '*', 'sorting', 'AND pages.doktype NOT IN (' . implode(', ', $excludedDoktypes) . ')');
1072  if ($disableGroupCheck) {
1073  $this->where_groupAccess = $savedWhereGroupAccess;
1074  }
1075  $pO = 0;
1076  if ($shortcutMode == self::SHORTCUT_MODE_RANDOM_SUBPAGE && !empty($pageArray)) {
1077  $pO = (int)random_int(0, count($pageArray) - 1);
1078  }
1079  $c = 0;
1080  $page = [];
1081  foreach ($pageArray as $pV) {
1082  if ($c === $pO) {
1083  $page = $pV;
1084  break;
1085  }
1086  $c++;
1087  }
1088  if (empty($page)) {
1089  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a subpage. However, this page has no accessible subpages.';
1090  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648328);
1091  }
1092  break;
1094  $parent = $this->‪getPage(($idArray[0] ?: (int)$thisUid), $disableGroupCheck);
1095  $page = $this->‪getPage((int)$parent['pid'], $disableGroupCheck);
1096  if (empty($page)) {
1097  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to its parent page. However, the parent page is not accessible.';
1098  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648358);
1099  }
1100  break;
1101  default:
1102  $page = $this->‪getPage($idArray[0], $disableGroupCheck);
1103  if (empty($page)) {
1104  $message = 'This page (ID ' . $thisUid . ') is of type "Shortcut" and configured to redirect to a page, which is not accessible (ID ' . $idArray[0] . ').';
1105  throw new ‪ShortcutTargetPageNotFoundException($message, 1301648404);
1106  }
1107  }
1108  // Check if shortcut page was a shortcut itself, if so look up recursively
1109  if ((int)$page['doktype'] === self::DOKTYPE_SHORTCUT) {
1110  if (!in_array($page['uid'], $pageLog) && $iteration > 0) {
1111  $pageLog[] = $page['uid'];
1112  $page = $this->‪getPageShortcut((string)$page['shortcut'], $page['shortcut_mode'], $page['uid'], $iteration - 1, $pageLog, $disableGroupCheck);
1113  } else {
1114  $pageLog[] = $page['uid'];
1115  $this->logger->error('Page shortcuts were looping in uids {uids}', ['uids' => implode(', ', array_values($pageLog))]);
1116  // @todo: This shouldn't be a \RuntimeException since editors can construct loops. It should trigger 500 handling or something.
1117  throw new \RuntimeException('Page shortcuts were looping in uids: ' . implode(', ', array_values($pageLog)), 1294587212);
1118  }
1119  }
1120  // Return resulting page:
1121  return $page;
1122  }
1123 
1134  public function ‪resolveShortcutPage(array $page, bool $resolveRandomSubpages = false, bool $disableGroupAccessCheck = false): array
1135  {
1136  if ((int)($page['doktype'] ?? 0) !== self::DOKTYPE_SHORTCUT) {
1137  return $page;
1138  }
1139  $shortcutMode = (int)($page['shortcut_mode'] ?? self::SHORTCUT_MODE_NONE);
1140  $shortcutTarget = (string)($page['shortcut'] ?? '');
1141 
1142  $cacheIdentifier = 'shortcuts_resolved_' . ($disableGroupAccessCheck ? '1' : '0') . '_' . $page['uid'] . '_' . $this->context->getPropertyFromAspect('language', 'id', 0) . '_' . $page['sys_language_uid'];
1143  // Only use the runtime cache if we do not support the random subpages functionality
1144  if ($resolveRandomSubpages === false) {
1145  $cachedResult = $this->‪getRuntimeCache()->get($cacheIdentifier);
1146  if (is_array($cachedResult)) {
1147  return $cachedResult;
1148  }
1149  }
1150  $shortcut = $this->‪getPageShortcut(
1151  $shortcutTarget,
1152  $shortcutMode,
1153  $page['uid'],
1154  20,
1155  [],
1156  $disableGroupAccessCheck,
1157  $resolveRandomSubpages
1158  );
1159  if (!empty($shortcut)) {
1160  $shortcutOriginalPageUid = (int)$page['uid'];
1161  $page = $shortcut;
1162  $page['_SHORTCUT_ORIGINAL_PAGE_UID'] = $shortcutOriginalPageUid;
1163  }
1164 
1165  if ($resolveRandomSubpages === false) {
1166  $this->‪getRuntimeCache()->set($cacheIdentifier, $page);
1167  }
1168 
1169  return $page;
1170  }
1171 
1204  public function ‪getMountPointInfo($pageId, $pageRec = false, $prevMountPids = [], $firstPageUid = 0)
1205  {
1206  if (!‪$GLOBALS['TYPO3_CONF_VARS']['FE']['enable_mount_pids']) {
1207  return false;
1208  }
1209  $cacheIdentifier = 'PageRepository_getMountPointInfo_' . $pageId;
1210  $cache = $this->‪getRuntimeCache();
1211  if ($cache->has($cacheIdentifier)) {
1212  return $cache->get($cacheIdentifier);
1213  }
1214  $result = false;
1215  // Get pageRec if not supplied:
1216  if (!is_array($pageRec)) {
1217  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1218  $queryBuilder->getRestrictions()
1219  ->removeAll()
1220  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1221 
1222  $pageRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
1223  ->from('pages')
1224  ->where(
1225  $queryBuilder->expr()->eq(
1226  'uid',
1227  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
1228  )
1229  )
1230  ->executeQuery()
1231  ->fetchAssociative();
1232 
1233  // Only look for version overlay if page record is not supplied; This assumes
1234  // that the input record is overlaid with preview version, if any!
1235  $this->‪versionOL('pages', $pageRec);
1236  }
1237  // Set first Page uid:
1238  if (!$firstPageUid) {
1239  $firstPageUid = (int)($pageRec['l10n_parent'] ?? false) ?: $pageRec['uid'] ?? 0;
1240  }
1241  // Look for mount pid value plus other required circumstances:
1242  $mount_pid = (int)($pageRec['mount_pid'] ?? 0);
1243  $doktype = (int)($pageRec['doktype'] ?? 0);
1244  if (is_array($pageRec) && $doktype === self::DOKTYPE_MOUNTPOINT && $mount_pid > 0 && !in_array($mount_pid, $prevMountPids, true)) {
1245  // Get the mount point record (to verify its general existence):
1246  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1247  $queryBuilder->getRestrictions()
1248  ->removeAll()
1249  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1250 
1251  $mountRec = $queryBuilder->select('uid', 'pid', 'doktype', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'l10n_parent')
1252  ->from('pages')
1253  ->where(
1254  $queryBuilder->expr()->eq(
1255  'uid',
1256  $queryBuilder->createNamedParameter($mount_pid, ‪Connection::PARAM_INT)
1257  )
1258  )
1259  ->executeQuery()
1260  ->fetchAssociative();
1261 
1262  $this->‪versionOL('pages', $mountRec);
1263  if (is_array($mountRec)) {
1264  // Look for recursive mount point:
1265  $prevMountPids[] = $mount_pid;
1266  $recursiveMountPid = $this->‪getMountPointInfo($mount_pid, $mountRec, $prevMountPids, $firstPageUid);
1267  // Return mount point information:
1268  $result = $recursiveMountPid ?: [
1269  'mount_pid' => $mount_pid,
1270  'overlay' => $pageRec['mount_pid_ol'],
1271  'MPvar' => $mount_pid . '-' . $firstPageUid,
1272  'mount_point_rec' => $pageRec,
1273  'mount_pid_rec' => $mountRec,
1274  ];
1275  } else {
1276  // Means, there SHOULD have been a mount point, but there was none!
1277  $result = -1;
1278  }
1279  }
1280  $cache->set($cacheIdentifier, $result);
1281  return $result;
1282  }
1283 
1292  public function ‪filterAccessiblePageIds(array $pageIds, ‪QueryRestrictionContainerInterface $restrictionContainer = null): array
1293  {
1294  if ($pageIds === []) {
1295  return [];
1296  }
1297  $validPageIds = [];
1298  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1299  $queryBuilder->setRestrictions(
1300  $restrictionContainer ?? GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)
1301  );
1302  $statement = $queryBuilder->select('uid')
1303  ->from('pages')
1304  ->where(
1305  $queryBuilder->expr()->in(
1306  'uid',
1307  $queryBuilder->createNamedParameter($pageIds, ‪Connection::PARAM_INT_ARRAY)
1308  )
1309  )
1310  ->executeQuery();
1311  while ($row = $statement->fetchAssociative()) {
1312  $validPageIds[] = (int)$row['uid'];
1313  }
1314  return $validPageIds;
1315  }
1316  /********************************
1317  *
1318  * Selecting records in general
1319  *
1320  ********************************/
1321 
1331  public function ‪checkRecord(string $table, int ‪$uid, bool $checkPage = false): ?array
1332  {
1333  if (!is_array(‪$GLOBALS['TCA'][$table])) {
1334  return null;
1335  }
1336  if (‪$uid <= 0) {
1337  return null;
1338  }
1339  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1340  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1341  $row = $queryBuilder->select('*')
1342  ->from($table)
1343  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)))
1344  ->executeQuery()
1345  ->fetchAssociative();
1346 
1347  if ($row) {
1348  $this->‪versionOL($table, $row);
1349  if (is_array($row)) {
1350  if ($checkPage) {
1351  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1352  ->getQueryBuilderForTable('pages');
1353  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1354  $numRows = (int)$queryBuilder->count('*')
1355  ->from('pages')
1356  ->where(
1357  $queryBuilder->expr()->eq(
1358  'uid',
1359  $queryBuilder->createNamedParameter($row['pid'], ‪Connection::PARAM_INT)
1360  )
1361  )
1362  ->executeQuery()
1363  ->fetchOne();
1364  if ($numRows > 0) {
1365  return $row;
1366  }
1367  return null;
1368  }
1369  return $row;
1370  }
1371  }
1372  return null;
1373  }
1374 
1385  public function ‪getRawRecord(string $table, int ‪$uid, array ‪$fields = ['*']): ?array
1386  {
1387  if (‪$uid <= 0) {
1388  return null;
1389  }
1390  if (!is_array(‪$GLOBALS['TCA'][$table])) {
1391  return null;
1392  }
1393  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1394  $queryBuilder->getRestrictions()
1395  ->removeAll()
1396  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1397  $row = $queryBuilder
1398  ->select(...‪$fields)
1399  ->from($table)
1400  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)))
1401  ->executeQuery()
1402  ->fetchAssociative();
1403 
1404  if ($row) {
1405  $this->‪versionOL($table, $row);
1406  if (is_array($row)) {
1407  return $row;
1408  }
1409  }
1410  return null;
1411  }
1412 
1413  /********************************
1414  *
1415  * Standard clauses
1416  *
1417  ********************************/
1418 
1435  public function ‪enableFields(string $table, int $show_hidden = -1, array $ignore_array = []): string
1436  {
1437  trigger_error('PageRepository->enableFields() will be removed in TYPO3 v14.0. Use ->getDefaultConstraints() instead.', E_USER_DEPRECATED);
1438  if ($show_hidden === -1) {
1439  // If show_hidden was not set from outside, use the current context
1440  $ignore_array['disabled'] = (bool)$this->context->getPropertyFromAspect('visibility', $table === 'pages' ? 'includeHiddenPages' : 'includeHiddenContent', false);
1441  } else {
1442  $ignore_array['disabled'] = (bool)$show_hidden;
1443  }
1444  $constraints = $this->‪getDefaultConstraints($table, $ignore_array);
1445  if ($constraints === []) {
1446  return '';
1447  }
1448  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1449  ->getQueryBuilderForTable($table)
1450  ->expr();
1451  return ' AND ' . $expressionBuilder->and(...$constraints);
1452  }
1453 
1467  public function ‪getDefaultConstraints(string $table, array $enableFieldsToIgnore = [], string $tableAlias = null): array
1468  {
1469  if (array_is_list($enableFieldsToIgnore)) {
1470  $enableFieldsToIgnore = array_flip($enableFieldsToIgnore);
1471  foreach ($enableFieldsToIgnore as $key => $value) {
1472  $enableFieldsToIgnore[$key] = true;
1473  }
1474  }
1475  $ctrl = ‪$GLOBALS['TCA'][$table]['ctrl'] ?? null;
1476  if (!is_array($ctrl)) {
1477  return [];
1478  }
1479  $tableAlias ??= $table;
1480 
1481  // If set, any hidden-fields in records are ignored, falling back to the default property from the visibility aspect
1482  if (!isset($enableFieldsToIgnore['disabled'])) {
1483  $enableFieldsToIgnore['disabled'] = (bool)$this->context->getPropertyFromAspect('visibility', $table === 'pages' ? 'includeHiddenPages' : 'includeHiddenContent', false);
1484  }
1485  $showScheduledRecords = $this->context->getPropertyFromAspect('visibility', 'includeScheduledRecords', false);
1486  if (!isset($enableFieldsToIgnore['starttime'])) {
1487  $enableFieldsToIgnore['starttime'] = $showScheduledRecords;
1488  }
1489  if (!isset($enableFieldsToIgnore['endtime'])) {
1490  $enableFieldsToIgnore['endtime'] = $showScheduledRecords;
1491  }
1492 
1493  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1494  ->getQueryBuilderForTable($table)
1495  ->expr();
1496 
1497  $constraints = [];
1498  // Delete field check
1499  if ($ctrl['delete'] ?? false) {
1500  $constraints['deleted'] = $expressionBuilder->eq($tableAlias . '.' . $ctrl['delete'], 0);
1501  }
1502 
1503  if ($this->‪hasTableWorkspaceSupport($table)) {
1504  // This should work exactly as WorkspaceRestriction and WorkspaceRestriction should be used instead
1505  if ((int)$this->context->getPropertyFromAspect('workspace', 'id') === 0) {
1506  // Filter out placeholder records (new/deleted items)
1507  // in case we are NOT in a version preview (that means we are online!)
1508  $constraints['workspaces'] = $expressionBuilder->and(
1509  $expressionBuilder->lte(
1510  $tableAlias . '.t3ver_state',
1511  VersionState::DEFAULT_STATE->value
1512  ),
1513  $expressionBuilder->eq($tableAlias . '.t3ver_wsid', 0)
1514  );
1515  } else {
1516  // show only records of live and of the current workspace
1517  // in case we are in a versioning preview
1518  $constraints['workspaces'] = $expressionBuilder->or(
1519  $expressionBuilder->eq($tableAlias . '.t3ver_wsid', 0),
1520  $expressionBuilder->eq($tableAlias . '.t3ver_wsid', (int)$this->context->getPropertyFromAspect('workspace', 'id'))
1521  );
1522  }
1523 
1524  // Filter out versioned records
1525  if (empty($enableFieldsToIgnore['pid'])) {
1526  // Always filter out versioned records that have an "offline" record
1527  $constraints['pid'] = $expressionBuilder->or(
1528  $expressionBuilder->eq($tableAlias . '.t3ver_oid', 0),
1529  $expressionBuilder->eq($tableAlias . '.t3ver_state', VersionState::MOVE_POINTER->value)
1530  );
1531  }
1532  }
1533 
1534  // Enable fields
1535  if (is_array($ctrl['enablecolumns'] ?? false)) {
1536  // In case of versioning-preview, enableFields are ignored (checked in versionOL())
1537  if ((int)$this->context->getPropertyFromAspect('workspace', 'id') === 0 || !$this->hasTableWorkspaceSupport($table)) {
1538 
1539  if (($ctrl['enablecolumns']['disabled'] ?? false) && !$enableFieldsToIgnore['disabled']) {
1540  $constraints['disabled'] = $expressionBuilder->eq(
1541  $tableAlias . '.' . $ctrl['enablecolumns']['disabled'],
1542  0
1543  );
1544  }
1545  if (($ctrl['enablecolumns']['starttime'] ?? false) && !($enableFieldsToIgnore['starttime'] ?? false)) {
1546  $constraints['starttime'] = $expressionBuilder->lte(
1547  $tableAlias . '.' . $ctrl['enablecolumns']['starttime'],
1548  $this->context->getPropertyFromAspect('date', 'accessTime', 0)
1549  );
1550  }
1551  if (($ctrl['enablecolumns']['endtime'] ?? false) && !($enableFieldsToIgnore['endtime'] ?? false)) {
1552  $field = $tableAlias . '.' . $ctrl['enablecolumns']['endtime'];
1553  $constraints['endtime'] = $expressionBuilder->or(
1554  $expressionBuilder->eq($field, 0),
1555  $expressionBuilder->gt(
1556  $field,
1557  $this->context->getPropertyFromAspect('date', 'accessTime', 0)
1558  )
1559  );
1560  }
1561  if (($ctrl['enablecolumns']['fe_group'] ?? false) && !($enableFieldsToIgnore['fe_group'] ?? false)) {
1562  $field = $tableAlias . '.' . $ctrl['enablecolumns']['fe_group'];
1563  $constraints['fe_group'] = ‪QueryHelper::stripLogicalOperatorPrefix(
1564  $this->‪getMultipleGroupsWhereClause($field, $table)
1565  );
1566  }
1567  }
1568  }
1569 
1570  // Call a PSR-14 Event for additional constraints
1571  $event = new ‪ModifyDefaultConstraintsForDatabaseQueryEvent($table, $tableAlias, $expressionBuilder, $constraints, $enableFieldsToIgnore, $this->context);
1572  $event = GeneralUtility::makeInstance(EventDispatcherInterface::class)->dispatch($event);
1573  return $event->getConstraints();
1574  }
1575 
1585  public function ‪getMultipleGroupsWhereClause(string $field, string $table): string
1586  {
1587  if (!$this->context->hasAspect('frontend.user')) {
1588  return '';
1589  }
1591  $userAspect = $this->context->getAspect('frontend.user');
1592  $memberGroups = $userAspect->getGroupIds();
1593  $cache = $this->‪getRuntimeCache();
1594  $cacheIdentifier = 'PageRepository_groupAccessWhere_' . md5($field . '_' . $table . '_' . implode('_', $memberGroups));
1595  $cacheEntry = $cache->get($cacheIdentifier);
1596  if ($cacheEntry) {
1597  return $cacheEntry;
1598  }
1599 
1600  $expressionBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1601  ->getQueryBuilderForTable($table)
1602  ->expr();
1603  $orChecks = [];
1604  // If the field is empty, then OK
1605  $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal(''));
1606  // If the field is NULL, then OK
1607  $orChecks[] = $expressionBuilder->isNull($field);
1608  // If the field contains zero, then OK
1609  $orChecks[] = $expressionBuilder->eq($field, $expressionBuilder->literal('0'));
1610  foreach ($memberGroups as $value) {
1611  $orChecks[] = $expressionBuilder->inSet($field, $expressionBuilder->literal((string)($value ?? '')));
1612  }
1613 
1614  $accessGroupWhere = ' AND (' . $expressionBuilder->or(...$orChecks) . ')';
1615  $cache->set($cacheIdentifier, $accessGroupWhere);
1616  return $accessGroupWhere;
1617  }
1618 
1619  /**********************
1620  *
1621  * Versioning Preview
1622  *
1623  **********************/
1624 
1644  public function ‪versionOL(string $table, &$row, bool $unsetMovePointers = false, bool $bypassEnableFieldsCheck = false): void
1645  {
1646  if ((int)$this->context->getPropertyFromAspect('workspace', 'id') <= 0) {
1647  return;
1648  }
1649  if (!is_array($row)) {
1650  return;
1651  }
1652  if (!isset($row['uid'], $row['t3ver_oid'])) {
1653  return;
1654  }
1655  // implode(',',array_keys($row)) = Using fields from original record to make
1656  // sure no additional fields are selected. This is best for eg. getPageOverlay()
1657  // Computed properties are excluded since those would lead to SQL errors.
1658  ‪$fields = array_keys($this->‪purgeComputedProperties($row));
1659  // will overlay any incoming moved record with the live record, which in turn
1660  // will be overlaid with its workspace version again to fetch both PID fields.
1661  $incomingRecordIsAMoveVersion = (int)$row['t3ver_oid'] > 0 && VersionState::tryFrom($row['t3ver_state'] ?? 0) === VersionState::MOVE_POINTER;
1662  if ($incomingRecordIsAMoveVersion) {
1663  // Fetch the live version again if the given $row is a move pointer, so we know the original PID
1664  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1665  $queryBuilder->getRestrictions()
1666  ->removeAll()
1667  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1668  $row = $queryBuilder->select(...‪$fields)
1669  ->from($table)
1670  ->where($queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$row['t3ver_oid'], ‪Connection::PARAM_INT)))
1671  ->executeQuery()
1672  ->fetchAssociative();
1673  }
1674  if ($wsAlt = $this->‪getWorkspaceVersionOfRecord($table, (int)$row['uid'], ‪$fields, $bypassEnableFieldsCheck)) {
1675  if (is_array($wsAlt)) {
1676  $rowVersionState = VersionState::tryFrom($wsAlt['t3ver_state'] ?? 0);
1677  if ($rowVersionState === VersionState::MOVE_POINTER) {
1678  // For move pointers, store the actual live PID in the _ORIG_pid
1679  // The only place where PID is actually different in a workspace
1680  $wsAlt['_ORIG_pid'] = $row['pid'];
1681  }
1682  // For versions of single elements or page+content, preserve online UID
1683  // (this will produce true "overlay" of element _content_, not any references)
1684  // For new versions there is no online counterpart
1685  if ($rowVersionState !== VersionState::NEW_PLACEHOLDER) {
1686  $wsAlt['_ORIG_uid'] = $wsAlt['uid'];
1687  }
1688  $wsAlt['uid'] = $row['uid'];
1689  // Changing input record to the workspace version alternative:
1690  $row = $wsAlt;
1691  // Unset record if it turned out to be deleted in workspace
1692  if ($rowVersionState === VersionState::DELETE_PLACEHOLDER) {
1693  $row = false;
1694  }
1695  // Check if move-pointer in workspace (unless if a move-placeholder is the
1696  // reason why it appears!):
1697  // You have to specifically set $unsetMovePointers in order to clear these
1698  // because it is normally a display issue if it should be shown or not.
1699  if ($rowVersionState === VersionState::MOVE_POINTER && !$incomingRecordIsAMoveVersion && $unsetMovePointers) {
1700  // Unset record if it turned out to be deleted in workspace
1701  $row = false;
1702  }
1703  } else {
1704  // No version found, then check if online version is dummy-representation
1705  // Notice, that unless $bypassEnableFieldsCheck is TRUE, the $row is unset if
1706  // enablefields for BOTH the version AND the online record deselects it. See
1707  // note for $bypassEnableFieldsCheck
1708  if ($wsAlt <= -1 || VersionState::tryFrom($row['t3ver_state'] ?? 0)->‪indicatesPlaceholder()) {
1709  // Unset record if it turned out to be "hidden"
1710  $row = false;
1711  }
1712  }
1713  }
1714  }
1715 
1728  public function ‪getWorkspaceVersionOfRecord(string $table, int ‪$uid, array ‪$fields = ['*'], bool $bypassEnableFieldsCheck = false): array|int|bool
1729  {
1730  $workspace = (int)$this->context->getPropertyFromAspect('workspace', 'id');
1731  // No look up in database because versioning not enabled / or workspace not offline
1732  if ($workspace === 0) {
1733  return false;
1734  }
1735  if (!$this->‪hasTableWorkspaceSupport($table)) {
1736  return false;
1737  }
1738  // Select workspace version of record, only testing for deleted.
1739  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1740  $queryBuilder->getRestrictions()
1741  ->removeAll()
1742  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
1743 
1744  $newrow = $queryBuilder
1745  ->select(...‪$fields)
1746  ->from($table)
1747  ->where(
1748  $queryBuilder->expr()->eq(
1749  't3ver_wsid',
1750  $queryBuilder->createNamedParameter($workspace, ‪Connection::PARAM_INT)
1751  ),
1752  $queryBuilder->expr()->or(
1753  // t3ver_state=1 does not contain a t3ver_oid, and returns itself
1754  $queryBuilder->expr()->and(
1755  $queryBuilder->expr()->eq(
1756  'uid',
1757  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1758  ),
1759  $queryBuilder->expr()->eq(
1760  't3ver_state',
1761  $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)
1762  )
1763  ),
1764  $queryBuilder->expr()->eq(
1765  't3ver_oid',
1766  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1767  )
1768  )
1769  )
1770  ->setMaxResults(1)
1771  ->executeQuery()
1772  ->fetchAssociative();
1773 
1774  // If version found, check if it could have been selected with enableFields on
1775  // as well:
1776  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($table);
1777  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context));
1778  // Remove the workspace restriction because we are testing a version record
1779  $queryBuilder->getRestrictions()->removeByType(WorkspaceRestriction::class);
1780  $queryBuilder->select('uid')
1781  ->from($table)
1782  ->setMaxResults(1);
1783 
1784  if (is_array($newrow)) {
1785  $queryBuilder->where(
1786  $queryBuilder->expr()->eq(
1787  't3ver_wsid',
1788  $queryBuilder->createNamedParameter($workspace, ‪Connection::PARAM_INT)
1789  ),
1790  $queryBuilder->expr()->or(
1791  // t3ver_state=1 does not contain a t3ver_oid, and returns itself
1792  $queryBuilder->expr()->and(
1793  $queryBuilder->expr()->eq(
1794  'uid',
1795  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1796  ),
1797  $queryBuilder->expr()->eq(
1798  't3ver_state',
1799  $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER->value, ‪Connection::PARAM_INT)
1800  )
1801  ),
1802  $queryBuilder->expr()->eq(
1803  't3ver_oid',
1804  $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT)
1805  )
1806  )
1807  );
1808  if ($bypassEnableFieldsCheck || $queryBuilder->executeQuery()->fetchOne()) {
1809  // Return offline version, tested for its enableFields.
1810  return $newrow;
1811  }
1812  // Return -1 because offline version was de-selected due to its enableFields.
1813  return -1;
1814  }
1815  // OK, so no workspace version was found. Then check if online version can be
1816  // selected with full enable fields and if so, return 1:
1817  $queryBuilder->where(
1818  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter(‪$uid, ‪Connection::PARAM_INT))
1819  );
1820  if ($bypassEnableFieldsCheck || $queryBuilder->executeQuery()->fetchOne()) {
1821  // Means search was done, but no version found.
1822  return 1;
1823  }
1824  // Return -2 because the online record was de-selected due to its enableFields.
1825  return -2;
1826  }
1827 
1836  public function ‪getPageIdsRecursive(array $pageIds, int $depth): array
1837  {
1838  if ($pageIds === []) {
1839  return [];
1840  }
1841  $pageIds = array_map(intval(...), $pageIds);
1842  if ($depth === 0) {
1843  return $pageIds;
1844  }
1845  $allPageIds = [];
1846  foreach ($pageIds as $pageId) {
1847  $allPageIds = array_merge($allPageIds, [$pageId], $this->‪getDescendantPageIdsRecursive($pageId, $depth));
1848  }
1849  return array_unique($allPageIds);
1850  }
1851 
1873  public function ‪getDescendantPageIdsRecursive(int $startPageId, int $depth, int $begin = 0, array $excludePageIds = [], bool $bypassEnableFieldsCheck = false): array
1874  {
1875  if (!$startPageId) {
1876  return [];
1877  }
1878  if (!$this->‪getRawRecord('pages', $startPageId, ['uid'])) {
1879  // Start page does not exist
1880  return [];
1881  }
1882  // Find mount point if any
1883  $mount_info = $this->‪getMountPointInfo($startPageId);
1884  $includePageId = false;
1885  if (is_array($mount_info)) {
1886  $startPageId = (int)$mount_info['mount_pid'];
1887  // In overlay mode, use the mounted page uid
1888  if ($mount_info['overlay']) {
1889  $includePageId = true;
1890  }
1891  }
1892  $descendantPageIds = $this->‪getSubpagesRecursive($startPageId, $depth, $begin, $excludePageIds, $bypassEnableFieldsCheck);
1893  if ($includePageId) {
1894  $descendantPageIds = array_merge([$startPageId], $descendantPageIds);
1895  }
1896  return $descendantPageIds;
1897  }
1898 
1906  protected function ‪getSubpagesRecursive(int $pageId, int $depth, int $begin, array $excludePageIds, bool $bypassEnableFieldsCheck, array $prevId_array = []): array
1907  {
1908  $descendantPageIds = [];
1909  // if $depth is 0, then we do not fetch subpages
1910  if ($depth === 0) {
1911  return [];
1912  }
1913  // Add this ID to the array of IDs
1914  if ($begin <= 0) {
1915  $prevId_array[] = $pageId;
1916  }
1917  // Select subpages
1918  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('pages');
1919  $queryBuilder->getRestrictions()
1920  ->removeAll()
1921  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1922  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->context->getPropertyFromAspect('workspace', 'id')));
1923  $queryBuilder->select('*')
1924  ->from('pages')
1925  ->where(
1926  $queryBuilder->expr()->eq(
1927  'pid',
1928  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
1929  ),
1930  // tree is only built by language=0 pages
1931  $queryBuilder->expr()->eq('sys_language_uid', 0)
1932  )
1933  ->orderBy('sorting');
1934 
1935  if ($excludePageIds !== []) {
1936  $queryBuilder->andWhere(
1937  $queryBuilder->expr()->notIn('uid', $queryBuilder->createNamedParameter($excludePageIds, ‪Connection::PARAM_INT_ARRAY))
1938  );
1939  }
1940 
1941  $result = $queryBuilder->executeQuery();
1942  while ($row = $result->fetchAssociative()) {
1943  $versionState = VersionState::tryFrom($row['t3ver_state'] ?? 0);
1944  $this->‪versionOL('pages', $row, false, $bypassEnableFieldsCheck);
1945  if ($row === false
1946  || (int)$row['doktype'] === self::DOKTYPE_BE_USER_SECTION
1947  || $versionState->indicatesPlaceholder()
1948  ) {
1949  // falsy row means Overlay prevents access to this page.
1950  // Doing this after the overlay to make sure changes
1951  // in the overlay are respected.
1952  // However, we do not process pages below of and
1953  // including of type BE user section
1954  continue;
1955  }
1956  // Find mount point if any:
1957  $next_id = (int)$row['uid'];
1958  $mount_info = $this->‪getMountPointInfo($next_id, $row);
1959  // Overlay mode:
1960  if (is_array($mount_info) && $mount_info['overlay']) {
1961  $next_id = (int)$mount_info['mount_pid'];
1962  // @todo: check if we could use $mount_info[mount_pid_rec] and check against $excludePageIds?
1963  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1964  ->getQueryBuilderForTable('pages');
1965  $queryBuilder->getRestrictions()
1966  ->removeAll()
1967  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
1968  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, (int)$this->context->getPropertyFromAspect('workspace', 'id')));
1969  $queryBuilder->select('*')
1970  ->from('pages')
1971  ->where(
1972  $queryBuilder->expr()->eq(
1973  'uid',
1974  $queryBuilder->createNamedParameter($next_id, ‪Connection::PARAM_INT)
1975  )
1976  )
1977  ->orderBy('sorting')
1978  ->setMaxResults(1);
1979 
1980  if ($excludePageIds !== []) {
1981  $queryBuilder->andWhere(
1982  $queryBuilder->expr()->notIn('uid', $queryBuilder->createNamedParameter($excludePageIds, ‪Connection::PARAM_INT_ARRAY))
1983  );
1984  }
1985 
1986  $row = $queryBuilder->executeQuery()->fetchAssociative();
1987  $this->‪versionOL('pages', $row);
1988  $versionState = VersionState::tryFrom($row['t3ver_state'] ?? 0);
1989  if ($row === false
1990  || (int)$row['doktype'] === self::DOKTYPE_BE_USER_SECTION
1991  || $versionState->indicatesPlaceholder()
1992  ) {
1993  // Doing this after the overlay to make sure
1994  // changes in the overlay are respected.
1995  // see above
1996  continue;
1997  }
1998  }
1999  $accessVoter = GeneralUtility::makeInstance(RecordAccessVoter::class);
2000  // Add record:
2001  if ($bypassEnableFieldsCheck || $accessVoter->accessGrantedForPageInRootLine($row, $this->context)) {
2002  // Add ID to list:
2003  if ($begin <= 0) {
2004  if ($bypassEnableFieldsCheck || $accessVoter->accessGranted('pages', $row, $this->context)) {
2005  $descendantPageIds[] = $next_id;
2006  }
2007  }
2008  // Next level
2009  if (!$row['php_tree_stop']) {
2010  // Normal mode:
2011  if (is_array($mount_info) && !$mount_info['overlay']) {
2012  $next_id = (int)$mount_info['mount_pid'];
2013  }
2014  // Call recursively, if the id is not in prevID_array:
2015  if (!in_array($next_id, $prevId_array, true)) {
2016  $descendantPageIds = array_merge(
2017  $descendantPageIds,
2018  $this->‪getSubpagesRecursive(
2019  $next_id,
2020  $depth - 1,
2021  $begin - 1,
2022  $excludePageIds,
2023  $bypassEnableFieldsCheck,
2024  $prevId_array
2025  )
2026  );
2027  }
2028  }
2029  }
2030  }
2031  return $descendantPageIds;
2032  }
2033 
2040  public function ‪checkIfPageIsHidden(int $pageId, ‪LanguageAspect $languageAspect): bool
2041  {
2042  if ($pageId === 0) {
2043  return false;
2044  }
2045  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
2046  ->getQueryBuilderForTable('pages');
2047  $queryBuilder
2048  ->getRestrictions()
2049  ->removeAll()
2050  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
2051 
2052  $queryBuilder
2053  ->select('uid', 'hidden', 'starttime', 'endtime')
2054  ->from('pages')
2055  ->setMaxResults(1);
2056 
2057  // $pageId always points to the ID of the default language page, so we check
2058  // the current site language to determine if we need to fetch a translation but consider fallbacks
2059  if ($languageAspect->‪getId() > 0) {
2060  $languagesToCheck = [$languageAspect->‪getId()];
2061  // Remove fallback information like "pageNotFound"
2062  foreach ($languageAspect->‪getFallbackChain() as $languageToCheck) {
2063  if (is_numeric($languageToCheck)) {
2064  $languagesToCheck[] = $languageToCheck;
2065  }
2066  }
2067  // Check for the language and all its fallbacks (except for default language)
2068  $constraint = $queryBuilder->expr()->and(
2069  $queryBuilder->expr()->eq('l10n_parent', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)),
2070  $queryBuilder->expr()->in('sys_language_uid', $queryBuilder->createNamedParameter(array_filter($languagesToCheck), ‪Connection::PARAM_INT_ARRAY))
2071  );
2072  // If the fallback language Ids also contains the default language, this needs to be considered
2073  if (in_array(0, $languagesToCheck, true)) {
2074  $constraint = $queryBuilder->expr()->or(
2075  $constraint,
2076  // Ensure to also fetch the default record
2077  $queryBuilder->expr()->and(
2078  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)),
2079  $queryBuilder->expr()->eq('sys_language_uid', 0)
2080  )
2081  );
2082  }
2083  $queryBuilder->where($constraint);
2084  // Ensure that the translated records are shown first (maxResults is set to 1)
2085  $queryBuilder->orderBy('sys_language_uid', 'DESC');
2086  } else {
2087  $queryBuilder->where(
2088  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT))
2089  );
2090  }
2091  $page = $queryBuilder->executeQuery()->fetchAssociative();
2092  if ((int)$this->context->getPropertyFromAspect('workspace', 'id') > 0) {
2093  // Fetch overlay of page if in workspace and check if it is hidden
2094  $backupContext = clone ‪$this->context;
2095  $this->context->‪setAspect('visibility', GeneralUtility::makeInstance(VisibilityAspect::class));
2096  $targetPage = $this->‪getWorkspaceVersionOfRecord('pages', (int)$page['uid']);
2097  // Also checks if the workspace version is NOT hidden but the live version is in fact still hidden
2098  $result = $targetPage === -1 || $targetPage === -2 || (is_array($targetPage) && $targetPage['hidden'] == 0 && $page['hidden'] == 1);
2099  $this->context = $backupContext;
2100  } else {
2101  $result = is_array($page) && ($page['hidden'] || $page['starttime'] > ‪$GLOBALS['SIM_EXEC_TIME'] || $page['endtime'] != 0 && $page['endtime'] <= ‪$GLOBALS['SIM_EXEC_TIME']);
2102  }
2103  return $result;
2104  }
2105 
2110  protected function ‪purgeComputedProperties(array $row): array
2111  {
2112  foreach ($this->computedPropertyNames as $computedPropertyName) {
2113  if (array_key_exists($computedPropertyName, $row)) {
2114  unset($row[$computedPropertyName]);
2115  }
2116  }
2117  return $row;
2118  }
2119 
2121  {
2122  return GeneralUtility::makeInstance(CacheManager::class)->getCache('runtime');
2123  }
2124 
2125  protected function ‪hasTableWorkspaceSupport(string $tableName): bool
2126  {
2127  return !empty(‪$GLOBALS['TCA'][$tableName]['ctrl']['versioningWS']);
2128  }
2129 }
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getRawRecord
‪array null getRawRecord(string $table, int $uid, array $fields=[' *'])
Definition: PageRepository.php:1385
‪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:854
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\isPageSuitableForLanguage
‪bool isPageSuitableForLanguage(array $page, LanguageAspect $languageAspect)
Definition: PageRepository.php:507
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\enableFields
‪string enableFields(string $table, int $show_hidden=-1, array $ignore_array=[])
Definition: PageRepository.php:1435
‪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\Domain\Repository\PageRepository\getMultipleGroupsWhereClause
‪string getMultipleGroupsWhereClause(string $field, string $table)
Definition: PageRepository.php:1585
‪TYPO3\CMS\Core\Context\VisibilityAspect
Definition: VisibilityAspect.php:31
‪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:789
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getRuntimeCache
‪getRuntimeCache()
Definition: PageRepository.php:2120
‪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:106
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\resolveShortcutPage
‪resolveShortcutPage(array $page, bool $resolveRandomSubpages=false, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:1134
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_DEFAULT
‪const DOKTYPE_DEFAULT
Definition: PageRepository.php:98
‪TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction
Definition: StartTimeRestriction.php:27
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPage_noCheck
‪array getPage_noCheck(int $uid)
Definition: PageRepository.php:285
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$context
‪Context $context
Definition: PageRepository.php:114
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getLanguageFallbackChain
‪int[] getLanguageFallbackChain(?LanguageAspect $languageAspect)
Definition: PageRepository.php:534
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$where_hid_del
‪string $where_hid_del
Definition: PageRepository.php:76
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPage
‪array getPage(int $uid, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:216
‪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:1873
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPagesOverlay
‪array getPagesOverlay(array $pagesInput, int|LanguageAspect $language=null)
Definition: PageRepository.php:447
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SHORTCUT
‪const DOKTYPE_SHORTCUT
Definition: PageRepository.php:100
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_LINK
‪const DOKTYPE_LINK
Definition: PageRepository.php:99
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$computedPropertyNames
‪array $computedPropertyNames
Definition: PageRepository.php:86
‪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:1292
‪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:634
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\addMountPointParameterToPage
‪array addMountPointParameterToPage(array $page)
Definition: PageRepository.php:947
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_RANDOM_SUBPAGE
‪const SHORTCUT_MODE_RANDOM_SUBPAGE
Definition: PageRepository.php:111
‪$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:110
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:131
‪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:1906
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageOverlay
‪array getPageOverlay(int|array $pageInput, LanguageAspect|int $language=null)
Definition: PageRepository.php:429
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageShortcut
‪mixed getPageShortcut($shortcutFieldValue, $shortcutMode, $thisUid, $iteration=20, $pageLog=[], $disableGroupCheck=false, bool $resolveRandomPageShortcuts=true)
Definition: PageRepository.php:1049
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkIfPageIsHidden
‪checkIfPageIsHidden(int $pageId, LanguageAspect $languageAspect)
Definition: PageRepository.php:2040
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkRecord
‪array null checkRecord(string $table, int $uid, bool $checkPage=false)
Definition: PageRepository.php:1331
‪TYPO3\CMS\Core\Utility\MathUtility\canBeInterpretedAsInteger
‪static bool canBeInterpretedAsInteger(mixed $var)
Definition: MathUtility.php:69
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\versionOL
‪versionOL(string $table, &$row, bool $unsetMovePointers=false, bool $bypassEnableFieldsCheck=false)
Definition: PageRepository.php:1644
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:102
‪TYPO3\CMS\Core\Database\Query\Expression\CompositeExpression
Definition: CompositeExpression.php:27
‪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\getWorkspaceVersionOfRecord
‪array int bool getWorkspaceVersionOfRecord(string $table, int $uid, array $fields=[' *'], bool $bypassEnableFieldsCheck=false)
Definition: PageRepository.php:1728
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\hasTableWorkspaceSupport
‪hasTableWorkspaceSupport(string $tableName)
Definition: PageRepository.php:2125
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\__construct
‪__construct(Context $context=null)
Definition: PageRepository.php:120
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\checkValidShortcutOfPage
‪checkValidShortcutOfPage(array $page, string $additionalWhereClause)
Definition: PageRepository.php:978
‪TYPO3\CMS\Core\Domain\Event\BeforePageIsRetrievedEvent
Definition: BeforePageIsRetrievedEvent.php:30
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SPACER
‪const DOKTYPE_SPACER
Definition: PageRepository.php:103
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_BE_USER_SECTION
‪const DOKTYPE_BE_USER_SECTION
Definition: PageRepository.php:101
‪TYPO3\CMS\Core\Cache\CacheManager
Definition: CacheManager.php:36
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageOverlaysForLanguage
‪getPageOverlaysForLanguage(array $pageUids, LanguageAspect $languageAspect)
Definition: PageRepository.php:551
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getMenuForPages
‪array getMenuForPages(array $pageIds, $fields=' *', $sortField='sorting', $additionalWhereClause='', $checkShortcuts=true, bool $disableGroupAccessCheck=false)
Definition: PageRepository.php:813
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_SYSFOLDER
‪const DOKTYPE_SYSFOLDER
Definition: PageRepository.php:104
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:57
‪TYPO3\CMS\Core\Domain\Repository
Definition: PageRepository.php:18
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\SHORTCUT_MODE_PARENT_PAGE
‪const SHORTCUT_MODE_PARENT_PAGE
Definition: PageRepository.php:112
‪TYPO3\CMS\Core\Context\Exception\AspectNotFoundException
Definition: AspectNotFoundException.php:25
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Cache\Frontend\FrontendInterface
Definition: FrontendInterface.php:22
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getPageIdsRecursive
‪int[] getPageIdsRecursive(array $pageIds, int $depth)
Definition: PageRepository.php:1836
‪TYPO3\CMS\Core\Error\Http\ShortcutTargetPageNotFoundException
Definition: ShortcutTargetPageNotFoundException.php:23
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\purgeComputedProperties
‪purgeComputedProperties(array $row)
Definition: PageRepository.php:2110
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getDefaultConstraints
‪CompositeExpression[] getDefaultConstraints(string $table, array $enableFieldsToIgnore=[], string $tableAlias=null)
Definition: PageRepository.php:1467
‪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
‪TYPO3\CMS\Core\Domain\Event\ModifyDefaultConstraintsForDatabaseQueryEvent
Definition: ModifyDefaultConstraintsForDatabaseQueryEvent.php:34
‪$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:109
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\$where_groupAccess
‪string $where_groupAccess
Definition: PageRepository.php:81
‪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\Context\LanguageAspect\OVERLAYS_OFF
‪const OVERLAYS_OFF
Definition: LanguageAspect.php:74
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:69
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪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\Utility\GeneralUtility\intExplode
‪static list< int > intExplode(string $delimiter, string $string, bool $removeEmptyValues=false)
Definition: GeneralUtility.php:756
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT_ARRAY
‪const PARAM_INT_ARRAY
Definition: Connection.php:72
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\init
‪init()
Definition: PageRepository.php:133
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\getLanguageOverlay
‪array null getLanguageOverlay(string $table, array $originalRow, LanguageAspect $languageAspect=null)
Definition: PageRepository.php:325
‪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:1204
‪TYPO3\CMS\Core\Context\UserAspect
Definition: UserAspect.php:37
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode(string $delim, string $string, bool $removeEmptyValues=false, int $limit=0)
Definition: GeneralUtility.php:822
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39