From 08250b28cecf7224490022368c3fd017656736f5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 11 Jul 2019 20:00:12 +0200 Subject: [PATCH 01/15] regexp: \z replaced with D modifier --- src/Application/LinkGenerator.php | 2 +- src/Application/PresenterFactory.php | 6 +++--- src/Application/Responses/FileResponse.php | 2 +- src/Application/UI/ComponentReflection.php | 4 ++-- src/Application/UI/Presenter.php | 4 ++-- src/Bridges/ApplicationLatte/UIMacros.php | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 811d72d29..803300779 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -45,7 +45,7 @@ public function __construct(Router $router, Nette\Http\UrlScript $refUrl, IPrese */ public function link(string $dest, array $params = []): string { - if (!preg_match('~^([\w:]+):(\w*+)(#.*)?()\z~', $dest, $m)) { + if (!preg_match('~^([\w:]+):(\w*+)(#.*)?()$~D', $dest, $m)) { throw new UI\InvalidLinkException("Invalid link destination '$dest'."); } [, $presenter, $action, $frag] = $m; diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index 0cee96c92..d27a821ab 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -60,7 +60,7 @@ public function getPresenterClass(string &$name): string return $this->cache[$name]; } - if (!Nette\Utils\Strings::match($name, '#^[a-zA-Z\x7f-\xff][a-zA-Z0-9\x7f-\xff:]*\z#')) { + if (!Nette\Utils\Strings::match($name, '#^[a-zA-Z\x7f-\xff][a-zA-Z0-9\x7f-\xff:]*$#D')) { throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$name' is invalid."); } @@ -97,7 +97,7 @@ public function setMapping(array $mapping) { foreach ($mapping as $module => $mask) { if (is_string($mask)) { - if (!preg_match('#^\\\\?([\w\\\\]*\\\\)?(\w*\*\w*?\\\\)?([\w\\\\]*\*\w*)\z#', $mask, $m)) { + if (!preg_match('#^\\\\?([\w\\\\]*\\\\)?(\w*\*\w*?\\\\)?([\w\\\\]*\*\w*)$#D', $mask, $m)) { throw new Nette\InvalidStateException("Invalid mapping mask '$mask'."); } $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]]; @@ -137,7 +137,7 @@ public function unformatPresenterClass(string $class): ?string { foreach ($this->mapping as $module => $mapping) { $mapping = str_replace(['\\', '*'], ['\\\\', '(\w+)'], $mapping); - if (preg_match("#^\\\\?$mapping[0]((?:$mapping[1])*)$mapping[2]\\z#i", $class, $matches)) { + if (preg_match("#^\\\\?$mapping[0]((?:$mapping[1])*)$mapping[2]$#Di", $class, $matches)) { return ($module === '*' ? '' : $module . ':') . preg_replace("#$mapping[1]#iA", '$1:', $matches[1]) . $matches[3]; } diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index cc0d4e1f1..18d31df19 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -91,7 +91,7 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt if ($this->resuming) { $httpResponse->setHeader('Accept-Ranges', 'bytes'); - if (preg_match('#^bytes=(\d*)-(\d*)\z#', (string) $httpRequest->getHeader('Range'), $matches)) { + if (preg_match('#^bytes=(\d*)-(\d*)$#D', (string) $httpRequest->getHeader('Range'), $matches)) { [, $start, $end] = $matches; if ($start === '') { $start = max(0, $filesize - $end); diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index cda5c066d..32d122515 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -207,7 +207,7 @@ public static function convertType(&$val, string $type, bool $isClass = false): } else { $tmp = ($val === false ? '0' : (string) $val); if ($type === 'double' || $type === 'float') { - $tmp = preg_replace('#\.0*\z#', '', $tmp); + $tmp = preg_replace('#\.0*$#D', '', $tmp); } $orig = $tmp; settype($tmp, $type); @@ -226,7 +226,7 @@ public static function convertType(&$val, string $type, bool $isClass = false): */ public static function parseAnnotation(\Reflector $ref, string $name): ?array { - if (!preg_match_all('#[\\s*]@' . preg_quote($name, '#') . '(?:\(\\s*([^)]*)\\s*\)|\\s|$)#', (string) $ref->getDocComment(), $m)) { + if (!preg_match_all('#[\s*]@' . preg_quote($name, '#') . '(?:\(\s*([^)]*)\s*\)|\s|$)#', (string) $ref->getDocComment(), $m)) { return null; } static $tokens = ['true' => true, 'false' => false, 'null' => null]; diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 59c4ce941..d8cf8436a 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -1186,7 +1186,7 @@ private function initGlobalParameters(): void } foreach ($params as $key => $value) { - if (!preg_match('#^((?:[a-z0-9_]+-)*)((?!\d+\z)[a-z0-9_]+)\z#i', (string) $key, $matches)) { + if (!preg_match('#^((?:[a-z0-9_]+-)*)((?!\d+$)[a-z0-9_]+)$#Di', (string) $key, $matches)) { continue; } elseif (!$matches[1]) { $selfParams[$key] = $value; @@ -1197,7 +1197,7 @@ private function initGlobalParameters(): void // init & validate $this->action & $this->view $action = $selfParams[self::ACTION_KEY] ?? self::DEFAULT_ACTION; - if (!is_string($action) || !Nette\Utils\Strings::match($action, '#^[a-zA-Z0-9][a-zA-Z0-9_\x7f-\xff]*\z#')) { + if (!is_string($action) || !Nette\Utils\Strings::match($action, '#^[a-zA-Z0-9][a-zA-Z0-9_\x7f-\xff]*$#D')) { $this->error('Action name is not valid.'); } $this->changeAction($action); diff --git a/src/Bridges/ApplicationLatte/UIMacros.php b/src/Bridges/ApplicationLatte/UIMacros.php index ecc792c8e..4a6773baf 100644 --- a/src/Bridges/ApplicationLatte/UIMacros.php +++ b/src/Bridges/ApplicationLatte/UIMacros.php @@ -80,7 +80,7 @@ public function macroControl(MacroNode $node, PhpWriter $writer) } $name = $writer->formatWord($words[0]); $method = ucfirst($words[1] ?? ''); - $method = Strings::match($method, '#^\w*\z#') ? "render$method" : "{\"render$method\"}"; + $method = Strings::match($method, '#^\w*$#D') ? "render$method" : "{\"render$method\"}"; $tokens = $node->tokenizer; $pos = $tokens->position; @@ -113,7 +113,7 @@ public function macroControl(MacroNode $node, PhpWriter $writer) */ public function macroLink(MacroNode $node, PhpWriter $writer) { - $node->modifiers = preg_replace('#\|safeurl\s*(?=\||\z)#i', '', $node->modifiers); + $node->modifiers = preg_replace('#\|safeurl\s*(?=\||$)#Di', '', $node->modifiers); return $writer->using($node) ->write('echo %escape(%modify(' . ($node->name === 'plink' ? '$this->global->uiPresenter' : '$this->global->uiControl') From 14f6ce4db245b4289e632ab471c57e1bff35c2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bar=C3=A1=C5=A1ek?= Date: Mon, 22 Jul 2019 16:33:30 +0200 Subject: [PATCH 02/15] Fix $this to self:: in case of static method. (#224) * Fix $this to self:: in case of static method. * Presenter: Replace $this and self:: to static:: in static methods --- src/Application/UI/Presenter.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index d8cf8436a..14945dfd9 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -188,15 +188,15 @@ public function run(Application\Request $request): Application\IResponse } $this->initGlobalParameters(); - $this->checkRequirements($this->getReflection()); + $this->checkRequirements(static::getReflection()); $this->onStartup($this); $this->startup(); if (!$this->startupCheck) { - $class = $this->getReflection()->getMethod('startup')->getDeclaringClass()->getName(); + $class = static::getReflection()->getMethod('startup')->getDeclaringClass()->getName(); throw new Nette\InvalidStateException("Method $class::startup() or its descendant doesn't call parent::startup()."); } // calls $this->action() - $this->tryCall($this->formatActionMethod($this->action), $this->params); + $this->tryCall(static::formatActionMethod($this->action), $this->params); // autoload components foreach ($this->globalParams as $id => $foo) { @@ -218,7 +218,7 @@ public function run(Application\Request $request): Application\IResponse $this->beforeRender(); $this->onRender($this); // calls $this->render() - $this->tryCall($this->formatRenderMethod($this->view), $this->params); + $this->tryCall(static::formatRenderMethod($this->view), $this->params); $this->afterRender(); // save component tree persistent state @@ -506,7 +506,7 @@ public function formatLayoutTemplateFiles(): array } [$module, $presenter] = Helpers::splitName($this->getName()); $layout = $this->layout ?: 'layout'; - $dir = dirname($this->getReflection()->getFileName()); + $dir = dirname(static::getReflection()->getFileName()); $dir = is_dir("$dir/templates") ? $dir : dirname($dir); $list = [ "$dir/templates/$presenter/@$layout.latte", @@ -526,7 +526,7 @@ public function formatLayoutTemplateFiles(): array public function formatTemplateFiles(): array { [, $presenter] = Helpers::splitName($this->getName()); - $dir = dirname($this->getReflection()->getFileName()); + $dir = dirname(static::getReflection()->getFileName()); $dir = is_dir("$dir/templates") ? $dir : dirname($dir); return [ "$dir/templates/$presenter/$this->view.latte", @@ -740,7 +740,7 @@ final protected function createRequest(Component $component, string $destination $this->lastCreatedRequest = $this->lastCreatedRequestFlag = null; - $parts = $this->parseDestination($destination); + $parts = static::parseDestination($destination); $path = $parts['path']; $args = $parts['args'] ?? $args; @@ -1151,7 +1151,7 @@ protected function getGlobalState(string $forClass = null): array */ public function saveState(array &$params, ComponentReflection $reflection = null): void { - ($reflection ?: $this->getReflection())->saveState($this, $params); + ($reflection ?: static::getReflection())->saveState($this, $params); } From af851cba10aa6674412c24ee61b766acc225756c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bar=C3=A1=C5=A1ek?= Date: Mon, 5 Aug 2019 14:16:13 +0200 Subject: [PATCH 03/15] RoutingPanel: Add better panel responsivity (#225) --- .../templates/RoutingPanel.panel.phtml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Bridges/ApplicationTracy/templates/RoutingPanel.panel.phtml b/src/Bridges/ApplicationTracy/templates/RoutingPanel.panel.phtml index 52effa8c8..ba682286e 100644 --- a/src/Bridges/ApplicationTracy/templates/RoutingPanel.panel.phtml +++ b/src/Bridges/ApplicationTracy/templates/RoutingPanel.panel.phtml @@ -22,6 +22,14 @@ use Tracy\Helpers; background: #C1D3FF !important; } + #tracy-debug .nette-RoutingPanel td:first-child { + width: 20px; + } + + #tracy-debug .nette-RoutingPanel td:nth-child(2) { + white-space: nowrap; + } + #tracy-debug .nette-RoutingPanel pre, #tracy-debug .nette-RoutingPanel code { display: inline; background: transparent; @@ -29,7 +37,7 @@ use Tracy\Helpers; #tracy-debug .nette-RoutingPanel-rel { background: #eee; - white-space: nowrap + white-space: nowrap; } @@ -63,7 +71,7 @@ use Tracy\Helpers; - + /', Helpers::escapeHtml($router['mask'])) : str_replace('\\', '\\', Helpers::escapeHtml($router['class'])) ?> $value): ?> @@ -88,9 +96,11 @@ use Tracy\Helpers; + +

- getBaseUrl()) ?>getRelativeUrl()) ?>

+ getBaseUrl()) ?>&', '?'], Helpers::escapeHtml($url->getRelativeUrl())) ?>

getName() : $source->getDeclaringClass()->getName() . '::' . $source->getName() . '()' ?>

From 03e942803159d5cd195054bd5a5b07abc8081dee Mon Sep 17 00:00:00 2001 From: David Grudl Date: Fri, 13 Sep 2019 11:29:38 +0200 Subject: [PATCH 04/15] added support for persistent parameters with property typehints in PHP 7.4 [Closes #230] --- src/Application/UI/Component.php | 5 +- src/Application/UI/ComponentReflection.php | 6 +- tests/UI/Presenter.link().74.phptx | 319 +++++++++++++++++++++ tests/UI/Presenter.link().persistent.phpt | 26 +- 4 files changed, 337 insertions(+), 19 deletions(-) create mode 100644 tests/UI/Presenter.link().74.phptx diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index c170c00d2..f9b7f5eb2 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -135,13 +135,12 @@ public function loadState(array $params): void $reflection = $this->getReflection(); foreach ($reflection->getPersistentParams() as $name => $meta) { if (isset($params[$name])) { // nulls are ignored - $type = gettype($meta['def']); - if (!$reflection->convertType($params[$name], $type)) { + if (!$reflection->convertType($params[$name], $meta['type'])) { throw new Nette\Application\BadRequestException(sprintf( "Value passed to persistent parameter '%s' in %s must be %s, %s given.", $name, $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", - $type === 'NULL' ? 'scalar' : $type, + $meta['type'] === 'NULL' ? 'scalar' : $meta['type'], is_object($params[$name]) ? get_class($params[$name]) : gettype($params[$name]) )); } diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 32d122515..55fe9b612 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -52,6 +52,7 @@ public function getPersistentParams(string $class = null): array if (!$rp->isStatic() && self::parseAnnotation($rp, 'persistent')) { $params[$name] = [ 'def' => $default, + 'type' => Nette\Utils\Reflection::getPropertyType($rp) ?: gettype($default), 'since' => $isPresenter ? Nette\Utils\Reflection::getPropertyDeclaringClass($rp)->getName() : null, ]; } @@ -111,13 +112,12 @@ public function saveState(Component $component, array &$params): void $params[$name] = $component->$name; // object property value } - $type = gettype($meta['def']); - if (!self::convertType($params[$name], $type)) { + if (!self::convertType($params[$name], $meta['type'])) { throw new InvalidLinkException(sprintf( "Value passed to persistent parameter '%s' in %s must be %s, %s given.", $name, $component instanceof Presenter ? 'presenter ' . $component->getName() : "component '{$component->getUniqueId()}'", - $type === 'NULL' ? 'scalar' : $type, + $meta['type'] === 'NULL' ? 'scalar' : $meta['type'], is_object($params[$name]) ? get_class($params[$name]) : gettype($params[$name]) )); } diff --git a/tests/UI/Presenter.link().74.phptx b/tests/UI/Presenter.link().74.phptx new file mode 100644 index 000000000..237651e32 --- /dev/null +++ b/tests/UI/Presenter.link().74.phptx @@ -0,0 +1,319 @@ +invalidLinkMode = self::INVALID_LINK_TEXTUAL; + + // standard + Assert::same("#error: Invalid destination ''.", $this->link('')); + Assert::same('/index.php?action=params&presenter=Test', $this->link('params')); + Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('http://localhost/index.php?action=params&presenter=Test', $this->link('//params')); + Assert::same('#error: Argument $arr passed to TestPresenter::actionParams() must be scalar, array given.', $this->link('params', [1, 2, 3, []])); + Assert::same('/index.php?xx=1&action=params&presenter=Test', $this->link('params', ['xx' => 1, 'yy' => []])); + Assert::same(['xx' => 1, 'yy' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', $this->link('this', ['sort' => ['y' => ['asc' => true]]])); + Assert::same(['sort' => ['y' => ['asc' => true]], 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default'], $this->getLastCreatedRequest()->getParameters()); + + Assert::same("#error: Unable to pass parameters to action 'Test:product', missing corresponding method.", $this->link('product', 1)); + Assert::same('/index.php?a=1&action=product&presenter=Test', $this->link('product', ['a' => 1])); + Assert::same('#error: Passed more parameters than method TestPresenter::actionParams() expects.', $this->link('params', 1, 2, 3, 4, 5)); + + // special url + Assert::same('/index.php?x=1&y=2&action=product&presenter=Test', $this->link('product?x=1&y=2')); + Assert::same('/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product?x=1&y=2#fragment')); + + // absolute + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product?x=1&y=2#fragment')); + $this->absoluteUrls = true; + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product?x=1&y=2#fragment')); + Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product?x=1&y=2#fragment')); + $this->absoluteUrls = false; + + // persistent params + Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); + Assert::same('/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]])); + Assert::same(['pint' => 20, 'pbool' => null, 'parr' => [1], 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?pint=1&pbool=0&action=params&presenter=Test', $this->link('params', ['pint' => true, 'pbool' => '0', 'parr' => []])); + Assert::same('/index.php?pint=0&pbool=0&p=0&action=params&presenter=Test', $this->link('params', ['pint' => false, 'pbool' => false, 'p' => false, 'parr' => null])); + Assert::same("#error: Value passed to persistent parameter 'pbool' in presenter Test must be boolean, string given.", $this->link('this', ['p' => null, 'pbool' => 'a'])); + Assert::same("#error: Value passed to persistent parameter 'p' in presenter Test must be scalar, array given.", $this->link('this', ['p' => [1], 'pbool' => false])); + Assert::same('/index.php?action=persistent&presenter=Test', $this->link('persistent')); + + Assert::same('/index.php?pbooln=1&parrn%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pbooln' => true, 'parrn' => [1]])); + Assert::same(['pbooln' => true, 'parrn' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?pbooln=0&action=params&presenter=Test', $this->link('params', ['pbooln' => '0', 'parrn' => []])); + Assert::same(['pbooln' => false, 'parrn' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same("#error: Value passed to persistent parameter 'pbooln' in presenter Test must be bool, string given.", $this->link('params', ['pbooln' => 'a'])); + Assert::same("#error: Value passed to persistent parameter 'parrn' in presenter Test must be array, string given.", $this->link('params', ['parrn' => 'a'])); + + // Other presenter & action link + Assert::same('/index.php?action=product&presenter=Other', $this->link('Other:product', ['p' => $this->p])); + Assert::same('/index.php?p=0&action=product&presenter=Other', $this->link('Other:product', ['p' => $this->p * 2])); + Assert::same('/index.php?p=123&presenter=Nette%3AMicro', $this->link('Nette:Micro:', ['p' => 123])); + Assert::same(['p' => 123], $this->getLastCreatedRequest()->getParameters()); + + // signal link + Assert::same('#error: Signal must be non-empty string.', $this->link('mycontrol:!')); + Assert::same('/index.php?action=default&presenter=Test', $this->link('this', ['p' => $this->p])); + Assert::same('/index.php?action=default&presenter=Test', $this->link('this!', ['p' => $this->p])); + Assert::same('/index.php?action=default&do=signal&presenter=Test', $this->link('signal!', ['p' => $this->p])); + Assert::same('/index.php?p=0&action=default&do=signal&presenter=Test', $this->link('signal!', [1, 'p' => $this->p * 2])); + Assert::same('/index.php?y=2&action=default&do=signal&presenter=Test', $this->link('signal!', 1, 2)); + Assert::same(['x' => null, 'y' => 2, 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default', 'do' => 'signal'], $this->getLastCreatedRequest()->getParameters()); + + // Component link + Assert::same("#error: Invalid destination ''.", $this['mycontrol']->link('', 0, 1)); + Assert::same('/index.php?mycontrol-x=0&mycontrol-y=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', 0, 1)); + Assert::same('/index.php?mycontrol-x=0a&mycontrol-y=1a&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', '0a', '1a')); + Assert::same('/index.php?mycontrol-x=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', [1])); + Assert::same('#error: Argument $x passed to TestControl::handleClick() must be scalar, array given.', $this['mycontrol']->link('click', [1], (object) [1])); + Assert::same('/index.php?mycontrol-x=1&mycontrol-y=0&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', true, false)); + Assert::same('/index.php?action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', null, '')); + Assert::same('#error: Passed more parameters than method TestControl::handleClick() expects.', $this['mycontrol']->link('click', 1, 2, 3)); + Assert::same('http://localhost/index.php?mycontrol-x=1&mycontrol-round=1&action=default&presenter=Test#frag', $this['mycontrol']->link('//this?x=1&round=1#frag')); + Assert::same('/index.php?mycontrol-x=1&mycontrol-y=2&action=default&do=mycontrol-click&presenter=Test', $this->link('mycontrol:click!', ['x' => 1, 'y' => 2, 'round' => 0])); + Assert::same(['mycontrol-x' => 1, 'mycontrol-y' => 2, 'mycontrol-round' => null, 'mycontrol-order' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'default', 'do' => 'mycontrol-click'], $this->getLastCreatedRequest()->getParameters()); + + // Component link type checking + Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, integer given.", $this['mycontrol']->link('click', ['order' => 1])); + Assert::same("#error: Value passed to persistent parameter 'round' in component 'mycontrol' must be integer, array given.", $this['mycontrol']->link('click', ['round' => []])); + $this['mycontrol']->order = 1; + Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, integer given.", $this['mycontrol']->link('click')); + $this['mycontrol']->order = null; + + // type checking + Assert::same('/index.php?action=params&presenter=Test', $this->link('params', [])); + Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?int=1&bool=1&str=abc&arr=1&action=params&presenter=Test', $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1'])); + Assert::same('/index.php?int=0&bool=0&action=params&presenter=Test', $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => ''])); + Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => new stdClass])); + + Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', [])); + Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hints&presenter=Test', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); + Assert::same('/index.php?int=0&bool=0&action=hints&presenter=Test', $this->link('hints', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, string given.', $this->link('hints', ['int' => ''])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, stdClass given.', $this->link('hints', ['int' => new stdClass])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, array given.', $this->link('hints', ['int' => []])); + Assert::same('#error: Argument $bool passed to TestPresenter::actionHints() must be bool, string given.', $this->link('hints', ['int' => '1', 'bool' => ''])); + Assert::same('#error: Argument $arr passed to TestPresenter::actionHints() must be array, string given.', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + + Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', [])); + Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); + Assert::same('/index.php?int=0&bool=0&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be int, string given.', $this->link('hintsNulls', ['int' => ''])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be int, stdClass given.', $this->link('hintsNulls', ['int' => new stdClass])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be int, array given.', $this->link('hintsNulls', ['int' => []])); + Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNulls() must be bool, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => ''])); + Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNulls() must be array, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + + Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', [])); + Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); + Assert::same('/index.php?int=0&bool=0&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be int, string given.', $this->link('hintsNullable', ['int' => ''])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be int, stdClass given.', $this->link('hintsNullable', ['int' => new stdClass])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be int, array given.', $this->link('hintsNullable', ['int' => []])); + Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNullable() must be bool, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => ''])); + Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNullable() must be array, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + + Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', [])); + Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); + Assert::same(['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'hintsDefaults'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, string given.', $this->link('hintsDefaults', ['int' => ''])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, stdClass given.', $this->link('hintsDefaults', ['int' => new stdClass])); + Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, array given.', $this->link('hintsDefaults', ['int' => []])); + Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsDefaults() must be bool, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => ''])); + Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsDefaults() must be array, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + + Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', [])); + Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); + Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => 'a', 'arr' => [1]])); + Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'defaults'], $this->getLastCreatedRequest()->getParameters()); + Assert::same('/index.php?int=0&bool=0&str=&action=defaults&presenter=Test', $this->link('defaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be integer, string given.', $this->link('defaults', ['int' => ''])); + Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be integer, array given.', $this->link('defaults', ['int' => []])); + Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be integer, stdClass given.', $this->link('defaults', ['int' => new stdClass])); + Assert::same('#error: Argument $bool passed to TestPresenter::actionDefaults() must be boolean, string given.', $this->link('defaults', ['int' => '1', 'bool' => ''])); + Assert::same('#error: Argument $arr passed to TestPresenter::actionDefaults() must be array, string given.', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + + Assert::same('/index.php?action=objects&presenter=Test', $this->link('objects', ['req' => new stdClass, 'nullable' => new stdClass, 'opt' => new stdClass])); + Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', [])); + Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', ['req' => null, 'nullable' => null, 'opt' => null])); + Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, Exception given.', $this->link('objects', ['req' => new Exception, 'opt' => null])); + Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, array given.', $this->link('objects', ['req' => []])); + + // silent invalid link mode + $this->invalidLinkMode = self::INVALID_LINK_SILENT; + Assert::same('#', $this->link('params', ['p' => null, 'pbool' => 'a'])); + + // warning invalid link mode + $this->invalidLinkMode = self::INVALID_LINK_WARNING; + Assert::error(function () { + $this->link('params', ['p' => null, 'pbool' => 'a']); + }, E_USER_WARNING, "Invalid link: Value passed to persistent parameter 'pbool' in presenter Test must be boolean, string given."); + + // exception invalid link mode + $this->invalidLinkMode = self::INVALID_LINK_EXCEPTION; + Assert::exception(function () { + $this->link('params', ['p' => null, 'pbool' => 'a']); + }, Nette\Application\UI\InvalidLinkException::class, "Value passed to persistent parameter 'pbool' in presenter Test must be boolean, string given."); + + $this->p = null; // null in persistent parameter means default + Assert::same('/index.php?action=params&presenter=Test', $this->link('params')); + $this->terminate(); + } + + + public function actionParams($int, $bool, $str, $arr) + { + } + + + public function actionHints(int $int, bool $bool, string $str, array $arr) + { + } + + + public function actionHintsNulls(int $int = null, bool $bool = null, string $str = null, array $arr = null) + { + } + + + public function actionHintsNullable(?int $int, ?bool $bool, ?string $str, ?array $arr) + { + } + + + public function actionHintsDefaults(int $int = 0, bool $bool = false, string $str = '', array $arr = []) + { + } + + + public function actionDefaults($int = 1, $bool = true, $str = 'a', $arr = [1]) + { + } + + + public function actionObjects(stdClass $req, ?stdClass $nullable, stdClass $opt = null) + { + } + + + public function actionPersistent(int $pint) + { + } + + + public function handleSignal($x = 1, $y = 1) + { + } +} + + +class OtherPresenter extends TestPresenter +{ + /** @persistent */ + public $p = 20; +} + + +$url = new Http\UrlScript('http://localhost/index.php', '/index.php'); + +$presenterFactory = Mockery::mock(Nette\Application\IPresenterFactory::class); +$presenterFactory->shouldReceive('getPresenterClass') + ->andReturnUsing(function ($presenter) { + return $presenter . 'Presenter'; + }); + +$presenter = new TestPresenter; +$presenter->injectPrimary( + null, + $presenterFactory, + new Application\Routers\SimpleRouter, + new Http\Request($url), + new Http\Response +); + +$presenter->invalidLinkMode = TestPresenter::INVALID_LINK_WARNING; +$presenter->autoCanonicalize = false; + +$request = new Application\Request('Test', Http\Request::GET, []); +$presenter->run($request); diff --git a/tests/UI/Presenter.link().persistent.phpt b/tests/UI/Presenter.link().persistent.phpt index 04ef25169..7e25ebb12 100644 --- a/tests/UI/Presenter.link().persistent.phpt +++ b/tests/UI/Presenter.link().persistent.phpt @@ -96,28 +96,28 @@ class ThirdPresenter extends BasePresenter Assert::same([ - 'p1' => ['def' => null, 'since' => 'BasePresenter'], - 't1' => ['def' => null, 'since' => 'PersistentParam1'], + 'p1' => ['def' => null, 'type' => 'NULL', 'since' => 'BasePresenter'], + 't1' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam1'], ], BasePresenter::getReflection()->getPersistentParams()); Assert::same([ - 'p2' => ['def' => null, 'since' => 'TestPresenter'], - 'p1' => ['def' => null, 'since' => 'BasePresenter'], - 't1' => ['def' => null, 'since' => 'PersistentParam1'], - 't2' => ['def' => null, 'since' => 'PersistentParam2A'], + 'p2' => ['def' => null, 'type' => 'NULL', 'since' => 'TestPresenter'], + 'p1' => ['def' => null, 'type' => 'NULL', 'since' => 'BasePresenter'], + 't1' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam1'], + 't2' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam2A'], ], TestPresenter::getReflection()->getPersistentParams()); Assert::same([ - 'p1' => ['def' => 20, 'since' => 'BasePresenter'], - 'p3' => ['def' => null, 'since' => 'SecondPresenter'], - 't1' => ['def' => null, 'since' => 'PersistentParam1'], - 't3' => ['def' => null, 'since' => 'PersistentParam3'], + 'p1' => ['def' => 20, 'type' => 'integer', 'since' => 'BasePresenter'], + 'p3' => ['def' => null, 'type' => 'NULL', 'since' => 'SecondPresenter'], + 't1' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam1'], + 't3' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam3'], ], SecondPresenter::getReflection()->getPersistentParams()); Assert::same([ - 'p1' => ['def' => null, 'since' => 'BasePresenter'], - 't1' => ['def' => null, 'since' => 'PersistentParam1'], - 't2' => ['def' => null, 'since' => 'PersistentParam2A'], + 'p1' => ['def' => null, 'type' => 'NULL', 'since' => 'BasePresenter'], + 't1' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam1'], + 't2' => ['def' => null, 'type' => 'NULL', 'since' => 'PersistentParam2A'], ], ThirdPresenter::getReflection()->getPersistentParams()); From 3a650466f379d843fa965437c12f7e63292cb657 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 15 Aug 2019 13:39:33 +0200 Subject: [PATCH 05/15] ApplicationExtension: can use external RobotLoader [Closes #227] --- .../ApplicationDI/ApplicationExtension.php | 22 +++++++++++++++---- .../Bridges.DI/ApplicationExtension.scan.phpt | 22 +++++++++++++++++++ tests/bootstrap.php | 21 ++++++++++++++++++ 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index cdb09ecec..c74089010 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -28,6 +28,9 @@ final class ApplicationExtension extends Nette\DI\CompilerExtension /** @var array */ private $scanDirs; + /** @var Nette\Loaders\RobotLoader|null */ + private $robotLoader; + /** @var int */ private $invalidLinkMode; @@ -35,11 +38,12 @@ final class ApplicationExtension extends Nette\DI\CompilerExtension private $tempDir; - public function __construct(bool $debugMode = false, array $scanDirs = null, string $tempDir = null) + public function __construct(bool $debugMode = false, array $scanDirs = null, string $tempDir = null, Nette\Loaders\RobotLoader $robotLoader = null) { $this->debugMode = $debugMode; $this->scanDirs = (array) $scanDirs; $this->tempDir = $tempDir; + $this->robotLoader = $robotLoader; } @@ -78,7 +82,7 @@ public function loadConfiguration() } $this->compiler->addExportedType(Nette\Application\Application::class); - $touch = $this->debugMode && $config->scanDirs && $this->tempDir ? $this->tempDir . '/touch' : null; + $touch = $this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir ? $this->tempDir . '/touch' : null; $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory')) ->setType(Nette\Application\IPresenterFactory::class) ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( @@ -133,9 +137,15 @@ public function beforeCompile() private function findPresenters(): array { $config = $this->getConfig(); - $classes = []; - if ($config->scanDirs) { + if ($this->robotLoader) { + if ($config->scanDirs && $config->scanDirs !== $this->scanDirs) { + trigger_error("Option 'scanDir' has no effect, global RobotLoader is used.", E_USER_DEPRECATED); + } + $robot = $this->robotLoader; + $robot->refresh(); + + } elseif ($config->scanDirs) { if (!class_exists(Nette\Loaders\RobotLoader::class)) { throw new Nette\NotSupportedException("RobotLoader is required to find presenters, install package `nette/robot-loader` or disable option {$this->prefix('scanDirs')}: false"); } @@ -148,6 +158,10 @@ private function findPresenters(): array } else { $robot->rebuild(); } + } + + $classes = []; + if (isset($robot)) { $classes = array_keys($robot->getIndexedClasses()); $this->getContainerBuilder()->addDependency($this->tempDir . '/touch'); } diff --git a/tests/Bridges.DI/ApplicationExtension.scan.phpt b/tests/Bridges.DI/ApplicationExtension.scan.phpt index 84e1209cb..59723a575 100644 --- a/tests/Bridges.DI/ApplicationExtension.scan.phpt +++ b/tests/Bridges.DI/ApplicationExtension.scan.phpt @@ -78,3 +78,25 @@ test(function () { $name = $container->findByType(Presenter1::class)[0]; Assert::same('test', $container->createService($name)->getView()); }); + + +test(function () { + $robot = new Nette\Loaders\RobotLoader; + $robot->addDirectory(__DIR__ . '/files'); + $robot->setTempDirectory(getTempDir()); + + $compiler = new DI\Compiler; + $compiler->addExtension('application', new ApplicationExtension(false, null, null, $robot)); + + $builder = $compiler->getContainerBuilder(); + $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new DI\Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $code = $compiler->setClassName('Container4')->compile(); + eval($code); + + $container = new Container2; + Assert::count(3, $container->findByType(BasePresenter::class)); + Assert::count(1, $container->findByType(Presenter1::class)); + Assert::count(1, $container->findByType(Presenter2::class)); +}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 600348824..4d8c52cf9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -22,6 +22,27 @@ }, ob_get_level()); +function getTempDir(): string +{ + $dir = __DIR__ . '/tmp/' . getmypid(); + + if (empty($GLOBALS['\\lock'])) { + // garbage collector + $GLOBALS['\\lock'] = $lock = fopen(__DIR__ . '/lock', 'w'); + if (rand(0, 100)) { + flock($lock, LOCK_SH); + @mkdir(dirname($dir)); + } elseif (flock($lock, LOCK_EX)) { + Tester\Helpers::purge(dirname($dir)); + } + + @mkdir($dir); + } + + return $dir; +} + + function test(\Closure $function): void { $function(); From 67f45fb72d663ab2dc68f3d6dbae4f2d989e9ef5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Barto=C5=A1?= Date: Wed, 18 Sep 2019 10:37:56 +0200 Subject: [PATCH 06/15] TemplateFactory: remove unused Loader (#232) --- src/Bridges/ApplicationLatte/Loader.php | 30 ------------------- .../ApplicationLatte/TemplateFactory.php | 4 --- 2 files changed, 34 deletions(-) delete mode 100644 src/Bridges/ApplicationLatte/Loader.php diff --git a/src/Bridges/ApplicationLatte/Loader.php b/src/Bridges/ApplicationLatte/Loader.php deleted file mode 100644 index 3bb600129..000000000 --- a/src/Bridges/ApplicationLatte/Loader.php +++ /dev/null @@ -1,30 +0,0 @@ -presenter = $presenter; - } -} diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index c9d527c95..5ed5d1008 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -60,10 +60,6 @@ public function createTemplate(UI\Control $control = null): UI\ITemplate $template = new $this->templateClass($latte); $presenter = ($control && $control->hasPresenter()) ? $control->getPresenter() : null; - if ($control instanceof UI\Presenter) { - $latte->setLoader(new Loader($control)); - } - if ($latte->onCompile instanceof \Traversable) { $latte->onCompile = iterator_to_array($latte->onCompile); } From 165cc68d17ab1e3f1079acf195ba8536adcb6c70 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 30 Sep 2019 09:31:47 +0200 Subject: [PATCH 07/15] composer: fixed lower dependency --- composer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 026a7d154..bdddad5de 100644 --- a/composer.json +++ b/composer.json @@ -38,7 +38,8 @@ "conflict": { "nette/di": "<3.0-stable", "nette/forms": "<3.0", - "nette/latte": "<3.0" + "latte/latte": "<2.4", + "tracy/tracy": "<2.5" }, "autoload": { "classmap": ["src/"], From d6dd226956e3977b53af6bf964b0eeb76701223c Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 8 Oct 2019 13:59:01 +0200 Subject: [PATCH 08/15] Template: removed 'final' [Closes #235] --- src/Bridges/ApplicationLatte/Template.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bridges/ApplicationLatte/Template.php b/src/Bridges/ApplicationLatte/Template.php index e4be0cbc0..e28db75c1 100644 --- a/src/Bridges/ApplicationLatte/Template.php +++ b/src/Bridges/ApplicationLatte/Template.php @@ -64,7 +64,7 @@ public function renderToString(string $file = null, array $params = []): string * Renders template to string. * @param can throw exceptions? (hidden parameter) */ - final public function __toString(): string + public function __toString(): string { try { return $this->latte->renderToString($this->file, $this->params); From 5ae6bc13248cf4a28b7c97090d12e30fabfd22af Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 8 Oct 2019 14:06:11 +0200 Subject: [PATCH 09/15] throwing exceptions from __toString() is allowed since PHP 7.4 --- src/Application/UI/Link.php | 2 +- src/Bridges/ApplicationLatte/Template.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Application/UI/Link.php b/src/Application/UI/Link.php index 8830b8179..a48ddc5f2 100644 --- a/src/Application/UI/Link.php +++ b/src/Application/UI/Link.php @@ -89,7 +89,7 @@ public function __toString(): string return $this->component->link($this->destination, $this->params); } catch (\Throwable $e) { - if (func_num_args()) { + if (func_num_args() || PHP_VERSION_ID >= 70400) { throw $e; } trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); diff --git a/src/Bridges/ApplicationLatte/Template.php b/src/Bridges/ApplicationLatte/Template.php index e28db75c1..5107e8d13 100644 --- a/src/Bridges/ApplicationLatte/Template.php +++ b/src/Bridges/ApplicationLatte/Template.php @@ -69,7 +69,7 @@ public function __toString(): string try { return $this->latte->renderToString($this->file, $this->params); } catch (\Throwable $e) { - if (func_num_args()) { + if (func_num_args() || PHP_VERSION_ID >= 70400) { throw $e; } trigger_error('Exception in ' . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR); From 5868278c14ca465d4dab866d065f4dfbf1446de5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 16 Oct 2019 13:20:00 +0200 Subject: [PATCH 10/15] Component, Form: added getPresenterIfExists() --- src/Application/UI/Component.php | 11 ++++++++++- src/Application/UI/Form.php | 11 ++++++++++- src/Application/UI/Presenter.php | 6 ++++++ src/Bridges/ApplicationLatte/TemplateFactory.php | 2 +- .../TemplateFactory.nonce.presenter.phpt | 3 +-- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index f9b7f5eb2..7abc1290e 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -39,13 +39,22 @@ abstract class Component extends Nette\ComponentModel\Container implements ISign public function getPresenter(): ?Presenter { if (func_num_args()) { - trigger_error(__METHOD__ . '() parameter $throw is deprecated, use hasPresenter()', E_USER_DEPRECATED); + trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); $throw = func_get_arg(0); } return $this->lookup(Presenter::class, $throw ?? true); } + /** + * Returns the presenter where this component belongs to. + */ + public function getPresenterIfExists(): ?Presenter + { + return $this->lookup(Presenter::class, false); + } + + /** * Returns whether there is a presenter. */ diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index d1d55ed8b..a92809848 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -65,13 +65,22 @@ protected function validateParent(Nette\ComponentModel\IContainer $parent): void final public function getPresenter(): ?Presenter { if (func_num_args()) { - trigger_error(__METHOD__ . '() parameter $throw is deprecated, use hasPresenter()', E_USER_DEPRECATED); + trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); $throw = func_get_arg(0); } return $this->lookup(Presenter::class, $throw ?? true); } + /** + * Returns the presenter where this component belongs to. + */ + final public function getPresenterIfExists(): ?Presenter + { + return $this->lookup(Presenter::class, false); + } + + /** * Returns whether there is a presenter. */ diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 14945dfd9..0ed44094c 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -157,6 +157,12 @@ final public function getPresenter(): self } + final public function getPresenterIfExists(): self + { + return $this; + } + + final public function hasPresenter(): bool { return true; diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index 5ed5d1008..e34e0fec4 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -58,7 +58,7 @@ public function createTemplate(UI\Control $control = null): UI\ITemplate { $latte = $this->latteFactory->create(); $template = new $this->templateClass($latte); - $presenter = ($control && $control->hasPresenter()) ? $control->getPresenter() : null; + $presenter = $control ? $control->getPresenterIfExists() : null; if ($latte->onCompile instanceof \Traversable) { $latte->onCompile = iterator_to_array($latte->onCompile); diff --git a/tests/Bridges.Latte/TemplateFactory.nonce.presenter.phpt b/tests/Bridges.Latte/TemplateFactory.nonce.presenter.phpt index 5255f79be..b8bc9b7ee 100644 --- a/tests/Bridges.Latte/TemplateFactory.nonce.presenter.phpt +++ b/tests/Bridges.Latte/TemplateFactory.nonce.presenter.phpt @@ -24,8 +24,7 @@ $response = Mockery::mock(Nette\Http\IResponse::class); $response->shouldReceive('getHeader')->with('Content-Security-Policy')->andReturn("hello 'nonce-abcd123==' world"); $presenter = Mockery::mock(UI\Presenter::class); -$presenter->shouldReceive('hasPresenter')->andReturn(true); -$presenter->shouldReceive('getPresenter')->andReturn($presenter); +$presenter->shouldReceive('getPresenterIfExists')->andReturn($presenter); $presenter->shouldReceive('getHttpResponse')->andReturn($response); $presenter->shouldIgnoreMissing(); From 56e9d459f778ee50cb3458b020291c0343118f78 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 16 Oct 2019 13:20:50 +0200 Subject: [PATCH 11/15] Component, Form: deprecated hasPresenter() --- src/Application/UI/Component.php | 4 +--- src/Application/UI/Form.php | 4 +--- src/Application/UI/Presenter.php | 1 + 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 7abc1290e..411233a18 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -55,9 +55,7 @@ public function getPresenterIfExists(): ?Presenter } - /** - * Returns whether there is a presenter. - */ + /** @deprecated */ public function hasPresenter(): bool { return (bool) $this->lookup(Presenter::class, false); diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index a92809848..9c69289b8 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -81,9 +81,7 @@ final public function getPresenterIfExists(): ?Presenter } - /** - * Returns whether there is a presenter. - */ + /** @deprecated */ public function hasPresenter(): bool { return (bool) $this->lookup(Presenter::class, false); diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index 0ed44094c..638ddeef9 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -163,6 +163,7 @@ final public function getPresenterIfExists(): self } + /** @deprecated */ final public function hasPresenter(): bool { return true; From b912f9651c89ba4eabd8e36f4c2e40b750527789 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20Ot=C3=A1hal?= Date: Mon, 21 Oct 2019 13:10:52 +0200 Subject: [PATCH 12/15] BadRequestException: parent exception is Throwable (#236) --- src/Application/exceptions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Application/exceptions.php b/src/Application/exceptions.php index 3f0717bca..cad173ea6 100644 --- a/src/Application/exceptions.php +++ b/src/Application/exceptions.php @@ -46,7 +46,7 @@ class BadRequestException extends \Exception protected $code = Http\IResponse::S404_NOT_FOUND; - public function __construct(string $message = '', int $httpCode = 0, \Exception $previous = null) + public function __construct(string $message = '', int $httpCode = 0, \Throwable $previous = null) { parent::__construct($message, $httpCode ?: $this->code, $previous); } From 7e6b48fdda7b85bc08cf1713e17e40b622b2b085 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 21 Oct 2019 20:06:15 +0200 Subject: [PATCH 13/15] tests: improved --- tests/Routers/Route.basic.phpt | 2 ++ tests/Routers/Route.extraDefaultParam.phpt | 3 +++ tests/Routers/Route.optional.module.phpt | 3 +++ 3 files changed, 8 insertions(+) diff --git a/tests/Routers/Route.basic.phpt b/tests/Routers/Route.basic.phpt index 369f39bcc..f344806ef 100644 --- a/tests/Routers/Route.basic.phpt +++ b/tests/Routers/Route.basic.phpt @@ -17,6 +17,8 @@ require __DIR__ . '/Route.php'; $route = new Route('//'); +Assert::same([], $route->getConstantParameters()); + Assert::same('http://example.com/homepage/', testRouteOut($route, ['presenter' => 'Homepage'])); Assert::same('http://example.com/homepage/', testRouteOut($route, ['presenter' => 'Homepage', 'action' => 'default'])); diff --git a/tests/Routers/Route.extraDefaultParam.phpt b/tests/Routers/Route.extraDefaultParam.phpt index b4427664d..c0e073552 100644 --- a/tests/Routers/Route.extraDefaultParam.phpt +++ b/tests/Routers/Route.extraDefaultParam.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Application\Routers\Route; +use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -18,6 +19,8 @@ $route = new Route('///', [ 'extra' => null, ]); +Assert::same(['extra' => null], $route->getConstantParameters()); + testRouteIn($route, '/presenter/action/12/any'); testRouteIn($route, '/presenter/action/12', [ diff --git a/tests/Routers/Route.optional.module.phpt b/tests/Routers/Route.optional.module.phpt index 5fa984011..b24b52517 100644 --- a/tests/Routers/Route.optional.module.phpt +++ b/tests/Routers/Route.optional.module.phpt @@ -7,6 +7,7 @@ declare(strict_types=1); use Nette\Application\Routers\Route; +use Tester\Assert; require __DIR__ . '/../bootstrap.php'; @@ -20,6 +21,8 @@ $route = new Route('[/]/', [ 'action' => 'default', ]); +Assert::same([], $route->getConstantParameters()); + testRouteIn($route, '/one', [ 'presenter' => 'Front:One', 'action' => 'default', From 69255746eaba0a4693f9e16fdd809a459c9acd17 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 21 Oct 2019 19:50:05 +0200 Subject: [PATCH 14/15] Route: fixed bug for constant module & presenter [Closes #216] --- src/Application/Routers/Route.php | 7 ++++- tests/Routers/Route.modules.fixity.phpt | 38 +++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/Routers/Route.modules.fixity.phpt diff --git a/src/Application/Routers/Route.php b/src/Application/Routers/Route.php index c5ba79dc9..5cca47530 100644 --- a/src/Application/Routers/Route.php +++ b/src/Application/Routers/Route.php @@ -139,7 +139,12 @@ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?stri public function getConstantParameters(): array { $res = parent::getConstantParameters(); - unset($res['module']); + if (isset($res[self::MODULE_KEY], $res[self::PRESENTER_KEY])) { + $res[self::PRESENTER_KEY] = $res[self::MODULE_KEY] . ':' . $res[self::PRESENTER_KEY]; + } elseif (isset($this->getMetadata()[self::MODULE_KEY])) { + unset($res[self::PRESENTER_KEY]); + } + unset($res[self::MODULE_KEY]); return $res; } diff --git a/tests/Routers/Route.modules.fixity.phpt b/tests/Routers/Route.modules.fixity.phpt new file mode 100644 index 000000000..c9d707f93 --- /dev/null +++ b/tests/Routers/Route.modules.fixity.phpt @@ -0,0 +1,38 @@ + 'Auth', + 'presenter' => 'Homepage', + 'action' => 'default', +]); + +Assert::same(['presenter' => 'Auth:Homepage', 'action' => 'default'], $route->getConstantParameters()); + + +$route = new Route('', [ + 'presenter' => 'Homepage', + 'action' => 'default', +]); + +Assert::same(['action' => 'default'], $route->getConstantParameters()); + + +$route = new Route('', [ + 'module' => 'Auth', + 'action' => 'default', +]); + +Assert::same(['action' => 'default'], $route->getConstantParameters()); From f7df426a5af59daec71f43e8ba6057422d8aaaef Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 21 Oct 2019 21:00:10 +0200 Subject: [PATCH 15/15] Form: added disableSameSiteProtection() see https://forum.nette.org/cs/32609-nette-samesite-trvale-jak-resit-iframe --- src/Application/UI/Form.php | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index 9c69289b8..9c821062c 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -20,6 +20,9 @@ class Form extends Nette\Forms\Form implements ISignalReceiver /** @var callable[]&(callable(Form $sender): void)[]; Occurs when form is attached to presenter */ public $onAnchor; + /** @var bool */ + private $sameSiteProtection = true; + /** * Application form constructor. @@ -97,6 +100,15 @@ public function isAnchored(): bool } + /** + * Disables CSRF protection using a SameSite cookie. + */ + public function disableSameSiteProtection(): void + { + $this->sameSiteProtection = false; + } + + /** * Internal: returns submitted HTTP data or null when form was not submitted. */ @@ -143,7 +155,7 @@ public function signalReceived(string $signal): void $class = get_class($this); throw new BadSignalException("Missing handler for signal '$signal' in $class."); - } elseif (!$this->getPresenter()->getHttpRequest()->isSameSite()) { + } elseif ($this->sameSiteProtection && !$this->getPresenter()->getHttpRequest()->isSameSite()) { $this->getPresenter()->detectedCsrf(); } elseif (!$this->getPresenter()->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) {