vendor/ibexa/core/src/lib/MVC/Symfony/Routing/UrlAliasRouter.php line 255

Open in your IDE?
  1. <?php
  2. /**
  3.  * @copyright Copyright (C) Ibexa AS. All rights reserved.
  4.  * @license For full copyright and license information view LICENSE file distributed with this source code.
  5.  */
  6. namespace Ibexa\Core\MVC\Symfony\Routing;
  7. use Ibexa\Contracts\Core\Repository\ContentService;
  8. use Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException;
  9. use Ibexa\Contracts\Core\Repository\LocationService;
  10. use Ibexa\Contracts\Core\Repository\URLAliasService;
  11. use Ibexa\Contracts\Core\Repository\Values\Content\Location;
  12. use Ibexa\Contracts\Core\Repository\Values\Content\URLAlias;
  13. use Ibexa\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator;
  14. use Ibexa\Core\MVC\Symfony\View\Manager as ViewManager;
  15. use InvalidArgumentException;
  16. use LogicException;
  17. use Psr\Log\LoggerInterface;
  18. use Symfony\Cmf\Component\Routing\ChainedRouterInterface;
  19. use Symfony\Cmf\Component\Routing\RouteObjectInterface;
  20. use Symfony\Component\HttpFoundation\Request;
  21. use Symfony\Component\Routing\Exception\ResourceNotFoundException;
  22. use Symfony\Component\Routing\Exception\RouteNotFoundException;
  23. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  24. use Symfony\Component\Routing\Matcher\RequestMatcherInterface;
  25. use Symfony\Component\Routing\RequestContext;
  26. use Symfony\Component\Routing\Route as SymfonyRoute;
  27. use Symfony\Component\Routing\RouteCollection;
  28. class UrlAliasRouter implements ChainedRouterInterfaceRequestMatcherInterface
  29. {
  30.     public const URL_ALIAS_ROUTE_NAME 'ibexa.url.alias';
  31.     public const VIEW_ACTION 'ibexa_content::viewAction';
  32.     /** @var \Symfony\Component\Routing\RequestContext */
  33.     protected $requestContext;
  34.     /** @var \Ibexa\Contracts\Core\Repository\LocationService */
  35.     protected $locationService;
  36.     /** @var \Ibexa\Contracts\Core\Repository\URLAliasService */
  37.     protected $urlAliasService;
  38.     /** @var \Ibexa\Contracts\Core\Repository\ContentService */
  39.     protected $contentService;
  40.     /** @var \Ibexa\Core\MVC\Symfony\Routing\Generator\UrlAliasGenerator */
  41.     protected $generator;
  42.     /**
  43.      * Holds current root Location id.
  44.      *
  45.      * @var int|string
  46.      */
  47.     protected $rootLocationId;
  48.     /** @var \Psr\Log\LoggerInterface */
  49.     protected $logger;
  50.     public function __construct(
  51.         LocationService $locationService,
  52.         URLAliasService $urlAliasService,
  53.         ContentService $contentService,
  54.         UrlAliasGenerator $generator,
  55.         RequestContext $requestContext,
  56.         LoggerInterface $logger null
  57.     ) {
  58.         $this->locationService $locationService;
  59.         $this->urlAliasService $urlAliasService;
  60.         $this->contentService $contentService;
  61.         $this->generator $generator;
  62.         $this->requestContext $requestContext !== null $requestContext : new RequestContext();
  63.         $this->logger $logger;
  64.     }
  65.     /**
  66.      * Injects current root Location id.
  67.      *
  68.      * @param int|string $rootLocationId
  69.      */
  70.     public function setRootLocationId($rootLocationId)
  71.     {
  72.         $this->rootLocationId $rootLocationId;
  73.     }
  74.     /**
  75.      * Tries to match a request with a set of routes.
  76.      *
  77.      * If the matcher can not find information, it must throw one of the exceptions documented
  78.      * below.
  79.      *
  80.      * @param \Symfony\Component\HttpFoundation\Request $request The request to match
  81.      *
  82.      * @return array An array of parameters
  83.      *
  84.      * @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no matching resource could be found
  85.      */
  86.     public function matchRequest(Request $request)
  87.     {
  88.         try {
  89.             $requestedPath $request->attributes->get('semanticPathinfo'$request->getPathInfo());
  90.             $urlAlias $this->getUrlAlias($requestedPath);
  91.             if ($this->rootLocationId === null) {
  92.                 $pathPrefix '/';
  93.             } else {
  94.                 $pathPrefix $this->generator->getPathPrefixByRootLocationId($this->rootLocationId);
  95.             }
  96.             $params = [
  97.                 '_route' => self::URL_ALIAS_ROUTE_NAME,
  98.             ];
  99.             switch ($urlAlias->type) {
  100.                 case URLAlias::LOCATION:
  101.                     $location $this->generator->loadLocation($urlAlias->destination);
  102.                     $params += [
  103.                         '_controller' => static::VIEW_ACTION,
  104.                         'contentId' => $location->contentId,
  105.                         'locationId' => $urlAlias->destination,
  106.                         'viewType' => ViewManager::VIEW_TYPE_FULL,
  107.                         'layout' => true,
  108.                     ];
  109.                     // For Location alias setup 301 redirect to Location's current URL when:
  110.                     // 1. alias is history
  111.                     // 2. alias is custom with forward flag true
  112.                     // 3. requested URL is not case-sensitive equal with the one loaded
  113.                     if ($urlAlias->isHistory === true || ($urlAlias->isCustom === true && $urlAlias->forward === true)) {
  114.                         $params += [
  115.                             'semanticPathinfo' => $this->generator->generate($location, []),
  116.                             'needsRedirect' => true,
  117.                             // Specify not to prepend siteaccess while redirecting when applicable since it would be already present (see UrlAliasGenerator::doGenerate())
  118.                             'prependSiteaccessOnRedirect' => false,
  119.                         ];
  120.                     } elseif ($this->needsCaseRedirect($urlAlias$requestedPath$pathPrefix)) {
  121.                         $params += [
  122.                             'semanticPathinfo' => $this->removePathPrefix($urlAlias->path$pathPrefix),
  123.                             'needsRedirect' => true,
  124.                         ];
  125.                         if ($urlAlias->destination instanceof Location) {
  126.                             $params += ['locationId' => $urlAlias->destination->id];
  127.                         }
  128.                     }
  129.                     if (isset($this->logger)) {
  130.                         $this->logger->info("UrlAlias matched location #{$urlAlias->destination}. Forwarding to ViewController");
  131.                     }
  132.                     break;
  133.                 case URLAlias::RESOURCE:
  134.                     // In URLAlias terms, "forward" means "redirect".
  135.                     if ($urlAlias->forward) {
  136.                         $params += [
  137.                             'semanticPathinfo' => '/' trim($urlAlias->destination'/'),
  138.                             'needsRedirect' => true,
  139.                         ];
  140.                     } elseif ($this->needsCaseRedirect($urlAlias$requestedPath$pathPrefix)) {
  141.                         // Handle case-correction redirect
  142.                         $params += [
  143.                             'semanticPathinfo' => $this->removePathPrefix($urlAlias->path$pathPrefix),
  144.                             'needsRedirect' => true,
  145.                         ];
  146.                     } else {
  147.                         $params += [
  148.                             'semanticPathinfo' => '/' trim($urlAlias->destination'/'),
  149.                             'needsForward' => true,
  150.                         ];
  151.                     }
  152.                     break;
  153.                 case URLAlias::VIRTUAL:
  154.                     // Handle case-correction redirect
  155.                     if ($this->needsCaseRedirect($urlAlias$requestedPath$pathPrefix)) {
  156.                         $params += [
  157.                             'semanticPathinfo' => $this->removePathPrefix($urlAlias->path$pathPrefix),
  158.                             'needsRedirect' => true,
  159.                         ];
  160.                     } else {
  161.                         // Virtual aliases should load the Content at homepage URL
  162.                         $params += [
  163.                             'semanticPathinfo' => '/',
  164.                             'needsForward' => true,
  165.                         ];
  166.                     }
  167.                     break;
  168.             }
  169.             return $params;
  170.         } catch (NotFoundException $e) {
  171.             throw new ResourceNotFoundException($e->getMessage(), $e->getCode(), $e);
  172.         }
  173.     }
  174.     /**
  175.      * Removes prefix from path.
  176.      *
  177.      * Checks for presence of $prefix and removes it from $path if found.
  178.      *
  179.      * @param string $path
  180.      * @param string $prefix
  181.      *
  182.      * @return string
  183.      */
  184.     protected function removePathPrefix($path$prefix)
  185.     {
  186.         if ($prefix !== '/' && mb_stripos($path$prefix) === 0) {
  187.             $path mb_substr($pathmb_strlen($prefix));
  188.         }
  189.         return $path;
  190.     }
  191.     /**
  192.      * Returns true of false on comparing $urlAlias->path and $path with case sensitivity.
  193.      *
  194.      * Used to determine if redirect is needed because requested path is case-different
  195.      * from the stored one.
  196.      *
  197.      * @param \Ibexa\Contracts\Core\Repository\Values\Content\URLAlias $loadedUrlAlias
  198.      * @param string $requestedPath
  199.      * @param string $pathPrefix
  200.      *
  201.      * @return bool
  202.      */
  203.     protected function needsCaseRedirect(URLAlias $loadedUrlAlias$requestedPath$pathPrefix)
  204.     {
  205.         // If requested path is excluded from tree root jail, compare it to loaded UrlAlias directly.
  206.         if ($this->generator->isUriPrefixExcluded($requestedPath)) {
  207.             return strcmp($loadedUrlAlias->path$requestedPath) !== 0;
  208.         }
  209.         // Compare loaded UrlAlias with requested path, prefixed with configured path prefix.
  210.         return
  211.             strcmp(
  212.                 $loadedUrlAlias->path,
  213.                 $pathPrefix . ($pathPrefix === '/' trim($requestedPath'/') : rtrim($requestedPath'/'))
  214.             ) !== 0
  215.         ;
  216.     }
  217.     /**
  218.      * Returns the UrlAlias object to use, starting from the request.
  219.      *
  220.      * @param $pathinfo
  221.      *
  222.      * @throws \Ibexa\Contracts\Core\Repository\Exceptions\NotFoundException if the path does not exist or is not valid for the given language
  223.      *
  224.      * @return \Ibexa\Contracts\Core\Repository\Values\Content\URLAlias
  225.      */
  226.     protected function getUrlAlias($pathinfo)
  227.     {
  228.         return $this->urlAliasService->lookup($pathinfo);
  229.     }
  230.     /**
  231.      * Gets the RouteCollection instance associated with this Router.
  232.      *
  233.      * @return \Symfony\Component\Routing\RouteCollection A RouteCollection instance
  234.      */
  235.     public function getRouteCollection()
  236.     {
  237.         return new RouteCollection();
  238.     }
  239.     /**
  240.      * Generates a URL for a location, from the given parameters.
  241.      *
  242.      * It is possible to directly pass a Location object as the route name, as the ChainRouter allows it through ChainedRouterInterface.
  243.      *
  244.      * If $name is a route name, the "location" key in $parameters must be set to a valid {@see \Ibexa\Contracts\Core\Repository\Values\Content\Location} object.
  245.      * "locationId" can also be provided.
  246.      *
  247.      * If the generator is not able to generate the url, it must throw the RouteNotFoundException
  248.      * as documented below.
  249.      *
  250.      * @see UrlAliasRouter::supports()
  251.      *
  252.      * @param string $name The name of the route or a Location instance
  253.      * @param array $parameters An array of parameters
  254.      * @param int $referenceType The type of reference to be generated (one of the constants)
  255.      *
  256.      * @throws \LogicException
  257.      * @throws \Symfony\Component\Routing\Exception\RouteNotFoundException
  258.      * @throws \InvalidArgumentException
  259.      *
  260.      * @return string The generated URL
  261.      *
  262.      * @api
  263.      */
  264.     public function generate(string $name, array $parameters = [], int $referenceType UrlGeneratorInterface::ABSOLUTE_PATH): string
  265.     {
  266.         if ($name === '' &&
  267.             array_key_exists(RouteObjectInterface::ROUTE_OBJECT$parameters) &&
  268.             $this->supportsObject($parameters[RouteObjectInterface::ROUTE_OBJECT])
  269.         ) {
  270.             $location $parameters[RouteObjectInterface::ROUTE_OBJECT];
  271.             unset($parameters[RouteObjectInterface::ROUTE_OBJECT]);
  272.             return $this->generator->generate($location$parameters$referenceType);
  273.         }
  274.         // Normal route name
  275.         if ($name === self::URL_ALIAS_ROUTE_NAME) {
  276.             if (isset($parameters['location']) || isset($parameters['locationId'])) {
  277.                 // Check if location is a valid Location object
  278.                 if (isset($parameters['location']) && !$parameters['location'] instanceof Location) {
  279.                     throw new LogicException(
  280.                         "When generating a UrlAlias route, the 'location' parameter must be a valid " Location::class . '.'
  281.                     );
  282.                 }
  283.                 $location = isset($parameters['location']) ? $parameters['location'] : $this->locationService->loadLocation($parameters['locationId']);
  284.                 unset($parameters['location'], $parameters['locationId'], $parameters['viewType'], $parameters['layout']);
  285.                 return $this->generator->generate($location$parameters$referenceType);
  286.             }
  287.             if (isset($parameters['contentId'])) {
  288.                 $contentInfo $this->contentService->loadContentInfo($parameters['contentId']);
  289.                 unset($parameters['contentId'], $parameters['viewType'], $parameters['layout']);
  290.                 if (empty($contentInfo->mainLocationId)) {
  291.                     throw new LogicException('Cannot generate a UrlAlias route for content without main Location.');
  292.                 }
  293.                 return $this->generator->generate(
  294.                     $this->locationService->loadLocation($contentInfo->mainLocationId),
  295.                     $parameters,
  296.                     $referenceType
  297.                 );
  298.             }
  299.             throw new InvalidArgumentException(
  300.                 "When generating a UrlAlias route, either 'location', 'locationId', or 'contentId' must be provided."
  301.             );
  302.         }
  303.         throw new RouteNotFoundException('Could not match route');
  304.     }
  305.     public function setContext(RequestContext $context)
  306.     {
  307.         $this->requestContext $context;
  308.         $this->generator->setRequestContext($context);
  309.     }
  310.     public function getContext()
  311.     {
  312.         return $this->requestContext;
  313.     }
  314.     /**
  315.      * Not supported. Please use matchRequest() instead.
  316.      *
  317.      * @param $pathinfo
  318.      *
  319.      * @throws \RuntimeException
  320.      */
  321.     public function match($pathinfo)
  322.     {
  323.         throw new \RuntimeException("The UrlAliasRouter doesn't support the match() method. Use matchRequest() instead.");
  324.     }
  325.     /**
  326.      * Whether the router supports the thing in $name to generate a route.
  327.      *
  328.      * This check does not need to look if the specific instance can be
  329.      * resolved to a route, only whether the router can generate routes from
  330.      * objects of this class.
  331.      *
  332.      * @param mixed $name The route name or route object
  333.      *
  334.      * @return bool
  335.      */
  336.     public function supports($name)
  337.     {
  338.         return $name === self::URL_ALIAS_ROUTE_NAME || $this->supportsObject($name);
  339.     }
  340.     private function supportsObject($object): bool
  341.     {
  342.         return $object instanceof Location;
  343.     }
  344.     /**
  345.      * @see \Symfony\Cmf\Component\Routing\VersatileGeneratorInterface::getRouteDebugMessage()
  346.      */
  347.     public function getRouteDebugMessage($name, array $parameters = [])
  348.     {
  349.         if ($name instanceof RouteObjectInterface) {
  350.             return 'Route with key ' $name->getRouteKey();
  351.         }
  352.         if ($name instanceof SymfonyRoute) {
  353.             return 'Route with pattern ' $name->getPath();
  354.         }
  355.         return $name;
  356.     }
  357. }
  358. class_alias(UrlAliasRouter::class, 'eZ\Publish\Core\MVC\Symfony\Routing\UrlAliasRouter');