Skip to content

Commit

Permalink
Merged branch '4.5'
Browse files Browse the repository at this point in the history
  • Loading branch information
alongosz committed Aug 4, 2023
2 parents d201149 + e95fbc8 commit c02f77e
Show file tree
Hide file tree
Showing 9 changed files with 501 additions and 189 deletions.
37 changes: 25 additions & 12 deletions src/bundle/EventListener/RequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,39 @@
*/
namespace Ibexa\Bundle\Rest\EventListener;

use Ibexa\Bundle\Rest\UriParser\UriParser;
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
* @internal
*
* REST request listener.
*
* Flags a REST request as such using the is_rest_request attribute.
*/
class RequestListener implements EventSubscriberInterface
{
public const REST_PREFIX_PATTERN = '/^\/api\/[a-zA-Z0-9-_]+\/v\d+(\.\d+)?\//';
/**
* @deprecated rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead.
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
*/
public const REST_PREFIX_PATTERN = UriParser::DEFAULT_REST_PREFIX_PATTERN;

private UriParserInterface $uriParser;

public function __construct(UriParserInterface $uriParser)
{
$this->uriParser = $uriParser;
}

/**
* @return array
*/
public static function getSubscribedEvents()
public static function getSubscribedEvents(): array
{
return [
// 10001 is to ensure that REST requests are tagged before CorsListener is called
Expand All @@ -33,24 +48,22 @@ public static function getSubscribedEvents()

/**
* If the request is a REST one, sets the is_rest_request request attribute.
*
* @param \Symfony\Component\HttpKernel\Event\RequestEvent $event
*/
public function onKernelRequest(RequestEvent $event)
public function onKernelRequest(RequestEvent $event): void
{
$isRestRequest = true;

if (!$this->hasRestPrefix($event->getRequest())) {
$isRestRequest = false;
}

$event->getRequest()->attributes->set('is_rest_request', $isRestRequest);
$event->getRequest()->attributes->set(
'is_rest_request',
$this->uriParser->isRestRequest($event->getRequest())
);
}

/**
* @param \Symfony\Component\HttpFoundation\Request $request
*
* @return bool
*
* @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest instead
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
*/
protected function hasRestPrefix(Request $request)
{
Expand Down
74 changes: 16 additions & 58 deletions src/bundle/RequestParser/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,55 +6,37 @@
*/
namespace Ibexa\Bundle\Rest\RequestParser;

use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException;
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
use Ibexa\Rest\RequestParser;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\RouterInterface;

/**
* @deprecated use \Ibexa\Contracts\Rest\UriParser\UriParserInterface instead
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface
*
* Router based request parser.
*/
class Router implements RequestParser
{
/**
* @var \Symfony\Cmf\Component\Routing\ChainRouter
*/
private $router;
private RouterInterface $router;

private UriParserInterface $uriParser;

public function __construct(RouterInterface $router)
public function __construct(RouterInterface $router, UriParserInterface $uriParser)
{
$this->router = $router;
$this->uriParser = $uriParser;
}

/**
* @throws \Symfony\Component\Routing\Exception\ResourceNotFoundException If no match was found
* @return array<mixed> matched route configuration and parameters
*
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException If no match was found
*/
public function parse($url)
public function parse($url): array
{
// we create a request with a new context in order to match $url to a route and get its properties
$request = Request::create($url, 'GET');
$originalContext = $this->router->getContext();
$context = clone $originalContext;
$context->fromRequest($request);
$this->router->setContext($context);

try {
$matchResult = $this->router->matchRequest($request);
} catch (ResourceNotFoundException $e) {
// Note: this probably won't occur in real life because of the legacy matcher
$this->router->setContext($originalContext);
throw new InvalidArgumentException("No route matched '$url'");
}

if (!$this->matchesRestRequest($matchResult)) {
$this->router->setContext($originalContext);
throw new InvalidArgumentException("No route matched '$url'");
}

$this->router->setContext($originalContext);

return $matchResult;
return $this->uriParser->matchUri($url);
}

public function generate($type, array $values = [])
Expand All @@ -63,35 +45,11 @@ public function generate($type, array $values = [])
}

/**
* @throws \Ibexa\Core\Base\Exceptions\InvalidArgumentException If $attribute wasn't found in the match
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If $attribute wasn't found in the match
*/
public function parseHref($href, $attribute)
{
$parsingResult = $this->parse($href);

if (!isset($parsingResult[$attribute])) {
throw new InvalidArgumentException("No attribute '$attribute' in route matched from $href");
}

return $parsingResult[$attribute];
}

/**
* Checks if a router match response matches a REST resource.
*
* @param array $match Match array returned by Router::match() / Router::matchRequest()
*
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException if the \$match isn't valid
*
* @return bool
*/
private function matchesRestRequest(array $match)
{
if (!isset($match['_route'])) {
throw new InvalidArgumentException('Invalid $match parameter, no _route key');
}

return strpos($match['_route'], 'ibexa.rest.') === 0;
return $this->uriParser->getAttributeFromUri($href, $attribute);
}
}

Expand Down
13 changes: 12 additions & 1 deletion src/bundle/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ parameters:
- '(^application/vnd\.ibexa\.api\.[A-Za-z]+\+xml$)'
- '(^application/xml$)'
- '(^.*/.*$)'
ibexa.rest.path_prefix.pattern: !php/const \Ibexa\Bundle\Rest\UriParser\UriParser::DEFAULT_REST_PREFIX_PATTERN

services:
Ibexa\Bundle\Rest\Serializer\SerializerFactory:
Expand Down Expand Up @@ -43,7 +44,15 @@ services:

Ibexa\Bundle\Rest\RequestParser\Router:
arguments:
- "@router"
$router: '@router'
$uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface'

Ibexa\Contracts\Rest\UriParser\UriParserInterface: '@Ibexa\Bundle\Rest\UriParser\UriParser'

Ibexa\Bundle\Rest\UriParser\UriParser:
arguments:
$urlMatcher: '@Symfony\Component\Routing\Matcher\UrlMatcherInterface'
$restPrefixPattern: '%ibexa.rest.path_prefix.pattern%'

Ibexa\Rest\Input\ParserTools: ~

Expand Down Expand Up @@ -193,6 +202,8 @@ services:
tags: [controller.service_arguments]

Ibexa\Bundle\Rest\EventListener\RequestListener:
arguments:
$uriParser: '@Ibexa\Contracts\Rest\UriParser\UriParserInterface'
tags:
- { name: kernel.event_subscriber }

Expand Down
95 changes: 95 additions & 0 deletions src/bundle/UriParser/UriParser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Rest\UriParser;

use Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException;
use Ibexa\Contracts\Rest\UriParser\UriParserInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Exception\MethodNotAllowedException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;

/**
* @internal
*/
final class UriParser implements UriParserInterface
{
/**
* @internal rely on \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest
* or \Ibexa\Contracts\Rest\UriParser\UriParserInterface::hasRestPrefix instead.
*
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::isRestRequest()
* @see \Ibexa\Contracts\Rest\UriParser\UriParserInterface::hasRestPrefix()
*/
public const DEFAULT_REST_PREFIX_PATTERN = '/^\/api\/[a-zA-Z0-9-_]+\/v\d+(\.\d+)?\//';

private UrlMatcherInterface $urlMatcher;

private string $restPrefixPattern;

public function __construct(
UrlMatcherInterface $urlMatcher,
string $restPrefixPattern = self::DEFAULT_REST_PREFIX_PATTERN
) {
$this->urlMatcher = $urlMatcher;
$this->restPrefixPattern = $restPrefixPattern;
}

public function matchUri(string $uri, string $method = 'GET'): array
{
if (!$this->hasRestPrefix($uri)) {
// keeping the original exception message for BC, otherwise could be more verbose
throw new InvalidArgumentException("No route matched '$uri'");
}

$request = Request::create($uri, $method);

$originalContext = $this->urlMatcher->getContext();
$context = clone $originalContext;
$context->fromRequest($request);
$this->urlMatcher->setContext($context);

try {
return $this->urlMatcher->match($request->getPathInfo());
} catch (MethodNotAllowedException $e) {
// seems MethodNotAllowedException has no message set
$allowedMethods = implode(', ', $e->getAllowedMethods());
throw new InvalidArgumentException(
"Method '$method' is not allowed for '$uri'. Allowed: [$allowedMethods]",
$e->getCode(),
$e
);
} catch (ResourceNotFoundException $e) {
throw new InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
} finally {
$this->urlMatcher->setContext($originalContext);
}
}

public function getAttributeFromUri(string $uri, string $attribute, string $method = 'GET'): string
{
$parsingResult = $this->matchUri($uri, $method);

if (!isset($parsingResult[$attribute])) {
throw new InvalidArgumentException("No attribute '$attribute' in route matched from $uri");
}

return (string)$parsingResult[$attribute];
}

public function isRestRequest(Request $request): bool
{
return $this->hasRestPrefix($request->getPathInfo());
}

public function hasRestPrefix(string $uri): bool
{
return (bool)preg_match($this->restPrefixPattern, $uri);
}
}
32 changes: 32 additions & 0 deletions src/contracts/UriParser/UriParserInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Contracts\Rest\UriParser;

use Symfony\Component\HttpFoundation\Request;

interface UriParserInterface
{
/**
* @throws \Ibexa\Contracts\Core\Repository\Exceptions\InvalidArgumentException If $attribute wasn't found in the matched URI attributes
*/
public function getAttributeFromUri(string $uri, string $attribute, string $method = 'GET'): string;

public function isRestRequest(Request $request): bool;

public function hasRestPrefix(string $uri): bool;

/**
* @internal use getAttributeFromUri
*
* @return array<mixed> matched route configuration and parameters
*
* @throws \Ibexa\Contracts\Rest\Exceptions\InvalidArgumentException
*/
public function matchUri(string $uri, string $method = 'GET'): array;
}
Loading

0 comments on commit c02f77e

Please sign in to comment.