Skip to content

Commit

Permalink
[TASK] Move Slug Candidate functionality into its own class
Browse files Browse the repository at this point in the history
When Page-based slug handling + routing was introduced,
the initial PageRouter class turned big very quickly.

Especially managing the possible pages that could match
was initially based on the candidates principle that could
be moved / exchanged later-on.

For this to happen the initial step is to move this to a separate
PHP class called "PageSlugCandidateProvider".

As seen in the patch, all calls to the database related
to retrieving pages (DB + PageRepository) are
moved into PageSlugCandidateProvider.

Resolves: #88575
Releases: master
Change-Id: I14b3ebccf439613cc9aa7943d1a44d0892cf04e5
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61050
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: Andreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Andreas Fernandez <a.fernandez@scripting-base.de>
  • Loading branch information
bmack authored and andreaskienast committed Jul 17, 2019
1 parent 6a23b8e commit 990b59f
Show file tree
Hide file tree
Showing 3 changed files with 315 additions and 223 deletions.
230 changes: 22 additions & 208 deletions typo3/sysext/core/Classes/Routing/PageRouter.php
Expand Up @@ -16,7 +16,6 @@
* The TYPO3 project - inspiring people to share!
*/

use Doctrine\DBAL\Connection;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Routing\Exception\MissingMandatoryParametersException;
Expand All @@ -25,11 +24,7 @@
use Symfony\Component\Routing\RequestContext;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Context\LanguageAspectFactory;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Exception\SiteNotFoundException;
use TYPO3\CMS\Core\Http\NormalizedParams;
use TYPO3\CMS\Core\Http\Uri;
use TYPO3\CMS\Core\Routing\Aspect\AspectFactory;
Expand All @@ -42,7 +37,6 @@
use TYPO3\CMS\Core\Routing\Enhancer\RoutingEnhancerInterface;
use TYPO3\CMS\Core\Site\Entity\Site;
use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
use TYPO3\CMS\Core\Site\SiteFinder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\Page\CacheHashCalculator;

Expand Down Expand Up @@ -118,12 +112,15 @@ public function matchRequest(ServerRequestInterface $request, RouteResultInterfa
if (!($previousResult instanceof RouteResultInterface)) {
throw new RouteNotFoundException('No previous result given. Cannot find a page for an empty route part', 1555303496);
}

$candidateProvider = $this->getSlugCandidateProvider(GeneralUtility::makeInstance(Context::class));

// Legacy URIs (?id=12345) takes precedence, no matter if a route is given
$requestId = (string)($request->getQueryParams()['id'] ?? '');
if (!empty($requestId)) {
if (!empty($page = $this->resolvePageId($requestId))) {
$requestId = (int)($request->getQueryParams()['id'] ?? 0);
if ($requestId > 0) {
if (!empty($pageId = $candidateProvider->getRealPageIdForPageIdAsPossibleCandidate($requestId))) {
return new PageArguments(
(int)($page['l10n_parent'] ?: $page['uid']),
$pageId,
(string)($request->getQueryParams()['type'] ?? '0'),
[],
[],
Expand All @@ -132,6 +129,7 @@ public function matchRequest(ServerRequestInterface $request, RouteResultInterfa
}
throw new RouteNotFoundException('The requested page does not exist.', 1557839801);
}

$urlPath = $previousResult->getTail();
// Remove the script name (e.g. index.php), if given
if (!empty($urlPath)) {
Expand All @@ -144,38 +142,14 @@ public function matchRequest(ServerRequestInterface $request, RouteResultInterfa
}
}

$prefixedUrlPath = '/' . trim($urlPath, '/');
$slugCandidates = $this->getCandidateSlugsFromRoutePath($urlPath ?: '/');
$pageCandidates = [];
$language = $previousResult->getLanguage();
$languages = [$language->getLanguageId()];
if (!empty($language->getFallbackLanguageIds())) {
$languages = array_merge($languages, $language->getFallbackLanguageIds());
}
// Iterate all defined languages in their configured order to get matching page candidates somewhere in the language fallback chain
foreach ($languages as $languageId) {
$pageCandidatesFromSlugsAndLanguage = $this->getPagesFromDatabaseForCandidates($slugCandidates, $languageId);
// Determine whether fetched page candidates qualify for the request. The incoming URL is checked against all
// pages found for the current URL and language.
foreach ($pageCandidatesFromSlugsAndLanguage as $candidate) {
$slugCandidate = '/' . trim($candidate['slug'], '/');
if ($slugCandidate === '/' || strpos($prefixedUrlPath, $slugCandidate) === 0) {
// The slug is a subpart of the requested URL, so it's a possible candidate
if ($prefixedUrlPath === $slugCandidate) {
// The requested URL matches exactly the found slug. We can't find a better match,
// so use that page candidate and stop any further querying.
$pageCandidates = [$candidate];
break 2;
}

$pageCandidates[] = $candidate;
}
}
}
$prefixedUrlPath = '/' . trim($urlPath, '/');

$pageCandidates = $candidateProvider->getCandidatesForPath($prefixedUrlPath, $language);

// Stop if there are no candidates
if (empty($pageCandidates)) {
throw new RouteNotFoundException('No page candidates found for path "' . $urlPath . '"', 1538389999);
throw new RouteNotFoundException('No page candidates found for path "' . $prefixedUrlPath . '"', 1538389999);
}

$fullCollection = new RouteCollection();
Expand Down Expand Up @@ -347,93 +321,6 @@ public function generateUri($route, array $parameters = [], string $fragment = '
return $uri;
}

/**
* Check for records in the database which matches one of the slug candidates.
*
* @param array $slugCandidates
* @param int $languageId
* @return array
*/
protected function getPagesFromDatabaseForCandidates(array $slugCandidates, int $languageId): array
{
$context = GeneralUtility::makeInstance(Context::class);
$searchLiveRecordsOnly = $context->getPropertyFromAspect('workspace', 'isLive');
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class, null, null, $searchLiveRecordsOnly));

$statement = $queryBuilder
->select('uid', 'l10n_parent', 'pid', 'slug')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'sys_language_uid',
$queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
),
$queryBuilder->expr()->in(
'slug',
$queryBuilder->createNamedParameter(
$slugCandidates,
Connection::PARAM_STR_ARRAY
)
)
)
// Exact match will be first, that's important
->orderBy('slug', 'desc')
->execute();

$pages = [];
$siteFinder = GeneralUtility::makeInstance(SiteFinder::class);
$pageRepository = GeneralUtility::makeInstance(PageRepository::class, $context);
while ($row = $statement->fetch()) {
$pageRepository->fixVersioningPid('pages', $row);
$pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
try {
if ($siteFinder->getSiteByPageId($pageIdInDefaultLanguage)->getRootPageId() === $this->site->getRootPageId()) {
$pages[] = $row;
}
} catch (SiteNotFoundException $e) {
}
}
return $pages;
}

/**
* @param string $pageId
* @return array|null
*/
protected function resolvePageId(string $pageId): ?array
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('pages');
$queryBuilder
->getRestrictions()
->removeAll()
->add(GeneralUtility::makeInstance(DeletedRestriction::class))
->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class));

