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

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