Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Neos.Flow/Classes/Http/ServerRequestAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,6 @@ final class ServerRequestAttributes
* @internal Don't use this. The ActionRequest is supposed to only exist inside the MVC dispatch context.
*/
public const ACTION_REQUEST = 'actionRequest';

public const REQUEST_TAGS = 'requestTags';
}
18 changes: 18 additions & 0 deletions Neos.Flow/Classes/Mvc/ActionRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/

use Neos\Flow\Annotations as Flow;
use Neos\Flow\Http\ServerRequestAttributes;
use Neos\Http\Factories\FlowUploadedFile;
use Psr\Http\Message\ServerRequestInterface as HttpRequestInterface;
use Neos\Flow\ObjectManagement\Exception\UnknownObjectException;
Expand Down Expand Up @@ -105,6 +106,8 @@ class ActionRequest
*/
protected $format = '';

protected RequestTags $requestTags;

/**
* The parent request – either another sub ActionRequest a main ActionRequest or null
* @var ?ActionRequest
Expand All @@ -130,6 +133,7 @@ class ActionRequest
*/
protected function __construct()
{
$this->requestTags = RequestTags::empty();
}

/**
Expand All @@ -139,6 +143,10 @@ protected function __construct()
public static function fromHttpRequest(HttpRequestInterface $request): ActionRequest
{
$mainActionRequest = new ActionRequest();
if ($request->getAttribute(ServerRequestAttributes::REQUEST_TAGS)) {
$mainActionRequest->setRequestTags(RequestTags::fromString($request->getAttribute(ServerRequestAttributes::REQUEST_TAGS)));
}

$mainActionRequest->httpRequest = $request;
return $mainActionRequest;
}
Expand Down Expand Up @@ -650,6 +658,16 @@ public function getFormat(): string
return $this->format;
}

public function getRequestTags(): RequestTags
{
return $this->requestTags;
}

public function setRequestTags(RequestTags $requestTags): void
{
$this->requestTags = $requestTags;
}

/**
* We provide our own __sleep method, where we serialize all properties *except* the parentRequest if it is
* a HTTP request -- as this one contains $_SERVER etc.
Expand Down
91 changes: 91 additions & 0 deletions Neos.Flow/Classes/Mvc/RequestTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php
namespace Neos\Flow\Mvc;

/**
*
*/
final readonly class RequestTag implements \JsonSerializable
{
public const VALUE_PATTERN = '/^[A-Za-z0-9][A-Za-z0-9.:]{0,63}$/';

public const TYPE_PATTERN = '/[A-Za-z0-9]{1,32}/';

public string $value;

public string $type;

/**
* @param string $value
* @param string $type
*/
public function __construct(string $value, string $type)
{
if ($value === '') {
throw new \InvalidArgumentException('RequestTag value cannot be empty', 1745266796);
}

if (strlen($value) > 64) {
throw new \InvalidArgumentException('RequestTag value cannot be longer than 64 characters', 1745266792);
}

if (!preg_match(self::VALUE_PATTERN, $value)) {
throw new \InvalidArgumentException('RequestTag value contains invalid characters, only A-Z, a-z, 0-9, ":" and "." are allowed ', 1745266788);
}

if (strlen($type) > 32) {
throw new \InvalidArgumentException('RequestTag type cannot be longer than 32 characters', 1745266824);
}

if ($type !== '' && !preg_match(self::TYPE_PATTERN, $type)) {
throw new \InvalidArgumentException('RequestTag type contains invalid characters, only A-Z, a-z, 0-9 are allowed.', 1745266895);
}

$this->value = $value;
$this->type = $type;
}

/**
* From serialized "TheType-TheValue.WithDots:AndColons"
*
* @param string $requestTag
* @return self
*/
public static function fromString(string $requestTag): RequestTag
{
$parts = explode('-', $requestTag);

if (count($parts) > 2) {
throw new \InvalidArgumentException('More than one separator found in RequestTag, "-" can only occur once.', 1745267312);
}

if (count($parts) === 2) {
return new self($parts[1], $parts[0]);
}

// untyped
return new self($parts[0], '');
}

public function matches(RequestTag $requestTag): bool
{
return $requestTag->type === $this->type && $requestTag->value === $this->value;
}

public function __toString(): string
{
$result = '';
if ($this->type !== '') {
$result .= $this->type;
$result .= '-';
}

$result .= $this->value;

return $result;
}

public function jsonSerialize(): string
{
return json_encode($this->__toString(), JSON_THROW_ON_ERROR);
}
}
54 changes: 54 additions & 0 deletions Neos.Flow/Classes/Mvc/RequestTags.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php
namespace Neos\Flow\Mvc;

