‪TYPO3CMS  ‪main
PageSlugCandidateProvider.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 
35 
43 {
44  protected ‪Site ‪$site;
47 
49  {
50  $this->context = ‪$context;
51  $this->site = ‪$site;
52  $this->enhancerFactory = ‪$enhancerFactory ?? GeneralUtility::makeInstance(EnhancerFactory::class);
53  }
54 
60  public function ‪getCandidatesForPath(string $urlPath, ‪SiteLanguage $language): array
61  {
62  $slugCandidates = $this->‪getCandidateSlugsFromRoutePath($urlPath ?: '/');
63  $pageCandidates = [];
64  ‪$languages = [$language->‪getLanguageId()];
65  if (!empty($language->‪getFallbackLanguageIds())) {
66  ‪$languages = array_merge(‪$languages, $language->‪getFallbackLanguageIds());
67  }
68  // Iterate all defined languages in their configured order to get matching page candidates somewhere in the language fallback chain
69  foreach (‪$languages as $languageId) {
70  $pageCandidatesFromSlugsAndLanguage = $this->‪getPagesFromDatabaseForCandidates($slugCandidates, $languageId);
71  // Determine whether fetched page candidates qualify for the request. The incoming URL is checked against all
72  // pages found for the current URL and language.
73  foreach ($pageCandidatesFromSlugsAndLanguage as $candidate) {
74  $slugCandidate = '/' . trim($candidate['slug'], '/');
75  if ($slugCandidate === '/' || str_starts_with($urlPath, $slugCandidate)) {
76  // The slug is a subpart of the requested URL, so it's a possible candidate
77  if ($urlPath === $slugCandidate) {
78  // The requested URL matches exactly the found slug. We can't find a better match,
79  // so use that page candidate and stop any further querying.
80  $pageCandidates = [$candidate];
81  break 2;
82  }
83 
84  $pageCandidates[] = $candidate;
85  }
86  }
87  }
88  return $pageCandidates;
89  }
90 
101  public function ‪getRealPageIdForPageIdAsPossibleCandidate(int $pageId): ?int
102  {
103  $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id');
104  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
105  ->getQueryBuilderForTable('pages');
106  $queryBuilder
107  ->getRestrictions()
108  ->removeAll()
109  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
110  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId));
111 
112  $statement = $queryBuilder
113  ->select('uid', 'l10n_parent')
114  ->from('pages')
115  ->where(
116  $queryBuilder->expr()->eq(
117  'uid',
118  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
119  )
120  )
121  ->executeQuery();
122 
123  $page = $statement->fetchAssociative();
124  if (empty($page)) {
125  return null;
126  }
127  return (int)($page['l10n_parent'] ?: $page['uid']);
128  }
129 
136  protected function ‪getRoutePathRedecorationPattern(): string
137  {
138  $decoratingEnhancers = $this->‪getDecoratingEnhancers();
139  if (empty($decoratingEnhancers)) {
140  return '';
141  }
142  $redecorationPatterns = array_map(
143  static function (‪DecoratingEnhancerInterface $decorationEnhancers) {
144  $pattern = $decorationEnhancers->‪getRoutePathRedecorationPattern();
145  return '(?:' . $pattern . ')';
146  },
147  $decoratingEnhancers
148  );
149  return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
150  }
151 
159  protected function ‪getDecoratingEnhancers(): array
160  {
161  $enhancers = [];
162  foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
163  $enhancerType = $enhancerConfiguration['type'] ?? '';
164  $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
165  if ($enhancer instanceof ‪DecoratingEnhancerInterface) {
166  $enhancers[] = $enhancer;
167  }
168  }
169  return $enhancers;
170  }
171 
179  protected function ‪getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids = []): array
180  {
181  $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id');
182  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
183  ->getQueryBuilderForTable('pages');
184  $queryBuilder
185  ->getRestrictions()
186  ->removeAll()
187  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
188  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId, true));
189 
190  $statement = $queryBuilder
191  ->select('uid', 'sys_language_uid', 'l10n_parent', 'l18n_cfg', 'pid', 'slug', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'doktype', 't3ver_wsid', 't3ver_oid')
192  ->from('pages')
193  ->where(
194  $queryBuilder->expr()->eq(
195  'sys_language_uid',
196  $queryBuilder->createNamedParameter($languageId, ‪Connection::PARAM_INT)
197  ),
198  $queryBuilder->expr()->in(
199  'slug',
200  $queryBuilder->createNamedParameter(
201  $slugCandidates,
203  )
204  )
205  )
206  // Exact match will be first, that's important
207  ->orderBy('slug', 'desc')
208  // versioned records should be rendered before the live records
209  ->addOrderBy('t3ver_wsid', 'desc')
210  // Sort pages that are not MountPoint pages before mount points
211  ->addOrderBy('mount_pid_ol', 'asc')
212  ->addOrderBy('mount_pid', 'asc')
213  ->executeQuery();
214 
215  $pages = [];
216  $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
217  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $this->context);
218  $isRecursiveCall = !empty($excludeUids);
219 
220  while ($row = $statement->fetchAssociative()) {
221  $mountPageInformation = null;
222  $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : ($row['t3ver_oid'] ?: $row['uid']));
223  // When this page was added before via recursion, this page should be skipped
224  if (in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
225  continue;
226  }
227 
228  try {
229  $isOnSameSite = $siteFinder->getSiteByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId();
230  } catch (‪SiteNotFoundException $e) {
231  // Page is not in a site, so it's not considered
232  $isOnSameSite = false;
233  }
234 
235  // If a MountPoint is found on the current site, and it hasn't been added yet by some other iteration
236  // (see below "findPageCandidatesOfMountPoint"), then let's resolve the MountPoint information now
237  if (!$isOnSameSite && $isRecursiveCall) {
238  // Not in the same site, and called recursive, should be skipped
239  continue;
240  }
241  $mountPageInformation = $pageRepository->getMountPointInfo($pageIdInDefaultLanguage, $row);
242 
243  // Mount Point Pages which are not on the same site (when not called on the first level) should be skipped
244  // As they just clutter up the queries.
245  if (!$isOnSameSite && !$isRecursiveCall && $mountPageInformation) {
246  continue;
247  }
248 
249  $mountedPage = null;
250  if ($mountPageInformation) {
251  // Add the MPvar to the row, so it can be used later-on in the PageRouter / PageArguments
252  $row['MPvar'] = $mountPageInformation['MPvar'];
253  $mountedPage = $pageRepository->getPage_noCheck((int)$mountPageInformation['mount_pid_rec']['uid']);
254  // Ensure to fetch the slug in the translated page
255  $mountedPage = $pageRepository->getLanguageOverlay('pages', $mountedPage, new ‪LanguageAspect($languageId, $languageId));
256  // Mount wasn't connected properly, so it is skipped
257  if (!$mountedPage) {
258  continue;
259  }
260  // If the page is a MountPoint which should be overlaid with the contents of the mounted page,
261  // it must never be accessible directly, but only in the MountPoint context. Therefore we change
262  // the current ID and slug.
263  // This needs to happen before the regular case, as the $pageToAdd contains the MPvar information
264  if ((int)$row['doktype'] === ‪PageRepository::DOKTYPE_MOUNTPOINT && $row['mount_pid_ol']) {
265  // If the mounted page was already added from above, this should not be added again (to include
266  // the mount point parameter).
267  if (in_array((int)$mountedPage['uid'], $excludeUids, true)) {
268  continue;
269  }
270  $pageToAdd = $mountedPage;
271  // Make sure target page "/about-us" is replaced by "/global-site/about-us" so router works
272  $pageToAdd['MPvar'] = $mountPageInformation['MPvar'];
273  $pageToAdd['slug'] = $row['slug'];
274  $pages[] = $pageToAdd;
275  $excludeUids[] = (int)$pageToAdd['uid'];
276  $excludeUids[] = $pageIdInDefaultLanguage;
277  }
278  }
279 
280  // This is the regular "non-MountPoint page" case (must happen after the if condition so MountPoint
281  // pages that have been replaced by the Mounted Page will not be added again.
282  if ($isOnSameSite && !in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
283  $pages[] = $row;
284  $excludeUids[] = $pageIdInDefaultLanguage;
285  }
286 
287  // Add possible sub-pages prepended with the MountPoint page slug
288  if ($mountPageInformation) {
290  $siteOfMountedPage = $siteFinder->getSiteByPageId((int)$mountedPage['uid']);
291  $morePageCandidates = $this->‪findPageCandidatesOfMountPoint(
292  $row,
293  $mountedPage,
294  $siteOfMountedPage,
295  $languageId,
296  $slugCandidates
297  );
298  foreach ($morePageCandidates as $candidate) {
299  // When called previously this MountPoint page should be skipped
300  if (in_array((int)$candidate['uid'], $excludeUids, true)) {
301  continue;
302  }
303  $pages[] = $candidate;
304  }
305  }
306  }
307  return $pages;
308  }
309 
329  array $mountPointPage,
330  array $mountedPage,
331  ‪Site $siteOfMountedPage,
332  int $languageId,
333  array $slugCandidates
334  ): array {
335  $pages = [];
336  $slugOfMountPoint = $mountPointPage['slug'] ?? '';
337  $commonSlugPrefixOfMountedPage = rtrim($mountedPage['slug'] ?? '', '/');
338  $narrowedDownSlugPrefixes = [];
339  foreach ($slugCandidates as $slugCandidate) {
340  // Remove the mount point prefix (that we just found) from the slug candidates
341  if (str_starts_with($slugCandidate, $slugOfMountPoint)) {
342  // Find pages without the common prefix
343  $narrowedDownSlugPrefix = '/' . trim(substr($slugCandidate, strlen($slugOfMountPoint)), '/');
344  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
345  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
346  // Find pages with the prefix of the mounted page as well
347  if ($commonSlugPrefixOfMountedPage) {
348  $narrowedDownSlugPrefix = $commonSlugPrefixOfMountedPage . $narrowedDownSlugPrefix;
349  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
350  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
351  }
352  }
353  }
354  $trimmedSlugPrefixes = [];
355  $narrowedDownSlugPrefixes = array_unique($narrowedDownSlugPrefixes);
356  foreach ($narrowedDownSlugPrefixes as $narrowedDownSlugPrefix) {
357  $narrowedDownSlugPrefix = trim($narrowedDownSlugPrefix, '/');
358  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix;
359  if (!empty($narrowedDownSlugPrefix)) {
360  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix . '/';
361  }
362  }
363  $trimmedSlugPrefixes = array_unique($trimmedSlugPrefixes);
364  rsort($trimmedSlugPrefixes);
365 
366  $slugProviderForMountPage = GeneralUtility::makeInstance(static::class, $this->context, $siteOfMountedPage, $this->enhancerFactory);
367  // Find the right pages for which have been matched
368  $excludedPageIds = [(int)$mountPointPage['uid']];
369  $pageCandidates = $slugProviderForMountPage->getPagesFromDatabaseForCandidates(
370  $trimmedSlugPrefixes,
371  $languageId,
372  $excludedPageIds
373  );
374  // Depending on the "mount_pid_ol" parameter, the mountedPage or the mounted page is in the rootline
375  $pageWhichMustBeInRootLine = (int)($mountPointPage['mount_pid_ol'] ? $mountedPage['uid'] : $mountPointPage['uid']);
376  foreach ($pageCandidates as $pageCandidate) {
377  if (!$pageCandidate['mount_pid_ol']) {
378  $pageCandidate['MPvar'] = !empty($pageCandidate['MPvar'])
379  ? $mountPointPage['MPvar'] . ',' . $pageCandidate['MPvar']
380  : $mountPointPage['MPvar'];
381  }
382  // In order to avoid the possibility that any random page like /about-us which is not connected to the mount
383  // point is not possible to be called via /my-mount-point/about-us, let's check the
384  $pageCandidateIsConnectedInMountPoint = false;
385  $rootLine = GeneralUtility::makeInstance(
386  RootlineUtility::class,
387  $pageCandidate['uid'],
388  (string)$pageCandidate['MPvar'],
389  $this->context
390  )->get();
391  foreach ($rootLine as $pageInRootLine) {
392  if ((int)$pageInRootLine['uid'] === $pageWhichMustBeInRootLine) {
393  $pageCandidateIsConnectedInMountPoint = true;
394  break;
395  }
396  }
397  if ($pageCandidateIsConnectedInMountPoint === false) {
398  continue;
399  }
400  // Rewrite the slug of the subpage to match the PageRouter matching again
401  // This is done by first removing the "common" prefix possibly provided by the Mounted Page
402  // But more importantly adding the $slugOfMountPoint of the MountPoint Page
403  $slugOfSubpage = $pageCandidate['slug'];
404  if ($commonSlugPrefixOfMountedPage && str_starts_with($slugOfSubpage, $commonSlugPrefixOfMountedPage)) {
405  $slugOfSubpage = substr($slugOfSubpage, strlen($commonSlugPrefixOfMountedPage));
406  }
407  $pageCandidate['slug'] = $slugOfMountPoint . (($slugOfSubpage && $slugOfSubpage !== '/') ? '/' . trim($slugOfSubpage, '/') : '');
408  $pages[] = $pageCandidate;
409  }
410  return $pages;
411  }
412 
429  protected function ‪getCandidateSlugsFromRoutePath(string $routePath): array
430  {
431  $redecorationPattern = $this->‪getRoutePathRedecorationPattern();
432  if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
433  $decoration = $matches['decoration'];
434  $decorationPattern = preg_quote($decoration, '#');
435  $routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath) ?? '';
436  }
437 
438  $candidatePathParts = [];
439  $pathParts = ‪GeneralUtility::trimExplode('/', $routePath, true);
440  if (empty($pathParts)) {
441  return ['/'];
442  }
443 
444  while (!empty($pathParts)) {
445  $prefix = '/' . implode('/', $pathParts);
446  $candidatePathParts[] = $prefix . '/';
447  $candidatePathParts[] = $prefix;
448  array_pop($pathParts);
449  }
450  $candidatePathParts[] = '/';
451  return $candidatePathParts;
452  }
453 }
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\findPageCandidatesOfMountPoint
‪array findPageCandidatesOfMountPoint(array $mountPointPage, array $mountedPage, Site $siteOfMountedPage, int $languageId, array $slugCandidates)
Definition: PageSlugCandidateProvider.php:328
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:52
‪TYPO3\CMS\Core\Routing\Enhancer\EnhancerFactory
Definition: EnhancerFactory.php:26
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider
Definition: PageSlugCandidateProvider.php:43
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getCandidateSlugsFromRoutePath
‪string[] getCandidateSlugsFromRoutePath(string $routePath)
Definition: PageSlugCandidateProvider.php:429
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getPagesFromDatabaseForCandidates
‪array[] array getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids=[])
Definition: PageSlugCandidateProvider.php:179
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$site
‪Site $site
Definition: PageSlugCandidateProvider.php:44
‪$languages
‪$languages
Definition: updateIsoDatabase.php:104
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:40
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\__construct
‪__construct(Context $context, Site $site, ?EnhancerFactory $enhancerFactory)
Definition: PageSlugCandidateProvider.php:48
‪TYPO3\CMS\Core\Routing
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:54
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
Definition: PageSlugCandidateProvider.php:136
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:27
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getFallbackLanguageIds
‪getFallbackLanguageIds()
Definition: SiteLanguage.php:267
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:102
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getDecoratingEnhancers
‪DecoratingEnhancerInterface[] getDecoratingEnhancers()
Definition: PageSlugCandidateProvider.php:159
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR_ARRAY
‪const PARAM_STR_ARRAY
Definition: Connection.php:77
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getLanguageId
‪getLanguageId()
Definition: SiteLanguage.php:169
‪TYPO3\CMS\Core\Context\LanguageAspect
Definition: LanguageAspect.php:57
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$enhancerFactory
‪EnhancerFactory $enhancerFactory
Definition: PageSlugCandidateProvider.php:46
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:41
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$context
‪Context $context
Definition: PageSlugCandidateProvider.php:45
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪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\Routing\PageSlugCandidateProvider\getRealPageIdForPageIdAsPossibleCandidate
‪getRealPageIdForPageIdAsPossibleCandidate(int $pageId)
Definition: PageSlugCandidateProvider.php:101
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getCandidatesForPath
‪array< int, array< string, mixed > > getCandidatesForPath(string $urlPath, SiteLanguage $language)
Definition: PageSlugCandidateProvider.php:60
‪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\Routing\Enhancer\DecoratingEnhancerInterface
Definition: DecoratingEnhancerInterface.php:26
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:39