├── src ├── Application │ ├── UI │ │ ├── InvalidLinkException.php │ │ ├── BadSignalException.php │ │ ├── TemplateFactory.php │ │ ├── SignalReceiver.php │ │ ├── Renderable.php │ │ ├── StatePersistent.php │ │ ├── Template.php │ │ ├── Multiplier.php │ │ ├── Link.php │ │ ├── AccessPolicy.php │ │ ├── Form.php │ │ ├── Control.php │ │ ├── ParameterConverter.php │ │ ├── ComponentReflection.php │ │ └── Component.php │ ├── IPresenter.php │ ├── Attributes │ │ ├── Deprecated.php │ │ ├── TemplateVariable.php │ │ ├── Parameter.php │ │ ├── Persistent.php │ │ ├── CrossOrigin.php │ │ └── Requires.php │ ├── Responses │ │ ├── VoidResponse.php │ │ ├── ForwardResponse.php │ │ ├── CallbackResponse.php │ │ ├── TextResponse.php │ │ ├── RedirectResponse.php │ │ ├── JsonResponse.php │ │ └── FileResponse.php │ ├── Response.php │ ├── IPresenterFactory.php │ ├── Routers │ │ ├── SimpleRouter.php │ │ ├── CliRouter.php │ │ ├── RouteList.php │ │ └── Route.php │ ├── ErrorPresenter.php │ ├── Helpers.php │ ├── exceptions.php │ ├── templates │ │ └── error.phtml │ ├── Request.php │ ├── PresenterFactory.php │ ├── MicroPresenter.php │ ├── Application.php │ └── LinkGenerator.php ├── Bridges │ ├── ApplicationLatte │ │ ├── LatteFactory.php │ │ ├── Nodes │ │ │ ├── NNonceNode.php │ │ │ ├── IfCurrentNode.php │ │ │ ├── LinkBaseNode.php │ │ │ ├── SnippetAreaNode.php │ │ │ ├── LinkNode.php │ │ │ ├── ControlNode.php │ │ │ └── SnippetNode.php │ │ ├── DefaultTemplate.php │ │ ├── TemplateFactory.php │ │ ├── Template.php │ │ ├── SnippetRuntime.php │ │ ├── UIExtension.php │ │ └── TemplateGenerator.php │ ├── ApplicationTracy │ │ ├── tab.latte │ │ ├── dist │ │ │ ├── tab.phtml │ │ │ └── panel.phtml │ │ ├── RoutingPanel.php │ │ └── panel.latte │ └── ApplicationDI │ │ ├── PresenterFactoryCallback.php │ │ ├── RoutingExtension.php │ │ ├── LatteExtension.php │ │ └── ApplicationExtension.php └── compatibility-intf.php ├── readme.md ├── composer.json ├── license.md └── .phpstorm.meta.php /src/Application/UI/InvalidLinkException.php: -------------------------------------------------------------------------------- 1 | factory = $factory; 27 | } 28 | 29 | 30 | protected function createComponent(string $name): ?Nette\ComponentModel\IComponent 31 | { 32 | return ($this->factory)($name, $this); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/NNonceNode.php: -------------------------------------------------------------------------------- 1 | global->uiNonce ? " nonce=\"{$this->global->uiNonce}\"" : "";'; 30 | } 31 | 32 | 33 | public function &getIterator(): \Generator 34 | { 35 | false && yield; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Application/Responses/ForwardResponse.php: -------------------------------------------------------------------------------- 1 | request = $request; 26 | } 27 | 28 | 29 | public function getRequest(): Nette\Application\Request 30 | { 31 | return $this->request; 32 | } 33 | 34 | 35 | /** 36 | * Sends response to output. 37 | */ 38 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 39 | { 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/Responses/CallbackResponse.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 30 | } 31 | 32 | 33 | /** 34 | * Sends response to output. 35 | */ 36 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 37 | { 38 | ($this->callback)($httpRequest, $httpResponse); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Application/Responses/TextResponse.php: -------------------------------------------------------------------------------- 1 | source = $source; 26 | } 27 | 28 | 29 | public function getSource(): mixed 30 | { 31 | return $this->source; 32 | } 33 | 34 | 35 | /** 36 | * Sends response to output. 37 | */ 38 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 39 | { 40 | if ($this->source instanceof Nette\Application\UI\Template) { 41 | $this->source->render(); 42 | 43 | } else { 44 | echo $this->source; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Application/Attributes/Requires.php: -------------------------------------------------------------------------------- 1 | methods = $methods === null ? null : (array) $methods; 30 | $this->actions = $actions === null ? null : (array) $actions; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Application/Responses/RedirectResponse.php: -------------------------------------------------------------------------------- 1 | url = $url; 28 | $this->httpCode = $httpCode; 29 | } 30 | 31 | 32 | public function getUrl(): string 33 | { 34 | return $this->url; 35 | } 36 | 37 | 38 | public function getCode(): int 39 | { 40 | return $this->httpCode; 41 | } 42 | 43 | 44 | /** 45 | * Sends response to output. 46 | */ 47 | public function send(Http\IRequest $httpRequest, Http\IResponse $httpResponse): void 48 | { 49 | $httpResponse->redirect($this->url, $this->httpCode); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationTracy/tab.latte: -------------------------------------------------------------------------------- 1 | {use Nette\Application\UI\Presenter} 2 | 3 | 4 | 5 | 6 | 7 | 9 | {if $matched === null} 10 | no route 11 | {elseif isset($matched[Presenter::PresenterKey])} 12 | {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])} 13 | {$matched[Presenter::SignalKey]}!{/if} 14 | {/if} 15 | 16 | 17 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | Nette Application MVC 2 | ===================== 3 | 4 | [![Downloads this Month](https://img.shields.io/packagist/dm/nette/application.svg)](https://packagist.org/packages/nette/application) 5 | [![Tests](https://github.com/nette/application/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nette/application/actions) 6 | [![Latest Stable Version](https://poser.pugx.org/nette/application/v/stable)](https://github.com/nette/application/releases) 7 | [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/application/blob/master/license.md) 8 | 9 | Model-View-Controller is a software architecture that was created to satisfy the need to separate utility code (controller) from application logic code (model) and from code for displaying data (view) in applications with graphical user interface. With this approach we make the application better understandable, simplify future development and enable testing each unit of the application separately. 10 | 11 | Please, [see documentation](https://doc.nette.org/application). 12 | 13 | If you like Nette, **[please make a donation now](https://nette.org/donate)**. Thank you! 14 | -------------------------------------------------------------------------------- /src/Application/Routers/SimpleRouter.php: -------------------------------------------------------------------------------- 1 | $presenter, 35 | 'action' => $action === '' ? Application\UI\Presenter::DefaultAction : $action, 36 | ]; 37 | } 38 | 39 | parent::__construct($defaults); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Application/ErrorPresenter.php: -------------------------------------------------------------------------------- 1 | getParameter('exception'); 31 | if ($e instanceof Application\BadRequestException) { 32 | $code = $e->getHttpCode(); 33 | } else { 34 | $code = 500; 35 | $this->logger?->log($e, ILogger::EXCEPTION); 36 | } 37 | 38 | return new Application\Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void { 39 | if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { 40 | require __DIR__ . '/templates/error.phtml'; 41 | } 42 | }); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/DefaultTemplate.php: -------------------------------------------------------------------------------- 1 | $name = $value; 41 | return $this; 42 | } 43 | 44 | 45 | /** 46 | * Sets all parameters. 47 | */ 48 | public function setParameters(array $params): static 49 | { 50 | return Nette\Utils\Arrays::toObject($params, $this); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Application/Responses/JsonResponse.php: -------------------------------------------------------------------------------- 1 | payload = $payload; 27 | $this->contentType = $contentType ?? 'application/json'; 28 | } 29 | 30 | 31 | public function getPayload(): mixed 32 | { 33 | return $this->payload; 34 | } 35 | 36 | 37 | /** 38 | * Returns the MIME content type of a downloaded file. 39 | */ 40 | public function getContentType(): string 41 | { 42 | return $this->contentType; 43 | } 44 | 45 | 46 | /** 47 | * Sends response to output. 48 | */ 49 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 50 | { 51 | $httpResponse->setContentType($this->contentType, 'utf-8'); 52 | echo Nette\Utils\Json::encode($this->payload); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Application/Helpers.php: -------------------------------------------------------------------------------- 1 | $class] + class_parents($class); 42 | $addTraits = function (string $type) use (&$res, &$addTraits): void { 43 | $res += class_uses($type); 44 | foreach (class_uses($type) as $trait) { 45 | $addTraits($trait); 46 | } 47 | }; 48 | foreach ($res as $type) { 49 | $addTraits($type); 50 | } 51 | 52 | return $res; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationTracy/dist/tab.phtml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | no route 12 | 13 | : 14 | 15 | 16 | 17 | ! 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Application/exceptions.php: -------------------------------------------------------------------------------- 1 | code, $previous); 58 | } 59 | 60 | 61 | public function getHttpCode(): int 62 | { 63 | return $this->code; 64 | } 65 | } 66 | 67 | 68 | /** 69 | * Access to the requested resource is forbidden. 70 | */ 71 | class ForbiddenRequestException extends BadRequestException 72 | { 73 | /** @var int */ 74 | protected $code = Http\IResponse::S403_Forbidden; 75 | } 76 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationDI/PresenterFactoryCallback.php: -------------------------------------------------------------------------------- 1 | container->findByType($class); 32 | if (count($services) > 1) { 33 | $services = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); 34 | if (count($services) > 1) { 35 | throw new Nette\Application\InvalidPresenterException("Multiple services of type $class found: " . implode(', ', $services) . '.'); 36 | } 37 | } 38 | 39 | if (count($services) === 1) { 40 | return $this->container->createService($services[0]); 41 | } 42 | 43 | if ($this->touchToRefresh && class_exists($class)) { 44 | touch($this->touchToRefresh); 45 | echo 'Class ' . htmlspecialchars($class) . ' was not found in DI container.

If you just created this presenter, it should be enough to refresh the page. It will happen automatically in 5 seconds.

Otherwise, please check the configuration of your DI container.'; 46 | header('Refresh: 5'); 47 | exit; 48 | } 49 | 50 | throw new Nette\Application\InvalidPresenterException("No services of type $class found."); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Application/UI/Link.php: -------------------------------------------------------------------------------- 1 | component; 33 | } 34 | 35 | 36 | /** 37 | * Returns link destination. 38 | */ 39 | public function getDestination(): string 40 | { 41 | return $this->destination; 42 | } 43 | 44 | 45 | /** 46 | * Changes link parameter. 47 | */ 48 | public function setParameter(string $key, mixed $value): static 49 | { 50 | $this->params[$key] = $value; 51 | return $this; 52 | } 53 | 54 | 55 | /** 56 | * Returns link parameter. 57 | */ 58 | public function getParameter(string $key): mixed 59 | { 60 | return $this->params[$key] ?? null; 61 | } 62 | 63 | 64 | /** 65 | * Returns link parameters. 66 | */ 67 | public function getParameters(): array 68 | { 69 | return $this->params; 70 | } 71 | 72 | 73 | /** 74 | * Determines whether this links to the current page. 75 | */ 76 | public function isLinkCurrent(): bool 77 | { 78 | return $this->component->isLinkCurrent($this->destination, $this->params); 79 | } 80 | 81 | 82 | /** 83 | * Converts link to URL. 84 | */ 85 | public function __toString(): string 86 | { 87 | return $this->component->link($this->destination, $this->params); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nette/application", 3 | "description": "🏆 Nette Application: a full-stack component-based MVC kernel for PHP that helps you write powerful and modern web applications. Write less, have cleaner code and your work will bring you joy.", 4 | "keywords": ["nette", "mvc", "framework", "component-based", "routing", "seo", "mvp", "presenter", "control", "forms"], 5 | "homepage": "https://nette.org", 6 | "license": ["BSD-3-Clause", "GPL-2.0-only", "GPL-3.0-only"], 7 | "authors": [ 8 | { 9 | "name": "David Grudl", 10 | "homepage": "https://davidgrudl.com" 11 | }, 12 | { 13 | "name": "Nette Community", 14 | "homepage": "https://nette.org/contributors" 15 | } 16 | ], 17 | "require": { 18 | "php": "8.2 - 8.5", 19 | "nette/component-model": "^3.2", 20 | "nette/http": "^3.3.2", 21 | "nette/routing": "^3.1.1", 22 | "nette/utils": "^4.1" 23 | }, 24 | "suggest": { 25 | "nette/forms": "Allows to use Nette\\Application\\UI\\Form", 26 | "latte/latte": "Allows using Latte in templates" 27 | }, 28 | "require-dev": { 29 | "nette/tester": "^2.5.2", 30 | "nette/di": "^3.2", 31 | "nette/forms": "^3.2", 32 | "nette/robot-loader": "^4.0", 33 | "nette/security": "^3.2", 34 | "latte/latte": "^3.1", 35 | "tracy/tracy": "^2.11", 36 | "mockery/mockery": "1.6.x-dev", 37 | "phpstan/phpstan-nette": "^2.0@stable", 38 | "jetbrains/phpstorm-attributes": "^1.2" 39 | }, 40 | "conflict": { 41 | "nette/caching": "<3.2", 42 | "nette/di": "<3.2", 43 | "nette/forms": "<3.2", 44 | "nette/schema": "<1.3", 45 | "latte/latte": "<3.1 || >=3.2", 46 | "tracy/tracy": "<2.11" 47 | }, 48 | "autoload": { 49 | "classmap": ["src/"], 50 | "psr-4": { 51 | "Nette\\": "src" 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "scripts": { 56 | "phpstan": "phpstan analyse", 57 | "tester": "tester tests -s" 58 | }, 59 | "extra": { 60 | "branch-alias": { 61 | "dev-master": "4.0-dev" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/IfCurrentNode.php: -------------------------------------------------------------------------------- 1 | position->line})", E_USER_DEPRECATED); 34 | $node = $tag->node = new static; 35 | if (!$tag->parser->isEnd()) { 36 | $node->destination = $tag->parser->parseUnquotedStringOrExpression(); 37 | $tag->parser->stream->tryConsume(','); 38 | $node->args = $tag->parser->parseArguments(); 39 | } 40 | 41 | [$node->content] = yield; 42 | return $node; 43 | } 44 | 45 | 46 | public function print(PrintContext $context): string 47 | { 48 | return $this->destination 49 | ? $context->format( 50 | 'if ($this->global->uiPresenter->isLinkCurrent(%node, %args?)) { %node } ', 51 | $this->destination, 52 | $this->args, 53 | $this->content, 54 | ) 55 | : $context->format( 56 | 'if ($this->global->uiPresenter->getLastCreatedRequestFlag("current")) { %node } ', 57 | $this->content, 58 | ); 59 | } 60 | 61 | 62 | public function &getIterator(): \Generator 63 | { 64 | if ($this->destination) { 65 | yield $this->destination; 66 | yield $this->args; 67 | } 68 | yield $this->content; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/compatibility-intf.php: -------------------------------------------------------------------------------- 1 | expectArguments(); 37 | if (!$tag->isInHead()) { 38 | throw new CompileException("{{$tag->name}} must be placed in template head.", $tag->position); 39 | } 40 | 41 | $node = new static; 42 | $node->base = $tag->parser->parseUnquotedStringOrExpression(); 43 | return $node; 44 | } 45 | 46 | 47 | public function print(PrintContext $context): string 48 | { 49 | return ''; 50 | } 51 | 52 | 53 | public function &getIterator(): \Generator 54 | { 55 | yield $this->base; 56 | } 57 | 58 | 59 | public static function applyLinkBasePass(TemplateNode $node): void 60 | { 61 | $base = NodeHelpers::findFirst($node, fn(Node $node) => $node instanceof self)?->base; 62 | if ($base === null) { 63 | return; 64 | } 65 | 66 | (new NodeTraverser)->traverse($node, function (Node $link) use ($base) { 67 | if ($link instanceof LinkNode) { 68 | if ($link->destination instanceof StringNode && $base instanceof StringNode) { 69 | $link->destination->value = LinkGenerator::applyBase($link->destination->value, $base->value); 70 | } else { 71 | $origDestination = $link->destination; 72 | $link->destination = new AuxiliaryNode( 73 | fn(PrintContext $context) => $context->format( 74 | LinkGenerator::class . '::applyBase(%node, %node)', 75 | $origDestination, 76 | $base, 77 | ), 78 | ); 79 | } 80 | } 81 | }); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Application/templates/error.phtml: -------------------------------------------------------------------------------- 1 | ['Oops...', 'Your browser sent a request that this server could not understand or process.'], 14 | 403 => ['Access Denied', 'You do not have permission to view this page. Please try contact the web site administrator if you believe you should be able to view this page.'], 15 | 404 => ['Page Not Found', 'The page you requested could not be found. It is possible that the address is incorrect, or that the page no longer exists. Please use a search engine to find what you are looking for.'], 16 | 405 => ['Method Not Allowed', 'The requested method is not allowed for the URL.'], 17 | 410 => ['Page Not Found', 'The page you requested has been taken off the site. We apologize for the inconvenience.'], 18 | 500 => ['Server Error', 'We\'re sorry! The server encountered an internal error and was unable to complete your request. Please try again later.'], 19 | ]; 20 | $message = $messages[$code] ?? $messages[0]; 21 | 22 | ?> 23 | 24 | 25 | 26 | 27 | <?= $message[0] ?> 28 | 29 | 37 | 38 |
39 |
40 |

41 | 42 |

43 | 44 |

error

