Skip to content

Commit 1b0aa55

Browse files
committed
LinkGenerator: throws an error when the link points to #[Requires(forward: true)]
1 parent 1de532b commit 1b0aa55

File tree

3 files changed

+113
-4
lines changed

3 files changed

+113
-4
lines changed

src/Application/LinkGenerator.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ public function createRequest(
137137
throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()");
138138
}
139139

140-
$this->validateLinkTarget($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class));
140+
$this->validateLinkTarget($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode);
141141

142142
// convert indexed parameters to named
143143
UI\ParameterConverter::toParameters($method, $args, [], $missing);
@@ -166,7 +166,7 @@ public function createRequest(
166166
$current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class;
167167

168168
$reflection = new UI\ComponentReflection($presenterClass);
169-
$this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'");
169+
$this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'", $mode);
170170

171171
foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) {
172172
if ($args[$name] === $param['def']) {
@@ -176,7 +176,7 @@ public function createRequest(
176176

177177
// counterpart of run() & tryCall()
178178
if ($method = $reflection->getActionRenderMethod($action)) {
179-
$this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'");
179+
$this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode);
180180

181181
UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? $refPresenter->getParameters() : [], $missing);
182182

@@ -296,9 +296,12 @@ private function validateLinkTarget(
296296
?UI\Presenter $presenter,
297297
\ReflectionClass|\ReflectionMethod $element,
298298
string $message,
299+
string $mode,
299300
): void
300301
{
301-
if ($presenter?->invalidLinkMode
302+
if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) {
303+
throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'.");
304+
} elseif ($presenter?->invalidLinkMode
302305
&& (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class))
303306
) {
304307
trigger_error("Link to deprecated $message from '{$presenter->getName()}:{$presenter->getAction()}'.", E_USER_DEPRECATED);

src/Application/UI/AccessPolicy.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@ public function checkAccess(Component $component): void
4343
}
4444

4545

46+
public function isLinkable(): bool
47+
{
48+
$attrs = $this->getAttributes();
49+
return !$attrs || !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->forward === true);
50+
}
51+
52+
4653
/**
4754
* @return Attributes\Requires[]
4855
*/
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
<?php
2+
3+
/**
4+
* Test: Nette\Application\UI\Presenter::link() and #[Requires(forward: true)]
5+
*/
6+
7+
declare(strict_types=1);
8+
9+
use Nette\Application;
10+
use Nette\Application\Attributes\Requires;
11+
use Nette\Http;
12+
use Tester\Assert;
13+
14+
require __DIR__ . '/../bootstrap.php';
15+
16+
17+
class TestPresenter extends Application\UI\Presenter
18+
{
19+
protected function startup(): void
20+
{
21+
parent::startup();
22+
$this->terminate();
23+
}
24+
25+
26+
#[Requires(forward: true)]
27+
public function actionFoo()
28+
{
29+
}
30+
31+
32+
#[Requires(forward: true)]
33+
public function handleFoo()
34+
{
35+
}
36+
37+
38+
#[Requires(forward: true)]
39+
public function renderBar()
40+
{
41+
}
42+
}
43+
44+
45+
#[Requires(forward: true)]
46+
class ForwardPresenter extends TestPresenter
47+
{
48+
}
49+
50+
51+
$url = new Http\UrlScript('http://localhost/index.php', '/index.php');
52+
53+
$presenterFactory = Mockery::mock(Nette\Application\IPresenterFactory::class);
54+
$presenterFactory->shouldReceive('getPresenterClass')
55+
->andReturnUsing(fn($presenter) => $presenter . 'Presenter');
56+
57+
$presenter = new TestPresenter;
58+
$presenter->injectPrimary(
59+
new Http\Request($url),
60+
new Http\Response,
61+
$presenterFactory,
62+
new Application\Routers\SimpleRouter,
63+
);
64+
65+
$presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning;
66+
67+
$request = new Application\Request('Test', Http\Request::Get, []);
68+
$presenter->run($request);
69+
70+
Assert::error(
71+
fn() => $presenter->link('foo'),
72+
E_USER_WARNING,
73+
"Invalid link: Link to forbidden action 'Test:foo' from 'Test:default'.",
74+
);
75+
76+
Assert::error(
77+
fn() => $presenter->link('bar'),
78+
E_USER_WARNING,
79+
"Invalid link: Link to forbidden action 'Test:bar' from 'Test:default'.",
80+
);
81+
82+
Assert::error(
83+
fn() => $presenter->link('foo!'),
84+
E_USER_WARNING,
85+
"Invalid link: Link to forbidden signal 'foo' from 'Test:default'.",
86+
);
87+
88+
Assert::error(
89+
fn() => $presenter->link('Forward:'),
90+
E_USER_WARNING,
91+
"Invalid link: Link to forbidden presenter 'Forward' from 'Test:default'.",
92+
);
93+
94+
Assert::noError(fn() => $presenter->link('Test:'));
95+
96+
Assert::error( // no error
97+
fn() => $presenter->forward('Forward:'),
98+
Application\AbortException::class,
99+
);

0 commit comments

Comments
 (0)