vendor/ezsystems/ezpublish-kernel/eZ/Publish/Core/MVC/Symfony/Routing/UrlAliasRouter.php line 112

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