‪TYPO3CMS  11.5
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 
34 
42 {
46  protected ‪$site;
47 
51  protected ‪$context;
52 
56  protected ‪$enhancerFactory;
57 
59  {
60  $this->context = ‪$context;
61  $this->site = ‪$site;
62  $this->enhancerFactory = ‪$enhancerFactory ?? GeneralUtility::makeInstance(EnhancerFactory::class);
63  }
64 
72  public function ‪getCandidatesForPath(string $urlPath, ‪SiteLanguage $language): array
73  {
74  $slugCandidates = $this->‪getCandidateSlugsFromRoutePath($urlPath ?: '/');
75  $pageCandidates = [];
76  $languages = [$language->‪getLanguageId()];
77  if (!empty($language->‪getFallbackLanguageIds())) {
78  $languages = array_merge($languages, $language->‪getFallbackLanguageIds());
79  }
80  // Iterate all defined languages in their configured order to get matching page candidates somewhere in the language fallback chain
81  foreach ($languages as $languageId) {
82  $pageCandidatesFromSlugsAndLanguage = $this->‪getPagesFromDatabaseForCandidates($slugCandidates, $languageId);
83  // Determine whether fetched page candidates qualify for the request. The incoming URL is checked against all
84  // pages found for the current URL and language.
85  foreach ($pageCandidatesFromSlugsAndLanguage as $candidate) {
86  $slugCandidate = '/' . trim($candidate['slug'], '/');
87  if ($slugCandidate === '/' || strpos($urlPath, $slugCandidate) === 0) {
88  // The slug is a subpart of the requested URL, so it's a possible candidate
89  if ($urlPath === $slugCandidate) {
90  // The requested URL matches exactly the found slug. We can't find a better match,
91  // so use that page candidate and stop any further querying.
92  $pageCandidates = [$candidate];
93  break 2;
94  }
95 
96  $pageCandidates[] = $candidate;
97  }
98  }
99  }
100  return $pageCandidates;
101  }
102 
114  public function ‪getRealPageIdForPageIdAsPossibleCandidate(int $pageId): ?int
115  {
116  $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id');
117  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
118  ->getQueryBuilderForTable('pages');
119  $queryBuilder
120  ->getRestrictions()
121  ->removeAll()
122  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
123  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId));
124 
125  $statement = $queryBuilder
126  ->select('uid', 'l10n_parent')
127  ->from('pages')
128  ->where(
129  $queryBuilder->expr()->eq(
130  'uid',
131  $queryBuilder->createNamedParameter($pageId, ‪Connection::PARAM_INT)
132  )
133  )
134  ->executeQuery();
135 
136  $page = $statement->fetchAssociative();
137  if (empty($page)) {
138  return null;
139  }
140  return (int)($page['l10n_parent'] ?: $page['uid']);
141  }
142 
149  protected function ‪getRoutePathRedecorationPattern(): string
150  {
151  $decoratingEnhancers = $this->‪getDecoratingEnhancers();
152  if (empty($decoratingEnhancers)) {
153  return '';
154  }
155  $redecorationPatterns = array_map(
156  static function (‪DecoratingEnhancerInterface $decorationEnhancers) {
157  $pattern = $decorationEnhancers->‪getRoutePathRedecorationPattern();
158  return '(?:' . $pattern . ')';
159  },
160  $decoratingEnhancers
161  );
162  return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
163  }
164 
172  protected function ‪getDecoratingEnhancers(): array
173  {
174  $enhancers = [];
175  foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
176  $enhancerType = $enhancerConfiguration['type'] ?? '';
177  $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
178  if ($enhancer instanceof ‪DecoratingEnhancerInterface) {
179  $enhancers[] = $enhancer;
180  }
181  }
182  return $enhancers;
183  }
184 
194  protected function ‪getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids = []): array
195  {
196  $workspaceId = (int)$this->context->getPropertyFromAspect('workspace', 'id');
197  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
198  ->getQueryBuilderForTable('pages');
199  $queryBuilder
200  ->getRestrictions()
201  ->removeAll()
202  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
203  ->add(GeneralUtility::makeInstance(WorkspaceRestriction::class, $workspaceId, true));
204 
205  $statement = $queryBuilder
206  ->select('uid', 'sys_language_uid', 'l10n_parent', 'l18n_cfg', 'pid', 'slug', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'doktype', 't3ver_wsid', 't3ver_oid')
207  ->from('pages')
208  ->where(
209  $queryBuilder->expr()->eq(
210  'sys_language_uid',
211  $queryBuilder->createNamedParameter($languageId, ‪Connection::PARAM_INT)
212  ),
213  $queryBuilder->expr()->in(
214  'slug',
215  $queryBuilder->createNamedParameter(
216  $slugCandidates,
217  Connection::PARAM_STR_ARRAY
218  )
219  )
220  )
221  // Exact match will be first, that's important
222  ->orderBy('slug', 'desc')
223  // versioned records should be rendered before the live records
224  ->addOrderBy('t3ver_wsid', 'desc')
225  // Sort pages that are not MountPoint pages before mount points
226  ->addOrderBy('mount_pid_ol', 'asc')
227  ->addOrderBy('mount_pid', 'asc')
228  ->executeQuery();
229 
230  $pages = [];
231  $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
232  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $this->context);
233  $isRecursiveCall = !empty($excludeUids);
234 
235  while ($row = $statement->fetchAssociative()) {
236  $mountPageInformation = null;
237  $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : ($row['t3ver_oid'] ?: $row['uid']));
238  // When this page was added before via recursion, this page should be skipped
239  if (in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
240  continue;
241  }
242 
243  try {
244  $isOnSameSite = $siteFinder->getSiteByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId();
245  } catch (‪SiteNotFoundException $e) {
246  // Page is not in a site, so it's not considered
247  $isOnSameSite = false;
248  }
249 
250  // If a MountPoint is found on the current site, and it hasn't been added yet by some other iteration
251  // (see below "findPageCandidatesOfMountPoint"), then let's resolve the MountPoint information now
252  if (!$isOnSameSite && $isRecursiveCall) {
253  // Not in the same site, and called recursive, should be skipped
254  continue;
255  }
256  $mountPageInformation = $pageRepository->getMountPointInfo($pageIdInDefaultLanguage, $row);
257 
258  // Mount Point Pages which are not on the same site (when not called on the first level) should be skipped
259  // As they just clutter up the queries.
260  if (!$isOnSameSite && !$isRecursiveCall && $mountPageInformation) {
261  continue;
262  }
263 
264  $mountedPage = null;
265  if ($mountPageInformation) {
266  // Add the MPvar to the row, so it can be used later-on in the PageRouter / PageArguments
267  $row['MPvar'] = $mountPageInformation['MPvar'];
268  $mountedPage = $pageRepository->getPage_noCheck($mountPageInformation['mount_pid_rec']['uid']);
269  // Ensure to fetch the slug in the translated page
270  $mountedPage = $pageRepository->getPageOverlay($mountedPage, $languageId);
271  // Mount wasn't connected properly, so it is skipped
272  if (!$mountedPage) {
273  continue;
274  }
275  // If the page is a MountPoint which should be overlaid with the contents of the mounted page,
276  // it must never be accessible directly, but only in the MountPoint context. Therefore we change
277  // the current ID and slug.
278  // This needs to happen before the regular case, as the $pageToAdd contains the MPvar information
279  if ((int)$row['doktype'] === ‪PageRepository::DOKTYPE_MOUNTPOINT && $row['mount_pid_ol']) {
280  // If the mounted page was already added from above, this should not be added again (to include
281  // the mount point parameter).
282  if (in_array((int)$mountedPage['uid'], $excludeUids, true)) {
283  continue;
284  }
285  $pageToAdd = $mountedPage;
286  // Make sure target page "/about-us" is replaced by "/global-site/about-us" so router works
287  $pageToAdd['MPvar'] = $mountPageInformation['MPvar'];
288  $pageToAdd['slug'] = $row['slug'];
289  $pages[] = $pageToAdd;
290  $excludeUids[] = (int)$pageToAdd['uid'];
291  $excludeUids[] = $pageIdInDefaultLanguage;
292  }
293  }
294 
295  // This is the regular "non-MountPoint page" case (must happen after the if condition so MountPoint
296  // pages that have been replaced by the Mounted Page will not be added again.
297  if ($isOnSameSite && !in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
298  $pages[] = $row;
299  $excludeUids[] = $pageIdInDefaultLanguage;
300  }
301 
302  // Add possible sub-pages prepended with the MountPoint page slug
303  if ($mountPageInformation) {
305  $siteOfMountedPage = $siteFinder->getSiteByPageId((int)$mountedPage['uid']);
306  $morePageCandidates = $this->‪findPageCandidatesOfMountPoint(
307  $row,
308  $mountedPage,
309  $siteOfMountedPage,
310  $languageId,
311  $slugCandidates
312  );
313  foreach ($morePageCandidates as $candidate) {
314  // When called previously this MountPoint page should be skipped
315  if (in_array((int)$candidate['uid'], $excludeUids, true)) {
316  continue;
317  }
318  $pages[] = $candidate;
319  }
320  }
321  }
322  return $pages;
323  }
324 
343  protected function ‪findPageCandidatesOfMountPoint(
344  array $mountPointPage,
345  array $mountedPage,
346  ‪Site $siteOfMountedPage,
347  int $languageId,
348  array $slugCandidates
349  ): array {
350  $pages = [];
351  $slugOfMountPoint = $mountPointPage['slug'] ?? '';
352  $commonSlugPrefixOfMountedPage = rtrim($mountedPage['slug'] ?? '', '/');
353  $narrowedDownSlugPrefixes = [];
354  foreach ($slugCandidates as $slugCandidate) {
355  // Remove the mount point prefix (that we just found) from the slug candidates
356  if (strpos($slugCandidate, $slugOfMountPoint) === 0) {
357  // Find pages without the common prefix
358  $narrowedDownSlugPrefix = '/' . trim(substr($slugCandidate, strlen($slugOfMountPoint)), '/');
359  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
360  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
361  // Find pages with the prefix of the mounted page as well
362  if ($commonSlugPrefixOfMountedPage) {
363  $narrowedDownSlugPrefix = $commonSlugPrefixOfMountedPage . $narrowedDownSlugPrefix;
364  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
365  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
366  }
367  }
368  }
369  $trimmedSlugPrefixes = [];
370  $narrowedDownSlugPrefixes = array_unique($narrowedDownSlugPrefixes);
371  foreach ($narrowedDownSlugPrefixes as $narrowedDownSlugPrefix) {
372  $narrowedDownSlugPrefix = trim($narrowedDownSlugPrefix, '/');
373  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix;
374  if (!empty($narrowedDownSlugPrefix)) {
375  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix . '/';
376  }
377  }
378  $trimmedSlugPrefixes = array_unique($trimmedSlugPrefixes);
379  rsort($trimmedSlugPrefixes);
380 
381  $slugProviderForMountPage = GeneralUtility::makeInstance(static::class, $this->context, $siteOfMountedPage, $this->enhancerFactory);
382  // Find the right pages for which have been matched
383  $excludedPageIds = [(int)$mountPointPage['uid']];
384  $pageCandidates = $slugProviderForMountPage->getPagesFromDatabaseForCandidates(
385  $trimmedSlugPrefixes,
386  $languageId,
387  $excludedPageIds
388  );
389  // Depending on the "mount_pid_ol" parameter, the mountedPage or the mounted page is in the rootline
390  $pageWhichMustBeInRootLine = (int)($mountPointPage['mount_pid_ol'] ? $mountedPage['uid'] : $mountPointPage['uid']);
391  foreach ($pageCandidates as $pageCandidate) {
392  if (!$pageCandidate['mount_pid_ol']) {
393  $pageCandidate['MPvar'] = !empty($pageCandidate['MPvar'])
394  ? $mountPointPage['MPvar'] . ',' . $pageCandidate['MPvar']
395  : $mountPointPage['MPvar'];
396  }
397  // In order to avoid the possibility that any random page like /about-us which is not connected to the mount
398  // point is not possible to be called via /my-mount-point/about-us, let's check the
399  $pageCandidateIsConnectedInMountPoint = false;
400  $rootLine = GeneralUtility::makeInstance(
401  RootlineUtility::class,
402  $pageCandidate['uid'],
403  (string)$pageCandidate['MPvar'],
404  $this->context
405  )->get();
406  foreach ($rootLine as $pageInRootLine) {
407  if ((int)$pageInRootLine['uid'] === $pageWhichMustBeInRootLine) {
408  $pageCandidateIsConnectedInMountPoint = true;
409  break;
410  }
411  }
412  if ($pageCandidateIsConnectedInMountPoint === false) {
413  continue;
414  }
415  // Rewrite the slug of the subpage to match the PageRouter matching again
416  // This is done by first removing the "common" prefix possibly provided by the Mounted Page
417  // But more importantly adding the $slugOfMountPoint of the MountPoint Page
418  $slugOfSubpage = $pageCandidate['slug'];
419  if ($commonSlugPrefixOfMountedPage && strpos($slugOfSubpage, $commonSlugPrefixOfMountedPage) === 0) {
420  $slugOfSubpage = substr($slugOfSubpage, strlen($commonSlugPrefixOfMountedPage));
421  }
422  $pageCandidate['slug'] = $slugOfMountPoint . (($slugOfSubpage && $slugOfSubpage !== '/') ? '/' . trim($slugOfSubpage, '/') : '');
423  $pages[] = $pageCandidate;
424  }
425  return $pages;
426  }
427 
444  protected function ‪getCandidateSlugsFromRoutePath(string $routePath): array
445  {
446  $redecorationPattern = $this->‪getRoutePathRedecorationPattern();
447  if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
448  $decoration = $matches['decoration'];
449  $decorationPattern = preg_quote($decoration, '#');
450  $routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath) ?? '';
451  }
452 
453  $candidatePathParts = [];
454  $pathParts = ‪GeneralUtility::trimExplode('/', $routePath, true);
455  if (empty($pathParts)) {
456  return ['/'];
457  }
458 
459  while (!empty($pathParts)) {
460  $prefix = '/' . implode('/', $pathParts);
461  $candidatePathParts[] = $prefix . '/';
462  $candidatePathParts[] = $prefix;
463  array_pop($pathParts);
464  }
465  $candidatePathParts[] = '/';
466  return $candidatePathParts;
467  }
468 }
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static list< string > trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:999
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getLanguageId
‪int getLanguageId()
Definition: SiteLanguage.php:198
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\findPageCandidatesOfMountPoint
‪array findPageCandidatesOfMountPoint(array $mountPointPage, array $mountedPage, Site $siteOfMountedPage, int $languageId, array $slugCandidates)
Definition: PageSlugCandidateProvider.php:340
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Core\Routing\Enhancer\EnhancerFactory
Definition: EnhancerFactory.php:26
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider
Definition: PageSlugCandidateProvider.php:42
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getCandidateSlugsFromRoutePath
‪string[] getCandidateSlugsFromRoutePath(string $routePath)
Definition: PageSlugCandidateProvider.php:441
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getPagesFromDatabaseForCandidates
‪array[] array getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids=[])
Definition: PageSlugCandidateProvider.php:191
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$site
‪Site $site
Definition: PageSlugCandidateProvider.php:45
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage\getFallbackLanguageIds
‪array getFallbackLanguageIds()
Definition: SiteLanguage.php:320
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:38
‪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:55
‪TYPO3\CMS\Core\Routing
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:53
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
Definition: PageSlugCandidateProvider.php:146
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:42
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:26
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:114
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getDecoratingEnhancers
‪DecoratingEnhancerInterface[] getDecoratingEnhancers()
Definition: PageSlugCandidateProvider.php:169
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$enhancerFactory
‪EnhancerFactory $enhancerFactory
Definition: PageSlugCandidateProvider.php:53
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$context
‪Context $context
Definition: PageSlugCandidateProvider.php:49
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:28
‪TYPO3\CMS\Core\Domain\Repository\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getRealPageIdForPageIdAsPossibleCandidate
‪int null getRealPageIdForPageIdAsPossibleCandidate(int $pageId)
Definition: PageSlugCandidateProvider.php:111
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getCandidatesForPath
‪array< int, array< string, mixed > > getCandidatesForPath(string $urlPath, SiteLanguage $language)
Definition: PageSlugCandidateProvider.php:69
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface
Definition: DecoratingEnhancerInterface.php:26
‪TYPO3\CMS\Core\Database\Query\Restriction\WorkspaceRestriction
Definition: WorkspaceRestriction.php:40