[TASK] Detect page routes based on deleted+workspace restriction
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Routing / PageRouter.php
1 <?php
2 declare(strict_types = 1);
3
4 namespace TYPO3\CMS\Core\Routing;
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 Symfony\Component\Routing\Exception\ResourceNotFoundException;
22 use Symfony\Component\Routing\Matcher\UrlMatcher;
23 use Symfony\Component\Routing\RequestContext;
24 use Symfony\Component\Routing\Route;
25 use Symfony\Component\Routing\RouteCollection;
26 use TYPO3\CMS\Core\Database\ConnectionPool;
27 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
28 use TYPO3\CMS\Core\Database\Query\Restriction\FrontendWorkspaceRestriction;
29 use TYPO3\CMS\Core\Site\Entity\SiteInterface;
30 use TYPO3\CMS\Core\Site\Entity\SiteLanguage;
31 use TYPO3\CMS\Core\Utility\GeneralUtility;
32
33 /**
34 * Page Router looking up the slug of the page path.
35 *
36 * This is done via the "Route Candidate" pattern.
37 *
38 * Example:
39 * - /about-us/team/management/
40 *
41 * will look for all pages that have
42 * - /about-us
43 * - /about-us/
44 * - /about-us/team
45 * - /about-us/team/
46 * - /about-us/team/management
47 * - /about-us/team/management/
48 *
49 * And create route candidates for that.
50 *
51 * Please note: PageRouter does not restrict the HTTP method or is bound to any domain constraints,
52 * as the SiteMatcher has done that already.
53 *
54 * The concept of the PageRouter is to *resolve*, and not build URIs. On top, it is a facade to hide the
55 * dependency to symfony and to not expose its logic.
56
57 * @internal This API is not public yet and might change in the future, until TYPO3 v9 or TYPO3 v10.
58 */
59 class PageRouter
60 {
61 /**
62 * @param ServerRequestInterface $request
63 * @param string $routePathTail
64 * @param SiteInterface $site
65 * @param SiteLanguage $language
66 * @return RouteResult|null
67 */
68 public function matchRoute(ServerRequestInterface $request, string $routePathTail, SiteInterface $site, SiteLanguage $language): ?RouteResult
69 {
70 $slugCandidates = $this->getCandidateSlugsFromRoutePath($routePathTail);
71 if (empty($slugCandidates)) {
72 return null;
73 }
74 $pageCandidates = $this->getPagesFromDatabaseForCandidates($slugCandidates, $site, $language->getLanguageId());
75 // Stop if there are no candidates
76 if (empty($pageCandidates)) {
77 return null;
78 }
79
80 $collection = new RouteCollection();
81 foreach ($pageCandidates ?? [] as $page) {
82 $path = $page['slug'];
83 $route = new Route(
84 $path . '{tail}',
85 ['page' => $page, 'tail' => ''],
86 ['tail' => '.*'],
87 ['utf8' => true]
88 );
89 $collection->add('page_' . $page['uid'], $route);
90 }
91
92 $context = new RequestContext('/', $request->getMethod(), $request->getUri()->getHost());
93 $matcher = new UrlMatcher($collection, $context);
94 try {
95 $result = $matcher->match('/' . ltrim($routePathTail, '/'));
96 unset($result['_route']);
97 return new RouteResult($request->getUri(), $site, $language, $result['tail'], $result);
98 } catch (ResourceNotFoundException $e) {
99 // do nothing
100 }
101 return new RouteResult($request->getUri(), $site, $language);
102 }
103
104 /**
105 * Check for records in the database which matches one of the slug candidates.
106 *
107 * @param array $slugCandidates
108 * @param SiteInterface $site
109 * @param int $languageId
110 * @return array
111 */
112 protected function getPagesFromDatabaseForCandidates(array $slugCandidates, SiteInterface $site, int $languageId): array
113 {
114 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
115 ->getQueryBuilderForTable('pages');
116 $queryBuilder
117 ->getRestrictions()
118 ->removeAll()
119 ->add(GeneralUtility::makeInstance(DeletedRestriction::class))
120 ->add(GeneralUtility::makeInstance(FrontendWorkspaceRestriction::class));
121
122 $statement = $queryBuilder
123 ->select('uid', 'l10n_parent', 'pid', 'slug')
124 ->from('pages')
125 ->where(
126 $queryBuilder->expr()->eq(
127 'sys_language_uid',
128 $queryBuilder->createNamedParameter($languageId, \PDO::PARAM_INT)
129 ),
130 $queryBuilder->expr()->in(
131 'slug',
132 $queryBuilder->createNamedParameter(
133 $slugCandidates,
134 Connection::PARAM_STR_ARRAY
135 )
136 )
137 )
138 // Exact match will be first, that's important
139 ->orderBy('slug', 'desc')
140 ->execute();
141
142 $pages = [];
143 $siteMatcher = GeneralUtility::makeInstance(SiteMatcher::class);
144 while ($row = $statement->fetch()) {
145 $pageIdInDefaultLanguage = (int)($languageId > 0 ? $row['l10n_parent'] : $row['uid']);
146 if ($siteMatcher->matchByPageId($pageIdInDefaultLanguage)->getRootPageId() === $site->getRootPageId()) {
147 $pages[] = $row;
148 }
149 }
150 return $pages;
151 }
152
153 /**
154 * Returns possible URL parts for a string like /home/about-us/offices/
155 * to return
156 * /home/about-us/offices/
157 * /home/about-us/offices
158 * /home/about-us/
159 * /home/about-us
160 * /home/
161 * /home
162 *
163 * @param string $routePath
164 * @return array
165 */
166 protected function getCandidateSlugsFromRoutePath(string $routePath): array
167 {
168 $candidatePathParts = [];
169 $pathParts = GeneralUtility::trimExplode('/', $routePath, true);
170 while (!empty($pathParts)) {
171 $prefix = '/' . implode('/', $pathParts);
172 $candidatePathParts[] = $prefix . '/';
173 $candidatePathParts[] = $prefix;
174 array_pop($pathParts);
175 }
176 return $candidatePathParts;
177 }
178 }