‪TYPO3CMS  9.5
PageRouter.php
Go to the documentation of this file.
1 <?php
2 declare(strict_types = 1);
3 
5 
6 /*
7  * This file is part of the TYPO3 CMS project.
8  *
9  * It is free software; you can redistribute it and/or modify it under
10  * the terms of the GNU General Public License, either version 2
11  * of the License, or any later version.
12  *
13  * For the full copyright and license information, please read the
14  * LICENSE.txt file that was distributed with this source code.
15  *
16  * The TYPO3 project - inspiring people to share!
17  */
18 
19 use Doctrine\DBAL\Connection;
20 use Psr\Http\Message\ServerRequestInterface;
21 use Psr\Http\Message\UriInterface;
22 use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
23 use Symfony\Component\Routing\Exception\ResourceNotFoundException;
24 use Symfony\Component\Routing\RequestContext;
48 
75 {
79  protected ‪$site;
80 
84  protected ‪$enhancerFactory;
85 
89  protected ‪$aspectFactory;
90 
95 
99  protected ‪$context;
100 
107  public function ‪__construct(‪Site ‪$site, ‪Context ‪$context = null)
108  {
109  $this->site = ‪$site;
110  $this->context = ‪$context ?? GeneralUtility::makeInstance(Context::class);
111  $this->enhancerFactory = GeneralUtility::makeInstance(EnhancerFactory::class);
112  $this->aspectFactory = GeneralUtility::makeInstance(AspectFactory::class, $this->context);
113  $this->cacheHashCalculator = GeneralUtility::makeInstance(CacheHashCalculator::class);
114  $this->context = ‪$context ?? GeneralUtility::makeInstance(Context::class);
115  }
116 
125  public function ‪matchRequest(ServerRequestInterface $request, ‪RouteResultInterface $previousResult = null): ‪RouteResultInterface
126  {
127  if (!($previousResult instanceof ‪RouteResultInterface)) {
128  throw new ‪RouteNotFoundException('No previous result given. Cannot find a page for an empty route part', 1555303496);
129  }
130  $urlPath = $previousResult->getTail();
131  // Remove the script name (e.g. index.php), if given
132  if (!empty($urlPath)) {
133  $normalizedParams = $request->getAttribute('normalizedParams');
134  if ($normalizedParams instanceof NormalizedParams) {
135  $scriptName = ltrim($normalizedParams->getScriptName(), '/');
136  if ($scriptName !== '' && strpos($urlPath, $scriptName) !== false) {
137  $urlPath = str_replace($scriptName, '', $urlPath);
138  }
139  }
140  }
141 
142  $prefixedUrlPath = '/' . trim($urlPath, '/');
143  $slugCandidates = $this->‪getCandidateSlugsFromRoutePath($urlPath ?: '/');
144  $pageCandidates = [];
145  $language = $previousResult->getLanguage();
146  $languages = [$language->getLanguageId()];
147  if (!empty($language->getFallbackLanguageIds())) {
148  $languages = array_merge($languages, $language->getFallbackLanguageIds());
149  }
150  // Iterate all defined languages in their configured order to get matching page candidates somewhere in the language fallback chain
151  foreach ($languages as $languageId) {
152  $pageCandidatesFromSlugsAndLanguage = $this->‪getPagesFromDatabaseForCandidates($slugCandidates, $languageId);
153  // Determine whether fetched page candidates qualify for the request. The incoming URL is checked against all
154  // pages found for the current URL and language.
155  foreach ($pageCandidatesFromSlugsAndLanguage as $candidate) {
156  $slugCandidate = '/' . trim($candidate['slug'], '/');
157  if ($slugCandidate === '/' || strpos($prefixedUrlPath, $slugCandidate) === 0) {
158  // The slug is a subpart of the requested URL, so it's a possible candidate
159  if ($prefixedUrlPath === $slugCandidate) {
160  // The requested URL matches exactly the found slug. We can't find a better match,
161  // so use that page candidate and stop any further querying.
162  $pageCandidates = [$candidate];
163  break 2;
164  }
165 
166  $pageCandidates[] = $candidate;
167  }
168  }
169  }
170 
171  // Stop if there are no candidates
172  if (empty($pageCandidates)) {
173  throw new RouteNotFoundException('No page candidates found for path "' . $urlPath . '"', 1538389999);
174  }
175 
176  $fullCollection = new RouteCollection();
177  foreach ($pageCandidates ?? [] as $page) {
178  $pageIdForDefaultLanguage = (int)($page['l10n_parent'] ?: $page['uid']);
179  $pagePath = $page['slug'];
180  $pageCollection = new RouteCollection();
181  $defaultRouteForPage = new Route(
182  $pagePath,
183  [],
184  [],
185  ['utf8' => true, '_page' => $page]
186  );
187  $pageCollection->add('default', $defaultRouteForPage);
188  $enhancers = $this->‪getEnhancersForPage($pageIdForDefaultLanguage, $language);
189  foreach ($enhancers as $enhancer) {
190  if ($enhancer instanceof DecoratingEnhancerInterface) {
191  $enhancer->decorateForMatching($pageCollection, $urlPath);
192  }
193  }
194  foreach ($enhancers as $enhancer) {
195  if ($enhancer instanceof RoutingEnhancerInterface) {
196  $enhancer->enhanceForMatching($pageCollection);
197  }
198  }
199 
200  $collectionPrefix = 'page_' . $page['uid'];
201  // Pages with a MountPoint Parameter means that they have a different context, and should be treated
202  // as a separate instance
203  if (isset($page['MPvar'])) {
204  $collectionPrefix .= '_MP_' . str_replace(',', '', $page['MPvar']);
205  }
206  $pageCollection->addNamePrefix($collectionPrefix . '_');
207  $fullCollection->addCollection($pageCollection);
208  // set default route flag after all routes have been processed
209  $defaultRouteForPage->setOption('_isDefault', true);
210  }
211 
212  $matcher = new PageUriMatcher($fullCollection);
213  try {
214  $result = $matcher->match($prefixedUrlPath);
216  $matchedRoute = $fullCollection->get($result['_route']);
217  return $this->‪buildPageArguments($matchedRoute, $result, $request->getQueryParams());
218  } catch (ResourceNotFoundException $e) {
219  // Do nothing
220  }
221  throw new RouteNotFoundException('No route found for path "' . $urlPath . '"', 1538389998);
222  }
223 
234  public function ‪generateUri($route, array $parameters = [], string $fragment = '', string $type = ''): UriInterface
235  {
236  // Resolve language
237  $language = null;
238  $languageOption = $parameters['_language'] ?? null;
239  unset($parameters['_language']);
240  if ($languageOption instanceof SiteLanguage) {
241  $language = $languageOption;
242  } elseif ($languageOption !== null) {
243  $language = $this->site->getLanguageById((int)$languageOption);
244  }
245  if ($language === null) {
246  $language = $this->site->getDefaultLanguage();
247  }
248 
249  $pageId = 0;
250  if (is_array($route)) {
251  $pageId = (int)$route['uid'];
252  } elseif (is_scalar($route)) {
253  $pageId = (int)$route;
254  }
255 
258  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, ‪$context);
259  $page = $pageRepository->getPage($pageId, true);
260  $pagePath = $page['slug'] ?? '';
261 
262  if ($parameters['MP'] ?? false) {
263  $mountPointPairs = explode(',', $parameters['MP']);
265  $pageId,
266  $pagePath,
267  $mountPointPairs,
268  $pageRepository
269  );
270 
271  // If the MountPoint page has a different site, the link needs to be generated
272  // with the base of the MountPoint page, this is especially relevant for cross-domain linking
273  // Because the language contains the full base, it is retrieved in this case.
274  try {
275  [, $mountPointPage] = explode('-', reset($mountPointPairs));
276  ‪$site = GeneralUtility::makeInstance(SiteMatcher::class)
277  ->matchByPageId((int)$mountPointPage);
278  $language = ‪$site->‪getLanguageById($language->getLanguageId());
279  } catch (SiteNotFoundException $e) {
280  // No alternative site found, use the existing one
281  }
282  // Store the MP parameter in the page record, so it could be used for any enhancers
283  $page['MPvar'] = $parameters['MP'];
284  unset($parameters['MP']);
285  }
286 
287  $originalParameters = $parameters;
288  $collection = new RouteCollection();
289  $defaultRouteForPage = new Route(
290  '/' . ltrim($pagePath, '/'),
291  [],
292  [],
293  ['utf8' => true, '_page' => $page]
294  );
295  $collection->add('default', $defaultRouteForPage);
296 
297  // cHash is never considered because cHash is built by this very method.
298  unset($originalParameters['cHash']);
299  $enhancers = $this->‪getEnhancersForPage($pageId, $language);
300  foreach ($enhancers as $enhancer) {
301  if ($enhancer instanceof RoutingEnhancerInterface) {
302  $enhancer->enhanceForGeneration($collection, $originalParameters);
303  }
304  }
305  foreach ($enhancers as $enhancer) {
306  if ($enhancer instanceof DecoratingEnhancerInterface) {
307  $enhancer->decorateForGeneration($collection, $originalParameters);
308  }
309  }
310 
311  $scheme = $language->getBase()->getScheme();
312  $mappableProcessor = new MappableProcessor();
313  ‪$context = new RequestContext(
314  // page segment (slug & enhanced part) is supposed to start with '/'
315  rtrim($language->getBase()->getPath(), '/'),
316  'GET',
317  $language->getBase()->getHost(),
318  $scheme ?: 'http',
319  $scheme === 'http' ? $language->getBase()->getPort() ?? 80 : 80,
320  $scheme === 'https' ? $language->getBase()->getPort() ?? 443 : 443
321  );
322  $generator = new UrlGenerator($collection, ‪$context);
323  $generator->injectMappableProcessor($mappableProcessor);
324  // set default route flag after all routes have been processed
325  $defaultRouteForPage->setOption('_isDefault', true);
326  $allRoutes = GeneralUtility::makeInstance(RouteSorter::class)
327  ->withRoutes($collection->all())
328  ->withOriginalParameters($originalParameters)
329  ->sortRoutesForGeneration()
330  ->getRoutes();
331  $matchedRoute = null;
332  $pageRouteResult = null;
333  $uri = null;
334  // map our reference type to symfony's custom paths
335  $referenceType = $type === static::ABSOLUTE_PATH ? UrlGenerator::ABSOLUTE_PATH : UrlGenerator::ABSOLUTE_URL;
340  foreach ($allRoutes as $routeName => $route) {
341  try {
342  $parameters = $originalParameters;
343  if ($route->hasOption('deflatedParameters')) {
344  $parameters = $route->getOption('deflatedParameters');
345  }
346  $mappableProcessor->generate($route, $parameters);
347  // ABSOLUTE_URL is used as default fallback
348  $urlAsString = $generator->generate($routeName, $parameters, $referenceType);
349  $uri = new Uri($urlAsString);
351  $matchedRoute = $collection->get($routeName);
352  // fetch potential applied defaults for later cHash generation
353  // (even if not applied in route, it will be exposed during resolving)
354  $appliedDefaults = $matchedRoute->getOption('_appliedDefaults') ?? [];
355  parse_str($uri->getQuery() ?? '', $remainingQueryParameters);
356  $enhancer = $route->getEnhancer();
357  if ($enhancer instanceof InflatableEnhancerInterface) {
358  $remainingQueryParameters = $enhancer->inflateParameters($remainingQueryParameters);
359  }
360  $pageRouteResult = $this->‪buildPageArguments($route, array_merge($appliedDefaults, $parameters), $remainingQueryParameters);
361  break;
362  } catch (MissingMandatoryParametersException $e) {
363  // no match
364  }
365  }
366 
367  if (!$uri instanceof UriInterface) {
368  throw new InvalidRouteArgumentsException('Uri could not be built for page "' . $pageId . '"', 1538390230);
369  }
370 
371  if ($pageRouteResult && $pageRouteResult->areDirty()) {
372  // for generating URLs this should(!) never happen
373  // if it does happen, generator logic has flaws
374  throw new InvalidRouteArgumentsException('Route arguments are dirty', 1537613247);
375  }
376 
377  if ($matchedRoute && $pageRouteResult && !empty($pageRouteResult->getDynamicArguments())) {
378  $cacheHash = $this->‪generateCacheHash($pageId, $pageRouteResult);
379 
380  $queryArguments = $pageRouteResult->getQueryArguments();
381  if (!empty($cacheHash)) {
382  $queryArguments['cHash'] = $cacheHash;
383  }
384  $uri = $uri->withQuery(http_build_query($queryArguments, '', '&', PHP_QUERY_RFC3986));
385  }
386  if ($fragment) {
387  $uri = $uri->withFragment($fragment);
388  }
389  return $uri;
390  }
391 
401  protected function ‪getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids = []): array
402  {
403  ‪$context = GeneralUtility::makeInstance(Context::class);
404  $searchLiveRecordsOnly = ‪$context->‪getPropertyFromAspect('workspace', 'isLive');
405  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
406  ->getQueryBuilderForTable('pages');
407  $queryBuilder
408  ->getRestrictions()
409  ->removeAll()
410  ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
411  ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class, null, null, $searchLiveRecordsOnly));
412 
413  $statement = $queryBuilder
414  ->select('uid', 'l10n_parent', 'pid', 'slug', 'mount_pid', 'mount_pid_ol', 't3ver_state', 'doktype', 't3ver_wsid', 't3ver_oid')
415  ->from('pages')
416  ->where(
417  $queryBuilder->expr()->eq(
418  'sys_language_uid',
419  $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
420  ),
421  $queryBuilder->expr()->in(
422  'slug',
423  $queryBuilder->createNamedParameter(
424  $slugCandidates,
425  Connection::PARAM_STR_ARRAY
426  )
427  )
428  )
429  // Exact match will be first, that's important
430  ->orderBy('slug', 'desc')
431  // Sort pages that are not MountPoint pages before mount points
432  ->addOrderBy('mount_pid_ol', 'asc')
433  ->addOrderBy('mount_pid', 'asc')
434  ->execute();
435  $isRecursiveCall = !empty($excludeUids);
436 
437  $pages = [];
438  $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
439  $pageRepository = GeneralUtility::makeInstance(PageRepository::class, ‪$context);
440 
441  while ($row = $statement->fetch()) {
442  $mountPageInformation = null;
443  $pageRepository->fixVersioningPid('pages', $row);
444  $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
445  // When this page was added before via recursion, this page should be skipped
446  if (in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
447  continue;
448  }
449  try {
450  $isOnSameSite = $siteMatcher->matchByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId();
451  } catch (SiteNotFoundException $e) {
452  // Page is not in a site, so it's not considered
453  $isOnSameSite = false;
454  }
455 
456  // If a MountPoint is found on the current site, and it hasn't been added yet by some other iteration
457  // (see below "findPageCandidatesOfMountPoint"), then let's resolve the MountPoint information now
458  if (!$isOnSameSite && $isRecursiveCall) {
459  // Not in the same site, and called recursive, should be skipped
460  continue;
461  }
462  $mountPageInformation = $pageRepository->getMountPointInfo($pageIdInDefaultLanguage, $row);
463  // Mount Point Pages which are not on the same site (when not called on the first level) should be skipped
464  // As they just clutter up the queries.
465  if (!$isOnSameSite && !$isRecursiveCall && $mountPageInformation) {
466  continue;
467  }
468  $mountedPage = null;
469  if ($mountPageInformation) {
470  // Add the MPvar to the row, so it can be used later-on in the PageRouter / PageArguments
471  $row['MPvar'] = $mountPageInformation['MPvar'];
472  $mountedPage = $pageRepository->getPage_noCheck($mountPageInformation['mount_pid_rec']['uid']);
473  // Ensure to fetch the slug in the translated page
474  $mountedPage = $pageRepository->getPageOverlay($mountedPage, $languageId);
475  // Mount wasn't connected properly, so it is skipped
476  if (!$mountedPage) {
477  continue;
478  }
479  // If the page is a MountPoint which should be overlaid with the contents of the mounted page,
480  // it must never be accessible directly, but only in the MountPoint context. Therefore we change
481  // the current ID and slug.
482  // This needs to happen before the regular case, as the $pageToAdd contains the MPvar information
483  if (‪PageRepository::DOKTYPE_MOUNTPOINT === (int)$row['doktype'] && $row['mount_pid_ol']) {
484  // If the mounted page was already added from above, this should not be added again (to include
485  // the mount point parameter).
486  if (in_array((int)$mountedPage['uid'], $excludeUids, true)) {
487  continue;
488  }
489  $pageToAdd = $mountedPage;
490  // Make sure target page "/about-us" is replaced by "/global-site/about-us" so router works
491  $pageToAdd['MPvar'] = $mountPageInformation['MPvar'];
492  $pageToAdd['slug'] = $row['slug'];
493  $pages[] = $pageToAdd;
494  $excludeUids[] = (int)$pageToAdd['uid'];
495  $excludeUids[] = $pageIdInDefaultLanguage;
496  }
497  }
498 
499  // This is the regular "non-MountPoint page" case (must happen after the if condition so MountPoint
500  // pages that have been replaced by the Mounted Page will not be added again.
501  if ($isOnSameSite && !in_array($pageIdInDefaultLanguage, $excludeUids, true)) {
502  $pages[] = $row;
503  $excludeUids[] = $pageIdInDefaultLanguage;
504  }
505 
506  // Add possible sub-pages prepended with the MountPoint page slug
507  if ($mountPageInformation) {
508  $siteOfMountedPage = $siteMatcher->matchByPageId((int)$mountedPage['uid']);
509  if ($siteOfMountedPage instanceof Site) {
510  $morePageCandidates = $this->‪findPageCandidatesOfMountPoint(
511  $row,
512  $mountedPage,
513  $siteOfMountedPage,
514  $languageId,
515  $slugCandidates,
517  );
518  foreach ($morePageCandidates as $candidate) {
519  // When called previously this MountPoint page should be skipped
520  if (in_array((int)$candidate['uid'], $excludeUids, true)) {
521  continue;
522  }
523  $pages[] = $candidate;
524  }
525  }
526  }
527  }
528  return $pages;
529  }
530 
550  protected function ‪findPageCandidatesOfMountPoint(
551  array $mountPointPage,
552  array $mountedPage,
553  Site $siteOfMountedPage,
554  int $languageId,
555  array $slugCandidates,
556  Context ‪$context
557  ): array {
558  $pages = [];
559  $slugOfMountPoint = $mountPointPage['slug'] ?? '';
560  $commonSlugPrefixOfMountedPage = rtrim($mountedPage['slug'] ?? '', '/');
561  $narrowedDownSlugPrefixes = [];
562  foreach ($slugCandidates as $slugCandidate) {
563  // Remove the mount point prefix (that we just found) from the slug candidates
564  if (strpos($slugCandidate, $slugOfMountPoint) === 0) {
565  // Find pages without the common prefix
566  $narrowedDownSlugPrefix = '/' . trim(substr($slugCandidate, strlen($slugOfMountPoint)), '/');
567  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
568  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
569  // Find pages with the prefix of the mounted page as well
570  if ($commonSlugPrefixOfMountedPage) {
571  $narrowedDownSlugPrefix = $commonSlugPrefixOfMountedPage . $narrowedDownSlugPrefix;
572  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix;
573  $narrowedDownSlugPrefixes[] = $narrowedDownSlugPrefix . '/';
574  }
575  }
576  }
577  $trimmedSlugPrefixes = [];
578  $narrowedDownSlugPrefixes = array_unique($narrowedDownSlugPrefixes);
579  foreach ($narrowedDownSlugPrefixes as $narrowedDownSlugPrefix) {
580  $narrowedDownSlugPrefix = trim($narrowedDownSlugPrefix, '/');
581  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix;
582  if (!empty($narrowedDownSlugPrefix)) {
583  $trimmedSlugPrefixes[] = '/' . $narrowedDownSlugPrefix . '/';
584  }
585  }
586  $trimmedSlugPrefixes = array_unique($trimmedSlugPrefixes);
587  rsort($trimmedSlugPrefixes);
588  $routerForSite = GeneralUtility::makeInstance(static::class, $siteOfMountedPage);
589  // Find the right pages for which have been matched
590  $excludedPageIds = [(int)$mountPointPage['uid']];
591  $pageCandidates = $routerForSite->getPagesFromDatabaseForCandidates(
592  $trimmedSlugPrefixes,
593  $languageId,
594  $excludedPageIds
595  );
596  // Depending on the "mount_pid_ol" parameter, the mountedPage or the mounted page is in the rootline
597  $pageWhichMustBeInRootLine = (int)($mountPointPage['mount_pid_ol'] ? $mountedPage['uid'] : $mountPointPage['uid']);
598  foreach ($pageCandidates as $pageCandidate) {
599  if (!$pageCandidate['mount_pid_ol']) {
600  $pageCandidate['MPvar'] = $mountPointPage['MPvar'] . ($pageCandidate['MPvar'] ? ',' . $pageCandidate['MPvar'] : '');
601  }
602  // In order to avoid the possibility that any random page like /about-us which is not connected to the mount
603  // point is not possible to be called via /my-mount-point/about-us, let's check the
604  $pageCandidateIsConnectedInMountPoint = false;
605  $rootLine = GeneralUtility::makeInstance(
606  RootlineUtility::class,
607  $pageCandidate['uid'],
608  $pageCandidate['MPvar'],
610  )->get();
611  foreach ($rootLine as $pageInRootLine) {
612  if ((int)$pageInRootLine['uid'] === $pageWhichMustBeInRootLine) {
613  $pageCandidateIsConnectedInMountPoint = true;
614  break;
615  }
616  }
617  if ($pageCandidateIsConnectedInMountPoint === false) {
618  continue;
619  }
620  // Rewrite the slug of the subpage to match the PageRouter matching again
621  // This is done by first removing the "common" prefix possibly provided by the Mounted Page
622  // But more importantly adding the $slugOfMountPoint of the MountPoint Page
623  $slugOfSubpage = $pageCandidate['slug'];
624  if ($commonSlugPrefixOfMountedPage && strpos($slugOfSubpage, $commonSlugPrefixOfMountedPage) === 0) {
625  $slugOfSubpage = substr($slugOfSubpage, strlen($commonSlugPrefixOfMountedPage));
626  }
627  $pageCandidate['slug'] = $slugOfMountPoint . (($slugOfSubpage && $slugOfSubpage !== '/') ? '/' . trim($slugOfSubpage, '/') : '');
628  $pages[] = $pageCandidate;
629  }
630  return $pages;
631  }
632 
648  int $pageId,
649  string $pagePath,
650  array $mountPointPairs,
651  PageRepository $pageRepository
652  ): string {
653  // Handle recursive mount points
654  $prefixesToRemove = [];
655  $slugPrefixesToAdd = [];
656  foreach ($mountPointPairs as $mountPointPair) {
657  [$mountRoot, $mountedPage] = GeneralUtility::intExplode('-', $mountPointPair);
658  $mountPageInformation = $pageRepository->getMountPointInfo($mountedPage);
659  if ($mountPageInformation) {
660  if ($pageId === $mountedPage) {
661  continue;
662  }
663  // Get slugs in the translated page
664  $mountedPage = $pageRepository->getPage($mountedPage);
665  $mountRoot = $pageRepository->getPage($mountRoot);
666  $slugPrefix = $mountedPage['slug'] ?? '';
667  if ($slugPrefix === '/') {
668  $slugPrefix = '';
669  }
670  $prefixToRemove = $mountRoot['slug'] ?? '';
671  if ($prefixToRemove === '/') {
672  $prefixToRemove = '';
673  }
674  $prefixesToRemove[] = $prefixToRemove;
675  $slugPrefixesToAdd[] = $slugPrefix;
676  }
677  }
678  $slugPrefixesToAdd = array_reverse($slugPrefixesToAdd);
679  $prefixesToRemove = array_reverse($prefixesToRemove);
680  foreach ($prefixesToRemove as $prefixToRemove) {
681  // Slug prefixes are taken from the beginning of the array, where as the parts to be removed
682  // Are taken from the end.
683  $replacement = array_shift($slugPrefixesToAdd);
684  if ($prefixToRemove !== '' && strpos($pagePath, $prefixToRemove) === 0) {
685  $pagePath = substr($pagePath, strlen($prefixToRemove));
686  }
687  $pagePath = $replacement . ($pagePath !== '/' ? '/' . ltrim($pagePath, '/') : '');
688  }
689  return $pagePath;
690  }
691 
700  protected function ‪getEnhancersForPage(int $pageId, SiteLanguage $language): array
701  {
702  $enhancers = [];
703  foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
704  // Check if there is a restriction to page Ids.
705  if (is_array($enhancerConfiguration['limitToPages'] ?? null) && !in_array($pageId, $enhancerConfiguration['limitToPages'])) {
706  continue;
707  }
708  $enhancerType = $enhancerConfiguration['type'] ?? '';
709  $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
710  if (!empty($enhancerConfiguration['aspects'] ?? null)) {
711  $aspects = $this->aspectFactory->createAspects(
712  $enhancerConfiguration['aspects'],
713  $language,
714  $this->site
715  );
716  $enhancer->setAspects($aspects);
717  }
718  $enhancers[] = $enhancer;
719  }
720  return $enhancers;
721  }
722 
730  protected function ‪getDecoratingEnhancers(): array
731  {
732  $enhancers = [];
733  foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
734  $enhancerType = $enhancerConfiguration['type'] ?? '';
735  $enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
736  if ($enhancer instanceof DecoratingEnhancerInterface) {
737  $enhancers[] = $enhancer;
738  }
739  }
740  return $enhancers;
741  }
742 
749  protected function ‪getRoutePathRedecorationPattern(): string
750  {
751  $decoratingEnhancers = $this->‪getDecoratingEnhancers();
752  if (empty($decoratingEnhancers)) {
753  return '';
754  }
755  $redecorationPatterns = array_map(
756  function (DecoratingEnhancerInterface $decorationEnhancers) {
757  $pattern = $decorationEnhancers->getRoutePathRedecorationPattern();
758  return '(?:' . $pattern . ')';
759  },
760  $decoratingEnhancers
761  );
762  return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
763  }
764 
781  protected function ‪getCandidateSlugsFromRoutePath(string $routePath): array
782  {
783  $redecorationPattern = $this->‪getRoutePathRedecorationPattern();
784  if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
785  $decoration = $matches['decoration'];
786  $decorationPattern = preg_quote($decoration, '#');
787  $routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath);
788  }
789 
790  $candidatePathParts = [];
791  $pathParts = GeneralUtility::trimExplode('/', $routePath, true);
792  if (empty($pathParts)) {
793  return ['/'];
794  }
795 
796  while (!empty($pathParts)) {
797  $prefix = '/' . implode('/', $pathParts);
798  $candidatePathParts[] = $prefix . '/';
799  $candidatePathParts[] = $prefix;
800  array_pop($pathParts);
801  }
802  $candidatePathParts[] = '/';
803  return $candidatePathParts;
804  }
805 
811  protected function ‪generateCacheHash(int $pageId, PageArguments $arguments): string
812  {
813  return $this->cacheHashCalculator->calculateCacheHash(
814  $this->‪getCacheHashParameters($pageId, $arguments)
815  );
816  }
817 
823  protected function ‪getCacheHashParameters(int $pageId, PageArguments $arguments): array
824  {
825  $hashParameters = $arguments->getDynamicArguments();
826  $hashParameters['id'] = $pageId;
827  $uri = http_build_query($hashParameters, '', '&', PHP_QUERY_RFC3986);
828  return $this->cacheHashCalculator->getRelevantParameters($uri);
829  }
830 
848  protected function ‪buildPageArguments(Route $route, array $results, array $remainingQueryParameters = []): PageArguments
849  {
850  // only use parameters that actually have been processed
851  // (thus stripping internals like _route, _controller, ...)
852  $routeArguments = $this->‪filterProcessedParameters($route, $results);
853  // assert amount of "static" mappers is not too "dynamic"
854  $this->‪assertMaximumStaticMappableAmount($route, array_keys($routeArguments));
855  // delegate result handling to enhancer
856  $enhancer = $route->getEnhancer();
857  if ($enhancer instanceof ResultingInterface) {
858  // forward complete(!) results, not just filtered parameters
859  return $enhancer->buildResult($route, $results, $remainingQueryParameters);
860  }
861  $page = $route->getOption('_page');
862  $pageId = (int)($page['l10n_parent'] > 0 ? $page['l10n_parent'] : $page['uid']);
863  $type = $this->‪resolveType($route, $remainingQueryParameters);
864  // See PageSlugCandidateProvider where this is added.
865  if ($page['MPvar'] ?? '') {
866  $routeArguments['MP'] = $page['MPvar'];
867  }
868  return new PageArguments($pageId, $type, $routeArguments, [], $remainingQueryParameters);
869  }
870 
878  protected function ‪resolveType(Route $route, array &$remainingQueryParameters): string
879  {
880  $type = $remainingQueryParameters['type'] ?? 0;
881  $decoratedParameters = $route->getOption('_decoratedParameters');
882  if (isset($decoratedParameters['type'])) {
883  $type = $decoratedParameters['type'];
884  unset($decoratedParameters['type']);
885  $remainingQueryParameters = array_replace_recursive(
886  $remainingQueryParameters,
887  $decoratedParameters
888  );
889  }
890  return (string)$type;
891  }
892 
902  protected function ‪assertMaximumStaticMappableAmount(Route $route, array $variableNames = [])
903  {
904  // empty when only values of route defaults where used
905  if (empty($variableNames)) {
906  return;
907  }
908  $mappers = $route->filterAspects(
909  [StaticMappableAspectInterface::class, \Countable::class],
910  $variableNames
911  );
912  if (empty($mappers)) {
913  return;
914  }
915 
916  $multipliers = array_map('count', $mappers);
917  $product = array_product($multipliers);
918  if ($product > 10000) {
919  throw new \OverflowException(
920  'Possible range of all mappers is larger than 10000 items',
921  1537696772
922  );
923  }
924  }
925 
933  protected function ‪filterProcessedParameters(Route $route, $results): array
934  {
935  return array_intersect_key(
936  $results,
937  array_flip($route->compile()->getPathVariables())
938  );
939  }
940 }
‪TYPO3\CMS\Core\Routing\PageArguments
Definition: PageArguments.php:25
‪TYPO3\CMS\Core\Context\Context\getPropertyFromAspect
‪mixed null getPropertyFromAspect(string $name, string $property, $default=null)
Definition: Context.php:123
‪TYPO3\CMS\Frontend\Page\PageRepository\DOKTYPE_MOUNTPOINT
‪const DOKTYPE_MOUNTPOINT
Definition: PageRepository.php:172
‪TYPO3\CMS\Core\Routing\Enhancer\EnhancerFactory
Definition: EnhancerFactory.php:25
‪TYPO3\CMS\Core\Context\LanguageAspectFactory
Definition: LanguageAspectFactory.php:25
‪TYPO3\CMS\Core\Routing\RouterInterface
Definition: RouterInterface.php:27
‪TYPO3\CMS\Core\Site\Entity\Site\getLanguageById
‪SiteLanguage getLanguageById(int $languageId)
Definition: Site.php:234
‪TYPO3\CMS\Core\Routing\Enhancer\RoutingEnhancerInterface
Definition: RoutingEnhancerInterface.php:25
‪TYPO3\CMS\Core\Routing\RouteResultInterface
Definition: RouteResultInterface.php:23
‪TYPO3\CMS\Core\Routing\PageUriMatcher
Definition: PageUriMatcher.php:32
‪TYPO3\CMS\Core\Routing\RouteCollection
Definition: RouteCollection.php:27
‪TYPO3\CMS\Core\Utility\RootlineUtility
Definition: RootlineUtility.php:36
‪TYPO3\CMS\Core\Exception\SiteNotFoundException
Definition: SiteNotFoundException.php:25
‪TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction
Definition: FrontendWorkspaceRestriction.php:28
‪TYPO3\CMS\Core\Routing\PageRouter
Definition: PageRouter.php:75
‪TYPO3\CMS\Core\Context\LanguageAspectFactory\createFromSiteLanguage
‪static LanguageAspect createFromSiteLanguage(SiteLanguage $language)
Definition: LanguageAspectFactory.php:102
‪TYPO3\CMS\Core\Routing
‪TYPO3\CMS\Core\Routing\PageRouter\resolveMountPointParameterIntoPageSlug
‪string resolveMountPointParameterIntoPageSlug(int $pageId, string $pagePath, array $mountPointPairs, PageRepository $pageRepository)
Definition: PageRouter.php:642
‪TYPO3\CMS\Frontend\Page\PageRepository\getPage
‪array getPage($uid, $disableGroupAccessCheck=false)
Definition: PageRepository.php:312
‪TYPO3\CMS\Core\Routing\RouteNotFoundException
Definition: RouteNotFoundException.php:23
‪TYPO3\CMS\Core\Routing\Enhancer\ResultingInterface
Definition: ResultingInterface.php:27
‪TYPO3\CMS\Core\Context\Context
Definition: Context.php:49
‪TYPO3\CMS\Core\Routing\RouterInterface\generateUri
‪UriInterface generateUri($route, array $parameters=[], string $fragment='', string $type=self::ABSOLUTE_URL)
‪TYPO3\CMS\Frontend\Page\PageRepository\getMountPointInfo
‪mixed getMountPointInfo($pageId, $pageRec=false, $prevMountPids=[], $firstPageUid=0)
Definition: PageRepository.php:1310
‪TYPO3\CMS\Core\Routing\PageRouter\getPagesFromDatabaseForCandidates
‪array getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId, array $excludeUids=[])
Definition: PageRouter.php:396
‪TYPO3\CMS\Core\Http\Uri
Definition: Uri.php:27
‪TYPO3\CMS\Core\Context\Context\setAspect
‪setAspect(string $name, AspectInterface $aspect)
Definition: Context.php:141
‪TYPO3\CMS\Core\Site\Entity\Site
Definition: Site.php:39
‪TYPO3\CMS\Core\Site\Entity\SiteLanguage
Definition: SiteLanguage.php:25
‪TYPO3\CMS\Core\Routing\PageRouter\getDecoratingEnhancers
‪DecoratingEnhancerInterface[] getDecoratingEnhancers()
Definition: PageRouter.php:725
‪TYPO3\CMS\Core\Routing\PageRouter\getCacheHashParameters
‪array getCacheHashParameters(int $pageId, PageArguments $arguments)
Definition: PageRouter.php:818
‪TYPO3\CMS\Core\Routing\Route\getEnhancer
‪EnhancerInterface null getEnhancer()
Definition: Route.php:87
‪TYPO3\CMS\Core\Routing\PageRouter\filterProcessedParameters
‪array filterProcessedParameters(Route $route, $results)
Definition: PageRouter.php:928
‪TYPO3\CMS\Core\Routing\PageRouter\$site
‪Site $site
Definition: PageRouter.php:78
‪TYPO3\CMS\Core\Routing\SiteRouteResult
Definition: SiteRouteResult.php:29
‪TYPO3\CMS\Core\Routing\PageRouter\buildPageArguments
‪PageArguments buildPageArguments(Route $route, array $results, array $remainingQueryParameters=[])
Definition: PageRouter.php:843
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
‪TYPO3\CMS\Frontend\Page\PageRepository
Definition: PageRepository.php:53
‪TYPO3\CMS\Core\Routing\PageRouter\$enhancerFactory
‪EnhancerFactory $enhancerFactory
Definition: PageRouter.php:82
‪TYPO3\CMS\Core\Routing\PageRouter\assertMaximumStaticMappableAmount
‪assertMaximumStaticMappableAmount(Route $route, array $variableNames=[])
Definition: PageRouter.php:897
‪TYPO3\CMS\Core\Routing\PageRouter\getEnhancersForPage
‪EnhancerInterface[] getEnhancersForPage(int $pageId, SiteLanguage $language)
Definition: PageRouter.php:695
‪TYPO3\CMS\Core\Routing\PageRouter\getCandidateSlugsFromRoutePath
‪array getCandidateSlugsFromRoutePath(string $routePath)
Definition: PageRouter.php:776
‪TYPO3\CMS\Core\Routing\PageRouter\$aspectFactory
‪AspectFactory $aspectFactory
Definition: PageRouter.php:86
‪TYPO3\CMS\Core\Routing\PageRouter\$context
‪TYPO3 CMS Core Context Context null $context
Definition: PageRouter.php:94
‪TYPO3\CMS\Core\Routing\Aspect\MappableProcessor
Definition: MappableProcessor.php:25
‪TYPO3\CMS\Core\Routing\PageRouter\$cacheHashCalculator
‪CacheHashCalculator $cacheHashCalculator
Definition: PageRouter.php:90
‪TYPO3\CMS\Core\Routing\PageRouter\__construct
‪__construct(Site $site, Context $context=null)
Definition: PageRouter.php:102
‪TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction
Definition: DeletedRestriction.php:26
‪TYPO3\CMS\Core\Routing\Route
Definition: Route.php:31
‪TYPO3\CMS\Core\Routing\PageRouter\getRoutePathRedecorationPattern
‪string getRoutePathRedecorationPattern()
Definition: PageRouter.php:744
‪TYPO3\CMS\Frontend\Page\CacheHashCalculator
Definition: CacheHashCalculator.php:24
‪TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface
Definition: StaticMappableAspectInterface.php:23
‪TYPO3\CMS\Core\Routing\Aspect\AspectFactory
Definition: AspectFactory.php:32
‪TYPO3\CMS\Core\Routing\Enhancer\InflatableEnhancerInterface
Definition: InflatableEnhancerInterface.php:23
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Routing\PageRouter\resolveType
‪string resolveType(Route $route, array &$remainingQueryParameters)
Definition: PageRouter.php:873
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Core\Routing\PageRouter\generateCacheHash
‪string generateCacheHash(int $pageId, PageArguments $arguments)
Definition: PageRouter.php:806
‪TYPO3\CMS\Core\Routing\Route\filterAspects
‪AspectInterface[] filterAspects(array $classNames, array $variableNames=[])
Definition: Route.php:181
‪TYPO3\CMS\Core\Http\NormalizedParams
Definition: NormalizedParams.php:32
‪TYPO3\CMS\Core\Routing\PageRouter\matchRequest
‪SiteRouteResult matchRequest(ServerRequestInterface $request, RouteResultInterface $previousResult=null)
Definition: PageRouter.php:120
‪TYPO3\CMS\Core\Routing\Enhancer\DecoratingEnhancerInterface
Definition: DecoratingEnhancerInterface.php:25
‪TYPO3\CMS\Core\Routing\Enhancer\EnhancerInterface
Definition: EnhancerInterface.php:26
‪TYPO3\CMS\Core\Routing\PageRouter\findPageCandidatesOfMountPoint
‪array findPageCandidatesOfMountPoint(array $mountPointPage, array $mountedPage, Site $siteOfMountedPage, int $languageId, array $slugCandidates, Context $context)
Definition: PageRouter.php:545