/**
* @implements \IteratorAggregate<RequestTag>
*/
final readonly class RequestTags implements \IteratorAggregate, \Countable
{
/**
* @var RequestTag[]
*/
public array $tags;

/**
*/
public function __construct(RequestTag ...$tags) {
$this->tags = $tags;
}

public static function fromString(string $json): RequestTags
{
$tagsAsString = json_decode($json, false, 512, JSON_THROW_ON_ERROR);
$tags = [];
foreach ($tagsAsString as $tagString) {
$tag = RequestTag::fromString($tagString);
$tags[] = $tag;
}

return new self(...$tags);
}

public static function empty(): RequestTags
{
return new self();
}

public function __toString(): string
{
return json_encode($this->tags, JSON_THROW_ON_ERROR);
}

/**
* @return \Traversable<RequestTag>
*/
public function getIterator(): \Traversable
{
yield from $this->tags;
}

public function count(): int
{
return count($this->tags);
}
}
19 changes: 19 additions & 0 deletions Neos.Flow/Classes/Mvc/Routing/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,11 @@ class Route
*/
protected $httpMethods = [];

/**
* @var array<int, string>
*/
protected array $requestTags = [];

/**
* Indicates whether this route is parsed.
* For better performance, routes are only parsed if needed.
Expand Down Expand Up @@ -204,6 +209,10 @@ public static function fromConfiguration(array $configuration): static
if (isset($configuration['httpMethods'])) {
$route->setHttpMethods($configuration['httpMethods']);
}
if (isset($configuration['requestTags'])) {
$route->setRequestTags($configuration['requestTags']);
}

return $route;
}

Expand Down Expand Up @@ -372,6 +381,16 @@ public function getHttpMethods()
return $this->httpMethods;
}

public function getRequestTags(): array
{
return $this->requestTags;
}

public function setRequestTags(array $requestTags): void
{
$this->requestTags = $requestTags;
}

/**
* Tags for cache items
*
Expand Down
3 changes: 3 additions & 0 deletions Neos.Flow/Classes/Mvc/Routing/Router.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ public function route(RouteContext $routeContext): array
$matchResults = $route->getMatchResults();
$this->routerCachingService->storeMatchResults($routeContext, $matchResults, $route->getMatchedTags(), $route->getMatchedLifetime());
$this->logger->debug(sprintf('Router route(): Route "%s" matched the request "%s (%s)".', $route->getName(), $httpRequest->getUri(), $httpRequest->getMethod()));
if ($route->getRequestTags() !== []) {
$matchResults['@requestTags'] = json_encode($route->getRequestTags(), JSON_THROW_ON_ERROR);
}
return $matchResults;
}
}
Expand Down
5 changes: 5 additions & 0 deletions Neos.Flow/Classes/Mvc/Routing/RoutingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$matchResults['@package'] = $this->packageManager->getCaseSensitivePackageKey($matchResults['@package']);
}

if (isset($matchResults['@requestTags'])) {
$request = $request->withAttribute(ServerRequestAttributes::REQUEST_TAGS, $matchResults['@requestTags']);
unset($matchResults['@requestTags']);
}

return $next->handle($request->withAttribute(ServerRequestAttributes::ROUTING_RESULTS, $matchResults));
}
}
46 changes: 46 additions & 0 deletions Neos.Flow/Classes/Security/RequestPattern/RequestTag.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php
namespace Neos\Flow\Security\RequestPattern;

use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Security\Exception\InvalidRequestPatternException;
use Neos\Flow\Security\RequestPatternInterface;

/**
*
*/
class RequestTag implements RequestPatternInterface
{
/**
* @var array
*/
protected array $options = [];

/**
* @param array{"tag": string} $options
*/
public function __construct(array $options)
{
$this->options = $options;
}

/**
* @param ActionRequest $request
* @return bool
* @throws InvalidRequestPatternException
*/
public function matchRequest(ActionRequest $request): bool
{
if (!isset($this->options["tag"]) || !is_string($this->options["tag"])) {
throw new InvalidRequestPatternException('Needs a "tag" option to match against');
}

$tagToMatch = \Neos\Flow\Mvc\RequestTag::fromString($this->options["tag"]);
foreach ($request->getRequestTags() as $tag) {
if ($tagToMatch->matches($tag)) {
return true;
}
}

return false;
}
}
13 changes: 13 additions & 0 deletions Neos.Flow/Configuration/Testing/Routes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,16 @@
'@controller': 'Standard'
'@action': 'index'
'@format': 'html'

