vendor/sulu/sulu/src/Sulu/Bundle/WebsiteBundle/Routing/ContentRouteProvider.php line 208

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Sulu.
  4. *
  5. * (c) Sulu GmbH
  6. *
  7. * This source file is subject to the MIT license that is bundled
  8. * with this source code in the file LICENSE.
  9. */
  10. namespace Sulu\Bundle\WebsiteBundle\Routing;
  11. use PHPCR\RepositoryException;
  12. use Sulu\Bundle\DocumentManagerBundle\Bridge\DocumentInspector;
  13. use Sulu\Bundle\PageBundle\Document\PageDocument;
  14. use Sulu\Component\Content\Compat\Structure\PageBridge;
  15. use Sulu\Component\Content\Compat\StructureManagerInterface;
  16. use Sulu\Component\Content\Document\Behavior\ExtensionBehavior;
  17. use Sulu\Component\Content\Document\Behavior\ResourceSegmentBehavior;
  18. use Sulu\Component\Content\Document\Behavior\WebspaceBehavior;
  19. use Sulu\Component\Content\Document\RedirectType;
  20. use Sulu\Component\Content\Exception\ResourceLocatorMovedException;
  21. use Sulu\Component\Content\Exception\ResourceLocatorNotFoundException;
  22. use Sulu\Component\Content\Types\ResourceLocator\Strategy\ResourceLocatorStrategyPoolInterface;
  23. use Sulu\Component\DocumentManager\DocumentManagerInterface;
  24. use Sulu\Component\Security\Authorization\SecurityCheckerInterface;
  25. use Sulu\Component\Webspace\Analyzer\Attributes\RequestAttributes;
  26. use Sulu\Component\Webspace\Analyzer\RequestAnalyzerInterface;
  27. use Sulu\Component\Webspace\Manager\WebspaceManagerInterface;
  28. use Symfony\Cmf\Component\Routing\RouteProviderInterface;
  29. use Symfony\Component\HttpFoundation\Request;
  30. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  31. use Symfony\Component\Routing\Route;
  32. use Symfony\Component\Routing\RouteCollection;
  33. use Webmozart\Assert\Assert;
  34. /**
  35. * The PortalRouteProvider should load the dynamic routes created by Sulu.
  36. */
  37. class ContentRouteProvider implements RouteProviderInterface
  38. {
  39. /**
  40. * @var DocumentManagerInterface
  41. */
  42. private $documentManager;
  43. /**
  44. * @var DocumentInspector
  45. */
  46. private $documentInspector;
  47. /**
  48. * @var ResourceLocatorStrategyPoolInterface
  49. */
  50. private $resourceLocatorStrategyPool;
  51. /**
  52. * @var StructureManagerInterface
  53. */
  54. private $structureManager;
  55. /**
  56. * @var WebspaceManagerInterface
  57. */
  58. private $webspaceManager;
  59. /**
  60. * @var RequestAnalyzerInterface
  61. */
  62. private $requestAnalyzer;
  63. /**
  64. * @var SecurityCheckerInterface|null
  65. */
  66. private $securityChecker;
  67. /**
  68. * @var array
  69. */
  70. private $defaultOptions;
  71. public function __construct(
  72. DocumentManagerInterface $documentManager,
  73. DocumentInspector $documentInspector,
  74. ResourceLocatorStrategyPoolInterface $resourceLocatorStrategyPool,
  75. StructureManagerInterface $structureManager,
  76. WebspaceManagerInterface $webspaceManager,
  77. RequestAnalyzerInterface $requestAnalyzer,
  78. ?SecurityCheckerInterface $securityChecker = null,
  79. array $defaultOptions = []
  80. ) {
  81. $this->documentManager = $documentManager;
  82. $this->documentInspector = $documentInspector;
  83. $this->resourceLocatorStrategyPool = $resourceLocatorStrategyPool;
  84. $this->structureManager = $structureManager;
  85. $this->webspaceManager = $webspaceManager;
  86. $this->requestAnalyzer = $requestAnalyzer;
  87. $this->securityChecker = $securityChecker;
  88. Assert::null($securityChecker, 'The security checker should be called by the SecurityListener not the ContentRouteProvider.'); // people who overwrite the ContentRouteProvider should make aware of that they also need to refactor this
  89. $this->defaultOptions = $defaultOptions;
  90. }
  91. public function getRouteCollectionForRequest(Request $request): RouteCollection
  92. {
  93. $collection = new RouteCollection();
  94. if ('' === $request->getRequestFormat()) {
  95. return $collection;
  96. }
  97. /** @var RequestAttributes $attributes */
  98. $attributes = $request->attributes->get('_sulu');
  99. if (!$attributes) {
  100. return $collection;
  101. }
  102. $matchType = $attributes->getAttribute('matchType');
  103. // no portal information without localization supported
  104. if (null === $attributes->getAttribute('localization')
  105. && RequestAnalyzerInterface::MATCH_TYPE_PARTIAL !== $matchType
  106. && RequestAnalyzerInterface::MATCH_TYPE_REDIRECT !== $matchType
  107. ) {
  108. return $collection;
  109. }
  110. $resourceLocator = $this->decodePathInfo($attributes->getAttribute('resourceLocator'));
  111. $prefix = $attributes->getAttribute('resourceLocatorPrefix');
  112. $pathInfo = $this->decodePathInfo($request->getPathInfo());
  113. $htmlRedirect = $pathInfo !== $prefix . $resourceLocator
  114. && \in_array($request->getRequestFormat(), ['htm', 'html']);
  115. if ($htmlRedirect
  116. || RequestAnalyzerInterface::MATCH_TYPE_REDIRECT == $matchType
  117. || RequestAnalyzerInterface::MATCH_TYPE_PARTIAL == $matchType
  118. ) {
  119. return $collection;
  120. }
  121. // just show the page
  122. $portal = $attributes->getAttribute('portal');
  123. $locale = $attributes->getAttribute('localization')->getLocale();
  124. $resourceLocatorStrategy = $this->resourceLocatorStrategyPool->getStrategyByWebspaceKey(
  125. $portal->getWebspace()->getKey()
  126. );
  127. try {
  128. // load content by url ignore ending trailing slash
  129. /** @var PageDocument $document */
  130. $document = $this->documentManager->find(
  131. $resourceLocatorStrategy->loadByResourceLocator(
  132. \rtrim($resourceLocator, '/'),
  133. $portal->getWebspace()->getKey(),
  134. $locale
  135. ),
  136. $locale,
  137. [
  138. 'load_ghost_content' => false,
  139. ]
  140. );
  141. if (!$document->getTitle()) {
  142. // If the title is empty the document does not exist in this locale
  143. // Necessary because of https://github.com/sulu/sulu/issues/2724, otherwise locale could be checked
  144. return $collection;
  145. }
  146. if (\preg_match('/\/$/', $resourceLocator) && ('/' !== $resourceLocator || $prefix)) {
  147. // redirect page to page without slash at the end
  148. $url = $prefix . \rtrim($resourceLocator, '/');
  149. if ($request->getQueryString()) {
  150. $url .= '?' . $request->getQueryString();
  151. }
  152. $collection->add('redirect_' . \uniqid(), $this->getRedirectRoute($request, $url));
  153. } elseif (RedirectType::INTERNAL === $document->getRedirectType()) {
  154. $redirectTarget = $document->getRedirectTarget();
  155. if (!$redirectTarget instanceof ResourceSegmentBehavior || !$redirectTarget instanceof WebspaceBehavior) {
  156. return $collection;
  157. }
  158. $redirectUrl = $this->webspaceManager->findUrlByResourceLocator(
  159. $redirectTarget->getResourceSegment(),
  160. null,
  161. $document->getLocale(),
  162. $redirectTarget->getWebspaceName()
  163. );
  164. if ($request->getQueryString()) {
  165. $redirectUrl .= '?' . $request->getQueryString();
  166. }
  167. $collection->add(
  168. $document->getStructureType() . '_' . $document->getUuid(),
  169. $this->getRedirectRoute($request, $redirectUrl)
  170. );
  171. } elseif (RedirectType::EXTERNAL === $document->getRedirectType()) {
  172. $collection->add(
  173. $document->getStructureType() . '_' . $document->getUuid(),
  174. $this->getRedirectRoute($request, $document->getRedirectExternal())
  175. );
  176. } elseif (!$this->checkResourceLocator($resourceLocator, $prefix)) {
  177. return $collection;
  178. } else {
  179. if ($document instanceof ExtensionBehavior) {
  180. $documentSegments = $document->getExtensionsData()['excerpt']['segments'] ?? [];
  181. $documentSegmentKey = $documentSegments[$portal->getWebspace()->getKey()] ?? null;
  182. $segment = $this->requestAnalyzer->getSegment();
  183. if ($segment && $documentSegmentKey && $segment->getKey() !== $documentSegmentKey) {
  184. $this->requestAnalyzer->changeSegment($documentSegmentKey);
  185. }
  186. }
  187. // convert the page to a StructureBridge because of BC
  188. $metadata = $this->documentInspector->getStructureMetadata($document);
  189. if (!$metadata) {
  190. return $collection;
  191. }
  192. /** @var PageBridge $structure */
  193. $structure = $this->structureManager->wrapStructure(
  194. $this->documentInspector->getMetadata($document)->getAlias(),
  195. $metadata
  196. );
  197. $structure->setDocument($document);
  198. // show the page
  199. $collection->add(
  200. $document->getStructureType() . '_' . $document->getUuid(),
  201. $this->getStructureRoute($request, $structure)
  202. );
  203. }
  204. } catch (ResourceLocatorNotFoundException $exc) {
  205. // just do not add any routes to the collection
  206. } catch (ResourceLocatorMovedException $exc) {
  207. $url = $prefix . $exc->getNewResourceLocator();
  208. if ($request->getQueryString()) {
  209. $url .= '?' . $request->getQueryString();
  210. }
  211. // old url resource was moved
  212. $collection->add(
  213. $exc->getNewResourceLocatorUuid() . '_' . \uniqid(),
  214. $this->getRedirectRoute($request, $url)
  215. );
  216. } catch (RepositoryException $exc) {
  217. // just do not add any routes to the collection
  218. }
  219. return $collection;
  220. }
  221. /**
  222. * @param string $name
  223. */
  224. public function getRouteByName($name): Route
  225. {
  226. throw new RouteNotFoundException();
  227. }
  228. public function getRoutesByNames($names = null): iterable
  229. {
  230. return [];
  231. }
  232. /**
  233. * Checks if the resource locator is valid.
  234. * A resource locator with a slash only is not allowed, the only exception is when it is a single language
  235. * website, where the browser automatically adds the slash.
  236. *
  237. * @param string $resourceLocator
  238. * @param string $resourceLocatorPrefix
  239. *
  240. * @return bool
  241. */
  242. private function checkResourceLocator($resourceLocator, $resourceLocatorPrefix)
  243. {
  244. return !('/' === $resourceLocator && $resourceLocatorPrefix);
  245. }
  246. /**
  247. * @param string $url
  248. *
  249. * @return Route
  250. */
  251. protected function getRedirectRoute(Request $request, $url)
  252. {
  253. $requestFormat = $request->getRequestFormat(null);
  254. $formatSuffix = $requestFormat ? '.' . $requestFormat : '';
  255. $urlParts = \explode('?', $url, 2);
  256. $url = $urlParts[0] . $formatSuffix;
  257. if ($urlParts[1] ?? null) {
  258. $url .= '?' . $urlParts[1];
  259. }
  260. // redirect to linked page
  261. return new Route(
  262. $this->decodePathInfo($request->getPathInfo()),
  263. [
  264. '_controller' => 'sulu_website.redirect_controller::redirectAction',
  265. 'url' => $url,
  266. ],
  267. [],
  268. $this->defaultOptions
  269. );
  270. }
  271. /**
  272. * @return Route
  273. */
  274. protected function getStructureRoute(Request $request, PageBridge $content)
  275. {
  276. return new Route(
  277. $this->decodePathInfo($request->getPathInfo()),
  278. [
  279. '_controller' => $content->getController(),
  280. 'structure' => $content,
  281. 'partial' => 'true' === $request->get('partial', 'false'),
  282. ],
  283. [],
  284. $this->defaultOptions
  285. );
  286. }
  287. /**
  288. * Server encodes the url and symfony does not encode it
  289. * Symfony decodes this data here https://github.com/symfony/symfony/blob/3.3/src/Symfony/Component/Routing/Matcher/UrlMatcher.php#L91.
  290. *
  291. * @param string $pathInfo
  292. *
  293. * @return string
  294. */
  295. private function decodePathInfo($pathInfo)
  296. {
  297. if (null === $pathInfo || '' === $pathInfo) {
  298. return '';
  299. }
  300. return '/' . \ltrim(\rawurldecode($pathInfo), '/');
  301. }
  302. }