Skip to content

Commit

Permalink
added attribute #[Requires]
Browse files Browse the repository at this point in the history
  • Loading branch information
dg committed Apr 6, 2024
1 parent d780fe8 commit 60b3300
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 4 deletions.
2 changes: 1 addition & 1 deletion src/Application/Attributes/CrossOrigin.php
Expand Up @@ -13,6 +13,6 @@


#[Attribute(Attribute::TARGET_METHOD)]
class CrossOrigin
class CrossOrigin // replaced by Requires(sameOrigin: false)
{
}
27 changes: 27 additions & 0 deletions src/Application/Attributes/Requires.php
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Nette\Application\Attributes;

use Attribute;


#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS | Attribute::IS_REPEATABLE)]
class Requires
{
public ?array $methods = null;
public ?array $actions = null;


public function __construct(
string|array|null $methods = null,
string|array|null $actions = null,
public ?bool $forward = null,
public ?bool $sameOrigin = null,
public ?bool $ajax = null,
) {
$this->methods = $methods === null ? null : (array) $methods;
$this->actions = $actions === null ? null : (array) $actions;
}
}
68 changes: 65 additions & 3 deletions src/Application/UI/Presenter.php
Expand Up @@ -288,14 +288,76 @@ protected function shutdown(Application\Response $response): void
*/
public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void
{
$attrs = array_map(
fn($ra) => $ra->newInstance(),
$element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF),
);

if (
$element instanceof \ReflectionMethod
&& str_starts_with($element->getName(), 'handle')
&& !ComponentReflection::parseAnnotation($element, 'crossOrigin')
&& !$element->getAttributes(Nette\Application\Attributes\CrossOrigin::class)
&& !$this->httpRequest->isSameSite()
&& !$element->getAttributes(Attributes\CrossOrigin::class)
&& !Arrays::some($attrs, fn($attr) => $attr->sameOrigin)
) {
$this->detectedCsrf();
$attrs[] = new Attributes\Requires(sameOrigin: true);
}

foreach ($attrs as $attribute) {
if ($attribute->methods) {
if ($element instanceof \ReflectionClass) { // presenter class
$this->allowedMethods = [];
}
$attribute->methods = array_map(strtoupper(...), $attribute->methods);
if (!in_array($method = $this->httpRequest->getMethod(), $attribute->methods, strict: true)) {
$this->httpResponse->setHeader('Allow', implode(',', $attribute->methods));
$this->error("Method $method is not allowed", $this->httpResponse::S405_MethodNotAllowed);
}
}

if ($attribute->actions === ['*']) {
if (!$element instanceof \ReflectionClass) { // presenter class
throw new Nette\InvalidStateException('The * action option of the Requires attribute is allowed only for presenters.');
}
if (!$this->getReflection()->hasCallableMethod(static::formatActionMethod($this->action))) {
$this->error("Action $this->action is not allowed", $this->httpResponse::S403_Forbidden);
}
} elseif ($attribute->actions) {
if ($element instanceof \ReflectionMethod && $element->getDeclaringClass()->getName() !== self::class) {
throw new Nette\InvalidStateException("The 'action' option of the Requires attribute is allowed only for presenters.");
}
if (!in_array($this->action, $attribute->actions, strict: true)) {
$this->error("Action $this->action is not allowed", $this->httpResponse::S403_Forbidden);
}
}

if ($attribute->actions) {
if ($element instanceof \ReflectionClass) { // presenter class
if ($attribute->actions === ['*']) {
if ($this->getReflection()->hasCallableMethod(static::formatActionMethod($this->action))) {
$this->error("Action $this->action is not allowed", $this->httpResponse::S403_Forbidden);
}
}
} elseif ($element->getDeclaringClass()->getName() !== self::class) {
throw new Nette\InvalidStateException("The 'action' option of the Requires attribute is allowed only for presenters.");
}

if (!in_array($this->action, $attribute->actions, strict: true)) {
$this->error("Action $this->action is not allowed", $this->httpResponse::S403_Forbidden);
}
}

if ($attribute->forward && !$this->request->isMethod($this->request::FORWARD)) {
$this->error('Is achievable only via forward.', $this->httpResponse::S403_Forbidden);
}

if ($attribute->sameOrigin && !$this->httpRequest->isSameSite()) {
$this->getPresenter()->detectedCsrf();
}

if ($attribute->ajax && !$this->httpRequest->isAjax()) {
$this->error('Request is not AJAX.', $this->httpResponse::S403_Forbidden);
}
}
}

Expand Down

0 comments on commit 60b3300

Please sign in to comment.