-
name: 'Functional Test: With RouteTags'
uriPattern: 'neos/flow/test/routetags'
defaults:
'@package': 'Neos.Flow'
'@subpackage': 'Tests\Functional\Mvc\Fixtures'
'@controller': 'Standard'
'@action': 'index'
'@format': 'html'
requestTags:
- Security-Neos.Neos:Backend
- Something-Neos.Flow:Testing
22 changes: 22 additions & 0 deletions Neos.Flow/Tests/Functional/Mvc/RoutingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

use GuzzleHttp\Psr7\Uri;
use Neos\Flow\Configuration\ConfigurationManager;
use Neos\Flow\Http\ServerRequestAttributes;
use Neos\Flow\Mvc\ActionRequest;
use Neos\Flow\Mvc\Exception\NoMatchingRouteException;
use Neos\Flow\Mvc\Routing\Dto\RouteParameters;
Expand All @@ -22,6 +23,7 @@
use Neos\Flow\Mvc\Routing\TestingRoutesProvider;
use Neos\Flow\Tests\Functional\Mvc\Fixtures\Controller\ActionControllerTestAController;
use Neos\Flow\Tests\Functional\Mvc\Fixtures\Controller\RoutingTestAController;
use Neos\Flow\Tests\Functional\Mvc\Fixtures\Controller\StandardController;
use Neos\Flow\Tests\FunctionalTestCase;
use Neos\Utility\Arrays;
use Psr\Http\Message\ServerRequestFactoryInterface;
Expand Down Expand Up @@ -98,6 +100,26 @@ public function httpMethodsAreRespectedForPostRequests()
self::assertEquals('second', $actionRequest->getControllerActionName());
}

public function testRouteCanProvideRequestTags()
{
$requestUri = 'http://localhost/neos/flow/test/routetags';
$request = $this->serverRequestFactory->createServerRequest('GET', new Uri($requestUri));
$matchResults = $this->router->route(new RouteContext($request, RouteParameters::createEmpty()));
$this->assertTrue(isset($matchResults['@requestTags']), 'Route tags should be set.');
$request = $request->withAttribute(ServerRequestAttributes::REQUEST_TAGS, $matchResults['@requestTags']);
$actionRequest = $this->createActionRequest($request, $matchResults);
self::assertEquals(StandardController::class, $actionRequest->getControllerObjectName());
self::assertCount(2, $actionRequest->getRequestTags());
$expectedTags = [
['Security', 'Neos.Neos:Backend'],
['Something', 'Neos.Flow:Testing']
];
foreach ($actionRequest->getRequestTags() as $i => $tag) {
self::assertEquals($expectedTags[$i][0], $tag->type);
self::assertEquals($expectedTags[$i][1], $tag->value);
}
}

/**
* Data provider for routeTests()
*
Expand Down