‪TYPO3CMS  10.4
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 
20 use Doctrine\DBAL\Connection;
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  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
117  ->getQueryBuilderForTable('pages');
118  $queryBuilder
119  ->getRestrictions()
120  ->removeAll()
121  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
122  ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class));
123 
124  $statement = $queryBuilder
125  ->select('uid', 'l10n_parent')
126  ->from('pages')
127  ->where(
128  $queryBuilder->expr()->eq(
129  'uid',
130  $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
131  )
132  )
133  ->execute();
134 
135  $page = $statement->fetch();
136  if (empty($page)) {
137  return null;
138  }
139  return (int)($page['l10n_parent'] ?: $page['uid']);
140  }
141 
148  protected function ‪getRoutePathRedecorationPattern(): string
149  {
150  $decoratingEnhancers = $this->‪getDecoratingEnhancers();
151  if (empty($decoratingEnhancers)) {
152  return '';
153  }
154  $redecorationPatterns = array_map(
155  function (‪DecoratingEnhancerInterface $decorationEnhancers) {
156  $pattern = $decorationEnhancers->‪getRoutePathRedecorationPattern();
157  return '(?:' . $pattern . ')';
158  },
159  $decoratingEnhancers
160  );
161  return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
162  }
163 
171  protected function ‪getDecoratingEnhancers(): array
172  {
173  $enhancers = [];
174  foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
175  $enhancerType = $enhancerConfiguration['type'] ?? '';
176  $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
177  if ($enhancer instanceof ‪DecoratingEnhancerInterface) {
178  $enhancers[] = $enhancer;
179  }
180  }
181  return $enhancers;
182  }
183 
193  protected function ‪getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids = []): array
194  {
195  $searchLiveRecordsOnly = $this->context->getPropertyFromAspect('workspace', 'isLive');
196  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
197  ->getQueryBuilderForTable('pages');
198  $queryBuilder
199  ->getRestrictions()
200  ->removeAll()
201  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
202  ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class, null, null, $searchLiveRecordsOnly));
203 
204  $statement = $queryBuilder
205  ->select('uid', 'sys_language_uid', 'l10n_parent', 'l18n_cfg', 'pid', 'slug', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'doktype', 't3ver_wsid', 't3ver_oid')
206  ->from('pages')
207  ->where(
208  $queryBuilder->expr()->eq(
209  'sys_language_uid',
210  $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
211  ),
212  $queryBuilder->expr()->in(
213  'slug',
214  $queryBuilder->createNamedParameter(
215  $slugCandidates,
216  Connection::PARAM_STR_ARRAY
217  )
218  )
219  )
220  // Exact match will be first, that's important
221  ->orderBy('slug', 'desc')
222  // Sort pages that are not MountPoint pages before mount points
223  ->addOrderBy('mount_pid_ol', 'asc')
224  ->addOrderBy('mount_pid', 'asc')
225  ->execute();
226 
227  $pages = [];
228  $siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
229  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, $this->context);
230  $isRecursiveCall = !empty($excludeUids);
231 
232  while ($row = $statement->fetch()) {
233  $mountPageInformation = null;
234  // This changes the PID value and adds a _ORIG_PID value (only different in move actions)
235  // In live: This fetches everything in a bad way ! as there is no workspace limitation given, fetching all new and moved placeholders here!
236  // In a workspace: Filter out versioned records (t3ver_oid=0), leaving effectively the new/move placeholders in place, where the new placeholder
237  // However, this is checked in $siteFinder->getSiteByPageId() via RootlineUtility where overlays are happening
238  // so the fixVersioningPid() call is probably irrelevant.
239  $pageRepository->fixVersioningPid('pages', $row);
240  $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
241  // When this page was added before via recursion, this page should be skipped
242  if (in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
243  continue;
244  }
245 
246  try {
247  $isOnSameSite = $siteFinder->getSiteByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId();
248  } catch (‪SiteNotFoundException $e) {
249  // Page is not in a site, so it's not considered
250  $isOnSameSite = false;
251  }
252 
253  // If a MountPoint is found on the current site, and it hasn't been added yet by some other iteration
254  // (see below "findPageCandidatesOfMountPoint"), then let's resolve the MountPoint information now
255  if (!$isOnSameSite && $isRecursiveCall) {
256  // Not in the same site, and called recursive, should be skipped
257  continue;
258  }
259  $mountPageInformation = $pageRepository->getMountPointInfo($pageIdInDefaultLanguage, $row);
260 
261  // Mount Point Pages which are not on the same site (when not called on the first level) should be skipped
262  // As they just clutter up the queries.
263  if (!$isOnSameSite && !$isRecursiveCall && $mountPageInformation) {
264  continue;
265  }
266 
267  $mountedPage = null;
268  if ($mountPageInformation) {
269  // Add the MPvar to the row, so it can be used later-on in the PageRouter / PageArguments
270  $row['MPvar'] = $mountPageInformation['MPvar'];
271  $mountedPage = $pageRepository->getPage_noCheck($mountPageInformation['mount_pid_rec']['uid']);
272  // Ensure to fetch the slug in the translated page
273  $mountedPage = $pageRepository->getPageOverlay($mountedPage, $languageId);
274  // Mount wasn't connected properly, so it is skipped
275  if (!$mountedPage) {
276  continue;
277  }
278  // If the page is a MountPoint which should be overlaid with the contents of the mounted page,
279  // it must never be accessible directly, but only in the MountPoint context. Therefore we change
280  // the current ID and slug.
281  // This needs to happen before the regular case, as the $pageToAdd contains the MPvar information
282  if (‪PageRepository::DOKTYPE_MOUNTPOINT === (int)$row['doktype'] && $row['mount_pid_ol']) {
283  // If the mounted page was already added from above, this should not be added again (to include
284  // the mount point parameter).
285  if (in_array((int)$mountedPage['uid'], $excludeUids, true)) {
286  continue;
287  }
288  $pageToAdd = $mountedPage;
289  // Make sure target page "/about-us" is replaced by "/global-site/about-us" so router works
290  $pageToAdd['MPvar'] = $mountPageInformation['MPvar'];
291  $pageToAdd['slug'] = $row['slug'];
292  $pages[] = $pageToAdd;
293  $excludeUids[] = (int)$pageToAdd['uid'];
294  $excludeUids[] = $pageIdInDefaultLanguage;
295  }
296  }
297 
298  // This is the regular "non-MountPoint page" case (must happen after the if condition so MountPoint
299  // pages that have been replaced by the Mounted Page will not be added again.
300  if ($isOnSameSite && !in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
301  $pages[] = $row;
302  $excludeUids[] = $pageIdInDefaultLanguage;
303  }
304 
305  // Add possible sub-pages prepended with the MountPoint page slug
306  if ($mountPageInformation) {
308  $siteOfMountedPage = $siteFinder->getSiteByPageId((int)$mountedPage['uid']);
309  $morePageCandidates = $this->‪findPageCandidatesOfMountPoint(
310  $row,
311  $mountedPage,
312  $siteOfMountedPage,
313  $languageId,
314  $slugCandidates
315  );
316  foreach ($morePageCandidates as $candidate) {
317  // When called previously this MountPoint page should be skipped
318  if (in_array((int)$candidate['uid'], $excludeUids, true)) {
319  continue;
320  }
321  $pages[] = $candidate;
322  }
323  }
324  }
325  return $pages;
326  }
327 
346  protected function ‪findPageCandidatesOfMountPoint(
347  array $mountPointPage,
348  array $mountedPage,
349  ‪Site $siteOfMountedPage,
350  int $languageId,
351  array $slugCandidates
352  ): array {
353  $pages = [];
354  $slugOfMountPoint = $mountPointPage['slug'] ?? '';
355  $commonSlugPrefixOfMountedPage = rtrim($mountedPage['slug'] ?? '', '/');
356  $narrowedDownSlugPrefixes = [];
357  foreach ($slugCandidates as $slugCandidate) {
358  // Remove the mount point prefix (that we just found) from the slug candidates
359  if (strpos($slugCandidate, $slugOfMountPoint) === 0) {
360  // Find pages without the common prefix
361  $narrowedDownSlugPrefix = '/' . trim(substr($slugCandidate, strlen($slugOfMountPoint)), '/');
362  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
363  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
364  // Find pages with the prefix of the mounted page as well
365  if ($commonSlugPrefixOfMountedPage) {
366  $narrowedDownSlugPrefix = $commonSlugPrefixOfMountedPage . $narrowedDownSlugPrefix;
367  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
368  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
369  }
370  }
371  }
372  $trimmedSlugPrefixes = [];
373  $narrowedDownSlugPrefixes = array_unique($narrowedDownSlugPrefixes);
374  foreach ($narrowedDownSlugPrefixes as $narrowedDownSlugPrefix) {
375  $narrowedDownSlugPrefix = trim($narrowedDownSlugPrefix, '/');
376  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix;
377  if (!empty($narrowedDownSlugPrefix)) {
378  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix . '/';
379  }
380  }
381  $trimmedSlugPrefixes = array_unique($trimmedSlugPrefixes);
382  rsort($trimmedSlugPrefixes);
383 
384  $slugProviderForMountPage = GeneralUtility::makeInstance(static::class, $this->context, $siteOfMountedPage, $this->enhancerFactory);
385  // Find the right pages for which have been matched
386  $excludedPageIds = [(int)$mountPointPage['uid']];
387  $pageCandidates = $slugProviderForMountPage->getPagesFromDatabaseForCandidates(
388  $trimmedSlugPrefixes,
389  $languageId,
390  $excludedPageIds
391  );
392  // Depending on the "mount_pid_ol" parameter, the mountedPage or the mounted page is in the rootline
393  $pageWhichMustBeInRootLine = (int)($mountPointPage['mount_pid_ol'] ? $mountedPage['uid'] : $mountPointPage['uid']);
394  foreach ($pageCandidates as $pageCandidate) {
395  if (!$pageCandidate['mount_pid_ol']) {
396  $pageCandidate['MPvar'] = $mountPointPage['MPvar'] . ($pageCandidate['MPvar'] ? ',' . $pageCandidate['MPvar'] : '');
397  }
398  // In order to avoid the possibility that any random page like /about-us which is not connected to the mount
399  // point is not possible to be called via /my-mount-point/about-us, let's check the
400  $pageCandidateIsConnectedInMountPoint = false;
401  $rootLine = GeneralUtility::makeInstance(
402  RootlineUtility::class,
403  $pageCandidate['uid'],
404  $pageCandidate['MPvar'],
405  $this->context
406  )->get();
407  foreach ($rootLine as $pageInRootLine) {
408  if ((int)$pageInRootLine['uid'] === $pageWhichMustBeInRootLine) {
409  $pageCandidateIsConnectedInMountPoint = true;
410  break;
411  }
412  }
413  if ($pageCandidateIsConnectedInMountPoint === false) {
414  continue;
415  }
416  // Rewrite the slug of the subpage to match the PageRouter matching again
417  // This is done by first removing the "common" prefix possibly provided by the Mounted Page
418  // But more importantly adding the $slugOfMountPoint of the MountPoint Page
419  $slugOfSubpage = $pageCandidate['slug'];
420  if ($commonSlugPrefixOfMountedPage && strpos($slugOfSubpage, $commonSlugPrefixOfMountedPage) === 0) {
421  $slugOfSubpage = substr($slugOfSubpage, strlen($commonSlugPrefixOfMountedPage));
422  }
423  $pageCandidate['slug'] = $slugOfMountPoint . (($slugOfSubpage && $slugOfSubpage !== '/') ? '/' . trim($slugOfSubpage, '/') : '');
424  $pages[] = $pageCandidate;
425  }
426  return $pages;
427  }
428 
445  protected function ‪getCandidateSlugsFromRoutePath(string $routePath): array
446  {
447  $redecorationPattern = $this->‪getRoutePathRedecorationPattern();
448  if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
449  $decoration = $matches['decoration'];
450  $decorationPattern = preg_quote($decoration, '#');
451  $routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath) ?? '';
452  }
453 
454  $candidatePathParts = [];
455  $pathParts = ‪GeneralUtility::trimExplode('/', $routePath, true);
456  if (empty($pathParts)) {
457  return ['/'];
458  }
459 
460  while (!empty($pathParts)) {
461  $prefix = '/' . implode('/', $pathParts);
462  $candidatePathParts[] = $prefix . '/';
463  $candidatePathParts[] = $prefix;
464  array_pop($pathParts);
465  }
466  $candidatePathParts[] = '/';
467  return $candidatePathParts;
468  }
469 }
‪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:343
‪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:442
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getPagesFromDatabaseForCandidates
‪array[] array getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids=[])
Definition: PageSlugCandidateProvider.php:190
‪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:39
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:26
‪TYPO3\CMS\Core\Site\SiteFinder
Definition: SiteFinder.php:31
‪TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction
Definition: FrontendWorkspaceRestriction.php:30
‪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:145
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:40
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:26
‪TYPO3\CMS\Core\Domain\Repository\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:107
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\getDecoratingEnhancers
‪DecoratingEnhancerInterface[] getDecoratingEnhancers()
Definition: PageSlugCandidateProvider.php:168
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
‪TYPO3\CMS\Core\Routing\PageSlugCandidateProvider\$enhancerFactory
‪EnhancerFactory $enhancerFactory
Definition: PageSlugCandidateProvider.php:53
‪TYPO3\CMS\Core\Utility\GeneralUtility\trimExplode
‪static string[] trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
Definition: GeneralUtility.php:1059
‪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:52
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46
‪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