45 |
46 |
47 | 48 | 51 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/SnippetAreaNode.php: -------------------------------------------------------------------------------- 1 | */ 35 | public static function create(Tag $tag, TemplateParser $parser): \Generator 36 | { 37 | $node = $tag->node = new static; 38 | $name = $tag->parser->parseUnquotedStringOrExpression(); 39 | if ( 40 | $name instanceof Expression\ClassConstantFetchNode 41 | && $name->class instanceof Php\NameNode 42 | && $name->name instanceof Php\IdentifierNode 43 | ) { 44 | $name = new Scalar\StringNode(constant($name->class . '::' . $name->name), $name->position); 45 | } 46 | $node->block = new Block($name, Template::LayerSnippet, $tag); 47 | $parser->checkBlockIsUnique($node->block); 48 | [$node->content, $endTag] = yield; 49 | if ($endTag && $name instanceof Scalar\StringNode) { 50 | $endTag->parser->stream->tryConsume($name->value); 51 | } 52 | return $node; 53 | } 54 | 55 | 56 | public function print(PrintContext $context): string 57 | { 58 | $context->addBlock($this->block); 59 | $this->block->content = $context->format( 60 | <<<'XX' 61 | $this->global->snippetDriver->enter(%node, %dump); 62 | try { 63 | %node 64 | } finally { 65 | $this->global->snippetDriver->leave(); 66 | } 67 | 68 | XX, 69 | $this->block->name, 70 | SnippetRuntime::TypeArea, 71 | $this->content, 72 | ); 73 | 74 | return $context->format( 75 | '$this->renderBlock(%node, [], null, %dump) %line;', 76 | $this->block->name, 77 | Template::LayerSnippet, 78 | $this->position, 79 | ); 80 | } 81 | 82 | 83 | public function &getIterator(): \Generator 84 | { 85 | yield $this->block->name; 86 | yield $this->content; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/LinkNode.php: -------------------------------------------------------------------------------- 1 | outputMode = $tag::OutputKeepIndentation; 36 | $tag->expectArguments(); 37 | $node = new static; 38 | $node->destination = $tag->parser->parseUnquotedStringOrExpression(); 39 | $tag->parser->stream->tryConsume(','); 40 | $node->args = $tag->parser->parseArguments(); 41 | $node->modifier = $tag->parser->parseModifier(); 42 | $node->modifier->escape = true; 43 | $node->mode = $tag->name; 44 | 45 | if ($tag->isNAttribute()) { 46 | // move at the beginning 47 | $node->position = $tag->position; 48 | array_unshift($tag->htmlElement->attributes->children, $node); 49 | return null; 50 | } 51 | 52 | return $node; 53 | } 54 | 55 | 56 | public function print(PrintContext $context): string 57 | { 58 | if ($this->mode === 'href') { 59 | $context->beginEscape()->enterHtmlAttribute(null); 60 | $res = $context->format( 61 | <<<'XX' 62 | echo ' href="'; echo %modify($this->global->uiControl->link(%node, %node?)) %line; echo '"'; 63 | XX, 64 | $this->modifier, 65 | $this->destination, 66 | $this->args, 67 | $this->position, 68 | ); 69 | $context->restoreEscape(); 70 | return $res; 71 | } 72 | 73 | return $context->format( 74 | 'echo %modify(' 75 | . ($this->mode === 'plink' ? '$this->global->uiPresenter' : '$this->global->uiControl') 76 | . '->link(%node, %node?)) %line;', 77 | $this->modifier, 78 | $this->destination, 79 | $this->args, 80 | $this->position, 81 | ); 82 | } 83 | 84 | 85 | public function &getIterator(): \Generator 86 | { 87 | yield $this->destination; 88 | yield $this->args; 89 | yield $this->modifier; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Application/Routers/CliRouter.php: -------------------------------------------------------------------------------- 1 | defaults = $defaults; 29 | } 30 | 31 | 32 | /** 33 | * Maps command line arguments to an array. 34 | */ 35 | public function match(Nette\Http\IRequest $httpRequest): ?array 36 | { 37 | if (empty($_SERVER['argv']) || !is_array($_SERVER['argv'])) { 38 | return null; 39 | } 40 | 41 | $names = [self::PresenterKey]; 42 | $params = $this->defaults; 43 | $args = $_SERVER['argv']; 44 | array_shift($args); 45 | $args[] = '--'; 46 | 47 | foreach ($args as $arg) { 48 | $opt = preg_replace('#/|-+#A', '', $arg); 49 | if ($opt === $arg) { 50 | if (isset($flag) || $flag = array_shift($names)) { 51 | $params[$flag] = $arg; 52 | } else { 53 | $params[] = $arg; 54 | } 55 | 56 | $flag = null; 57 | continue; 58 | } 59 | 60 | if (isset($flag)) { 61 | $params[$flag] = true; 62 | $flag = null; 63 | } 64 | 65 | if ($opt === '') { 66 | continue; 67 | } 68 | 69 | $pair = explode('=', $opt, 2); 70 | if (isset($pair[1])) { 71 | $params[$pair[0]] = $pair[1]; 72 | } else { 73 | $flag = $pair[0]; 74 | } 75 | } 76 | 77 | if (!isset($params[self::PresenterKey])) { 78 | throw new Nette\InvalidStateException('Missing presenter & action in route definition.'); 79 | } 80 | 81 | [$module, $presenter] = Nette\Application\Helpers::splitName($params[self::PresenterKey]); 82 | if ($module !== '') { 83 | $params[self::PresenterKey] = $presenter; 84 | $presenter = $module; 85 | } 86 | 87 | $params['presenter'] = $presenter; 88 | 89 | return $params; 90 | } 91 | 92 | 93 | /** 94 | * This router is only unidirectional. 95 | */ 96 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 97 | { 98 | return null; 99 | } 100 | 101 | 102 | /** 103 | * Returns default values. 104 | */ 105 | public function getDefaults(): array 106 | { 107 | return $this->defaults; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | Licenses 2 | ======== 3 | 4 | Good news! You may use Nette Framework under the terms of either 5 | the New BSD License or the GNU General Public License (GPL) version 2 or 3. 6 | 7 | The BSD License is recommended for most projects. It is easy to understand and it 8 | places almost no restrictions on what you can do with the framework. If the GPL 9 | fits better to your project, you can use the framework under this license. 10 | 11 | You don't have to notify anyone which license you are using. You can freely 12 | use Nette Framework in commercial projects as long as the copyright header 13 | remains intact. 14 | 15 | Please be advised that the name "Nette Framework" is a protected trademark and its 16 | usage has some limitations. So please do not use word "Nette" in the name of your 17 | project or top-level domain, and choose a name that stands on its own merits. 18 | If your stuff is good, it will not take long to establish a reputation for yourselves. 19 | 20 | 21 | New BSD License 22 | --------------- 23 | 24 | Copyright (c) 2004, 2014 David Grudl (https://davidgrudl.com) 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without modification, 28 | are permitted provided that the following conditions are met: 29 | 30 | * Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | 33 | * Redistributions in binary form must reproduce the above copyright notice, 34 | this list of conditions and the following disclaimer in the documentation 35 | and/or other materials provided with the distribution. 36 | 37 | * Neither the name of "Nette Framework" nor the names of its contributors 38 | may be used to endorse or promote products derived from this software 39 | without specific prior written permission. 40 | 41 | This software is provided by the copyright holders and contributors "as is" and 42 | any express or implied warranties, including, but not limited to, the implied 43 | warranties of merchantability and fitness for a particular purpose are 44 | disclaimed. In no event shall the copyright owner or contributors be liable for 45 | any direct, indirect, incidental, special, exemplary, or consequential damages 46 | (including, but not limited to, procurement of substitute goods or services; 47 | loss of use, data, or profits; or business interruption) however caused and on 48 | any theory of liability, whether in contract, strict liability, or tort 49 | (including negligence or otherwise) arising in any way out of the use of this 50 | software, even if advised of the possibility of such damage. 51 | 52 | 53 | GNU General Public License 54 | -------------------------- 55 | 56 | GPL licenses are very very long, so instead of including them here we offer 57 | you URLs with full text: 58 | 59 | - [GPL version 2](http://www.gnu.org/licenses/gpl-2.0.html) 60 | - [GPL version 3](http://www.gnu.org/licenses/gpl-3.0.html) 61 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/TemplateFactory.php: -------------------------------------------------------------------------------- 1 | Occurs when a new template is created */ 23 | public array $onCreate = []; 24 | private string $templateClass; 25 | 26 | 27 | public function __construct( 28 | private readonly LatteFactory $latteFactory, 29 | private readonly ?Nette\Http\IRequest $httpRequest = null, 30 | private readonly ?Nette\Security\User $user = null, 31 | $templateClass = null, 32 | private bool $generate = false, 33 | ) { 34 | if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, true))) { 35 | throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.'); 36 | } 37 | 38 | $this->templateClass = $templateClass ?? DefaultTemplate::class; 39 | } 40 | 41 | 42 | public function createTemplate(?UI\Control $control = null, ?string $class = null): Template 43 | { 44 | $class ??= $this->templateClass; 45 | if (!is_a($class, Template::class, allow_string: true)) { 46 | throw new Nette\InvalidArgumentException("Class $class does not implement " . Template::class . ' or it does not exist.'); 47 | } 48 | 49 | $latte = $this->latteFactory->create($control); 50 | $template = $this->generate && $control instanceof UI\Presenter 51 | ? new TemplateGenerator($latte, $class, $control) 52 | : new $class($latte); 53 | $this->injectDefaultVariables($template, $control); 54 | 55 | Nette\Utils\Arrays::invoke($this->onCreate, $template); 56 | 57 | return $template; 58 | } 59 | 60 | 61 | private function injectDefaultVariables(Template $template, ?UI\Control $control): void 62 | { 63 | $presenter = $control?->getPresenterIfExists(); 64 | $baseUrl = $this->httpRequest 65 | ? rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/') 66 | : null; 67 | $flashes = $presenter instanceof UI\Presenter && $presenter->hasFlashSession() 68 | ? (array) $presenter->getFlashSession()->get($control->getParameterId('flash')) 69 | : []; 70 | 71 | $vars = [ 72 | 'user' => $this->user, 73 | 'baseUrl' => $baseUrl, 74 | 'basePath' => $baseUrl ? preg_replace('#https?://[^/]+#A', '', $baseUrl) : null, 75 | 'flashes' => $flashes, 76 | 'control' => $control, 77 | 'presenter' => $presenter, 78 | ]; 79 | 80 | foreach ($vars as $key => $value) { 81 | if ($value !== null && property_exists($template, $key)) { 82 | try { 83 | $template->$key = $value; 84 | } catch (\TypeError) { 85 | } 86 | } elseif ($template instanceof TemplateGenerator) { 87 | $template->addDefaultVariable($key, $value); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationDI/RoutingExtension.php: -------------------------------------------------------------------------------- 1 | Expect::bool(), 33 | 'routes' => Expect::arrayOf('string'), 34 | 'cache' => Expect::bool(false), 35 | ]); 36 | } 37 | 38 | 39 | public function loadConfiguration(): void 40 | { 41 | if (!$this->config->routes) { 42 | return; 43 | } 44 | 45 | $builder = $this->getContainerBuilder(); 46 | 47 | $router = $builder->addDefinition($this->prefix('router')) 48 | ->setFactory(Nette\Application\Routers\RouteList::class); 49 | 50 | foreach ($this->config->routes as $mask => $action) { 51 | $router->addSetup('$service->addRoute(?, ?)', [$mask, $action]); 52 | } 53 | 54 | if ($this->name === 'routing') { 55 | $builder->addAlias('router', $this->prefix('router')); 56 | } 57 | } 58 | 59 | 60 | public function beforeCompile(): void 61 | { 62 | $builder = $this->getContainerBuilder(); 63 | 64 | if ( 65 | $this->debugMode && 66 | ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) && 67 | ($name = $builder->getByType(Nette\Application\Application::class)) && 68 | ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition 69 | ) { 70 | $application->addSetup('@Tracy\Bar::addPanel', [ 71 | new Definitions\Statement(Nette\Bridges\ApplicationTracy\RoutingPanel::class), 72 | ]); 73 | } 74 | 75 | if (!$builder->getByType(Nette\Routing\Router::class)) { 76 | $builder->addDefinition($this->prefix('router')) 77 | ->setType(Nette\Routing\Router::class) 78 | ->setFactory(Nette\Routing\SimpleRouter::class); 79 | $builder->addAlias('router', $this->prefix('router')); 80 | } 81 | } 82 | 83 | 84 | public function afterCompile(Nette\PhpGenerator\ClassType $class): void 85 | { 86 | if ($this->config->cache) { 87 | $builder = $this->getContainerBuilder(); 88 | $def = $builder->getDefinitionByType(Nette\Routing\Router::class); 89 | $method = $class->getMethod(Nette\DI\Container::getMethodName($def->getName())); 90 | try { 91 | $router = eval($method->getBody()); 92 | if ($router instanceof Nette\Application\Routers\RouteList) { 93 | $router->warmupCache(); 94 | } 95 | 96 | $s = serialize($router); 97 | } catch (\Throwable $e) { 98 | throw new Nette\DI\ServiceCreationException('Unable to cache router due to error: ' . $e->getMessage(), 0, $e); 99 | } 100 | 101 | $method->setBody('return unserialize(?);', [$s]); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@'])); 67 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Template.php: -------------------------------------------------------------------------------- 1 | latte; 33 | } 34 | 35 | 36 | /** 37 | * Renders template to output. 38 | */ 39 | public function render(?string $file = null, array $params = []): void 40 | { 41 | Nette\Utils\Arrays::toObject($params, $this); 42 | $this->latte->render($file ?? $this->file, $this); 43 | } 44 | 45 | 46 | /** 47 | * Renders template to output. 48 | */ 49 | public function renderToString(?string $file = null, array $params = []): string 50 | { 51 | Nette\Utils\Arrays::toObject($params, $this); 52 | return $this->latte->renderToString($file ?? $this->file, $this); 53 | } 54 | 55 | 56 | /** 57 | * Renders template to string. 58 | */ 59 | public function __toString(): string 60 | { 61 | return $this->latte->renderToString($this->file, $this->getParameters()); 62 | } 63 | 64 | 65 | /********************* template filters & helpers ****************d*g**/ 66 | 67 | 68 | /** 69 | * Registers run-time filter. 70 | */ 71 | public function addFilter(?string $name, callable $callback): static 72 | { 73 | $this->latte->addFilter($name, $callback); 74 | return $this; 75 | } 76 | 77 | 78 | /** 79 | * Registers run-time function. 80 | */ 81 | public function addFunction(string $name, callable $callback): static 82 | { 83 | $this->latte->addFunction($name, $callback); 84 | return $this; 85 | } 86 | 87 | 88 | /** 89 | * Sets translate adapter. 90 | */ 91 | public function setTranslator(?Nette\Localization\Translator $translator, ?string $language = null): static 92 | { 93 | $this->latte->addExtension(new Latte\Essential\TranslatorExtension($translator, $language)); 94 | return $this; 95 | } 96 | 97 | 98 | /********************* template parameters ****************d*g**/ 99 | 100 | 101 | /** 102 | * Sets the path to the template file. 103 | */ 104 | public function setFile(string $file): static 105 | { 106 | $this->file = $file; 107 | return $this; 108 | } 109 | 110 | 111 | final public function getFile(): ?string 112 | { 113 | return $this->file; 114 | } 115 | 116 | 117 | /** 118 | * Returns array of all parameters. 119 | */ 120 | public function getParameters(): array 121 | { 122 | $res = []; 123 | foreach ((new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { 124 | if ($prop->isInitialized($this)) { 125 | $res[$prop->getName()] = $prop->getValue($this); 126 | } 127 | } 128 | 129 | return $res; 130 | } 131 | 132 | 133 | /** 134 | * Prevents unserialization. 135 | */ 136 | final public function __unserialize($_) 137 | { 138 | throw new Nette\NotImplementedException('Object unserialization is not supported by class ' . static::class); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Application/Responses/FileResponse.php: -------------------------------------------------------------------------------- 1 | file = $file; 39 | $this->name = $name ?? basename($file); 40 | $this->contentType = $contentType ?? 'application/octet-stream'; 41 | $this->forceDownload = $forceDownload; 42 | } 43 | 44 | 45 | /** 46 | * Returns the path to a downloaded file. 47 | */ 48 | public function getFile(): string 49 | { 50 | return $this->file; 51 | } 52 | 53 | 54 | /** 55 | * Returns the file name. 56 | */ 57 | public function getName(): string 58 | { 59 | return $this->name; 60 | } 61 | 62 | 63 | /** 64 | * Returns the MIME content type of a downloaded file. 65 | */ 66 | public function getContentType(): string 67 | { 68 | return $this->contentType; 69 | } 70 | 71 | 72 | /** 73 | * Sends response to output. 74 | */ 75 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 76 | { 77 | $httpResponse->setContentType($this->contentType); 78 | $httpResponse->setHeader( 79 | 'Content-Disposition', 80 | ($this->forceDownload ? 'attachment' : 'inline') 81 | . '; filename="' . $this->name . '"' 82 | . '; filename*=utf-8\'\'' . rawurlencode($this->name), 83 | ); 84 | 85 | $filesize = $length = filesize($this->file); 86 | $handle = fopen($this->file, 'r'); 87 | if (!$handle) { 88 | throw new Nette\Application\BadRequestException("Cannot open file: '{$this->file}'."); 89 | } 90 | 91 | if ($this->resuming) { 92 | $httpResponse->setHeader('Accept-Ranges', 'bytes'); 93 | if (preg_match('#^bytes=(\d*)-(\d*)$#D', (string) $httpRequest->getHeader('Range'), $matches)) { 94 | [, $start, $end] = $matches; 95 | if ($start === '') { 96 | $start = max(0, $filesize - $end); 97 | $end = $filesize - 1; 98 | 99 | } elseif ($end === '' || $end > $filesize - 1) { 100 | $end = $filesize - 1; 101 | } 102 | 103 | if ($end < $start) { 104 | $httpResponse->setCode(416); // requested range not satisfiable 105 | return; 106 | } 107 | 108 | $httpResponse->setCode(206); 109 | $httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize); 110 | $length = $end - $start + 1; 111 | fseek($handle, (int) $start); 112 | 113 | } else { 114 | $httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize); 115 | } 116 | } 117 | 118 | $httpResponse->setHeader('Content-Length', (string) $length); 119 | while (!feof($handle) && $length > 0) { 120 | echo $s = fread($handle, min(4_000_000, $length)); 121 | $length -= strlen($s); 122 | } 123 | 124 | fclose($handle); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/SnippetRuntime.php: -------------------------------------------------------------------------------- 1 | */ 30 | private array $stack = []; 31 | private int $nestingLevel = 0; 32 | private bool $renderingSnippets = false; 33 | 34 | private ?\stdClass $payload; 35 | 36 | 37 | public function __construct( 38 | private readonly Control $control, 39 | ) { 40 | } 41 | 42 | 43 | public function enter(string $name, string $type): void 44 | { 45 | if (!$this->renderingSnippets) { 46 | if ($type === self::TypeDynamic && $this->nestingLevel === 0) { 47 | trigger_error('Dynamic snippets are allowed only inside static snippet/snippetArea.', E_USER_WARNING); 48 | } 49 | 50 | $this->nestingLevel++; 51 | return; 52 | } 53 | 54 | $obStarted = false; 55 | if ( 56 | ($this->nestingLevel === 0 && $this->control->isControlInvalid($name)) 57 | || ($type === self::TypeDynamic && ($previous = end($this->stack)) && $previous[1] === true) 58 | ) { 59 | ob_start(fn() => ''); 60 | $this->nestingLevel = $type === self::TypeArea ? 0 : 1; 61 | $obStarted = true; 62 | } elseif ($this->nestingLevel > 0) { 63 | $this->nestingLevel++; 64 | } 65 | 66 | $this->stack[] = [$name, $obStarted]; 67 | if ($name !== '') { 68 | $this->control->redrawControl($name, false); 69 | } 70 | } 71 | 72 | 73 | public function leave(): void 74 | { 75 | if (!$this->renderingSnippets) { 76 | $this->nestingLevel--; 77 | return; 78 | } 79 | 80 | [$name, $obStarted] = array_pop($this->stack); 81 | if ($this->nestingLevel > 0 && --$this->nestingLevel === 0) { 82 | $content = ob_get_clean(); 83 | $this->payload ??= $this->control->getPresenter()->getPayload(); 84 | $this->payload->snippets[$this->control->getSnippetId($name)] = $content; 85 | 86 | } elseif ($obStarted) { // dynamic snippet wrapper or snippet area 87 | ob_end_clean(); 88 | } 89 | } 90 | 91 | 92 | public function getHtmlId(string $name): string 93 | { 94 | return $this->control->getSnippetId($name); 95 | } 96 | 97 | 98 | /** 99 | * @param Block[] $blocks 100 | * @param mixed[] $params 101 | */ 102 | public function renderSnippets(array $blocks, array $params): bool 103 | { 104 | if ($this->renderingSnippets || !$this->control->snippetMode) { 105 | return false; 106 | } 107 | 108 | $this->renderingSnippets = true; 109 | $this->control->snippetMode = false; 110 | foreach ($blocks as $name => $block) { 111 | if (!$this->control->isControlInvalid($name)) { 112 | continue; 113 | } 114 | 115 | $function = reset($block->functions); 116 | $function($params); 117 | } 118 | 119 | $this->control->snippetMode = true; 120 | $this->renderChildren(); 121 | 122 | $this->renderingSnippets = false; 123 | return true; 124 | } 125 | 126 | 127 | private function renderChildren(): void 128 | { 129 | $queue = [$this->control]; 130 | do { 131 | foreach (array_shift($queue)->getComponents() as $child) { 132 | if ($child instanceof Renderable) { 133 | if ($child->isControlInvalid()) { 134 | $child->snippetMode = true; 135 | $child->render(); 136 | $child->snippetMode = false; 137 | } 138 | } elseif ($child instanceof Nette\ComponentModel\IContainer) { 139 | $queue[] = $child; 140 | } 141 | } 142 | } while ($queue); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Application/Request.php: -------------------------------------------------------------------------------- 1 | name = $name; 56 | return $this; 57 | } 58 | 59 | 60 | /** 61 | * Retrieve the presenter name. 62 | */ 63 | public function getPresenterName(): string 64 | { 65 | return $this->name; 66 | } 67 | 68 | 69 | /** 70 | * Sets variables provided to the presenter. 71 | */ 72 | public function setParameters(array $params): static 73 | { 74 | $this->params = $params; 75 | return $this; 76 | } 77 | 78 | 79 | /** 80 | * Returns all variables provided to the presenter (usually via URL). 81 | */ 82 | public function getParameters(): array 83 | { 84 | return $this->params; 85 | } 86 | 87 | 88 | /** 89 | * Returns a parameter provided to the presenter. 90 | */ 91 | public function getParameter(string $key): mixed 92 | { 93 | return $this->params[$key] ?? null; 94 | } 95 | 96 | 97 | /** 98 | * Sets variables provided to the presenter via POST. 99 | */ 100 | public function setPost(array $params): static 101 | { 102 | $this->post = $params; 103 | return $this; 104 | } 105 | 106 | 107 | /** 108 | * Returns a variable provided to the presenter via POST. 109 | * If no key is passed, returns the entire array. 110 | */ 111 | public function getPost(?string $key = null): mixed 112 | { 113 | return func_num_args() === 0 114 | ? $this->post 115 | : ($this->post[$key] ?? null); 116 | } 117 | 118 | 119 | /** 120 | * Sets all uploaded files. 121 | */ 122 | public function setFiles(array $files): static 123 | { 124 | $this->files = $files; 125 | return $this; 126 | } 127 | 128 | 129 | /** 130 | * Returns all uploaded files. 131 | */ 132 | public function getFiles(): array 133 | { 134 | return $this->files; 135 | } 136 | 137 | 138 | /** 139 | * Sets the method. 140 | */ 141 | public function setMethod(?string $method): static 142 | { 143 | $this->method = $method; 144 | return $this; 145 | } 146 | 147 | 148 | /** 149 | * Returns the method. 150 | */ 151 | public function getMethod(): ?string 152 | { 153 | return $this->method; 154 | } 155 | 156 | 157 | /** 158 | * Checks if the method is the given one. 159 | */ 160 | public function isMethod(string $method): bool 161 | { 162 | return strcasecmp($this->method ?? '', $method) === 0; 163 | } 164 | 165 | 166 | /** 167 | * Sets the flag. 168 | */ 169 | public function setFlag(string $flag, bool $value = true): static 170 | { 171 | $this->flags[$flag] = $value; 172 | return $this; 173 | } 174 | 175 | 176 | /** 177 | * Checks the flag. 178 | */ 179 | public function hasFlag(string $flag): bool 180 | { 181 | return !empty($this->flags[$flag]); 182 | } 183 | 184 | 185 | public function toArray(): array 186 | { 187 | $params = $this->params; 188 | $params['presenter'] = $this->name; 189 | return $params; 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Application/Routers/RouteList.php: -------------------------------------------------------------------------------- 1 | module = $module ? $module . ':' : null; 31 | } 32 | 33 | 34 | /** 35 | * Support for modules. 36 | */ 37 | protected function completeParameters(array $params): ?array 38 | { 39 | $presenter = $params[self::PresenterKey] ?? null; 40 | if (is_string($presenter) && strncmp($presenter, 'Nette:', 6)) { 41 | $params[self::PresenterKey] = $this->module . $presenter; 42 | } 43 | 44 | return $params; 45 | } 46 | 47 | 48 | /** 49 | * Constructs absolute URL from array. 50 | */ 51 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 52 | { 53 | if ($this->module) { 54 | if (strncmp($params[self::PresenterKey], $this->module, strlen($this->module)) !== 0) { 55 | return null; 56 | } 57 | 58 | $params[self::PresenterKey] = substr($params[self::PresenterKey], strlen($this->module)); 59 | } 60 | 61 | return parent::constructUrl($params, $refUrl); 62 | } 63 | 64 | 65 | public function addRoute( 66 | #[Language('TEXT')] 67 | string $mask, 68 | array|string|\Closure $metadata = [], 69 | bool $oneWay = false, 70 | ): static 71 | { 72 | $this->add(new Route($mask, $metadata), $oneWay); 73 | return $this; 74 | } 75 | 76 | 77 | public function withModule(string $module): static 78 | { 79 | $router = new static; 80 | $router->module = $module . ':'; 81 | $router->parent = $this; 82 | $this->add($router); 83 | return $router; 84 | } 85 | 86 | 87 | public function getModule(): ?string 88 | { 89 | return $this->module; 90 | } 91 | 92 | 93 | /** 94 | * @param mixed $index 95 | * @param Nette\Routing\Router $router 96 | */ 97 | public function offsetSet($index, $router): void 98 | { 99 | if ($router instanceof Route) { 100 | trigger_error('Usage `$router[] = new Route(...)` is deprecated, use `$router->addRoute(...)`.', E_USER_DEPRECATED); 101 | } else { 102 | $class = getclass($router); 103 | trigger_error("Usage `\$router[] = new $class` is deprecated, use `\$router->add(new $class)`.", E_USER_DEPRECATED); 104 | } 105 | 106 | if ($index === null) { 107 | $this->add($router); 108 | } else { 109 | $this->modify($index, $router); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * @param int $index 116 | * @throws Nette\OutOfRangeException 117 | */ 118 | public function offsetGet($index): Nette\Routing\Router 119 | { 120 | trigger_error('Usage `$route = $router[...]` is deprecated, use `$router->getRouters()`.', E_USER_DEPRECATED); 121 | if (!$this->offsetExists($index)) { 122 | throw new Nette\OutOfRangeException('Offset invalid or out of range'); 123 | } 124 | 125 | return $this->getRouters()[$index]; 126 | } 127 | 128 | 129 | /** 130 | * @param int $index 131 | */ 132 | public function offsetExists($index): bool 133 | { 134 | trigger_error('Usage `isset($router[...])` is deprecated.', E_USER_DEPRECATED); 135 | return is_int($index) && $index >= 0 && $index < count($this->getRouters()); 136 | } 137 | 138 | 139 | /** 140 | * @param int $index 141 | * @throws Nette\OutOfRangeException 142 | */ 143 | public function offsetUnset($index): void 144 | { 145 | trigger_error('Usage `unset($router[$index])` is deprecated, use `$router->modify($index, null)`.', E_USER_DEPRECATED); 146 | if (!$this->offsetExists($index)) { 147 | throw new Nette\OutOfRangeException('Offset invalid or out of range'); 148 | } 149 | 150 | $this->modify($index, null); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/ControlNode.php: -------------------------------------------------------------------------------- 1 | outputMode = $tag::OutputRemoveIndentation; 39 | $tag->expectArguments(); 40 | $stream = $tag->parser->stream; 41 | $node = new static; 42 | $node->name = $tag->parser->parseUnquotedStringOrExpression(colon: false); 43 | if ($stream->tryConsume(':')) { 44 | $node->method = $tag->parser->parseExpression(); 45 | } 46 | 47 | $stream->tryConsume(','); 48 | $start = $stream->getIndex(); 49 | $node->args = $tag->parser->parseArguments(); 50 | $start -= $stream->getIndex(); 51 | $depth = $wrap = null; 52 | for (; $start < 0; $start++) { 53 | $token = $stream->peek($start); 54 | match (true) { 55 | $token->is('[') => $depth++, 56 | $token->is(']') => $depth--, 57 | $token->is('=>') && !$depth => $wrap = true, 58 | default => null, 59 | }; 60 | } 61 | 62 | if ($wrap) { 63 | $node->args = new ArrayNode([new ArrayItemNode($node->args)]); 64 | } 65 | 66 | $modifier = $tag->parser->parseModifier(); 67 | $node->escape = $modifier->removeFilter('noescape') ? false : null; 68 | if ($modifier->filters) { 69 | throw new Latte\CompileException('Only modifier |noescape is allowed here.', reset($modifier->filters)->position); 70 | } 71 | 72 | return $node; 73 | } 74 | 75 | 76 | public function print(PrintContext $context): string 77 | { 78 | if ($this->escape === null && $context->getEscaper()->getState() !== Escaper::HtmlText) { 79 | $this->escape = true; 80 | } 81 | 82 | $method = match (true) { 83 | !$this->method => 'render', 84 | $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' . ucfirst($this->method->value), 85 | default => "{'render' . " . $this->method->print($context) . '}', 86 | }; 87 | 88 | $fetchCode = $context->format( 89 | $this->name instanceof StringNode 90 | ? '$ʟ_tmp = $this->global->uiControl->getComponent(%node);' 91 | : 'if (!is_object($ʟ_tmp = %node)) $ʟ_tmp = $this->global->uiControl->getComponent($ʟ_tmp);', 92 | $this->name, 93 | ); 94 | 95 | if ($this->escape) { 96 | return $context->format( 97 | <<<'XX' 98 | %raw 99 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); 100 | ob_start(fn() => ''); 101 | $ʟ_tmp->%raw(%args) %line; 102 | $ʟ_fi = new LR\FilterInfo(%dump); echo %modifyContent(ob_get_clean()); 103 | 104 | 105 | XX, 106 | $fetchCode, 107 | $method, 108 | $this->args, 109 | $this->position, 110 | Latte\ContentType::Html, 111 | new ModifierNode([], $this->escape), 112 | ); 113 | 114 | } else { 115 | return $context->format( 116 | <<<'XX' 117 | %raw 118 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); 119 | $ʟ_tmp->%raw(%args) %line; 120 | 121 | 122 | XX, 123 | $fetchCode, 124 | $method, 125 | $this->args, 126 | $this->position, 127 | ); 128 | } 129 | } 130 | 131 | 132 | public function &getIterator(): \Generator 133 | { 134 | yield $this->name; 135 | if ($this->method) { 136 | yield $this->method; 137 | } 138 | yield $this->args; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Application/PresenterFactory.php: -------------------------------------------------------------------------------- 1 | splited mask */ 22 | private array $mapping = [ 23 | '*' => ['App\Presentation\\', '*\\', '**Presenter'], 24 | 'Nette' => ['NetteModule\\', '*\\', '*Presenter'], 25 | ]; 26 | 27 | private array $aliases = []; 28 | private array $cache = []; 29 | 30 | /** @var callable */ 31 | private $factory; 32 | 33 | 34 | /** 35 | * @param ?callable(string): IPresenter $factory 36 | */ 37 | public function __construct(?callable $factory = null) 38 | { 39 | $this->factory = $factory ?? fn(string $class): IPresenter => new $class; 40 | } 41 | 42 | 43 | /** 44 | * Creates new presenter instance. 45 | */ 46 | public function createPresenter(string $name): IPresenter 47 | { 48 | return ($this->factory)($this->getPresenterClass($name)); 49 | } 50 | 51 | 52 | /** 53 | * Generates and checks presenter class name. 54 | * @throws InvalidPresenterException 55 | */ 56 | public function getPresenterClass(string &$name): string 57 | { 58 | if (isset($this->cache[$name])) { 59 | return $this->cache[$name]; 60 | } 61 | 62 | $class = $this->formatPresenterClass($name); 63 | if (!class_exists($class)) { 64 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' was not found."); 65 | } 66 | 67 | $reflection = new \ReflectionClass($class); 68 | $class = $reflection->getName(); 69 | if (!$reflection->implementsInterface(IPresenter::class)) { 70 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is not Nette\\Application\\IPresenter implementor."); 71 | } elseif ($reflection->isAbstract()) { 72 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is abstract."); 73 | } 74 | 75 | return $this->cache[$name] = $class; 76 | } 77 | 78 | 79 | /** 80 | * Sets mapping as pairs [module => mask] 81 | */ 82 | public function setMapping(array $mapping): static 83 | { 84 | foreach ($mapping as $module => $mask) { 85 | if (is_string($mask)) { 86 | if (!preg_match('#^\\\?([\w\\\]*\\\)?(\w*\*\w*?\\\)?([\w\\\]*\*\*?\w*)$#D', $mask, $m)) { 87 | throw new Nette\InvalidStateException("Invalid mapping mask '$mask'."); 88 | } 89 | 90 | $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]]; 91 | } elseif (is_array($mask) && count($mask) === 3) { 92 | $this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]]; 93 | } else { 94 | throw new Nette\InvalidStateException("Invalid mapping mask for module $module."); 95 | } 96 | } 97 | 98 | return $this; 99 | } 100 | 101 | 102 | /** 103 | * Formats presenter class name from its name. 104 | * @internal 105 | */ 106 | public function formatPresenterClass(string $presenter): string 107 | { 108 | if (!Nette\Utils\Strings::match($presenter, '#^[a-zA-Z\x7f-\xff][a-zA-Z0-9\x7f-\xff:]*$#D')) { 109 | throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$presenter' is invalid."); 110 | } 111 | $parts = explode(':', $presenter); 112 | $mapping = isset($parts[1], $this->mapping[$parts[0]]) 113 | ? $this->mapping[array_shift($parts)] 114 | : $this->mapping['*']; 115 | 116 | while ($part = array_shift($parts)) { 117 | $mapping[0] .= strtr($mapping[$parts ? 1 : 2], ['**' => "$part\\$part", '*' => $part]); 118 | } 119 | 120 | return $mapping[0]; 121 | } 122 | 123 | 124 | /** 125 | * Sets pairs [alias => destination] 126 | */ 127 | public function setAliases(array $aliases): static 128 | { 129 | $this->aliases = $aliases; 130 | return $this; 131 | } 132 | 133 | 134 | public function getAlias(string $alias): string 135 | { 136 | return $this->aliases[$alias] ?? throw new Nette\InvalidStateException("Link alias '$alias' was not found."); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/UIExtension.php: -------------------------------------------------------------------------------- 1 | control?->getPresenterIfExists(); 36 | return [ 37 | 'modifyDate' => fn($time, $delta, $unit = null) => $time 38 | ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) 39 | : null, 40 | ] + ($presenter ? [ 41 | 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null 42 | ? null 43 | : $presenter->getHttpRequest()->getUrl()->resolve((string) $link)->getAbsoluteUrl(), 44 | ] : []); 45 | } 46 | 47 | 48 | public function getFunctions(): array 49 | { 50 | if ($presenter = $this->control?->getPresenterIfExists()) { 51 | return [ 52 | 'isLinkCurrent' => $presenter->isLinkCurrent(...), 53 | 'isModuleCurrent' => $presenter->isModuleCurrent(...), 54 | ]; 55 | } 56 | return []; 57 | } 58 | 59 | 60 | public function getProviders(): array 61 | { 62 | $presenter = $this->control?->getPresenterIfExists(); 63 | $httpResponse = $presenter?->getHttpResponse(); 64 | return [ 65 | 'coreParentFinder' => $this->findLayoutTemplate(...), 66 | 'uiControl' => $this->control, 67 | 'uiPresenter' => $presenter, 68 | 'snippetDriver' => $this->control ? new SnippetRuntime($this->control) : null, 69 | 'uiNonce' => $httpResponse ? $this->findNonce($httpResponse) : null, 70 | ]; 71 | } 72 | 73 | 74 | public function getTags(): array 75 | { 76 | return [ 77 | 'n:href' => Nodes\LinkNode::create(...), 78 | 'n:nonce' => Nodes\NNonceNode::create(...), 79 | 'control' => Nodes\ControlNode::create(...), 80 | 'plink' => Nodes\LinkNode::create(...), 81 | 'link' => Nodes\LinkNode::create(...), 82 | 'linkBase' => Nodes\LinkBaseNode::create(...), 83 | 'ifCurrent' => Nodes\IfCurrentNode::create(...), 84 | 'snippet' => Nodes\SnippetNode::create(...), 85 | 'snippetArea' => Nodes\SnippetAreaNode::create(...), 86 | 'layout' => $this->createExtendsNode(...), 87 | 'extends' => $this->createExtendsNode(...), 88 | ]; 89 | } 90 | 91 | 92 | public function getPasses(): array 93 | { 94 | return [ 95 | 'snippetRendering' => $this->snippetRenderingPass(...), 96 | 'applyLinkBase' => [Nodes\LinkBaseNode::class, 'applyLinkBasePass'], 97 | ]; 98 | } 99 | 100 | 101 | /** 102 | * Render snippets instead of template in snippet-mode. 103 | */ 104 | public function snippetRenderingPass(TemplateNode $templateNode): void 105 | { 106 | array_unshift($templateNode->main->children, new Latte\Compiler\Nodes\AuxiliaryNode(fn() => <<<'XX' 107 | if ($this->global->snippetDriver?->renderSnippets($this->blocks[self::LayerSnippet], $this->params)) { return; } 108 | 109 | 110 | XX)); 111 | } 112 | 113 | 114 | public static function findLayoutTemplate(Latte\Runtime\Template $template): ?string 115 | { 116 | $presenter = $template->global->uiControl ?? null; 117 | return $presenter instanceof UI\Presenter && !empty($template::Blocks[$template::LayerTop]) 118 | ? $presenter->findLayoutTemplateFile() 119 | : null; 120 | } 121 | 122 | 123 | private function findNonce(Nette\Http\IResponse $httpResponse): ?string 124 | { 125 | $header = $httpResponse->getHeader('Content-Security-Policy') 126 | ?: $httpResponse->getHeader('Content-Security-Policy-Report-Only'); 127 | return preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null; 128 | } 129 | 130 | 131 | public static function createExtendsNode(Tag $tag): ExtendsNode 132 | { 133 | $auto = $tag->parser->stream->is('auto'); 134 | $node = ExtendsNode::create($tag); 135 | if ($auto) { 136 | $node->extends = new AuxiliaryNode(fn() => '$this->global->uiPresenter->findLayoutTemplateFile()'); 137 | } 138 | return $node; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationTracy/RoutingPanel.php: -------------------------------------------------------------------------------- 1 | routes = $this->analyse( 41 | $this->router instanceof Routing\RouteList 42 | ? $this->router 43 | : (new Routing\RouteList)->add($this->router), 44 | $this->httpRequest, 45 | ); 46 | return Nette\Utils\Helpers::capture(function () { 47 | $matched = $this->matched; 48 | require __DIR__ . '/dist/tab.phtml'; 49 | }); 50 | } 51 | 52 | 53 | /** 54 | * Renders panel. 55 | */ 56 | public function getPanel(): string 57 | { 58 | return Nette\Utils\Helpers::capture(function () { 59 | $matched = $this->matched; 60 | $routes = $this->routes; 61 | $source = $this->matched ? $this->findSource() : null; 62 | $url = $this->httpRequest->getUrl(); 63 | $method = $this->httpRequest->getMethod(); 64 | require __DIR__ . '/dist/panel.phtml'; 65 | }); 66 | } 67 | 68 | 69 | private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array 70 | { 71 | $res = [ 72 | 'path' => $router->getPath(), 73 | 'domain' => $router->getDomain(), 74 | 'module' => ($router instanceof Nette\Application\Routers\RouteList ? $router->getModule() : ''), 75 | 'routes' => [], 76 | ]; 77 | $httpRequest = $httpRequest 78 | ? (fn() => $this->prepareRequest($httpRequest))->bindTo($router, Routing\RouteList::class)() 79 | : null; 80 | $flags = $router->getFlags(); 81 | 82 | foreach ($router->getRouters() as $i => $innerRouter) { 83 | if ($innerRouter instanceof Routing\RouteList) { 84 | $res['routes'][] = $this->analyse($innerRouter, $httpRequest); 85 | continue; 86 | } 87 | 88 | $matched = empty($flags[$i]['oneWay']) ? 'no' : 'oneway'; 89 | $params = $e = null; 90 | try { 91 | if ( 92 | $httpRequest 93 | && ($params = $innerRouter->match($httpRequest)) !== null 94 | && ($params = (fn() => $this->completeParameters($params))->bindTo($router, Routing\RouteList::class)()) !== null 95 | ) { 96 | $matched = 'may'; 97 | if ($this->matched === null) { 98 | $this->matched = $params; 99 | $matched = 'yes'; 100 | } 101 | } 102 | } catch (\Throwable $e) { 103 | $matched = 'error'; 104 | } 105 | 106 | $res['routes'][] = (object) [ 107 | 'matched' => $matched, 108 | 'class' => $innerRouter::class, 109 | 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter ? $innerRouter->getDefaults() : [], 110 | 'mask' => $innerRouter instanceof Routing\Route ? $innerRouter->getMask() : null, 111 | 'params' => $params, 112 | 'error' => $e, 113 | ]; 114 | } 115 | return $res; 116 | } 117 | 118 | 119 | private function findSource(): \ReflectionClass|\ReflectionMethod|string|null 120 | { 121 | $params = $this->matched; 122 | $presenter = $params['presenter'] ?? ''; 123 | try { 124 | $class = $this->presenterFactory->getPresenterClass($presenter); 125 | } catch (Nette\Application\InvalidPresenterException) { 126 | if ($this->presenterFactory instanceof Nette\Application\PresenterFactory) { 127 | return $this->presenterFactory->formatPresenterClass($presenter); 128 | } 129 | return null; 130 | } 131 | 132 | if (is_a($class, Nette\Application\UI\Presenter::class, allow_string: true)) { 133 | $rc = $class::getReflection(); 134 | if (isset($params[Presenter::SignalKey])) { 135 | return $rc->getSignalMethod($params[Presenter::SignalKey]); 136 | } elseif (isset($params[Presenter::ActionKey]) 137 | && ($method = $rc->getActionRenderMethod($params[Presenter::ActionKey])) 138 | ) { 139 | return $method; 140 | } 141 | } 142 | 143 | return new \ReflectionClass($class); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Application/UI/AccessPolicy.php: -------------------------------------------------------------------------------- 1 | presenter ??= $component->getPresenterIfExists() ?? 36 | throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); 37 | 38 | $attrs = $this->getAttributes(); 39 | $attrs = self::applyInternalRules($attrs, $component); 40 | foreach ($attrs as $attribute) { 41 | $this->checkAttribute($attribute); 42 | } 43 | } 44 | 45 | 46 | public function canGenerateLink(): bool 47 | { 48 | $attrs = $this->getAttributes(); 49 | return !$attrs || !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->forward === true); 50 | } 51 | 52 | 53 | /** 54 | * @return Attributes\Requires[] 55 | */ 56 | private function getAttributes(): array 57 | { 58 | return array_map( 59 | fn($ra) => $ra->newInstance(), 60 | $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), 61 | ); 62 | } 63 | 64 | 65 | private function applyInternalRules(array $attrs, Component $component): array 66 | { 67 | if ( 68 | $this->element instanceof \ReflectionMethod 69 | && str_starts_with($this->element->getName(), $component::formatSignalMethod('')) 70 | && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin') 71 | && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) 72 | ) { 73 | $attrs[] = new Attributes\Requires(sameOrigin: true); 74 | } 75 | return $attrs; 76 | } 77 | 78 | 79 | private function checkAttribute(Attributes\Requires $attribute): void 80 | { 81 | if ($attribute->methods !== null) { 82 | $this->checkHttpMethod($attribute); 83 | } 84 | 85 | if ($attribute->actions !== null) { 86 | $this->checkActions($attribute); 87 | } 88 | 89 | if ($attribute->forward && !$this->presenter->isForwarded()) { 90 | $this->presenter->error('Forwarded request is required by ' . Reflection::toString($this->element)); 91 | } 92 | 93 | if ($attribute->sameOrigin && !$this->presenter->getHttpRequest()->isSameSite()) { 94 | $this->presenter->detectedCsrf(); 95 | } 96 | 97 | if ($attribute->ajax && !$this->presenter->getHttpRequest()->isAjax()) { 98 | $this->presenter->error('AJAX request is required by ' . Reflection::toString($this->element), Nette\Http\IResponse::S403_Forbidden); 99 | } 100 | } 101 | 102 | 103 | private function checkActions(Attributes\Requires $attribute): void 104 | { 105 | if ( 106 | $this->element instanceof \ReflectionMethod 107 | && !$this->element->getDeclaringClass()->isSubclassOf(Presenter::class) 108 | ) { 109 | throw new \LogicException('Requires(actions) used by ' . Reflection::toString($this->element) . ' is allowed only in presenter.'); 110 | } 111 | 112 | if (!in_array($this->presenter->getAction(), $attribute->actions, strict: true)) { 113 | $this->presenter->error("Action '{$this->presenter->getAction()}' is not allowed by " . Reflection::toString($this->element)); 114 | } 115 | } 116 | 117 | 118 | private function checkHttpMethod(Attributes\Requires $attribute): void 119 | { 120 | if ($this->element instanceof \ReflectionClass) { 121 | $this->presenter->allowedMethods = []; // bypass Presenter::checkHttpMethod() 122 | } 123 | 124 | $allowed = array_map(strtoupper(...), $attribute->methods); 125 | $method = $this->presenter->getHttpRequest()->getMethod(); 126 | 127 | if ($allowed !== ['*'] && !in_array($method, $allowed, strict: true)) { 128 | $this->presenter->getHttpResponse()->setHeader('Allow', implode(',', $allowed)); 129 | $this->presenter->error( 130 | "Method $method is not allowed by " . Reflection::toString($this->element), 131 | Nette\Http\IResponse::S405_MethodNotAllowed, 132 | ); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Application/UI/Form.php: -------------------------------------------------------------------------------- 1 | Occurs when form is attached to presenter */ 22 | public array $onAnchor = []; 23 | 24 | 25 | /** 26 | * Application form constructor. 27 | */ 28 | public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) 29 | { 30 | parent::__construct(); 31 | $parent?->addComponent($this, $name); 32 | } 33 | 34 | 35 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void 36 | { 37 | parent::validateParent($parent); 38 | 39 | $this->monitor(Presenter::class, function (Presenter $presenter): void { 40 | if (!isset($this->getElementPrototype()->id)) { 41 | $this->getElementPrototype()->id = 'frm-' . $this->lookupPath(Presenter::class); 42 | } 43 | 44 | if (!$this->getAction()) { 45 | $this->setAction(new Link($presenter, 'this')); 46 | } 47 | 48 | $controls = $this->getControls(); 49 | if (iterator_count($controls) && $this->isSubmitted()) { 50 | foreach ($controls as $control) { 51 | if (!$control->isDisabled()) { 52 | $control->loadHttpData(); 53 | } 54 | } 55 | } 56 | 57 | Nette\Utils\Arrays::invoke($this->onAnchor, $this); 58 | }); 59 | } 60 | 61 | 62 | /** 63 | * Returns the presenter where this component belongs to. 64 | */ 65 | final public function getPresenter(): ?Presenter 66 | { 67 | if (func_num_args()) { 68 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); 69 | $throw = func_get_arg(0); 70 | } 71 | 72 | return $this->lookup(Presenter::class, throw: $throw ?? true); 73 | } 74 | 75 | 76 | /** 77 | * Returns the presenter where this component belongs to. 78 | */ 79 | final public function getPresenterIfExists(): ?Presenter 80 | { 81 | return $this->lookup(Presenter::class, throw: false); 82 | } 83 | 84 | 85 | /** @deprecated */ 86 | public function hasPresenter(): bool 87 | { 88 | return (bool) $this->lookup(Presenter::class, throw: false); 89 | } 90 | 91 | 92 | /** 93 | * Tells if the form is anchored. 94 | */ 95 | public function isAnchored(): bool 96 | { 97 | return $this->hasPresenter(); 98 | } 99 | 100 | 101 | #[\Deprecated('use allowCrossOrigin()')] 102 | public function disableSameSiteProtection(): void 103 | { 104 | $this->allowCrossOrigin(); 105 | } 106 | 107 | 108 | /** 109 | * Internal: returns submitted HTTP data or null when form was not submitted. 110 | */ 111 | protected function receiveHttpData(): ?array 112 | { 113 | $presenter = $this->getPresenter(); 114 | if (!$presenter->isSignalReceiver($this, 'submit')) { 115 | return null; 116 | } 117 | 118 | $request = $presenter->getRequest(); 119 | if ($request->isMethod('forward') || $request->isMethod('post') !== $this->isMethod('post')) { 120 | return null; 121 | } 122 | 123 | return $this->isMethod('post') 124 | ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) 125 | : $request->getParameters(); 126 | } 127 | 128 | 129 | protected function beforeRender(): void 130 | { 131 | parent::beforeRender(); 132 | $key = ($this->isMethod('post') ? '_' : '') . Presenter::SignalKey; 133 | if (!isset($this[$key]) && $this->getAction() !== '') { 134 | $do = $this->lookupPath(Presenter::class) . self::NameSeparator . 'submit'; 135 | $this[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); 136 | } 137 | } 138 | 139 | 140 | /********************* interface SignalReceiver ****************d*g**/ 141 | 142 | 143 | /** 144 | * This method is called by presenter. 145 | */ 146 | public function signalReceived(string $signal): void 147 | { 148 | if ($signal !== 'submit') { 149 | $class = static::class; 150 | throw new BadSignalException("Missing handler for signal '$signal' in $class."); 151 | 152 | } elseif (!$this->crossOrigin && !$this->getPresenter()->getHttpRequest()->isSameSite()) { 153 | $this->getPresenter()->detectedCsrf(); 154 | 155 | } elseif (!$this->getPresenter()->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) { 156 | $this->fireEvents(); 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Application/UI/Control.php: -------------------------------------------------------------------------------- 1 | templateFactory = $templateFactory; 35 | return $this; 36 | } 37 | 38 | 39 | final public function getTemplate(): Template 40 | { 41 | if (!isset($this->template)) { 42 | $this->template = $this->createTemplate(); 43 | } 44 | 45 | return $this->template; 46 | } 47 | 48 | 49 | protected function createTemplate(?string $class = null): Template 50 | { 51 | $class ??= $this->formatTemplateClass(); 52 | $templateFactory = $this->templateFactory ?? $this->getPresenter()->getTemplateFactory(); 53 | return $templateFactory->createTemplate($this, $class); 54 | } 55 | 56 | 57 | public function formatTemplateClass(): ?string 58 | { 59 | return $this->checkTemplateClass(preg_replace('#Control$#', '', static::class) . 'Template'); 60 | } 61 | 62 | 63 | /** @internal */ 64 | protected function checkTemplateClass(string $class): ?string 65 | { 66 | if (!class_exists($class)) { 67 | return null; 68 | } elseif (!is_a($class, Template::class, allow_string: true)) { 69 | trigger_error(sprintf( 70 | '%s: class %s was found but does not implement the %s, so it will not be used for the template.', 71 | static::class, 72 | $class, 73 | Template::class, 74 | )); 75 | return null; 76 | } else { 77 | return $class; 78 | } 79 | } 80 | 81 | 82 | #[\Deprecated] 83 | public function templatePrepareFilters(Template $template): void 84 | { 85 | } 86 | 87 | 88 | /** 89 | * Saves the message to template, that can be displayed after redirect. 90 | */ 91 | public function flashMessage(string|\stdClass|\Stringable $message, string $type = 'info'): \stdClass 92 | { 93 | $id = $this->getParameterId('flash'); 94 | $flash = $message instanceof \stdClass ? $message : (object) [ 95 | 'message' => $message, 96 | 'type' => $type, 97 | ]; 98 | $messages = $this->getPresenter()->getFlashSession()->get($id); 99 | $messages[] = $flash; 100 | $this->getTemplate()->flashes = $messages; 101 | $this->getPresenter()->getFlashSession()->set($id, $messages); 102 | return $flash; 103 | } 104 | 105 | 106 | /********************* rendering ****************d*g**/ 107 | 108 | 109 | /** 110 | * Forces control or its snippet to repaint. 111 | */ 112 | public function redrawControl(?string $snippet = null, bool $redraw = true): void 113 | { 114 | if ($redraw) { 115 | $this->invalidSnippets[$snippet ?? "\0"] = true; 116 | 117 | } elseif ($snippet === null) { 118 | $this->invalidSnippets = []; 119 | 120 | } else { 121 | $this->invalidSnippets[$snippet] = false; 122 | } 123 | } 124 | 125 | 126 | /** 127 | * Is required to repaint the control or its snippet? 128 | */ 129 | public function isControlInvalid(?string $snippet = null): bool 130 | { 131 | if ($snippet !== null) { 132 | return $this->invalidSnippets[$snippet] ?? isset($this->invalidSnippets["\0"]); 133 | 134 | } elseif (count($this->invalidSnippets) > 0) { 135 | return true; 136 | } 137 | 138 | $queue = [$this]; 139 | do { 140 | foreach (array_shift($queue)->getComponents() as $component) { 141 | if ($component instanceof Renderable) { 142 | if ($component->isControlInvalid()) { 143 | // $this->invalidSnippets['__child'] = true; // as cache 144 | return true; 145 | } 146 | } elseif ($component instanceof Nette\ComponentModel\IContainer) { 147 | $queue[] = $component; 148 | } 149 | } 150 | } while ($queue); 151 | 152 | return false; 153 | } 154 | 155 | 156 | /** 157 | * Returns snippet HTML ID. 158 | */ 159 | public function getSnippetId(string $name): string 160 | { 161 | // HTML 4 ID & NAME: [A-Za-z][A-Za-z0-9:_.-]* 162 | return 'snippet-' . $this->getUniqueId() . '-' . $name; 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/Application/MicroPresenter.php: -------------------------------------------------------------------------------- 1 | context; 43 | } 44 | 45 | 46 | public function run(Application\Request $request): Application\Response 47 | { 48 | $this->request = $request; 49 | 50 | if ( 51 | $this->httpRequest 52 | && $this->router 53 | && !$this->httpRequest->isAjax() 54 | && ($request->isMethod('get') || $request->isMethod('head')) 55 | ) { 56 | $refUrl = $this->httpRequest->getUrl(); 57 | $url = $this->router->constructUrl($request->toArray(), $refUrl); 58 | if ($url !== null && !$refUrl->isEqual($url)) { 59 | return new Responses\RedirectResponse($url, Http\IResponse::S301_MovedPermanently); 60 | } 61 | } 62 | 63 | $params = $request->getParameters(); 64 | $callback = $params['callback'] ?? null; 65 | if (!is_object($callback) || !is_callable($callback)) { 66 | throw new Application\BadRequestException('Parameter callback is not a valid closure.'); 67 | } 68 | 69 | $reflection = Nette\Utils\Callback::toReflection($callback); 70 | 71 | if ($this->context) { 72 | foreach ($reflection->getParameters() as $param) { 73 | $type = $param->getType(); 74 | if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { 75 | $params[$param->getName()] = $this->context->getByType($type->getName(), throw: false); 76 | } 77 | } 78 | } 79 | 80 | $params['presenter'] = $this; 81 | try { 82 | $params = Application\UI\ParameterConverter::toArguments($reflection, $params); 83 | } catch (Nette\InvalidArgumentException $e) { 84 | $this->error($e->getMessage()); 85 | } 86 | 87 | $response = $callback(...array_values($params)); 88 | 89 | if (is_string($response)) { 90 | $response = [$response, []]; 91 | } 92 | 93 | if (is_array($response)) { 94 | [$templateSource, $templateParams] = $response; 95 | $response = $this->createTemplate()->setParameters($templateParams); 96 | if (!$templateSource instanceof \SplFileInfo) { 97 | $response->getLatte()->setLoader(new Latte\Loaders\StringLoader); 98 | } 99 | 100 | $response->setFile((string) $templateSource); 101 | } 102 | 103 | if ($response instanceof Application\UI\Template) { 104 | return new Responses\TextResponse($response); 105 | } else { 106 | return $response ?: new Responses\VoidResponse; 107 | } 108 | } 109 | 110 | 111 | /** 112 | * Template factory. 113 | */ 114 | public function createTemplate(?string $class = null, ?callable $latteFactory = null): Application\UI\Template 115 | { 116 | $latte = $latteFactory 117 | ? $latteFactory() 118 | : $this->getContext()->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); 119 | $template = $class 120 | ? new $class 121 | : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte); 122 | 123 | $template->setParameters($this->request->getParameters()); 124 | $template->presenter = $this; 125 | $template->context = $this->context; 126 | if ($this->httpRequest) { 127 | $url = $this->httpRequest->getUrl(); 128 | $template->baseUrl = rtrim($url->getBaseUrl(), '/'); 129 | $template->basePath = rtrim($url->getBasePath(), '/'); 130 | } 131 | 132 | return $template; 133 | } 134 | 135 | 136 | /** 137 | * Redirects to another URL. 138 | */ 139 | public function redirectUrl(string $url, int $httpCode = Http\IResponse::S302_Found): Responses\RedirectResponse 140 | { 141 | return new Responses\RedirectResponse($url, $httpCode); 142 | } 143 | 144 | 145 | /** 146 | * Throws HTTP error. 147 | * @throws Nette\Application\BadRequestException 148 | */ 149 | public function error(string $message = '', int $httpCode = Http\IResponse::S404_NotFound): void 150 | { 151 | throw new Application\BadRequestException($message, $httpCode); 152 | } 153 | 154 | 155 | public function getRequest(): ?Nette\Application\Request 156 | { 157 | return $this->request; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationDI/LatteExtension.php: -------------------------------------------------------------------------------- 1 | Expect::anyOf(true, false, 'all'), 37 | 'extensions' => Expect::arrayOf('string|Nette\DI\Definitions\Statement'), 38 | 'templateClass' => Expect::string(), 39 | 'strictTypes' => Expect::bool(false), 40 | 'strictParsing' => Expect::bool(false), 41 | 'phpLinter' => Expect::string(), 42 | 'locale' => Expect::string(), 43 | ]); 44 | } 45 | 46 | 47 | public function loadConfiguration(): void 48 | { 49 | if (!class_exists(Latte\Engine::class)) { 50 | return; 51 | } 52 | 53 | $config = $this->config; 54 | $builder = $this->getContainerBuilder(); 55 | 56 | $builder->addFactoryDefinition($this->prefix('latteFactory')) 57 | ->setImplement(ApplicationLatte\LatteFactory::class) 58 | ->getResultDefinition() 59 | ->setFactory(Latte\Engine::class) 60 | ->addSetup('setTempDirectory', [$this->tempDir]) 61 | ->addSetup('setAutoRefresh', [$this->debugMode]) 62 | ->addSetup('setStrictTypes', [$config->strictTypes]) 63 | ->addSetup('setStrictParsing', [$config->strictParsing]) 64 | ->addSetup('enablePhpLinter', [$config->phpLinter]) 65 | ->addSetup('setLocale', [$config->locale]) 66 | ->addSetup('addExtension', [new Statement(ApplicationLatte\UIExtension::class, [$builder::literal('$control')])]); 67 | 68 | if ($builder->getByType(Nette\Caching\Storage::class)) { 69 | $this->addExtension(new Statement(Nette\Bridges\CacheLatte\CacheExtension::class)); 70 | } 71 | if (class_exists(Nette\Bridges\FormsLatte\FormsExtension::class)) { 72 | $this->addExtension(new Statement(Nette\Bridges\FormsLatte\FormsExtension::class)); 73 | } 74 | 75 | foreach ($config->extensions as $extension) { 76 | if ($extension === Latte\Essential\TranslatorExtension::class) { 77 | $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]); 78 | } 79 | $this->addExtension($extension); 80 | } 81 | 82 | $builder->addDefinition($this->prefix('templateFactory')) 83 | ->setFactory(ApplicationLatte\TemplateFactory::class) 84 | ->setArguments(['templateClass' => $config->templateClass]); 85 | 86 | if ($this->name === 'latte') { 87 | $builder->addAlias('nette.latteFactory', $this->prefix('latteFactory')); 88 | $builder->addAlias('nette.templateFactory', $this->prefix('templateFactory')); 89 | } 90 | } 91 | 92 | 93 | public function beforeCompile(): void 94 | { 95 | $builder = $this->getContainerBuilder(); 96 | 97 | if ( 98 | $this->debugMode 99 | && ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) 100 | && class_exists(Latte\Bridges\Tracy\LattePanel::class) 101 | ) { 102 | $factory = $builder->getDefinition($this->prefix('templateFactory')); 103 | $factory->addSetup([self::class, 'initLattePanel'], [$factory, 'all' => $this->config->debugger === 'all']); 104 | } 105 | } 106 | 107 | 108 | public static function initLattePanel( 109 | Nette\Application\UI\TemplateFactory $factory, 110 | Tracy\Bar $bar, 111 | bool $all = false, 112 | ): void 113 | { 114 | if (!$factory instanceof ApplicationLatte\TemplateFactory) { 115 | return; 116 | } 117 | 118 | $factory->onCreate[] = function (ApplicationLatte\Template $template) use ($bar, $all) { 119 | $control = $template->getLatte()->getProviders()['uiControl'] ?? null; 120 | if ($all || $control instanceof Nette\Application\UI\Presenter) { 121 | $name = $all && $control ? (new \ReflectionObject($control))->getShortName() : ''; 122 | $template->getLatte()->addExtension(new Latte\Bridges\Tracy\TracyExtension($name)); 123 | } 124 | }; 125 | } 126 | 127 | 128 | public function addExtension(Statement|string $extension): void 129 | { 130 | $extension = is_string($extension) 131 | ? new Statement($extension) 132 | : $extension; 133 | 134 | $builder = $this->getContainerBuilder(); 135 | $builder->getDefinition($this->prefix('latteFactory')) 136 | ->getResultDefinition() 137 | ->addSetup('addExtension', [$extension]); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Nodes/SnippetNode.php: -------------------------------------------------------------------------------- 1 | */ 40 | public static function create(Tag $tag, TemplateParser $parser): \Generator 41 | { 42 | $tag->outputMode = $tag::OutputKeepIndentation; 43 | 44 | $node = $tag->node = new static; 45 | $node->htmlElement = $tag->isNAttribute() ? $tag->htmlElement : null; 46 | 47 | if ($tag->parser->isEnd()) { 48 | $name = null; 49 | $node->block = new Block(new Scalar\StringNode(''), Template::LayerSnippet, $tag); 50 | } else { 51 | $name = $tag->parser->parseUnquotedStringOrExpression(); 52 | if ( 53 | $name instanceof Expression\ClassConstantFetchNode 54 | && $name->class instanceof Php\NameNode 55 | && $name->name instanceof Php\IdentifierNode 56 | ) { 57 | $name = new Scalar\StringNode(constant($name->class . '::' . $name->name), $name->position); 58 | } 59 | $node->block = new Block($name, Template::LayerSnippet, $tag); 60 | if (!$node->block->isDynamic()) { 61 | $parser->checkBlockIsUnique($node->block); 62 | } 63 | } 64 | 65 | if ($tag->isNAttribute()) { 66 | if ($tag->prefix !== $tag::PrefixNone) { 67 | throw new CompileException("Use n:snippet instead of {$tag->getNotation()}", $tag->position); 68 | 69 | } elseif ($tag->htmlElement->getAttribute(self::$snippetAttribute)) { 70 | throw new CompileException('Cannot combine HTML attribute ' . self::$snippetAttribute . ' with n:snippet.', $tag->position); 71 | 72 | } elseif (isset($tag->htmlElement->nAttributes['ifcontent'])) { 73 | throw new CompileException('Cannot combine n:ifcontent with n:snippet.', $tag->position); 74 | 75 | } elseif (isset($tag->htmlElement->nAttributes['foreach'])) { 76 | throw new CompileException('Combination of n:snippet with n:foreach is invalid, use n:inner-foreach.', $tag->position); 77 | } 78 | 79 | $tag->replaceNAttribute(new AuxiliaryNode( 80 | fn(PrintContext $context) => "echo ' " . $node->printAttribute($context) . "';", 81 | )); 82 | } 83 | 84 | [$node->content, $endTag] = yield; 85 | if ($endTag && $name instanceof Scalar\StringNode) { 86 | $endTag->parser->stream->tryConsume($name->value); 87 | } 88 | 89 | return $node; 90 | } 91 | 92 | 93 | public function print(PrintContext $context): string 94 | { 95 | if (!$this->block->isDynamic()) { 96 | $context->addBlock($this->block); 97 | } 98 | 99 | if ($this->htmlElement) { 100 | try { 101 | $inner = $this->htmlElement->content; 102 | $this->htmlElement->content = new AuxiliaryNode(fn() => $this->printContent($context, $inner)); 103 | return $this->content->print($context); 104 | } finally { 105 | $this->htmlElement->content = $inner; 106 | } 107 | } else { 108 | return <<printAttribute($context)}>'; 110 | {$this->printContent($context, $this->content)} 111 | echo ''; 112 | XX; 113 | } 114 | } 115 | 116 | 117 | private function printContent(PrintContext $context, AreaNode $inner): string 118 | { 119 | $dynamic = $this->block->isDynamic(); 120 | $res = $context->format( 121 | <<<'XX' 122 | $this->global->snippetDriver->enter(%node, %dump) %line; 123 | try { 124 | %node 125 | } finally { 126 | $this->global->snippetDriver->leave(); 127 | } 128 | 129 | XX, 130 | $dynamic ? new AuxiliaryNode(fn() => '$ʟ_nm') : $this->block->name, 131 | $dynamic ? SnippetRuntime::TypeDynamic : SnippetRuntime::TypeStatic, 132 | $this->position, 133 | $inner, 134 | ); 135 | 136 | if ($dynamic) { 137 | return $res; 138 | } 139 | 140 | $this->block->content = $res; 141 | return $context->format( 142 | '$this->renderBlock(%node, [], null, %dump) %line;', 143 | $this->block->name, 144 | Template::LayerSnippet, 145 | $this->position, 146 | ); 147 | } 148 | 149 | 150 | private function printAttribute(PrintContext $context): string 151 | { 152 | return $context->format( 153 | <<<'XX' 154 | %raw="', htmlspecialchars($this->global->snippetDriver->getHtmlId(%node)), '" 155 | XX, 156 | self::$snippetAttribute, 157 | $this->block->isDynamic() 158 | ? new Expression\AssignNode(new Expression\VariableNode('ʟ_nm'), $this->block->name) 159 | : $this->block->name, 160 | ); 161 | } 162 | 163 | 164 | public function &getIterator(): \Generator 165 | { 166 | yield $this->block->name; 167 | yield $this->content; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/Application/UI/ParameterConverter.php: -------------------------------------------------------------------------------- 1 | getParameters() as $i => $param) { 29 | $name = $param->getName(); 30 | $type = self::getType($param); 31 | if (isset($args[$name])) { 32 | $res[$i] = $args[$name]; 33 | if (!self::convertType($res[$i], $type)) { 34 | throw new Nette\InvalidArgumentException(sprintf( 35 | 'Argument $%s passed to %s must be %s, %s given.', 36 | $name, 37 | Reflection::toString($method), 38 | $type, 39 | get_debug_type($args[$name]), 40 | )); 41 | } 42 | } elseif ($param->isDefaultValueAvailable()) { 43 | $res[$i] = $param->getDefaultValue(); 44 | } elseif ($type === 'scalar' || $param->allowsNull()) { 45 | $res[$i] = null; 46 | } elseif ($type === 'array' || $type === 'iterable') { 47 | $res[$i] = []; 48 | } else { 49 | throw new Nette\InvalidArgumentException(sprintf( 50 | 'Missing parameter $%s required by %s', 51 | $name, 52 | Reflection::toString($method), 53 | )); 54 | } 55 | } 56 | 57 | return $res; 58 | } 59 | 60 | 61 | /** 62 | * Converts list of arguments to named parameters & check types. 63 | * @param \ReflectionParameter[] $missing arguments 64 | * @throws InvalidLinkException 65 | * @internal 66 | */ 67 | public static function toParameters( 68 | \ReflectionMethod $method, 69 | array &$args, 70 | array $supplemental = [], 71 | ?array &$missing = null, 72 | ): void 73 | { 74 | $i = 0; 75 | foreach ($method->getParameters() as $param) { 76 | $type = self::getType($param); 77 | $name = $param->getName(); 78 | 79 | if (array_key_exists($i, $args)) { 80 | $args[$name] = $args[$i]; 81 | unset($args[$i]); 82 | $i++; 83 | 84 | } elseif (array_key_exists($name, $args)) { 85 | // continue with process 86 | 87 | } elseif (array_key_exists($name, $supplemental)) { 88 | $args[$name] = $supplemental[$name]; 89 | } 90 | 91 | if (!isset($args[$name])) { 92 | if ( 93 | !$param->isDefaultValueAvailable() 94 | && !$param->allowsNull() 95 | && $type !== 'scalar' 96 | && $type !== 'array' 97 | && $type !== 'iterable' 98 | ) { 99 | $missing[] = $param; 100 | unset($args[$name]); 101 | } 102 | 103 | continue; 104 | } 105 | 106 | if (!self::convertType($args[$name], $type)) { 107 | throw new InvalidLinkException(sprintf( 108 | 'Argument $%s passed to %s must be %s, %s given.', 109 | $name, 110 | Reflection::toString($method), 111 | $type, 112 | get_debug_type($args[$name]), 113 | )); 114 | } 115 | 116 | $def = $param->isDefaultValueAvailable() 117 | ? $param->getDefaultValue() 118 | : null; 119 | if ($args[$name] === $def || ($def === null && $args[$name] === '')) { 120 | $args[$name] = null; // value transmit is unnecessary 121 | } 122 | } 123 | 124 | if (array_key_exists($i, $args)) { 125 | throw new InvalidLinkException('Passed more parameters than method ' . Reflection::toString($method) . ' expects.'); 126 | } 127 | } 128 | 129 | 130 | /** 131 | * Lossless type conversion. 132 | */ 133 | public static function convertType(mixed &$val, string $types): bool 134 | { 135 | $scalars = ['string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'true' => 1, 'false' => 1]; 136 | $testable = ['iterable' => 1, 'object' => 1, 'array' => 1, 'null' => 1]; 137 | 138 | foreach (explode('|', ltrim($types, '?')) as $type) { 139 | if (match (true) { 140 | isset($scalars[$type]) => self::castScalar($val, $type), 141 | isset($testable[$type]) => "is_$type"($val), 142 | $type === 'scalar' => !is_array($val), // special type due to historical reasons 143 | $type === 'mixed' => true, 144 | $type === 'callable' => false, // intentionally disabled for security reasons 145 | default => $val instanceof $type, 146 | }) { 147 | return true; 148 | } 149 | } 150 | 151 | return false; 152 | } 153 | 154 | 155 | /** 156 | * Lossless type casting. 157 | */ 158 | private static function castScalar(mixed &$val, string $type): bool 159 | { 160 | if (!is_scalar($val)) { 161 | return false; 162 | } 163 | 164 | $tmp = ($val === false ? '0' : (string) $val); 165 | if ($type === 'float') { 166 | $tmp = preg_replace('#\.0*$#D', '', $tmp); 167 | } 168 | 169 | $orig = $tmp; 170 | $spec = ['true' => true, 'false' => false]; 171 | isset($spec[$type]) ? $tmp = $spec[$type] : settype($tmp, $type); 172 | if ($orig !== ($tmp === false ? '0' : (string) $tmp)) { 173 | return false; // data-loss occurs 174 | } 175 | 176 | $val = $tmp; 177 | return true; 178 | } 179 | 180 | 181 | public static function getType(\ReflectionParameter|\ReflectionProperty $item): string 182 | { 183 | if ($type = $item->getType()) { 184 | return (string) $type; 185 | } 186 | $default = $item instanceof \ReflectionProperty || $item->isDefaultValueAvailable() 187 | ? $item->getDefaultValue() 188 | : null; 189 | return $default === null ? 'scalar' : get_debug_type($default); 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /src/Application/UI/ComponentReflection.php: -------------------------------------------------------------------------------- 1 | $name 21 | * @property-read string $fileName 22 | * @internal 23 | */ 24 | final class ComponentReflection extends \ReflectionClass 25 | { 26 | private static array $ppCache = []; 27 | private static array $pcCache = []; 28 | private static array $armCache = []; 29 | 30 | 31 | /** 32 | * Returns array of class properties that are public and have attribute #[Persistent] or #[Parameter]. 33 | * @return array 34 | */ 35 | public function getParameters(): array 36 | { 37 | $params = &self::$ppCache[$this->getName()]; 38 | if ($params !== null) { 39 | return $params; 40 | } 41 | 42 | $params = []; 43 | $isPresenter = $this->isSubclassOf(Presenter::class); 44 | foreach ($this->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { 45 | if ($prop->isStatic()) { 46 | continue; 47 | } elseif ($prop->getAttributes(Attributes\Persistent::class)) { 48 | $params[$prop->getName()] = [ 49 | 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null, 50 | 'type' => ParameterConverter::getType($prop), 51 | 'since' => $isPresenter ? Reflection::getPropertyDeclaringClass($prop)->getName() : null, 52 | ]; 53 | } elseif ($prop->getAttributes(Attributes\Parameter::class)) { 54 | $params[$prop->getName()] = [ 55 | 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null, 56 | 'type' => (string) ($prop->getType() ?? 'mixed'), 57 | ]; 58 | } 59 | } 60 | 61 | if ($this->getParentClass()->isSubclassOf(Component::class)) { 62 | $parent = new self($this->getParentClass()->getName()); 63 | foreach ($parent->getParameters() as $name => $meta) { 64 | if (!isset($params[$name])) { 65 | $params[$name] = $meta; 66 | } elseif (array_key_exists('since', $params[$name])) { 67 | $params[$name]['since'] = $meta['since']; 68 | } 69 | } 70 | } 71 | 72 | return $params; 73 | } 74 | 75 | 76 | /** 77 | * Returns array of persistent properties. They are public and have attribute #[Persistent]. 78 | * @return array 79 | */ 80 | public function getPersistentParams(): array 81 | { 82 | return array_filter($this->getParameters(), fn($param) => array_key_exists('since', $param)); 83 | } 84 | 85 | 86 | /** 87 | * Returns array of persistent components. They are tagged with class-level attribute 88 | * #[Persistent] or returned by Presenter::getPersistentComponents(). 89 | * @return array 90 | */ 91 | public function getPersistentComponents(): array 92 | { 93 | $class = $this->name; 94 | $components = &self::$pcCache[$class]; 95 | if ($components !== null) { 96 | return $components; 97 | } 98 | 99 | $attrs = $this->getAttributes(Attributes\Persistent::class); 100 | $names = $attrs ? $attrs[0]->getArguments() : []; 101 | $names = array_merge($names, $class::getPersistentComponents()); 102 | $components = array_fill_keys($names, ['since' => $class]); 103 | 104 | if ($this->isSubclassOf(Presenter::class)) { 105 | $parent = new self($this->getParentClass()->getName()); 106 | $components = $parent->getPersistentComponents() + $components; 107 | } 108 | 109 | return $components; 110 | } 111 | 112 | 113 | public function getTemplateVariables(Control $control): array 114 | { 115 | $res = []; 116 | foreach ($this->getProperties() as $prop) { 117 | if ($prop->getAttributes(Attributes\TemplateVariable::class)) { 118 | $name = $prop->getName(); 119 | if (!$prop->isPublic()) { 120 | throw new \LogicException("Property {$this->getName()}::\$$name must be public to be used as TemplateVariable."); 121 | } elseif ($prop->isInitialized($control) || (PHP_VERSION_ID >= 80400 && $prop->hasHook(\PropertyHookType::Get))) { 122 | $res[] = $name; 123 | } 124 | } 125 | } 126 | return $res; 127 | } 128 | 129 | 130 | /** 131 | * Is a method callable? It means class is instantiable and method has 132 | * public visibility, is non-static and non-abstract. 133 | */ 134 | public function hasCallableMethod(string $method): bool 135 | { 136 | return $this->isInstantiable() 137 | && $this->hasMethod($method) 138 | && ($rm = $this->getMethod($method)) 139 | && $rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic(); 140 | } 141 | 142 | 143 | /** Returns action*() or render*() method if available */ 144 | public function getActionRenderMethod(string $action): ?\ReflectionMethod 145 | { 146 | $class = $this->name; 147 | return self::$armCache[$class][$action] ??= 148 | $this->hasCallableMethod($name = $class::formatActionMethod($action)) 149 | || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) 150 | ? parent::getMethod($name) 151 | : null; 152 | } 153 | 154 | 155 | /** Returns handle*() method if available */ 156 | public function getSignalMethod(string $signal): ?\ReflectionMethod 157 | { 158 | $class = $this->name; 159 | return $this->hasCallableMethod($name = $class::formatSignalMethod($signal)) 160 | ? parent::getMethod($name) 161 | : null; 162 | } 163 | 164 | 165 | #[\Deprecated] 166 | public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array 167 | { 168 | return ParameterConverter::toArguments($method, $args); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/Application/Routers/Route.php: -------------------------------------------------------------------------------- 1 | [ 28 | self::Pattern => '[a-z][a-z0-9.-]*', 29 | self::FilterIn => [self::class, 'path2presenter'], 30 | self::FilterOut => [self::class, 'presenter2path'], 31 | ], 32 | 'presenter' => [ 33 | self::Pattern => '[a-z][a-z0-9.-]*', 34 | self::FilterIn => [self::class, 'path2presenter'], 35 | self::FilterOut => [self::class, 'presenter2path'], 36 | ], 37 | 'action' => [ 38 | self::Pattern => '[a-z][a-z0-9-]*', 39 | self::FilterIn => [self::class, 'path2action'], 40 | self::FilterOut => [self::class, 'action2path'], 41 | ], 42 | ]; 43 | 44 | 45 | /** 46 | * @param string $mask e.g. '//' 47 | * @param array|string|\Closure $metadata default values or metadata or callback for NetteModule\MicroPresenter 48 | */ 49 | public function __construct(string $mask, array|string|\Closure $metadata = []) 50 | { 51 | if (is_string($metadata)) { 52 | [$presenter, $action] = Nette\Application\Helpers::splitName($metadata); 53 | if (!$presenter) { 54 | throw new Nette\InvalidArgumentException("Second argument must be array or string in format Presenter:action, '$metadata' given."); 55 | } 56 | 57 | $metadata = [self::PresenterKey => $presenter]; 58 | if ($action !== '') { 59 | $metadata['action'] = $action; 60 | } 61 | } elseif ($metadata instanceof \Closure) { 62 | $metadata = [ 63 | self::PresenterKey => 'Nette:Micro', 64 | 'callback' => $metadata, 65 | ]; 66 | } 67 | 68 | $this->defaultMeta += self::UIMeta; 69 | parent::__construct($mask, $metadata); 70 | } 71 | 72 | 73 | /** 74 | * Maps HTTP request to an array. 75 | */ 76 | public function match(Nette\Http\IRequest $httpRequest): ?array 77 | { 78 | $params = parent::match($httpRequest); 79 | 80 | if ($params === null) { 81 | return null; 82 | } elseif (!isset($params[self::PresenterKey])) { 83 | throw new Nette\InvalidStateException('Missing presenter in route definition.'); 84 | } elseif (!is_string($params[self::PresenterKey])) { 85 | return null; 86 | } 87 | 88 | $presenter = $params[self::PresenterKey] ?? null; 89 | if (isset($this->getMetadata()[self::ModuleKey], $params[self::ModuleKey]) && is_string($presenter)) { 90 | $params[self::PresenterKey] = $params[self::ModuleKey] . ':' . $params[self::PresenterKey]; 91 | } 92 | 93 | unset($params[self::ModuleKey]); 94 | 95 | return $params; 96 | } 97 | 98 | 99 | /** 100 | * Constructs absolute URL from array. 101 | */ 102 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 103 | { 104 | $metadata = $this->getMetadata(); 105 | if (isset($metadata[self::ModuleKey])) { // try split into module and [submodule:]presenter parts 106 | $presenter = $params[self::PresenterKey]; 107 | $module = $metadata[self::ModuleKey]; 108 | $a = isset($module['fixity'], $module[self::Value]) 109 | && strncmp($presenter, $module[self::Value] . ':', strlen($module[self::Value]) + 1) === 0 110 | ? strlen($module[self::Value]) 111 | : strrpos($presenter, ':'); 112 | if ($a === false) { 113 | $params[self::ModuleKey] = isset($module[self::Value]) ? '' : null; 114 | } else { 115 | $params[self::ModuleKey] = substr($presenter, 0, $a); 116 | $params[self::PresenterKey] = substr($presenter, $a + 1); 117 | } 118 | } 119 | 120 | return parent::constructUrl($params, $refUrl); 121 | } 122 | 123 | 124 | /** @internal */ 125 | public function getConstantParameters(): array 126 | { 127 | $res = parent::getConstantParameters(); 128 | if (isset($res[self::ModuleKey], $res[self::PresenterKey])) { 129 | $res[self::PresenterKey] = $res[self::ModuleKey] . ':' . $res[self::PresenterKey]; 130 | } elseif (isset($this->getMetadata()[self::ModuleKey])) { 131 | unset($res[self::PresenterKey]); 132 | } 133 | 134 | unset($res[self::ModuleKey]); 135 | return $res; 136 | } 137 | 138 | 139 | /********************* Inflectors ****************d*g**/ 140 | 141 | 142 | /** 143 | * camelCaseAction name -> dash-separated. 144 | */ 145 | public static function action2path(string $s): string 146 | { 147 | $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s); 148 | $s = strtolower($s); 149 | $s = rawurlencode($s); 150 | return $s; 151 | } 152 | 153 | 154 | /** 155 | * dash-separated -> camelCaseAction name. 156 | */ 157 | public static function path2action(string $s): string 158 | { 159 | $s = preg_replace('#-(?=[a-z])#', ' ', $s); 160 | $s = lcfirst(ucwords($s)); 161 | $s = str_replace(' ', '', $s); 162 | return $s; 163 | } 164 | 165 | 166 | /** 167 | * PascalCase:Presenter name -> dash-and-dot-separated. 168 | */ 169 | public static function presenter2path(string $s): string 170 | { 171 | $s = strtr($s, ':', '.'); 172 | $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s); 173 | $s = strtolower($s); 174 | $s = rawurlencode($s); 175 | return $s; 176 | } 177 | 178 | 179 | /** 180 | * dash-and-dot-separated -> PascalCase:Presenter name. 181 | */ 182 | public static function path2presenter(string $s): string 183 | { 184 | $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s); 185 | $s = ucwords($s); 186 | $s = str_replace('. ', ':', $s); 187 | $s = str_replace('- ', '', $s); 188 | return $s; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationTracy/panel.latte: -------------------------------------------------------------------------------- 1 | {use Nette\Application\UI\Presenter} 2 | {use Tracy\Dumper} 3 | 4 | 81 | 82 |

83 | {if $matched === null} 84 | no route 85 | {elseif isset($matched[Presenter::PresenterKey])} 86 | {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction} 87 | {if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if} 88 | {/if} 89 |

90 | 91 |
92 |
93 |

94 | {$method} 95 | {$url->getBaseUrl()}{str_replace(['&', '?'], ['&', '?'], htmlspecialchars($url->getRelativeUrl()))|noescape} 96 |

97 | 98 | {if is_string($source)} 99 |

{$source} (class not found)

100 | {elseif $source} 101 |

{$source instanceof ReflectionClass ? $source->getName() : $source->getDeclaringClass()->getName() . '::' . $source->getName() . '()'}

102 | {/if} 103 |
104 | 105 |
106 | {if empty($routes)} 107 |

No routes defined.

108 | {else} 109 |
110 |
111 |
112 |
Mask / Class
113 |
Defaults
114 |
Matched as
115 |
116 | 117 | {define routeList $list, $path = ''} 118 |
119 | {if $list[domain] || $list[module]} 120 |
121 | {if $list[domain]}domain = {$list[domain]}{/if} 122 | {if $list[module]}module = {$list[module]}{/if} 123 |
124 | {/if} 125 | {do $path .= $list[path]} 126 | {foreach $list[routes] as $router} 127 | {if is_array($router)} 128 | {include routeList $router, $path} 129 | {else} 130 | {include route $router, $path} 131 | {/if} 132 | {/foreach} 133 |
134 | {/define} 135 | 136 | {define route $route, $path} 137 |
138 |
139 | {=[yes => '✓', may => '≈', no => '', oneway => '⛔', error => '❌'][$route->matched]} 140 |
141 | 142 |
143 | 144 | {if $path !== ''}{$path}{/if} 145 | {isset($route->mask) ? str_replace(['/', '-'], ['/', '-'], htmlspecialchars($route->mask)) : str_replace('\\', '\\', htmlspecialchars($route->class))|noescape} 146 | 147 |
148 | 149 |
150 | 151 | {foreach $route->defaults as $key => $value} 152 | {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} 153 | {/foreach} 154 |
155 |
156 | 157 |
158 | {if $route->params} 159 | 160 | {do $params = $route->params} 161 | {if isset($params[Presenter::PresenterKey])} 162 | {$params[presenter]}:{$params[Presenter::ActionKey] ?? Presenter::DefaultAction} 163 |
164 | {do unset($params[Presenter::PresenterKey], $params[Presenter::ActionKey])} 165 | {/if} 166 | {foreach $params as $key => $value} 167 | {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} 168 | {/foreach} 169 |
170 | {elseif $route->error} 171 | {$route->error->getMessage()} 172 | {/if} 173 |
174 |
175 | {/define} 176 | 177 | {include routeList $routes} 178 |
179 | {/if} 180 |
181 |
182 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/TemplateGenerator.php: -------------------------------------------------------------------------------- 1 | */ 30 | private array $properties = []; 31 | private bool $trackProperties = true; 32 | 33 | 34 | public function __construct( 35 | Latte\Engine $latte, 36 | ?string $className = null, 37 | ?UI\Control $control = null, 38 | ) { 39 | parent::__construct($latte); 40 | 41 | $this->className = $className && $className !== DefaultTemplate::class 42 | ? $className 43 | : preg_replace('#Control|Presenter$#', '', $control::class) . 'Template'; 44 | 45 | if (!class_exists($this->className)) { 46 | $this->createTemplateClass($control); 47 | $this->updateControlPhpDoc($control); 48 | } 49 | $this->loadTemplateClass(); 50 | } 51 | 52 | 53 | public function __set($name, $value): void 54 | { 55 | if ($this->trackProperties) { 56 | ($this->findPropertyOwner($name) ?? $this)->ensureProperty($name, $value); 57 | } 58 | $this->$name = $value; 59 | } 60 | 61 | 62 | public function addDefaultVariable(string $name, mixed $value): void 63 | { 64 | $owner = $this->findPropertyOwner($name) ?? $this; 65 | if (!isset($owner->properties[$name])) { 66 | if (is_object($value)) { 67 | if (PHP_VERSION_ID >= 80400) { 68 | $rc = new \ReflectionClass($value); 69 | $rc->initializeLazyObject($value); 70 | $value = ($rc)->newLazyProxy(fn() => $this->ensureProperty($name, $value)); 71 | } 72 | 73 | } else { 74 | $value = new class (fn() => $this->ensureProperty($name, $value)) implements \IteratorAggregate { 75 | public function __construct( 76 | private \Closure $cb, 77 | ) { 78 | } 79 | 80 | 81 | public function __toString(): string 82 | { 83 | return ($this->cb)(); // basePath & baseUrl 84 | } 85 | 86 | 87 | public function getIterator(): \Traversable 88 | { 89 | yield from ($this->cb)(); // flashes 90 | } 91 | }; 92 | } 93 | } 94 | 95 | $this->trackProperties = false; 96 | $this->$name = $value; 97 | $this->trackProperties = true; 98 | } 99 | 100 | 101 | private function ensureProperty(string $name, mixed $value): mixed 102 | { 103 | $declaredType = $this->properties[$name] ?? null; 104 | $actualType = Type::fromValue($value); 105 | if (!$declaredType) { 106 | $this->properties[$name] = $actualType; 107 | $this->updateTemplateClass($name); 108 | } elseif (!$declaredType->allows($actualType)) { 109 | $this->properties[$name] = $declaredType->with($actualType); 110 | $this->updateTemplateClass($name); 111 | } 112 | return $value; 113 | } 114 | 115 | 116 | private function findPropertyOwner(string $name): ?self 117 | { 118 | return match (true) { 119 | isset($this->properties[$name]) => $this, 120 | $this->parent !== null => $this->parent->findPropertyOwner($name), 121 | default => null, 122 | }; 123 | } 124 | 125 | 126 | /********************* generator ****************d*g**/ 127 | 128 | 129 | private function createTemplateClass(UI\Control $control): void 130 | { 131 | [$namespace, $shortName] = Helpers::splitClassName($this->className); 132 | $namespaceCode = $namespace ? PHP_EOL . "namespace $namespace;" . PHP_EOL : ''; 133 | $fileName = dirname((new \ReflectionClass($control))->getFileName()) . '/' . $shortName . '.php'; 134 | file_put_contents($fileName, <<className); 152 | foreach ($rc->getProperties() as $prop) { 153 | if ($prop->getDeclaringClass() == $rc) { // intentionally == 154 | $this->properties[$prop->getName()] = Type::fromReflection($prop); 155 | } 156 | } 157 | 158 | $parent = $rc->getParentClass()->getName(); 159 | if ($parent !== Template::class) { 160 | $this->parent = new self($this->getLatte(), $parent); 161 | } 162 | } 163 | 164 | 165 | private function updateTemplateClass(string $name): void 166 | { 167 | $file = (new \ReflectionClass($this->className))->getFileName(); 168 | if (!is_file($file)) { 169 | throw new \RuntimeException("Cannot update class file for {$this->className}, file not found."); 170 | } 171 | 172 | $src = file_get_contents($file); 173 | $typeDecl = preg_replace_callback( 174 | '/([\w\\\]+)/', 175 | fn($m) => Validators::isBuiltinType($m[1]) ? $m[1] : '\\' . $m[1], 176 | (string) $this->properties[$name], 177 | ); 178 | $decl = "\tpublic $typeDecl \$$name"; 179 | 180 | $src = preg_replace( 181 | '/^\s*public\s+[^$;]*\s*\$' . $name . '\b/m', 182 | $decl, 183 | $src, 184 | count: $count, 185 | ); 186 | if (!$count) { 187 | if ($pos = strrpos($src, '}')) { 188 | $src = substr_replace($src, $decl . ';' . PHP_EOL, $pos, 0); 189 | } else { 190 | throw new \RuntimeException("Cannot update class file for {$this->className}, invalid syntax."); 191 | } 192 | } 193 | file_put_contents($file, $src); 194 | } 195 | 196 | 197 | private function updateControlPhpDoc(UI\Control $control): void 198 | { 199 | $rc = new \ReflectionClass($control); 200 | $doc = $rc->getDocComment(); 201 | $content = file_get_contents($rc->getFileName()); 202 | $nl = PHP_EOL; 203 | $decl = '* @property-read ' . Helpers::splitClassName($this->className)[1] . ' $template'; 204 | 205 | if (!$doc) { 206 | $content = preg_replace( 207 | '/^((final\s+)class\s+' . $rc->getShortName() . ')/m', 208 | "/**$nl $decl$nl */$nl$1", 209 | $content, 210 | ); 211 | } elseif (!preg_match('/@property(-read)?\s+.*\$template/', $doc)) { 212 | $newDoc = preg_replace('~(\s*)\*/\s*$~', "$1$decl$0", $doc, 1); 213 | $content = str_replace($doc, $newDoc, $content); 214 | } else { 215 | return; 216 | } 217 | 218 | file_put_contents($rc->getFileName(), $content); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationTracy/dist/panel.phtml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 82 | 83 |

84 | no route 85 | 86 | : 87 | 88 | 89 | 90 | ! 91 | 92 |

93 | 94 |
95 |
96 |

97 | 98 | 99 | getBaseUrl()) ?> 100 | &', '?'], htmlspecialchars($url->getRelativeUrl())) ?> 101 | 102 |

103 | 104 |

105 | (class not found)

106 |

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

109 |
110 | 111 |
112 |

No routes defined.

113 |
114 |
115 |
116 |
Mask / Class
117 |
Defaults
118 |
Matched as
119 |
120 | 121 |
122 |
123 | 124 | domain = 125 | 126 | 127 | 128 | module = 129 | 130 | 131 |
132 |
133 | 134 |
136 |
138 | '✓', 'may' => '≈', 'no' => '', 'oneway' => '⛔', 'error' => '❌'][$route->matched]) ?> 139 | 140 |
141 | 142 |
143 | 145 | 146 | 147 | 148 | 149 | mask) ? str_replace(['/', '-'], ['/', '-'], htmlspecialchars($route->mask)) : str_replace('\\', '\\', htmlspecialchars($route->class)) ?> 150 | 151 | 152 |
153 | 154 |
155 | 156 | defaults as $key => $value): ?> 157 |  =  158 | 159 |
true, Dumper::LIVE => true]) ?> 160 | 161 | 162 |
163 |
164 | 165 |
166 | params): ?> 167 | params ?> 168 | : 169 | 170 |
171 | $value): ?> 172 |  =  173 | 174 |
true, Dumper::LIVE => true]) ?> 175 | 176 | 177 |
178 | error): ?> error->getMessage()) ?> 179 | 180 |
181 |
182 | 183 |
184 |
185 |
186 | -------------------------------------------------------------------------------- /src/Application/Application.php: -------------------------------------------------------------------------------- 1 | Occurs before the application loads presenter */ 31 | public array $onStartup = []; 32 | 33 | /** @var array Occurs before the application shuts down */ 34 | public array $onShutdown = []; 35 | 36 | /** @var array Occurs when a new request is received */ 37 | public array $onRequest = []; 38 | 39 | /** @var array Occurs when a presenter is created */ 40 | public array $onPresenter = []; 41 | 42 | /** @var array Occurs when a new response is ready for dispatch */ 43 | public array $onResponse = []; 44 | 45 | /** @var array Occurs when an unhandled exception occurs in the application */ 46 | public array $onError = []; 47 | 48 | /** @var Request[] */ 49 | private array $requests = []; 50 | private ?IPresenter $presenter = null; 51 | private Nette\Http\IRequest $httpRequest; 52 | private Nette\Http\IResponse $httpResponse; 53 | private IPresenterFactory $presenterFactory; 54 | private Router $router; 55 | 56 | 57 | public function __construct( 58 | IPresenterFactory $presenterFactory, 59 | Router $router, 60 | Nette\Http\IRequest $httpRequest, 61 | Nette\Http\IResponse $httpResponse, 62 | ) { 63 | $this->httpRequest = $httpRequest; 64 | $this->httpResponse = $httpResponse; 65 | $this->presenterFactory = $presenterFactory; 66 | $this->router = $router; 67 | } 68 | 69 | 70 | /** 71 | * Dispatch a HTTP request to a front controller. 72 | */ 73 | public function run(): void 74 | { 75 | try { 76 | Arrays::invoke($this->onStartup, $this); 77 | $this->processRequest($this->createInitialRequest()) 78 | ->send($this->httpRequest, $this->httpResponse); 79 | Arrays::invoke($this->onShutdown, $this); 80 | 81 | } catch (\Throwable $e) { 82 | $this->sendHttpCode($e); 83 | Arrays::invoke($this->onError, $this, $e); 84 | if ($this->catchExceptions && ($req = $this->createErrorRequest($e))) { 85 | try { 86 | $this->processRequest($req) 87 | ->send($this->httpRequest, $this->httpResponse); 88 | Arrays::invoke($this->onShutdown, $this, $e); 89 | return; 90 | 91 | } catch (\Throwable $e) { 92 | Arrays::invoke($this->onError, $this, $e); 93 | } 94 | } 95 | 96 | Arrays::invoke($this->onShutdown, $this, $e); 97 | throw $e; 98 | } 99 | } 100 | 101 | 102 | public function createInitialRequest(): Request 103 | { 104 | $params = $this->router->match($this->httpRequest); 105 | $presenter = $params[UI\Presenter::PresenterKey] ?? null; 106 | 107 | if ($params === null) { 108 | throw new BadRequestException('No route for HTTP request.'); 109 | } elseif (!is_string($presenter)) { 110 | throw new Nette\InvalidStateException('Missing presenter in route definition.'); 111 | } elseif (str_starts_with($presenter, 'Nette:') && $presenter !== 'Nette:Micro') { 112 | throw new BadRequestException('Invalid request. Presenter is not achievable.'); 113 | } 114 | 115 | unset($params[UI\Presenter::PresenterKey]); 116 | return new Request( 117 | $presenter, 118 | $this->httpRequest->getMethod(), 119 | $params, 120 | $this->httpRequest->getPost(), 121 | $this->httpRequest->getFiles(), 122 | ); 123 | } 124 | 125 | 126 | public function processRequest(Request $request): Response 127 | { 128 | process: 129 | if (count($this->requests) > $this->maxLoop) { 130 | throw new ApplicationException('Too many loops detected in application life cycle.'); 131 | } 132 | 133 | $this->requests[] = $request; 134 | Arrays::invoke($this->onRequest, $this, $request); 135 | 136 | if ( 137 | !$request->isMethod($request::FORWARD) 138 | && (!strcasecmp($request->getPresenterName(), (string) $this->errorPresenter) 139 | || !strcasecmp($request->getPresenterName(), (string) $this->error4xxPresenter)) 140 | ) { 141 | throw new BadRequestException('Invalid request. Presenter is not achievable.'); 142 | } 143 | 144 | try { 145 | $this->presenter = $this->presenterFactory->createPresenter($request->getPresenterName()); 146 | } catch (InvalidPresenterException $e) { 147 | throw count($this->requests) > 1 148 | ? $e 149 | : new BadRequestException($e->getMessage(), 0, $e); 150 | } 151 | 152 | Arrays::invoke($this->onPresenter, $this, $this->presenter); 153 | $response = $this->presenter->run(clone $request); 154 | 155 | if ($response instanceof Responses\ForwardResponse) { 156 | $request = $response->getRequest(); 157 | goto process; 158 | } 159 | 160 | Arrays::invoke($this->onResponse, $this, $response); 161 | return $response; 162 | } 163 | 164 | 165 | public function createErrorRequest(\Throwable $e): ?Request 166 | { 167 | $errorPresenter = $e instanceof BadRequestException 168 | ? $this->error4xxPresenter ?? $this->errorPresenter 169 | : $this->errorPresenter; 170 | 171 | if ($errorPresenter === null) { 172 | return null; 173 | } 174 | 175 | $args = ['exception' => $e, 'previousPresenter' => $this->presenter, 'request' => Arrays::last($this->requests) ?? null]; 176 | if ($this->presenter instanceof UI\Presenter) { 177 | try { 178 | $this->presenter->forward(":$errorPresenter:", $args); 179 | } catch (AbortException) { 180 | return $this->presenter->getLastCreatedRequest(); 181 | } 182 | } 183 | 184 | return new Request($errorPresenter, Request::FORWARD, $args); 185 | } 186 | 187 | 188 | private function sendHttpCode(\Throwable $e): void 189 | { 190 | if (!$e instanceof BadRequestException && $this->httpResponse instanceof Nette\Http\Response) { 191 | $this->httpResponse->warnOnBuffer = false; 192 | } 193 | 194 | if (!$this->httpResponse->isSent()) { 195 | $this->httpResponse->setCode($e instanceof BadRequestException ? ($e->getHttpCode() ?: 404) : 500); 196 | } 197 | } 198 | 199 | 200 | /** 201 | * Returns all processed requests. 202 | * @return Request[] 203 | */ 204 | final public function getRequests(): array 205 | { 206 | return $this->requests; 207 | } 208 | 209 | 210 | /** 211 | * Returns current presenter. 212 | */ 213 | final public function getPresenter(): ?IPresenter 214 | { 215 | return $this->presenter; 216 | } 217 | 218 | 219 | /********************* services ****************d*g**/ 220 | 221 | 222 | /** 223 | * Returns router. 224 | */ 225 | public function getRouter(): Router 226 | { 227 | return $this->router; 228 | } 229 | 230 | 231 | /** 232 | * Returns presenter factory. 233 | */ 234 | public function getPresenterFactory(): IPresenterFactory 235 | { 236 | return $this->presenterFactory; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationDI/ApplicationExtension.php: -------------------------------------------------------------------------------- 1 | scanDirs = (array) $scanDirs; 40 | } 41 | 42 | 43 | public function getConfigSchema(): Nette\Schema\Schema 44 | { 45 | return Expect::structure([ 46 | 'debugger' => Expect::bool(), 47 | 'errorPresenter' => Expect::anyOf( 48 | Expect::structure([ 49 | '4xx' => Expect::string('Nette:Error')->dynamic(), 50 | '5xx' => Expect::string('Nette:Error')->dynamic(), 51 | ])->castTo('array'), 52 | Expect::string()->dynamic(), 53 | )->firstIsDefault(), 54 | 'catchExceptions' => Expect::bool(false)->dynamic(), 55 | 'mapping' => Expect::anyOf( 56 | Expect::string(), 57 | Expect::arrayOf('string|array'), 58 | ), 59 | 'aliases' => Expect::arrayOf('string'), 60 | 'scanDirs' => Expect::anyOf( 61 | Expect::arrayOf('string')->default($this->scanDirs)->mergeDefaults(), 62 | false, 63 | )->firstIsDefault(), 64 | 'scanComposer' => Expect::bool(class_exists(ClassLoader::class)), 65 | 'scanFilter' => Expect::string('*Presenter'), 66 | 'silentLinks' => Expect::bool(), 67 | 'generateTemplateClasses' => Expect::bool(), 68 | ]); 69 | } 70 | 71 | 72 | public function loadConfiguration(): void 73 | { 74 | $config = $this->config; 75 | $builder = $this->getContainerBuilder(); 76 | $builder->addExcludedClasses([UI\Presenter::class]); 77 | 78 | $this->invalidLinkMode = $this->debugMode 79 | ? UI\Presenter::InvalidLinkTextual | ($config->silentLinks ? 0 : UI\Presenter::InvalidLinkWarning) 80 | : UI\Presenter::InvalidLinkWarning; 81 | 82 | $application = $builder->addDefinition($this->prefix('application')) 83 | ->setFactory(Nette\Application\Application::class); 84 | if ($config->catchExceptions || !$this->debugMode) { 85 | $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]); 86 | $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]); 87 | } 88 | 89 | $this->compiler->addExportedType(Nette\Application\Application::class); 90 | 91 | if ($this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir) { 92 | $touch = $this->tempDir . '/touch'; 93 | Nette\Utils\FileSystem::createDir($this->tempDir); 94 | $this->getContainerBuilder()->addDependency($touch); 95 | } 96 | 97 | $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory')) 98 | ->setType(Nette\Application\IPresenterFactory::class) 99 | ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( 100 | Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class, 101 | [1 => $touch ?? null], 102 | )]); 103 | 104 | if ($config->mapping) { 105 | $presenterFactory->addSetup('setMapping', [ 106 | is_string($config->mapping) ? ['*' => $config->mapping] : $config->mapping, 107 | ]); 108 | } 109 | 110 | if ($config->aliases) { 111 | $presenterFactory->addSetup('setAliases', [$config->aliases]); 112 | } 113 | 114 | $builder->addDefinition($this->prefix('linkGenerator')) 115 | ->setFactory(Nette\Application\LinkGenerator::class, [ 116 | 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']), 117 | ]); 118 | 119 | if ($this->name === 'application') { 120 | $builder->addAlias('application', $this->prefix('application')); 121 | $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory')); 122 | } 123 | } 124 | 125 | 126 | public function beforeCompile(): void 127 | { 128 | $builder = $this->getContainerBuilder(); 129 | 130 | if ($this->config->debugger ?? $builder->getByType(Tracy\BlueScreen::class)) { 131 | $builder->getDefinition($this->prefix('application')) 132 | ->addSetup([self::class, 'initializeBlueScreenPanel']); 133 | } 134 | 135 | if ($this->debugMode && $this->config->generateTemplateClasses) { 136 | $builder->getDefinition('latte.templateFactory') 137 | ->setArgument('generate', true); 138 | } 139 | 140 | $all = []; 141 | 142 | foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) { 143 | $all[$def->getType()] = $def; 144 | } 145 | 146 | $counter = 0; 147 | foreach ($this->findPresenters() as $class) { 148 | $this->checkPresenter($class); 149 | if (empty($all[$class])) { 150 | $all[$class] = $builder->addDefinition($this->prefix((string) ++$counter)) 151 | ->setType($class); 152 | } 153 | } 154 | 155 | foreach ($all as $def) { 156 | $def->addTag(Nette\DI\Extensions\InjectExtension::TagInject) 157 | ->setAutowired(false); 158 | 159 | if (is_subclass_of($def->getType(), UI\Presenter::class) && $def instanceof Definitions\ServiceDefinition) { 160 | $def->addSetup('$invalidLinkMode', [$this->invalidLinkMode]); 161 | } 162 | 163 | $this->compiler->addExportedType($def->getType()); 164 | } 165 | } 166 | 167 | 168 | /** @return string[] */ 169 | private function findPresenters(): array 170 | { 171 | $config = $this->getConfig(); 172 | 173 | if ($config->scanDirs) { 174 | if (!class_exists(Nette\Loaders\RobotLoader::class)) { 175 | throw new Nette\NotSupportedException("RobotLoader is required to find presenters, install package `nette/robot-loader` or disable option {$this->prefix('scanDirs')}: false"); 176 | } 177 | 178 | $robot = new Nette\Loaders\RobotLoader; 179 | $robot->addDirectory(...$config->scanDirs); 180 | $robot->acceptFiles = [$config->scanFilter . '.php']; 181 | if ($this->tempDir) { 182 | $robot->setTempDirectory($this->tempDir); 183 | $robot->refresh(); 184 | } else { 185 | $robot->rebuild(); 186 | } 187 | } elseif ($this->robotLoader && $config->scanDirs !== false) { 188 | $robot = $this->robotLoader; 189 | $robot->refresh(); 190 | } 191 | 192 | $classes = []; 193 | if (isset($robot)) { 194 | $classes = array_keys($robot->getIndexedClasses()); 195 | } 196 | 197 | if ($config->scanComposer) { 198 | $rc = new \ReflectionClass(ClassLoader::class); 199 | $classFile = dirname($rc->getFileName()) . '/autoload_classmap.php'; 200 | if (is_file($classFile)) { 201 | $this->getContainerBuilder()->addDependency($classFile); 202 | $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile))); 203 | } 204 | } 205 | 206 | $presenters = []; 207 | foreach (array_unique($classes) as $class) { 208 | if ( 209 | fnmatch($config->scanFilter, $class) 210 | && class_exists($class) 211 | && ($rc = new \ReflectionClass($class)) 212 | && $rc->implementsInterface(Nette\Application\IPresenter::class) 213 | && !$rc->isAbstract() 214 | ) { 215 | $presenters[] = $rc->getName(); 216 | } 217 | } 218 | 219 | return $presenters; 220 | } 221 | 222 | 223 | /** @internal */ 224 | public static function initializeBlueScreenPanel( 225 | Tracy\BlueScreen $blueScreen, 226 | Nette\Application\Application $application, 227 | ): void 228 | { 229 | $blueScreen->addPanel(function (?\Throwable $e) use ($application, $blueScreen): ?array { 230 | $dumper = $blueScreen->getDumper(); 231 | return $e ? null : [ 232 | 'tab' => 'Nette Application', 233 | 'panel' => '

Requests

' . $dumper($application->getRequests()) 234 | . '

Presenter

' . $dumper($application->getPresenter()), 235 | ]; 236 | }); 237 | if ( 238 | version_compare(Tracy\Debugger::Version, '2.9.0', '>=') 239 | && version_compare(Tracy\Debugger::Version, '3.0', '<') 240 | ) { 241 | $blueScreen->addFileGenerator(self::generateNewPresenterFileContents(...)); 242 | } 243 | } 244 | 245 | 246 | public static function generateNewPresenterFileContents(string $file, ?string $class = null): ?string 247 | { 248 | if (!$class || !str_ends_with($file, 'Presenter.php')) { 249 | return null; 250 | } 251 | 252 | $res = "checked[$class])) { 266 | return; 267 | } 268 | $this->checked[$class] = true; 269 | 270 | $rc = new \ReflectionClass($class); 271 | if ($rc->getParentClass()) { 272 | $this->checkPresenter($rc->getParentClass()->getName()); 273 | } 274 | 275 | foreach ($rc->getProperties() as $rp) { 276 | if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class)) 277 | && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) 278 | ) { 279 | throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr)); 280 | } 281 | } 282 | 283 | $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; 284 | foreach ($rc->getMethods() as $rm) { 285 | if (preg_match("#^$re#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { 286 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm))); 287 | } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) { 288 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm))); 289 | } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) 290 | && !preg_match("#^$re|createComponent.#", $rm->getName()) 291 | ) { 292 | throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render, handle or createComponent methods.', Reflection::toString($rm), Attributes\Requires::class)); 293 | } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) { 294 | throw new Nette\InvalidStateException(sprintf('Method %s: attribute %s can be used only with action, render or handle methods.', Reflection::toString($rm), Attributes\Deprecated::class)); 295 | } 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Application/LinkGenerator.php: -------------------------------------------------------------------------------- 1 | createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link'); 50 | $relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls; 51 | return $mode === 'forward' || $mode === 'test' 52 | ? null 53 | : $this->requestToUrl($request, $relative) . $parts['fragment']; 54 | } 55 | 56 | 57 | /** 58 | * @param string $destination in format "[[[module:]presenter:]action | signal! | this | @alias]" 59 | * @param string $mode forward|redirect|link 60 | * @throws UI\InvalidLinkException 61 | * @internal 62 | */ 63 | public function createRequest( 64 | ?UI\Component $component, 65 | string $destination, 66 | array $args, 67 | string $mode, 68 | ): Request 69 | { 70 | // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final 71 | 72 | $this->lastRequest = null; 73 | $refPresenter = $component?->getPresenter(); 74 | $path = $destination; 75 | 76 | if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) { 77 | [$cname, $signal] = Helpers::splitName(rtrim($destination, '!')); 78 | if ($cname !== '') { 79 | $component = $component->getComponent(strtr($cname, ':', '-')); 80 | } 81 | 82 | if ($signal === '') { 83 | throw new UI\InvalidLinkException('Signal must be non-empty string.'); 84 | } 85 | 86 | $path = 'this'; 87 | } 88 | 89 | if ($path[0] === '@') { 90 | if (!$this->presenterFactory instanceof PresenterFactory) { 91 | throw new \LogicException('Link aliasing requires PresenterFactory service.'); 92 | } 93 | $path = ':' . $this->presenterFactory->getAlias(substr($path, 1)); 94 | } 95 | 96 | $current = false; 97 | [$presenter, $action] = Helpers::splitName($path); 98 | if ($presenter === '') { 99 | if (!$refPresenter) { 100 | throw new \LogicException("Presenter must be specified in '$destination'."); 101 | } 102 | $action = $path === 'this' ? $refPresenter->getAction() : $action; 103 | $presenter = $refPresenter->getName(); 104 | $presenterClass = $refPresenter::class; 105 | 106 | } else { 107 | if ($presenter[0] === ':') { // absolute 108 | $presenter = substr($presenter, 1); 109 | if (!$presenter) { 110 | throw new UI\InvalidLinkException("Missing presenter name in '$destination'."); 111 | } 112 | } elseif ($refPresenter) { // relative 113 | [$module, , $sep] = Helpers::splitName($refPresenter->getName()); 114 | $presenter = $module . $sep . $presenter; 115 | } 116 | 117 | try { 118 | $presenterClass = $this->presenterFactory?->getPresenterClass($presenter); 119 | } catch (InvalidPresenterException $e) { 120 | throw new UI\InvalidLinkException($e->getMessage(), 0, $e); 121 | } 122 | } 123 | 124 | // PROCESS SIGNAL ARGUMENTS 125 | if (isset($signal)) { // $component must be StatePersistent 126 | $reflection = new UI\ComponentReflection($component::class); 127 | if ($signal === 'this') { // means "no signal" 128 | $signal = ''; 129 | if (array_key_exists(0, $args)) { 130 | throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal."); 131 | } 132 | } elseif (!str_contains($signal, UI\Component::NameSeparator)) { 133 | // counterpart of signalReceived() & tryCall() 134 | 135 | $method = $reflection->getSignalMethod($signal); 136 | if (!$method) { 137 | throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()"); 138 | } 139 | 140 | $this->checkAllowed($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode); 141 | 142 | // convert indexed parameters to named 143 | UI\ParameterConverter::toParameters($method, $args, [], $missing); 144 | } 145 | 146 | // counterpart of StatePersistent 147 | if ($args && array_intersect_key($args, $reflection->getPersistentParams())) { 148 | $component->saveState($args); 149 | } 150 | 151 | if ($args && $component !== $refPresenter) { 152 | $prefix = $component->getUniqueId() . UI\Component::NameSeparator; 153 | foreach ($args as $key => $val) { 154 | unset($args[$key]); 155 | $args[$prefix . $key] = $val; 156 | } 157 | } 158 | } 159 | 160 | // PROCESS ARGUMENTS 161 | if (is_subclass_of($presenterClass, UI\Presenter::class)) { 162 | if ($action === '') { 163 | $action = UI\Presenter::DefaultAction; 164 | } 165 | 166 | $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class; 167 | 168 | $reflection = new UI\ComponentReflection($presenterClass); 169 | $this->checkAllowed($refPresenter, $reflection, "presenter '$presenter'", $mode); 170 | 171 | foreach (array_intersect_key($reflection->getParameters(), $args) as $name => $param) { 172 | if ($args[$name] === $param['def']) { 173 | $args[$name] = null; // value transmit is unnecessary 174 | } 175 | } 176 | 177 | // counterpart of run() & tryCall() 178 | if ($method = $reflection->getActionRenderMethod($action)) { 179 | $this->checkAllowed($refPresenter, $method, "action '$presenter:$action'", $mode); 180 | 181 | UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? $refPresenter->getParameters() : [], $missing); 182 | 183 | } elseif (array_key_exists(0, $args)) { 184 | throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}()."); 185 | } 186 | 187 | // counterpart of StatePersistent 188 | if ($refPresenter) { 189 | if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) { 190 | $refPresenter->saveStatePartial($args, $reflection); 191 | } 192 | 193 | $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass); 194 | if ($current && $args) { 195 | $tmp = $globalState + $refPresenter->getParameters(); 196 | foreach ($args as $key => $val) { 197 | if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { 198 | $current = false; 199 | break; 200 | } 201 | } 202 | } 203 | 204 | $args += $globalState; 205 | } 206 | } 207 | 208 | if ($mode !== 'test' && !empty($missing)) { 209 | foreach ($missing as $rp) { 210 | if (!array_key_exists($rp->getName(), $args)) { 211 | throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction())); 212 | } 213 | } 214 | } 215 | 216 | // ADD ACTION & SIGNAL & FLASH 217 | if ($action) { 218 | $args[UI\Presenter::ActionKey] = $action; 219 | } 220 | 221 | if (!empty($signal)) { 222 | $args[UI\Presenter::SignalKey] = $component->getParameterId($signal); 223 | $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey); 224 | } 225 | 226 | if (($mode === 'redirect' || $mode === 'forward') && $refPresenter->hasFlashSession()) { 227 | $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey); 228 | $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null; 229 | } 230 | 231 | return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]); 232 | } 233 | 234 | 235 | /** 236 | * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]" 237 | * @throws UI\InvalidLinkException 238 | * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} 239 | * @internal 240 | */ 241 | public static function parseDestination(string $destination): array 242 | { 243 | if (!preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches)) { 244 | throw new UI\InvalidLinkException("Invalid destination '$destination'."); 245 | } 246 | 247 | if (!empty($matches['query'])) { 248 | trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED); 249 | parse_str(substr($matches['query'], 1), $args); 250 | } 251 | 252 | return [ 253 | 'absolute' => (bool) $matches['absolute'], 254 | 'path' => $matches['path'], 255 | 'signal' => !empty($matches['signal']), 256 | 'args' => $args ?? null, 257 | 'fragment' => $matches['fragment'] ?? '', 258 | ]; 259 | } 260 | 261 | 262 | /** 263 | * Converts Request to URL. 264 | */ 265 | public function requestToUrl(Request $request, ?bool $relative = false): string 266 | { 267 | $url = $this->router->constructUrl($request->toArray(), $this->refUrl); 268 | if ($url === null) { 269 | $params = $request->getParameters(); 270 | unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]); 271 | $params = urldecode(http_build_query($params, '', ', ')); 272 | throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)"); 273 | } 274 | 275 | if ($relative) { 276 | $hostUrl = $this->refUrl->getHostUrl() . '/'; 277 | if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) { 278 | $url = substr($url, strlen($hostUrl) - 1); 279 | } 280 | } 281 | 282 | return $url; 283 | } 284 | 285 | 286 | public function withReferenceUrl(string $url): static 287 | { 288 | return new self( 289 | $this->router, 290 | new UrlScript($url), 291 | $this->presenterFactory, 292 | ); 293 | } 294 | 295 | 296 | private function checkAllowed( 297 | ?UI\Presenter $presenter, 298 | \ReflectionClass|\ReflectionMethod $element, 299 | string $message, 300 | string $mode, 301 | ): void 302 | { 303 | if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->canGenerateLink()) { 304 | throw new UI\InvalidLinkException("Link to forbidden $message from '{$presenter->getName()}:{$presenter->getAction()}'."); 305 | } elseif ($presenter?->invalidLinkMode && $element->getAttributes(Attributes\Deprecated::class)) { 306 | trigger_error("Link to deprecated $message from '{$presenter->getName()}:{$presenter->getAction()}'.", E_USER_DEPRECATED); 307 | } 308 | } 309 | 310 | 311 | /** @internal */ 312 | public static function applyBase(string $link, string $base): string 313 | { 314 | return str_contains($link, ':') && $link[0] !== ':' 315 | ? ":$base:$link" 316 | : $link; 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/Application/UI/Component.php: -------------------------------------------------------------------------------- 1 | Occurs when component is attached to presenter */ 31 | public array $onAnchor = []; 32 | protected array $params = []; 33 | 34 | 35 | /** 36 | * Returns the presenter where this component belongs to. 37 | */ 38 | public function getPresenter(): ?Presenter 39 | { 40 | if (func_num_args()) { 41 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); 42 | $throw = func_get_arg(0); 43 | } 44 | 45 | return $this->lookup(Presenter::class, throw: $throw ?? true); 46 | } 47 | 48 | 49 | /** 50 | * Returns the presenter where this component belongs to. 51 | */ 52 | public function getPresenterIfExists(): ?Presenter 53 | { 54 | return $this->lookup(Presenter::class, throw: false); 55 | } 56 | 57 | 58 | /** @deprecated */ 59 | public function hasPresenter(): bool 60 | { 61 | return (bool) $this->lookup(Presenter::class, throw: false); 62 | } 63 | 64 | 65 | /** 66 | * Returns a fully-qualified name that uniquely identifies the component 67 | * within the presenter hierarchy. 68 | */ 69 | public function getUniqueId(): string 70 | { 71 | return $this->lookupPath(Presenter::class); 72 | } 73 | 74 | 75 | public function addComponent( 76 | Nette\ComponentModel\IComponent $component, 77 | ?string $name, 78 | ?string $insertBefore = null, 79 | ): static 80 | { 81 | if (!$component instanceof SignalReceiver && !$component instanceof StatePersistent) { 82 | throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the Presenter.'); 83 | } 84 | 85 | return parent::addComponent($component, $name, $insertBefore = null); 86 | } 87 | 88 | 89 | protected function createComponent(string $name): ?Nette\ComponentModel\IComponent 90 | { 91 | if (method_exists($this, $method = 'createComponent' . $name)) { 92 | (new AccessPolicy(new \ReflectionMethod($this, $method)))->checkAccess($this); 93 | $this->checkRequirements($rm); 94 | } 95 | return parent::createComponent($name); 96 | } 97 | 98 | 99 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void 100 | { 101 | parent::validateParent($parent); 102 | $this->monitor(Presenter::class, function (Presenter $presenter): void { 103 | $this->loadState($presenter->popGlobalParameters($this->getUniqueId())); 104 | Nette\Utils\Arrays::invoke($this->onAnchor, $this); 105 | }); 106 | } 107 | 108 | 109 | /** 110 | * Calls public method if exists. 111 | */ 112 | protected function tryCall(string $method, array $params): bool 113 | { 114 | $rc = $this->getReflection(); 115 | if (!$rc->hasMethod($method)) { 116 | return false; 117 | } elseif (!$rc->hasCallableMethod($method)) { 118 | $this->error('Method ' . Nette\Utils\Reflection::toString($rc->getMethod($method)) . ' is not callable.'); 119 | } 120 | 121 | $rm = $rc->getMethod($method); 122 | (new AccessPolicy($rm))->checkAccess($this); 123 | $this->checkRequirements($rm); 124 | try { 125 | $args = ParameterConverter::toArguments($rm, $params); 126 | } catch (Nette\InvalidArgumentException $e) { 127 | $this->error($e->getMessage()); 128 | } 129 | 130 | $rm->invokeArgs($this, $args); 131 | return true; 132 | } 133 | 134 | 135 | /** 136 | * Descendant can override this method to check for permissions. 137 | * It is called with the presenter class and the render*(), action*(), and handle*() methods. 138 | */ 139 | public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void 140 | { 141 | } 142 | 143 | 144 | /** 145 | * Access to reflection. 146 | */ 147 | public static function getReflection(): ComponentReflection 148 | { 149 | return new ComponentReflection(static::class); 150 | } 151 | 152 | 153 | /********************* interface StatePersistent ****************d*g**/ 154 | 155 | 156 | /** 157 | * Loads state information. 158 | */ 159 | public function loadState(array $params): void 160 | { 161 | $reflection = $this->getReflection(); 162 | foreach ($reflection->getParameters() as $name => $meta) { 163 | if (isset($params[$name])) { // nulls are ignored 164 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) { 165 | $this->error(sprintf( 166 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.", 167 | $name, 168 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", 169 | $meta['type'], 170 | get_debug_type($params[$name]), 171 | )); 172 | } 173 | 174 | $this->$name = &$params[$name]; 175 | } else { 176 | $params[$name] = &$this->$name; 177 | } 178 | } 179 | 180 | $this->params = $params; 181 | } 182 | 183 | 184 | /** 185 | * Saves state information for next request. 186 | */ 187 | public function saveState(array &$params): void 188 | { 189 | $this->saveStatePartial($params, static::getReflection()); 190 | } 191 | 192 | 193 | /** 194 | * @internal used by presenter 195 | */ 196 | public function saveStatePartial(array &$params, ComponentReflection $reflection): void 197 | { 198 | $tree = Nette\Application\Helpers::getClassesAndTraits(static::class); 199 | 200 | foreach ($reflection->getPersistentParams() as $name => $meta) { 201 | if (isset($params[$name])) { 202 | // injected value 203 | 204 | } elseif ( 205 | array_key_exists($name, $params) // nulls are skipped 206 | || (isset($meta['since']) && !isset($tree[$meta['since']])) // not related 207 | || !isset($this->$name) 208 | ) { 209 | continue; 210 | 211 | } else { 212 | $params[$name] = $this->$name; // object property value 213 | } 214 | 215 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) { 216 | throw new InvalidLinkException(sprintf( 217 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.", 218 | $name, 219 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", 220 | $meta['type'], 221 | get_debug_type($params[$name]), 222 | )); 223 | } 224 | 225 | if ($params[$name] === $meta['def'] || ($meta['def'] === null && $params[$name] === '')) { 226 | $params[$name] = null; // value transmit is unnecessary 227 | } 228 | } 229 | } 230 | 231 | 232 | /** 233 | * Returns component param. 234 | */ 235 | final public function getParameter(string $name): mixed 236 | { 237 | if (func_num_args() > 1) { 238 | trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED); 239 | $default = func_get_arg(1); 240 | } 241 | return $this->params[$name] ?? $default ?? null; 242 | } 243 | 244 | 245 | /** 246 | * Returns component parameters. 247 | */ 248 | final public function getParameters(): array 249 | { 250 | return array_map(fn($item) => $item, $this->params); 251 | } 252 | 253 | 254 | /** 255 | * Returns a fully-qualified name that uniquely identifies the parameter. 256 | */ 257 | final public function getParameterId(string $name): string 258 | { 259 | $uid = $this->getUniqueId(); 260 | return $uid === '' ? $name : $uid . self::NameSeparator . $name; 261 | } 262 | 263 | 264 | /********************* interface SignalReceiver ****************d*g**/ 265 | 266 | 267 | /** 268 | * Calls signal handler method. 269 | * @throws BadSignalException if there is not handler method 270 | */ 271 | public function signalReceived(string $signal): void 272 | { 273 | if (!$this->tryCall($this->formatSignalMethod($signal), $this->params)) { 274 | $class = static::class; 275 | throw new BadSignalException("There is no handler for signal '$signal' in class $class."); 276 | } 277 | } 278 | 279 | 280 | /** 281 | * Formats signal handler method name -> case sensitivity doesn't matter. 282 | */ 283 | public static function formatSignalMethod(string $signal): string 284 | { 285 | return 'handle' . $signal; 286 | } 287 | 288 | 289 | /********************* navigation ****************d*g**/ 290 | 291 | 292 | /** 293 | * Generates URL to presenter, action or signal. 294 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 295 | * @param mixed ...$args 296 | * @throws InvalidLinkException 297 | */ 298 | public function link(string $destination, ...$args): string 299 | { 300 | try { 301 | $args = count($args) === 1 && is_array($args[0] ?? null) 302 | ? $args[0] 303 | : $args; 304 | return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link'); 305 | 306 | } catch (InvalidLinkException $e) { 307 | return $this->getPresenter()->processInvalidLink($e); 308 | } 309 | } 310 | 311 | 312 | /** 313 | * Returns destination as Link object. 314 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 315 | * @param mixed ...$args 316 | */ 317 | public function lazyLink(string $destination, ...$args): Link 318 | { 319 | $args = count($args) === 1 && is_array($args[0] ?? null) 320 | ? $args[0] 321 | : $args; 322 | return new Link($this, $destination, $args); 323 | } 324 | 325 | 326 | /** 327 | * Determines whether it links to the current page. 328 | * @param ?string $destination in format "[[[module:]presenter:]action | signal! | this]" 329 | * @param mixed ...$args 330 | * @throws InvalidLinkException 331 | */ 332 | public function isLinkCurrent(?string $destination = null, ...$args): bool 333 | { 334 | if ($destination !== null) { 335 | $args = count($args) === 1 && is_array($args[0] ?? null) 336 | ? $args[0] 337 | : $args; 338 | $this->getPresenter()->getLinkGenerator()->createRequest($this, $destination, $args, 'test'); 339 | } 340 | 341 | return $this->getPresenter()->getLastCreatedRequestFlag('current'); 342 | } 343 | 344 | 345 | /** 346 | * Redirect to another presenter, action or signal. 347 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 348 | * @param mixed ...$args 349 | * @throws Nette\Application\AbortException 350 | */ 351 | public function redirect(string $destination, ...$args): never 352 | { 353 | $args = count($args) === 1 && is_array($args[0] ?? null) 354 | ? $args[0] 355 | : $args; 356 | $presenter = $this->getPresenter(); 357 | $presenter->saveGlobalState(); 358 | $presenter->redirectUrl($presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect')); 359 | } 360 | 361 | 362 | /** 363 | * Permanently redirects to presenter, action or signal. 364 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 365 | * @param mixed ...$args 366 | * @throws Nette\Application\AbortException 367 | */ 368 | public function redirectPermanent(string $destination, ...$args): never 369 | { 370 | $args = count($args) === 1 && is_array($args[0] ?? null) 371 | ? $args[0] 372 | : $args; 373 | $presenter = $this->getPresenter(); 374 | $presenter->redirectUrl( 375 | $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'), 376 | Nette\Http\IResponse::S301_MovedPermanently, 377 | ); 378 | } 379 | 380 | 381 | /** 382 | * Throws HTTP error. 383 | * @throws Nette\Application\BadRequestException 384 | */ 385 | public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): never 386 | { 387 | throw new Nette\Application\BadRequestException($message, $httpCode); 388 | } 389 | } 390 | --------------------------------------------------------------------------------