├── .phpstorm.meta.php ├── composer.json ├── license.md ├── readme.md └── src ├── Application ├── Application.php ├── Attributes │ ├── CrossOrigin.php │ ├── Deprecated.php │ ├── Parameter.php │ ├── Persistent.php │ └── Requires.php ├── ErrorPresenter.php ├── Helpers.php ├── IPresenter.php ├── IPresenterFactory.php ├── LinkGenerator.php ├── MicroPresenter.php ├── PresenterFactory.php ├── Request.php ├── Response.php ├── Responses │ ├── CallbackResponse.php │ ├── FileResponse.php │ ├── ForwardResponse.php │ ├── JsonResponse.php │ ├── RedirectResponse.php │ ├── TextResponse.php │ └── VoidResponse.php ├── Routers │ ├── CliRouter.php │ ├── Route.php │ ├── RouteList.php │ └── SimpleRouter.php ├── UI │ ├── AccessPolicy.php │ ├── BadSignalException.php │ ├── Component.php │ ├── ComponentReflection.php │ ├── Control.php │ ├── Form.php │ ├── InvalidLinkException.php │ ├── Link.php │ ├── MethodReflection.php │ ├── Multiplier.php │ ├── ParameterConverter.php │ ├── Presenter.php │ ├── Renderable.php │ ├── SignalReceiver.php │ ├── StatePersistent.php │ ├── Template.php │ └── TemplateFactory.php ├── exceptions.php └── templates │ └── error.phtml ├── Bridges ├── ApplicationDI │ ├── ApplicationExtension.php │ ├── LatteExtension.php │ ├── PresenterFactoryCallback.php │ └── RoutingExtension.php ├── ApplicationLatte │ ├── DefaultTemplate.php │ ├── LatteFactory.php │ ├── Nodes │ │ ├── ControlNode.php │ │ ├── IfCurrentNode.php │ │ ├── LinkNode.php │ │ ├── NNonceNode.php │ │ ├── SnippetAreaNode.php │ │ ├── SnippetNode.php │ │ └── TemplatePrintNode.php │ ├── SnippetRuntime.php │ ├── Template.php │ ├── TemplateFactory.php │ └── UIExtension.php └── ApplicationTracy │ ├── RoutingPanel.php │ ├── dist │ ├── panel.phtml │ └── tab.phtml │ ├── panel.latte │ └── tab.latte └── compatibility-intf.php /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | '@'])); 67 | -------------------------------------------------------------------------------- /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.1 - 8.4", 19 | "nette/component-model": "^3.1", 20 | "nette/http": "^3.3", 21 | "nette/routing": "^3.1", 22 | "nette/utils": "^4.0" 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", 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.0.18", 35 | "tracy/tracy": "^2.9", 36 | "mockery/mockery": "^2.0", 37 | "phpstan/phpstan-nette": "^1.0", 38 | "jetbrains/phpstorm-attributes": "dev-master" 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.0.18", 46 | "tracy/tracy": "<2.9" 47 | }, 48 | "autoload": { 49 | "classmap": ["src/"] 50 | }, 51 | "minimum-stability": "dev", 52 | "scripts": { 53 | "phpstan": "phpstan analyse", 54 | "tester": "tester tests -s" 55 | }, 56 | "extra": { 57 | "branch-alias": { 58 | "dev-master": "4.0-dev" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Application.php: -------------------------------------------------------------------------------- 1 | Occurs before the application loads presenter */ 30 | public array $onStartup = []; 31 | 32 | /** @var array Occurs before the application shuts down */ 33 | public array $onShutdown = []; 34 | 35 | /** @var array Occurs when a new request is received */ 36 | public array $onRequest = []; 37 | 38 | /** @var array Occurs when a presenter is created */ 39 | public array $onPresenter = []; 40 | 41 | /** @var array Occurs when a new response is ready for dispatch */ 42 | public array $onResponse = []; 43 | 44 | /** @var array Occurs when an unhandled exception occurs in the application */ 45 | public array $onError = []; 46 | 47 | /** @var Request[] */ 48 | private array $requests = []; 49 | private ?IPresenter $presenter = null; 50 | private Nette\Http\IRequest $httpRequest; 51 | private Nette\Http\IResponse $httpResponse; 52 | private IPresenterFactory $presenterFactory; 53 | private Router $router; 54 | 55 | 56 | public function __construct( 57 | IPresenterFactory $presenterFactory, 58 | Router $router, 59 | Nette\Http\IRequest $httpRequest, 60 | Nette\Http\IResponse $httpResponse, 61 | ) { 62 | $this->httpRequest = $httpRequest; 63 | $this->httpResponse = $httpResponse; 64 | $this->presenterFactory = $presenterFactory; 65 | $this->router = $router; 66 | } 67 | 68 | 69 | /** 70 | * Dispatch a HTTP request to a front controller. 71 | */ 72 | public function run(): void 73 | { 74 | try { 75 | Arrays::invoke($this->onStartup, $this); 76 | $this->processRequest($this->createInitialRequest()); 77 | Arrays::invoke($this->onShutdown, $this); 78 | 79 | } catch (\Throwable $e) { 80 | $this->sendHttpCode($e); 81 | Arrays::invoke($this->onError, $this, $e); 82 | if ($this->catchExceptions && ($req = $this->createErrorRequest($e))) { 83 | try { 84 | $this->processRequest($req); 85 | Arrays::invoke($this->onShutdown, $this, $e); 86 | return; 87 | 88 | } catch (\Throwable $e) { 89 | Arrays::invoke($this->onError, $this, $e); 90 | } 91 | } 92 | 93 | Arrays::invoke($this->onShutdown, $this, $e); 94 | throw $e; 95 | } 96 | } 97 | 98 | 99 | public function createInitialRequest(): Request 100 | { 101 | $params = $this->router->match($this->httpRequest); 102 | $presenter = $params[UI\Presenter::PresenterKey] ?? null; 103 | 104 | if ($params === null) { 105 | throw new BadRequestException('No route for HTTP request.'); 106 | } elseif (!is_string($presenter)) { 107 | throw new Nette\InvalidStateException('Missing presenter in route definition.'); 108 | } elseif (str_starts_with($presenter, 'Nette:') && $presenter !== 'Nette:Micro') { 109 | throw new BadRequestException('Invalid request. Presenter is not achievable.'); 110 | } 111 | 112 | unset($params[UI\Presenter::PresenterKey]); 113 | return new Request( 114 | $presenter, 115 | $this->httpRequest->getMethod(), 116 | $params, 117 | $this->httpRequest->getPost(), 118 | $this->httpRequest->getFiles(), 119 | ); 120 | } 121 | 122 | 123 | public function processRequest(Request $request): void 124 | { 125 | process: 126 | if (count($this->requests) > $this->maxLoop) { 127 | throw new ApplicationException('Too many loops detected in application life cycle.'); 128 | } 129 | 130 | $this->requests[] = $request; 131 | Arrays::invoke($this->onRequest, $this, $request); 132 | 133 | if ( 134 | !$request->isMethod($request::FORWARD) 135 | && (!strcasecmp($request->getPresenterName(), (string) $this->errorPresenter) 136 | || !strcasecmp($request->getPresenterName(), (string) $this->error4xxPresenter)) 137 | ) { 138 | throw new BadRequestException('Invalid request. Presenter is not achievable.'); 139 | } 140 | 141 | try { 142 | $this->presenter = $this->presenterFactory->createPresenter($request->getPresenterName()); 143 | } catch (InvalidPresenterException $e) { 144 | throw count($this->requests) > 1 145 | ? $e 146 | : new BadRequestException($e->getMessage(), 0, $e); 147 | } 148 | 149 | Arrays::invoke($this->onPresenter, $this, $this->presenter); 150 | $response = $this->presenter->run(clone $request); 151 | 152 | if ($response instanceof Responses\ForwardResponse) { 153 | $request = $response->getRequest(); 154 | goto process; 155 | } 156 | 157 | Arrays::invoke($this->onResponse, $this, $response); 158 | $response->send($this->httpRequest, $this->httpResponse); 159 | } 160 | 161 | 162 | public function createErrorRequest(\Throwable $e): ?Request 163 | { 164 | $errorPresenter = $e instanceof BadRequestException 165 | ? $this->error4xxPresenter ?? $this->errorPresenter 166 | : $this->errorPresenter; 167 | 168 | if ($errorPresenter === null) { 169 | return null; 170 | } 171 | 172 | $args = ['exception' => $e, 'previousPresenter' => $this->presenter, 'request' => Arrays::last($this->requests) ?: null]; 173 | if ($this->presenter instanceof UI\Presenter) { 174 | try { 175 | $this->presenter->forward(":$errorPresenter:", $args); 176 | } catch (AbortException) { 177 | return $this->presenter->getLastCreatedRequest(); 178 | } 179 | } 180 | 181 | return new Request($errorPresenter, Request::FORWARD, $args); 182 | } 183 | 184 | 185 | private function sendHttpCode(\Throwable $e): void 186 | { 187 | if (!$e instanceof BadRequestException && $this->httpResponse instanceof Nette\Http\Response) { 188 | $this->httpResponse->warnOnBuffer = false; 189 | } 190 | 191 | if (!$this->httpResponse->isSent()) { 192 | $this->httpResponse->setCode($e instanceof BadRequestException ? ($e->getHttpCode() ?: 404) : 500); 193 | } 194 | } 195 | 196 | 197 | /** 198 | * Returns all processed requests. 199 | * @return Request[] 200 | */ 201 | final public function getRequests(): array 202 | { 203 | return $this->requests; 204 | } 205 | 206 | 207 | /** 208 | * Returns current presenter. 209 | */ 210 | final public function getPresenter(): ?IPresenter 211 | { 212 | return $this->presenter; 213 | } 214 | 215 | 216 | /********************* services ****************d*g**/ 217 | 218 | 219 | /** 220 | * Returns router. 221 | */ 222 | public function getRouter(): Router 223 | { 224 | return $this->router; 225 | } 226 | 227 | 228 | /** 229 | * Returns presenter factory. 230 | */ 231 | public function getPresenterFactory(): IPresenterFactory 232 | { 233 | return $this->presenterFactory; 234 | } 235 | } 236 | -------------------------------------------------------------------------------- /src/Application/Attributes/CrossOrigin.php: -------------------------------------------------------------------------------- 1 | methods = $methods === null ? null : (array) $methods; 25 | $this->actions = $actions === null ? null : (array) $actions; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /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/Application/Helpers.php: -------------------------------------------------------------------------------- 1 | $class] + class_parents($class); 41 | $addTraits = function (string $type) use (&$res, &$addTraits): void { 42 | $res += class_uses($type); 43 | foreach (class_uses($type) as $trait) { 44 | $addTraits($trait); 45 | } 46 | }; 47 | foreach ($res as $type) { 48 | $addTraits($type); 49 | } 50 | 51 | return $res; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Application/IPresenter.php: -------------------------------------------------------------------------------- 1 | createRequest($component, $parts['path'] . ($parts['signal'] ? '!' : ''), $args, $mode ?? 'link'); 49 | $relative = $mode === 'link' && !$parts['absolute'] && !$component?->getPresenter()->absoluteUrls; 50 | return $mode === 'forward' || $mode === 'test' 51 | ? null 52 | : $this->requestToUrl($request, $relative) . $parts['fragment']; 53 | } 54 | 55 | 56 | /** 57 | * @param string $destination in format "[[[module:]presenter:]action | signal! | this | @alias]" 58 | * @param string $mode forward|redirect|link 59 | * @throws UI\InvalidLinkException 60 | * @internal 61 | */ 62 | public function createRequest( 63 | ?UI\Component $component, 64 | string $destination, 65 | array $args, 66 | string $mode, 67 | ): Request 68 | { 69 | // note: createRequest supposes that saveState(), run() & tryCall() behaviour is final 70 | 71 | $this->lastRequest = null; 72 | $refPresenter = $component?->getPresenter(); 73 | $path = $destination; 74 | 75 | if (($component && !$component instanceof UI\Presenter) || str_ends_with($destination, '!')) { 76 | [$cname, $signal] = Helpers::splitName(rtrim($destination, '!')); 77 | if ($cname !== '') { 78 | $component = $component->getComponent(strtr($cname, ':', '-')); 79 | } 80 | 81 | if ($signal === '') { 82 | throw new UI\InvalidLinkException('Signal must be non-empty string.'); 83 | } 84 | 85 | $path = 'this'; 86 | } 87 | 88 | if ($path[0] === '@') { 89 | if (!$this->presenterFactory instanceof PresenterFactory) { 90 | throw new \LogicException('Link aliasing requires PresenterFactory service.'); 91 | } 92 | $path = ':' . $this->presenterFactory->getAlias(substr($path, 1)); 93 | } 94 | 95 | $current = false; 96 | [$presenter, $action] = Helpers::splitName($path); 97 | if ($presenter === '') { 98 | if (!$refPresenter) { 99 | throw new \LogicException("Presenter must be specified in '$destination'."); 100 | } 101 | $action = $path === 'this' ? $refPresenter->getAction() : $action; 102 | $presenter = $refPresenter->getName(); 103 | $presenterClass = $refPresenter::class; 104 | 105 | } else { 106 | if ($presenter[0] === ':') { // absolute 107 | $presenter = substr($presenter, 1); 108 | if (!$presenter) { 109 | throw new UI\InvalidLinkException("Missing presenter name in '$destination'."); 110 | } 111 | } elseif ($refPresenter) { // relative 112 | [$module, , $sep] = Helpers::splitName($refPresenter->getName()); 113 | $presenter = $module . $sep . $presenter; 114 | } 115 | 116 | try { 117 | $presenterClass = $this->presenterFactory?->getPresenterClass($presenter); 118 | } catch (InvalidPresenterException $e) { 119 | throw new UI\InvalidLinkException($e->getMessage(), 0, $e); 120 | } 121 | } 122 | 123 | // PROCESS SIGNAL ARGUMENTS 124 | if (isset($signal)) { // $component must be StatePersistent 125 | $reflection = new UI\ComponentReflection($component::class); 126 | if ($signal === 'this') { // means "no signal" 127 | $signal = ''; 128 | if (array_key_exists(0, $args)) { 129 | throw new UI\InvalidLinkException("Unable to pass parameters to 'this!' signal."); 130 | } 131 | } elseif (!str_contains($signal, UI\Component::NameSeparator)) { 132 | // counterpart of signalReceived() & tryCall() 133 | 134 | $method = $reflection->getSignalMethod($signal); 135 | if (!$method) { 136 | throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()"); 137 | } elseif ($this->isDeprecated($refPresenter, $method)) { 138 | trigger_error("Link to deprecated signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class) . " from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED); 139 | } 140 | 141 | // convert indexed parameters to named 142 | UI\ParameterConverter::toParameters($method, $args, [], $missing); 143 | } 144 | 145 | // counterpart of StatePersistent 146 | if ($args && array_intersect_key($args, $reflection->getPersistentParams())) { 147 | $component->saveState($args); 148 | } 149 | 150 | if ($args && $component !== $refPresenter) { 151 | $prefix = $component->getUniqueId() . UI\Component::NameSeparator; 152 | foreach ($args as $key => $val) { 153 | unset($args[$key]); 154 | $args[$prefix . $key] = $val; 155 | } 156 | } 157 | } 158 | 159 | // PROCESS ARGUMENTS 160 | if (is_subclass_of($presenterClass, UI\Presenter::class)) { 161 | if ($action === '') { 162 | $action = UI\Presenter::DefaultAction; 163 | } 164 | 165 | $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class; 166 | 167 | $reflection = new UI\ComponentReflection($presenterClass); 168 | if ($this->isDeprecated($refPresenter, $reflection)) { 169 | trigger_error("Link to deprecated presenter '$presenter' from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED); 170 | } 171 | 172 | // counterpart of run() & tryCall() 173 | if ($method = $reflection->getActionRenderMethod($action)) { 174 | if ($this->isDeprecated($refPresenter, $method)) { 175 | trigger_error("Link to deprecated action '$presenter:$action' from '{$refPresenter->getName()}:{$refPresenter->getAction()}'.", E_USER_DEPRECATED); 176 | } 177 | 178 | UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? $refPresenter->getParameters() : [], $missing); 179 | 180 | } elseif (array_key_exists(0, $args)) { 181 | throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}()."); 182 | } 183 | 184 | // counterpart of StatePersistent 185 | if ($refPresenter) { 186 | if (empty($signal) && $args && array_intersect_key($args, $reflection->getPersistentParams())) { 187 | $refPresenter->saveStatePartial($args, $reflection); 188 | } 189 | 190 | $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass); 191 | if ($current && $args) { 192 | $tmp = $globalState + $refPresenter->getParameters(); 193 | foreach ($args as $key => $val) { 194 | if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { 195 | $current = false; 196 | break; 197 | } 198 | } 199 | } 200 | 201 | $args += $globalState; 202 | } 203 | } 204 | 205 | if ($mode !== 'test' && !empty($missing)) { 206 | foreach ($missing as $rp) { 207 | if (!array_key_exists($rp->getName(), $args)) { 208 | throw new UI\InvalidLinkException("Missing parameter \${$rp->getName()} required by " . Reflection::toString($rp->getDeclaringFunction())); 209 | } 210 | } 211 | } 212 | 213 | // ADD ACTION & SIGNAL & FLASH 214 | if ($action) { 215 | $args[UI\Presenter::ActionKey] = $action; 216 | } 217 | 218 | if (!empty($signal)) { 219 | $args[UI\Presenter::SignalKey] = $component->getParameterId($signal); 220 | $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey); 221 | } 222 | 223 | if (($mode === 'redirect' || $mode === 'forward') && $refPresenter->hasFlashSession()) { 224 | $flashKey = $refPresenter->getParameter(UI\Presenter::FlashKey); 225 | $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null; 226 | } 227 | 228 | return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]); 229 | } 230 | 231 | 232 | /** 233 | * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]" 234 | * @throws UI\InvalidLinkException 235 | * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} 236 | * @internal 237 | */ 238 | public static function parseDestination(string $destination): array 239 | { 240 | if (!preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches)) { 241 | throw new UI\InvalidLinkException("Invalid destination '$destination'."); 242 | } 243 | 244 | if (!empty($matches['query'])) { 245 | trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED); 246 | parse_str(substr($matches['query'], 1), $args); 247 | } 248 | 249 | return [ 250 | 'absolute' => (bool) $matches['absolute'], 251 | 'path' => $matches['path'], 252 | 'signal' => !empty($matches['signal']), 253 | 'args' => $args ?? null, 254 | 'fragment' => $matches['fragment'] ?? '', 255 | ]; 256 | } 257 | 258 | 259 | /** 260 | * Converts Request to URL. 261 | */ 262 | public function requestToUrl(Request $request, ?bool $relative = false): string 263 | { 264 | $url = $this->router->constructUrl($request->toArray(), $this->refUrl); 265 | if ($url === null) { 266 | $params = $request->getParameters(); 267 | unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]); 268 | $params = urldecode(http_build_query($params, '', ', ')); 269 | throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)"); 270 | } 271 | 272 | if ($relative) { 273 | $hostUrl = $this->refUrl->getHostUrl() . '/'; 274 | if (strncmp($url, $hostUrl, strlen($hostUrl)) === 0) { 275 | $url = substr($url, strlen($hostUrl) - 1); 276 | } 277 | } 278 | 279 | return $url; 280 | } 281 | 282 | 283 | public function withReferenceUrl(string $url): static 284 | { 285 | return new self( 286 | $this->router, 287 | new UrlScript($url), 288 | $this->presenterFactory, 289 | ); 290 | } 291 | 292 | 293 | private function isDeprecated(?UI\Presenter $presenter, \ReflectionClass|\ReflectionMethod $reflection): bool 294 | { 295 | return $presenter?->invalidLinkMode 296 | && (UI\ComponentReflection::parseAnnotation($reflection, 'deprecated') || $reflection->getAttributes(Attributes\Deprecated::class)); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /src/Application/MicroPresenter.php: -------------------------------------------------------------------------------- 1 | context; 42 | } 43 | 44 | 45 | public function run(Application\Request $request): Application\Response 46 | { 47 | $this->request = $request; 48 | 49 | if ( 50 | $this->httpRequest 51 | && $this->router 52 | && !$this->httpRequest->isAjax() 53 | && ($request->isMethod('get') || $request->isMethod('head')) 54 | ) { 55 | $refUrl = $this->httpRequest->getUrl(); 56 | $url = $this->router->constructUrl($request->toArray(), $refUrl); 57 | if ($url !== null && !$refUrl->isEqual($url)) { 58 | return new Responses\RedirectResponse($url, Http\IResponse::S301_MovedPermanently); 59 | } 60 | } 61 | 62 | $params = $request->getParameters(); 63 | $callback = $params['callback'] ?? null; 64 | if (!is_object($callback) || !is_callable($callback)) { 65 | throw new Application\BadRequestException('Parameter callback is not a valid closure.'); 66 | } 67 | 68 | $reflection = Nette\Utils\Callback::toReflection($callback); 69 | 70 | if ($this->context) { 71 | foreach ($reflection->getParameters() as $param) { 72 | $type = $param->getType(); 73 | if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) { 74 | $params[$param->getName()] = $this->context->getByType($type->getName(), throw: false); 75 | } 76 | } 77 | } 78 | 79 | $params['presenter'] = $this; 80 | try { 81 | $params = Application\UI\ParameterConverter::toArguments($reflection, $params); 82 | } catch (Nette\InvalidArgumentException $e) { 83 | $this->error($e->getMessage()); 84 | } 85 | 86 | $response = $callback(...array_values($params)); 87 | 88 | if (is_string($response)) { 89 | $response = [$response, []]; 90 | } 91 | 92 | if (is_array($response)) { 93 | [$templateSource, $templateParams] = $response; 94 | $response = $this->createTemplate()->setParameters($templateParams); 95 | if (!$templateSource instanceof \SplFileInfo) { 96 | $response->getLatte()->setLoader(new Latte\Loaders\StringLoader); 97 | } 98 | 99 | $response->setFile((string) $templateSource); 100 | } 101 | 102 | if ($response instanceof Application\UI\Template) { 103 | return new Responses\TextResponse($response); 104 | } else { 105 | return $response ?: new Responses\VoidResponse; 106 | } 107 | } 108 | 109 | 110 | /** 111 | * Template factory. 112 | */ 113 | public function createTemplate(?string $class = null, ?callable $latteFactory = null): Application\UI\Template 114 | { 115 | $latte = $latteFactory 116 | ? $latteFactory() 117 | : $this->getContext()->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); 118 | $template = $class 119 | ? new $class 120 | : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte); 121 | 122 | $template->setParameters($this->request->getParameters()); 123 | $template->presenter = $this; 124 | $template->context = $this->context; 125 | if ($this->httpRequest) { 126 | $url = $this->httpRequest->getUrl(); 127 | $template->baseUrl = rtrim($url->getBaseUrl(), '/'); 128 | $template->basePath = rtrim($url->getBasePath(), '/'); 129 | } 130 | 131 | return $template; 132 | } 133 | 134 | 135 | /** 136 | * Redirects to another URL. 137 | */ 138 | public function redirectUrl(string $url, int $httpCode = Http\IResponse::S302_Found): Responses\RedirectResponse 139 | { 140 | return new Responses\RedirectResponse($url, $httpCode); 141 | } 142 | 143 | 144 | /** 145 | * Throws HTTP error. 146 | * @throws Nette\Application\BadRequestException 147 | */ 148 | public function error(string $message = '', int $httpCode = Http\IResponse::S404_NotFound): void 149 | { 150 | throw new Application\BadRequestException($message, $httpCode); 151 | } 152 | 153 | 154 | public function getRequest(): ?Nette\Application\Request 155 | { 156 | return $this->request; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Application/PresenterFactory.php: -------------------------------------------------------------------------------- 1 | splited mask */ 21 | private array $mapping = [ 22 | '*' => ['App\Presentation\\', '*\\', '**Presenter'], 23 | 'Nette' => ['NetteModule\\', '*\\', '*Presenter'], 24 | ]; 25 | 26 | private array $aliases = []; 27 | private array $cache = []; 28 | 29 | /** @var callable */ 30 | private $factory; 31 | 32 | 33 | /** 34 | * @param ?callable(string): IPresenter $factory 35 | */ 36 | public function __construct(?callable $factory = null) 37 | { 38 | $this->factory = $factory ?: fn(string $class): IPresenter => new $class; 39 | } 40 | 41 | 42 | /** 43 | * Creates new presenter instance. 44 | */ 45 | public function createPresenter(string $name): IPresenter 46 | { 47 | return ($this->factory)($this->getPresenterClass($name)); 48 | } 49 | 50 | 51 | /** 52 | * Generates and checks presenter class name. 53 | * @throws InvalidPresenterException 54 | */ 55 | public function getPresenterClass(string &$name): string 56 | { 57 | if (isset($this->cache[$name])) { 58 | return $this->cache[$name]; 59 | } 60 | 61 | $class = $this->formatPresenterClass($name); 62 | if (!class_exists($class)) { 63 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' was not found."); 64 | } 65 | 66 | $reflection = new \ReflectionClass($class); 67 | $class = $reflection->getName(); 68 | if (!$reflection->implementsInterface(IPresenter::class)) { 69 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is not Nette\\Application\\IPresenter implementor."); 70 | } elseif ($reflection->isAbstract()) { 71 | throw new InvalidPresenterException("Cannot load presenter '$name', class '$class' is abstract."); 72 | } 73 | 74 | return $this->cache[$name] = $class; 75 | } 76 | 77 | 78 | /** 79 | * Sets mapping as pairs [module => mask] 80 | */ 81 | public function setMapping(array $mapping): static 82 | { 83 | foreach ($mapping as $module => $mask) { 84 | if (is_string($mask)) { 85 | if (!preg_match('#^\\\?([\w\\\]*\\\)?(\w*\*\w*?\\\)?([\w\\\]*\*\*?\w*)$#D', $mask, $m)) { 86 | throw new Nette\InvalidStateException("Invalid mapping mask '$mask'."); 87 | } 88 | 89 | $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]]; 90 | } elseif (is_array($mask) && count($mask) === 3) { 91 | $this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]]; 92 | } else { 93 | throw new Nette\InvalidStateException("Invalid mapping mask for module $module."); 94 | } 95 | } 96 | 97 | return $this; 98 | } 99 | 100 | 101 | /** 102 | * Formats presenter class name from its name. 103 | * @internal 104 | */ 105 | public function formatPresenterClass(string $presenter): string 106 | { 107 | if (!Nette\Utils\Strings::match($presenter, '#^[a-zA-Z\x7f-\xff][a-zA-Z0-9\x7f-\xff:]*$#D')) { 108 | throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$presenter' is invalid."); 109 | } 110 | $parts = explode(':', $presenter); 111 | $mapping = isset($parts[1], $this->mapping[$parts[0]]) 112 | ? $this->mapping[array_shift($parts)] 113 | : $this->mapping['*']; 114 | 115 | while ($part = array_shift($parts)) { 116 | $mapping[0] .= strtr($mapping[$parts ? 1 : 2], ['**' => "$part\\$part", '*' => $part]); 117 | } 118 | 119 | return $mapping[0]; 120 | } 121 | 122 | 123 | /** 124 | * Sets pairs [alias => destination] 125 | */ 126 | public function setAliases(array $aliases): static 127 | { 128 | $this->aliases = $aliases; 129 | return $this; 130 | } 131 | 132 | 133 | public function getAlias(string $alias): string 134 | { 135 | return $this->aliases[$alias] ?? throw new Nette\InvalidStateException("Link alias '$alias' was not found."); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/Application/Request.php: -------------------------------------------------------------------------------- 1 | name = $name; 55 | return $this; 56 | } 57 | 58 | 59 | /** 60 | * Retrieve the presenter name. 61 | */ 62 | public function getPresenterName(): string 63 | { 64 | return $this->name; 65 | } 66 | 67 | 68 | /** 69 | * Sets variables provided to the presenter. 70 | */ 71 | public function setParameters(array $params): static 72 | { 73 | $this->params = $params; 74 | return $this; 75 | } 76 | 77 | 78 | /** 79 | * Returns all variables provided to the presenter (usually via URL). 80 | */ 81 | public function getParameters(): array 82 | { 83 | return $this->params; 84 | } 85 | 86 | 87 | /** 88 | * Returns a parameter provided to the presenter. 89 | */ 90 | public function getParameter(string $key): mixed 91 | { 92 | return $this->params[$key] ?? null; 93 | } 94 | 95 | 96 | /** 97 | * Sets variables provided to the presenter via POST. 98 | */ 99 | public function setPost(array $params): static 100 | { 101 | $this->post = $params; 102 | return $this; 103 | } 104 | 105 | 106 | /** 107 | * Returns a variable provided to the presenter via POST. 108 | * If no key is passed, returns the entire array. 109 | */ 110 | public function getPost(?string $key = null): mixed 111 | { 112 | return func_num_args() === 0 113 | ? $this->post 114 | : ($this->post[$key] ?? null); 115 | } 116 | 117 | 118 | /** 119 | * Sets all uploaded files. 120 | */ 121 | public function setFiles(array $files): static 122 | { 123 | $this->files = $files; 124 | return $this; 125 | } 126 | 127 | 128 | /** 129 | * Returns all uploaded files. 130 | */ 131 | public function getFiles(): array 132 | { 133 | return $this->files; 134 | } 135 | 136 | 137 | /** 138 | * Sets the method. 139 | */ 140 | public function setMethod(?string $method): static 141 | { 142 | $this->method = $method; 143 | return $this; 144 | } 145 | 146 | 147 | /** 148 | * Returns the method. 149 | */ 150 | public function getMethod(): ?string 151 | { 152 | return $this->method; 153 | } 154 | 155 | 156 | /** 157 | * Checks if the method is the given one. 158 | */ 159 | public function isMethod(string $method): bool 160 | { 161 | return strcasecmp($this->method, $method) === 0; 162 | } 163 | 164 | 165 | /** 166 | * Sets the flag. 167 | */ 168 | public function setFlag(string $flag, bool $value = true): static 169 | { 170 | $this->flags[$flag] = $value; 171 | return $this; 172 | } 173 | 174 | 175 | /** 176 | * Checks the flag. 177 | */ 178 | public function hasFlag(string $flag): bool 179 | { 180 | return !empty($this->flags[$flag]); 181 | } 182 | 183 | 184 | public function toArray(): array 185 | { 186 | $params = $this->params; 187 | $params['presenter'] = $this->name; 188 | return $params; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Application/Response.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/FileResponse.php: -------------------------------------------------------------------------------- 1 | file = $file; 38 | $this->name = $name ?? basename($file); 39 | $this->contentType = $contentType ?: 'application/octet-stream'; 40 | $this->forceDownload = $forceDownload; 41 | } 42 | 43 | 44 | /** 45 | * Returns the path to a downloaded file. 46 | */ 47 | public function getFile(): string 48 | { 49 | return $this->file; 50 | } 51 | 52 | 53 | /** 54 | * Returns the file name. 55 | */ 56 | public function getName(): string 57 | { 58 | return $this->name; 59 | } 60 | 61 | 62 | /** 63 | * Returns the MIME content type of a downloaded file. 64 | */ 65 | public function getContentType(): string 66 | { 67 | return $this->contentType; 68 | } 69 | 70 | 71 | /** 72 | * Sends response to output. 73 | */ 74 | public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void 75 | { 76 | $httpResponse->setContentType($this->contentType); 77 | $httpResponse->setHeader( 78 | 'Content-Disposition', 79 | ($this->forceDownload ? 'attachment' : 'inline') 80 | . '; filename="' . $this->name . '"' 81 | . '; filename*=utf-8\'\'' . rawurlencode($this->name), 82 | ); 83 | 84 | $filesize = $length = filesize($this->file); 85 | $handle = fopen($this->file, 'r'); 86 | if (!$handle) { 87 | throw new Nette\Application\BadRequestException("Cannot open file: '{$this->file}'."); 88 | } 89 | 90 | if ($this->resuming) { 91 | $httpResponse->setHeader('Accept-Ranges', 'bytes'); 92 | if (preg_match('#^bytes=(\d*)-(\d*)$#D', (string) $httpRequest->getHeader('Range'), $matches)) { 93 | [, $start, $end] = $matches; 94 | if ($start === '') { 95 | $start = max(0, $filesize - $end); 96 | $end = $filesize - 1; 97 | 98 | } elseif ($end === '' || $end > $filesize - 1) { 99 | $end = $filesize - 1; 100 | } 101 | 102 | if ($end < $start) { 103 | $httpResponse->setCode(416); // requested range not satisfiable 104 | return; 105 | } 106 | 107 | $httpResponse->setCode(206); 108 | $httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize); 109 | $length = $end - $start + 1; 110 | fseek($handle, (int) $start); 111 | 112 | } else { 113 | $httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize); 114 | } 115 | } 116 | 117 | $httpResponse->setHeader('Content-Length', (string) $length); 118 | while (!feof($handle) && $length > 0) { 119 | echo $s = fread($handle, min(4_000_000, $length)); 120 | $length -= strlen($s); 121 | } 122 | 123 | fclose($handle); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /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/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/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/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/Responses/VoidResponse.php: -------------------------------------------------------------------------------- 1 | defaults = $defaults; 28 | } 29 | 30 | 31 | /** 32 | * Maps command line arguments to an array. 33 | */ 34 | public function match(Nette\Http\IRequest $httpRequest): ?array 35 | { 36 | if (empty($_SERVER['argv']) || !is_array($_SERVER['argv'])) { 37 | return null; 38 | } 39 | 40 | $names = [self::PresenterKey]; 41 | $params = $this->defaults; 42 | $args = $_SERVER['argv']; 43 | array_shift($args); 44 | $args[] = '--'; 45 | 46 | foreach ($args as $arg) { 47 | $opt = preg_replace('#/|-+#A', '', $arg); 48 | if ($opt === $arg) { 49 | if (isset($flag) || $flag = array_shift($names)) { 50 | $params[$flag] = $arg; 51 | } else { 52 | $params[] = $arg; 53 | } 54 | 55 | $flag = null; 56 | continue; 57 | } 58 | 59 | if (isset($flag)) { 60 | $params[$flag] = true; 61 | $flag = null; 62 | } 63 | 64 | if ($opt === '') { 65 | continue; 66 | } 67 | 68 | $pair = explode('=', $opt, 2); 69 | if (isset($pair[1])) { 70 | $params[$pair[0]] = $pair[1]; 71 | } else { 72 | $flag = $pair[0]; 73 | } 74 | } 75 | 76 | if (!isset($params[self::PresenterKey])) { 77 | throw new Nette\InvalidStateException('Missing presenter & action in route definition.'); 78 | } 79 | 80 | [$module, $presenter] = Nette\Application\Helpers::splitName($params[self::PresenterKey]); 81 | if ($module !== '') { 82 | $params[self::PresenterKey] = $presenter; 83 | $presenter = $module; 84 | } 85 | 86 | $params['presenter'] = $presenter; 87 | 88 | return $params; 89 | } 90 | 91 | 92 | /** 93 | * This router is only unidirectional. 94 | */ 95 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 96 | { 97 | return null; 98 | } 99 | 100 | 101 | /** 102 | * Returns default values. 103 | */ 104 | public function getDefaults(): array 105 | { 106 | return $this->defaults; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Application/Routers/Route.php: -------------------------------------------------------------------------------- 1 | [ 27 | self::Pattern => '[a-z][a-z0-9.-]*', 28 | self::FilterIn => [self::class, 'path2presenter'], 29 | self::FilterOut => [self::class, 'presenter2path'], 30 | ], 31 | 'presenter' => [ 32 | self::Pattern => '[a-z][a-z0-9.-]*', 33 | self::FilterIn => [self::class, 'path2presenter'], 34 | self::FilterOut => [self::class, 'presenter2path'], 35 | ], 36 | 'action' => [ 37 | self::Pattern => '[a-z][a-z0-9-]*', 38 | self::FilterIn => [self::class, 'path2action'], 39 | self::FilterOut => [self::class, 'action2path'], 40 | ], 41 | ]; 42 | 43 | 44 | /** 45 | * @param string $mask e.g. '//' 46 | * @param array|string|\Closure $metadata default values or metadata or callback for NetteModule\MicroPresenter 47 | */ 48 | public function __construct(string $mask, array|string|\Closure $metadata = []) 49 | { 50 | if (is_string($metadata)) { 51 | [$presenter, $action] = Nette\Application\Helpers::splitName($metadata); 52 | if (!$presenter) { 53 | throw new Nette\InvalidArgumentException("Second argument must be array or string in format Presenter:action, '$metadata' given."); 54 | } 55 | 56 | $metadata = [self::PresenterKey => $presenter]; 57 | if ($action !== '') { 58 | $metadata['action'] = $action; 59 | } 60 | } elseif ($metadata instanceof \Closure) { 61 | $metadata = [ 62 | self::PresenterKey => 'Nette:Micro', 63 | 'callback' => $metadata, 64 | ]; 65 | } 66 | 67 | $this->defaultMeta += self::UIMeta; 68 | parent::__construct($mask, $metadata); 69 | } 70 | 71 | 72 | /** 73 | * Maps HTTP request to an array. 74 | */ 75 | public function match(Nette\Http\IRequest $httpRequest): ?array 76 | { 77 | $params = parent::match($httpRequest); 78 | 79 | if ($params === null) { 80 | return null; 81 | } elseif (!isset($params[self::PresenterKey])) { 82 | throw new Nette\InvalidStateException('Missing presenter in route definition.'); 83 | } elseif (!is_string($params[self::PresenterKey])) { 84 | return null; 85 | } 86 | 87 | $presenter = $params[self::PresenterKey] ?? null; 88 | if (isset($this->getMetadata()[self::ModuleKey], $params[self::ModuleKey]) && is_string($presenter)) { 89 | $params[self::PresenterKey] = $params[self::ModuleKey] . ':' . $params[self::PresenterKey]; 90 | } 91 | 92 | unset($params[self::ModuleKey]); 93 | 94 | return $params; 95 | } 96 | 97 | 98 | /** 99 | * Constructs absolute URL from array. 100 | */ 101 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 102 | { 103 | $metadata = $this->getMetadata(); 104 | if (isset($metadata[self::ModuleKey])) { // try split into module and [submodule:]presenter parts 105 | $presenter = $params[self::PresenterKey]; 106 | $module = $metadata[self::ModuleKey]; 107 | $a = isset($module['fixity'], $module[self::Value]) 108 | && strncmp($presenter, $module[self::Value] . ':', strlen($module[self::Value]) + 1) === 0 109 | ? strlen($module[self::Value]) 110 | : strrpos($presenter, ':'); 111 | if ($a === false) { 112 | $params[self::ModuleKey] = isset($module[self::Value]) ? '' : null; 113 | } else { 114 | $params[self::ModuleKey] = substr($presenter, 0, $a); 115 | $params[self::PresenterKey] = substr($presenter, $a + 1); 116 | } 117 | } 118 | 119 | return parent::constructUrl($params, $refUrl); 120 | } 121 | 122 | 123 | /** @internal */ 124 | public function getConstantParameters(): array 125 | { 126 | $res = parent::getConstantParameters(); 127 | if (isset($res[self::ModuleKey], $res[self::PresenterKey])) { 128 | $res[self::PresenterKey] = $res[self::ModuleKey] . ':' . $res[self::PresenterKey]; 129 | } elseif (isset($this->getMetadata()[self::ModuleKey])) { 130 | unset($res[self::PresenterKey]); 131 | } 132 | 133 | unset($res[self::ModuleKey]); 134 | return $res; 135 | } 136 | 137 | 138 | /********************* Inflectors ****************d*g**/ 139 | 140 | 141 | /** 142 | * camelCaseAction name -> dash-separated. 143 | */ 144 | public static function action2path(string $s): string 145 | { 146 | $s = preg_replace('#(.)(?=[A-Z])#', '$1-', $s); 147 | $s = strtolower($s); 148 | $s = rawurlencode($s); 149 | return $s; 150 | } 151 | 152 | 153 | /** 154 | * dash-separated -> camelCaseAction name. 155 | */ 156 | public static function path2action(string $s): string 157 | { 158 | $s = preg_replace('#-(?=[a-z])#', ' ', $s); 159 | $s = lcfirst(ucwords($s)); 160 | $s = str_replace(' ', '', $s); 161 | return $s; 162 | } 163 | 164 | 165 | /** 166 | * PascalCase:Presenter name -> dash-and-dot-separated. 167 | */ 168 | public static function presenter2path(string $s): string 169 | { 170 | $s = strtr($s, ':', '.'); 171 | $s = preg_replace('#([^.])(?=[A-Z])#', '$1-', $s); 172 | $s = strtolower($s); 173 | $s = rawurlencode($s); 174 | return $s; 175 | } 176 | 177 | 178 | /** 179 | * dash-and-dot-separated -> PascalCase:Presenter name. 180 | */ 181 | public static function path2presenter(string $s): string 182 | { 183 | $s = preg_replace('#([.-])(?=[a-z])#', '$1 ', $s); 184 | $s = ucwords($s); 185 | $s = str_replace('. ', ':', $s); 186 | $s = str_replace('- ', '', $s); 187 | return $s; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Application/Routers/RouteList.php: -------------------------------------------------------------------------------- 1 | module = $module ? $module . ':' : null; 30 | } 31 | 32 | 33 | /** 34 | * Support for modules. 35 | */ 36 | protected function completeParameters(array $params): ?array 37 | { 38 | $presenter = $params[self::PresenterKey] ?? null; 39 | if (is_string($presenter) && strncmp($presenter, 'Nette:', 6)) { 40 | $params[self::PresenterKey] = $this->module . $presenter; 41 | } 42 | 43 | return $params; 44 | } 45 | 46 | 47 | /** 48 | * Constructs absolute URL from array. 49 | */ 50 | public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string 51 | { 52 | if ($this->module) { 53 | if (strncmp($params[self::PresenterKey], $this->module, strlen($this->module)) !== 0) { 54 | return null; 55 | } 56 | 57 | $params[self::PresenterKey] = substr($params[self::PresenterKey], strlen($this->module)); 58 | } 59 | 60 | return parent::constructUrl($params, $refUrl); 61 | } 62 | 63 | 64 | public function addRoute( 65 | #[Language('TEXT')] 66 | string $mask, 67 | array|string|\Closure $metadata = [], 68 | int|bool $oneWay = 0, 69 | ): static 70 | { 71 | $this->add(new Route($mask, $metadata), (int) $oneWay); 72 | return $this; 73 | } 74 | 75 | 76 | public function withModule(string $module): static 77 | { 78 | $router = new static; 79 | $router->module = $module . ':'; 80 | $router->parent = $this; 81 | $this->add($router); 82 | return $router; 83 | } 84 | 85 | 86 | public function getModule(): ?string 87 | { 88 | return $this->module; 89 | } 90 | 91 | 92 | /** 93 | * @param mixed $index 94 | * @param Nette\Routing\Router $router 95 | */ 96 | public function offsetSet($index, $router): void 97 | { 98 | if ($router instanceof Route) { 99 | trigger_error('Usage `$router[] = new Route(...)` is deprecated, use `$router->addRoute(...)`.', E_USER_DEPRECATED); 100 | } else { 101 | $class = getclass($router); 102 | trigger_error("Usage `\$router[] = new $class` is deprecated, use `\$router->add(new $class)`.", E_USER_DEPRECATED); 103 | } 104 | 105 | if ($index === null) { 106 | $this->add($router); 107 | } else { 108 | $this->modify($index, $router); 109 | } 110 | } 111 | 112 | 113 | /** 114 | * @param int $index 115 | * @throws Nette\OutOfRangeException 116 | */ 117 | public function offsetGet($index): Nette\Routing\Router 118 | { 119 | trigger_error('Usage `$route = $router[...]` is deprecated, use `$router->getRouters()`.', E_USER_DEPRECATED); 120 | if (!$this->offsetExists($index)) { 121 | throw new Nette\OutOfRangeException('Offset invalid or out of range'); 122 | } 123 | 124 | return $this->getRouters()[$index]; 125 | } 126 | 127 | 128 | /** 129 | * @param int $index 130 | */ 131 | public function offsetExists($index): bool 132 | { 133 | trigger_error('Usage `isset($router[...])` is deprecated.', E_USER_DEPRECATED); 134 | return is_int($index) && $index >= 0 && $index < count($this->getRouters()); 135 | } 136 | 137 | 138 | /** 139 | * @param int $index 140 | * @throws Nette\OutOfRangeException 141 | */ 142 | public function offsetUnset($index): void 143 | { 144 | trigger_error('Usage `unset($router[$index])` is deprecated, use `$router->modify($index, null)`.', E_USER_DEPRECATED); 145 | if (!$this->offsetExists($index)) { 146 | throw new Nette\OutOfRangeException('Offset invalid or out of range'); 147 | } 148 | 149 | $this->modify($index, null); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/Application/Routers/SimpleRouter.php: -------------------------------------------------------------------------------- 1 | $presenter, 34 | 'action' => $action === '' ? Application\UI\Presenter::DefaultAction : $action, 35 | ]; 36 | } 37 | 38 | parent::__construct($defaults); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Application/UI/AccessPolicy.php: -------------------------------------------------------------------------------- 1 | getAttributes(); 36 | $attrs = self::applyInternalRules($attrs); 37 | foreach ($attrs as $attribute) { 38 | $this->checkAttribute($attribute); 39 | } 40 | } 41 | 42 | 43 | private function getAttributes(): array 44 | { 45 | return array_map( 46 | fn($ra) => $ra->newInstance(), 47 | $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), 48 | ); 49 | } 50 | 51 | 52 | private function applyInternalRules(array $attrs): array 53 | { 54 | if ( 55 | $this->element instanceof \ReflectionMethod 56 | && str_starts_with($this->element->getName(), $this->component::formatSignalMethod('')) 57 | && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin') 58 | && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) 59 | ) { 60 | $attrs[] = new Attributes\Requires(sameOrigin: true); 61 | } 62 | return $attrs; 63 | } 64 | 65 | 66 | private function checkAttribute(Attributes\Requires $attribute): void 67 | { 68 | $this->presenter ??= $this->component->getPresenterIfExists() ?? 69 | throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); 70 | 71 | if ($attribute->methods !== null) { 72 | $this->checkHttpMethod($attribute); 73 | } 74 | 75 | if ($attribute->actions !== null) { 76 | $this->checkActions($attribute); 77 | } 78 | 79 | if ($attribute->forward && !$this->presenter->isForwarded()) { 80 | $this->presenter->error('Forwarded request is required by ' . Reflection::toString($this->element)); 81 | } 82 | 83 | if ($attribute->sameOrigin && !$this->presenter->getHttpRequest()->isSameSite()) { 84 | $this->presenter->detectedCsrf(); 85 | } 86 | 87 | if ($attribute->ajax && !$this->presenter->getHttpRequest()->isAjax()) { 88 | $this->presenter->error('AJAX request is required by ' . Reflection::toString($this->element), Nette\Http\IResponse::S403_Forbidden); 89 | } 90 | } 91 | 92 | 93 | private function checkActions(Attributes\Requires $attribute): void 94 | { 95 | if ( 96 | $this->element instanceof \ReflectionMethod 97 | && !$this->element->getDeclaringClass()->isSubclassOf(Presenter::class) 98 | ) { 99 | throw new \LogicException('Requires(actions) used by ' . Reflection::toString($this->element) . ' is allowed only in presenter.'); 100 | } 101 | 102 | if (!in_array($this->presenter->getAction(), $attribute->actions, strict: true)) { 103 | $this->presenter->error("Action '{$this->presenter->getAction()}' is not allowed by " . Reflection::toString($this->element)); 104 | } 105 | } 106 | 107 | 108 | private function checkHttpMethod(Attributes\Requires $attribute): void 109 | { 110 | if ($this->element instanceof \ReflectionClass) { 111 | $this->presenter->allowedMethods = []; // bypass Presenter::checkHttpMethod() 112 | } 113 | 114 | $allowed = array_map(strtoupper(...), $attribute->methods); 115 | $method = $this->presenter->getHttpRequest()->getMethod(); 116 | 117 | if ($allowed !== ['*'] && !in_array($method, $allowed, strict: true)) { 118 | $this->presenter->getHttpResponse()->setHeader('Allow', implode(',', $allowed)); 119 | $this->presenter->error( 120 | "Method $method is not allowed by " . Reflection::toString($this->element), 121 | Nette\Http\IResponse::S405_MethodNotAllowed, 122 | ); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/Application/UI/BadSignalException.php: -------------------------------------------------------------------------------- 1 | Occurs when component is attached to presenter */ 30 | public array $onAnchor = []; 31 | protected array $params = []; 32 | 33 | 34 | /** 35 | * Returns the presenter where this component belongs to. 36 | */ 37 | public function getPresenter(): ?Presenter 38 | { 39 | if (func_num_args()) { 40 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); 41 | $throw = func_get_arg(0); 42 | } 43 | 44 | return $this->lookup(Presenter::class, throw: $throw ?? true); 45 | } 46 | 47 | 48 | /** 49 | * Returns the presenter where this component belongs to. 50 | */ 51 | public function getPresenterIfExists(): ?Presenter 52 | { 53 | return $this->lookup(Presenter::class, throw: false); 54 | } 55 | 56 | 57 | /** @deprecated */ 58 | public function hasPresenter(): bool 59 | { 60 | return (bool) $this->lookup(Presenter::class, throw: false); 61 | } 62 | 63 | 64 | /** 65 | * Returns a fully-qualified name that uniquely identifies the component 66 | * within the presenter hierarchy. 67 | */ 68 | public function getUniqueId(): string 69 | { 70 | return $this->lookupPath(Presenter::class); 71 | } 72 | 73 | 74 | public function addComponent( 75 | Nette\ComponentModel\IComponent $component, 76 | ?string $name, 77 | ?string $insertBefore = null, 78 | ): static 79 | { 80 | if (!$component instanceof SignalReceiver && !$component instanceof StatePersistent) { 81 | throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the Presenter.'); 82 | } 83 | 84 | return parent::addComponent($component, $name, $insertBefore = null); 85 | } 86 | 87 | 88 | protected function createComponent(string $name): ?Nette\ComponentModel\IComponent 89 | { 90 | if (method_exists($this, $method = 'createComponent' . $name)) { 91 | (new AccessPolicy($this, $rm = new \ReflectionMethod($this, $method)))->checkAccess(); 92 | $this->checkRequirements($rm); 93 | } 94 | return parent::createComponent($name); 95 | } 96 | 97 | 98 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void 99 | { 100 | parent::validateParent($parent); 101 | $this->monitor(Presenter::class, function (Presenter $presenter): void { 102 | $this->loadState($presenter->popGlobalParameters($this->getUniqueId())); 103 | Nette\Utils\Arrays::invoke($this->onAnchor, $this); 104 | }); 105 | } 106 | 107 | 108 | /** 109 | * Calls public method if exists. 110 | */ 111 | protected function tryCall(string $method, array $params): bool 112 | { 113 | $rc = $this->getReflection(); 114 | if (!$rc->hasMethod($method)) { 115 | return false; 116 | } elseif (!$rc->hasCallableMethod($method)) { 117 | $this->error('Method ' . Nette\Utils\Reflection::toString($rc->getMethod($method)) . ' is not callable.'); 118 | } 119 | 120 | $rm = $rc->getMethod($method); 121 | (new AccessPolicy($this, $rm))->checkAccess(); 122 | $this->checkRequirements($rm); 123 | try { 124 | $args = ParameterConverter::toArguments($rm, $params); 125 | } catch (Nette\InvalidArgumentException $e) { 126 | $this->error($e->getMessage()); 127 | } 128 | 129 | $rm->invokeArgs($this, $args); 130 | return true; 131 | } 132 | 133 | 134 | /** 135 | * Descendant can override this method to check for permissions. 136 | * It is called with the presenter class and the render*(), action*(), and handle*() methods. 137 | */ 138 | public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void 139 | { 140 | } 141 | 142 | 143 | /** 144 | * Access to reflection. 145 | */ 146 | public static function getReflection(): ComponentReflection 147 | { 148 | return new ComponentReflection(static::class); 149 | } 150 | 151 | 152 | /********************* interface StatePersistent ****************d*g**/ 153 | 154 | 155 | /** 156 | * Loads state information. 157 | */ 158 | public function loadState(array $params): void 159 | { 160 | $reflection = $this->getReflection(); 161 | foreach ($reflection->getParameters() as $name => $meta) { 162 | if (isset($params[$name])) { // nulls are ignored 163 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) { 164 | $this->error(sprintf( 165 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.", 166 | $name, 167 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", 168 | $meta['type'], 169 | get_debug_type($params[$name]), 170 | )); 171 | } 172 | 173 | $this->$name = &$params[$name]; 174 | } else { 175 | $params[$name] = &$this->$name; 176 | } 177 | } 178 | 179 | $this->params = $params; 180 | } 181 | 182 | 183 | /** 184 | * Saves state information for next request. 185 | */ 186 | public function saveState(array &$params): void 187 | { 188 | $this->saveStatePartial($params, static::getReflection()); 189 | } 190 | 191 | 192 | /** 193 | * @internal used by presenter 194 | */ 195 | public function saveStatePartial(array &$params, ComponentReflection $reflection): void 196 | { 197 | $tree = Nette\Application\Helpers::getClassesAndTraits(static::class); 198 | 199 | foreach ($reflection->getPersistentParams() as $name => $meta) { 200 | if (isset($params[$name])) { 201 | // injected value 202 | 203 | } elseif ( 204 | array_key_exists($name, $params) // nulls are skipped 205 | || (isset($meta['since']) && !isset($tree[$meta['since']])) // not related 206 | || !isset($this->$name) 207 | ) { 208 | continue; 209 | 210 | } else { 211 | $params[$name] = $this->$name; // object property value 212 | } 213 | 214 | if (!ParameterConverter::convertType($params[$name], $meta['type'])) { 215 | throw new InvalidLinkException(sprintf( 216 | "Value passed to persistent parameter '%s' in %s must be %s, %s given.", 217 | $name, 218 | $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", 219 | $meta['type'], 220 | get_debug_type($params[$name]), 221 | )); 222 | } 223 | 224 | if ($params[$name] === $meta['def'] || ($meta['def'] === null && $params[$name] === '')) { 225 | $params[$name] = null; // value transmit is unnecessary 226 | } 227 | } 228 | } 229 | 230 | 231 | /** 232 | * Returns component param. 233 | */ 234 | final public function getParameter(string $name): mixed 235 | { 236 | if (func_num_args() > 1) { 237 | trigger_error(__METHOD__ . '() parameter $default is deprecated, use operator ??', E_USER_DEPRECATED); 238 | $default = func_get_arg(1); 239 | } 240 | return $this->params[$name] ?? $default ?? null; 241 | } 242 | 243 | 244 | /** 245 | * Returns component parameters. 246 | */ 247 | final public function getParameters(): array 248 | { 249 | return array_map(fn($item) => $item, $this->params); 250 | } 251 | 252 | 253 | /** 254 | * Returns a fully-qualified name that uniquely identifies the parameter. 255 | */ 256 | final public function getParameterId(string $name): string 257 | { 258 | $uid = $this->getUniqueId(); 259 | return $uid === '' ? $name : $uid . self::NameSeparator . $name; 260 | } 261 | 262 | 263 | /********************* interface SignalReceiver ****************d*g**/ 264 | 265 | 266 | /** 267 | * Calls signal handler method. 268 | * @throws BadSignalException if there is not handler method 269 | */ 270 | public function signalReceived(string $signal): void 271 | { 272 | if (!$this->tryCall($this->formatSignalMethod($signal), $this->params)) { 273 | $class = static::class; 274 | throw new BadSignalException("There is no handler for signal '$signal' in class $class."); 275 | } 276 | } 277 | 278 | 279 | /** 280 | * Formats signal handler method name -> case sensitivity doesn't matter. 281 | */ 282 | public static function formatSignalMethod(string $signal): string 283 | { 284 | return 'handle' . $signal; 285 | } 286 | 287 | 288 | /********************* navigation ****************d*g**/ 289 | 290 | 291 | /** 292 | * Generates URL to presenter, action or signal. 293 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 294 | * @param mixed ...$args 295 | * @throws InvalidLinkException 296 | */ 297 | public function link(string $destination, ...$args): string 298 | { 299 | try { 300 | $args = count($args) === 1 && is_array($args[0] ?? null) 301 | ? $args[0] 302 | : $args; 303 | return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link'); 304 | 305 | } catch (InvalidLinkException $e) { 306 | return $this->getPresenter()->processInvalidLink($e); 307 | } 308 | } 309 | 310 | 311 | /** 312 | * Returns destination as Link object. 313 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 314 | * @param mixed ...$args 315 | */ 316 | public function lazyLink(string $destination, ...$args): Link 317 | { 318 | $args = count($args) === 1 && is_array($args[0] ?? null) 319 | ? $args[0] 320 | : $args; 321 | return new Link($this, $destination, $args); 322 | } 323 | 324 | 325 | /** 326 | * Determines whether it links to the current page. 327 | * @param ?string $destination in format "[[[module:]presenter:]action | signal! | this]" 328 | * @param mixed ...$args 329 | * @throws InvalidLinkException 330 | */ 331 | public function isLinkCurrent(?string $destination = null, ...$args): bool 332 | { 333 | if ($destination !== null) { 334 | $args = count($args) === 1 && is_array($args[0] ?? null) 335 | ? $args[0] 336 | : $args; 337 | $this->getPresenter()->getLinkGenerator()->createRequest($this, $destination, $args, 'test'); 338 | } 339 | 340 | return $this->getPresenter()->getLastCreatedRequestFlag('current'); 341 | } 342 | 343 | 344 | /** 345 | * Redirect to another presenter, action or signal. 346 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 347 | * @param mixed ...$args 348 | * @throws Nette\Application\AbortException 349 | */ 350 | public function redirect(string $destination, ...$args): never 351 | { 352 | $args = count($args) === 1 && is_array($args[0] ?? null) 353 | ? $args[0] 354 | : $args; 355 | $presenter = $this->getPresenter(); 356 | $presenter->saveGlobalState(); 357 | $presenter->redirectUrl($presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect')); 358 | } 359 | 360 | 361 | /** 362 | * Permanently redirects to presenter, action or signal. 363 | * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" 364 | * @param mixed ...$args 365 | * @throws Nette\Application\AbortException 366 | */ 367 | public function redirectPermanent(string $destination, ...$args): never 368 | { 369 | $args = count($args) === 1 && is_array($args[0] ?? null) 370 | ? $args[0] 371 | : $args; 372 | $presenter = $this->getPresenter(); 373 | $presenter->redirectUrl( 374 | $presenter->getLinkGenerator()->link($destination, $args, $this, 'redirect'), 375 | Nette\Http\IResponse::S301_MovedPermanently, 376 | ); 377 | } 378 | 379 | 380 | /** 381 | * Throws HTTP error. 382 | * @throws Nette\Application\BadRequestException 383 | */ 384 | public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): void 385 | { 386 | throw new Nette\Application\BadRequestException($message, $httpCode); 387 | } 388 | } 389 | -------------------------------------------------------------------------------- /src/Application/UI/ComponentReflection.php: -------------------------------------------------------------------------------- 1 | 32 | */ 33 | public function getParameters(): array 34 | { 35 | $params = &self::$ppCache[$this->getName()]; 36 | if ($params !== null) { 37 | return $params; 38 | } 39 | 40 | $params = []; 41 | $isPresenter = $this->isSubclassOf(Presenter::class); 42 | foreach ($this->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { 43 | if ($prop->isStatic()) { 44 | continue; 45 | } elseif ( 46 | self::parseAnnotation($prop, 'persistent') 47 | || $prop->getAttributes(Attributes\Persistent::class) 48 | ) { 49 | $params[$prop->getName()] = [ 50 | 'def' => $prop->getDefaultValue(), 51 | 'type' => ParameterConverter::getType($prop), 52 | 'since' => $isPresenter ? Reflection::getPropertyDeclaringClass($prop)->getName() : null, 53 | ]; 54 | } elseif ($prop->getAttributes(Attributes\Parameter::class)) { 55 | $params[$prop->getName()] = [ 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 annotation @persistent or returned by Presenter::getPersistentComponents(). 89 | * @return array 90 | */ 91 | public function getPersistentComponents(): array 92 | { 93 | $class = $this->getName(); 94 | $components = &self::$pcCache[$class]; 95 | if ($components !== null) { 96 | return $components; 97 | } 98 | 99 | $attrs = $this->getAttributes(Attributes\Persistent::class); 100 | $names = $attrs 101 | ? $attrs[0]->getArguments() 102 | : (array) self::parseAnnotation($this, 'persistent'); 103 | $names = array_merge($names, $class::getPersistentComponents()); 104 | $components = array_fill_keys($names, ['since' => $class]); 105 | 106 | if ($this->isSubclassOf(Presenter::class)) { 107 | $parent = new self($this->getParentClass()->getName()); 108 | $components = $parent->getPersistentComponents() + $components; 109 | } 110 | 111 | return $components; 112 | } 113 | 114 | 115 | /** 116 | * Is a method callable? It means class is instantiable and method has 117 | * public visibility, is non-static and non-abstract. 118 | */ 119 | public function hasCallableMethod(string $method): bool 120 | { 121 | return $this->isInstantiable() 122 | && $this->hasMethod($method) 123 | && ($rm = $this->getMethod($method)) 124 | && $rm->isPublic() && !$rm->isAbstract() && !$rm->isStatic(); 125 | } 126 | 127 | 128 | /** Returns action*() or render*() method if available */ 129 | public function getActionRenderMethod(string $action): ?\ReflectionMethod 130 | { 131 | $class = $this->name; 132 | return self::$armCache[$class][$action] ??= 133 | $this->hasCallableMethod($name = $class::formatActionMethod($action)) 134 | || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) 135 | ? parent::getMethod($name) 136 | : null; 137 | } 138 | 139 | 140 | /** Returns handle*() method if available */ 141 | public function getSignalMethod(string $signal): ?\ReflectionMethod 142 | { 143 | $class = $this->name; 144 | return $this->hasCallableMethod($name = $class::formatSignalMethod($signal)) 145 | ? parent::getMethod($name) 146 | : null; 147 | } 148 | 149 | 150 | /** 151 | * Returns an annotation value. 152 | * @deprecated 153 | */ 154 | public static function parseAnnotation(\Reflector $ref, string $name): ?array 155 | { 156 | if (!preg_match_all('#[\s*]@' . preg_quote($name, '#') . '(?:\(\s*([^)]*)\s*\)|\s|$)#', (string) $ref->getDocComment(), $m)) { 157 | return null; 158 | } 159 | 160 | $tokens = ['true' => true, 'false' => false, 'null' => null]; 161 | $res = []; 162 | foreach ($m[1] as $s) { 163 | foreach (preg_split('#\s*,\s*#', $s, -1, PREG_SPLIT_NO_EMPTY) ?: ['true'] as $item) { 164 | $res[] = array_key_exists($tmp = strtolower($item), $tokens) 165 | ? $tokens[$tmp] 166 | : $item; 167 | } 168 | } 169 | 170 | $alt = match ($name) { 171 | 'persistent' => '#[Nette\Application\Attributes\Persistent]', 172 | 'deprecated' => '#[Nette\Application\Attributes\Deprecated]', 173 | 'crossOrigin' => '#[Nette\Application\Attributes\Request(sameOrigin: false)]', 174 | default => 'alternative' 175 | }; 176 | trigger_error("Annotation @$name is deprecated, use $alt (used in " . Nette\Utils\Reflection::toString($ref) . ')', E_USER_DEPRECATED); 177 | return $res; 178 | } 179 | 180 | 181 | /** 182 | * Has class specified annotation? 183 | * @deprecated 184 | */ 185 | public function hasAnnotation(string $name): bool 186 | { 187 | return (bool) self::parseAnnotation($this, $name); 188 | } 189 | 190 | 191 | /** 192 | * Returns an annotation value. 193 | * @deprecated 194 | */ 195 | public function getAnnotation(string $name): mixed 196 | { 197 | $res = self::parseAnnotation($this, $name); 198 | return $res ? end($res) : null; 199 | } 200 | 201 | 202 | public function getMethod($name): MethodReflection 203 | { 204 | return new MethodReflection($this->getName(), $name); 205 | } 206 | 207 | 208 | /** 209 | * @return MethodReflection[] 210 | */ 211 | public function getMethods($filter = -1): array 212 | { 213 | foreach ($res = parent::getMethods($filter) as $key => $val) { 214 | $res[$key] = new MethodReflection($this->getName(), $val->getName()); 215 | } 216 | 217 | return $res; 218 | } 219 | 220 | 221 | /** @deprecated */ 222 | public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array 223 | { 224 | return ParameterConverter::toArguments($method, $args); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/Application/UI/Control.php: -------------------------------------------------------------------------------- 1 | templateFactory = $templateFactory; 34 | return $this; 35 | } 36 | 37 | 38 | final public function getTemplate(): Template 39 | { 40 | if (!isset($this->template)) { 41 | $this->template = $this->createTemplate(); 42 | } 43 | 44 | return $this->template; 45 | } 46 | 47 | 48 | protected function createTemplate(?string $class = null): Template 49 | { 50 | $class ??= $this->formatTemplateClass(); 51 | $templateFactory = $this->templateFactory ?? $this->getPresenter()->getTemplateFactory(); 52 | return $templateFactory->createTemplate($this, $class); 53 | } 54 | 55 | 56 | public function formatTemplateClass(): ?string 57 | { 58 | return $this->checkTemplateClass(preg_replace('#Control$#', '', static::class) . 'Template'); 59 | } 60 | 61 | 62 | /** @internal */ 63 | protected function checkTemplateClass(string $class): ?string 64 | { 65 | if (!class_exists($class)) { 66 | return null; 67 | } elseif (!is_a($class, Template::class, allow_string: true)) { 68 | trigger_error(sprintf( 69 | '%s: class %s was found but does not implement the %s, so it will not be used for the template.', 70 | static::class, 71 | $class, 72 | Template::class, 73 | )); 74 | return null; 75 | } else { 76 | return $class; 77 | } 78 | } 79 | 80 | 81 | /** 82 | * Descendant can override this method to customize template compile-time filters. 83 | * @deprecated 84 | */ 85 | public function templatePrepareFilters(Template $template): void 86 | { 87 | } 88 | 89 | 90 | /** 91 | * Saves the message to template, that can be displayed after redirect. 92 | */ 93 | public function flashMessage(string|\stdClass|\Stringable $message, string $type = 'info'): \stdClass 94 | { 95 | $id = $this->getParameterId('flash'); 96 | $flash = $message instanceof \stdClass ? $message : (object) [ 97 | 'message' => $message, 98 | 'type' => $type, 99 | ]; 100 | $messages = $this->getPresenter()->getFlashSession()->get($id); 101 | $messages[] = $flash; 102 | $this->getTemplate()->flashes = $messages; 103 | $this->getPresenter()->getFlashSession()->set($id, $messages); 104 | return $flash; 105 | } 106 | 107 | 108 | /********************* rendering ****************d*g**/ 109 | 110 | 111 | /** 112 | * Forces control or its snippet to repaint. 113 | */ 114 | public function redrawControl(?string $snippet = null, bool $redraw = true): void 115 | { 116 | if ($redraw) { 117 | $this->invalidSnippets[$snippet ?? "\0"] = true; 118 | 119 | } elseif ($snippet === null) { 120 | $this->invalidSnippets = []; 121 | 122 | } else { 123 | $this->invalidSnippets[$snippet] = false; 124 | } 125 | } 126 | 127 | 128 | /** 129 | * Is required to repaint the control or its snippet? 130 | */ 131 | public function isControlInvalid(?string $snippet = null): bool 132 | { 133 | if ($snippet !== null) { 134 | return $this->invalidSnippets[$snippet] ?? isset($this->invalidSnippets["\0"]); 135 | 136 | } elseif (count($this->invalidSnippets) > 0) { 137 | return true; 138 | } 139 | 140 | $queue = [$this]; 141 | do { 142 | foreach (array_shift($queue)->getComponents() as $component) { 143 | if ($component instanceof Renderable) { 144 | if ($component->isControlInvalid()) { 145 | // $this->invalidSnippets['__child'] = true; // as cache 146 | return true; 147 | } 148 | } elseif ($component instanceof Nette\ComponentModel\IContainer) { 149 | $queue[] = $component; 150 | } 151 | } 152 | } while ($queue); 153 | 154 | return false; 155 | } 156 | 157 | 158 | /** 159 | * Returns snippet HTML ID. 160 | */ 161 | public function getSnippetId(string $name): string 162 | { 163 | // HTML 4 ID & NAME: [A-Za-z][A-Za-z0-9:_.-]* 164 | return 'snippet-' . $this->getUniqueId() . '-' . $name; 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Application/UI/Form.php: -------------------------------------------------------------------------------- 1 | Occurs when form is attached to presenter */ 21 | public array $onAnchor = []; 22 | 23 | 24 | /** 25 | * Application form constructor. 26 | */ 27 | public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) 28 | { 29 | parent::__construct(); 30 | $parent?->addComponent($this, $name); 31 | } 32 | 33 | 34 | protected function validateParent(Nette\ComponentModel\IContainer $parent): void 35 | { 36 | parent::validateParent($parent); 37 | 38 | $this->monitor(Presenter::class, function (Presenter $presenter): void { 39 | if (!isset($this->getElementPrototype()->id)) { 40 | $this->getElementPrototype()->id = 'frm-' . $this->lookupPath(Presenter::class); 41 | } 42 | 43 | if (!$this->getAction()) { 44 | $this->setAction(new Link($presenter, 'this')); 45 | } 46 | 47 | $controls = $this->getControls(); 48 | if (iterator_count($controls) && $this->isSubmitted()) { 49 | foreach ($controls as $control) { 50 | if (!$control->isDisabled()) { 51 | $control->loadHttpData(); 52 | } 53 | } 54 | } 55 | 56 | Nette\Utils\Arrays::invoke($this->onAnchor, $this); 57 | }); 58 | } 59 | 60 | 61 | /** 62 | * Returns the presenter where this component belongs to. 63 | */ 64 | final public function getPresenter(): ?Presenter 65 | { 66 | if (func_num_args()) { 67 | trigger_error(__METHOD__ . '() parameter $throw is deprecated, use getPresenterIfExists()', E_USER_DEPRECATED); 68 | $throw = func_get_arg(0); 69 | } 70 | 71 | return $this->lookup(Presenter::class, throw: $throw ?? true); 72 | } 73 | 74 | 75 | /** 76 | * Returns the presenter where this component belongs to. 77 | */ 78 | final public function getPresenterIfExists(): ?Presenter 79 | { 80 | return $this->lookup(Presenter::class, throw: false); 81 | } 82 | 83 | 84 | /** @deprecated */ 85 | public function hasPresenter(): bool 86 | { 87 | return (bool) $this->lookup(Presenter::class, throw: false); 88 | } 89 | 90 | 91 | /** 92 | * Tells if the form is anchored. 93 | */ 94 | public function isAnchored(): bool 95 | { 96 | return $this->hasPresenter(); 97 | } 98 | 99 | 100 | /** @deprecated use allowCrossOrigin() */ 101 | public function disableSameSiteProtection(): void 102 | { 103 | $this->allowCrossOrigin(); 104 | } 105 | 106 | 107 | /** 108 | * Internal: returns submitted HTTP data or null when form was not submitted. 109 | */ 110 | protected function receiveHttpData(): ?array 111 | { 112 | $presenter = $this->getPresenter(); 113 | if (!$presenter->isSignalReceiver($this, 'submit')) { 114 | return null; 115 | } 116 | 117 | $request = $presenter->getRequest(); 118 | if ($request->isMethod('forward') || $request->isMethod('post') !== $this->isMethod('post')) { 119 | return null; 120 | } 121 | 122 | return $this->isMethod('post') 123 | ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) 124 | : $request->getParameters(); 125 | } 126 | 127 | 128 | protected function beforeRender(): void 129 | { 130 | parent::beforeRender(); 131 | $key = ($this->isMethod('post') ? '_' : '') . Presenter::SignalKey; 132 | if (!isset($this[$key]) && $this->getAction() !== '') { 133 | $do = $this->lookupPath(Presenter::class) . self::NameSeparator . 'submit'; 134 | $this[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); 135 | } 136 | } 137 | 138 | 139 | /********************* interface SignalReceiver ****************d*g**/ 140 | 141 | 142 | /** 143 | * This method is called by presenter. 144 | */ 145 | public function signalReceived(string $signal): void 146 | { 147 | if ($signal !== 'submit') { 148 | $class = static::class; 149 | throw new BadSignalException("Missing handler for signal '$signal' in $class."); 150 | 151 | } elseif (!$this->crossOrigin && !$this->getPresenter()->getHttpRequest()->isSameSite()) { 152 | $this->getPresenter()->detectedCsrf(); 153 | 154 | } elseif (!$this->getPresenter()->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) { 155 | $this->fireEvents(); 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Application/UI/InvalidLinkException.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 | -------------------------------------------------------------------------------- /src/Application/UI/MethodReflection.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/Application/UI/ParameterConverter.php: -------------------------------------------------------------------------------- 1 | getParameters() as $i => $param) { 28 | $name = $param->getName(); 29 | $type = self::getType($param); 30 | if (isset($args[$name])) { 31 | $res[$i] = $args[$name]; 32 | if (!self::convertType($res[$i], $type)) { 33 | throw new Nette\InvalidArgumentException(sprintf( 34 | 'Argument $%s passed to %s must be %s, %s given.', 35 | $name, 36 | Reflection::toString($method), 37 | $type, 38 | get_debug_type($args[$name]), 39 | )); 40 | } 41 | } elseif ($param->isDefaultValueAvailable()) { 42 | $res[$i] = $param->getDefaultValue(); 43 | } elseif ($type === 'scalar' || $param->allowsNull()) { 44 | $res[$i] = null; 45 | } elseif ($type === 'array' || $type === 'iterable') { 46 | $res[$i] = []; 47 | } else { 48 | throw new Nette\InvalidArgumentException(sprintf( 49 | 'Missing parameter $%s required by %s', 50 | $name, 51 | Reflection::toString($method), 52 | )); 53 | } 54 | } 55 | 56 | return $res; 57 | } 58 | 59 | 60 | /** 61 | * Converts list of arguments to named parameters & check types. 62 | * @param \ReflectionParameter[] $missing arguments 63 | * @throws InvalidLinkException 64 | * @internal 65 | */ 66 | public static function toParameters( 67 | \ReflectionMethod $method, 68 | array &$args, 69 | array $supplemental = [], 70 | ?array &$missing = null, 71 | ): void 72 | { 73 | $i = 0; 74 | foreach ($method->getParameters() as $param) { 75 | $type = self::getType($param); 76 | $name = $param->getName(); 77 | 78 | if (array_key_exists($i, $args)) { 79 | $args[$name] = $args[$i]; 80 | unset($args[$i]); 81 | $i++; 82 | 83 | } elseif (array_key_exists($name, $args)) { 84 | // continue with process 85 | 86 | } elseif (array_key_exists($name, $supplemental)) { 87 | $args[$name] = $supplemental[$name]; 88 | } 89 | 90 | if (!isset($args[$name])) { 91 | if ( 92 | !$param->isDefaultValueAvailable() 93 | && !$param->allowsNull() 94 | && $type !== 'scalar' 95 | && $type !== 'array' 96 | && $type !== 'iterable' 97 | ) { 98 | $missing[] = $param; 99 | unset($args[$name]); 100 | } 101 | 102 | continue; 103 | } 104 | 105 | if (!self::convertType($args[$name], $type)) { 106 | throw new InvalidLinkException(sprintf( 107 | 'Argument $%s passed to %s must be %s, %s given.', 108 | $name, 109 | Reflection::toString($method), 110 | $type, 111 | get_debug_type($args[$name]), 112 | )); 113 | } 114 | 115 | $def = $param->isDefaultValueAvailable() 116 | ? $param->getDefaultValue() 117 | : null; 118 | if ($args[$name] === $def || ($def === null && $args[$name] === '')) { 119 | $args[$name] = null; // value transmit is unnecessary 120 | } 121 | } 122 | 123 | if (array_key_exists($i, $args)) { 124 | throw new InvalidLinkException('Passed more parameters than method ' . Reflection::toString($method) . ' expects.'); 125 | } 126 | } 127 | 128 | 129 | /** 130 | * Lossless type conversion. 131 | */ 132 | public static function convertType(mixed &$val, string $types): bool 133 | { 134 | $scalars = ['string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'true' => 1, 'false' => 1]; 135 | $testable = ['iterable' => 1, 'object' => 1, 'array' => 1, 'null' => 1]; 136 | 137 | foreach (explode('|', ltrim($types, '?')) as $type) { 138 | if (match (true) { 139 | isset($scalars[$type]) => self::castScalar($val, $type), 140 | isset($testable[$type]) => "is_$type"($val), 141 | $type === 'scalar' => !is_array($val), // special type due to historical reasons 142 | $type === 'mixed' => true, 143 | $type === 'callable' => false, // intentionally disabled for security reasons 144 | default => $val instanceof $type, 145 | }) { 146 | return true; 147 | } 148 | } 149 | 150 | return false; 151 | } 152 | 153 | 154 | /** 155 | * Lossless type casting. 156 | */ 157 | private static function castScalar(mixed &$val, string $type): bool 158 | { 159 | if (!is_scalar($val)) { 160 | return false; 161 | } 162 | 163 | $tmp = ($val === false ? '0' : (string) $val); 164 | if ($type === 'float') { 165 | $tmp = preg_replace('#\.0*$#D', '', $tmp); 166 | } 167 | 168 | $orig = $tmp; 169 | $spec = ['true' => true, 'false' => false]; 170 | isset($spec[$type]) ? $tmp = $spec[$type] : settype($tmp, $type); 171 | if ($orig !== ($tmp === false ? '0' : (string) $tmp)) { 172 | return false; // data-loss occurs 173 | } 174 | 175 | $val = $tmp; 176 | return true; 177 | } 178 | 179 | 180 | public static function getType(\ReflectionParameter|\ReflectionProperty $item): string 181 | { 182 | if ($type = $item->getType()) { 183 | return (string) $type; 184 | } 185 | $default = $item instanceof \ReflectionProperty || $item->isDefaultValueAvailable() 186 | ? $item->getDefaultValue() 187 | : null; 188 | return $default === null ? 'scalar' : get_debug_type($default); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Application/UI/Renderable.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/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/ApplicationDI/ApplicationExtension.php: -------------------------------------------------------------------------------- 1 | scanDirs = (array) $scanDirs; 39 | } 40 | 41 | 42 | public function getConfigSchema(): Nette\Schema\Schema 43 | { 44 | return Expect::structure([ 45 | 'debugger' => Expect::bool(), 46 | 'errorPresenter' => Expect::anyOf( 47 | Expect::structure([ 48 | '4xx' => Expect::string('Nette:Error')->dynamic(), 49 | '5xx' => Expect::string('Nette:Error')->dynamic(), 50 | ])->castTo('array'), 51 | Expect::string()->dynamic(), 52 | )->firstIsDefault(), 53 | 'catchExceptions' => Expect::bool(false)->dynamic(), 54 | 'mapping' => Expect::anyOf( 55 | Expect::string(), 56 | Expect::arrayOf('string|array'), 57 | ), 58 | 'aliases' => Expect::arrayOf('string'), 59 | 'scanDirs' => Expect::anyOf( 60 | Expect::arrayOf('string')->default($this->scanDirs)->mergeDefaults(), 61 | false, 62 | )->firstIsDefault(), 63 | 'scanComposer' => Expect::bool(class_exists(ClassLoader::class)), 64 | 'scanFilter' => Expect::string('*Presenter'), 65 | 'silentLinks' => Expect::bool(), 66 | ]); 67 | } 68 | 69 | 70 | public function loadConfiguration(): void 71 | { 72 | $config = $this->config; 73 | $builder = $this->getContainerBuilder(); 74 | $builder->addExcludedClasses([UI\Presenter::class]); 75 | 76 | $this->invalidLinkMode = $this->debugMode 77 | ? UI\Presenter::InvalidLinkTextual | ($config->silentLinks ? 0 : UI\Presenter::InvalidLinkWarning) 78 | : UI\Presenter::InvalidLinkWarning; 79 | 80 | $application = $builder->addDefinition($this->prefix('application')) 81 | ->setFactory(Nette\Application\Application::class); 82 | if ($config->catchExceptions || !$this->debugMode) { 83 | $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]); 84 | $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]); 85 | } 86 | 87 | $this->compiler->addExportedType(Nette\Application\Application::class); 88 | 89 | if ($this->debugMode && ($config->scanDirs || $this->robotLoader) && $this->tempDir) { 90 | $touch = $this->tempDir . '/touch'; 91 | Nette\Utils\FileSystem::createDir($this->tempDir); 92 | $this->getContainerBuilder()->addDependency($touch); 93 | } 94 | 95 | $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory')) 96 | ->setType(Nette\Application\IPresenterFactory::class) 97 | ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( 98 | Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class, 99 | [1 => $touch ?? null], 100 | )]); 101 | 102 | if ($config->mapping) { 103 | $presenterFactory->addSetup('setMapping', [ 104 | is_string($config->mapping) ? ['*' => $config->mapping] : $config->mapping, 105 | ]); 106 | } 107 | 108 | if ($config->aliases) { 109 | $presenterFactory->addSetup('setAliases', [$config->aliases]); 110 | } 111 | 112 | $builder->addDefinition($this->prefix('linkGenerator')) 113 | ->setFactory(Nette\Application\LinkGenerator::class, [ 114 | 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']), 115 | ]); 116 | 117 | if ($this->name === 'application') { 118 | $builder->addAlias('application', $this->prefix('application')); 119 | $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory')); 120 | } 121 | } 122 | 123 | 124 | public function beforeCompile(): void 125 | { 126 | $builder = $this->getContainerBuilder(); 127 | 128 | if ($this->config->debugger ?? $builder->getByType(Tracy\BlueScreen::class)) { 129 | $builder->getDefinition($this->prefix('application')) 130 | ->addSetup([self::class, 'initializeBlueScreenPanel']); 131 | } 132 | 133 | $all = []; 134 | 135 | foreach ($builder->findByType(Nette\Application\IPresenter::class) as $def) { 136 | $all[$def->getType()] = $def; 137 | } 138 | 139 | $counter = 0; 140 | foreach ($this->findPresenters() as $class) { 141 | $this->checkPresenter($class); 142 | if (empty($all[$class])) { 143 | $all[$class] = $builder->addDefinition($this->prefix((string) ++$counter)) 144 | ->setType($class); 145 | } 146 | } 147 | 148 | foreach ($all as $def) { 149 | $def->addTag(Nette\DI\Extensions\InjectExtension::TagInject) 150 | ->setAutowired(false); 151 | 152 | if (is_subclass_of($def->getType(), UI\Presenter::class) && $def instanceof Definitions\ServiceDefinition) { 153 | $def->addSetup('$invalidLinkMode', [$this->invalidLinkMode]); 154 | } 155 | 156 | $this->compiler->addExportedType($def->getType()); 157 | } 158 | } 159 | 160 | 161 | /** @return string[] */ 162 | private function findPresenters(): array 163 | { 164 | $config = $this->getConfig(); 165 | 166 | if ($config->scanDirs) { 167 | if (!class_exists(Nette\Loaders\RobotLoader::class)) { 168 | throw new Nette\NotSupportedException("RobotLoader is required to find presenters, install package `nette/robot-loader` or disable option {$this->prefix('scanDirs')}: false"); 169 | } 170 | 171 | $robot = new Nette\Loaders\RobotLoader; 172 | $robot->addDirectory(...$config->scanDirs); 173 | $robot->acceptFiles = [$config->scanFilter . '.php']; 174 | if ($this->tempDir) { 175 | $robot->setTempDirectory($this->tempDir); 176 | $robot->refresh(); 177 | } else { 178 | $robot->rebuild(); 179 | } 180 | } elseif ($this->robotLoader && $config->scanDirs !== false) { 181 | $robot = $this->robotLoader; 182 | $robot->refresh(); 183 | } 184 | 185 | $classes = []; 186 | if (isset($robot)) { 187 | $classes = array_keys($robot->getIndexedClasses()); 188 | } 189 | 190 | if ($config->scanComposer) { 191 | $rc = new \ReflectionClass(ClassLoader::class); 192 | $classFile = dirname($rc->getFileName()) . '/autoload_classmap.php'; 193 | if (is_file($classFile)) { 194 | $this->getContainerBuilder()->addDependency($classFile); 195 | $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile))); 196 | } 197 | } 198 | 199 | $presenters = []; 200 | foreach (array_unique($classes) as $class) { 201 | if ( 202 | fnmatch($config->scanFilter, $class) 203 | && class_exists($class) 204 | && ($rc = new \ReflectionClass($class)) 205 | && $rc->implementsInterface(Nette\Application\IPresenter::class) 206 | && !$rc->isAbstract() 207 | ) { 208 | $presenters[] = $rc->getName(); 209 | } 210 | } 211 | 212 | return $presenters; 213 | } 214 | 215 | 216 | /** @internal */ 217 | public static function initializeBlueScreenPanel( 218 | Tracy\BlueScreen $blueScreen, 219 | Nette\Application\Application $application, 220 | ): void 221 | { 222 | $blueScreen->addPanel(function (?\Throwable $e) use ($application, $blueScreen): ?array { 223 | $dumper = $blueScreen->getDumper(); 224 | return $e ? null : [ 225 | 'tab' => 'Nette Application', 226 | 'panel' => '

Requests

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

Presenter

' . $dumper($application->getPresenter()), 228 | ]; 229 | }); 230 | if ( 231 | version_compare(Tracy\Debugger::Version, '2.9.0', '>=') 232 | && version_compare(Tracy\Debugger::Version, '3.0', '<') 233 | ) { 234 | $blueScreen->addFileGenerator(self::generateNewPresenterFileContents(...)); 235 | } 236 | } 237 | 238 | 239 | public static function generateNewPresenterFileContents(string $file, ?string $class = null): ?string 240 | { 241 | if (!$class || !str_ends_with($file, 'Presenter.php')) { 242 | return null; 243 | } 244 | 245 | $res = "checked[$class])) { 259 | return; 260 | } 261 | $this->checked[$class] = true; 262 | 263 | $rc = new \ReflectionClass($class); 264 | if ($rc->getParentClass()) { 265 | $this->checkPresenter($rc->getParentClass()->getName()); 266 | } 267 | 268 | foreach ($rc->getProperties() as $rp) { 269 | if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class)) 270 | && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) 271 | ) { 272 | throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr)); 273 | } 274 | } 275 | 276 | $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; 277 | foreach ($rc->getMethods() as $rm) { 278 | if (preg_match("#^$re#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { 279 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm))); 280 | } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) { 281 | throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm))); 282 | } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) 283 | && !preg_match("#^$re|createComponent.#", $rm->getName()) 284 | ) { 285 | 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)); 286 | } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) { 287 | 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)); 288 | } 289 | } 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationDI/LatteExtension.php: -------------------------------------------------------------------------------- 1 | Expect::anyOf(true, false, 'all'), 36 | 'extensions' => Expect::arrayOf('string|Nette\DI\Definitions\Statement'), 37 | 'templateClass' => Expect::string(), 38 | 'strictTypes' => Expect::bool(false), 39 | 'strictParsing' => Expect::bool(false), 40 | 'phpLinter' => Expect::string(), 41 | 'locale' => Expect::string(), 42 | ]); 43 | } 44 | 45 | 46 | public function loadConfiguration(): void 47 | { 48 | if (!class_exists(Latte\Engine::class)) { 49 | return; 50 | } 51 | 52 | $config = $this->config; 53 | $builder = $this->getContainerBuilder(); 54 | 55 | $builder->addFactoryDefinition($this->prefix('latteFactory')) 56 | ->setImplement(ApplicationLatte\LatteFactory::class) 57 | ->getResultDefinition() 58 | ->setFactory(Latte\Engine::class) 59 | ->addSetup('setTempDirectory', [$this->tempDir]) 60 | ->addSetup('setAutoRefresh', [$this->debugMode]) 61 | ->addSetup('setStrictTypes', [$config->strictTypes]) 62 | ->addSetup('setStrictParsing', [$config->strictParsing]) 63 | ->addSetup('enablePhpLinter', [$config->phpLinter]) 64 | ->addSetup('setLocale', [$config->locale]); 65 | 66 | $this->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/ApplicationDI/PresenterFactoryCallback.php: -------------------------------------------------------------------------------- 1 | container->findByType($class); 31 | if (count($services) > 1) { 32 | $services = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); 33 | if (count($services) > 1) { 34 | throw new Nette\Application\InvalidPresenterException("Multiple services of type $class found: " . implode(', ', $services) . '.'); 35 | } 36 | } 37 | 38 | if (count($services) === 1) { 39 | return $this->container->createService($services[0]); 40 | } 41 | 42 | if ($this->touchToRefresh && class_exists($class)) { 43 | touch($this->touchToRefresh); 44 | 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.'; 45 | header('Refresh: 5'); 46 | exit; 47 | } 48 | 49 | throw new Nette\Application\InvalidPresenterException("No services of type $class found."); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/DefaultTemplate.php: -------------------------------------------------------------------------------- 1 | $name = $value; 44 | return $this; 45 | } 46 | 47 | 48 | /** 49 | * Sets all parameters. 50 | */ 51 | public function setParameters(array $params): static 52 | { 53 | return Nette\Utils\Arrays::toObject($params, $this); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/LatteFactory.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 | foreach ($modifier->filters as $filter) { 68 | match ($filter->name->name) { 69 | 'noescape' => $node->escape = false, 70 | default => throw new Latte\CompileException('Only modifier |noescape is allowed here.', $tag->position), 71 | }; 72 | } 73 | 74 | return $node; 75 | } 76 | 77 | 78 | public function print(PrintContext $context): string 79 | { 80 | if ($this->escape === null && $context->getEscaper()->getState() !== Escaper::HtmlText) { 81 | $this->escape = true; 82 | } 83 | 84 | $method = match (true) { 85 | !$this->method => 'render', 86 | $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' . ucfirst($this->method->value), 87 | default => "{'render' . " . $this->method->print($context) . '}', 88 | }; 89 | 90 | $fetchCode = $context->format( 91 | $this->name instanceof StringNode 92 | ? '$ʟ_tmp = $this->global->uiControl->getComponent(%node);' 93 | : 'if (!is_object($ʟ_tmp = %node)) $ʟ_tmp = $this->global->uiControl->getComponent($ʟ_tmp);', 94 | $this->name, 95 | ); 96 | 97 | if ($this->escape) { 98 | return $context->format( 99 | <<<'XX' 100 | %raw 101 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); 102 | ob_start(fn() => ''); 103 | $ʟ_tmp->%raw(%args) %line; 104 | $ʟ_fi = new LR\FilterInfo(%dump); echo %modifyContent(ob_get_clean()); 105 | 106 | 107 | XX, 108 | $fetchCode, 109 | $method, 110 | $this->args, 111 | $this->position, 112 | Latte\ContentType::Html, 113 | new ModifierNode([], $this->escape), 114 | ); 115 | 116 | } else { 117 | return $context->format( 118 | <<<'XX' 119 | %raw 120 | if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); 121 | $ʟ_tmp->%raw(%args) %line; 122 | 123 | 124 | XX, 125 | $fetchCode, 126 | $method, 127 | $this->args, 128 | $this->position, 129 | ); 130 | } 131 | } 132 | 133 | 134 | public function &getIterator(): \Generator 135 | { 136 | yield $this->name; 137 | if ($this->method) { 138 | yield $this->method; 139 | } 140 | yield $this->args; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /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/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->modifier->check = false; 44 | $node->mode = $tag->name; 45 | 46 | if ($tag->isNAttribute()) { 47 | // move at the beginning 48 | $node->position = $tag->position; 49 | array_unshift($tag->htmlElement->attributes->children, $node); 50 | return null; 51 | } 52 | 53 | return $node; 54 | } 55 | 56 | 57 | public function print(PrintContext $context): string 58 | { 59 | if ($this->mode === 'href') { 60 | $context->beginEscape()->enterHtmlAttribute(null, '"'); 61 | $res = $context->format( 62 | <<<'XX' 63 | echo ' href="'; echo %modify($this->global->uiControl->link(%node, %node?)) %line; echo '"'; 64 | XX, 65 | $this->modifier, 66 | $this->destination, 67 | $this->args, 68 | $this->position, 69 | ); 70 | $context->restoreEscape(); 71 | return $res; 72 | } 73 | 74 | return $context->format( 75 | 'echo %modify(' 76 | . ($this->mode === 'plink' ? '$this->global->uiPresenter' : '$this->global->uiControl') 77 | . '->link(%node, %node?)) %line;', 78 | $this->modifier, 79 | $this->destination, 80 | $this->args, 81 | $this->position, 82 | ); 83 | } 84 | 85 | 86 | public function &getIterator(): \Generator 87 | { 88 | yield $this->destination; 89 | yield $this->args; 90 | yield $this->modifier; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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/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/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/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php: -------------------------------------------------------------------------------- 1 | getParameters(), ' . PhpHelpers::dump($this->template ?? Template::class) . '); exit;'; 26 | } 27 | 28 | 29 | public static function printClass(array $params, string $parentClass): void 30 | { 31 | $bp = new Latte\Essential\Blueprint; 32 | if (!method_exists($bp, 'generateTemplateClass')) { 33 | throw new \LogicException("Please update 'latte/latte' to version 3.0.15 or newer."); 34 | } 35 | 36 | $control = $params['control'] ?? $params['presenter'] ?? null; 37 | $name = 'Template'; 38 | if ($control instanceof UI\Control) { 39 | $name = preg_replace('#(Control|Presenter)$#', '', $control::class) . 'Template'; 40 | unset($params[$control instanceof UI\Presenter ? 'control' : 'presenter']); 41 | } 42 | $class = $bp->generateTemplateClass($params, $name, $parentClass); 43 | $code = (string) $class->getNamespace(); 44 | 45 | $bp->printBegin(); 46 | $bp->printCode($code); 47 | 48 | if ($control instanceof UI\Control) { 49 | $file = dirname((new \ReflectionClass($control))->getFileName()) . '/' . $class->getName() . '.php'; 50 | if (file_exists($file)) { 51 | echo "unsaved, file {$bp->clickableFile($file)} already exists"; 52 | } else { 53 | echo "saved to file {$bp->clickableFile($file)}"; 54 | file_put_contents($file, "printEnd(); 59 | exit; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/SnippetRuntime.php: -------------------------------------------------------------------------------- 1 | */ 29 | private array $stack = []; 30 | private int $nestingLevel = 0; 31 | private bool $renderingSnippets = false; 32 | 33 | private ?\stdClass $payload; 34 | 35 | 36 | public function __construct( 37 | private readonly Control $control, 38 | ) { 39 | } 40 | 41 | 42 | public function enter(string $name, string $type): void 43 | { 44 | if (!$this->renderingSnippets) { 45 | if ($type === self::TypeDynamic && $this->nestingLevel === 0) { 46 | trigger_error('Dynamic snippets are allowed only inside static snippet/snippetArea.', E_USER_WARNING); 47 | } 48 | 49 | $this->nestingLevel++; 50 | return; 51 | } 52 | 53 | $obStarted = false; 54 | if ( 55 | ($this->nestingLevel === 0 && $this->control->isControlInvalid($name)) 56 | || ($type === self::TypeDynamic && ($previous = end($this->stack)) && $previous[1] === true) 57 | ) { 58 | ob_start(fn() => null); 59 | $this->nestingLevel = $type === self::TypeArea ? 0 : 1; 60 | $obStarted = true; 61 | } elseif ($this->nestingLevel > 0) { 62 | $this->nestingLevel++; 63 | } 64 | 65 | $this->stack[] = [$name, $obStarted]; 66 | if ($name !== '') { 67 | $this->control->redrawControl($name, false); 68 | } 69 | } 70 | 71 | 72 | public function leave(): void 73 | { 74 | if (!$this->renderingSnippets) { 75 | $this->nestingLevel--; 76 | return; 77 | } 78 | 79 | [$name, $obStarted] = array_pop($this->stack); 80 | if ($this->nestingLevel > 0 && --$this->nestingLevel === 0) { 81 | $content = ob_get_clean(); 82 | $this->payload ??= $this->control->getPresenter()->getPayload(); 83 | $this->payload->snippets[$this->control->getSnippetId($name)] = $content; 84 | 85 | } elseif ($obStarted) { // dynamic snippet wrapper or snippet area 86 | ob_end_clean(); 87 | } 88 | } 89 | 90 | 91 | public function getHtmlId(string $name): string 92 | { 93 | return $this->control->getSnippetId($name); 94 | } 95 | 96 | 97 | /** 98 | * @param Block[] $blocks 99 | * @param mixed[] $params 100 | */ 101 | public function renderSnippets(array $blocks, array $params): bool 102 | { 103 | if ($this->renderingSnippets || !$this->control->snippetMode) { 104 | return false; 105 | } 106 | 107 | $this->renderingSnippets = true; 108 | $this->control->snippetMode = false; 109 | foreach ($blocks as $name => $block) { 110 | if (!$this->control->isControlInvalid($name)) { 111 | continue; 112 | } 113 | 114 | $function = reset($block->functions); 115 | $function($params); 116 | } 117 | 118 | $this->control->snippetMode = true; 119 | $this->renderChildren(); 120 | return true; 121 | } 122 | 123 | 124 | private function renderChildren(): void 125 | { 126 | $queue = [$this->control]; 127 | do { 128 | foreach (array_shift($queue)->getComponents() as $child) { 129 | if ($child instanceof Renderable) { 130 | if ($child->isControlInvalid()) { 131 | $child->snippetMode = true; 132 | $child->render(); 133 | $child->snippetMode = false; 134 | } 135 | } elseif ($child instanceof Nette\ComponentModel\IContainer) { 136 | $queue[] = $child; 137 | } 138 | } 139 | } while ($queue); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/Template.php: -------------------------------------------------------------------------------- 1 | latte; 34 | } 35 | 36 | 37 | /** 38 | * Renders template to output. 39 | */ 40 | public function render(?string $file = null, array $params = []): void 41 | { 42 | Nette\Utils\Arrays::toObject($params, $this); 43 | if (isset($this->blueprint)) { 44 | Nodes\TemplatePrintNode::printClass($this->getParameters(), $this->blueprint); 45 | } 46 | $this->latte->render($file ?: $this->file, $this); 47 | } 48 | 49 | 50 | /** 51 | * Renders template to output. 52 | */ 53 | public function renderToString(?string $file = null, array $params = []): string 54 | { 55 | Nette\Utils\Arrays::toObject($params, $this); 56 | return $this->latte->renderToString($file ?: $this->file, $this); 57 | } 58 | 59 | 60 | /** 61 | * Renders template to string. 62 | */ 63 | public function __toString(): string 64 | { 65 | return $this->latte->renderToString($this->file, $this->getParameters()); 66 | } 67 | 68 | 69 | /********************* template filters & helpers ****************d*g**/ 70 | 71 | 72 | /** 73 | * Registers run-time filter. 74 | */ 75 | public function addFilter(?string $name, callable $callback): static 76 | { 77 | $this->latte->addFilter($name, $callback); 78 | return $this; 79 | } 80 | 81 | 82 | /** 83 | * Registers run-time function. 84 | */ 85 | public function addFunction(string $name, callable $callback): static 86 | { 87 | $this->latte->addFunction($name, $callback); 88 | return $this; 89 | } 90 | 91 | 92 | /** 93 | * Sets translate adapter. 94 | */ 95 | public function setTranslator(?Nette\Localization\Translator $translator, ?string $language = null): static 96 | { 97 | $this->latte->addExtension(new Latte\Essential\TranslatorExtension($translator, $language)); 98 | return $this; 99 | } 100 | 101 | 102 | /********************* template parameters ****************d*g**/ 103 | 104 | 105 | /** 106 | * Sets the path to the template file. 107 | */ 108 | public function setFile(string $file): static 109 | { 110 | $this->file = $file; 111 | return $this; 112 | } 113 | 114 | 115 | final public function getFile(): ?string 116 | { 117 | return $this->file; 118 | } 119 | 120 | 121 | /** 122 | * Returns array of all parameters. 123 | */ 124 | final public function getParameters(): array 125 | { 126 | $res = []; 127 | foreach ((new \ReflectionObject($this))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { 128 | if ($prop->isInitialized($this)) { 129 | $res[$prop->getName()] = $prop->getValue($this); 130 | } 131 | } 132 | 133 | return $res; 134 | } 135 | 136 | 137 | public function blueprint(?string $parentClass = null): void 138 | { 139 | $this->blueprint = $parentClass ?? self::class; 140 | } 141 | 142 | 143 | /** 144 | * Prevents unserialization. 145 | */ 146 | final public function __wakeup() 147 | { 148 | throw new Nette\NotImplementedException('Object unserialization is not supported by class ' . static::class); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/TemplateFactory.php: -------------------------------------------------------------------------------- 1 | Occurs when a new template is created */ 22 | public array $onCreate = []; 23 | private string $templateClass; 24 | 25 | 26 | public function __construct( 27 | private readonly LatteFactory $latteFactory, 28 | private readonly ?Nette\Http\IRequest $httpRequest = null, 29 | private readonly ?Nette\Security\User $user = null, 30 | $templateClass = null, 31 | ) { 32 | if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, true))) { 33 | throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.'); 34 | } 35 | 36 | $this->templateClass = $templateClass ?: DefaultTemplate::class; 37 | } 38 | 39 | 40 | public function createTemplate(?UI\Control $control = null, ?string $class = null): Template 41 | { 42 | $class ??= $this->templateClass; 43 | if (!is_a($class, Template::class, allow_string: true)) { 44 | throw new Nette\InvalidArgumentException("Class $class does not implement " . Template::class . ' or it does not exist.'); 45 | } 46 | 47 | $latte = $this->latteFactory->create($control); 48 | $template = new $class($latte); 49 | $presenter = $control?->getPresenterIfExists(); 50 | 51 | // default parameters 52 | $baseUrl = $this->httpRequest 53 | ? rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/') 54 | : null; 55 | $flashes = $presenter instanceof UI\Presenter && $presenter->hasFlashSession() 56 | ? (array) $presenter->getFlashSession()->get($control->getParameterId('flash')) 57 | : []; 58 | 59 | $params = [ 60 | 'user' => $this->user, 61 | 'baseUrl' => $baseUrl, 62 | 'basePath' => $baseUrl ? preg_replace('#https?://[^/]+#A', '', $baseUrl) : null, 63 | 'flashes' => $flashes, 64 | 'control' => $control, 65 | 'presenter' => $presenter, 66 | ]; 67 | 68 | foreach ($params as $key => $value) { 69 | if ($value !== null && property_exists($template, $key)) { 70 | $template->$key = $value; 71 | } 72 | } 73 | 74 | Nette\Utils\Arrays::invoke($this->onCreate, $template); 75 | 76 | return $template; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Bridges/ApplicationLatte/UIExtension.php: -------------------------------------------------------------------------------- 1 | fn($time, $delta, $unit = null) => $time 36 | ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) 37 | : null, 38 | ]; 39 | } 40 | 41 | 42 | public function getFunctions(): array 43 | { 44 | if ($presenter = $this->control?->getPresenterIfExists()) { 45 | return [ 46 | 'isLinkCurrent' => $presenter->isLinkCurrent(...), 47 | 'isModuleCurrent' => $presenter->isModuleCurrent(...), 48 | ]; 49 | } 50 | return []; 51 | } 52 | 53 | 54 | public function getProviders(): array 55 | { 56 | $presenter = $this->control?->getPresenterIfExists(); 57 | $httpResponse = $presenter?->getHttpResponse(); 58 | return [ 59 | 'coreParentFinder' => $this->findLayoutTemplate(...), 60 | 'uiControl' => $this->control, 61 | 'uiPresenter' => $presenter, 62 | 'snippetDriver' => $this->control ? new SnippetRuntime($this->control) : null, 63 | 'uiNonce' => $httpResponse ? $this->findNonce($httpResponse) : null, 64 | ]; 65 | } 66 | 67 | 68 | public function getTags(): array 69 | { 70 | return [ 71 | 'n:href' => Nodes\LinkNode::create(...), 72 | 'n:nonce' => Nodes\NNonceNode::create(...), 73 | 'control' => Nodes\ControlNode::create(...), 74 | 'plink' => Nodes\LinkNode::create(...), 75 | 'link' => Nodes\LinkNode::create(...), 76 | 'ifCurrent' => Nodes\IfCurrentNode::create(...), 77 | 'templatePrint' => Nodes\TemplatePrintNode::create(...), 78 | 'snippet' => Nodes\SnippetNode::create(...), 79 | 'snippetArea' => Nodes\SnippetAreaNode::create(...), 80 | 'layout' => $this->createExtendsNode(...), 81 | 'extends' => $this->createExtendsNode(...), 82 | ]; 83 | } 84 | 85 | 86 | public function getPasses(): array 87 | { 88 | return [ 89 | 'snippetRendering' => $this->snippetRenderingPass(...), 90 | ]; 91 | } 92 | 93 | 94 | /** 95 | * Render snippets instead of template in snippet-mode. 96 | */ 97 | public function snippetRenderingPass(TemplateNode $templateNode): void 98 | { 99 | array_unshift($templateNode->main->children, new Latte\Compiler\Nodes\AuxiliaryNode(fn() => <<<'XX' 100 | if ($this->global->snippetDriver?->renderSnippets($this->blocks[self::LayerSnippet], $this->params)) { return; } 101 | 102 | 103 | XX)); 104 | } 105 | 106 | 107 | public static function findLayoutTemplate(Latte\Runtime\Template $template): ?string 108 | { 109 | $presenter = $template->global->uiControl ?? null; 110 | return $presenter instanceof UI\Presenter && !empty($template::Blocks[$template::LayerTop]) 111 | ? $presenter->findLayoutTemplateFile() 112 | : null; 113 | } 114 | 115 | 116 | private function findNonce(Nette\Http\IResponse $httpResponse): ?string 117 | { 118 | $header = $httpResponse->getHeader('Content-Security-Policy') 119 | ?: $httpResponse->getHeader('Content-Security-Policy-Report-Only'); 120 | return preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null; 121 | } 122 | 123 | 124 | public static function createExtendsNode(Tag $tag): ExtendsNode 125 | { 126 | $auto = $tag->parser->stream->is('auto'); 127 | $node = ExtendsNode::create($tag); 128 | if ($auto) { 129 | $node->extends = new AuxiliaryNode(fn() => '$this->global->uiPresenter->findLayoutTemplateFile()'); 130 | } 131 | return $node; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /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 = $flags[$i] & $router::ONE_WAY ? 'oneway' : 'no'; 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/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/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/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/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 | -------------------------------------------------------------------------------- /src/compatibility-intf.php: -------------------------------------------------------------------------------- 1 |