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

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. if (!in_array($type, $this->getDirectRouteDocumentTypes())) {
  104. $this->directRouteDocumentTypes[] = $type;
  105. }
  106. }
  107. /**
  108. * {@inheritdoc}
  109. */
  110. public function getRouteByName(string $name)
  111. {
  112. if (preg_match('/^document_(\d+)$/', $name, $match)) {
  113. $document = Document::getById((int) $match[1]);
  114. if ($this->isDirectRouteDocument($document)) {
  115. return $this->buildRouteForDocument($document);
  116. }
  117. }
  118. throw new RouteNotFoundException(sprintf("Route for name '%s' was not found", $name));
  119. }
  120. /**
  121. * {@inheritdoc}
  122. */
  123. public function matchRequest(RouteCollection $collection, DynamicRequestContext $context)
  124. {
  125. $document = Document::getByPath($context->getPath());
  126. // check for a pretty url inside a site
  127. if (!$document && $this->siteResolver->isSiteRequest($context->getRequest())) {
  128. $site = $this->siteResolver->getSite($context->getRequest());
  129. $sitePrettyDocId = $this->documentService->getDao()->getDocumentIdByPrettyUrlInSite($site, $context->getOriginalPath());
  130. if ($sitePrettyDocId) {
  131. if ($sitePrettyDoc = Document::getById($sitePrettyDocId)) {
  132. $document = $sitePrettyDoc;
  133. // TODO set pretty path via siteResolver?
  134. // undo the modification of the path by the site detection (prefixing with site root path)
  135. // this is not necessary when using pretty-urls and will cause problems when validating the
  136. // prettyUrl later (redirecting to the prettyUrl in the case the page was called by the real path)
  137. $context->setPath($context->getOriginalPath());
  138. }
  139. }
  140. }
  141. // check for a parent hardlink with children
  142. if (!$document instanceof Document) {
  143. $hardlinkedParentDocument = $this->documentService->getNearestDocumentByPath($context->getPath(), true);
  144. if ($hardlinkedParentDocument instanceof Document\Hardlink) {
  145. if ($hardLinkedDocument = Document\Hardlink\Service::getChildByPath($hardlinkedParentDocument, $context->getPath())) {
  146. $document = $hardLinkedDocument;
  147. }
  148. }
  149. }
  150. if ($document && $document instanceof Document) {
  151. if ($route = $this->buildRouteForDocument($document, $context)) {
  152. $collection->add($route->getRouteKey(), $route);
  153. }
  154. }
  155. }
  156. /**
  157. * Build a route for a document. Context is only set from match mode, not when generating URLs.
  158. *
  159. * @param Document $document
  160. * @param DynamicRequestContext|null $context
  161. *
  162. * @return DocumentRoute|null
  163. */
  164. public function buildRouteForDocument(Document $document, DynamicRequestContext $context = null)
  165. {
  166. // check for direct hardlink
  167. if ($document instanceof Document\Hardlink) {
  168. $document = Document\Hardlink\Service::wrap($document);
  169. if (!$document) {
  170. return null;
  171. }
  172. }
  173. $route = new DocumentRoute($document->getFullPath());
  174. $route->setOption('utf8', true);
  175. // coming from matching -> set route path the currently matched one
  176. if (null !== $context) {
  177. $route->setPath($context->getOriginalPath());
  178. }
  179. $route->setDefault('_locale', $document->getProperty('language'));
  180. $route->setDocument($document);
  181. if ($this->isDirectRouteDocument($document)) {
  182. /** @var Document\PageSnippet $document */
  183. $route = $this->handleDirectRouteDocument($document, $route, $context);
  184. } elseif ($document->getType() === 'link') {
  185. /** @var Document\Link $document */
  186. $route = $this->handleLinkDocument($document, $route);
  187. }
  188. return $route;
  189. }
  190. /**
  191. * Handle route params for link document
  192. *
  193. * @param Document\Link $document
  194. * @param DocumentRoute $route
  195. *
  196. * @return DocumentRoute
  197. */
  198. private function handleLinkDocument(Document\Link $document, DocumentRoute $route)
  199. {
  200. $route->setDefault('_controller', 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  201. $route->setDefault('path', $document->getHref());
  202. $route->setDefault('permanent', true);
  203. return $route;
  204. }
  205. /**
  206. * Handle direct route documents (not link)
  207. *
  208. * @param Document\PageSnippet $document
  209. * @param DocumentRoute $route
  210. * @param DynamicRequestContext|null $context
  211. *
  212. * @return DocumentRoute|null
  213. */
  214. private function handleDirectRouteDocument(
  215. Document\PageSnippet $document,
  216. DocumentRoute $route,
  217. DynamicRequestContext $context = null
  218. ) {
  219. // if we have a request in context, we're currently in match mode (not generating URLs) -> only match when frontend request by admin
  220. try {
  221. $request = $context ? $context->getRequest() : $this->requestHelper->getMainRequest();
  222. $isAdminRequest = $this->requestHelper->isFrontendRequestByAdmin($request);
  223. } catch (\LogicException $e) {
  224. // catch logic exception here - when the exception fires, it is no admin request
  225. $isAdminRequest = false;
  226. }
  227. // abort if document is not published and the request is no admin request
  228. // and matching unpublished documents was not forced
  229. if (!$document->isPublished()) {
  230. if (!($isAdminRequest || $this->forceHandleUnpublishedDocuments)) {
  231. return null;
  232. }
  233. }
  234. if (!$isAdminRequest && null !== $context) {
  235. // check for redirects (pretty URL, SEO) when not in admin mode and while matching (not generating route)
  236. if ($redirectRoute = $this->handleDirectRouteRedirect($document, $route, $context)) {
  237. return $redirectRoute;
  238. }
  239. // set static page context
  240. if ($document instanceof Page && $document->getStaticGeneratorEnabled()) {
  241. $this->staticPageResolver->setStaticPageContext($context->getRequest());
  242. }
  243. }
  244. // Use latest version, if available, when the request is admin request
  245. // so then route should be built based on latest Document settings
  246. // https://github.com/pimcore/pimcore/issues/9644
  247. if ($isAdminRequest) {
  248. $latestVersion = $document->getLatestVersion();
  249. if ($latestVersion) {
  250. $latestDoc = $latestVersion->loadData();
  251. if ($latestDoc instanceof Document\PageSnippet) {
  252. $document = $latestDoc;
  253. }
  254. }
  255. }
  256. return $this->buildRouteForPageSnippetDocument($document, $route);
  257. }
  258. /**
  259. * Handle document redirects (pretty url, SEO without trailing slash)
  260. *
  261. * @param Document\PageSnippet $document
  262. * @param DocumentRoute $route
  263. * @param DynamicRequestContext|null $context
  264. *
  265. * @return DocumentRoute|null
  266. */
  267. private function handleDirectRouteRedirect(
  268. Document\PageSnippet $document,
  269. DocumentRoute $route,
  270. DynamicRequestContext $context = null
  271. ) {
  272. $redirectTargetUrl = $context->getOriginalPath();
  273. // check for a pretty url, and if the document is called by that, otherwise redirect to pretty url
  274. if ($document instanceof Document\Page && !$document instanceof Document\Hardlink\Wrapper\WrapperInterface) {
  275. if ($prettyUrl = $document->getPrettyUrl()) {
  276. if (rtrim(strtolower($prettyUrl), ' /') !== rtrim(strtolower($context->getOriginalPath()), '/')) {
  277. $redirectTargetUrl = $prettyUrl;
  278. }
  279. }
  280. }
  281. // check for a trailing slash in path, if exists, redirect to this page without the slash
  282. // the only reason for this is: SEO, Analytics, ... there is no system specific reason, pimcore would work also with a trailing slash without problems
  283. // use $originalPath because of the sites
  284. // only do redirecting with GET requests
  285. if ($context->getRequest()->getMethod() === 'GET') {
  286. if (($this->config['documents']['allow_trailing_slash'] ?? null) === 'no') {
  287. if ($redirectTargetUrl !== '/' && substr($redirectTargetUrl, -1) === '/') {
  288. $redirectTargetUrl = rtrim($redirectTargetUrl, '/');
  289. }
  290. }
  291. // only allow the original key of a document to be the URL (lowercase/uppercase)
  292. if ($redirectTargetUrl !== '/' && rtrim($redirectTargetUrl, '/') !== rawurldecode($document->getFullPath())) {
  293. $redirectTargetUrl = $document->getFullPath();
  294. }
  295. }
  296. if (null !== $redirectTargetUrl && $redirectTargetUrl !== $context->getOriginalPath()) {
  297. $route->setDefault('_controller', 'Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction');
  298. $route->setDefault('path', $redirectTargetUrl);
  299. $route->setDefault('permanent', true);
  300. return $route;
  301. }
  302. return null;
  303. }
  304. /**
  305. * Handle page snippet route (controller, action, view)
  306. *
  307. * @param Document\PageSnippet $document
  308. * @param DocumentRoute $route
  309. *
  310. * @return DocumentRoute
  311. */
  312. private function buildRouteForPageSnippetDocument(Document\PageSnippet $document, DocumentRoute $route)
  313. {
  314. $route->setDefault('_controller', $document->getController());
  315. if ($document->getTemplate()) {
  316. $route->setDefault('_template', $document->getTemplate());
  317. }
  318. return $route;
  319. }
  320. /**
  321. * Check if document is can be used to generate a route
  322. *
  323. * @param Document|null $document
  324. *
  325. * @return bool
  326. */
  327. private function isDirectRouteDocument($document)
  328. {
  329. if ($document instanceof Document\PageSnippet) {
  330. if (in_array($document->getType(), $this->getDirectRouteDocumentTypes())) {
  331. return true;
  332. }
  333. }
  334. return false;
  335. }
  336. }