$statement = $queryBuilder
->select('uid', 'l10n_parent', 'pid')
->from('pages')
->where(
$queryBuilder->expr()->eq(
'uid',
$queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT)
)
)
->execute();

$page = $statement->fetch();
if (empty($page)) {
return null;
}
return $page;
}

/**
* Fetch possible enhancers + aspects based on the current page configuration and the site configuration put
* into "routeEnhancers"
Expand Down Expand Up @@ -464,89 +351,6 @@ protected function getEnhancersForPage(int $pageId, SiteLanguage $language): arr
return $enhancers;
}

/**
* Resolves decorating enhancers without having aspects assigned. These
* instances are used to pre-process URL path and MUST NOT be used for
* actually resolving or generating URL parameters.
*
* @return DecoratingEnhancerInterface[]
*/
protected function getDecoratingEnhancers(): array
{
$enhancers = [];
foreach ($this->site->getConfiguration()['routeEnhancers'] ?? [] as $enhancerConfiguration) {
$enhancerType = $enhancerConfiguration['type'] ?? '';
$enhancer = $this->enhancerFactory->create($enhancerType, $enhancerConfiguration);
if ($enhancer instanceof DecoratingEnhancerInterface) {
$enhancers[] = $enhancer;
}
}
return $enhancers;
}

/**
* Gets all patterns that can be used to redecorate (undecorate) a
* potential previously decorated route path.
*
* @return string regular expression pattern capable of redecorating
*/
protected function getRoutePathRedecorationPattern(): string
{
$decoratingEnhancers = $this->getDecoratingEnhancers();
if (empty($decoratingEnhancers)) {
return '';
}
$redecorationPatterns = array_map(
function (DecoratingEnhancerInterface $decorationEnhancers) {
$pattern = $decorationEnhancers->getRoutePathRedecorationPattern();
return '(?:' . $pattern . ')';
},
$decoratingEnhancers
);
return '(?P<decoration>' . implode('|', $redecorationPatterns) . ')';
}

/**
* Returns possible URL parts for a string like /home/about-us/offices/ or /home/about-us/offices.json
* to return.
*
* /home/about-us/offices/
* /home/about-us/offices.json
* /home/about-us/offices
* /home/about-us/
* /home/about-us
* /home/
* /home
* /
*
* @param string $routePath
* @return array
*/
protected function getCandidateSlugsFromRoutePath(string $routePath): array
{
$redecorationPattern = $this->getRoutePathRedecorationPattern();
if (!empty($redecorationPattern) && preg_match('#' . $redecorationPattern . '#', $routePath, $matches)) {
$decoration = $matches['decoration'];
$decorationPattern = preg_quote($decoration, '#');
$routePath = preg_replace('#' . $decorationPattern . '$#', '', $routePath);
}

$candidatePathParts = [];
$pathParts = GeneralUtility::trimExplode('/', $routePath, true);
if (empty($pathParts)) {
return ['/'];
}

while (!empty($pathParts)) {
$prefix = '/' . implode('/', $pathParts);
$candidatePathParts[] = $prefix . '/';
$candidatePathParts[] = $prefix;
array_pop($pathParts);
}
$candidatePathParts[] = '/';
return $candidatePathParts;
}

/**
* @param int $pageId
* @param PageArguments $arguments
Expand Down Expand Up @@ -673,4 +477,14 @@ protected function filterProcessedParameters(Route $route, $results): array
array_flip($route->compile()->getPathVariables())
);
}

protected function getSlugCandidateProvider(Context $context): PageSlugCandidateProvider
{
return GeneralUtility::makeInstance(
PageSlugCandidateProvider::class,
$context,
$this->site,
$this->enhancerFactory
);
}
}

0 comments on commit 990b59f

Please sign in to comment.