vendor/pimcore/pimcore/lib/Routing/Dynamic/DocumentRouteHandler.php line 173

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * Pimcore
  5. *
  6. * This source file is available under two different licenses:
  7. * - GNU General Public License version 3 (GPLv3)
  8. * - Pimcore Commercial License (PCL)
  9. * Full copyright and license information is available in
  10. * LICENSE.md which is distributed with this source code.
  11. *
  12. * @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
  13. * @license http://www.pimcore.org/license GPLv3 and PCL
  14. */
  15. namespace Pimcore\Routing\Dynamic;
  16. use Pimcore\Config;
  17. use Pimcore\Http\Request\Resolver\SiteResolver;
  18. use Pimcore\Http\Request\Resolver\StaticPageResolver;
  19. use Pimcore\Http\RequestHelper;
  20. use Pimcore\Model\Document;
  21. use Pimcore\Model\Document\Page;
  22. use Pimcore\Routing\DocumentRoute;
  23. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  24. use Symfony\Component\Routing\RouteCollection;
  25. /**
  26. * @internal
  27. */
  28. final class DocumentRouteHandler implements DynamicRouteHandlerInterface
  29. {
  30. /**
  31. * @var Document\Service
  32. */
  33. private $documentService;
  34. /**
  35. * @var SiteResolver
  36. */
  37. private $siteResolver;
  38. /**
  39. * @var RequestHelper
  40. */
  41. private $requestHelper;
  42. /**
  43. * Determines if unpublished documents should be matched, even when not in admin mode. This
  44. * is mainly needed for maintencance jobs/scripts.
  45. *
  46. * @var bool
  47. */
  48. private $forceHandleUnpublishedDocuments = false;
  49. /**
  50. * @var array
  51. */
  52. private $directRouteDocumentTypes = [];
  53. /**
  54. * @var Config
  55. */
  56. private $config;
  57. /**
  58. * @var StaticPageResolver
  59. */
  60. private StaticPageResolver $staticPageResolver;
  61. /**
  62. * @param Document\Service $documentService
  63. * @param SiteResolver $siteResolver
  64. * @param RequestHelper $requestHelper
  65. * @param Config $config
  66. * @param StaticPageResolver $staticPageResolver
  67. */
  68. public function __construct(
  69. Document\Service $documentService,
  70. SiteResolver $siteResolver,
  71. RequestHelper $requestHelper,
  72. Config $config,
  73. StaticPageResolver $staticPageResolver
  74. ) {
  75. $this->documentService = $documentService;
  76. $this->siteResolver = $siteResolver;
  77. $this->requestHelper = $requestHelper;
  78. $this->config = $config;
  79. $this->staticPageResolver = $staticPageResolver;
  80. }
  81. public function setForceHandleUnpublishedDocuments(bool $handle)
  82. {
  83. $this->forceHandleUnpublishedDocuments = $handle;
  84. }
  85. /**
  86. * @return array
  87. */
  88. public function getDirectRouteDocumentTypes()
  89. {
  90. if (empty($this->directRouteDocumentTypes)) {
  91. $routingConfig = \Pimcore\Config::getSystemConfiguration('routing');
  92. $this->directRouteDocumentTypes = $routingConfig['direct_route_document_types'];
  93. }
  94. return $this->directRouteDocumentTypes;
  95. }
  96. /**
  97. * @deprecated will be removed in Pimcore 11
  98. *
  99. * @param string $type
  100. */
  101. public function addDirectRouteDocumentType($type)
  102. {
  103. trigger_deprecation(
  104. 'pimcore/pimcore',
  105. '10.1',
  106. 'The DocumentRouteHandler::addDirectRouteDocumentType() method is deprecated, use pimcore.routing.direct_route_document_types config instead.'
  107. );
  108. if (!in_array($type, $this->getDirectRouteDocumentTypes())) {
  109. $this->directRouteDocumentTypes[] = $type;
  110. }
  111. }
  112. /**
  113. * {@inheritdoc}
  114. */
  115. public function getRouteByName(string $name)
  116. {
  117. if (preg_match('/^document_(\d+)$/', $name, $match)) {
  118. $document = Document::getById((int) $match[1]);
  119. if ($this->isDirectRouteDocument($document)) {
  120. return $this->buildRouteForDocument($document);
  121. }
  122. }
  123. throw new RouteNotFoundException(sprintf("Route for name '%s' was not found", $name));
  124. }
  125. /**
  126. * {@inheritdoc}
  127. */
  128. public function matchRequest(RouteCollection $collection, DynamicRequestContext $context)
  129. {
  130. $document = Document::getByPath($context->getPath());
  131. // check for a pretty url inside a site
  132. if (!$document && $this->siteResolver->isSiteRequest($context->getRequest())) {
  133. $site = $this->siteResolver->getSite($context->getRequest());
  134. $sitePrettyDocId = $this->documentService->getDao()->getDocumentIdByPrettyUrlInSite($site, $context->getOriginalPath());
  135. if ($sitePrettyDocId) {
  136. if ($sitePrettyDoc = Document::getById($sitePrettyDocId)) {
  137. $document = $sitePrettyDoc;
  138. // TODO set pretty path via siteResolver?
  139. // undo the modification of the path by the site detection (prefixing with site root path)
  140. // this is not necessary when using pretty-urls and will cause problems when validating the
  141. // prettyUrl later (redirecting to the prettyUrl in the case the page was called by the real path)
  142. $context->setPath($context->getOriginalPath());
  143. }
  144. }
  145. }
  146. // check for a parent hardlink with children
  147. if (!$document instanceof Document) {
  148. $hardlinkedParentDocument = $this->documentService->getNearestDocumentByPath($context->getPath(), true);
  149. if ($hardlinkedParentDocument instanceof Document\Hardlink) {
  150. if ($hardLinkedDocument = Document\Hardlink\Service::getChildByPath($hardlinkedParentDocument, $context->getPath())) {
  151. $document = $hardLinkedDocument;
  152. }
  153. }
  154. }
  155. if ($document && $document instanceof Document) {
  156. if ($route = $this->buildRouteForDocument($document, $context)) {
  157. $collection->add($route->getRouteKey(), $route);
  158. }
  159. }
  160. }
  161. /**
  162. * Build a route for a document. Context is only set from match mode, not when generating URLs.
  163. *
  164. * @param Document $document
  165. * @param DynamicRequestContext|null $context
  166. *
  167. * @return DocumentRoute|null
  168. */
  169. public function buildRouteForDocument(Document $document, DynamicRequestContext $context = null)
  170. {
  171. // check for direct hardlink
  172. if ($document instanceof Document\Hardlink) {
  173. $document = Document\Hardlink\Service::wrap($document);
  174. if (!$document) {
  175. return null;
  176. }
  177. }
  178. $route = new DocumentRoute($document->getFullPath());
  179. $route->setOption('utf8', true);
  180. // coming from matching -> set route path the currently matched one
  181. if (null !== $context) {
  182. $route->setPath($context->getOriginalPath());
  183. }
  184. $route->setDefault('_locale', $document->getProperty('language'));
  185. $route->setDocument($document);
  186. if ($this->isDirectRouteDocument($document)) {
  187. /** @var Document\PageSnippet $document */
  188. $route = $this->handleDirectRouteDocument($document, $route, $context);
  189. } elseif ($document->getType() === 'link') {
  190. /** @var Document\Link $document */
  191. $route = $this->handleLinkDocument($document, $route);
  192. }
  193. return $route;
  194. }
  195. /**
  196. * Handle route params for link document
  197. *
  198. * @param Document\Link $document
  199. * @param DocumentRoute $route
  200. *
  201. * @return DocumentRoute
  202. */
  203. private function handleLinkDocument(Document\Link $document, DocumentRoute $route)
  204. {
  205. $route->setDefault('_controller', 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  206. $route->setDefault('path', $document->getHref());
  207. $route->setDefault('permanent', true);
  208. return $route;
  209. }
  210. /**
  211. * Handle direct route documents (not link)
  212. *
  213. * @param Document\PageSnippet $document
  214. * @param DocumentRoute $route
  215. * @param DynamicRequestContext|null $context
  216. *
  217. * @return DocumentRoute|null
  218. */
  219. private function handleDirectRouteDocument(
  220. Document\PageSnippet $document,
  221. DocumentRoute $route,
  222. DynamicRequestContext $context = null
  223. ) {
  224. // if we have a request in context, we're currently in match mode (not generating URLs) -> only match when frontend request by admin
  225. try {
  226. $request = $context ? $context->getRequest() : $this->requestHelper->getMainRequest();
  227. $isAdminRequest = $this->requestHelper->isFrontendRequestByAdmin($request);
  228. } catch (\LogicException $e) {
  229. // catch logic exception here - when the exception fires, it is no admin request
  230. $isAdminRequest = false;
  231. }
  232. // abort if document is not published and the request is no admin request
  233. // and matching unpublished documents was not forced
  234. if (!$document->isPublished()) {
  235. if (!($isAdminRequest || $this->forceHandleUnpublishedDocuments)) {
  236. return null;
  237. }
  238. }
  239. if (!$isAdminRequest && null !== $context) {
  240. // check for redirects (pretty URL, SEO) when not in admin mode and while matching (not generating route)
  241. if ($redirectRoute = $this->handleDirectRouteRedirect($document, $route, $context)) {
  242. return $redirectRoute;
  243. }
  244. // set static page context
  245. if ($document instanceof Page && $document->getStaticGeneratorEnabled()) {
  246. $this->staticPageResolver->setStaticPageContext($context->getRequest());
  247. }
  248. }
  249. // Use latest version, if available, when the request is admin request
  250. // so then route should be built based on latest Document settings
  251. // https://github.com/pimcore/pimcore/issues/9644
  252. if ($isAdminRequest) {
  253. $latestVersion = $document->getLatestVersion();
  254. if ($latestVersion) {
  255. $latestDoc = $latestVersion->loadData();
  256. if ($latestDoc instanceof Document\PageSnippet) {
  257. $document = $latestDoc;
  258. }
  259. }
  260. }
  261. return $this->buildRouteForPageSnippetDocument($document, $route);
  262. }
  263. /**
  264. * Handle document redirects (pretty url, SEO without trailing slash)
  265. *
  266. * @param Document\PageSnippet $document
  267. * @param DocumentRoute $route
  268. * @param DynamicRequestContext|null $context
  269. *
  270. * @return DocumentRoute|null
  271. */
  272. private function handleDirectRouteRedirect(
  273. Document\PageSnippet $document,
  274. DocumentRoute $route,
  275. DynamicRequestContext $context = null
  276. ) {
  277. $redirectTargetUrl = $context->getOriginalPath();
  278. // check for a pretty url, and if the document is called by that, otherwise redirect to pretty url
  279. if ($document instanceof Document\Page && !$document instanceof Document\Hardlink\Wrapper\WrapperInterface) {
  280. if ($prettyUrl = $document->getPrettyUrl()) {
  281. if (rtrim(strtolower($prettyUrl), ' /') !== rtrim(strtolower($context->getOriginalPath()), '/')) {
  282. $redirectTargetUrl = $prettyUrl;
  283. }
  284. }
  285. }
  286. // check for a trailing slash in path, if exists, redirect to this page without the slash
  287. // the only reason for this is: SEO, Analytics, ... there is no system specific reason, pimcore would work also with a trailing slash without problems
  288. // use $originalPath because of the sites
  289. // only do redirecting with GET requests
  290. if ($context->getRequest()->getMethod() === 'GET') {
  291. if (($this->config['documents']['allow_trailing_slash'] ?? null) === 'no') {
  292. if ($redirectTargetUrl !== '/' && substr($redirectTargetUrl, -1) === '/') {
  293. $redirectTargetUrl = rtrim($redirectTargetUrl, '/');
  294. }
  295. }
  296. // only allow the original key of a document to be the URL (lowercase/uppercase)
  297. if ($redirectTargetUrl !== '/' && rtrim($redirectTargetUrl, '/') !== rawurldecode($document->getFullPath())) {
  298. $redirectTargetUrl = $document->getFullPath();
  299. }
  300. }
  301. if (null !== $redirectTargetUrl && $redirectTargetUrl !== $context->getOriginalPath()) {
  302. $route->setDefault('_controller', 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  303. $route->setDefault('path', $redirectTargetUrl);
  304. $route->setDefault('permanent', true);
  305. return $route;
  306. }
  307. return null;
  308. }
  309. /**
  310. * Handle page snippet route (controller, action, view)
  311. *
  312. * @param Document\PageSnippet $document
  313. * @param DocumentRoute $route
  314. *
  315. * @return DocumentRoute
  316. */
  317. private function buildRouteForPageSnippetDocument(Document\PageSnippet $document, DocumentRoute $route)
  318. {
  319. $route->setDefault('_controller', $document->getController());
  320. if ($document->getTemplate()) {
  321. $route->setDefault('_template', $document->getTemplate());
  322. }
  323. return $route;
  324. }
  325. /**
  326. * Check if document is can be used to generate a route
  327. *
  328. * @param Document|null $document
  329. *
  330. * @return bool
  331. */
  332. private function isDirectRouteDocument($document)
  333. {
  334. if ($document instanceof Document\PageSnippet) {
  335. if (in_array($document->getType(), $this->getDirectRouteDocumentTypes())) {
  336. return true;
  337. }
  338. }
  339. return false;
  340. }
  341. }