diff --git a/.gitattributes b/.gitattributes index 433a2de9a..7ff79987f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,13 @@ .gitattributes export-ignore .github/ export-ignore .gitignore export-ignore -ncs.* export-ignore +AGENTS.md export-ignore +dresscode.neon export-ignore phpstan*.neon export-ignore src/**/*.latte export-ignore +docs/ export-ignore tests/ export-ignore +* text=auto *.php* diff=php *.sh text eol=lf diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index 886eb330c..8ac229c34 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -3,29 +3,29 @@ name: Coding Style on: [push, pull_request] jobs: - nette_cc: - name: Nette Code Checker + dresscode: + name: DressCode runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: none - - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress - - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore expected --ignore "src/Bridges/ApplicationTracy/*.latte" + - run: composer create-project dresscode/dresscode temp/dresscode ^1.1 --no-progress + - run: php temp/dresscode/bin/dresscode check - nette_cs: - name: Nette Coding Standard + nette_cc: + name: Nette Code Checker runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: shivammathur/setup-php@v2 with: - php-version: 8.3 + php-version: 8.4 coverage: none - - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress - - run: php temp/coding-standard/ecs check + - run: composer create-project nette/code-checker temp/code-checker ^3 --no-progress + - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore expected --ignore "src/Bridges/ApplicationTracy/*.latte" diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0c2a203de..e62a9bbcc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: ['8.3', '8.4', '8.5'] + php: ['8.3', '8.4', '8.5', '8.6'] fail-fast: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..bab0d171b --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# To My Agents! + +It is my fervent wish that this file guide every AI coding agent working with code in this repository. + +## Documentation + +Any distilled, agent-facing documentation for this package - how it works +internally and the rationale behind key design decisions - lives in `docs/`. +Consult it before non-trivial changes; it is the source of truth from which the +public manual is distilled. + +The presenter lifecycle, link generation, persistent state, routing, and template +lookup carry subtle non-local invariants - read `docs/internals` before touching +them. + +`docs/upgrading.md` records, per released version, every change visible to +users of the package (renames, removals, deprecations, signatures, behavior, +configuration), each with its commit hash; internal changes are not in it. It +is the place to look up the history of a public symbol. Do not write to it; it +is extended at release time from the git history. + +## Project Overview + +Nette Application is a full-stack, component-based MVC framework library for PHP - +the core application layer of the Nette Framework: presenters, components, routing, +templating integration, and the request/response cycle. It is a library package, +not a runnable application, and integrates with the rest of the framework through +`src/Bridges` (DI, Latte, Tracy). + +- **PHP Version**: 8.3 - 8.5 +- **Package**: `nette/application` + +## Essential Commands + +```bash +# Run all tests +composer tester # or: vendor/bin/tester tests -s + +# Run one test directory / file +vendor/bin/tester tests/UI -s +php tests/Application/Presenter.twoDomains.phpt + +# Static analysis (PHPStan level 8) +composer phpstan +``` + +## Conventions + +- Every file starts with `declare(strict_types=1);`; tabs for indentation; Nette + Coding Standard (PSR-12 based) with the opening brace on its own line. +- Tests are Nette Tester `.phpt` files mirroring `src/` under `tests/`; use + `test()` / `testException()` and `Assert::*`. Temp files go to `tests/tmp/{pid}/`, + and `tests/types/` holds PHPStan type-inference fixtures. + +## Working in this repo + +- **The presenter lifecycle is exact and load-bearing.** The precise phase order + (action, canonicalization, signal dispatch, `beforeRender`, `render`, the + `onRender` hook, `afterRender`), `AbortException`-based termination, and the + 4xx-not-500 invalid-link contract are documented in `docs/internals/lifecycle.md`. + Don't reconstruct them from memory. +- **Links and persistent state span the whole component tree.** Link-generation + modes and whole-tree global-state aggregation are the trickiest code here; see + `docs/internals/link-generation.md` and `persistent-state.md`. +- User-facing how-to (routing DSL, signals, snippets, component factories, flash + messages, DI/NEON configuration) is manual material and lives in the public web + docs, not here. diff --git a/composer.json b/composer.json index 08247d89f..d065a7193 100644 --- a/composer.json +++ b/composer.json @@ -11,10 +11,10 @@ } ], "require": { - "php": "8.3 - 8.5", - "nette/component-model": "^3.2 || ^4.0", + "php": "8.3 - 8.6", + "nette/component-model": "^4.0", "nette/http": "^3.4", - "nette/routing": "^3.1.1", + "nette/routing": "^3.1.5", "nette/utils": "^4.1" }, "suggest": { @@ -25,7 +25,7 @@ "nette/tester": "^2.6", "nette/caching": "^3.2", "nette/di": "^3.2", - "nette/forms": "^3.2", + "nette/forms": "^4.0", "nette/robot-loader": "^4.0", "nette/security": "^3.2", "latte/latte": "^3.1.4", @@ -39,9 +39,9 @@ "conflict": { "nette/caching": "<3.2", "nette/di": "<3.2", - "nette/forms": "<3.2", + "nette/forms": "<4.0", "nette/schema": "<1.3", - "latte/latte": "<3.1.4 || >=3.2", + "latte/latte": "<3.1.5 || >=3.2", "tracy/tracy": "<2.12" }, "autoload": { @@ -57,7 +57,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.3-dev" + "dev-master": "4.0-dev" } }, "config": { diff --git a/docs/decisions/2025-02-27 linkbase-tag.md b/docs/decisions/2025-02-27 linkbase-tag.md new file mode 100644 index 000000000..8ebba910f --- /dev/null +++ b/docs/decisions/2025-02-27 linkbase-tag.md @@ -0,0 +1,81 @@ +# {linkBase} tag - compile-time base for relative links in a template + +Status: accepted · 2025-02-27 · branch `v3.2` (released in 3.2.7) + +## Context + +A flexible directory structure is one of the most valuable features of Nette +Application, but it creates a problem with shared layouts: relative link targets +(`Dashboard:`, `Products:Overview:`) are resolved against the *current* +presenter. When the same layout is used by both `Admin:Dashboard` and +`Admin:Products:Detail`, the target `Dashboard:` resolves correctly to +`Admin:Dashboard:default` in one case and to the non-existent +`Admin:Products:Dashboard:default` in the other. + +The previous workaround was to write every target in layouts absolutely +(`:Admin:Dashboard:`). It works, but the module prefix is duplicated in every +link, so moving or renaming a module means editing templates en masse. + +## Considered options + +- **Status quo (absolute links everywhere)** - no new concept, but prefix + duplication and fragility when refactoring modules. +- **Runtime solution** (setting the base on the presenter/component, or a + template variable threaded into `LinkGenerator`) - collides with the shared + template cache: the same compiled layout is used by presenters from different + modules, so the base would have to become part of the cache key, or a runtime + cost would be paid on every link. It would also make the template no longer + self-describing - a link's target would depend on state set somewhere in PHP. +- **Compile-time tag in the template** (chosen) - the base is declared directly + in the template it concerns, and expansion happens at compile time. + +## Decision + +A new tag `{linkBase module}` plus a compile-time pass `applyLinkBasePass` +registered in `UIExtension::getPasses()`: + +- The tag must appear in the template head (`Tag::isInHead()`, otherwise + `CompileException`) and applies to the **whole template file** - not to + included files, which are compiled separately and may carry their own + `{linkBase}`. It emits no code itself (`print()` returns `''`); all the work + is done by the pass. Only the first tag found is used + (`NodeHelpers::findFirst`). +- The pass traverses the AST and rewrites **all** `LinkNode`s - i.e. `{link}`, + `{plink}` and `n:href` uniformly. +- When both the target and the base are static strings, the destination is + rewritten directly at compile time (`LinkGenerator::applyBase()` called from + the pass) - **zero runtime overhead**. When either is an expression, the + destination is wrapped in a runtime call `LinkGenerator::applyBase(%node, + %node)` via `AuxiliaryNode`. +- Semantics of `LinkGenerator::applyBase($link, $base)`: the prefix `:$base:` is + added only when the target **contains a colon and does not start with one**. + Thus the base applies exclusively to relative targets that name a presenter + (`Users:list` → `:Admin:Users:list`); untouched are absolute targets + (`:Foo:`), targets within the current presenter (`show`, `this`), signals + (`click!`) and aliases - none of which have a colon in the middle. + +## Consequences + +- (+) Layouts and templates shared by presenters at different module depths link + consistently, regardless of who renders them. +- (+) Renaming or moving a module is a single change in `{linkBase}` instead of + editing every link. +- (+) For static links, expansion happens at compile time; nothing is paid at + runtime. +- (−) Another tag and concept to learn; the per-template-file scope may surprise + (includes do not inherit the base). +- (−) The base cannot be set per presenter/module from PHP - that remains open. + +## Code + +- `src/Bridges/ApplicationLatte/Nodes/LinkBaseNode.php` - the tag and the + `applyLinkBasePass` pass +- `src/Application/LinkGenerator.php` - `applyBase()` (`@internal`) +- tag and pass registration in `src/Bridges/ApplicationLatte/UIExtension.php` + (`getTags()`, `getPasses()`) +- test: `tests/Bridges.Latte/{linkBase}.phpt` (static and dynamic targets) +- commit `e3beab847`, released in `v3.2.7` + +The `|absoluteUrl` filter shipped in the same release for forcing an absolute URL +on an arbitrary value (image path, database value), complementing this tag for +cases outside presenter link targets. diff --git a/docs/internals/access-control.md b/docs/internals/access-control.md new file mode 100644 index 000000000..7d125d7cf --- /dev/null +++ b/docs/internals/access-control.md @@ -0,0 +1,65 @@ +# Access control & CSRF + +`AccessPolicy` (internal) evaluates `#[Requires]` attributes. The attribute is +repeatable, class- or method-level, and matched with `IS_INSTANCEOF`, so subclasses +(like the deprecated `#[CrossOrigin]`) count as `Requires` instances. + +Class-level attributes are **inherited**, unlike PHP attributes in general: +`AccessPolicy` collects them along the whole ancestor chain, so a rule on a base +presenter applies to every descendant. A rule (a property of `Requires`) set +explicitly in a descendant overrides the same rule of all its ancestors; the +other rules keep combining. Method-level attributes follow plain PHP semantics. + +## Enforcement runs at three sites + +No single file shows this; the policy is applied wherever user code is about to run: + +1. **Class-level** — `Presenter::run()`, in the checkRequirements phase, on the + presenter's own reflection (before `startup`). +2. **Method-level** — `Component::tryCall()`, i.e. on every `action*`, `render*` + and `handle*` invocation, right before argument conversion. +3. **Factory methods** — `Component::createComponent()` checks the policy on the + `createComponent()` method, so a component can be guarded at creation. + +At all three sites the (older, unrelated) `checkRequirements()` user hook is +also called with the same reflection — the two mechanisms coexist. + +## `#[Requires]` semantics + +- `methods:` — allowed HTTP methods; `['*']` is a wildcard. **Trap:** a + *class-level* `methods:` empties `$presenter->allowedMethods` to bypass the + deprecated legacy `Presenter::checkHttpMethod()`, so the attribute fully replaces + it. The legacy check runs before the method-level attributes, so it skips itself + when the `action*` or `render*` method of the current action carries `methods:`. + Violation → 405 with an `Allow` header. +- `actions:` — method-level use is allowed **only in presenters** + (`LogicException` otherwise). Violation → 4xx via `error()`. +- `forward: true` — the element is reachable only via forward; it also makes the + element **non-linkable**: `AccessPolicy::isLinkable()` is what + `LinkGenerator::validateLinkTarget` consults for every mode except `forward` + (see link-generation.md). Violation at runtime → 4xx. +- `ajax: true` — non-AJAX request → 403. +- `sameOrigin: true` — see below. + +## The implicit signal CSRF rule + +**Every `handle*` method implicitly gets `Requires(sameOrigin: true)`** — added by +`AccessPolicy::applyInternalRules` — unless the method opts out via +`#[Requires(sameOrigin: false)]`, the deprecated `#[CrossOrigin]`, or a legacy +`@crossOrigin` annotation. Same-origin is verified from the **`Sec-Fetch-Site` +header** (`$httpRequest->isFrom(FetchSite::SameOrigin)`), not from a SameSite +cookie. + +A violation does **not** produce an error: it calls `Presenter::detectedCsrf()`, +whose default is `redirect('this')` — **the signal is silently dropped** and the +page reloads without it. `detectedCsrf()` is public and overridable. + +## `UI\Form` has its own, parallel check + +`Form::signalReceived('submit')` does **not** go through `AccessPolicy` for CSRF: +it re-checks `Sec-Fetch-Site` itself, gated by the `$crossOrigin` flag from +nette/forms (`allowCrossOrigin()`), and calls the same `detectedCsrf()`. Two more +suppressions live here: a request with the `RESTORED` flag (set by +`Presenter::restoreRequest` for re-sent POSTs) skips `fireEvents()` entirely, and +`receiveHttpData()` returns null for forwarded requests — a form never submits +across a forward. diff --git a/docs/internals/lifecycle.md b/docs/internals/lifecycle.md new file mode 100644 index 000000000..d1dd98771 --- /dev/null +++ b/docs/internals/lifecycle.md @@ -0,0 +1,77 @@ +# Presenter lifecycle + +`Presenter::run(Request): Response` is a fixed sequence; the exact ordering is the +non-local knowledge (some steps sit where a reader would not expect). + +## Phase order (`run`) + +Before the `try`: `initGlobalParameters()` parses the action, the signal, and the +signal receiver, and calls `loadState()`. Then, inside the `try`: + +1. **checkRequirements** — access policy, `checkRequirements`, HTTP method check. +2. **startup** — `onStartup`, then `startup()`, then a guard that the child called + `parent::startup()` (else `InvalidStateException`). +3. **`action()`** — via `tryCall`. A `SwitchException` here re-enters with a + changed action **and disables `autoCanonicalize`**. +4. **component autoload** — every id in `globalParams` is `getComponent(…, throw: + false)`d. +5. **canonicalize** — only if `autoCanonicalize` (see below). +6. **HEAD** — terminates early on a HEAD request. +7. **`handle()`** — `processSignal()`. **This runs after the action and + before rendering**, which is the ordering that surprises people. +8. **beforeRender** → **`onRender`** (a hook that is easy to forget) → **`render()`** + (again `SwitchException` → `setView` + retry) → **afterRender**. +9. **sendTemplate** — completes and sends the template response. + +So the true order is **startup → action → (autoload + canonicalize) → signal → +beforeRender → onRender → render → afterRender → sendTemplate**. + +A `SwitchException` escaping anywhere other than the two retry sites above is +turned by `run()` into a `LogicException` ("Switch is only allowed inside +action*() or render*() method"). The two retry sites share one counter; more than +`Presenter::MaxSwitches` switches in a single `run()` end with a `LogicException` +too, so actions switching to each other cannot loop forever. + +## Termination is by exception; there is no explicit "frozen" flag + +Every terminating method funnels through `sendResponse()` → `terminate()`, which +**throws `AbortException`**: `sendTemplate`, `sendPayload`/`sendJson`, +`redirectUrl`, `forward`. `run()`'s **empty** `catch (AbortException)` then lets +control fall through to the post-lifecycle steps. So there is no separate "state is +frozen" mechanism — the response method simply throws, and nothing after it in the +lifecycle runs. (`error()` is different: it throws `BadRequestException`, which is +**not** caught in `run()` and bubbles up to `Application`.) + +**Post-lifecycle, always runs** (even after redirect/JSON, because the abort lands +here): `saveGlobalState()`, then the AJAX payload block (`payload->state = +getGlobalState()`, snippet mode for an invalidated `TextResponse`), flash-session +30-second expiration, a `VoidResponse` fallback, `onShutdown`, `shutdown()`, and +`return $this->response`. + +## The template file is resolved lazily, at `sendTemplate` + +`completeTemplate()` copies the presenter's `#[TemplateVariable]` properties into +the template (see templates.md) and, **only if the template has no file set**, +calls `findTemplateFile()` **then** — so template lookup happens at send time, not +when the template object is created. Changing the view is possible right +up to `render`; after `sendTemplate` throws `AbortException` nothing more can +affect it. + +## Canonicalization uses the `redirectX` mode + +`canonicalize()` (auto-called at step 5 unless `autoCanonicalize` was turned off by +an action switch) returns immediately for AJAX or non-GET/HEAD. Otherwise it builds +the canonical URL with `link(..., 'redirectX')` (see link-generation.md — an +absolute URL with no flash key, used purely for comparison), swallows any +`InvalidLinkException`, and if the current URL differs, redirects — **302 when the +`VARYING` request flag is set, else 301**. That flag is set by the router (external +`nette/routing`); this package only reads it. + +## Signals + +`initGlobalParameters` reads the signal from `_do` (POST) / `do` (the `SignalKey`), +splitting `{receiver}-{signal}` at the **last** `-` (so a receiver id may itself +contain dashes). `processSignal()` resolves the receiver (`''` = the presenter +itself), raising `BadSignalException` if it is missing or not a `SignalReceiver`, +then calls `signalReceived()` → `handle()`. A signal always targets the +current presenter/action; the URL carries it as `?do=-`. diff --git a/docs/internals/link-generation.md b/docs/internals/link-generation.md new file mode 100644 index 000000000..3ada2cd36 --- /dev/null +++ b/docs/internals/link-generation.md @@ -0,0 +1,84 @@ +# Link generation + +`LinkGenerator::createRequest` is the densest, most trap-laden code in +the package. It builds a `Request` from a destination string, in five phases: +(1) signal/component resolution, (2) alias + presenter/action split, (3) signal +argument processing, (4) action argument processing, (5) missing-param check and +assembly. + +## The mode strings — and `redirectX` is real + +The `$mode` string is **not** just `link`/`redirect`/`forward`. There are **five**: + +| mode | URL from `link()`? | flash `_fid`? | notes | +|---|---|---|---| +| `link` | relative (unless `absoluteUrls` or `//` prefix) | no | the normal case | +| `redirect` | absolute | yes* | `Component::redirect` | +| `forward` | — (returns null) | yes* | Request via `$lastRequest`; no `isLinkable` enforcement | +| `test` | — (returns null) | no | `isLinkCurrent`; skips the missing-param check and `isLinkable` enforcement | +| `redirectX` | absolute | no | **canonicalization only** | + +`redirectX` is **not a phantom** — it is live in `Presenter::canonicalize()`. It +behaves like a redirect that yields an **absolute URL with no flash key**, so the +result can be compared byte-for-byte against the current request URL. The +`@param` docblock under-documents it (it lists only `forward|redirect|link`); trust +the code. `*`Flash is added only when the mode is `redirect`/`forward` **and** the +presenter has an active flash session — never for `redirectX` or `link`. + +## The `'#'` argument is reserved for the fragment + +`link()` (not `createRequest`) strips a `'#'` key out of `$args` and appends it as +the URL fragment — rawurlencoded, overriding any `#fragment` in the destination +string (an empty-string value falls back to it); a non-scalar value throws +`InvalidLinkException`. So `'#'` never reaches the `Request`, and no real parameter +may be named `#`. A `?query` inside the destination is deprecated and **replaces** +`$args` entirely (`parseDestination` returns it as `args`). + +## `$lastRequest` is an out-of-band return channel + +`createRequest` returns a URL, but also stores the built `Request` in the public +`@internal $lastRequest` (reset at the start, set at the end). This is how callers +retrieve the request without it being the return value — `getLastCreatedRequest()`, +the `'current'` flag that `isLinkCurrent` reads (even though `test` mode returns +`null`), and `Application::createErrorRequest` after a forward. + +## The 4xx-not-500 contract + +A malformed parameter must degrade to 4xx, not crash to 500, but only for +*untrusted* values. The mechanism is a **subtype**: +`InvalidRequestParameterException extends InvalidLinkException`. + +- `ParameterConverter::toParameters` throws the **subtype only for supplemental + values** — those pulled from the *current request's own params* when the + destination is `this` (`$refPresenter->getParameters()`). Explicit caller-supplied + args throw the plain parent `InvalidLinkException`. +- **`redirect`/`redirectPermanent`/`forward` catch only the subtype** → `error()` + (4xx); **`isLinkCurrent` catches the subtype** → `false`. +- **`link`/`canonicalize` catch the parent** `InvalidLinkException` → the subtype + passes through as before → `#error` / silently abandoned canonical redirect. + +The net effect: a hostile `?param=` re-fed through `redirect('this')` in +`startup()` yields 4xx, while a programmer's typo in an explicit link argument still +surfaces as an exception. The subtype is BC-safe because it passes existing +`catch (InvalidLinkException)` blocks. + +`$invalidLinkMode` (bit flags `Silent`/`Warning`/`Exception`/`Textual`) decides what +`handleInvalidLink` does with an escaped `InvalidLinkException` in the `link` path +(`#`, `E_USER_WARNING`, rethrow, or `#error: `). + +## Two `link()`s + +`Presenter` has **no** `link()` of its own — it inherits `Component::link()`, which +delegates to the presenter's `LinkGenerator` in `link` mode and **wraps exceptions +via `handleInvalidLink` (respecting `$invalidLinkMode`)**. The standalone +`LinkGenerator::link()` (constructed with its own router + reference URL) does +**not** swallow exceptions — it always throws — and is what you use to generate +links outside a running presenter. Its default `$mode = null` also means the URL +is always **absolute** (relativization applies only to an explicit `'link'` mode). + +The reference URL is the HTTP request's URL (`ApplicationExtension` wires +`@Nette\Http\IRequest::getUrl`), and routers read only its host, base path and +path from it, never the script name. Outside an HTTP request (cron, CLI) the +request has no host of its own; nette/http fills it from `http: baseUrl`, so +this package needs no option for that. `withReferenceUrl()` is the per-call +override for multi-domain setups. diff --git a/docs/internals/persistent-state.md b/docs/internals/persistent-state.md new file mode 100644 index 000000000..665b66b32 --- /dev/null +++ b/docs/internals/persistent-state.md @@ -0,0 +1,60 @@ +# Persistent parameters & global state + +Persistent parameters survive across requests by riding in every generated URL. +The mechanism spans the whole component tree and has a strict validation contract. + +## Declaration and validation + +`#[Persistent]` on a **public, non-static** property is discovered by +`ComponentReflection::getParameters()`, which records its type and — for a +presenter — a `since` (the declaring class, used to scope which links may carry it). + +- **`loadState()`** converts each incoming param via + `ParameterConverter::convertType`; a **failure calls `error()` → 404** (a bad URL + value is client error, not a crash). Conversion is **lossless** for scalars (it + compares the round-tripped string), and **`callable` is deliberately rejected** + ("for security reasons"). Null params are ignored (the default stands). +- **`saveState()`/`saveStatePartial()`** re-convert on the way out; a failure here + throws `InvalidLinkException` (link generation, not a request), and a value equal + to its default is dropped (`null`) so it does not bloat the URL. + +## `#[Parameter]` rides the input path only + +`ComponentReflection::getParameters()` returns both `#[Persistent]` and +`#[Parameter]` properties, so `loadState()` fills and type-validates **both** the +same way (a bad value → `error()` → 404). A missing parameter whose property is +still uninitialized is required: non-nullable → `error()` → 404, nullable → `null`. +The property and `$params` entry are bound by reference. The asymmetry is on the way out: only +persistent entries carry a `since` key, and `getPersistentParams()` filters on its +presence — a `#[Parameter]` property is never written back by `saveState` and never +reaches a URL. + +## Global state is aggregated across the whole tree + +`getGlobalState()` builds the full persistent-state map that links carry: + +1. unconsumed URL params in `globalParams` (prefixed by `id . NameSeparator`); +2. the presenter's own `saveStatePartial`; +3. a walk of the **entire `getComponentTree()`** — each `StatePersistent` component + contributes `saveState`, prefixed by its `getUniqueId() . NameSeparator`, while a + `sinces` table records where each key was declared; +4. when a `forClass` is given, keys whose `since` class is not among the target's + classes/traits are filtered out — so a link only carries state the destination + can actually receive. + +**`saveGlobalState()` empties `globalParams` (consuming them) and caches the +result.** It runs at the end of `run()` (always, since the terminating +`AbortException` lands after it) and again inside `Component::redirect()` before the +link is built. Global state reaches the URL because `createRequest` does `$args += +$refPresenter->getGlobalState(...)`. When a component attaches, `popGlobalParameters` +hands it its slice of the URL params so its own `loadState` runs. + +## The `_fid` flash key + +Flash messages survive a redirect via a session section keyed by a short random id +stored in the `FlashKey` (`_fid`) parameter. `hasFlashSession()` is true only when +`_fid` is a non-empty param **and** the session section `Nette.Application.Flash/` +exists. Consequently `_fid` is appended to a link **only in `redirect`/`forward` +mode and only when a flash session is already live** — ordinary links, and +`redirectX`, never carry it. `getFlashSession()` lazily generates the id (storing it +into the presenter's params) the first time a flash is written. diff --git a/docs/internals/readme.md b/docs/internals/readme.md new file mode 100644 index 000000000..0bc0b31a9 --- /dev/null +++ b/docs/internals/readme.md @@ -0,0 +1,21 @@ +# Application internals + +How `nette/application` (the MVC layer) works underneath, for agents editing it. +The emergent models don't share much context, so they're split by seam: + +- **[lifecycle.md](lifecycle.md)** — `Presenter::run()`, the exact phase order, + `AbortException`, signal dispatch, and canonicalization. +- **[link-generation.md](link-generation.md)** — `LinkGenerator::createRequest`, + the mode strings (including the real `redirectX`), destination parsing, + `$lastRequest`, and the 4xx-not-500 contract. The densest, trickiest code. +- **[persistent-state.md](persistent-state.md)** — persistent parameters, + `loadState`/`saveState`, whole-tree global-state aggregation, and the `_fid` + flash key. +- **[access-control.md](access-control.md)** — `AccessPolicy`, `#[Requires]`, the + three enforcement sites, and the implicit same-origin (CSRF) rule for signals. +- **[snippets.md](snippets.md)** — AJAX partial rendering: invalidation flags, the + snippet-mode render, and the payload contract. +- **[routing.md](routing.md)** — the Application `Route`/`RouteList` subclasses, + the `Nette:Micro` firewall exception, and `PresenterFactory`. +- **[templates.md](templates.md)** — template/layout file lookup and typed + templates. diff --git a/docs/internals/routing.md b/docs/internals/routing.md new file mode 100644 index 000000000..82b3f0456 --- /dev/null +++ b/docs/internals/routing.md @@ -0,0 +1,62 @@ +# Routing (Application layer) + +`nette/application` subclasses the `nette/routing` engine to add presenter/action/ +module conventions. The heavy lifting (mask parsing, the generation index / +`warmupCache`, nested-list `getConstantParameters` aggregation) lives in the base +package — see `nette/routing`'s internals. What is **here**: + +## `Application\Routers\Route` (over the base `Route`) + +- **`UIMeta` defaults** — injects `module`/`presenter`/`action` metadata with + kebab-case `FilterIn`/`FilterOut` (merged into `defaultMeta` before the parent + constructor). This is where a URL segment like `product-edit` becomes the + `ProductEdit` presenter. +- **kebab-case inflectors** — `action2path`/`path2action` (camelCase ↔ dashes) and + `presenter2path`/`path2presenter` (`Foo:Bar` ↔ `foo.bar`, `:` ↔ `.`). +- **Closure metadata → `Nette:Micro`** — a `\Closure` target is rewritten to + `['presenter' => 'Nette:Micro', 'callback' => $closure]`. This is the closure-route + mechanism, and the reason `Nette:Micro` must stay reachable (below). +- **`` handling** — after a base match, a `module` mask parameter is + prepended to the presenter (`module:Presenter`) and unset; `constructUrl` splits it + back out; `getConstantParameters` re-joins `module:presenter`. + +## `RouteList::completeParameters` and module scoping + +The Application `RouteList` adds module scoping on match: `completeParameters` +**prepends the list's `module` to the matched presenter — unless it starts with +`Nette:`**. `constructUrl` is the mirror: with a `module` set, the target presenter +must start with that prefix (else `null` — not this list's route), and the prefix +is stripped before delegating down; a `Nette:` presenter is passed down unchanged. `withModule($m)` creates a nested list whose +`module` is `$m.':'` (a whole group under a module), which is distinct from the +per-route `` mask parameter above. `warmupCache` is *invoked* from `RoutingExtension::afterCompile` +(only when `routing: cache`), but is defined in the base package. + +## The `Nette:Micro` firewall exception + +`Application::createInitialRequest` **rejects any incoming request to a `Nette:*` +presenter except `Nette:Micro`**: `str_starts_with($presenter, 'Nette:') && +$presenter !== 'Nette:Micro'` → `BadRequestException`. Framework-internal presenters +must not be directly reachable from the network, but `Nette:Micro` is the legitimate +target that closure routes produce (mapped to `NetteModule\MicroPresenter`), so it +is the single allowed exception. The `Nette:`-prefix skip in `completeParameters` +and `constructUrl` exists for the same reason — module scoping must not be applied +to it. + +## Request loop and error routing + +`processRequest` has a `maxLoop` (default 20) guard, rejects direct requests **to** +the error presenters (only a forward may reach them), and turns an +`InvalidPresenterException` into a `BadRequestException` only on the first request. +`createErrorRequest` routes a `BadRequestException` to `error4xxPresenter ?? +errorPresenter` and anything else to `errorPresenter` (5xx); `sendHttpCode` maps a +`BadRequestException` to its HTTP code (else 404) and everything else to 500. + +## `PresenterFactory` + +`formatPresenterClass` maps a presenter name to a class through the mapping masks +(the hard default is `App\Presentation\*\**Presenter`; `setMapping` overrides), by +substituting `**`→`Part\Part` and `*`→`Part` per `:`-separated segment. A mapping +key may name a submodule (`Admin:Forum`); the longest matching module prefix wins. Names are +charset-validated, the class is checked for `IPresenter`/non-abstract, and resolved +classes are cached. Aliases are resolved in the **link** layer (the `@alias` +destination), not in routing. diff --git a/docs/internals/snippets.md b/docs/internals/snippets.md new file mode 100644 index 000000000..b15385af6 --- /dev/null +++ b/docs/internals/snippets.md @@ -0,0 +1,46 @@ +# Snippets & AJAX partial rendering + +The snippet mechanism is emergent across three files: `Control` (invalidation +flags), the post-lifecycle block in `Presenter::run()` (the re-send), and +`SnippetRuntime` in the Latte bridge (the capture). None of them shows the whole +picture. + +## The pipeline + +1. During the lifecycle, `redrawControl()` marks a control (key `"\0"`) or a named + snippet as invalid. +2. After the terminating `AbortException` lands in `run()`, the AJAX block sets + `payload->state`, and — when the response is a `TextResponse` and something is + invalid — flips `snippetMode = true` and sends the `TextResponse` **right + there**. Since `sendTemplate` only stores the response and aborts, this is the + template's **only** render, and it happens in snippet mode. Then `sendPayload()` + replaces `$this->response` with a `JsonResponse` of the payload — that is what + the presenter returns to `Application`. +3. Every compiled template starts with a prolog injected by + `UIExtension::snippetRenderingPass`: it calls + `SnippetRuntime::renderSnippets(...)` and **returns early when it rendered** — + in snippet mode the main template body never runs. +4. `renderSnippets` renders only the invalid snippet blocks (into + `payload->snippets[]`, captured via output buffering in `enter`/`leave`), + then `renderChildren` walks the component tree and recursively renders every + invalid child `Control` with its `snippetMode` on. + +## Invariants & traps + +- **Invalidation propagates upward:** `isControlInvalid()` with no argument walks + the whole `Renderable` subtree — an invalid child makes the parent report + invalid. A named query falls back to the whole-control `"\0"` flag. +- **Rendering a snippet clears its flag:** `SnippetRuntime::enter()` calls + `redrawControl($name, redraw: false)`. Re-rendering the same snippet twice in one + request therefore captures it once. +- **Dynamic snippets are valid only inside a static `{snippet}`/`{snippetArea}`** + (`E_USER_WARNING` otherwise). A `snippetArea` only relays: its own wrapper output + is discarded, only inner snippets are captured. +- **The payload contract** (what nette/naja-style clients consume): `snippets`, + `state` (always set on AJAX — the global persistent state, see + persistent-state.md), and `redirect` — on AJAX, `redirectUrl()` does not send an + HTTP redirect but puts the URL into `payload->redirect` and sends the payload. +- Snippet element ids are `snippet--` (`getSnippetId`). +- `{snippet}`/`{snippetArea}` compile to `enter`/`leave` calls + (`Nodes/SnippetNode.php`); the driver instance comes from the `snippetDriver` + provider and exists only when the template has a control. diff --git a/docs/internals/templates.md b/docs/internals/templates.md new file mode 100644 index 000000000..31e247838 --- /dev/null +++ b/docs/internals/templates.md @@ -0,0 +1,62 @@ +# Template & layout lookup + +Templates are found by convention; the candidate paths and their order are the only +non-obvious part. + +## Action template candidates (`formatTemplateFiles`) + +Starting from the presenter's own directory: + +- **If there is no `templates/` subdirectory**, the lookup climbs one level; if + there is still none, the single candidate is `"/.latte"` (the + "template next to the presenter" layout). +- **If a `templates/` subdirectory exists**, two candidates are tried, in order: + 1. `"/templates//.latte"` + 2. `"/templates/..latte"` + +`findTemplateFile()` returns the first that `is_file`, else calls `error()` → **404 +"Missing template"**. Remember (see lifecycle.md) this resolution happens **lazily +at `sendTemplate`**, via `completeTemplate`, only when the template has no explicit +file. + +## Layout candidates (`formatLayoutTemplateFiles`) + +- A `$layout` containing a slash (or backslash) is taken as a direct path. +- Otherwise the base name is `` (default `layout`). With a `templates/` + directory the candidates are `templates//@.latte` and + `templates/.@.latte` (once, at the presenter's level only), + then the shared `templates/@.latte` repeated **up the module levels** + (`substr_count(name, ':')` levels of `dirname`). Without a `templates/` + directory, `@.latte` next to the presenter, then walking up the same way. +- `setLayout(false)` disables the layout entirely (returns `null`); an explicitly + named layout that is not found throws `FileNotFoundException`, while the default + layout being absent is simply `null` (optional). + +## Typed templates + +`formatTemplateClass` derives the template class from the presenter/control name: +for a presenter it tries `Template` then `Template` (stripping +the `Presenter` suffix); for a control, `Template` (stripping `Control`). The +class is validated (`is_a(..., Template::class)`) or a `trigger_error` degrades to +the default. `TemplateFactory::createTemplate` instantiates it around a fresh Latte +engine and injects the default variables (`user`, `baseUrl`, `basePath`, `flashes`, +`control`, `presenter`). + +**`flashes` depends on this injection.** `Control::flashMessage()` writes the +message to the flash session and into the template only if the template already +exists; it never creates one. A template created later gets the messages solely +from the factory, which reads them from the flash session, so a custom +`TemplateFactory` has to inject `flashes` the same way. + +## `#[TemplateVariable]` + +At `sendTemplate`, `completeTemplate()` copies every `#[TemplateVariable]` property +of the presenter into the template via `$template->$name ??= $this->$name` — an +explicitly assigned template variable wins. The property must be **public** +(`LogicException` otherwise); uninitialized properties are skipped (unless they +have a get hook, PHP 8.4+). Presenter-only — plain controls don't run +`completeTemplate`. + +The `{templateType}` Latte tag is **not** in this package — it is implemented in +Latte core; here the typing is only the name-derived class plus the +`@property-read Template $template` docblock. diff --git a/docs/upgrading.md b/docs/upgrading.md new file mode 100644 index 000000000..d5768adb9 --- /dev/null +++ b/docs/upgrading.md @@ -0,0 +1,371 @@ +# Upgrading nette/application + +## 3.3.0 + +- **Removed** `Nette\Bridges\ApplicationLatte\UIMacros`: Latte 2 is no longer supported, the tags are provided by `Nette\Bridges\ApplicationLatte\UIExtension` under Latte 3 (92532c24) +- **Removed** `{ifCurrent}` (deprecated since 3.1.6): use `{if isLinkCurrent('destination')}` (d75c3057) +- **Renamed** `Nette\Application\UI\PresenterComponent` → `Nette\Application\UI\Component`; the old name was removed (3432ff34) +- **Renamed** `Nette\Application\UI\PresenterComponentReflection` → `Nette\Application\UI\ComponentReflection`; the old name was removed (3432ff34) +- **Renamed** `Nette\Application\IRouter` → `Nette\Routing\Router` (alias since 3.1.0); the old name was removed (3432ff34) +- **Renamed** `@persistent` → `#[Nette\Application\Attributes\Persistent]`; using the old name triggers E_USER_DEPRECATED (2b611739) +- **Renamed** `@deprecated` → `#[Nette\Application\Attributes\Deprecated]`; using the old name triggers E_USER_DEPRECATED (2b611739) +- **Replaced** `@crossOrigin` with `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`: using the annotation triggers E_USER_DEPRECATED (2b611739) +- **Replaced** `Nette\Application\Routers\RouteList::offsetSet()` with `Nette\Application\Routers\RouteList::addRoute()` for a route and `Nette\Application\Routers\RouteList::add()` for any other router: appending via `$routeList[] =` triggers E_USER_DEPRECATED; replacing the router at an index via `$routeList[$index] =` has no public replacement, `Nette\Routing\RouteList::modify()` being protected, so add the routers in the order they are to be matched (82c16085) +- **Replaced** `Nette\Application\Routers\RouteList::offsetGet()` with `Nette\Application\Routers\RouteList::getRouters()`: reading via `$routeList[$index]` triggers E_USER_DEPRECATED (82c16085) +- **Deprecated** `Nette\Application\Routers\RouteList::offsetUnset()`: `unset($routeList[$index])` triggers E_USER_DEPRECATED and has no public replacement, `Nette\Routing\RouteList::modify()`, which the deprecation notice names, being protected; leave the router out when the list is built, or call `modify($index, null)` from a subclass (82c16085) +- **Deprecated** `Nette\Application\Routers\RouteList::offsetExists()`: check `Nette\Application\Routers\RouteList::getRouters()` instead; using the old name triggers E_USER_DEPRECATED (82c16085) +- **Deprecated** `Nette\Application\UI\Component::$presenter`: use `Nette\Application\UI\Component::getPresenter()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Component::$linkCurrent`: use `Nette\Application\UI\Component::isLinkCurrent()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$action`: use `Nette\Application\UI\Presenter::getAction()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$view`: use `Nette\Application\UI\Presenter::getView()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$layout`: use `Nette\Application\UI\Presenter::getLayout()` and `Nette\Application\UI\Presenter::setLayout()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$request`: use `Nette\Application\UI\Presenter::getRequest()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\UI\Presenter::$session`: use `Nette\Application\UI\Presenter::getSession()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$method`: use `Nette\Application\Request::getMethod()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$presenterName`: use `Nette\Application\Request::getPresenterName()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$parameters`: use `Nette\Application\Request::getParameters()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$post`: use `Nette\Application\Request::getPost()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `Nette\Application\Request::$files`: use `Nette\Application\Request::getFiles()`; using the old name triggers E_USER_DEPRECATED (69bca2f1) +- **Deprecated** `$default` of `Nette\Application\UI\Component::getParameter()`: use `getParameter($name) ?? $default`; passing it triggers E_USER_DEPRECATED (6a10e27a) +- **Deprecated** a query string in a link destination, e.g. `link('Presenter:action?foo=bar')`: pass the parameters as arguments, e.g. `link('Presenter:action', ['foo' => 'bar'])`; using the old form triggers E_USER_DEPRECATED (b6da1870) +- **Deprecated** `Nette\Application\UI\Control::templatePrepareFilters()` (silently deprecated since 3.1.8): it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Form::disableSameSiteProtection()` (silently deprecated since 3.0.8): use `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::createRequest()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::parseDestination()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator::parseDestination()`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Deprecated** `Nette\Application\UI\Presenter::requestToUrl()` (deprecated since 3.2.3): use `Nette\Application\LinkGenerator::requestToUrl()`; it is marked `#[\Deprecated]`, so using it triggers a deprecation notice on PHP 8.4+ (5555cd9d) +- **Signature** `Nette\Application\Application::processRequest()` returns `Nette\Application\Response` instead of `void` and no longer sends it; the caller must call `send()` on the returned response (e0192d23) +- **Signature** `Nette\Bridges\ApplicationLatte\LatteFactory::create()` takes `?Nette\Application\UI\Control $control = null`; a custom implementation must accept it and pass it to `Nette\Bridges\ApplicationLatte\UIExtension` (bed96a1c) +- **Signature** `Nette\Application\UI\Presenter::getRequest()` returns `Nette\Application\Request` instead of `?Nette\Application\Request` and throws `Nette\InvalidStateException` when the presenter is not running (35c9c679) +- **Signature** `$signal` of `Nette\Application\UI\Presenter::isSignalReceiver()` is `string|true|null`; passing `false` now throws a `TypeError` (230381ee) +- **Signature** `$name` of `Nette\Bridges\ApplicationLatte\Template::addFilter()` is no longer nullable (fe26ff9b) +- **Signature** `$cacheStorage` of `Nette\Bridges\ApplicationLatte\TemplateFactory::__construct()` was removed; the Latte cache is set up by the caching extension instead (92532c24) +- **Changed** the default presenter mapping is `App\Presentation\*\**Presenter`; previously `*Module\*Presenter`; set `application.mapping` explicitly to keep the previous structure (30d82cfc) +- **Changed** `Nette\Application\LinkGenerator::link()` takes the `#` key of the arguments as the URL fragment; previously it was passed on as an ordinary parameter, and a non-scalar value now throws `Nette\Application\UI\InvalidLinkException` (e3dabf20) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory::createTemplate()` no longer adds `Nette\Bridges\ApplicationLatte\UIExtension` when the Latte factory did not; a custom `Nette\Bridges\ApplicationLatte\LatteFactory` must register it itself (bed96a1c) +- **Changed** a presenter that is not registered as a service in the DI container is no longer created dynamically; in development mode with `touchToRefresh` the page is refreshed, otherwise `Nette\Application\InvalidPresenterException` is thrown (4abfb482) +- **Changed** snippet redrawing recognizes only descendants of `Nette\Application\UI\Control`; a component implementing merely `Nette\Application\UI\Renderable` is no longer redrawn (fe26ff9b) +- **Config** `latte.macros`: removed, register Latte 3 extensions under `latte.extensions` (92532c24) + +## 3.2.12 + +- **Changed** `Nette\Application\UI\Component::redirect()` calls `error()` (4xx) when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously `Nette\Application\UI\InvalidLinkException` propagated and ended as a 500 (8d393b85) +- **Changed** `Nette\Application\UI\Component::isLinkCurrent()` returns false when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously the exception propagated (8d393b85) +- **Changed** `Nette\Application\UI\Presenter::forward()` calls `error()` (4xx) when a value carried over from the current request's own persisted parameters cannot be coerced to the target action's parameter type; previously the exception propagated and ended as a 500 (8d393b85) + +## 3.2.10 + +- **Deprecated** `Nette\Application\UI\Component::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Deprecated** `Nette\Application\UI\Form::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Deprecated** `Nette\Application\UI\Presenter::getPresenterIfExists()`: use `getPresenter(throw: false)`; the old name is silently deprecated (893fdcc7) +- **Signature** the `$throw` parameter is back in `Nette\Application\UI\Component::getPresenter()`, `Nette\Application\UI\Form::getPresenter()` and `Nette\Application\UI\Presenter::getPresenter()` and passing it no longer triggers E_USER_DEPRECATED (893fdcc7) +- **Signature** `$contentType` of `Nette\Application\Responses\FileResponse::__construct()` is `string` with the default `'application/octet-stream'`; passing `null` now throws a `TypeError` (dced88ca) +- **Signature** `$contentType` of `Nette\Application\Responses\JsonResponse::__construct()` is `string` with the default `'application/json'`; passing `null` now throws a `TypeError` (dced88ca) +- **Signature** `NetteModule\MicroPresenter::error()` returns `never` instead of `void` (f3f28851) +- **Changed** `{layout auto}` in a referring template no longer calls `Nette\Application\UI\Presenter::findLayoutTemplateFile()` and resolves to no layout there (cdf59b0a) + +## 3.2.9 + +- **Removed** `Nette\Bridges\ApplicationLatte\Template::blueprint()` (0044598c) +- **Deprecated** `Nette\Application\Attributes\CrossOrigin`: use `#[Nette\Application\Attributes\Requires(sameOrigin: false)]`; the old name is silently deprecated (75fb3f4d) +- **Changed** `Nette\Application\UI\Component::link()` throws `Nette\Application\UI\InvalidLinkException` when the target is marked `#[Nette\Application\Attributes\Requires(forward: true)]`, unless the link is built while forwarding (1b0aa552) + +## 3.2.8 + +- **Changed** `Nette\Application\UI\Presenter::$invalidLinkMode` configured in the DI container is now really applied to presenters created outside it; previously it stayed silent, so warnings or exceptions for invalid links may newly appear (74e5d82f) +- **Changed** `Nette\Application\LinkGenerator::createRequest()` no longer passes parameters marked `#[Nette\Application\Attributes\Parameter]` whose value equals the default (56986e64) + +## 3.2.7 + +- **Changed** `Nette\Bridges\ApplicationLatte\Template` is abstract; instantiate `Nette\Bridges\ApplicationLatte\DefaultTemplate` or another subclass (8f567030) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory::createTemplate()` silently skips a default variable (user, baseUrl, basePath, flashes, control, presenter) whose type does not match the template property; previously it failed with a `TypeError` (edf6c279) + +## 3.2.4 + +- **Signature** `Nette\Application\UI\Form::beforeRender()` returns `void`; affects subclasses overriding it (c0d041fe) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Deprecated]` is used on a method other than action, render or handle (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Parameter]` or `#[Nette\Application\Attributes\Persistent]` is used on a property that is not public, non-static and non-readonly (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when `#[Nette\Application\Attributes\Requires]` is used on a method other than action, render, handle or createComponent (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when an action, render or signal method of a presenter is not public and non-static (630ebb14) +- **Changed** container compilation throws `Nette\InvalidStateException` when a `createComponent()` method is private or static (630ebb14) + +## 3.2.3 + +- **Replaced** `Nette\Application\UI\Presenter::createRequest()` with `Nette\Application\LinkGenerator::createRequest()`: link building moved to the `Nette\Application\LinkGenerator` service, reachable via `Nette\Application\UI\Presenter::getLinkGenerator()`, so overriding the presenter method no longer affects generated links (850ab855) +- **Deprecated** `Nette\Application\UI\Presenter::$allowedMethods`: use `#[Nette\Application\Attributes\Requires(methods: [...])]` on the presenter class or on the action; the property is silently deprecated (368ae99f) +- **Deprecated** `Nette\Application\UI\Presenter::checkHttpMethod()`: use `#[Nette\Application\Attributes\Requires(methods: [...])]`; the method is silently deprecated (368ae99f) +- **Signature** `$element` of `Nette\Application\UI\Component::checkRequirements()` is typed `\ReflectionClass|\ReflectionMethod`; affects subclasses overriding it (7af4f067) +- **Signature** `Nette\Application\LinkGenerator::link()` is `link(string $destination, array $args = [], ?Nette\Application\UI\Component $component = null, ?string $mode = null): ?string`; the return type is nullable and the first parameter was renamed from `$dest` to `$destination` (51584954) +- **Signature** `$reflection` of `Nette\Application\UI\Presenter::saveState()` was removed; a second argument is now silently ignored (b644c0d4) +- **Signature** `Nette\Application\Attributes\Parameter` and `Nette\Application\Attributes\Persistent` are final and can no longer be extended (5d5f8d18) +- **Changed** `Nette\Application\UI\Component::checkRequirements()` no longer performs the same-origin check for signals; the check runs before it is called, so an override that does not call the parent no longer bypasses it (7af4f067) +- **Changed** `Nette\Application\UI\Component::redirectPermanent()` no longer saves the global state of subcomponents before redirecting; only `Nette\Application\UI\Component::redirect()` does (bb652c12) +- **Changed** `Nette\Application\LinkGenerator::link()` resolves the target action or render method only when it is callable (public, non-static, non-abstract); previously any existing method matched (b309aa94) +- **Changed** `Nette\Application\UI\Presenter::formatActionMethod()` returns `'actionDefault'`; previously `'actiondefault'` (def38b35) +- **Changed** `Nette\Application\UI\Presenter::formatRenderMethod()` returns `'renderDefault'`; previously `'renderdefault'` (def38b35) +- **Changed** `Nette\Application\UI\Presenter::getPersistentComponents()` returns an empty array by default; names declared by the attribute are merged elsewhere, so an override no longer has to call the parent to keep them (d95272f3) +- **Changed** `{templatePrint}` requires latte/latte 3.0.15 or newer and throws `\LogicException` otherwise (7f094568) +- **Changed** `|translate` used without `Nette\Bridges\ApplicationLatte\Template::setTranslator()` ends with Latte's undefined-filter compile error; previously `Nette\InvalidStateException` at render time (c86bdbef) +- **Changed** `latte.extensions`: the cache and forms extensions are registered before the user-configured ones; previously afterwards, which changes precedence of tags and filters defined under the same name by more than one extension (3a4f4017) +- **Config** `application.catchExceptions`: the value `'4xx'` introduced in 3.2.1 was removed, only `true` and `false` are accepted (56be3de3) + +## 3.2.1 + +- **Replaced** `Nette\Application\Application::processException()` with `Nette\Application\Application::createErrorRequest()`: the new method returns a `Nette\Application\Request` (or null) that the caller must pass to `Nette\Application\Application::processRequest()` and no longer sends the response itself (18a38043) +- **Deprecated** `Nette\Application\Application::$catchExceptions`: exceptions are caught whenever `Nette\Application\Application::$errorPresenter` is set; the property is silently deprecated (133d0e7f) +- **Changed** `Nette\Application\Application::$catchExceptions` defaults to `true`; previously `false` (133d0e7f) +- **Changed** `Nette\Application\UI\Presenter::injectPrimary()` called a second time throws a native `\Error` because the injected properties are readonly; previously `Nette\InvalidStateException` (bb59a51b) +- **Config** `application.catchExceptions` also accepts the string `'4xx'` and its default changed to `'4xx'`; previously `false` (d16847f4) + +## 3.2.0 + +- **Removed** `Nette\Application\UI\Presenter::getContext()` (deprecated since 3.1.0): inject the services you need explicitly (75912403) +- **Removed** `Nette\Application\Request::SECURED` (573cd9b9) +- **Removed** `Nette\Application\Routers\Route::getFlags()` (deprecated since 3.1.0) (75912403) +- **Removed** `Nette\Application\Routers\SimpleRouter::getFlags()` (deprecated since 3.1.0) (75912403) +- **Removed** `Nette\Application\Routers\RouteList::count()` (deprecated since 3.1.0): the class no longer implements `Countable`, count `Nette\Application\Routers\RouteList::getRouters()` instead (75912403) +- **Replaced** `Nette\Application\Routers\RouteList::getIterator()` with `Nette\Application\Routers\RouteList::getRouters()`: the class no longer implements `IteratorAggregate`, iterate over the returned array (75912403) +- **Replaced** `$flags` of `Nette\Application\Routers\Route::__construct()` with `$oneWay` of `Nette\Application\Routers\RouteList::addRoute()` (75912403) +- **Replaced** `$flags` of `Nette\Application\Routers\SimpleRouter::__construct()` with the second argument of `Nette\Application\Routers\RouteList::add()` (75912403) +- **Signature** `Nette\Application\UI\Presenter::injectPrimary()` no longer takes `Nette\DI\Container` and the parameter order is now httpRequest, httpResponse, presenterFactory, router, session, user, templateFactory; affects positional calls (bb8f93c6) +- **Signature** `Nette\Application\UI\Control::createTemplate()` and `Nette\Application\UI\Presenter::createTemplate()` take `?string $class = null`; affects subclasses overriding them (e2c2e74b) +- **Signature** property types were added: `Nette\Application\Application::$catchExceptions` is `bool` with default false, `Nette\Application\UI\Control::$snippetMode` is `bool` with default false and `Nette\Application\UI\Presenter::$invalidLinkMode` is `int` with default 0; assigning `null` throws a `TypeError` and an unset value can no longer be recognized by a null check (244fc48a) +- **Signature** `Nette\Bridges\ApplicationLatte\DefaultTemplate::$presenter` is typed `Nette\Application\IPresenter` instead of `Nette\Application\UI\Presenter` (244fc48a) +- **Signature** the third parameter of `Nette\Application\Routers\RouteList::addRoute()` was renamed from `$flags` to `$oneWay` and typed `int|bool`; affects calls using named arguments (b3d618a0) +- **Changed** the `Nette\SmartObject` trait was removed from `Nette\Application\Application`, `Nette\Application\PresenterFactory`, `Nette\Application\LinkGenerator`, `Nette\Application\UI\Link`, all classes in `Nette\Application\Responses`, `Nette\Application\Routers\CliRouter`, `Nette\Bridges\ApplicationLatte\TemplateFactory`, `Nette\Bridges\ApplicationTracy\RoutingPanel`, `NetteModule\ErrorPresenter` and `NetteModule\MicroPresenter`; reading an undefined property or calling an undefined method raises a native PHP error instead of `Nette\MemberAccessException`, and writing an undefined property is no longer blocked (a472b2e0) +- **Changed** `Nette\Application\UI\Form` whose action was changed by `setAction()` no longer adds the hidden signal field in `beforeRender()`, so the signal submit handler is not triggered for such forms (8d8313a3) +- **Changed** `Nette\Application\Routers\SimpleRouter::__construct()` no longer throws `Nette\DeprecatedException` for a `module` key in `$defaults`; the key is accepted as an ordinary default, use `Nette\Application\Routers\RouteList::withModule()` to scope a module (75912403) +- **Config** `application.catchExceptions` defaults to `false`; previously `null` (244fc48a) +- **Config** `latte.xhtml`: removed, using it throws an exception (75912403) +- **Config** `routing.routeClass`: removed, using it throws an exception (75912403) + +## 3.1.15 + +- **Changed** `Nette\Application\AbortException` extends `\LogicException` instead of `\Exception`; affects code catching it (00da68b6) +- **Changed** `Nette\Application\BadRequestException` extends `\LogicException` instead of `\Exception`; affects code catching it (00da68b6) +- **Changed** `Nette\Application\UI\Component::tryCall()` reports a handler that exists but is not callable through `error()`, i.e. `Nette\Application\BadRequestException` (404); previously `Nette\InvalidStateException`, also in 3.2.4 (dafa250c) + +## 3.1.14 + +- **Replaced** `Nette\Application\Routers\RouteList::match()` with `Nette\Application\Routers\RouteList::completeParameters()`: a subclass that overrode match() to complete the matched parameters now implements `protected function completeParameters(array $params): ?array`, which the parent class calls (7f871545) +- **Changed** `NetteModule\MicroPresenter::run()` autowires only callback parameters typed with a class or interface; a parameter with a builtin type keeps the value coming from the request instead of being overwritten by the container lookup (c634aabe) + +## 3.1.13 + +- **Changed** `Nette\Application\UI\Presenter::run()` rejects an HTTP method not listed in the new `Nette\Application\UI\Presenter::$allowedMethods` (GET, POST, HEAD, PUT, DELETE) with a 405 error; set the property to allow other methods (83f51448) + +## 3.1.11 + +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_SILENT` → `Nette\Application\UI\Presenter::InvalidLinkSilent`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_WARNING` → `Nette\Application\UI\Presenter::InvalidLinkWarning`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_EXCEPTION` → `Nette\Application\UI\Presenter::InvalidLinkException`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::INVALID_LINK_TEXTUAL` → `Nette\Application\UI\Presenter::InvalidLinkTextual`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::PRESENTER_KEY` → `Nette\Application\UI\Presenter::PresenterKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::SIGNAL_KEY` → `Nette\Application\UI\Presenter::SignalKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::ACTION_KEY` → `Nette\Application\UI\Presenter::ActionKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::FLASH_KEY` → `Nette\Application\UI\Presenter::FlashKey`; the old name still works as an alias (73391b9e) +- **Renamed** `Nette\Application\UI\Presenter::DEFAULT_ACTION` → `Nette\Application\UI\Presenter::DefaultAction`; the old name still works as an alias (73391b9e) +- **Changed** an action, render or signal method must be public; a protected one throws `Nette\InvalidStateException` instead of being invoked as it was since 3.1.2 (3137e5df) + +## 3.1.10 + +- **Changed** `Nette\Application\UI\Presenter::detectedCsrf()` converts `Nette\Application\UI\InvalidLinkException` raised for an invalid current link into `Nette\Application\BadRequestException`; previously the InvalidLinkException propagated (f2a72dc8) + +## 3.1.8 + +- **Deprecated** `Nette\Application\UI\Control::templatePrepareFilters()`: the method is silently deprecated (65f01cc8) +- **Signature** `$default` of `Nette\Application\UI\Component::getParameter()` was removed from the signature; a second argument still works through `func_get_arg()` but should no longer be passed (83212fd2) + +## 3.1.7 + +- **Renamed** `Nette\Bridges\ApplicationLatte\TranslatorExtension` → `Latte\Essential\TranslatorExtension`; the old name was removed (a831a22c) + +## 3.1.6 + +- **Replaced** `{ifCurrent destination}` with `{if isLinkCurrent('destination')}`: under Latte 3 the tag triggers E_USER_DEPRECATED (2bfc034d) +- **Changed** a modifier other than `|noescape` in `{control}` is a compile exception under Latte 3; under Latte 2 it only triggered E_USER_DEPRECATED (2bfc034d) +- **Config** `latte.macros` has no effect under Latte 3, register extensions under `latte.extensions` (2bfc034d) + +## 3.1.5 + +- **Signature** `Nette\Application\UI\Component::link()`, `lazyLink()`, `redirect()`, `redirectPermanent()` and `isLinkCurrent()` take `$args = []` again instead of the variadic `...$args` introduced in 3.1.4 (b89b3d6a) + +## 3.1.4 + +- **Replaced** `{templatePrint default}` with `{templatePrint Nette\Bridges\ApplicationLatte\DefaultTemplate}`: the special `default` keyword was removed (1256ecf7) +- **Signature** `Nette\Application\UI\Component::link()`, `lazyLink()`, `redirect()`, `redirectPermanent()` and `isLinkCurrent()` take a variadic `...$args` instead of `$args = []` (a987d588) +- **Signature** `Nette\Application\UI\Presenter::canonicalize()` takes a variadic `...$args` instead of `array $args = []` (ce7df20e) +- **Changed** generating a link to a presenter, an action or a signal handler annotated `@deprecated` triggers E_USER_DEPRECATED when invalidLinkMode is enabled; previously it was silent (4422ae62) +- **Changed** `Nette\Application\UI\Presenter::run()` saves the global state and the AJAX payload state even when the request ends early through `forward()`, `redirect()` or `sendResponse()`; previously only after a normal render, so persistent parameters were not saved on early termination (db6eedc1) + +## 3.1.2 + +- **Changed** a protected action, render or signal method is invoked like a public one; previously it was silently treated as not found (06823f59) +- **Changed** a private action, render or signal method makes `Nette\Application\UI\Component::tryCall()` throw `Nette\InvalidStateException`; previously it was silently treated as not found (06823f59) +- **Changed** `Nette\Application\UI\Control::formatTemplateClass()` strips only a trailing "Control" from the class name and returns a name even for a class not ending with it; previously it also stripped "Presenter" and returned null in that case (0a4aeabc) +- **Changed** `Nette\Application\UI\Presenter::formatTemplateClass()` looks for a per-action template class (`FooBarTemplate` for action bar of `FooPresenter`) before falling back to the default one (55c9d45c, 0a4aeabc) + +## 3.1.1 + +- **Deprecated** `latte.xhtml`: using the key triggers E_USER_DEPRECATED (d6989a2e) +- **Changed** `Nette\Application\UI\Component::createComponent()` triggers E_USER_NOTICE when the created component implements neither `Nette\Application\UI\SignalReceiver` nor `Nette\Application\UI\StatePersistent` (d07e7fb3) +- **Changed** a custom template class (`latte.templateClass` or the constructor argument of `Nette\Bridges\ApplicationLatte\TemplateFactory`) must extend `Nette\Bridges\ApplicationLatte\Template`; merely implementing `Nette\Application\UI\Template` makes the factory throw `Nette\InvalidArgumentException` (4b4f2866) +- **Changed** `{control}` outside HTML text, e.g. inside an attribute, is allowed and the component output is escaped for that context; previously it triggered E_USER_WARNING (6d81a0e4) + +## 3.1.0 + +- **Removed** `Nette\Application\Routers\Route::$styles` (deprecated since 3.0.0) (9a161a29) +- **Removed** the `@User(loggedIn)` annotation on presenter action and render methods (deprecated since 3.0.0): check the user in `Nette\Application\UI\Presenter::checkRequirements()` instead (9a161a29) +- **Removed** `|null` (deprecated since 3.0.0) (9a161a29) +- **Removed** a modifier in `{templatePrint}`: using one now throws a compile exception (09ac4e3e) +- **Renamed** `|url` → `|escapeUrl`; the old name was removed (9a161a29) +- **Renamed** `Nette\Application\IRouter` → `Nette\Routing\Router` (silently deprecated since 3.0.0); the old name still works as an alias (d200f234) +- **Renamed** `Nette\Application\IResponse` → `Nette\Application\Response`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\IRenderable` → `Nette\Application\UI\Renderable`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ISignalReceiver` → `Nette\Application\UI\SignalReceiver`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\IStatePersistent` → `Nette\Application\UI\StatePersistent`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ITemplate` → `Nette\Application\UI\Template`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Application\UI\ITemplateFactory` → `Nette\Application\UI\TemplateFactory`; the old name still works as an alias (8ffa8fcc) +- **Renamed** `Nette\Bridges\ApplicationLatte\ILatteFactory` → `Nette\Bridges\ApplicationLatte\LatteFactory`; the old name still works as an alias (8ffa8fcc) +- **Replaced** `Nette\Application\UI\Component::getParam()` (deprecated since 3.0.0) with `Nette\Application\UI\Component::getParameters()` when called without an argument and with `Nette\Application\UI\Component::getParameter()` when called with one (9a161a29) +- **Replaced** the numeric `$code` first argument of `Nette\Application\UI\Component::redirect()` (deprecated since 3.0.0) with `Nette\Application\UI\Component::redirectPermanent()` for 301; other codes have no replacement (9a161a29) +- **Replaced** the `module` key in `$defaults` of `Nette\Application\Routers\SimpleRouter::__construct()` with `Nette\Application\Routers\RouteList::withModule()`: passing it now throws `Nette\DeprecatedException` (9a161a29) +- **Replaced** `|normalize` with `Nette\Utils\Strings::normalize()` applied to the value (9a161a29) +- **Replaced** `|toAscii` with `Nette\Utils\Strings::toAscii()` applied to the value (9a161a29) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::add()` with `Nette\Bridges\ApplicationLatte\DefaultTemplate::add()`: the method moved to the subclass, extend DefaultTemplate if you need it (34c4a715) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::setParameters()` with `Nette\Bridges\ApplicationLatte\DefaultTemplate::setParameters()`: the method moved to the subclass (34c4a715) +- **Deprecated** `Nette\Application\UI\Presenter::getContext()`: inject the services you need explicitly; using it triggers E_USER_DEPRECATED (0d479ac0) +- **Deprecated** `$flags` of `Nette\Application\Routers\Route::__construct()`: pass the flag to `Nette\Application\Routers\RouteList::addRoute()`; passing it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `$flags` of `Nette\Application\Routers\SimpleRouter::__construct()`: pass the flag to `Nette\Application\Routers\RouteList::add()`; passing it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\Route::getFlags()`: using it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\SimpleRouter::getFlags()`: using it triggers E_USER_DEPRECATED (de327f3f) +- **Deprecated** `Nette\Application\Routers\RouteList::count()`: count `Nette\Application\Routers\RouteList::getRouters()`; using it triggers E_USER_DEPRECATED (46a15001) +- **Deprecated** `Nette\Application\Routers\RouteList::getIterator()`: iterate over `Nette\Application\Routers\RouteList::getRouters()`; using it triggers E_USER_DEPRECATED (46a15001) +- **Deprecated** a modifier in `{control}`: using one triggers E_USER_DEPRECATED (09ac4e3e) +- **Deprecated** `{control}` used outside HTML text: using it triggers E_USER_WARNING (6a187d1c) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory` creates `Nette\Bridges\ApplicationLatte\DefaultTemplate` by default; previously `Nette\Bridges\ApplicationLatte\Template`, which no longer declares the presenter, control, user, baseUrl, basePath and flashes variables (34c4a715) +- **Changed** `Nette\Bridges\ApplicationLatte\Template` has no magic `__get`, `__set`, `__isset` and `__unset` and does not use `Nette\SmartObject`; parameters are ordinary public properties and reading an undefined one raises a PHP warning instead of E_USER_NOTICE (450720cb) +- **Changed** `Nette\Bridges\ApplicationLatte\DefaultTemplate::add()` throws `Nette\InvalidStateException` on a second call for the same name even when the stored value was null; previously such a parameter could be overwritten (450720cb) +- **Changed** `Nette\Bridges\ApplicationLatte\TemplateFactory` sets the default variables only when the template class declares them as properties and the value is not null (981a81d2) +- **Changed** `Nette\Application\UI\Control::createTemplate()` and `Nette\Application\UI\Presenter::createTemplate()` look up a template class by naming convention (`FooControl` to `FooTemplate`) and use it instead of the default one when it exists (6c24aaa7) +- **Changed** a flash message always expires after 30 seconds; previously after 3 seconds unless the response was a redirect (669e02c0) +- **Changed** `Nette\Application\UI\Presenter::run()` always calls `sendTemplate()` regardless of what `getTemplate()` returned; overriding getTemplate() to return null no longer suppresses rendering (74c9111c) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::render()` passes the whole template object to Latte instead of an array of parameters, so the template's public methods become available as Latte functions (c3de3299) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::renderToString()` passes the whole template object to Latte instead of an array of parameters (c3de3299) +- **Config** `application.catchExceptions` is forced to true in production mode regardless of the configured value (2a12b806) +- **Config** `application.scanFilter` is an fnmatch pattern and defaults to `*Presenter`; previously a substring searched in the class name with the default "Presenter" (00dc0177) +- **Config** `routing.routeClass`: deprecated, using it triggers E_USER_DEPRECATED (f8677b06) +- **Config** `routing.router`: the service is created only when at least one route is configured (aeafb720) +- **Config** `routing.router`: when no other service of type `Nette\Routing\Router` exists, a default `Nette\Application\Routers\SimpleRouter` is registered (5c2b5413) + +## 3.0.8 + +- **Renamed** `Nette\Application\UI\Form::disableSameSiteProtection()` → `Nette\Application\UI\Form::allowCrossOrigin()`; the old name is silently deprecated, also in 3.1.2 (72d5945c) + +## 3.0.7 + +- **Config** `application.debugger`: when not set explicitly, the panel is enabled by the presence of a `Tracy\BlueScreen` service; previously by whether tracy/tracy was installed (872c1434) +- **Config** `routing.debugger`: when not set explicitly, the routing panel is enabled by the presence of a `Tracy\Bar` service; previously by whether tracy/tracy was installed (872c1434) + +## 3.0.5 + +- **Changed** unserializing `Nette\Bridges\ApplicationLatte\Template` throws `Nette\NotImplementedException`; previously it succeeded silently (edcd93d6) +- **Changed** `Nette\Application\Request::isMethod()` returns false when the request method is null; previously it threw a `TypeError` under strict types (2974fbfd) + +## 3.0.4 + +- **Changed** `Nette\Application\UI\Presenter::restoreRequest()` redirects via `redirectUrl()` on a GET request; previously it always forwarded the restored request with `Nette\Application\Responses\ForwardResponse` (c84fbf32) + +## 3.0.3 + +- **Signature** `$context`, `$presenterFactory` and `$router` of `Nette\Application\UI\Presenter::injectPrimary()` lost their default null value and must be passed explicitly (edcfc7eb) +- **Changed** `Nette\Application\Responses\FileResponse::__construct()` throws `Nette\Application\BadRequestException` also when the file exists but is not readable; previously only when it did not exist (095b6f2f) +- **Changed** `Nette\Application\Responses\FileResponse::send()` throws `Nette\Application\BadRequestException` when the file cannot be opened; previously it continued with an invalid handle (095b6f2f) +- **Config** `application.scanDirs` takes priority over a globally registered RobotLoader; previously the global loader always won and a differing value was ignored (6ef62373) + +## 3.0.2.1 + +- **Changed** the `callback` route parameter of `NetteModule\MicroPresenter` must be a `\Closure`; other callables are rejected with `Nette\InvalidStateException` (f3b08453) +- **Changed** a request whose presenter name starts with `Nette:`, other than `Nette:Micro`, throws `Nette\Application\BadRequestException`; previously it was routed to that presenter (5d542d8f) + +## 3.0.2 + +- **Removed** `Nette\Bridges\ApplicationLatte\Loader` (67f45fb7) +- **Deprecated** `Nette\Application\UI\Component::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Deprecated** `Nette\Application\UI\Form::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Deprecated** `Nette\Application\UI\Presenter::hasPresenter()`: use `getPresenterIfExists() !== null`; the method is silently deprecated (56e9d459) +- **Changed** the type of a persistent parameter is taken from the declared property type when there is one; previously always from the type of its default value, so typed persistent properties are converted and validated more strictly (03e94280) +- **Changed** `Nette\Application\UI\Link::__toString()` rethrows an exception raised during the conversion on PHP 7.4 and newer; previously it was turned into a fatal E_USER_ERROR (5ae6bc13) + +## 3.0.1 + +- **Changed** `Nette\Application\Application::createInitialRequest()` throws `Nette\InvalidStateException` when the router returns parameters without a string presenter key; previously `Nette\Application\BadRequestException` (104d54e4) +- **Changed** links generated by `Nette\Application\UI\Presenter::link()` and `Nette\Application\LinkGenerator` and the baseUrl template variable are built from the URL without user info; previously a `user:pass@` part of the request was kept in them (d1378d43) + +## 3.0.0 + +- **Removed** `Nette\Application\IRouter::SECURED`: specify the protocol directly in the route mask (afd8d321) +- **Removed** `Nette\Application\Routers\Route::$defaultFlags` (afd8d321) +- **Removed** `Nette\Application\Routers\Route::MODULE_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\Route::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\SimpleRouter::MODULE_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\SimpleRouter::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\CliRouter::PRESENTER_KEY` (d05637d8) +- **Removed** `Nette\Application\Routers\Route::getTargetPresenters()` (77f5dfd9) +- **Removed** `Nette\Application\Routers\RouteList::warmupCache()` (77f5dfd9) +- **Renamed** `Nette\Application\UI\Control::invalidateControl()` → `Nette\Application\UI\Control::redrawControl()`; the old name was removed (afd8d321) +- **Renamed** `Nette\Bridges\ApplicationLatte\UIMacros::renderSnippets()` → `Nette\Bridges\ApplicationLatte\UIRuntime::renderSnippets()`; the old name was removed (afd8d321) +- **Renamed** the template variable `$baseUri` → `$baseUrl`; the old name was removed (bbe70369) +- **Renamed** `|url` → `|escapeUrl`; using the old name triggers E_USER_DEPRECATED (28d429e4) +- **Replaced** `Nette\Application\UI\Control::validateControl()` with `Nette\Application\UI\Control::redrawControl($snippet, false)`: the old method was removed (afd8d321) +- **Replaced** `Nette\Application\UI\Presenter::backlink()` with `Nette\Application\UI\Presenter::getAction(true)`: the old method was removed (afd8d321) +- **Replaced** `Nette\Bridges\ApplicationLatte\Template::registerHelper()` with `$template->getLatte()->addFilter()`: the old method was removed (afd8d321) +- **Replaced** calling a filter directly on the template object, i.e. `Nette\Bridges\ApplicationLatte\Template::__call()`, with `$template->getLatte()->invokeFilter($name, $args)`: the magic method was removed (afd8d321) +- **Replaced** the `$_control` template variable with the `uiControl` provider added by `$latte->addProvider()` (afd8d321) +- **Replaced** the `$_presenter` template variable with the `uiPresenter` provider added by `$latte->addProvider()` (afd8d321) +- **Replaced** `Nette\Application\UI\Component::attached()` with an override of `Nette\Application\UI\Component::validateParent()` and a `monitor()` callback: the framework no longer calls attached() (047c42f6) +- **Replaced** `Nette\Application\UI\Form::attached()` with an override of `Nette\Application\UI\Form::validateParent()` and a `monitor()` callback; chaining via `parent::attached()` no longer works either (047c42f6) +- **Replaced** the static `Nette\Application\UI\Component::getPersistentParams()` with public properties annotated `@persistent`: overriding the method has no effect, persistent parameters are discovered by reflection only (7765c92a) +- **Replaced** `Nette\Application\Application::$maxLoop` used statically with the instance property of the same name: assigning to the class no longer has any effect (8593c507) +- **Replaced** `Nette\Application\UI\Component::getParam()` with `Nette\Application\UI\Component::getParameters()` when called without an argument and with `Nette\Application\UI\Component::getParameter()` when called with one; using it triggers E_USER_DEPRECATED (800508d6) +- **Replaced** the numeric `$code` first argument of `Nette\Application\UI\Component::redirect()` with `Nette\Application\UI\Component::redirectPermanent()` for 301; passing it triggers E_USER_DEPRECATED and other codes have no replacement (29602efe) +- **Replaced** the `module` key in `$defaults` of `Nette\Application\Routers\SimpleRouter::__construct()` with `Nette\Application\Routers\RouteList::withModule()`; passing it triggers E_USER_DEPRECATED (ac454ca8) +- **Replaced** `$metadata` of `Nette\Application\Routers\Route::__construct()` passed as `Nette\Callback` with a `\Closure` (afd8d321) +- **Deprecated** `Nette\Application\IRouter`: use `Nette\Routing\Router`; the old name is silently deprecated (77f5dfd9) +- **Deprecated** `Nette\Application\Routers\Route::$styles`: using it triggers E_USER_DEPRECATED (a0474ca5) +- **Deprecated** the `@User(loggedIn)` annotation on presenter action and render methods: check the user in the action or in `Nette\Application\UI\Presenter::checkRequirements()`; using it triggers E_USER_DEPRECATED (f90e7dca) +- **Deprecated** `|normalize`: use `Nette\Utils\Strings::normalize()`; using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|toAscii`: use `Nette\Utils\Strings::toAscii()`; using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|reverse`: using it triggers E_USER_DEPRECATED (28d429e4) +- **Deprecated** `|null`: using it triggers E_USER_DEPRECATED (28d429e4) +- **Signature** native parameter and return types were added throughout the package; affects implementations and overrides of `Nette\Application\IPresenter::run()`, `Nette\Application\IPresenterFactory::createPresenter()`, `Nette\Application\IPresenterFactory::getPresenterClass()`, `Nette\Application\IResponse::send()`, `Nette\Application\UI\IRenderable::isControlInvalid()`, `Nette\Application\UI\IRenderable::redrawControl()`, `Nette\Application\UI\ISignalReceiver::signalReceived()`, `Nette\Application\UI\IStatePersistent::loadState()`, `Nette\Application\UI\IStatePersistent::saveState()`, `Nette\Application\UI\ITemplate::render()`, `Nette\Application\UI\ITemplate::setFile()`, `Nette\Application\UI\ITemplate::getFile()`, `Nette\Application\UI\ITemplateFactory::createTemplate()`, `Nette\Bridges\ApplicationLatte\ILatteFactory::create()`, `Nette\Application\UI\Component::validateParent()`, `Nette\Application\UI\Component::checkRequirements()`, `Nette\Application\UI\Control::templatePrepareFilters()`, `Nette\Application\UI\Multiplier::createComponent()`, `Nette\Application\UI\Form::isAnchored()`, `Nette\Application\UI\Form::receiveHttpData()` and `Nette\Application\UI\Presenter::shutdown()` (b71d69c5, c94f654f, 3f03051d) +- **Signature** `Nette\Application\IRouter::match()` returns an array of parameters instead of `Nette\Application\Request`; affects custom routers, the new `Nette\Application\Request::toArray()` does the opposite conversion (ed4be551) +- **Signature** `Nette\Application\IRouter::constructUrl()` takes an array of parameters instead of `Nette\Application\Request`; affects custom routers (ed4be551) +- **Signature** `$refUrl` of `Nette\Application\IRouter::constructUrl()` is `Nette\Http\UrlScript` instead of `Nette\Http\Url`; affects custom routers (a0ddbee7) +- **Signature** `$refUrl` of `Nette\Application\LinkGenerator::__construct()` is `Nette\Http\UrlScript` instead of `Nette\Http\Url` (a0ddbee7) +- **Signature** `$reflection` of `Nette\Application\UI\Component::saveState()` was removed; affects subclasses overriding it and calls passing a second argument (86101dd9) +- **Signature** `$throw` of `Nette\Application\UI\Component::getPresenter()`, `Nette\Application\UI\Form::getPresenter()` and `Nette\Application\UI\Presenter::getPresenter()` was removed from the signature; passing it triggers E_USER_DEPRECATED, use `hasPresenter()` instead of `getPresenter(false)` (31ff0947) +- **Signature** `$message` of `Nette\Application\UI\Component::error()` and `NetteModule\MicroPresenter::error()` is `string` with the default `''`; passing null throws a `TypeError` (66c40ddc) +- **Signature** `$name` of `Nette\Application\UI\Control::getSnippetId()` is required and typed `string` (b71d69c5) +- **Signature** `$destination` of `Nette\Application\UI\Presenter::redirectPermanent()` is required and typed `string` (b71d69c5) +- **Signature** `Nette\Application\Application::getRouter()` returns `Nette\Routing\Router` instead of `Nette\Application\IRouter` (fc1a9f70) +- **Signature** these classes are final and can no longer be extended: `Nette\Application\Helpers`, `Nette\Application\LinkGenerator`, `Nette\Application\Request`, `Nette\Application\Responses\CallbackResponse`, `Nette\Application\Responses\FileResponse`, `Nette\Application\Responses\ForwardResponse`, `Nette\Application\Responses\JsonResponse`, `Nette\Application\Responses\RedirectResponse`, `Nette\Application\Responses\TextResponse`, `Nette\Application\Responses\VoidResponse`, `Nette\Application\Routers\CliRouter`, `Nette\Application\Routers\SimpleRouter`, `Nette\Application\UI\Link`, `Nette\Application\UI\Multiplier`, `Nette\Bridges\ApplicationDI\ApplicationExtension`, `Nette\Bridges\ApplicationDI\LatteExtension`, `Nette\Bridges\ApplicationDI\RoutingExtension`, `Nette\Bridges\ApplicationLatte\UIMacros`, `Nette\Bridges\ApplicationTracy\RoutingPanel`, `NetteModule\ErrorPresenter` and `NetteModule\MicroPresenter` (a70c7256) +- **Signature** these methods are final and can no longer be overridden: `Nette\Application\Application::getPresenter()`, `Nette\Application\Application::getRequests()`, `Nette\Application\UI\Component::getParameter()`, `Nette\Application\UI\Component::getParameters()`, `Nette\Application\UI\Component::getParameterId()`, `Nette\Application\UI\Control::getTemplate()`, `Nette\Application\UI\Control::setTemplateFactory()`, `Nette\Application\UI\Form::getPresenter()`, `Nette\Application\UI\Presenter::createRequest()`, `Nette\Application\UI\Presenter::getAction()`, `Nette\Application\UI\Presenter::getView()`, `Nette\Application\UI\Presenter::getLayout()`, `Nette\Application\UI\Presenter::getRequest()`, `Nette\Application\UI\Presenter::getPayload()`, `Nette\Application\UI\Presenter::getSignal()`, `Nette\Application\UI\Presenter::getUser()`, `Nette\Application\UI\Presenter::getSession()`, `Nette\Application\UI\Presenter::getContext()`, `Nette\Application\UI\Presenter::getHttpRequest()`, `Nette\Application\UI\Presenter::getHttpResponse()`, `Nette\Application\UI\Presenter::getTemplateFactory()`, `Nette\Application\UI\Presenter::isSignalReceiver()`, `Nette\Application\UI\Presenter::injectPrimary()`, `Nette\Bridges\ApplicationLatte\Template::getLatte()`, `Nette\Bridges\ApplicationLatte\Template::getFile()`, `Nette\Bridges\ApplicationLatte\Template::getParameters()` and `Nette\Bridges\ApplicationLatte\Template::__toString()` (a70c7256) +- **Changed** `Nette\Application\Routers\RouteList` extends `Nette\Routing\RouteList` instead of `Nette\Utils\ArrayList`; code relying on the ArrayList behavior breaks (77f5dfd9) +- **Changed** a signal is rejected and redirected through `Nette\Application\UI\Presenter::detectedCsrf()` when the HTTP request is not same-site; annotate the handler `@crossOrigin` to keep accepting cross-site calls (87696e09) +- **Changed** `Nette\Application\UI\Form::signalReceived()` rejects the submit signal when the HTTP request is not same-site (87696e09) +- **Changed** `Nette\Application\UI\Presenter::run()` returns `Nette\Application\Responses\VoidResponse` instead of null and always performs the shutdown, flash and AJAX payload handling; previously only when `Nette\Application\AbortException` was thrown (ad41a4cd) +- **Changed** `NetteModule\MicroPresenter::run()` returns `Nette\Application\Responses\VoidResponse` instead of null when the callback produces no response (ad41a4cd) +- **Changed** `Nette\Application\UI\Presenter::createRequest()` parses the destination with a strict regular expression; a destination that does not have the shape `[//] path[!][?query][#fragment]` throws `Nette\Application\UI\InvalidLinkException` (1c581500) +- **Changed** `NetteModule\ErrorPresenter::run()` renders the error template only when the response content type is text/html; previously always (82099365) +- **Changed** `Nette\Application\Responses\FileResponse::__construct()` falls back to the file base name only when `$name` is null; previously for any falsy value (bcaa9d91) +- **Changed** `Nette\Application\Routers\Route::constructUrl()` casts all scalar parameters to strings, so false becomes `'0'`; previously only parameters with a fixity were converted and a false default in a mask became an empty string (b62d2f52) +- **Changed** `Nette\Application\Routers\RouteList::getModule()` returns null instead of an empty string when the router was created without a module (06f98aac) +- **Changed** `Nette\Application\Routers\Route::match()` always returns the matched array and no longer throws `Nette\InvalidStateException` nor returns null when the presenter parameter is missing; the validation happens in `Nette\Application\Application::createInitialRequest()` (ae6e0bc4) +- **Changed** `Nette\Application\Routers\SimpleRouter::match()` always matches and no longer returns null when the presenter parameter is missing, so inside a `Nette\Application\Routers\RouteList` it no longer defers to the next router (ae6e0bc4) +- **Changed** `Nette\Application\UI\Presenter::changeAction()` no longer validates the action name; the validation happens once during request initialization (50e49fda) +- **Changed** `Nette\Bridges\ApplicationLatte\Template::setTranslator()` called with null keeps the `|translate` filter defined and returns the text unchanged; previously the filter was removed and using it failed (fc63a718) +- **Changed** `Nette\Application\UI\Control::getTemplate()` fails with a native `TypeError` when `createTemplate()` returns something else than a template; previously `Nette\UnexpectedValueException` with a descriptive message (bf347899) +- **Changed** `Nette\Application\PresenterFactory::getPresenterClass()` fails with a native `TypeError` for a non-string name; previously `Nette\Application\InvalidPresenterException` (bf347899) +- **Changed** `NetteModule\MicroPresenter` throws `Nette\InvalidStateException` (500) when the route has no callback parameter; previously `Nette\Application\BadRequestException` (404) (b7df270f) +- **Changed** presenter services registered by `Nette\Bridges\ApplicationDI\ApplicationExtension` are not autowired; reference a presenter explicitly when another service needs it (2a8d32a4) +- **Config** the `nette.presenter` DI service tag is no longer used, presenters are found by type (cb354357) diff --git a/dresscode.neon b/dresscode.neon new file mode 100644 index 000000000..10a8a1344 --- /dev/null +++ b/dresscode.neon @@ -0,0 +1,33 @@ +use: + - nette + - modernizations + +typeAnalysis: phpstan + +paths: + - src + - tests + +excludePaths: + - tests/**/expected + - tests/**/fixtures* + +fileExtensions: + - php + - phpt + +imports: {grouping: {class: grouped}} + +overrides: + - + paths: [src] + nameResolution: certain + use: + - compilerOptimizations + - deprecations + + - # the deprecated ArrayAccess keeps its parameter names + paths: + - src/Application/Routers/RouteList.php + - src/Application/UI/Component.php + classes: {overridingMethod: {parameterName: keep}} diff --git a/phpstan.neon b/phpstan.neon index 89a7fdc1d..d53f70233 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -27,7 +27,6 @@ parameters: identifiers: - function.alreadyNarrowedType - identical.alwaysTrue - - booleanAnd.alwaysTrue # PresenterFactory: default factory `new $class` yields object; $class is a validated class-string at the call site - @@ -50,11 +49,6 @@ parameters: identifier: nullCoalesce.offset path: src/Application/Routers/Route.php - # parse_str() native type is wider than actual query string result - - - identifier: varTag.nativeType - path: src/Application/LinkGenerator.php - # MicroPresenter uses duck typing for template objects and nullable container - path: src/Application/MicroPresenter.php @@ -65,11 +59,6 @@ parameters: - method.nonObject - property.notFound - # ErrorPresenter: $code is used in the required template file - - - identifier: closure.unusedUse - path: src/Application/ErrorPresenter.php - # BadRequestException intentionally narrows inherited Exception::$code to int (HTTP status codes only) - identifier: property.phpDocType @@ -95,11 +84,6 @@ parameters: count: 1 path: src/Application/UI/Presenter.php - # Nette Forms compatibility - methods differ between versions - - - identifier: method.notFound - path: src/Application/UI/Form.php - # Control::$template->flashes is set dynamically for flash messages - identifier: property.notFound @@ -211,3 +195,10 @@ parameters: - offsetAccess.notFound - closure.unusedUse - identical.alwaysFalse + + # Tracy routing agent panel: compiled Latte template with recursive block array (self-referencing $_blocks) + - + path: src/Bridges/ApplicationTracy/dist/panel.agent.phtml + identifiers: + - offsetAccess.notFound + - closure.unusedUse diff --git a/readme.md b/readme.md index 41ddf658c..b4b61840e 100644 --- a/readme.md +++ b/readme.md @@ -2,7 +2,7 @@ Nette Application MVC ===================== [![Downloads this Month](https://img.shields.io/packagist/dm/nette/application.svg)](https://packagist.org/packages/nette/application) -[![Tests](https://github.com/nette/application/actions/workflows/tests.yml/badge.svg?branch=v3.3)](https://github.com/nette/application/actions) +[![Tests](https://github.com/nette/application/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/nette/application/actions) [![Latest Stable Version](https://poser.pugx.org/nette/application/v/stable)](https://github.com/nette/application/releases) [![License](https://img.shields.io/badge/license-New%20BSD-blue.svg)](https://github.com/nette/application/blob/master/license.md) diff --git a/src/Application/Application.php b/src/Application/Application.php index d1afc0d4e..37972cfc5 100644 --- a/src/Application/Application.php +++ b/src/Application/Application.php @@ -66,7 +66,7 @@ public function run(): void Arrays::invoke($this->onStartup, $this); $this->processRequest($this->createInitialRequest()) ->send($this->httpRequest, $this->httpResponse); - Arrays::invoke($this->onShutdown, $this); + Arrays::invoke($this->onShutdown, $this, null); } catch (\Throwable $e) { $this->sendHttpCode($e); @@ -95,6 +95,10 @@ public function run(): void */ public function createInitialRequest(): Request { + if ($this->httpRequest->isMethod(Request::Forward)) { + throw new BadRequestException('Invalid request. Method FORWARD is reserved for internal use.'); + } + $params = $this->router->match($this->httpRequest); $presenter = $params[UI\Presenter::PresenterKey] ?? null; @@ -131,7 +135,7 @@ public function processRequest(Request $request): Response Arrays::invoke($this->onRequest, $this, $request); if ( - !$request->isMethod($request::FORWARD) + !$request->isMethod($request::Forward) && (!strcasecmp($request->getPresenterName(), (string) $this->errorPresenter) || !strcasecmp($request->getPresenterName(), (string) $this->error4xxPresenter)) ) { @@ -178,10 +182,12 @@ public function createErrorRequest(\Throwable $e): ?Request $this->presenter->forward(":$errorPresenter:", $args); } catch (AbortException) { return $this->presenter->getLastCreatedRequest(); + } catch (\Throwable) { + // a misconfigured error presenter must not replace $e, it fails in a controlled way inside processRequest() } } - return new Request($errorPresenter, Request::FORWARD, $args); + return new Request($errorPresenter, Request::Forward, $args); } diff --git a/src/Application/ErrorPresenter.php b/src/Application/ErrorPresenter.php index 01cce11e5..271b74bd5 100644 --- a/src/Application/ErrorPresenter.php +++ b/src/Application/ErrorPresenter.php @@ -32,10 +32,12 @@ public function run(Application\Request $request): Application\Response $this->logger?->log($e, ILogger::EXCEPTION); } - return new Application\Responses\CallbackResponse(function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void { - if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { - require __DIR__ . '/templates/error.phtml'; - } - }); + return new Application\Responses\CallbackResponse( + function (Http\IRequest $httpRequest, Http\IResponse $httpResponse) use ($code): void { + if (preg_match('#^text/html(?:;|$)#', (string) $httpResponse->getHeader('Content-Type'))) { + require __DIR__ . '/templates/error.phtml'; + } + }, + ); } } diff --git a/src/Application/LinkGenerator.php b/src/Application/LinkGenerator.php index 2fbc0d1f6..f2962ade7 100644 --- a/src/Application/LinkGenerator.php +++ b/src/Application/LinkGenerator.php @@ -8,7 +8,7 @@ namespace Nette\Application; use Nette\Http\UrlScript; -use Nette\Routing\Router; +use Nette\Routing\{Route, Router}; use Nette\Utils\Reflection; use function array_key_exists, is_scalar, is_string, strlen; @@ -154,7 +154,12 @@ public function createRequest( throw new UI\InvalidLinkException("Unknown signal '$signal', missing handler {$reflection->getName()}::{$component::formatSignalMethod($signal)}()"); } - $this->validateLinkTarget($refPresenter, $method, "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), $mode); + $this->validateLinkTarget( + $refPresenter, + $method, + "signal '$signal'" . ($component === $refPresenter ? '' : ' in ' . $component::class), + $mode, + ); // convert indexed parameters to named UI\ParameterConverter::toParameters($method, $args, [], $missing); @@ -180,7 +185,9 @@ public function createRequest( $action = UI\Presenter::DefaultAction; } - $current = $refPresenter && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) && $presenterClass === $refPresenter::class; + $current = $refPresenter + && ($action === '*' || strcasecmp($action, $refPresenter->getAction()) === 0) + && $presenterClass === $refPresenter::class; $reflection = new UI\ComponentReflection($presenterClass); $this->validateLinkTarget($refPresenter, $reflection, "presenter '$presenter'", $mode); @@ -195,7 +202,12 @@ public function createRequest( if ($method = $reflection->getActionRenderMethod($action)) { $this->validateLinkTarget($refPresenter, $method, "action '$presenter:$action'", $mode); - UI\ParameterConverter::toParameters($method, $args, $path === 'this' ? ($refPresenter?->getParameters() ?? []) : [], $missing); + UI\ParameterConverter::toParameters( + $method, + $args, + $path === 'this' ? ($refPresenter?->getParameters() ?? []) : [], + $missing, + ); } elseif (array_key_exists(0, $args)) { throw new UI\InvalidLinkException("Unable to pass parameters to action '$presenter:$action', missing corresponding method $presenterClass::{$presenterClass::formatRenderMethod($action)}()."); @@ -210,8 +222,9 @@ public function createRequest( $globalState = $refPresenter->getGlobalState($path === 'this' ? null : $presenterClass); if ($current && $args) { $tmp = $globalState + $refPresenter->getParameters(); + $serialize = fn($val) => http_build_query(Route::objectsToArrays([$val instanceof \BackedEnum ? $val->value : $val])); foreach ($args as $key => $val) { - if (http_build_query([$val]) !== (isset($tmp[$key]) ? http_build_query([$tmp[$key]]) : '')) { + if ($serialize($val) !== (isset($tmp[$key]) ? $serialize($tmp[$key]) : '')) { $current = false; break; } @@ -238,7 +251,7 @@ public function createRequest( if (!empty($signal)) { assert($component !== null); $args[UI\Presenter::SignalKey] = $component->getParameterId($signal); - $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter?->getParameter(UI\Presenter::SignalKey); + $current = $current && $args[UI\Presenter::SignalKey] === $refPresenter->getParameter(UI\Presenter::SignalKey); } if (($mode === 'redirect' || $mode === 'forward') && $refPresenter?->hasFlashSession()) { @@ -246,27 +259,35 @@ public function createRequest( $args[UI\Presenter::FlashKey] = is_string($flashKey) && $flashKey !== '' ? $flashKey : null; } - return $this->lastRequest = new Request($presenter, Request::FORWARD, $args, flags: ['current' => $current]); + foreach ($args as $key => $val) { + if ($val instanceof \BackedEnum) { // URL carries the case value + $args[$key] = $val->value; + } + } + + return $this->lastRequest = new Request($presenter, Request::Forward, $args, flags: ['current' => $current]); } /** * Parse destination in format "[//] [[[module:]presenter:]action | signal! | this | @alias] [?query] [#fragment]" * @throws UI\InvalidLinkException - * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} + * @return array{absolute: bool, path: string, signal: bool, args: mixed[]|null, fragment: string} * @internal */ public static function parseDestination(string $destination): array { - if (!preg_match('~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', $destination, $matches)) { + if (!preg_match( + '~^ (?//)?+ (?[^!?#]++) (?!)?+ (?\?[^#]*)?+ (?\#.*)?+ $~x', + $destination, + $matches, + )) { throw new UI\InvalidLinkException("Invalid destination '$destination'."); } if (!empty($matches['query'])) { trigger_error("Link format is obsolete, use arguments instead of query string in '$destination'.", E_USER_DEPRECATED); - parse_str(substr($matches['query'], 1), $parsed); - /** @var array $args */ - $args = $parsed; + parse_str(substr($matches['query'], 1), $args); } return [ @@ -288,7 +309,7 @@ public function requestToUrl(Request $request, ?bool $relative = false): string if ($url === null) { $params = $request->getParameters(); unset($params[UI\Presenter::ActionKey], $params[UI\Presenter::PresenterKey]); - $params = urldecode(http_build_query($params, '', ', ')); + $params = urldecode(http_build_query(Route::objectsToArrays($params), '', ', ')); throw new UI\InvalidLinkException("No route for {$request->getPresenterName()}:{$request->getParameter('action')}($params)"); } @@ -324,11 +345,9 @@ private function validateLinkTarget( $message .= $presenter === null ? '' : " from '{$presenter->getName()}:{$presenter->getAction()}'"; - if ($mode !== 'forward' && !(new UI\AccessPolicy($element))->isLinkable()) { + if ($mode !== 'forward' && $mode !== 'test' && !(new UI\AccessPolicy($element))->isLinkable()) { throw new UI\InvalidLinkException("Link to forbidden $message."); - } elseif ($presenter?->invalidLinkMode - && (UI\ComponentReflection::parseAnnotation($element, 'deprecated') || $element->getAttributes(Attributes\Deprecated::class)) - ) { + } elseif ($presenter?->invalidLinkMode && $element->getAttributes(Attributes\Deprecated::class)) { trigger_error("Link to deprecated $message.", E_USER_DEPRECATED); } } diff --git a/src/Application/MicroPresenter.php b/src/Application/MicroPresenter.php index bc2aff8cf..b253757c9 100644 --- a/src/Application/MicroPresenter.php +++ b/src/Application/MicroPresenter.php @@ -20,7 +20,7 @@ */ final class MicroPresenter implements Application\IPresenter { - private ?Application\Request $request; + private ?Application\Request $request = null; public function __construct( @@ -94,11 +94,9 @@ public function run(Application\Request $request): Application\Response $response->setFile((string) $templateSource); } - if ($response instanceof Application\UI\Template) { - return new Responses\TextResponse($response); - } else { - return $response ?: new Responses\VoidResponse; - } + return $response instanceof Application\UI\Template + ? new Responses\TextResponse($response) + : ($response ?: new Responses\VoidResponse); } @@ -111,7 +109,9 @@ public function createTemplate(?string $class = null, ?callable $latteFactory = { $latte = $latteFactory ? $latteFactory() - : $this->context->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class)->create(); + : ($this->context ?? throw new Nette\InvalidStateException('Unable to create template, provide the $latteFactory parameter or DI container.')) + ->getByType(Nette\Bridges\ApplicationLatte\LatteFactory::class) + ->create(); $template = $class ? new $class : new Nette\Bridges\ApplicationLatte\DefaultTemplate($latte); diff --git a/src/Application/PresenterFactory.php b/src/Application/PresenterFactory.php index db61f261f..90bb92024 100644 --- a/src/Application/PresenterFactory.php +++ b/src/Application/PresenterFactory.php @@ -93,6 +93,7 @@ public function setMapping(array $mapping): static } $this->mapping[$module] = [$m[1], $m[2] ?: '*Module\\', $m[3]]; + } elseif (is_array($mask) && count($mask) === 3) { $this->mapping[$module] = [$mask[0] ? $mask[0] . '\\' : '', $mask[1] . '\\', $mask[2]]; } else { @@ -114,9 +115,14 @@ public function formatPresenterClass(string $presenter): string throw new InvalidPresenterException("Presenter name must be alphanumeric string, '$presenter' is invalid."); } $parts = explode(':', $presenter); - $mapping = isset($parts[1], $this->mapping[$parts[0]]) - ? $this->mapping[array_shift($parts)] - : $this->mapping['*']; + $mapping = $this->mapping['*']; + for ($i = count($parts) - 1; $i > 0; $i--) { // the longest module prefix wins + if (isset($this->mapping[$module = implode(':', array_slice($parts, 0, $i))])) { + $mapping = $this->mapping[$module]; + $parts = array_slice($parts, $i); + break; + } + } while ($part = array_shift($parts)) { $mapping[0] .= strtr($mapping[$parts ? 1 : 2], ['**' => "$part\\$part", '*' => $part]); diff --git a/src/Application/Request.php b/src/Application/Request.php index 640c5445a..962fe5aba 100644 --- a/src/Application/Request.php +++ b/src/Application/Request.php @@ -25,13 +25,22 @@ final class Request use Nette\SmartObject; /** method */ - public const FORWARD = 'FORWARD'; + public const Forward = 'FORWARD'; /** flag */ - public const RESTORED = 'restored'; + public const Restored = 'restored'; /** flag */ - public const VARYING = 'varying'; + public const Varying = 'varying'; + + /** @deprecated use Request::Forward */ + public const FORWARD = self::Forward; + + /** @deprecated use Request::Restored */ + public const RESTORED = self::Restored; + + /** @deprecated use Request::Varying */ + public const VARYING = self::Varying; public function __construct( diff --git a/src/Application/Responses/FileResponse.php b/src/Application/Responses/FileResponse.php index beda43838..47f986537 100644 --- a/src/Application/Responses/FileResponse.php +++ b/src/Application/Responses/FileResponse.php @@ -67,11 +67,12 @@ public function getContentType(): string public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $httpResponse): void { $httpResponse->setContentType($this->contentType); + $name = str_replace(["\r", "\n", "\0", '"', '\\'], '', $this->name); $httpResponse->setHeader( 'Content-Disposition', ($this->forceDownload ? 'attachment' : 'inline') - . '; filename="' . $this->name . '"' - . '; filename*=utf-8\'\'' . rawurlencode($this->name), + . '; filename="' . $name . '"' + . '; filename*=utf-8\'\'' . rawurlencode($this->name), ); $filesize = $length = filesize($this->file); @@ -97,6 +98,7 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt if ($end < $start) { $httpResponse->setCode(416); // requested range not satisfiable + $httpResponse->setHeader('Content-Range', 'bytes */' . $filesize); return; } @@ -104,9 +106,6 @@ public function send(Nette\Http\IRequest $httpRequest, Nette\Http\IResponse $htt $httpResponse->setHeader('Content-Range', 'bytes ' . $start . '-' . $end . '/' . $filesize); $length = $end - $start + 1; fseek($handle, $start); - - } else { - $httpResponse->setHeader('Content-Range', 'bytes 0-' . ($filesize - 1) . '/' . $filesize); } } diff --git a/src/Application/Routers/Route.php b/src/Application/Routers/Route.php index eb033620c..867113e26 100644 --- a/src/Application/Routers/Route.php +++ b/src/Application/Routers/Route.php @@ -42,7 +42,8 @@ class Route extends Nette\Routing\Route implements Nette\Routing\Router /** * @param string $mask e.g. '//' - * @param array|string|\Closure(Nette\Http\IRequest, Nette\Http\IResponse): Nette\Application\Response $metadata default values or metadata or callback for NetteModule\MicroPresenter + * @param array|string|\Closure(Nette\Http\IRequest, Nette\Http\IResponse): Nette\Application\Response $metadata + * default values or metadata or callback for NetteModule\MicroPresenter */ public function __construct(string $mask, array|string|\Closure $metadata = []) { diff --git a/src/Application/Routers/RouteList.php b/src/Application/Routers/RouteList.php index c3311723e..8ae7cc88c 100644 --- a/src/Application/Routers/RouteList.php +++ b/src/Application/Routers/RouteList.php @@ -52,12 +52,13 @@ protected function completeParameters(array $params): ?array */ public function constructUrl(array $params, Nette\Http\UrlScript $refUrl): ?string { - if ($this->module) { - if (!str_starts_with($params[self::PresenterKey], $this->module)) { + $presenter = $params[self::PresenterKey] ?? null; + if ($this->module && is_string($presenter) && strncmp($presenter, 'Nette:', 6)) { // mirrors completeParameters() + if (!str_starts_with($presenter, $this->module)) { return null; } - $params[self::PresenterKey] = substr($params[self::PresenterKey], strlen($this->module)); + $params[self::PresenterKey] = substr($presenter, strlen($this->module)); } return parent::constructUrl($params, $refUrl); @@ -69,10 +70,10 @@ public function addRoute( #[Language('TEXT')] string $mask, array|string|\Closure $metadata = [], - int|bool $oneWay = 0, + bool $oneWay = false, ): static { - $this->add(new Route($mask, $metadata), (int) $oneWay); + $this->add(new Route($mask, $metadata), $oneWay); return $this; } diff --git a/src/Application/UI/AccessPolicy.php b/src/Application/UI/AccessPolicy.php index 0b372aa5e..18e136862 100644 --- a/src/Application/UI/AccessPolicy.php +++ b/src/Application/UI/AccessPolicy.php @@ -18,11 +18,13 @@ */ final class AccessPolicy { + /** @var array */ + private static array $classAttrsCache = []; private Presenter $presenter; - /** @param \ReflectionClass|\ReflectionMethod $element */ public function __construct( + /** @var \ReflectionClass|\ReflectionMethod */ private readonly \ReflectionClass|\ReflectionMethod $element, ) { } @@ -30,11 +32,14 @@ public function __construct( public function checkAccess(Component $component): void { - $this->presenter ??= $component->getPresenter(throw: false) ?? - throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); + $attrs = self::applyInternalRules($this->getAttributes(), $component); + if (!$attrs) { + return; + } + + $this->presenter ??= $component->getPresenter(throw: false) + ?? throw new Nette\InvalidStateException('Presenter is required for checking requirements of ' . Reflection::toString($this->element)); - $attrs = $this->getAttributes(); - $attrs = self::applyInternalRules($attrs, $component); foreach ($attrs as $attribute) { $this->checkAttribute($attribute); } @@ -53,10 +58,46 @@ public function isLinkable(): bool */ private function getAttributes(): array { - return array_map( - fn($ra) => $ra->newInstance(), - $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), - ); + return $this->element instanceof \ReflectionMethod + ? array_map( + fn($ra) => $ra->newInstance(), + $this->element->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), + ) + : self::$classAttrsCache[$this->element->getName()] ??= $this->getClassAttributes($this->element); + } + + + /** + * Collects class-level attributes including those inherited from ancestor classes. + * A rule explicitly set in a descendant overrides the same rule of all its ancestors. + * @param \ReflectionClass $class + * @return Attributes\Requires[] + */ + private function getClassAttributes(\ReflectionClass $class): array + { + $res = $overridden = []; + for ($rc = $class; $rc; $rc = $rc->getParentClass()) { + $defined = []; + foreach ($rc->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) as $ra) { + $attr = $ra->newInstance(); + $used = false; + foreach (array_keys(array_filter(get_object_vars($attr), fn($value) => $value !== null)) as $prop) { + if (isset($overridden[$prop])) { + $attr->$prop = null; + } else { + $defined[$prop] = $used = true; + } + } + + if ($used) { + $res[] = $attr; + } + } + + $overridden += $defined; + } + + return $res; } @@ -69,7 +110,6 @@ private function applyInternalRules(array $attrs, Component $component): array if ( $this->element instanceof \ReflectionMethod && str_starts_with($this->element->getName(), $component::formatSignalMethod('')) - && !ComponentReflection::parseAnnotation($this->element, 'crossOrigin') && !Nette\Utils\Arrays::some($attrs, fn($attr) => $attr->sameOrigin === false) ) { $attrs[] = new Attributes\Requires(sameOrigin: true); @@ -97,7 +137,10 @@ private function checkAttribute(Attributes\Requires $attribute): void } if ($attribute->ajax && !$this->presenter->getHttpRequest()->isAjax()) { - $this->presenter->error('AJAX request is required by ' . Reflection::toString($this->element), Nette\Http\IResponse::S403_Forbidden); + $this->presenter->error( + 'AJAX request is required by ' . Reflection::toString($this->element), + Nette\Http\IResponse::S403_Forbidden, + ); } } @@ -122,7 +165,7 @@ private function checkActions(array $actions): void private function checkHttpMethod(array $methods): void { if ($this->element instanceof \ReflectionClass) { - $this->presenter->allowedMethods = []; // bypass Presenter::checkHttpMethod() + $this->presenter->allowedMethods = []; // dresscode:ignore upgrading.declarations.deprecatedMember -- bypass Presenter::checkHttpMethod() } $allowed = array_map(strtoupper(...), $methods); diff --git a/src/Application/UI/Component.php b/src/Application/UI/Component.php index 4508ae19c..fb247049a 100644 --- a/src/Application/UI/Component.php +++ b/src/Application/UI/Component.php @@ -71,18 +71,30 @@ public function getUniqueId(): string } + public function addComponent( + Nette\ComponentModel\IComponent $component, + ?string $name, + ?string $insertBefore = null, + ): static + { + if (!$component instanceof SignalReceiver && !$component instanceof StatePersistent) { + throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' is not intended to be used in the Presenter.'); + } elseif ($component instanceof StatePersistent && !$component instanceof self) { + throw new Nette\InvalidStateException("Component '$name' of type " . get_debug_type($component) . ' cannot carry persistent state, only ' . self::class . ' can.'); + } + + return parent::addComponent($component, $name, $insertBefore); + } + + protected function createComponent(string $name): ?Nette\ComponentModel\IComponent { if (method_exists($this, $method = 'createComponent' . $name)) { - (new AccessPolicy(new \ReflectionMethod($this, $method)))->checkAccess($this); + $rm = new \ReflectionMethod($this, $method); + (new AccessPolicy($rm))->checkAccess($this); + $this->checkRequirements($rm); } - $res = parent::createComponent($name); - if ($res && !$res instanceof SignalReceiver && !$res instanceof StatePersistent) { - $type = $res::class; - trigger_error("It seems that component '$name' of type $type is not intended to be used in the Presenter."); - } - - return $res; + return parent::createComponent($name); } @@ -125,7 +137,7 @@ protected function tryCall(string $method, array $params): bool /** * Descendant can override this method to check for permissions. - * It is called with the presenter class and the render*(), action*(), and handle*() methods. + * It is called with the presenter class and the render*(), action*(), handle*() and createComponent*() methods. * @param \ReflectionClass|\ReflectionMethod $element */ public function checkRequirements(\ReflectionClass|\ReflectionMethod $element): void @@ -164,9 +176,20 @@ public function loadState(array $params): void )); } - $this->$name = $params[$name]; + $this->$name = &$params[$name]; + + } elseif ( + isset($this->$name) + || ((new \ReflectionProperty($this, $name))->getType()?->allowsNull() ?? true) + ) { + $params[$name] = &$this->$name; + } else { - $params[$name] = $this->$name ?? null; + $this->error(sprintf( + "Missing parameter '%s' required by %s.", + $name, + $this instanceof Presenter ? 'presenter ' . $this->getName() : "component '{$this->getUniqueId()}'", + )); } } @@ -217,7 +240,7 @@ public function saveStatePartial(array &$params, ComponentReflection $reflection )); } - if ($params[$name] === $meta['def'] || ($meta['def'] === null && $params[$name] === '')) { + if ($params[$name] === $meta['def']) { $params[$name] = null; // value transmit is unnecessary } } @@ -243,7 +266,7 @@ final public function getParameter(string $name): mixed */ final public function getParameters(): array { - return $this->params; + return array_map(fn($item) => $item, $this->params); } @@ -300,7 +323,7 @@ public function link(string $destination, $args = []): string return $this->getPresenter()->getLinkGenerator()->link($destination, $args, $this, 'link'); } catch (InvalidLinkException $e) { - return $this->getPresenter()->handleInvalidLink($e); + return $this->getPresenter()->processInvalidLink($e); } } @@ -346,10 +369,9 @@ public function isLinkCurrent(?string $destination = null, $args = []): bool * Redirect to another presenter, action or signal. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" * @param array|mixed $args - * @return never * @throws Nette\Application\AbortException */ - public function redirect(string $destination, $args = []): void + public function redirect(string $destination, $args = []): never { $args = func_num_args() < 3 && is_array($args) ? $args @@ -370,10 +392,9 @@ public function redirect(string $destination, $args = []): void * Permanently redirects to presenter, action or signal. * @param string $destination in format "[//] [[[module:]presenter:]action | signal! | this] [#fragment]" * @param array|mixed $args - * @return never * @throws Nette\Application\AbortException */ - public function redirectPermanent(string $destination, $args = []): void + public function redirectPermanent(string $destination, $args = []): never { $args = func_num_args() < 3 && is_array($args) ? $args @@ -392,9 +413,8 @@ public function redirectPermanent(string $destination, $args = []): void /** * Throws HTTP error. * @throws Nette\Application\BadRequestException - * @return never */ - public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): void + public function error(string $message = '', int $httpCode = Nette\Http\IResponse::S404_NotFound): never { throw new Nette\Application\BadRequestException($message, $httpCode); } diff --git a/src/Application/UI/ComponentReflection.php b/src/Application/UI/ComponentReflection.php index 3906a9a05..47f68230b 100644 --- a/src/Application/UI/ComponentReflection.php +++ b/src/Application/UI/ComponentReflection.php @@ -48,10 +48,7 @@ public function getParameters(): array foreach ($this->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { if ($prop->isStatic()) { continue; - } elseif ( - self::parseAnnotation($prop, 'persistent') - || $prop->getAttributes(Attributes\Persistent::class) - ) { + } elseif ($prop->getAttributes(Attributes\Persistent::class)) { $params[$prop->getName()] = [ 'def' => $prop->hasDefaultValue() ? $prop->getDefaultValue() : null, 'type' => ParameterConverter::getType($prop), @@ -92,7 +89,7 @@ public function getPersistentParams(): array /** * Returns array of persistent components. They are tagged with class-level attribute - * #[Persistent] or annotation @persistent or returned by Presenter::getPersistentComponents(). + * #[Persistent] or returned by Presenter::getPersistentComponents(). * @return array */ public function getPersistentComponents(): array @@ -104,10 +101,8 @@ public function getPersistentComponents(): array } $attrs = $this->getAttributes(Attributes\Persistent::class); - $names = $attrs - ? $attrs[0]->getArguments() - : (array) self::parseAnnotation($this, 'persistent'); - $names = array_merge($names, $class::getPersistentComponents()); + $names = $attrs ? $attrs[0]->getArguments() : []; + $names = [...$names, ...$class::getPersistentComponents()]; $components = array_fill_keys($names, ['since' => $class]); if ($this->isSubclassOf(Presenter::class)) { @@ -156,11 +151,15 @@ public function hasCallableMethod(string $method): bool public function getActionRenderMethod(string $action): ?\ReflectionMethod { $class = $this->name; - return self::$armCache[$class][$action] ??= - $this->hasCallableMethod($name = $class::formatActionMethod($action)) - || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) - ? parent::getMethod($name) - : null; + if (!array_key_exists($action, self::$armCache[$class] ?? [])) { + self::$armCache[$class][$action] = + $this->hasCallableMethod($name = $class::formatActionMethod($action)) + || $this->hasCallableMethod($name = $class::formatRenderMethod($action)) + ? parent::getMethod($name) + : null; + } + + return self::$armCache[$class][$action]; } @@ -174,77 +173,6 @@ public function getSignalMethod(string $signal): ?\ReflectionMethod } - /** - * Returns an annotation value. - * @deprecated - * @return ?list - */ - public static function parseAnnotation(\Reflector $ref, string $name): ?array - { - if (!preg_match_all('#[\s*]@' . preg_quote($name, '#') . '(?:\(\s*([^)]*)\s*\)|\s|$)#', (string) $ref->getDocComment(), $m)) { - return null; - } - - $tokens = ['true' => true, 'false' => false, 'null' => null]; - $res = []; - foreach ($m[1] as $s) { - foreach (preg_split('#\s*,\s*#', $s, -1, PREG_SPLIT_NO_EMPTY) ?: ['true'] as $item) { - $res[] = array_key_exists($tmp = strtolower($item), $tokens) - ? $tokens[$tmp] - : $item; - } - } - - $alt = match ($name) { - 'persistent' => '#[Nette\Application\Attributes\Persistent]', - 'deprecated' => '#[Nette\Application\Attributes\Deprecated]', - 'crossOrigin' => '#[Nette\Application\Attributes\Requires(sameOrigin: false)]', - default => 'alternative' - }; - trigger_error("Annotation @$name is deprecated, use $alt (used in " . Reflection::toString($ref) . ')', E_USER_DEPRECATED); - return $res; - } - - - #[\Deprecated] - public function hasAnnotation(string $name): bool - { - return (bool) self::parseAnnotation($this, $name); - } - - - #[\Deprecated] - public function getAnnotation(string $name): mixed - { - $res = self::parseAnnotation($this, $name); - return $res ? end($res) : null; - } - - - public function getMethod($name): MethodReflection - { - return new MethodReflection($this->getName(), $name); - } - - - /** - * @return list - */ - public function getMethods($filter = -1): array - { - foreach ($res = parent::getMethods($filter) as $key => $val) { - $res[$key] = new MethodReflection($this->getName(), $val->getName()); - } - - return $res; - } - - - /** - * @deprecated - * @param array $args - * @return list - */ #[\Deprecated] public static function combineArgs(\ReflectionFunctionAbstract $method, array $args): array { diff --git a/src/Application/UI/Control.php b/src/Application/UI/Control.php index 36fa26be7..0a0f9d142 100644 --- a/src/Application/UI/Control.php +++ b/src/Application/UI/Control.php @@ -104,10 +104,14 @@ public function flashMessage(string|\stdClass|\Stringable $message, string $type 'message' => $message, 'type' => $type, ]; - $messages = $this->getPresenter()->getFlashSession()->get($id); + $session = $this->getPresenter()->getFlashSession(); + $messages = $session->get($id); $messages[] = $flash; - $this->getTemplate()->flashes = $messages; - $this->getPresenter()->getFlashSession()->set($id, $messages); + $session->set($id, $messages); + if (isset($this->template)) { // a not-yet-created template gets flashes injected by the template factory + $this->template->flashes = $messages; + } + return $flash; } diff --git a/src/Application/UI/Form.php b/src/Application/UI/Form.php index b0309f43a..caec7b3a9 100644 --- a/src/Application/UI/Form.php +++ b/src/Application/UI/Form.php @@ -17,6 +17,7 @@ class Form extends Nette\Forms\Form implements SignalReceiver { /** @var array Occurs when form is attached to presenter */ public array $onAnchor = []; + private ?Nette\Http\FetchSite $allowedOrigin = Nette\Http\FetchSite::SameOrigin; public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?string $name = null) @@ -26,6 +27,15 @@ public function __construct(?Nette\ComponentModel\IContainer $parent = null, ?st } + /** + * The form is anchored via a presenter; the source is set when the form is attached to it. + */ + protected function createDefaultSource(): ?Nette\Forms\SubmissionSource + { + return null; + } + + protected function validateParent(Nette\ComponentModel\IContainer $parent): void { parent::validateParent($parent); @@ -39,13 +49,8 @@ protected function validateParent(Nette\ComponentModel\IContainer $parent): void $this->setAction(new Link($presenter, 'this')); } - $controls = $this->getControls(); - if (iterator_count($controls) && $this->isSubmitted()) { - foreach ($controls as $control) { - if (!$control->isDisabled()) { - $control->loadHttpData(); - } - } + if (!$this->isAnchored()) { // the write-once source survives re-anchoring, it resolves the current presenter itself + $this->setSubmissionSource(new FormSubmissionSource); // lets already attached controls load their values } Nette\Utils\Arrays::invoke($this->onAnchor, $this); @@ -81,11 +86,11 @@ public function hasPresenter(): bool /** - * Tells if the form is anchored. + * Disables the same-origin (Sec-Fetch) CSRF check, allowing cross-origin form submissions. */ - public function isAnchored(): bool + public function allowCrossOrigin(): void { - return (bool) $this->getPresenter(throw: false); + $this->allowedOrigin = null; } @@ -96,38 +101,6 @@ public function disableSameSiteProtection(): void } - /** - * Internal: returns submitted HTTP data or null when form was not submitted. - */ - protected function receiveHttpData(): ?array - { - $presenter = $this->getPresenter(); - if (!$presenter->isSignalReceiver($this, 'submit')) { - return null; - } - - $request = $presenter->getRequest(); - if ($request->isMethod('forward') || $request->isMethod('post') !== $this->isMethod('post')) { - return null; - } - - return $this->isMethod('post') - ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) - : $request->getParameters(); - } - - - protected function beforeRender(): void - { - parent::beforeRender(); - $key = ($this->isMethod('post') ? '_' : '') . Presenter::SignalKey; - if (!isset($this[$key]) && $this->getAction() !== '') { - $do = $this->lookupPath(Presenter::class) . self::NameSeparator . 'submit'; - $this[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); - } - } - - /********************* interface SignalReceiver ****************d*g**/ @@ -141,10 +114,10 @@ public function signalReceived(string $signal): void $class = static::class; throw new BadSignalException("Missing handler for signal '$signal' in $class."); - } elseif (!$this->crossOrigin && !$presenter->getHttpRequest()->isFrom(Nette\Http\FetchSite::SameOrigin)) { + } elseif ($this->allowedOrigin && !$presenter->getHttpRequest()->isFrom($this->allowedOrigin)) { $presenter->detectedCsrf(); - } elseif (!$presenter->getRequest()->hasFlag(Nette\Application\Request::RESTORED)) { + } elseif (!$presenter->getRequest()->hasFlag(Nette\Application\Request::Restored)) { $this->fireEvents(); } } diff --git a/src/Application/UI/FormSubmissionSource.php b/src/Application/UI/FormSubmissionSource.php new file mode 100644 index 000000000..8a527717d --- /dev/null +++ b/src/Application/UI/FormSubmissionSource.php @@ -0,0 +1,51 @@ +lookup(Presenter::class); + if (!$presenter->isSignalReceiver($form, 'submit')) { + return null; + } + + $request = $presenter->getRequest(); + if ($request->isMethod('forward') || $request->isMethod('post') !== $form->isMethod('post')) { + return null; + } + + return $form->isMethod('post') + ? Nette\Utils\Arrays::mergeTree($request->getPost(), $request->getFiles()) + : $request->getParameters(); + } + + + /** + * Installs the hidden field carrying the 'submit' signal. + */ + public function prepare(Nette\Forms\Form $form): void + { + $key = ($form->isMethod('post') ? '_' : '') . Presenter::SignalKey; + if (!isset($form[$key]) && $form->getAction() !== '') { + $do = $form->lookupPath(Presenter::class) . Nette\ComponentModel\IComponent::NameSeparator . 'submit'; + $form[$key] = (new Nette\Forms\Controls\HiddenField($do))->setOmitted(); + } + } +} diff --git a/src/Application/UI/MethodReflection.php b/src/Application/UI/MethodReflection.php deleted file mode 100644 index 3bd60cdae..000000000 --- a/src/Application/UI/MethodReflection.php +++ /dev/null @@ -1,31 +0,0 @@ -isDefaultValueAvailable() - ? $param->getDefaultValue() - : null; - if ($args[$name] === $def || ($def === null && $args[$name] === '')) { + $def = $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null; + if ($args[$name] === $def) { $args[$name] = null; // value transmit is unnecessary } } @@ -153,7 +151,8 @@ public static function convertType(mixed &$val, string $types): bool $type === 'scalar' => !is_array($val), // special type due to historical reasons $type === 'mixed' => true, $type === 'callable' => false, // intentionally disabled for security reasons - default => $val instanceof $type, + default => $val instanceof $type + || (is_subclass_of($type, \BackedEnum::class) && self::castEnum($val, $type)), }) { return true; } @@ -189,6 +188,29 @@ private static function castScalar(mixed &$val, string $type): bool } + /** + * Lossless conversion of scalar to backed enum case. + * @param class-string<\BackedEnum> $type + */ + private static function castEnum(mixed &$val, string $type): bool + { + if (!is_scalar($val)) { + return false; + } + + $tmp = $val; + if ( + !self::castScalar($tmp, (string) (new \ReflectionEnum($type))->getBackingType()) + || !($case = $type::tryFrom($tmp)) + ) { + return false; + } + + $val = $case; + return true; + } + + public static function getType(\ReflectionParameter|\ReflectionProperty $item): string { if ($type = $item->getType()) { diff --git a/src/Application/UI/Presenter.php b/src/Application/UI/Presenter.php index e0354e948..23b6578ad 100644 --- a/src/Application/UI/Presenter.php +++ b/src/Application/UI/Presenter.php @@ -69,6 +69,8 @@ abstract class Presenter extends Control implements Application\IPresenter /** @deprecated use Presenter::DefaultAction */ public const DEFAULT_ACTION = self::DefaultAction; + private const MaxSwitches = 20; + public int $invalidLinkMode = self::InvalidLinkSilent; /** @var array Occurs before starup() */ @@ -119,11 +121,6 @@ abstract class Presenter extends Control implements Application\IPresenter private readonly LinkGenerator $linkGenerator; - public function __construct() - { - } - - final public function getRequest(): Application\Request { return $this->request ?? throw new Nette\InvalidStateException('Request is not set, presenter is not running.'); @@ -177,7 +174,7 @@ public function isModuleCurrent(string $module): bool */ public function isForwarded(): bool { - return $this->forwarded || $this->request?->isMethod(Application\Request::FORWARD); + return $this->forwarded || $this->request?->isMethod(Application\Request::Forward); } @@ -210,10 +207,15 @@ public function run(Application\Request $request): Application\Response } // calls $this->action() + $switched = 0; try { actionMethod: $this->tryCall(static::formatActionMethod($this->action), $this->params); } catch (Application\SwitchException $e) { + if (++$switched > self::MaxSwitches) { + throw new \LogicException('Infinite loop of switch() calls detected.', 0, $e); + } + $this->changeAction($e->getMessage()); $this->autoCanonicalize = false; goto actionMethod; @@ -244,6 +246,10 @@ public function run(Application\Request $request): Application\Response renderMethod: $this->tryCall(static::formatRenderMethod($this->view), $this->params); } catch (Application\SwitchException $e) { + if (++$switched > self::MaxSwitches) { + throw new \LogicException('Infinite loop of switch() calls detected.', 0, $e); + } + $this->setView($e->getMessage()); goto renderMethod; } @@ -338,9 +344,23 @@ public function detectedCsrf(): void /** @deprecated use #[Requires(methods: ...)] to specify allowed methods */ protected function checkHttpMethod(): void { - if ($this->allowedMethods && - !in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true) - ) { + if (!$this->allowedMethods) { + return; + } + + $rc = static::getReflection(); + foreach ([static::formatActionMethod($this->action), static::formatRenderMethod($this->action)] as $method) { + if ($rc->hasCallableMethod($method) + && Arrays::some( + $rc->getMethod($method)->getAttributes(Application\Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF), + fn($ra) => $ra->newInstance()->methods !== null, + ) + ) { + return; // #[Requires(methods:)] on the action or render method takes precedence over $allowedMethods + } + } + + if (!in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, true)) { $this->httpResponse->setHeader('Allow', implode(',', $this->allowedMethods)); $this->error("Method $method is not allowed", Nette\Http\IResponse::S405_MethodNotAllowed); } @@ -487,9 +507,8 @@ public function setLayout(string|bool $layout): static /** * @throws Nette\Application\AbortException - * @return never */ - public function sendTemplate(?Template $template = null): void + public function sendTemplate(?Template $template = null): never { $template ??= $this->getTemplate(); $this->completeTemplate($template); @@ -560,7 +579,7 @@ public function findLayoutTemplateFile(): ?string */ public function formatLayoutTemplateFiles(): array { - if (is_string($this->layout) && preg_match('#/|\\\#', $this->layout)) { + if (is_string($this->layout) && strpbrk($this->layout, '/\\') !== false) { return [$this->layout]; } @@ -679,9 +698,8 @@ public function isAjax(): bool /** * Sends AJAX payload to the output. * @throws Nette\Application\AbortException - * @return never */ - public function sendPayload(): void + public function sendPayload(): never { $this->sendResponse(new Responses\JsonResponse($this->getPayload())); } @@ -690,9 +708,8 @@ public function sendPayload(): void /** * Sends JSON data to the output. * @throws Nette\Application\AbortException - * @return never */ - public function sendJson(mixed $data): void + public function sendJson(mixed $data): never { $this->sendResponse(new Responses\JsonResponse($data)); } @@ -704,9 +721,8 @@ public function sendJson(mixed $data): void /** * Sends response and terminates presenter. * @throws Nette\Application\AbortException - * @return never */ - public function sendResponse(Application\Response $response): void + public function sendResponse(Application\Response $response): never { $this->response = $response; $this->terminate(); @@ -716,9 +732,8 @@ public function sendResponse(Application\Response $response): void /** * Correctly terminates presenter. * @throws Nette\Application\AbortException - * @return never */ - public function terminate(): void + public function terminate(): never { throw new Application\AbortException; } @@ -728,9 +743,8 @@ public function terminate(): void * Forward to another presenter or action. * @param array|mixed $args * @throws Nette\Application\AbortException - * @return never */ - public function forward(string|Nette\Application\Request $destination, $args = []): void + public function forward(string|Nette\Application\Request $destination, $args = []): never { if ($destination instanceof Application\Request) { $this->sendResponse(new Responses\ForwardResponse($destination)); @@ -752,9 +766,8 @@ public function forward(string|Nette\Application\Request $destination, $args = [ /** * Redirect to another URL and ends presenter execution. * @throws Nette\Application\AbortException - * @return never */ - public function redirectUrl(string $url, ?int $httpCode = null): void + public function redirectUrl(string $url, ?int $httpCode = null): never { if ($this->isAjax()) { $this->getPayload()->redirect = $url; @@ -819,7 +832,7 @@ public function canonicalize(?string $destination = null, ...$args): void return; } - $code = $request->hasFlag($request::VARYING) + $code = $request->hasFlag($request::Varying) ? Http\IResponse::S302_Found : Http\IResponse::S301_MovedPermanently; $this->sendResponse(new Responses\RedirectResponse($url, $code)); @@ -860,7 +873,7 @@ protected function createRequest(Component $component, string $destination, arra /** - * @return array{absolute: bool, path: string, signal: bool, args: ?array, fragment: string} + * @return array{absolute: bool, path: string, signal: bool, args: mixed[]|null, fragment: string} */ #[\Deprecated] public static function parseDestination(string $destination): array @@ -880,7 +893,7 @@ protected function requestToUrl(Application\Request $request, ?bool $relative = * Invalid link handler. Descendant can override this method to change default behaviour. * @throws InvalidLinkException */ - protected function handleInvalidLink(InvalidLinkException $e): string + protected function processInvalidLink(InvalidLinkException $e): string { if ($this->invalidLinkMode & self::InvalidLinkException) { throw $e; @@ -931,7 +944,7 @@ public function restoreRequest(string $key): void $params[self::FlashKey] = $this->getFlashKey(); $request->setParameters($params); if ($request->isMethod('POST')) { - $request->setFlag(Application\Request::RESTORED, true); + $request->setFlag(Application\Request::Restored); $this->sendResponse(new Responses\ForwardResponse($request)); } else { $this->redirectUrl($this->linkGenerator->requestToUrl($request)); diff --git a/src/Bridges/ApplicationDI/ApplicationExtension.php b/src/Bridges/ApplicationDI/ApplicationExtension.php index 424d67368..417caf104 100644 --- a/src/Bridges/ApplicationDI/ApplicationExtension.php +++ b/src/Bridges/ApplicationDI/ApplicationExtension.php @@ -92,10 +92,16 @@ public function loadConfiguration(): void : UI\Presenter::InvalidLinkWarning; $application = $builder->addDefinition($this->prefix('application')) - ->setFactory(Nette\Application\Application::class); + ->setCreator(Nette\Application\Application::class); if ($config->catchExceptions || !$this->debugMode) { - $application->addSetup('$error4xxPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter]); - $application->addSetup('$errorPresenter', [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter]); + $application->addSetup( + '$error4xxPresenter', + [is_array($config->errorPresenter) ? $config->errorPresenter['4xx'] : $config->errorPresenter], + ); + $application->addSetup( + '$errorPresenter', + [is_array($config->errorPresenter) ? $config->errorPresenter['5xx'] : $config->errorPresenter], + ); } $this->compiler->addExportedType(Nette\Application\Application::class); @@ -108,7 +114,7 @@ public function loadConfiguration(): void $presenterFactory = $builder->addDefinition($this->prefix('presenterFactory')) ->setType(Nette\Application\IPresenterFactory::class) - ->setFactory(Nette\Application\PresenterFactory::class, [new Definitions\Statement( + ->setCreator(Nette\Application\PresenterFactory::class, [new Definitions\Statement( Nette\Bridges\ApplicationDI\PresenterFactoryCallback::class, [1 => $touch ?? null], )]); @@ -124,14 +130,9 @@ public function loadConfiguration(): void } $builder->addDefinition($this->prefix('linkGenerator')) - ->setFactory(Nette\Application\LinkGenerator::class, [ - 1 => new Definitions\Statement([new Definitions\Statement('@Nette\Http\IRequest::getUrl'), 'withoutUserInfo']), + ->setCreator(Nette\Application\LinkGenerator::class, [ + 1 => new Definitions\Statement('@Nette\Http\IRequest::getUrl'), ]); - - if ($this->name === 'application') { - $builder->addAlias('application', $this->prefix('application')); - $builder->addAlias('nette.presenterFactory', $this->prefix('presenterFactory')); - } } @@ -208,7 +209,7 @@ private function findPresenters(): array $classFile = dirname((string) $rc->getFileName()) . '/autoload_classmap.php'; if (is_file($classFile)) { $this->getContainerBuilder()->addDependency($classFile); - $classes = array_merge($classes, array_keys((fn($path) => require $path)($classFile))); + $classes = [...$classes, ...array_keys((fn($path) => require $path)($classFile))]; } } @@ -286,22 +287,51 @@ private function checkPresenter(string $class): void if (($rp->getAttributes($attr = Attributes\Parameter::class) || $rp->getAttributes($attr = Attributes\Persistent::class)) && (!$rp->isPublic() || $rp->isStatic() || $rp->isReadOnly()) ) { - throw new Nette\InvalidStateException(sprintf('Property %s: attribute %s can be used only with public non-static property.', Reflection::toString($rp), $attr)); + throw new Nette\InvalidStateException(sprintf( + 'Property %s: attribute %s can be used only with public non-static property.', + Reflection::toString($rp), + $attr, + )); + } + } + + foreach (['createRequest', 'requestToUrl'] as $method) { + $rm = $rc->getMethod($method); + if ($rm->getDeclaringClass()->getName() === $class) { + trigger_error(sprintf( + 'Method %s is no longer called, overriding it has no effect; links are generated by %s.', + Reflection::toString($rm), + Nette\Application\LinkGenerator::class, + ), E_USER_WARNING); } } $re = $class::formatActionMethod('') . '.|' . $class::formatRenderMethod('') . '.|' . $class::formatSignalMethod('') . '.'; foreach ($rc->getMethods() as $rm) { - if (preg_match("#^(?!handleInvalidLink)($re)#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { - throw new Nette\InvalidStateException(sprintf('Method %s: this method must be public non-static.', Reflection::toString($rm))); + if (preg_match("#^$re#", $rm->getName()) && (!$rm->isPublic() || $rm->isStatic())) { + throw new Nette\InvalidStateException(sprintf( + 'Method %s: this method must be public non-static.', + Reflection::toString($rm), + )); } elseif (preg_match('#^createComponent.#', $rm->getName()) && ($rm->isPrivate() || $rm->isStatic())) { - throw new Nette\InvalidStateException(sprintf('Method %s: this method must be non-private non-static.', Reflection::toString($rm))); + throw new Nette\InvalidStateException(sprintf( + 'Method %s: this method must be non-private non-static.', + Reflection::toString($rm), + )); } elseif ($rm->getAttributes(Attributes\Requires::class, \ReflectionAttribute::IS_INSTANCEOF) && !preg_match("#^$re|createComponent.#", $rm->getName()) ) { - 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)); + 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, + )); } elseif ($rm->getAttributes(Attributes\Deprecated::class) && !preg_match("#^$re#", $rm->getName())) { - 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)); + 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, + )); } } } diff --git a/src/Bridges/ApplicationDI/LatteExtension.php b/src/Bridges/ApplicationDI/LatteExtension.php index 9d8abd149..4b8c839e6 100644 --- a/src/Bridges/ApplicationDI/LatteExtension.php +++ b/src/Bridges/ApplicationDI/LatteExtension.php @@ -21,7 +21,6 @@ * * @property object{ * debugger: bool|'all'|null, - * macros: array, * extensions: array, * templateClass: string|null, * strictTypes: bool, @@ -69,7 +68,7 @@ public function loadConfiguration(): void $builder->addFactoryDefinition($this->prefix('latteFactory')) ->setImplement(ApplicationLatte\LatteFactory::class) ->getResultDefinition() - ->setFactory(Latte\Engine::class) + ->setCreator(Latte\Engine::class) ->addSetup('setCacheDirectory', [$this->tempDir]) ->addSetup('setAutoRefresh', [$this->debugMode]) ->addSetup('setFeature', [Latte\Feature::StrictTypes, $config->strictTypes]) @@ -89,19 +88,14 @@ public function loadConfiguration(): void foreach ($config->extensions as $extension) { if ($extension === Latte\Essential\TranslatorExtension::class) { - $extension = new Statement($extension, [new Nette\DI\Definitions\Reference(Nette\Localization\Translator::class)]); + $extension = new Statement($extension, ['@' . Nette\Localization\Translator::class]); } $this->addExtension($extension); } $builder->addDefinition($this->prefix('templateFactory')) - ->setFactory(ApplicationLatte\TemplateFactory::class) + ->setCreator(ApplicationLatte\TemplateFactory::class) ->setArguments(['templateClass' => $config->templateClass]); - - if ($this->name === 'latte') { - $builder->addAlias('nette.latteFactory', $this->prefix('latteFactory')); - $builder->addAlias('nette.templateFactory', $this->prefix('templateFactory')); - } } diff --git a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php index 3a50f5ef9..5dd920c98 100644 --- a/src/Bridges/ApplicationDI/PresenterFactoryCallback.php +++ b/src/Bridges/ApplicationDI/PresenterFactoryCallback.php @@ -29,10 +29,12 @@ public function __invoke(string $class): Nette\Application\IPresenter { $services = $this->container->findByType($class); if (count($services) > 1) { - $services = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); - if (count($services) > 1) { + $exact = array_values(array_filter($services, fn($service) => $this->container->getServiceType($service) === $class)); + if (count($exact) !== 1) { throw new Nette\Application\InvalidPresenterException("Multiple services of type $class found: " . implode(', ', $services) . '.'); } + + $services = $exact; } if (count($services) === 1) { @@ -43,8 +45,10 @@ public function __invoke(string $class): Nette\Application\IPresenter if ($this->touchToRefresh && class_exists($class)) { touch($this->touchToRefresh); - 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.'; header('Refresh: 5'); + 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.'; exit; } diff --git a/src/Bridges/ApplicationDI/RoutingExtension.php b/src/Bridges/ApplicationDI/RoutingExtension.php index 7ceae3a01..54ac34211 100644 --- a/src/Bridges/ApplicationDI/RoutingExtension.php +++ b/src/Bridges/ApplicationDI/RoutingExtension.php @@ -49,15 +49,11 @@ public function loadConfiguration(): void $builder = $this->getContainerBuilder(); $router = $builder->addDefinition($this->prefix('router')) - ->setFactory(Nette\Application\Routers\RouteList::class); + ->setCreator(Nette\Application\Routers\RouteList::class); foreach ($this->config->routes as $mask => $action) { $router->addSetup('$service->addRoute(?, ?)', [$mask, $action]); } - - if ($this->name === 'routing') { - $builder->addAlias('router', $this->prefix('router')); - } } @@ -66,10 +62,10 @@ public function beforeCompile(): void $builder = $this->getContainerBuilder(); if ( - $this->debugMode && - ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) && - ($name = $builder->getByType(Nette\Application\Application::class)) && - ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition + $this->debugMode + && ($this->config->debugger ?? $builder->getByType(Tracy\Bar::class)) + && ($name = $builder->getByType(Nette\Application\Application::class)) + && ($application = $builder->getDefinition($name)) instanceof Definitions\ServiceDefinition ) { $application->addSetup('@Tracy\Bar::addPanel', [ new Definitions\Statement(Nette\Bridges\ApplicationTracy\RoutingPanel::class), @@ -79,7 +75,7 @@ public function beforeCompile(): void if (!$builder->getByType(Nette\Routing\Router::class)) { $builder->addDefinition($this->prefix('router')) ->setType(Nette\Routing\Router::class) - ->setFactory(Nette\Routing\SimpleRouter::class); + ->setCreator(Nette\Routing\SimpleRouter::class); $builder->addAlias('router', $this->prefix('router')); } } @@ -98,6 +94,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void } $s = serialize($router); + } catch (\Throwable $e) { throw new Nette\DI\ServiceCreationException('Unable to cache router due to error: ' . $e->getMessage(), 0, $e); } diff --git a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php index a93a413b2..43f9d3926 100644 --- a/src/Bridges/ApplicationLatte/Nodes/ControlNode.php +++ b/src/Bridges/ApplicationLatte/Nodes/ControlNode.php @@ -17,7 +17,7 @@ /** - * {control name[:method] [,] [params]} + * {control name[:method] [, params]} * Renders a component by calling its render() method. */ class ControlNode extends StatementNode @@ -39,7 +39,7 @@ public static function create(Tag $tag): static $node->method = $tag->parser->parseExpression(); } - $stream->tryConsume(','); + $tag->parser->consumeCommaBeforeArguments(strict: false); $start = $stream->getIndex(); $node->args = $tag->parser->parseArguments(); $start -= $stream->getIndex(); @@ -77,7 +77,8 @@ public function print(PrintContext $context): string $method = match (true) { !$this->method => 'render', - $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' . ucfirst($this->method->value), + $this->method instanceof StringNode && Strings::match($this->method->value, '#^\w*$#D') => 'render' + . ucfirst($this->method->value), default => "{'render' . " . $this->method->print($context) . '}', }; @@ -88,8 +89,8 @@ public function print(PrintContext $context): string $this->name, ); - if ($this->escape) { - return $context->format( + return $this->escape + ? $context->format( <<<'XX' %raw if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); @@ -105,10 +106,8 @@ public function print(PrintContext $context): string $this->position, Latte\ContentType::Html, new ModifierNode([], $this->escape), - ); - - } else { - return $context->format( + ) + : $context->format( <<<'XX' %raw if ($ʟ_tmp instanceof Nette\Application\UI\Renderable) $ʟ_tmp->redrawControl(null, false); @@ -121,7 +120,6 @@ public function print(PrintContext $context): string $this->args, $this->position, ); - } } diff --git a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php b/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php deleted file mode 100644 index 885ecd242..000000000 --- a/src/Bridges/ApplicationLatte/Nodes/TemplatePrintNode.php +++ /dev/null @@ -1,58 +0,0 @@ -getParameters(), ' . PhpHelpers::dump($this->template ?? Template::class) . '); exit;'; - } - - - /** @param array $params */ - public static function printClass(array $params, string $parentClass): void - { - $bp = new Latte\Essential\Blueprint; - $control = $params['control'] ?? $params['presenter'] ?? null; - $name = 'Template'; - if ($control instanceof UI\Control) { - $name = preg_replace('#(Control|Presenter)$#', '', $control::class) . 'Template'; - unset($params[$control instanceof UI\Presenter ? 'control' : 'presenter']); - } - $class = $bp->generateTemplateClass($params, $name, $parentClass); - $code = (string) $class->getNamespace(); - - $bp->printBegin(); - $bp->printCode($code); - - if ($control instanceof UI\Control) { - $controlFile = (new \ReflectionClass($control))->getFileName() - ?: throw new \LogicException('Cannot determine file of class ' . $control::class . '.'); - $file = dirname($controlFile) . '/' . $class->getName() . '.php'; - if (file_exists($file)) { - echo "unsaved, file {$bp->clickableFile($file)} already exists"; - } else { - echo "saved to file {$bp->clickableFile($file)}"; - file_put_contents($file, "printEnd(); - exit; - } -} diff --git a/src/Bridges/ApplicationLatte/Template.php b/src/Bridges/ApplicationLatte/Template.php index 32378c9e0..adf1ff85a 100644 --- a/src/Bridges/ApplicationLatte/Template.php +++ b/src/Bridges/ApplicationLatte/Template.php @@ -9,6 +9,7 @@ use Latte; use Nette; +use function is_array; /** @@ -33,11 +34,11 @@ final public function getLatte(): Latte\Engine /** * Renders template to output. - * @param array $params + * @param object|array $params */ - public function render(?string $file = null, array $params = []): void + public function render(?string $file = null, object|array $params = []): void { - Nette\Utils\Arrays::toObject($params, $this); + Nette\Utils\Arrays::toObject(is_array($params) ? $params : get_object_vars($params), $this); $file ??= $this->file ?? throw new Nette\InvalidStateException('Template file name was not set.'); $this->latte->render($file, $this); } @@ -45,11 +46,11 @@ public function render(?string $file = null, array $params = []): void /** * Renders template to string. - * @param array $params + * @param object|array $params */ - public function renderToString(?string $file = null, array $params = []): string + public function renderToString(?string $file = null, object|array $params = []): string { - Nette\Utils\Arrays::toObject($params, $this); + Nette\Utils\Arrays::toObject(is_array($params) ? $params : get_object_vars($params), $this); $file ??= $this->file ?? throw new Nette\InvalidStateException('Template file name was not set.'); return $this->latte->renderToString($file, $this); } diff --git a/src/Bridges/ApplicationLatte/TemplateFactory.php b/src/Bridges/ApplicationLatte/TemplateFactory.php index fbc112219..0ed2ac622 100644 --- a/src/Bridges/ApplicationLatte/TemplateFactory.php +++ b/src/Bridges/ApplicationLatte/TemplateFactory.php @@ -28,7 +28,7 @@ public function __construct( private readonly ?Nette\Security\User $user = null, $templateClass = null, ) { - if ($templateClass && (!class_exists($templateClass) || !is_a($templateClass, Template::class, allow_string: true))) { + if ($templateClass && !is_a($templateClass, Template::class, allow_string: true)) { throw new Nette\InvalidArgumentException("Class $templateClass does not implement " . Template::class . ' or it does not exist.'); } @@ -41,7 +41,7 @@ public function __construct( * @param ?class-string $class * @return ($class is null ? Template : T) */ - public function createTemplate(?UI\Control $control = null, ?string $class = null): UI\Template + public function createTemplate(?UI\Control $control = null, ?string $class = null): Template { $class ??= $this->templateClass; if (!is_a($class, Template::class, allow_string: true)) { @@ -62,7 +62,7 @@ private function injectDefaultVariables(Template $template, ?UI\Control $control { $presenter = $control?->getPresenter(throw: false); $baseUrl = $this->httpRequest - ? rtrim($this->httpRequest->getUrl()->withoutUserInfo()->getBaseUrl(), '/') + ? rtrim($this->httpRequest->getUrl()->getBaseUrl(), '/') : null; $flashes = $presenter instanceof UI\Presenter && $presenter->hasFlashSession() ? (array) $presenter->getFlashSession()->get($control->getParameterId('flash')) diff --git a/src/Bridges/ApplicationLatte/UIExtension.php b/src/Bridges/ApplicationLatte/UIExtension.php index b934e59bd..25b338670 100644 --- a/src/Bridges/ApplicationLatte/UIExtension.php +++ b/src/Bridges/ApplicationLatte/UIExtension.php @@ -34,11 +34,13 @@ public function getFilters(): array 'modifyDate' => fn($time, $delta, $unit = null) => $time ? Nette\Utils\DateTime::from($time)->modify($delta . $unit) : null, - ] + ($presenter ? [ - 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null + ] + ($presenter + ? [ + 'absoluteUrl' => fn(\Stringable|string|null $link): ?string => $link === null ? null : $presenter->getHttpRequest()->getUrl()->resolve((string) $link)->getAbsoluteUrl(), - ] : []); + ] + : []); } @@ -77,7 +79,6 @@ public function getTags(): array 'plink' => Nodes\LinkNode::create(...), 'link' => Nodes\LinkNode::create(...), 'linkBase' => Nodes\LinkBaseNode::create(...), - 'templatePrint' => Nodes\TemplatePrintNode::create(...), 'snippet' => Nodes\SnippetNode::create(...), 'snippetArea' => Nodes\SnippetAreaNode::create(...), 'layout' => $this->createExtendsNode(...), @@ -95,6 +96,12 @@ public function getPasses(): array } + public function getCacheKey(Latte\Engine $engine): mixed + { + return ['snippetAttribute' => Nodes\SnippetNode::$snippetAttribute]; + } + + /** * Render snippets instead of template in snippet-mode. */ @@ -121,7 +128,7 @@ private function findNonce(Nette\Http\IResponse $httpResponse): ?string { $header = $httpResponse->getHeader('Content-Security-Policy') ?: $httpResponse->getHeader('Content-Security-Policy-Report-Only'); - return preg_match('#\s\'nonce-([\w+/]+=*)\'#', (string) $header, $m) ? $m[1] : null; + return preg_match('#(?:^|\s)\'nonce-([\w+/-]+=*)\'#', (string) $header, $m) ? $m[1] : null; } @@ -130,7 +137,9 @@ public static function createExtendsNode(Tag $tag): ExtendsNode $auto = $tag->parser->stream->is('auto'); $node = ExtendsNode::create($tag); if ($auto) { - $node->extends = new AuxiliaryNode(fn() => '($this->getReferringTemplate() ? null : $this->global->uiPresenter->findLayoutTemplateFile())'); + $node->extends = new AuxiliaryNode( + fn() => '($this->getReferringTemplate() ? null : $this->global->uiPresenter->findLayoutTemplateFile())', + ); } return $node; } diff --git a/src/Bridges/ApplicationTracy/RoutingPanel.php b/src/Bridges/ApplicationTracy/RoutingPanel.php index 6ada0036d..ba6b77342 100644 --- a/src/Bridges/ApplicationTracy/RoutingPanel.php +++ b/src/Bridges/ApplicationTracy/RoutingPanel.php @@ -68,6 +68,19 @@ public function getPanel(): string } + public function getAgentInfo(): string + { + return Nette\Utils\Helpers::capture(function () { + $matched = $this->matched; + $routes = $this->routes; + $source = $this->matched ? $this->findSource() : null; + $url = $this->httpRequest->getUrl(); + $method = $this->httpRequest->getMethod(); + require __DIR__ . '/dist/panel.agent.phtml'; + }); + } + + /** @return array{path: string, domain: ?string, module: string, routes: array} */ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRequest): array { @@ -88,7 +101,7 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe continue; } - $matched = $flags[$i] & $router::ONE_WAY ? 'oneway' : 'no'; + $matched = empty($flags[$i]['oneWay']) ? 'no' : 'oneway'; $params = $e = null; try { if ( @@ -109,7 +122,9 @@ private function analyse(Routing\RouteList $router, ?Nette\Http\IRequest $httpRe $res['routes'][] = (object) [ 'matched' => $matched, 'class' => $innerRouter::class, - 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter ? $innerRouter->getDefaults() : [], + 'defaults' => $innerRouter instanceof Routing\Route || $innerRouter instanceof Routing\SimpleRouter + ? $innerRouter->getDefaults() + : [], 'mask' => $innerRouter instanceof Routing\Route ? $innerRouter->getMask() : null, 'params' => $params, 'error' => $e, diff --git a/src/Bridges/ApplicationTracy/dist/panel.agent.phtml b/src/Bridges/ApplicationTracy/dist/panel.agent.phtml new file mode 100644 index 000000000..edd9d73b5 --- /dev/null +++ b/src/Bridges/ApplicationTracy/dist/panel.agent.phtml @@ -0,0 +1,131 @@ + $matched */ +/** @var array{path:string,domain:?string,module:string,routes:array} $routes */ +/** @var \ReflectionClass|\ReflectionMethod|string|null $source */ +/** @var Nette\Http\UrlScript $url */ +/** @var string $method */ +use Nette\Application\UI\Presenter; +echo "\n"; +$_blocks['route'] = function ($route,$path) use (&$_blocks) { + $mark = ['yes' => 'MATCHED', 'may' => 'MAY MATCH', 'no' => 'no match', 'oneway' => 'one-way', 'error' => 'ERROR'][$route->matched] ?? $route->matched /* pos 9:2 */; + $mask = $route->mask !== null ? $path . $route->mask : $route->class /* pos 10:2 */; + $defaults = [] /* pos 11:2 */; + foreach ($route->defaults as $k => $v) /* pos 12:2 */ { + $defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v))) /* pos 12:40 */; + } + + echo '- ['; + echo Tracy\Helpers::escapeMd($mark) /* pos 13:6 */; + echo '] `'; + echo $mask /* pos 13:16 */; + echo '`'; + if ($defaults) /* pos 13:33 */ { + echo ' ('; + echo Tracy\Helpers::escapeMd(implode(', ', $defaults)) /* pos 13:49 */; + echo ')'; + } + if ($route->error) /* pos 13:82 */ { + echo ' — '; + echo Tracy\Helpers::escapeMd($route->error->getMessage()) /* pos 13:105 */; + } + echo "\n"; +}; +echo "\n"; +$_blocks['routeList'] = function ($list,$path) use (&$_blocks) { + if ($list['domain']) /* pos 17:2 */ { + echo '- domain: `'; + echo Tracy\Helpers::escapeMd($list['domain']) /* pos 17:31 */; + echo '` +'; + } + if ($list['module']) /* pos 19:2 */ { + echo '- module: `'; + echo Tracy\Helpers::escapeMd($list['module']) /* pos 19:31 */; + echo '` +'; + } + $path .= $list['path'] /* pos 21:2 */; + foreach ($list['routes'] as $router) /* pos 22:2 */ { + if (is_array($router)) /* pos 23:3 */ { + $_blocks['routeList']($router,$path); + } else /* pos 25:3 */ { + $_blocks['route']($router,$path); + } + + } + +}; +echo ' +## Routing + +`'; +echo Tracy\Helpers::escapeMd($method) /* pos 33:2 */; +echo ' '; +echo Tracy\Helpers::escapeMd($url->getAbsoluteUrl()) /* pos 33:12 */; +echo '` + +'; +if ($matched === null) /* pos 35:1 */ { + echo '**Matched:** no route +'; +} else /* pos 37:1 */ { + $target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) /* pos 38:2 */; + echo '**Matched:** '; + echo Tracy\Helpers::escapeMd($target) /* pos 39:15 */; + if (isset($matched[Presenter::SignalKey])) /* pos 39:24 */ { + echo Tracy\Helpers::escapeMd($matched[Presenter::SignalKey]) /* pos 39:66 */; + echo '!'; + } + echo "\n"; + if (is_string($source)) /* pos 40:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source) /* pos 41:16 */; + echo ' (class not found) +'; + } elseif ($source instanceof \ReflectionMethod) /* pos 42:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source->getDeclaringClass()->getName()) /* pos 43:16 */; + echo '::'; + echo Tracy\Helpers::escapeMd($source->getName()) /* pos 43:59 */; + echo '() in '; + echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 43:85 */; + echo ':'; + echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 43:110 */; + echo "\n"; + } elseif ($source instanceof \ReflectionClass) /* pos 44:2 */ { + echo '**Handler:** '; + echo Tracy\Helpers::escapeMd($source->getName()) /* pos 45:16 */; + echo ' in '; + echo Tracy\Helpers::escapeMd($source->getFileName()) /* pos 45:40 */; + echo ':'; + echo Tracy\Helpers::escapeMd($source->getStartLine()) /* pos 45:65 */; + echo "\n"; + } + + + $skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1] /* pos 47:2 */; + $extras = array_diff_key($matched, $skip) /* pos 48:2 */; + if ($extras) /* pos 49:2 */ { + echo '**Parameters:** +'; + foreach ($extras as $key => $value) /* pos 51:3 */ { + echo '- '; + echo Tracy\Helpers::escapeMd($key) /* pos 52:6 */; + echo ' = '; + echo Tracy\Helpers::escapeMd(is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))) /* pos 52:15 */; + echo "\n"; + + } + + } +} +echo ' +### Routes +'; +if (empty($routes['routes'])) /* pos 58:1 */ { + echo '(none defined) +'; +} else /* pos 60:1 */ { + $_blocks['routeList']($routes,''); +} diff --git a/src/Bridges/ApplicationTracy/dist/panel.phtml b/src/Bridges/ApplicationTracy/dist/panel.phtml index 724c4db46..a16d5796f 100644 --- a/src/Bridges/ApplicationTracy/dist/panel.phtml +++ b/src/Bridges/ApplicationTracy/dist/panel.phtml @@ -53,16 +53,16 @@ echo ' padding: 2px 5px; } - #tracy-debug .nette-RoutingPanel-status-yes { - background: #BDE678 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-yes { + background: #BDE678; } - #tracy-debug .nette-RoutingPanel-status-may { - background: #C1D3FF !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-may { + background: #C1D3FF; } - #tracy-debug .nette-RoutingPanel-status-error { - background: #ffd2c3 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-error { + background: #ffd2c3; } #tracy-debug .nette-RoutingPanel-symbol { @@ -225,7 +225,8 @@ if (empty($routes['routes'])) /* pos 111:2 */ { if (is_string($value)) /* pos 155:28 */ { echo Tracy\Helpers::escapeHtml($value) /* pos 155:50 */; echo '
'; - echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 155:62 */; + } else /* pos 155:62 */ { + echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 155:68 */; } echo "\n"; @@ -257,7 +258,8 @@ if (empty($routes['routes'])) /* pos 111:2 */ { if (is_string($value)) /* pos 170:29 */ { echo Tracy\Helpers::escapeHtml($value) /* pos 170:51 */; echo '
'; - echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 170:63 */; + } else /* pos 170:63 */ { + echo Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true]) /* pos 170:69 */; } echo "\n"; diff --git a/src/Bridges/ApplicationTracy/dist/tab.phtml b/src/Bridges/ApplicationTracy/dist/tab.phtml index 7f66097f5..75934178a 100644 --- a/src/Bridges/ApplicationTracy/dist/tab.phtml +++ b/src/Bridges/ApplicationTracy/dist/tab.phtml @@ -18,7 +18,8 @@ if ($matched === null) /* pos 10:3 */ { echo ':'; echo Tracy\Helpers::escapeHtml($matched[Presenter::ActionKey] ?? Presenter::DefaultAction) /* pos 13:40 */; if (isset($matched[Presenter::SignalKey])) /* pos 13:100 */ { - echo Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) /* pos 13:142 */; + echo ' '; + echo Tracy\Helpers::escapeHtml($matched[Presenter::SignalKey]) /* pos 13:143 */; echo '!'; } echo "\n"; diff --git a/src/Bridges/ApplicationTracy/panel.agent.latte b/src/Bridges/ApplicationTracy/panel.agent.latte new file mode 100644 index 000000000..8efd97b14 --- /dev/null +++ b/src/Bridges/ApplicationTracy/panel.agent.latte @@ -0,0 +1,62 @@ +{varType ?array $matched} +{varType array{path: string, domain: ?string, module: string, routes: array} $routes} +{varType \ReflectionClass|\ReflectionMethod|string|null $source} +{varType Nette\Http\UrlScript $url} +{varType string $method} +{use Nette\Application\UI\Presenter} + +{define route $route, $path} + {do $mark = [yes => 'MATCHED', may => 'MAY MATCH', no => 'no match', oneway => 'one-way', error => 'ERROR'][$route->matched] ?? $route->matched} + {do $mask = $route->mask !== null ? $path . $route->mask : $route->class} + {do $defaults = []} + {foreach $route->defaults as $k => $v}{do $defaults[] = $k . '=' . (is_string($v) ? $v : (is_scalar($v) ? var_export($v, true) : get_debug_type($v)))}{/foreach} + - [{$mark}] `{$mask|noescape}`{if $defaults} ({=implode(', ', $defaults)}){/if}{if $route->error} — {$route->error->getMessage()}{/if} +{/define} + +{define routeList $list, $path} + {if $list[domain]}- domain: `{$list[domain]}` + {/if} + {if $list[module]}- module: `{$list[module]}` + {/if} + {do $path .= $list[path]} + {foreach $list[routes] as $router} + {if is_array($router)} + {include routeList $router, $path} + {else} + {include route $router, $path} + {/if} + {/foreach} +{/define} + +## Routing + +`{$method} {$url->getAbsoluteUrl()}` + +{if $matched === null} + **Matched:** no route +{else} + {do $target = ($matched[Presenter::PresenterKey] ?? '?') . ':' . ($matched[Presenter::ActionKey] ?? Presenter::DefaultAction)} + **Matched:** {$target}{if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if} + {if is_string($source)} + **Handler:** {$source} (class not found) + {elseif $source instanceof \ReflectionMethod} + **Handler:** {$source->getDeclaringClass()->getName()}::{$source->getName()}() in {$source->getFileName()}:{$source->getStartLine()} + {elseif $source instanceof \ReflectionClass} + **Handler:** {$source->getName()} in {$source->getFileName()}:{$source->getStartLine()} + {/if} + {do $skip = [Presenter::PresenterKey => 1, Presenter::ActionKey => 1, Presenter::SignalKey => 1]} + {do $extras = array_diff_key($matched, $skip)} + {if $extras} + **Parameters:** + {foreach $extras as $key => $value} + - {$key} = {is_string($value) ? $value : (is_scalar($value) ? var_export($value, true) : get_debug_type($value))} + {/foreach} + {/if} +{/if} + +### Routes +{if empty($routes[routes])} + (none defined) +{else} + {include routeList $routes, ''} +{/if} diff --git a/src/Bridges/ApplicationTracy/panel.latte b/src/Bridges/ApplicationTracy/panel.latte index 972bfd3f8..07d4c6c5e 100644 --- a/src/Bridges/ApplicationTracy/panel.latte +++ b/src/Bridges/ApplicationTracy/panel.latte @@ -51,16 +51,16 @@ padding: 2px 5px; } - #tracy-debug .nette-RoutingPanel-status-yes { - background: #BDE678 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-yes { + background: #BDE678; } - #tracy-debug .nette-RoutingPanel-status-may { - background: #C1D3FF !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-may { + background: #C1D3FF; } - #tracy-debug .nette-RoutingPanel-status-error { - background: #ffd2c3 !important; + #tracy-debug .nette-RoutingPanel-grid-columns.nette-RoutingPanel-status-error { + background: #ffd2c3; } #tracy-debug .nette-RoutingPanel-symbol { @@ -152,7 +152,7 @@
{foreach $route->defaults as $key => $value} - {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} + {$key} = {if is_string($value)}{$value}
{else}{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} {/foreach}
@@ -167,7 +167,7 @@ {do $params = array_diff_key($params, [Presenter::PresenterKey => 1, Presenter::ActionKey => 1])} {/if} {foreach $params as $key => $value} - {$key} = {if is_string($value)}{$value}
{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} + {$key} = {if is_string($value)}{$value}
{else}{Dumper::toHtml($value, [Dumper::COLLAPSE => true, Dumper::LIVE => true])}{/if} {/foreach} {elseif $route->error} diff --git a/src/Bridges/ApplicationTracy/tab.latte b/src/Bridges/ApplicationTracy/tab.latte index 7fa668ecd..1c3e41b6d 100644 --- a/src/Bridges/ApplicationTracy/tab.latte +++ b/src/Bridges/ApplicationTracy/tab.latte @@ -10,7 +10,7 @@ {if $matched === null} no route {elseif isset($matched[Presenter::PresenterKey])} - {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])}{$matched[Presenter::SignalKey]}!{/if} + {$matched[Presenter::PresenterKey]}:{$matched[Presenter::ActionKey] ?? Presenter::DefaultAction}{if isset($matched[Presenter::SignalKey])} {$matched[Presenter::SignalKey]}!{/if} {/if} diff --git a/tests/Application/Application.run.phpt b/tests/Application/Application.run.phpt index d7381ef68..49d465d14 100644 --- a/tests/Application/Application.run.phpt +++ b/tests/Application/Application.run.phpt @@ -5,7 +5,6 @@ use Nette\Application\Responses\{ForwardResponse, TextResponse}; use Nette\Routing\Router; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -78,6 +77,7 @@ class UIErrorPresenter extends Nette\Application\UI\Presenter $httpRequest = Mockery::mock(Nette\Http\IRequest::class); $httpRequest->shouldReceive('getMethod')->andReturn('GET'); +$httpRequest->shouldReceive('isMethod')->with(Request::Forward)->andReturn(false); $httpRequest->shouldReceive('getPost')->andReturn([]); $httpRequest->shouldReceive('getFiles')->andReturn([]); $httpRequest->shouldReceive('isSecured')->andReturn(false); @@ -114,7 +114,7 @@ test('error presenter handling for route mismatch', function () use ($httpReques $requests = $app->getRequests(); Assert::count(1, $requests); - Assert::same(Request::FORWARD, $requests[0]->getMethod()); + Assert::same(Request::Forward, $requests[0]->getMethod()); Assert::same('Error', $requests[0]->getPresenterName()); Assert::equal($requests[0], $errorPresenter->request); @@ -142,7 +142,7 @@ test('error presenter invoked directly via router', function () use ($httpReques Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Error', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); @@ -152,6 +152,32 @@ test('error presenter invoked directly via router', function () use ($httpReques }); +test('error presenter is not reachable by HTTP method FORWARD', function () use ($httpResponse) { + foreach (['FORWARD', 'forward'] as $method) { + $httpRequest = new Nette\Http\Request(new Nette\Http\UrlScript, method: $method); + $errorPresenter = new ErrorPresenter; + + $presenterFactory = Mockery::mock(IPresenterFactory::class); + $presenterFactory->shouldReceive('createPresenter')->with('Error')->andReturn($errorPresenter); + + $router = Mockery::mock(Router::class); + $router->shouldReceive('match')->andReturn(['presenter' => 'Error']); + + $app = new Application($presenterFactory, $router, $httpRequest, $httpResponse); + $app->errorPresenter = 'Error'; + $app->run(); + + $requests = $app->getRequests(); + Assert::count(1, $requests); + Assert::same(Request::Forward, $requests[0]->getMethod()); + Assert::null($requests[0]->getParameter('request')); + $e = $errorPresenter->request->getParameter('exception'); + Assert::type(BadRequestException::class, $e); + Assert::same('Invalid request. Method FORWARD is reserved for internal use.', $e->getMessage()); + } +}); + + testException('missing presenter exception without error presenter', function () use ($httpRequest, $httpResponse) { $presenterFactory = Mockery::mock(IPresenterFactory::class); $presenterFactory->shouldReceive('createPresenter')->with('Missing')->andThrow(Nette\Application\InvalidPresenterException::class); @@ -183,7 +209,7 @@ test('missing presenter fallback to error presenter', function () use ($httpRequ Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Missing', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); @@ -225,7 +251,7 @@ test('presenter exception propagation to error presenter', function () use ($htt Assert::count(2, $requests); Assert::same('GET', $requests[0]->getMethod()); Assert::same('Bad', $requests[0]->getPresenterName()); - Assert::same(Request::FORWARD, $requests[1]->getMethod()); + Assert::same(Request::Forward, $requests[1]->getMethod()); Assert::same('Error', $requests[1]->getPresenterName()); Assert::equal($requests[1], $errorPresenter->request); diff --git a/tests/Application/Helpers.splitName().phpt b/tests/Application/Helpers.splitName().phpt index 90f29b2d9..1ca77bd5a 100644 --- a/tests/Application/Helpers.splitName().phpt +++ b/tests/Application/Helpers.splitName().phpt @@ -7,7 +7,6 @@ use Nette\Application\Helpers; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Application/MicroPresenter.invoke.phpt b/tests/Application/MicroPresenter.invoke.phpt index db8f1aa2a..a989d66c7 100644 --- a/tests/Application/MicroPresenter.invoke.phpt +++ b/tests/Application/MicroPresenter.invoke.phpt @@ -7,7 +7,6 @@ use Nette\Application\Request; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -30,7 +29,10 @@ test('callback invocation with parameters', function () { test('dependency injection in callback', function () { $container = Mockery::mock(Nette\DI\Container::class) - ->shouldReceive('getByType')->with('stdClass', false)->once()->andReturn(new stdClass) + ->shouldReceive('getByType') + ->with('stdClass', false) + ->once() + ->andReturn(new stdClass) ->mock(); $presenter = new NetteModule\MicroPresenter($container); diff --git a/tests/Application/MicroPresenter.response.phpt b/tests/Application/MicroPresenter.response.phpt index 966002840..600b337a5 100644 --- a/tests/Application/MicroPresenter.response.phpt +++ b/tests/Application/MicroPresenter.response.phpt @@ -8,7 +8,6 @@ use Nette\Application\Request; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -30,7 +29,7 @@ function createContainer() $latteFactory = Mockery::mock(Nette\Bridges\ApplicationLatte\LatteFactory::class); $latteFactory->shouldReceive('create')->andReturn(new Latte\Engine); $container = Mockery::mock(Nette\DI\Container::class); - $container->shouldReceive('getByType')->with('Nette\Bridges\ApplicationLatte\LatteFactory')->andReturn($latteFactory); + $container->shouldReceive('getByType')->with(Nette\Bridges\ApplicationLatte\LatteFactory::class)->andReturn($latteFactory); return $container; } diff --git a/tests/Application/Presenter.twoDomains.phpt b/tests/Application/Presenter.twoDomains.phpt index 47cbbaef0..bbf089ad2 100644 --- a/tests/Application/Presenter.twoDomains.phpt +++ b/tests/Application/Presenter.twoDomains.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -31,7 +30,7 @@ function testLink($domain) new Application\Routers\SimpleRouter, ); - $request = new Application\Request('Test', Http\Request::Get, []); + $request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::same('http://' . $domain . '/index.php?action=default&presenter=Test', $presenter->link('//this')); diff --git a/tests/Application/PresenterFactory.formatPresenterClass.phpt b/tests/Application/PresenterFactory.formatPresenterClass.phpt index 080709ef3..32f18aa91 100644 --- a/tests/Application/PresenterFactory.formatPresenterClass.phpt +++ b/tests/Application/PresenterFactory.formatPresenterClass.phpt @@ -7,7 +7,6 @@ use Nette\Application\PresenterFactory; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -92,6 +91,25 @@ test('global wildcard mapping strategy', function () { }); +test('mapping of a submodule', function () { + $factory = new PresenterFactory; + + $factory->setMapping([ + 'Foo' => 'My\App\*Mod\*Presenter', + 'Foo:Bar' => 'Other\App\*Mod\*Presenter', + 'Foo:Bar:Baz' => 'Third\*Presenter', + ]); + + Assert::same('App\Presentation\Foo\FooPresenter', $factory->formatPresenterClass('Foo')); + Assert::same('My\App\BarPresenter', $factory->formatPresenterClass('Foo:Bar')); + Assert::same('My\App\QuxMod\BarPresenter', $factory->formatPresenterClass('Foo:Qux:Bar')); + Assert::same('Other\App\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Baz')); + Assert::same('Other\App\QuxMod\BazPresenter', $factory->formatPresenterClass('Foo:Bar:Qux:Baz')); + Assert::same('Third\QuxPresenter', $factory->formatPresenterClass('Foo:Bar:Baz:Qux')); + Assert::same('App\Presentation\Bar\Baz\BazPresenter', $factory->formatPresenterClass('Bar:Baz')); +}); + + test('array-based hierarchical mapping', function () { $factory = new PresenterFactory; $factory->setMapping([ diff --git a/tests/Application/Request.phpt b/tests/Application/Request.phpt index 42d61d576..9f8de1d38 100644 --- a/tests/Application/Request.phpt +++ b/tests/Application/Request.phpt @@ -7,7 +7,6 @@ use Nette\Application\Request; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Bridges.DI/ApplicationExtension.basic.phpt b/tests/Bridges.DI/ApplicationExtension.basic.phpt index b5c89830d..39b40903c 100644 --- a/tests/Bridges.DI/ApplicationExtension.basic.phpt +++ b/tests/Bridges.DI/ApplicationExtension.basic.phpt @@ -4,18 +4,18 @@ use Nette\Bridges\ApplicationDI\ApplicationExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; test('', function () { $compiler = new DI\Compiler; - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest') + ->setCreator(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->setClassName('Container1')->compile(); eval($code); diff --git a/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt b/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt new file mode 100644 index 000000000..de3ebc2e5 --- /dev/null +++ b/tests/Bridges.DI/ApplicationExtension.checkPresenter.phpt @@ -0,0 +1,40 @@ + $this->checkPresenter($class))->call(new ApplicationExtension); +} + + +Assert::error( + fn() => checkPresenter(ChildPresenter::class), + E_USER_WARNING, + 'Method OverridingPresenter::createRequest() is no longer called, overriding it has no effect; links are generated by Nette\Application\LinkGenerator.', +); + +Assert::noError(fn() => checkPresenter(Application\UI\Presenter::class)); diff --git a/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt b/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt index d8117e63b..4389cab21 100644 --- a/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt +++ b/tests/Bridges.DI/ApplicationExtension.invalidLink.phpt @@ -5,7 +5,6 @@ use Nette\Bridges\ApplicationDI\ApplicationExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/files/MyPresenter.php'; @@ -15,9 +14,10 @@ function createCompiler(string $config): DI\Compiler $compiler = new DI\Compiler; $compiler->loadConfig(Tester\FileMock::create($config, 'neon')); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest') + ->setCreator(Nette\Http\Request::class, [new DI\Definitions\Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); return $compiler; } @@ -30,7 +30,7 @@ test('silentLinks enabled with debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(true)); + $compiler->addExtension('application', new ApplicationExtension(debugMode: true)); $code = $compiler->setClassName('Container4')->compile(); eval($code); @@ -50,7 +50,7 @@ test('silentLinks disabled with debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(true)); + $compiler->addExtension('application', new ApplicationExtension(debugMode: true)); $code = $compiler->setClassName('Container5')->compile(); eval($code); @@ -70,7 +70,7 @@ test('silentLinks enabled without debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $code = $compiler->setClassName('Container6')->compile(); eval($code); @@ -90,7 +90,7 @@ test('silentLinks disabled without debugger', function () { services: presenter: Presenter1 '); - $compiler->addExtension('application', new ApplicationExtension(false)); + $compiler->addExtension('application', new ApplicationExtension); $code = $compiler->setClassName('Container7')->compile(); eval($code); diff --git a/tests/Bridges.DI/ApplicationExtension.scan.phpt b/tests/Bridges.DI/ApplicationExtension.scan.phpt index af97615b5..7bf2ace28 100644 --- a/tests/Bridges.DI/ApplicationExtension.scan.phpt +++ b/tests/Bridges.DI/ApplicationExtension.scan.phpt @@ -5,7 +5,6 @@ use Nette\DI; use Nette\DI\Definitions\Statement; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/files/MyPresenter.php'; @@ -15,9 +14,9 @@ test('default presenter registration', function () { $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->setClassName('Container1')->compile(); eval($code); @@ -33,9 +32,9 @@ test('scanDirs with filter', function () { $compiler->addExtension('application', new ApplicationExtension); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $code = $compiler->addConfig([ 'application' => [ 'scanDirs' => [__DIR__ . '/files'], @@ -56,9 +55,9 @@ test('combined scanDirs and config setup', function () { $compiler->addExtension('application', new ApplicationExtension(false, [__DIR__ . '/files'])); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); $loader = new DI\Config\Loader; $config = $loader->load(Tester\FileMock::create(' application: @@ -92,13 +91,17 @@ test('robotLoader discovery', function () { $compiler->addExtension('application', new ApplicationExtension(false, null, null, $robot)); $builder = $compiler->getContainerBuilder(); - $builder->addDefinition('myRouter')->setFactory(Nette\Application\Routers\SimpleRouter::class); - $builder->addDefinition('myHttpRequest')->setFactory(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); - $builder->addDefinition('myHttpResponse')->setFactory(Nette\Http\Response::class); - $code = $compiler->setClassName('Container4')->compile(); + $builder->addDefinition('myRouter')->setCreator(Nette\Application\Routers\SimpleRouter::class); + $builder->addDefinition('myHttpRequest')->setCreator(Nette\Http\Request::class, [new Statement(Nette\Http\UrlScript::class)]); + $builder->addDefinition('myHttpResponse')->setCreator(Nette\Http\Response::class); + $code = $compiler->addConfig([ + 'application' => [ + 'scanFilter' => '*Presenter*', + ], + ])->setClassName('Container4')->compile(); eval($code); - $container = new Container2; + $container = new Container4; Assert::count(3, $container->findByType(BasePresenter::class)); Assert::count(1, $container->findByType(Presenter1::class)); Assert::count(1, $container->findByType(Presenter2::class)); diff --git a/tests/Bridges.DI/LatteExtension.basic.phpt b/tests/Bridges.DI/LatteExtension.basic.phpt index 7acde0faf..944ee5bff 100644 --- a/tests/Bridges.DI/LatteExtension.basic.phpt +++ b/tests/Bridges.DI/LatteExtension.basic.phpt @@ -4,6 +4,8 @@ * Test: LatteExtension v3 */ +// dresscode:disable hygiene.defaultArgument -- the named arguments say what the call does + use Nette\DI; use Tester\Assert; @@ -12,12 +14,9 @@ require __DIR__ . '/../bootstrap.php'; class MyExtension extends Latte\Extension { - public $arg; - - - public function __construct($arg = null) - { - $this->arg = $arg; + public function __construct( + public $arg = null, + ) { } } diff --git a/tests/Bridges.DI/RoutingExtension.basic.phpt b/tests/Bridges.DI/RoutingExtension.basic.phpt index 5b45556f7..c20a3816b 100644 --- a/tests/Bridges.DI/RoutingExtension.basic.phpt +++ b/tests/Bridges.DI/RoutingExtension.basic.phpt @@ -8,7 +8,6 @@ use Nette\Bridges\ApplicationDI\RoutingExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -22,7 +21,7 @@ test('', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container1')->compile(); eval($code); diff --git a/tests/Bridges.DI/RoutingExtension.cache.phpt b/tests/Bridges.DI/RoutingExtension.cache.phpt index 99233b416..ef97ce23d 100644 --- a/tests/Bridges.DI/RoutingExtension.cache.phpt +++ b/tests/Bridges.DI/RoutingExtension.cache.phpt @@ -9,7 +9,6 @@ use Nette\Bridges\ApplicationDI\RoutingExtension; use Nette\DI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -43,7 +42,7 @@ test('router without cache', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container1')->compile(); eval($code); @@ -64,7 +63,7 @@ test('router with cache', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container2')->compile(); eval($code); @@ -91,6 +90,6 @@ testException('non-cacheable router factory', function () { ', 'neon')); $compiler = new DI\Compiler; - $compiler->addExtension('routing', new RoutingExtension(false)); + $compiler->addExtension('routing', new RoutingExtension); $code = $compiler->addConfig($config)->setClassName('Container3')->compile(); }, Nette\DI\ServiceCreationException::class, 'Unable to cache router due to error: %a%'); diff --git a/tests/Bridges.Latte/Template.getParentName().phpt b/tests/Bridges.Latte/Template.getParentName().phpt index 4579424a5..80bc51163 100644 --- a/tests/Bridges.Latte/Template.getParentName().phpt +++ b/tests/Bridges.Latte/Template.getParentName().phpt @@ -7,9 +7,12 @@ require __DIR__ . '/../bootstrap.php'; Tester\Environment::bypassFinals(); $presenter = Mockery::mock(Nette\Application\UI\Presenter::class) - ->shouldReceive('getPresenter')->andReturnSelf() - ->shouldReceive('getHttpResponse')->andReturn(Mockery::mock(Nette\Http\IResponse::class)->shouldIgnoreMissing()) - ->shouldReceive('findLayoutTemplateFile')->andReturn('layout.latte') + ->shouldReceive('getPresenter') + ->andReturnSelf() + ->shouldReceive('getHttpResponse') + ->andReturn(Mockery::mock(Nette\Http\IResponse::class)->shouldIgnoreMissing()) + ->shouldReceive('findLayoutTemplateFile') + ->andReturn('layout.latte') ->mock(); $latte = new Latte\Engine; diff --git a/tests/Bridges.Latte/Template.render.phpt b/tests/Bridges.Latte/Template.render.phpt new file mode 100644 index 000000000..b4ecb5ba7 --- /dev/null +++ b/tests/Bridges.Latte/Template.render.phpt @@ -0,0 +1,32 @@ +setLoader(new Latte\Loaders\StringLoader); + return new DefaultTemplate($latte); +} + + +test('render accepts object parameters', function () { + $template = createTemplate(); + ob_start(); + $template->render('{$message}', (object) ['message' => 'Hello']); + Assert::same('Hello', ob_get_clean()); +}); + + +test('renderToString accepts object parameters', function () { + $template = createTemplate(); + Assert::same('Hello', $template->renderToString('{$message}', (object) ['message' => 'Hello'])); +}); diff --git a/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt index c750e02d7..07647f73a 100644 --- a/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt +++ b/tests/Bridges.Latte/TemplateFactory.customTemplate.phpt @@ -18,7 +18,7 @@ class TemplateMock extends Template private string $file = 'ko'; - public function render(?string $file = null, array $params = []): void + public function render(?string $file = null, object|array $params = []): void { echo strrev($this->file); } diff --git a/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt b/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt index ca550da02..c8c1ea492 100644 --- a/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt +++ b/tests/Bridges.Latte/TemplateFactory.templateVariables.84.phpt @@ -5,7 +5,6 @@ * @phpVersion 8.4 */ -use Latte\Attributes\{TemplateFilter, TemplateFunction}; use Nette\Application\Attributes\TemplateVariable; use Nette\Bridges\ApplicationLatte\{LatteFactory, TemplateFactory}; use Nette\Http; diff --git a/tests/Bridges.Latte/renderSnippets.phpt b/tests/Bridges.Latte/renderSnippets.phpt index 282b5a74d..773fc6a2c 100644 --- a/tests/Bridges.Latte/renderSnippets.phpt +++ b/tests/Bridges.Latte/renderSnippets.phpt @@ -43,7 +43,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl(); $presenter['multi-1']->redrawControl(); @@ -67,7 +67,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('hello'); $presenter->redrawControl('array'); @@ -83,7 +83,7 @@ Assert::same([ ], (array) $presenter->payload); $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); ob_start(); $presenter->render(); $content = ob_get_clean(); diff --git a/tests/Bridges.Latte/renderSnippets2.phpt b/tests/Bridges.Latte/renderSnippets2.phpt index 657882a1b..cadff31b3 100644 --- a/tests/Bridges.Latte/renderSnippets2.phpt +++ b/tests/Bridges.Latte/renderSnippets2.phpt @@ -58,7 +58,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter['multi-1']->redrawControl(); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets3.phpt b/tests/Bridges.Latte/renderSnippets3.phpt index 68c600822..8d42c1ec0 100644 --- a/tests/Bridges.Latte/renderSnippets3.phpt +++ b/tests/Bridges.Latte/renderSnippets3.phpt @@ -40,7 +40,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter['test']->redrawControl('foo'); @@ -53,7 +53,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter['test']->redrawControl('foo'); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets4.phpt b/tests/Bridges.Latte/renderSnippets4.phpt index ddb8ea1fa..d264351af 100644 --- a/tests/Bridges.Latte/renderSnippets4.phpt +++ b/tests/Bridges.Latte/renderSnippets4.phpt @@ -32,7 +32,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter->render(); diff --git a/tests/Bridges.Latte/renderSnippets5.phpt b/tests/Bridges.Latte/renderSnippets5.phpt index 000acb7c3..0d0137496 100644 --- a/tests/Bridges.Latte/renderSnippets5.phpt +++ b/tests/Bridges.Latte/renderSnippets5.phpt @@ -23,7 +23,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); $presenter->render('
Hello
'); @@ -35,7 +35,7 @@ Assert::same([ $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); Assert::exception( @@ -47,7 +47,7 @@ Assert::exception( $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); $presenter->snippetMode = true; $presenter->redrawControl('foo'); Assert::exception( diff --git a/tests/Bridges.Latte/renderSnippets6.phpt b/tests/Bridges.Latte/renderSnippets6.phpt index 3b1693331..97f283c50 100644 --- a/tests/Bridges.Latte/renderSnippets6.phpt +++ b/tests/Bridges.Latte/renderSnippets6.phpt @@ -24,7 +24,7 @@ class TestPresenter extends Nette\Application\UI\Presenter $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); ob_start(); $presenter->render('
hello
'); $content = ob_get_clean(); @@ -32,7 +32,7 @@ Assert::same('
hello
', $content); $presenter = new TestPresenter; -$presenter->injectPrimary(new Http\Request(new Http\UrlScript('/')), new Http\Response); +$presenter->injectPrimary(new Http\Request(new Http\UrlScript), new Http\Response); Assert::exception( fn() => $presenter->render('
hello
'), Latte\CompileException::class, diff --git a/tests/Bridges.Latte/renderSnippets7.phpt b/tests/Bridges.Latte/renderSnippets7.phpt index e3c254e0c..0a0695578 100644 --- a/tests/Bridges.Latte/renderSnippets7.phpt +++ b/tests/Bridges.Latte/renderSnippets7.phpt @@ -10,21 +10,21 @@ require __DIR__ . '/ControlMock.php'; $dataSets = [ - //simple snippet + // simple snippet [ ['main' => '{snippet foo}hello{/snippet}'], ['foo' => 'hello'], '
hello
', ['foo'], ], - //snippetArea with nested snippet + // snippetArea with nested snippet [ ['main' => '{snippetArea fooWrapper}{do $global->value = hello}{snippet foo}{$global->value}{/snippet}{/snippetArea}'], ['foo' => 'hello'], '
hello
', ['fooWrapper', 'foo'], ], - //two nested snippets + // two nested snippets [ ['main' => '{snippet foo}{snippet foo2}hello{/snippet}{/snippet}'], ['foo' => '
hello
'], @@ -43,7 +43,7 @@ $dataSets = [ '
hello
', ['foo', 'foo2'], ], - //included template + // included template [ [ 'main' => '{snippet foo}{include file "sub"}{/snippet}', @@ -53,7 +53,7 @@ $dataSets = [ '
hello
', ['foo'], ], - //included template 2 + // included template 2 [ [ 'main' => '{snippet foo}{include file "sub"}{/snippet}', @@ -63,7 +63,7 @@ $dataSets = [ '
hello
', ['foo', 'subFoo'], ], - //included template - expected empty payload + // included template - expected empty payload [ [ 'main' => '{include file "sub"}', @@ -73,7 +73,7 @@ $dataSets = [ '
hello
', ['subFoo'], ], - //included template - snippetArea + // included template - snippetArea [ [ 'main' => '{snippetArea foo}{include file "sub"}{/snippetArea}', @@ -83,7 +83,7 @@ $dataSets = [ '
hello
', ['foo', 'subFoo'], ], - //nested included template - snippetArea + // nested included template - snippetArea [ [ 'main' => '{snippetArea foo}{include file "sub"}{/snippetArea}', @@ -94,7 +94,7 @@ $dataSets = [ '
hello
', ['foo', 'sub2Foo'], ], - //dynamic snippets + // dynamic snippets [ [ 'main' => ' @@ -109,7 +109,7 @@ $dataSets = [ "\n
\n\t\t
1
\n\t\t
2
\n
", ['foo'], ], - //dynamic snippets with a snippetArea + // dynamic snippets with a snippetArea [ [ 'main' => ' @@ -124,7 +124,7 @@ $dataSets = [ "\n\t\t
1
\n\t\t
2
", ['foo'], ], - //snippetArea with a dynamic snippet and included template + // snippetArea with a dynamic snippet and included template [ [ 'main' => ' @@ -140,7 +140,7 @@ $dataSets = [ "\n\t\t
1
\n\t\t
2
", ['foo'], ], - //extends + // extends [ [ 'main' => '{extends "layout"}{block content}{snippet foo}hello{/snippet}', @@ -150,7 +150,7 @@ $dataSets = [ '
hello
world
', ['foo', 'layoutFoo'], ], - //import + // import [ [ 'main' => '{import "blocks1"}{import "blocks2"}{snippet foo}{include block1}{include block2}{/snippet}', @@ -161,7 +161,7 @@ $dataSets = [ '
hello world
', ['foo'], ], - //import and extends + // import and extends [ [ 'main' => '{extends "layout"}{import "blocks1"}{import "blocks2"}{block content}{snippet foo}{include block1}{include block2}{/snippet}', @@ -173,7 +173,7 @@ $dataSets = [ '
hello world
', ['foo'], ], - //embed + // embed [ [ 'main' => '{embed file "embed"}{block embed}{snippet bar}world{/snippet}{/block}{/embed}', @@ -186,7 +186,7 @@ $dataSets = [ ]; foreach ($dataSets as $data) { - //snippet mode + // snippet mode $control = new ControlMock; $control->invalid = array_fill_keys($data[3], value: true); @@ -198,7 +198,7 @@ foreach ($dataSets as $data) { Assert::same($data[1], $control->payload); - //non snippet mode + // non snippet mode $control = new ControlMock; $control->snippetMode = false; $control->invalid = array_fill_keys($data[3], value: true); diff --git a/tests/Bridges.Latte/{control}.2.phpt b/tests/Bridges.Latte/{control}.2.phpt index e9bda2a50..e7e85f31c 100644 --- a/tests/Bridges.Latte/{control}.2.phpt +++ b/tests/Bridges.Latte/{control}.2.phpt @@ -51,15 +51,15 @@ $latte->renderToString(' {control $name:$name} -{control form var1} +{control form, var1} -{control form var1, 1, 2} +{control form, var1, 1, 2} -{control form wrap => 5, 1, 2} +{control form, wrap => 5, 1, 2} -{control form [var1 => 5], 1, 2} +{control form, [var1 => 5], 1, 2} -{control form 5, 1, var1: 2} +{control form, 5, 1, var1: 2} ', $params); Assert::same([ diff --git a/tests/Bridges.Latte/{control}.commaWarning.phpt b/tests/Bridges.Latte/{control}.commaWarning.phpt new file mode 100644 index 000000000..1a5dd3281 --- /dev/null +++ b/tests/Bridges.Latte/{control}.commaWarning.phpt @@ -0,0 +1,53 @@ +setLoader(new Latte\Loaders\StringLoader); +$latte->addExtension(new Nette\Bridges\ApplicationLatte\UIExtension(null)); + + +test('no warning when only the component name is given', function () use ($latte) { + Assert::noError(fn() => $latte->compile('{control form}')); +}); + + +test('no warning when comma separates name and arguments', function () use ($latte) { + Assert::noError(fn() => $latte->compile('{control form, param => 1}')); +}); + + +test('deprecation warning when comma is missing before arguments', function () use ($latte) { + Assert::error( + fn() => $latte->compile('{control form param => 1}'), + E_USER_DEPRECATED, + 'Missing comma before tag arguments on line 1 at column 15.', + ); +}); + + +test('deprecation warning also fires after the :method syntax', function () use ($latte) { + Assert::error( + fn() => $latte->compile('{control form:type param}'), + E_USER_DEPRECATED, + 'Missing comma before tag arguments on line 1 at column 20.', + ); +}); + + +test('a malformed tag emits the deprecation together with the exception', function () use ($latte) { + Assert::error(function () use ($latte) { + Assert::exception( + fn() => $latte->compile('{control form: param => 1}'), + Latte\CompileException::class, + "Unexpected '=>' (on line 1 at column 22)", + ); + }, E_USER_DEPRECATED, 'Missing comma before tag arguments on line 1 at column 22.'); +}); diff --git a/tests/Bridges.Latte/{control}.phpt b/tests/Bridges.Latte/{control}.phpt index a3c59f235..dccddaaba 100644 --- a/tests/Bridges.Latte/{control}.phpt +++ b/tests/Bridges.Latte/{control}.phpt @@ -60,7 +60,7 @@ Assert::match( $ʟ_tmp->renderType('param') /* pos 1:1 */; %A% XX, - $latte->compile('{control form:type param}'), + $latte->compile('{control form:type, param}'), ); Assert::match( @@ -69,7 +69,7 @@ Assert::match( $ʟ_tmp->render(...$params) /* pos 1:1 */; %A% XX, - $latte->compile('{control form (expand) $params}'), + $latte->compile('{control form, (expand) $params}'), ); Assert::match( @@ -78,7 +78,7 @@ Assert::match( $ʟ_tmp->renderType(['param' => 123]) /* pos 1:1 */; %A% XX, - $latte->compile('{control form:type param => 123}'), + $latte->compile('{control form:type, param => 123}'), ); Assert::match( diff --git a/tests/Bridges.Latte/{link}.2.phpt b/tests/Bridges.Latte/{link}.2.phpt index 7e6626180..72382383f 100644 --- a/tests/Bridges.Latte/{link}.2.phpt +++ b/tests/Bridges.Latte/{link}.2.phpt @@ -82,33 +82,33 @@ Assert::match(<<<'EOD' EOD, strtr($latte->renderToString(<<<'EOD' - {plink Homepage:} + {plink Homepage:} - {plink Homepage: } + {plink Homepage: } - {plink Homepage:action } + {plink Homepage:action } - {plink 'Homepage:action' } + {plink 'Homepage:action' } - {plink Homepage:action 10, 20, '{one}&two'} + {plink Homepage:action 10, 20, '{one}&two'} - {plink Homepage:action#hash 10, 20, '{one}&two'} + {plink Homepage:action#hash 10, 20, '{one}&two'} - {plink #hash} + {plink #hash} - {plink : 10 } + {plink : 10 } - {plink default 10, 'a' => 20, 'b' => 30} + {plink default 10, 'a' => 20, 'b' => 30} - {link $action} + {link $action} - + - + - + - + - - EOD, $params), [''' => "'", ''' => "'", '{' => '{'])); + + EOD, $params), [''' => "'", ''' => "'", '{' => '{'])); diff --git a/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt b/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt index b9ec12926..74d815358 100644 --- a/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt +++ b/tests/Bridges.Tracy/RoutingPanel.analyse.withPath.phpt @@ -32,7 +32,7 @@ test('URL: /', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -45,7 +45,7 @@ test('URL: /', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -53,7 +53,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -63,7 +63,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => ['presenter' => 'Article', 'action' => 'homepage'], @@ -71,7 +71,7 @@ test('URL: /', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => null, @@ -95,7 +95,7 @@ test('URL: /foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -108,7 +108,7 @@ test('URL: /foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -116,7 +116,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -126,7 +126,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -134,7 +134,7 @@ test('URL: /foo', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Foo', 'action' => 'default'], @@ -158,7 +158,7 @@ test('URL: /admin', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -171,7 +171,7 @@ test('URL: /admin', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => ['presenter' => 'Admin:Home', 'action' => 'default'], @@ -179,7 +179,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -189,7 +189,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -197,7 +197,7 @@ test('URL: /admin', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'default'], @@ -221,7 +221,7 @@ test('URL: /admin/', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -234,7 +234,7 @@ test('URL: /admin/', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => ['presenter' => 'Admin:Home', 'action' => 'default'], @@ -242,7 +242,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => null, @@ -252,7 +252,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -260,7 +260,7 @@ test('URL: /admin/', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'default'], @@ -284,7 +284,7 @@ test('URL: /admin/foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Sign', 'action' => 'in'], 'mask' => 'sign-in', 'params' => null, @@ -297,7 +297,7 @@ test('URL: /admin/foo', function () use ($router) { 'routes' => [ (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Admin:Home', 'action' => 'default'], 'mask' => '', 'params' => null, @@ -305,7 +305,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'yes', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['module' => 'Admin', 'action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin:Foo', 'action' => 'default'], @@ -315,7 +315,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'no', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['presenter' => 'Article', 'action' => 'homepage'], 'mask' => '', 'params' => null, @@ -323,7 +323,7 @@ test('URL: /admin/foo', function () use ($router) { ], (object) [ 'matched' => 'may', - 'class' => 'Nette\Application\Routers\Route', + 'class' => Nette\Application\Routers\Route::class, 'defaults' => ['action' => 'default'], 'mask' => '/', 'params' => ['presenter' => 'Admin', 'action' => 'foo'], diff --git a/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt b/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt new file mode 100644 index 000000000..a6d75db5a --- /dev/null +++ b/tests/Bridges.Tracy/RoutingPanel.getAgentInfo().phpt @@ -0,0 +1,105 @@ +addRoute('sign-in', 'Sign:in'); +$router->withPath('admin/') + ->addRoute('', 'Admin:Home:default') + ->addRoute('/', ['module' => 'Admin']); +$router->addRoute('/'); + + +test('matched route', function () use ($router) { + $httpRequest = new Http\Request(new UrlScript('http://example.com/admin', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/admin` + + **Matched:** Admin:Home:default + %A% + + ### Routes + - [no match] `sign-in` (presenter=Sign, action=in) + - [MATCHED] `admin/` (presenter=Admin:Home, action=default) + - [no match] `admin//` (module=Admin, action=default) + - [MAY MATCH] `/` (action=default) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('no matching route', function () { + $router = new RouteList; + $router->addRoute('sign-in', 'Sign:in'); + + $httpRequest = new Http\Request(new UrlScript('http://example.com/nowhere', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/nowhere` + + **Matched:** no route + + ### Routes + - [no match] `sign-in` (presenter=Sign, action=in) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('no routes defined', function () { + $router = new RouteList; + + $httpRequest = new Http\Request(new UrlScript('http://example.com/', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + Assert::match( + <<<'XX' + %A% + ## Routing + + `GET http://example.com/` + + **Matched:** no route + + ### Routes + (none defined) + XX, + $panel->getAgentInfo(), + ); +}); + + +test('matched with extra parameters', function () use ($router) { + $httpRequest = new Http\Request(new UrlScript('http://example.com/article/detail?id=42', '/')); + $panel = new RoutingPanel($router, $httpRequest, new PresenterFactory(function () {})); + $panel->getTab(); + + $output = $panel->getAgentInfo(); + Assert::contains('**Matched:** Article:detail', $output); + Assert::contains('**Parameters:**', $output); + Assert::contains('- id = 42', $output); +}); diff --git a/tests/Responses/CallbackResponse.phpt b/tests/Responses/CallbackResponse.phpt index e23bbf118..f9cafc264 100644 --- a/tests/Responses/CallbackResponse.phpt +++ b/tests/Responses/CallbackResponse.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\CallbackResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Responses/FileResponse.contentDisposition.phpt b/tests/Responses/FileResponse.contentDisposition.phpt index ae712ef00..3f422cdc2 100644 --- a/tests/Responses/FileResponse.contentDisposition.phpt +++ b/tests/Responses/FileResponse.contentDisposition.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; if (PHP_SAPI === 'cli') { @@ -28,7 +27,10 @@ test('default attachment content disposition', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); }); @@ -44,7 +46,10 @@ test('inline content disposition', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('inline; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'inline; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); }); @@ -58,5 +63,23 @@ test('filename with special characters', function () { $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); Assert::same($origData, ob_get_clean()); - Assert::same('attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), $response->getHeader('Content-Disposition')); + Assert::same( + 'attachment; filename="' . $fileName . '"; filename*=utf-8\'\'' . rawurlencode($fileName), + $response->getHeader('Content-Disposition'), + ); +}); + + +test('filename injection into Content-Disposition is neutralized', function () { + $file = __FILE__; + $fileName = 'evil".pdf; filename="hack.exe'; + $fileResponse = new FileResponse($file, $fileName); + + ob_start(); + $fileResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); + ob_get_clean(); + + // quotes and backslashes are stripped from the quoted filename, so the header cannot be broken apart + $header = $response->getHeader('Content-Disposition'); + Assert::same('attachment; filename="evil.pdf; filename=hack.exe"; filename*=utf-8\'\'' . rawurlencode($fileName), $header); }); diff --git a/tests/Responses/FileResponse.full.phpt b/tests/Responses/FileResponse.full.phpt index f6e36e186..53790ef4d 100644 --- a/tests/Responses/FileResponse.full.phpt +++ b/tests/Responses/FileResponse.full.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -29,9 +28,9 @@ test('large file content delivery', function () { $data = ''; for ($i = 0; $i < 20000; $i++) { - $data .= 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi efficitur nisl mauris, ' . - 'sed bibendum leo tempor ornare. Etiam sodales enim sem. Proin lobortis metus at sagittis ' . - 'imperdiet. In non fringilla libero, ac porta purus. Nam gravida quis tellus quis semper.'; + $data .= 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi efficitur nisl mauris, ' + . 'sed bibendum leo tempor ornare. Etiam sodales enim sem. Proin lobortis metus at sagittis ' + . 'imperdiet. In non fringilla libero, ac porta purus. Nam gravida quis tellus quis semper.'; } file_put_contents($file, $data); diff --git a/tests/Responses/FileResponse.range.phpt b/tests/Responses/FileResponse.range.phpt index 6118f4e76..bc7b037a5 100644 --- a/tests/Responses/FileResponse.range.phpt +++ b/tests/Responses/FileResponse.range.phpt @@ -9,7 +9,6 @@ use Nette\Application\Responses\FileResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Responses/JsonResponse.contentType.phpt b/tests/Responses/JsonResponse.contentType.phpt index 3fe4f538b..0ba18460c 100644 --- a/tests/Responses/JsonResponse.contentType.phpt +++ b/tests/Responses/JsonResponse.contentType.phpt @@ -8,7 +8,6 @@ use Nette\Application\Responses\JsonResponse; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; if (PHP_SAPI === 'cli') { @@ -18,7 +17,7 @@ if (PHP_SAPI === 'cli') { test('JSON content type with UTF-8 charset', function () { $data = ['text' => 'žluťoučký kůň']; $encoded = json_encode($data, JSON_UNESCAPED_UNICODE); - $jsonResponse = new JsonResponse($data, 'application/json'); + $jsonResponse = new JsonResponse($data); ob_start(); $jsonResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); @@ -30,7 +29,7 @@ test('JSON content type with UTF-8 charset', function () { test('boolean data JSON encoding', function () { $data = true; $encoded = json_encode($data, JSON_UNESCAPED_UNICODE); - $jsonResponse = new JsonResponse($data, 'application/json'); + $jsonResponse = new JsonResponse($data); ob_start(); $jsonResponse->send(new Http\Request(new Http\UrlScript), $response = new Http\Response); diff --git a/tests/Routers/CliRouter.invalid.phpt b/tests/Routers/CliRouter.invalid.phpt index 71e583f2b..2385d48f0 100644 --- a/tests/Routers/CliRouter.invalid.phpt +++ b/tests/Routers/CliRouter.invalid.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\CliRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/CliRouter.phpt b/tests/Routers/CliRouter.phpt index 669a3f4dd..144b51af9 100644 --- a/tests/Routers/CliRouter.phpt +++ b/tests/Routers/CliRouter.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\CliRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/LinkGenerator.phpt b/tests/Routers/LinkGenerator.phpt index 8fd727ea2..2db40179e 100644 --- a/tests/Routers/LinkGenerator.phpt +++ b/tests/Routers/LinkGenerator.phpt @@ -28,6 +28,7 @@ namespace App\Presentation\Module\My { use Nette; + class MyPresenter implements Nette\Application\IPresenter { public function run(Nette\Application\Request $request): Nette\Application\Response @@ -43,7 +44,6 @@ namespace { use Nette\Http; use Tester\Assert; - $pf = new PresenterFactory; @@ -51,10 +51,14 @@ namespace { $generator = new LinkGenerator(new Routers\SimpleRouter, new Http\UrlScript('http://nette.org/en/'), $pf); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:default')); Assert::same('http://nette.org/en/?action=default&presenter=Module%3AMy', $generator->link('Module:My:default')); + Assert::same('http://nette.org/en/?action=default&presenter=Module%3AMy', $generator->link(':Module:My:default')); Assert::same('http://nette.org/en/?presenter=Module%3AMy', $generator->link('Module:My:')); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:')); Assert::same('http://nette.org/en/?a=10&action=default&presenter=Homepage', $generator->link('Homepage:', [10])); - Assert::same('http://nette.org/en/?id=20&b=10&action=detail&presenter=Homepage', $generator->link('Homepage:detail', [10, 'id' => 20])); + Assert::same( + 'http://nette.org/en/?id=20&b=10&action=detail&presenter=Homepage', + $generator->link('Homepage:detail', [10, 'id' => 20]), + ); Assert::same('http://nette.org/en/?action=default&presenter=Homepage#frag:ment', $generator->link('Homepage:#frag:ment')); Assert::same('http://nette.org/en/?id=10&action=missing&presenter=Homepage', $generator->link('Homepage:missing', ['id' => 10])); }); @@ -72,6 +76,12 @@ namespace { }, Nette\Application\UI\InvalidLinkException::class, 'No route for Homepage:default(id=10)'); + testException('route mismatch exception with object parameter', function () use ($pf) { + $generator = new LinkGenerator(new Routers\Route('/', 'Product:'), new Http\UrlScript('http://nette.org/en/'), $pf); + $generator->link('Homepage:default', ['id' => (object) ['a' => 1]]); + }, Nette\Application\UI\InvalidLinkException::class, 'No route for Homepage:default(id[a]=1)'); + + testException('invalid action parameter propagation', function () use ($pf) { $generator = new LinkGenerator(new Routers\Route('/', 'Homepage:'), new Http\UrlScript('http://nette.org/en/'), $pf); $generator->link('Homepage:missing', [10]); @@ -85,7 +95,10 @@ namespace { Assert::same('http://nette.org/en/?presenter=Module%3AMy', $generator->link('Module:My:')); Assert::same('http://nette.org/en/?presenter=Homepage', $generator->link('Homepage:')); Assert::same('http://nette.org/en/?0=10&presenter=Homepage', $generator->link('Homepage:', [10])); - Assert::same('http://nette.org/en/?0=10&id=20&action=detail&presenter=Homepage', $generator->link('Homepage:detail', [10, 'id' => 20])); + Assert::same( + 'http://nette.org/en/?0=10&id=20&action=detail&presenter=Homepage', + $generator->link('Homepage:detail', [10, 'id' => 20]), + ); Assert::same('http://nette.org/en/?presenter=Homepage#frag:ment', $generator->link('Homepage:#frag:ment')); Assert::same('http://nette.org/en/?action=default&presenter=Homepage', $generator->link('Homepage:default')); }); diff --git a/tests/Routers/Route.arrayParams.phpt b/tests/Routers/Route.arrayParams.phpt index fa3711e49..6de4d776b 100644 --- a/tests/Routers/Route.arrayParams.phpt +++ b/tests/Routers/Route.arrayParams.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional1.phpt b/tests/Routers/Route.autooptional1.phpt index a4b2cc751..091e5bb63 100644 --- a/tests/Routers/Route.autooptional1.phpt +++ b/tests/Routers/Route.autooptional1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional2.phpt b/tests/Routers/Route.autooptional2.phpt index 5d4fab1aa..d75e27adc 100644 --- a/tests/Routers/Route.autooptional2.phpt +++ b/tests/Routers/Route.autooptional2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional3.phpt b/tests/Routers/Route.autooptional3.phpt index 802811ce4..82d30c9d3 100644 --- a/tests/Routers/Route.autooptional3.phpt +++ b/tests/Routers/Route.autooptional3.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.autooptional4.phpt b/tests/Routers/Route.autooptional4.phpt index 861a830ec..6801edf58 100644 --- a/tests/Routers/Route.autooptional4.phpt +++ b/tests/Routers/Route.autooptional4.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.basic.phpt b/tests/Routers/Route.basic.phpt index a8c7d0917..316626cdc 100644 --- a/tests/Routers/Route.basic.phpt +++ b/tests/Routers/Route.basic.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.camelcapsVsDash.phpt b/tests/Routers/Route.camelcapsVsDash.phpt index 2f1502188..8287deaab 100644 --- a/tests/Routers/Route.camelcapsVsDash.phpt +++ b/tests/Routers/Route.camelcapsVsDash.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.closure.phpt b/tests/Routers/Route.closure.phpt index 50a01ebf0..366ae225e 100644 --- a/tests/Routers/Route.closure.phpt +++ b/tests/Routers/Route.closure.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.combinedUrlParam.phpt b/tests/Routers/Route.combinedUrlParam.phpt index d9a8dce6c..d987f95f3 100644 --- a/tests/Routers/Route.combinedUrlParam.phpt +++ b/tests/Routers/Route.combinedUrlParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.errors.phpt b/tests/Routers/Route.errors.phpt index fa7a3a76c..ee905d65b 100644 --- a/tests/Routers/Route.errors.phpt +++ b/tests/Routers/Route.errors.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/Route.extraDefaultParam.phpt b/tests/Routers/Route.extraDefaultParam.phpt index 22d60174c..3e73456d2 100644 --- a/tests/Routers/Route.extraDefaultParam.phpt +++ b/tests/Routers/Route.extraDefaultParam.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.global.phpt b/tests/Routers/Route.filter.global.phpt index 335474922..b64514dd6 100644 --- a/tests/Routers/Route.filter.global.phpt +++ b/tests/Routers/Route.filter.global.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; @@ -16,7 +15,7 @@ require __DIR__ . '/Route.php'; $route = new Route('', [ '' => [ Route::FilterIn => function (array $arr) { - if (substr($arr['presenter'], 0, 3) !== 'Abc') { + if (!str_starts_with($arr['presenter'], 'Abc')) { return null; } @@ -25,7 +24,7 @@ $route = new Route('', [ return $arr; }, Route::FilterOut => function (array $arr) { - if (substr($arr['presenter'], 0, 3) !== 'Abc') { + if (!str_starts_with($arr['presenter'], 'Abc')) { return null; } diff --git a/tests/Routers/Route.filter.query.phpt b/tests/Routers/Route.filter.query.phpt index 7dd39e1a4..7ae708987 100644 --- a/tests/Routers/Route.filter.query.phpt +++ b/tests/Routers/Route.filter.query.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.url.object.phpt b/tests/Routers/Route.filter.url.object.phpt index 69868c304..1128502a7 100644 --- a/tests/Routers/Route.filter.url.object.phpt +++ b/tests/Routers/Route.filter.url.object.phpt @@ -7,8 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filter.url.phpt b/tests/Routers/Route.filter.url.phpt index c533e3880..f9fe7028c 100644 --- a/tests/Routers/Route.filter.url.phpt +++ b/tests/Routers/Route.filter.url.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.query.phpt b/tests/Routers/Route.filterTable.query.phpt index 122d1e4a0..511528117 100644 --- a/tests/Routers/Route.filterTable.query.phpt +++ b/tests/Routers/Route.filterTable.query.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.strict.phpt b/tests/Routers/Route.filterTable.strict.phpt index c7509c9f3..be618923b 100644 --- a/tests/Routers/Route.filterTable.strict.phpt +++ b/tests/Routers/Route.filterTable.strict.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.filterTable.url.phpt b/tests/Routers/Route.filterTable.url.phpt index daab4cf3d..c930f4f03 100644 --- a/tests/Routers/Route.filterTable.url.phpt +++ b/tests/Routers/Route.filterTable.url.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.firstOptionalParam.phpt b/tests/Routers/Route.firstOptionalParam.phpt index fa4ad1ece..4b9ee6e2e 100644 --- a/tests/Routers/Route.firstOptionalParam.phpt +++ b/tests/Routers/Route.firstOptionalParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fooParameter.default.phpt b/tests/Routers/Route.fooParameter.default.phpt index 235e29298..6f93d0379 100644 --- a/tests/Routers/Route.fooParameter.default.phpt +++ b/tests/Routers/Route.fooParameter.default.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fooParameter.phpt b/tests/Routers/Route.fooParameter.phpt index 6d1a0de85..ccf62c6b9 100644 --- a/tests/Routers/Route.fooParameter.phpt +++ b/tests/Routers/Route.fooParameter.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.fullMatchParam.phpt b/tests/Routers/Route.fullMatchParam.phpt index 399e0bd1d..a1afef570 100644 --- a/tests/Routers/Route.fullMatchParam.phpt +++ b/tests/Routers/Route.fullMatchParam.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.longParameter.phpt b/tests/Routers/Route.longParameter.phpt index e5de099b6..a4492c3ef 100644 --- a/tests/Routers/Route.longParameter.phpt +++ b/tests/Routers/Route.longParameter.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.mandatoryAction.phpt b/tests/Routers/Route.mandatoryAction.phpt index 5e887060c..285d53473 100644 --- a/tests/Routers/Route.mandatoryAction.phpt +++ b/tests/Routers/Route.mandatoryAction.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.modules.fixity.phpt b/tests/Routers/Route.modules.fixity.phpt index e133ebb18..01b239d4e 100644 --- a/tests/Routers/Route.modules.fixity.phpt +++ b/tests/Routers/Route.modules.fixity.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/Route.modules.phpt b/tests/Routers/Route.modules.phpt index 57e892268..94a3d14d2 100644 --- a/tests/Routers/Route.modules.phpt +++ b/tests/Routers/Route.modules.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.noDefaultParams.phpt b/tests/Routers/Route.noDefaultParams.phpt index 2533be317..fb2351adc 100644 --- a/tests/Routers/Route.noDefaultParams.phpt +++ b/tests/Routers/Route.noDefaultParams.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('//', [ -]); +$route = new Route('//'); testRouteIn($route, '/presenter/action/12', [ 'presenter' => 'Presenter', diff --git a/tests/Routers/Route.noParams.phpt b/tests/Routers/Route.noParams.phpt index 68c9ecce7..4adab93ce 100644 --- a/tests/Routers/Route.noParams.phpt +++ b/tests/Routers/Route.noParams.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional1.phpt b/tests/Routers/Route.optional.autooptional1.phpt index f3a39f278..e06932f29 100644 --- a/tests/Routers/Route.optional.autooptional1.phpt +++ b/tests/Routers/Route.optional.autooptional1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional2.phpt b/tests/Routers/Route.optional.autooptional2.phpt index 56304fd09..e35a4d203 100644 --- a/tests/Routers/Route.optional.autooptional2.phpt +++ b/tests/Routers/Route.optional.autooptional2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.autooptional3.phpt b/tests/Routers/Route.optional.autooptional3.phpt index c8ee32c75..7e3fd5760 100644 --- a/tests/Routers/Route.optional.autooptional3.phpt +++ b/tests/Routers/Route.optional.autooptional3.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.module.phpt b/tests/Routers/Route.optional.module.phpt index 578a051be..9b8c5faa4 100644 --- a/tests/Routers/Route.optional.module.phpt +++ b/tests/Routers/Route.optional.module.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.nested.phpt b/tests/Routers/Route.optional.nested.phpt index ed2a00548..1fcf61a36 100644 --- a/tests/Routers/Route.optional.nested.phpt +++ b/tests/Routers/Route.optional.nested.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.phpt b/tests/Routers/Route.optional.phpt index d17e7311d..2e302d56f 100644 --- a/tests/Routers/Route.optional.phpt +++ b/tests/Routers/Route.optional.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('index[.html]', [ -]); +$route = new Route('index[.html]'); testRouteIn($route, '/index.html', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.precedence.phpt b/tests/Routers/Route.optional.precedence.phpt index fc978779f..1fcb09d22 100644 --- a/tests/Routers/Route.optional.precedence.phpt +++ b/tests/Routers/Route.optional.precedence.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('[/][]', [ -]); +$route = new Route('[/][]'); testRouteIn($route, '/one', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.preferred.nested1.phpt b/tests/Routers/Route.optional.preferred.nested1.phpt index 44d13b9a8..f7e6eed53 100644 --- a/tests/Routers/Route.optional.preferred.nested1.phpt +++ b/tests/Routers/Route.optional.preferred.nested1.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.nested2.phpt b/tests/Routers/Route.optional.preferred.nested2.phpt index 004600464..0930e985f 100644 --- a/tests/Routers/Route.optional.preferred.nested2.phpt +++ b/tests/Routers/Route.optional.preferred.nested2.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.nested3.phpt b/tests/Routers/Route.optional.preferred.nested3.phpt index e7033874a..83a3d454b 100644 --- a/tests/Routers/Route.optional.preferred.nested3.phpt +++ b/tests/Routers/Route.optional.preferred.nested3.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.optional.preferred.phpt b/tests/Routers/Route.optional.preferred.phpt index 5da5178c2..0a5922705 100644 --- a/tests/Routers/Route.optional.preferred.phpt +++ b/tests/Routers/Route.optional.preferred.phpt @@ -6,14 +6,12 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; -$route = new Route('index[!.html]', [ -]); +$route = new Route('index[!.html]'); testRouteIn($route, '/index.html', [ 'presenter' => 'querypresenter', diff --git a/tests/Routers/Route.optional.regexp.phpt b/tests/Routers/Route.optional.regexp.phpt index 970a14c27..d32d29ab9 100644 --- a/tests/Routers/Route.optional.regexp.phpt +++ b/tests/Routers/Route.optional.regexp.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.scalarParams.phpt b/tests/Routers/Route.scalarParams.phpt index f951c161b..15ddbdca1 100644 --- a/tests/Routers/Route.scalarParams.phpt +++ b/tests/Routers/Route.scalarParams.phpt @@ -7,15 +7,13 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; test('numeric and boolean parameter serialization', function () { - $route = new Route('/', [ - ]); + $route = new Route('/'); Assert::same( 'http://example.com/homepage/12', diff --git a/tests/Routers/Route.secured.phpt b/tests/Routers/Route.secured.phpt index 70dac8abe..e3eb9d3ed 100644 --- a/tests/Routers/Route.secured.phpt +++ b/tests/Routers/Route.secured.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\Route; use Nette\Http\UrlScript; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.slash.phpt b/tests/Routers/Route.slash.phpt index 20a32ee68..35e13401b 100644 --- a/tests/Routers/Route.slash.phpt +++ b/tests/Routers/Route.slash.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.type.phpt b/tests/Routers/Route.type.phpt index 9d4577a9a..5b110c2d3 100644 --- a/tests/Routers/Route.type.phpt +++ b/tests/Routers/Route.type.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.urlEncoding.phpt b/tests/Routers/Route.urlEncoding.phpt index f554e4c4b..f56e9f947 100644 --- a/tests/Routers/Route.urlEncoding.phpt +++ b/tests/Routers/Route.urlEncoding.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.utf8Param.phpt b/tests/Routers/Route.utf8Param.phpt index bc905d16a..d83f4c2db 100644 --- a/tests/Routers/Route.utf8Param.phpt +++ b/tests/Routers/Route.utf8Param.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.variables.phpt b/tests/Routers/Route.variables.phpt index d5edae7b1..dca11d2bb 100644 --- a/tests/Routers/Route.variables.phpt +++ b/tests/Routers/Route.variables.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\Route; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withAbsolutePath.phpt b/tests/Routers/Route.withAbsolutePath.phpt index 41368727c..2dd6130c3 100644 --- a/tests/Routers/Route.withAbsolutePath.phpt +++ b/tests/Routers/Route.withAbsolutePath.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withDefaultPresenterAndAction.phpt b/tests/Routers/Route.withDefaultPresenterAndAction.phpt index 5c36fed10..e421c03df 100644 --- a/tests/Routers/Route.withDefaultPresenterAndAction.phpt +++ b/tests/Routers/Route.withDefaultPresenterAndAction.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.phpt b/tests/Routers/Route.withHost.phpt index 4d427dfdc..37da50316 100644 --- a/tests/Routers/Route.withHost.phpt +++ b/tests/Routers/Route.withHost.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.protocol.phpt b/tests/Routers/Route.withHost.protocol.phpt index d1a3f7d3b..16896ad70 100644 --- a/tests/Routers/Route.withHost.protocol.phpt +++ b/tests/Routers/Route.withHost.protocol.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withHost.secured.phpt b/tests/Routers/Route.withHost.secured.phpt index 0995bc3d3..29d0fe377 100644 --- a/tests/Routers/Route.withHost.secured.phpt +++ b/tests/Routers/Route.withHost.secured.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\Route; use Nette\Http\UrlScript; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withNamedParamsInQuery.phpt b/tests/Routers/Route.withNamedParamsInQuery.phpt index e28e19b3f..16d8b8a16 100644 --- a/tests/Routers/Route.withNamedParamsInQuery.phpt +++ b/tests/Routers/Route.withNamedParamsInQuery.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withParamsInQuery.phpt b/tests/Routers/Route.withParamsInQuery.phpt index e99626ca7..5d9b83175 100644 --- a/tests/Routers/Route.withParamsInQuery.phpt +++ b/tests/Routers/Route.withParamsInQuery.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/Route.withUserClassAlt.phpt b/tests/Routers/Route.withUserClassAlt.phpt index 8abccd6f6..80fd83d42 100644 --- a/tests/Routers/Route.withUserClassAlt.phpt +++ b/tests/Routers/Route.withUserClassAlt.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\Route; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.addRoute.phpt b/tests/Routers/RouteList.addRoute.phpt index 1e7dab56c..dc0388bf7 100644 --- a/tests/Routers/RouteList.addRoute.phpt +++ b/tests/Routers/RouteList.addRoute.phpt @@ -2,15 +2,14 @@ use Nette\Application\Routers\RouteList; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; $list = new RouteList; -$list->addRoute('foo', ['presenter' => 'foo'], RouteList::ONE_WAY); -$list->addRoute('bar', ['presenter' => 'bar'], RouteList::ONE_WAY); +$list->addRoute('foo', ['presenter' => 'foo'], oneWay: 1); +$list->addRoute('bar', ['presenter' => 'bar'], oneWay: true); $list->addRoute('hello', ['presenter' => 'hello']); diff --git a/tests/Routers/RouteList.basic.phpt b/tests/Routers/RouteList.basic.phpt index 65d1b1ea2..cadac1071 100644 --- a/tests/Routers/RouteList.basic.phpt +++ b/tests/Routers/RouteList.basic.phpt @@ -7,7 +7,6 @@ use Nette\Application\Routers\RouteList; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.modules.phpt b/tests/Routers/RouteList.modules.phpt index 9b34ccfcd..2bc3cfd11 100644 --- a/tests/Routers/RouteList.modules.phpt +++ b/tests/Routers/RouteList.modules.phpt @@ -6,7 +6,6 @@ use Nette\Application\Routers\RouteList; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/Route.php'; diff --git a/tests/Routers/RouteList.withModule.phpt b/tests/Routers/RouteList.withModule.phpt index 4e77d0859..6dbc75041 100644 --- a/tests/Routers/RouteList.withModule.phpt +++ b/tests/Routers/RouteList.withModule.phpt @@ -1,7 +1,7 @@ withModule('Admin') + ->addRoute('micro', fn() => new Nette\Application\Responses\VoidResponse); + +$url = new Nette\Http\UrlScript('http://example.com/micro', '/'); +$params = $list->match(new Nette\Http\Request($url)); +Assert::same('Nette:Micro', $params['presenter']); +Assert::same('http://example.com/micro', $list->constructUrl($params, $url)); diff --git a/tests/Routers/SimpleRouter.invalid.phpt b/tests/Routers/SimpleRouter.invalid.phpt index f9bd9881a..2f63a7498 100644 --- a/tests/Routers/SimpleRouter.invalid.phpt +++ b/tests/Routers/SimpleRouter.invalid.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\SimpleRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/Routers/SimpleRouter.phpt b/tests/Routers/SimpleRouter.phpt index 2f20ad085..dd3bd62d2 100644 --- a/tests/Routers/SimpleRouter.phpt +++ b/tests/Routers/SimpleRouter.phpt @@ -8,7 +8,6 @@ use Nette\Application\Routers\SimpleRouter; use Nette\Http; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/AccessPolicy.withoutPresenter.phpt b/tests/UI/AccessPolicy.withoutPresenter.phpt new file mode 100644 index 000000000..b4f9ba9bd --- /dev/null +++ b/tests/UI/AccessPolicy.withoutPresenter.phpt @@ -0,0 +1,46 @@ + $control['guarded'], + Nette\InvalidStateException::class, + 'Presenter is required for checking requirements of StandaloneControl::createComponentGuarded()', + ); +}); diff --git a/tests/UI/Component.addComponent().phpt b/tests/UI/Component.addComponent().phpt new file mode 100644 index 000000000..b50e432ce --- /dev/null +++ b/tests/UI/Component.addComponent().phpt @@ -0,0 +1,52 @@ + $control->addComponent(new TestControl, 'ok')); +$control->addComponent(new TestControl, 'first', insertBefore: 'ok'); +Assert::same(['first', 'ok'], array_keys($control->getComponents())); + +Assert::exception( + fn() => $control->addComponent(new ForeignComponent, 'foreign'), + Nette\InvalidStateException::class, + "Component 'foreign' of type ForeignComponent is not intended to be used in the Presenter.", +); + +Assert::exception( + fn() => $control->addComponent(new ForeignStatePersistent, 'state'), + Nette\InvalidStateException::class, + "Component 'state' of type ForeignStatePersistent cannot carry persistent state, only Nette\\Application\\UI\\Component can.", +); diff --git a/tests/UI/Component.isLinkCurrent().asserts.php b/tests/UI/Component.isLinkCurrent().asserts.php index 62ae5b39b..c3548a8ee 100644 --- a/tests/UI/Component.isLinkCurrent().asserts.php +++ b/tests/UI/Component.isLinkCurrent().asserts.php @@ -33,7 +33,7 @@ function callIsComponentLinkCurrent( $presenterFactory, new Application\Routers\SimpleRouter, ); - $presenter->onStartup[] = function () use (&$res, $component, $destination, $args) { + $presenter->onStartup[] = function () use (&$res, $component, $destination, $args) { $res = $component->isLinkCurrent($destination, $args); }; $presenter->run($request); @@ -53,6 +53,12 @@ function callIsComponentLinkCurrent( ['int' => 2], )); +Assert::false(callIsLinkCurrent( + new Application\Request('Test', Http\Request::Get, ['int' => 1, 'bool' => true]), + 'Test:default', + ['foo' => (object) ['a' => 1]], +)); + Assert::false(callIsLinkCurrent( new Application\Request('Test', Http\Request::Get, [Application\UI\Presenter::ActionKey => 'otherAction']), 'Test:default', diff --git a/tests/UI/Component.loadState().phpt b/tests/UI/Component.loadState().phpt new file mode 100644 index 000000000..bd7308eca --- /dev/null +++ b/tests/UI/Component.loadState().phpt @@ -0,0 +1,76 @@ + $presenter->loadState([]), + Application\BadRequestException::class, + "Missing parameter 'required' required by presenter .", + ); +}); + + +test('passed parameters', function () { + $presenter = new TestPresenter; + $presenter->loadState(['required' => 'abc']); + Assert::same('abc', $presenter->required); + Assert::null($presenter->nullable); + Assert::same('def', $presenter->default); + Assert::false(isset($presenter->persistent)); + Assert::same(['required' => 'abc', 'nullable' => null, 'default' => 'def', 'persistent' => null], $presenter->getParameters()); +}); + + +test('initialized property', function () { + $presenter = new TestPresenter; + $presenter->required = 'xyz'; + $presenter->loadState([]); + Assert::same('xyz', $presenter->required); +}); + + +test('missing persistent parameter without default value', function () { + $presenter = new RequiredPersistentPresenter; + Assert::exception( + fn() => $presenter->loadState([]), + Application\BadRequestException::class, + "Missing parameter 'page' required by presenter .", + ); + + $presenter->loadState(['page' => '2']); + Assert::same(2, $presenter->page); +}); diff --git a/tests/UI/Component.redirect().phpt b/tests/UI/Component.redirect().phpt index b5cf7101b..16e593305 100644 --- a/tests/UI/Component.redirect().phpt +++ b/tests/UI/Component.redirect().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -21,7 +20,7 @@ class TestPresenter extends Application\UI\Presenter } - public function sendResponse(Application\Response $response): void + public function sendResponse(Application\Response $response): never { parent::sendResponse($this->response = $response); } diff --git a/tests/UI/Component.signals.phpt b/tests/UI/Component.signals.phpt index a3e37c1e9..24dc3e457 100644 --- a/tests/UI/Component.signals.phpt +++ b/tests/UI/Component.signals.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/ComponentReflection.parseAnnotation.phpt b/tests/UI/ComponentReflection.parseAnnotation.phpt deleted file mode 100644 index d9151d743..000000000 --- a/tests/UI/ComponentReflection.parseAnnotation.phpt +++ /dev/null @@ -1,42 +0,0 @@ -injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::error( diff --git a/tests/UI/Form.phpt b/tests/UI/Form.phpt index bae67de9f..c21b8207c 100644 --- a/tests/UI/Form.phpt +++ b/tests/UI/Form.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/LinkGenerator.parseDestination().phpt b/tests/UI/LinkGenerator.parseDestination().phpt index c3dc3faa8..a6e0a5614 100644 --- a/tests/UI/LinkGenerator.parseDestination().phpt +++ b/tests/UI/LinkGenerator.parseDestination().phpt @@ -4,7 +4,6 @@ use Nette\Application\LinkGenerator; use Nette\Application\UI\InvalidLinkException; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -46,7 +45,7 @@ Assert::same([ 'signal' => false, 'args' => [], 'fragment' => '#fragment', -], @LinkGenerator::parseDestination('a:b?#fragment')); // deprecated +], @LinkGenerator::parseDestination('a:b?#fragment')); // @ query string in link is deprecated Assert::same([ 'absolute' => false, @@ -54,7 +53,7 @@ Assert::same([ 'signal' => false, 'args' => ['a' => 'b', 'c' => 'd'], 'fragment' => '#fragment', -], @LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); // deprecated +], @LinkGenerator::parseDestination('a:b?a=b&c=d#fragment')); // @ query string in link is deprecated Assert::exception( fn() => LinkGenerator::parseDestination(''), diff --git a/tests/UI/Multiplier.phpt b/tests/UI/Multiplier.phpt index 3496de3b6..0628b9896 100644 --- a/tests/UI/Multiplier.phpt +++ b/tests/UI/Multiplier.phpt @@ -7,7 +7,6 @@ use Nette\Application; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/ParameterConverter.convertType.phpt b/tests/UI/ParameterConverter.convertType.phpt index eae59e8e6..573b61aea 100644 --- a/tests/UI/ParameterConverter.convertType.phpt +++ b/tests/UI/ParameterConverter.convertType.phpt @@ -7,7 +7,6 @@ use Nette\Application\UI\ParameterConverter; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -33,6 +32,18 @@ function testIt(string $type, $val, $res = null) } +enum TestStringEnum: string +{ + case Foo = 'foo'; + case One = '1'; +} + +enum TestIntEnum: int +{ + case One = 1; +} + + $obj = new stdClass; testIt('scalar', null, null); @@ -273,3 +284,28 @@ testIt('int|array', 0, 0); testIt('int|array', 1, 1); testIt('int|array', 1.0, 1); testIt('int|array', 1.2); + + +testIt(TestStringEnum::class, null); +testIt(TestStringEnum::class, []); +testIt(TestStringEnum::class, $obj); +testIt(TestStringEnum::class, TestStringEnum::Foo, TestStringEnum::Foo); +testIt(TestStringEnum::class, TestIntEnum::One); +testIt(TestStringEnum::class, 'foo', TestStringEnum::Foo); +testIt(TestStringEnum::class, 'bar'); +testIt(TestStringEnum::class, ''); +testIt(TestStringEnum::class, 1, TestStringEnum::One); +testIt(TestStringEnum::class, true, TestStringEnum::One); + +testIt(TestIntEnum::class, null); +testIt(TestIntEnum::class, TestIntEnum::One, TestIntEnum::One); +testIt(TestIntEnum::class, '1', TestIntEnum::One); +testIt(TestIntEnum::class, 1, TestIntEnum::One); +testIt(TestIntEnum::class, 1.0, TestIntEnum::One); +testIt(TestIntEnum::class, '1.0'); +testIt(TestIntEnum::class, 2); +testIt(TestIntEnum::class, 'foo'); + +testIt('?' . TestStringEnum::class, 'foo', TestStringEnum::Foo); +testIt(TestStringEnum::class . '|int', 2, 2); +testIt(TestStringEnum::class . '|int', 'foo', TestStringEnum::Foo); diff --git a/tests/UI/Presenter.canonicalize.phpt b/tests/UI/Presenter.canonicalize.phpt index f9947d8fa..0ce84ea7e 100644 --- a/tests/UI/Presenter.canonicalize.phpt +++ b/tests/UI/Presenter.canonicalize.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; @@ -173,7 +172,7 @@ test('canonicalize() uses 302 for varying requests', function () { $presenter->autoCanonicalize = true; $request = new Application\Request('Varying', 'GET', ['action' => 'default']); - $request->setFlag($request::VARYING, true); + $request->setFlag($request::Varying); $response = $presenter->run($request); diff --git a/tests/UI/Presenter.flashMessage.phpt b/tests/UI/Presenter.flashMessage.phpt index 6671cf51d..30c26d024 100644 --- a/tests/UI/Presenter.flashMessage.phpt +++ b/tests/UI/Presenter.flashMessage.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; @@ -129,7 +128,7 @@ test('multiple flash messages are stored', function () { $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); - $presenter->flashMessage('First message', 'info'); + $presenter->flashMessage('First message'); $presenter->flashMessage('Second message', 'error'); $presenter->flashMessage('Third message', 'success'); @@ -143,7 +142,7 @@ test('multiple flash messages are stored', function () { }); -test('flash message is available in template', function () { +test('flash message is written to an already created template', function () { $messages = []; $sessionSection = Mockery::mock(Http\SessionSection::class); $sessionSection->shouldReceive('get')->andReturnUsing(function () use (&$messages) { @@ -173,6 +172,45 @@ test('flash message is available in template', function () { $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + $template = $presenter->getTemplate(); + $presenter->flashMessage('Template message'); + + Assert::count(1, $template->flashes); + Assert::same('Template message', $template->flashes[0]->message); +}); + + +test('flash message is injected into a template created later', function () { + $messages = []; + $sessionSection = Mockery::mock(Http\SessionSection::class); + $sessionSection->shouldReceive('get')->andReturnUsing(function () use (&$messages) { + return $messages; + }); + $sessionSection->shouldReceive('set')->andReturnUsing(function ($id, $value) use (&$messages) { + $messages = $value; + }); + + $session = Mockery::mock(Http\Session::class); + $session->shouldReceive('getSection')->andReturn($sessionSection); + $session->shouldReceive('hasSection')->andReturn(true); + + $latte = Mockery::mock(Latte\Engine::class); + $latte->shouldIgnoreMissing(); + $latteFactory = Mockery::mock(Nette\Bridges\ApplicationLatte\LatteFactory::class); + $latteFactory->shouldReceive('create')->andReturn($latte); + $templateFactory = new Nette\Bridges\ApplicationLatte\TemplateFactory($latteFactory); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request(new Http\UrlScript), + new Http\Response, + session: $session, + templateFactory: $templateFactory, + ); + $presenter->autoCanonicalize = false; + + $presenter->run(new Application\Request('Test', 'GET', ['action' => 'default'])); + $presenter->flashMessage('Template message'); $template = $presenter->getTemplate(); @@ -186,10 +224,13 @@ test('flash session expires after presenter run', function () { $sessionSection = Mockery::mock(Http\SessionSection::class); $sessionSection->shouldReceive('get')->andReturn([]); $sessionSection->shouldReceive('set')->once(); - $sessionSection->shouldReceive('setExpiration')->once()->with('30 seconds')->andReturnUsing(function () use (&$expirationSet, $sessionSection) { - $expirationSet = true; - return $sessionSection; - }); + $sessionSection->shouldReceive('setExpiration') + ->once() + ->with('30 seconds') + ->andReturnUsing(function () use (&$expirationSet, $sessionSection) { + $expirationSet = true; + return $sessionSection; + }); $session = Mockery::mock(Http\Session::class); $session->shouldReceive('getSection')->andReturn($sessionSection); diff --git a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt index d7f81ba01..8efd269fd 100644 --- a/tests/UI/Presenter.formatLayoutTemplateFiles.phpt +++ b/tests/UI/Presenter.formatLayoutTemplateFiles.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/one/APresenter.php'; require __DIR__ . '/one/sub/BPresenter.php'; @@ -98,4 +97,7 @@ test('explicit layout file assignment', function () { Assert::same([ __DIR__ . '/file.latte', ], $presenter->formatLayoutTemplateFiles()); + + $presenter->setLayout('dir\file.latte'); + Assert::same(['dir\file.latte'], $presenter->formatLayoutTemplateFiles()); }); diff --git a/tests/UI/Presenter.formatTemplateClass.phpt b/tests/UI/Presenter.formatTemplateClass.phpt index 68eebb781..115c65e71 100644 --- a/tests/UI/Presenter.formatTemplateClass.phpt +++ b/tests/UI/Presenter.formatTemplateClass.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.formatTemplateFiles.phpt b/tests/UI/Presenter.formatTemplateFiles.phpt index 8031f366f..e79b06f89 100644 --- a/tests/UI/Presenter.formatTemplateFiles.phpt +++ b/tests/UI/Presenter.formatTemplateFiles.phpt @@ -6,7 +6,6 @@ use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/one/APresenter.php'; require __DIR__ . '/one/sub/BPresenter.php'; diff --git a/tests/UI/Presenter.initGlobalParameters().phpt b/tests/UI/Presenter.initGlobalParameters().phpt index 3caee4d3d..bed395da4 100644 --- a/tests/UI/Presenter.initGlobalParameters().phpt +++ b/tests/UI/Presenter.initGlobalParameters().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.lifecycle.phpt b/tests/UI/Presenter.lifecycle.phpt index c1b3d38a4..b68e448ff 100644 --- a/tests/UI/Presenter.lifecycle.phpt +++ b/tests/UI/Presenter.lifecycle.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; diff --git a/tests/UI/Presenter.link().enum.phpt b/tests/UI/Presenter.link().enum.phpt new file mode 100644 index 000000000..84bf72514 --- /dev/null +++ b/tests/UI/Presenter.link().enum.phpt @@ -0,0 +1,99 @@ +sort); + + // non-default value is carried in URL as the case value + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this')); + + // enum instance and its value are accepted in args + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this', ['sort' => Sort::Desc])); + Assert::same('/index.php?sort=desc&action=default&presenter=Test', $this->link('this', ['sort' => 'desc'])); + + // default value is omitted + Assert::same('/index.php?action=default&presenter=Test', $this->link('this', ['sort' => Sort::Asc])); + $this->sort = Sort::Asc; + Assert::same('/index.php?action=default&presenter=Test', $this->link('this')); + + // enum in action method argument + Assert::same('/index.php?dir=desc&action=other&presenter=Test', $this->link('other', ['dir' => Sort::Desc])); + Assert::same('/index.php?dir=desc&action=other&presenter=Test', $this->link('other', ['dir' => 'desc'])); + Assert::same('/index.php?action=other&presenter=Test', $this->link('other', ['dir' => Sort::Asc])); + + $this->terminate(); + } + + + public function actionOther(Sort $dir = Sort::Asc): void + { + } +} + + +function createPresenter(): TestPresenter +{ + $url = new Http\UrlScript('http://localhost/index.php', '/index.php'); + $presenterFactory = Mockery::mock(Nette\Application\IPresenterFactory::class); + $presenterFactory->shouldReceive('getPresenterClass') + ->andReturnUsing(fn($presenter) => $presenter . 'Presenter'); + + $presenter = new TestPresenter; + $presenter->injectPrimary( + new Http\Request($url), + new Http\Response, + $presenterFactory, + new Application\Routers\SimpleRouter, + ); + $presenter->autoCanonicalize = false; + return $presenter; +} + + +createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => 'desc'])); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => 'bogus'])), + Application\BadRequestException::class, + "Value passed to persistent parameter 'sort' in presenter Test must be Sort, string given.", +); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['sort' => ['name' => 'desc', 'value' => 'desc']])), + Application\BadRequestException::class, + "Value passed to persistent parameter 'sort' in presenter Test must be Sort, array given.", +); + + +Assert::exception( + fn() => createPresenter()->run(new Application\Request('Test', Http\Request::Get, ['action' => 'other', 'dir' => 'bogus'])), + Application\BadRequestException::class, + 'Argument $dir passed to TestPresenter::actionOther() must be Sort, string given.', +); diff --git a/tests/UI/Presenter.link().persistent.phpt b/tests/UI/Presenter.link().persistent.phpt index ee4f24348..4a65954f3 100644 --- a/tests/UI/Presenter.link().persistent.phpt +++ b/tests/UI/Presenter.link().persistent.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -161,6 +160,6 @@ test('Presenter link() with persistent parameters', function () { $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; $presenter->autoCanonicalize = false; - $request = new Application\Request('Test', Http\Request::Get, []); + $request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); }); diff --git a/tests/UI/Presenter.link().phpt b/tests/UI/Presenter.link().phpt index 74ca7e02e..60afeb7e4 100644 --- a/tests/UI/Presenter.link().phpt +++ b/tests/UI/Presenter.link().phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -76,17 +75,46 @@ class TestPresenter extends Application\UI\Presenter // standard Assert::same("#error: Invalid destination ''.", $this->link('')); Assert::same('/index.php?action=params&presenter=Test', $this->link('params')); - Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + ['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); Assert::same('http://localhost/index.php?action=params&presenter=Test', $this->link('//params')); - Assert::same('#error: Argument $arr passed to TestPresenter::actionParams() must be scalar, array given.', $this->link('params', [1, 2, 3, []])); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionParams() must be scalar, array given.', + $this->link('params', [1, 2, 3, []]), + ); Assert::same('/index.php?xx=1&action=params&presenter=Test', $this->link('params', ['xx' => 1, 'yy' => []])); - Assert::same(['xx' => 1, 'yy' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', $this->link('this', ['sort' => ['y' => ['asc' => true]]])); - Assert::same(['sort' => ['y' => ['asc' => true]], 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + ['xx' => 1, 'yy' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?sort%5By%5D%5Basc%5D=1&action=default&presenter=Test', + $this->link('this', ['sort' => ['y' => ['asc' => true]]]), + ); + Assert::same( + [ + 'sort' => ['y' => ['asc' => true]], + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'mycontrol-order' => null, + 'mycontrol-round' => null, + 'action' => 'default', + ], + $this->getLastCreatedRequest()->getParameters(), + ); - Assert::same("#error: Unable to pass parameters to action 'Test:product', missing corresponding method TestPresenter::renderProduct().", $this->link('product', 1)); + Assert::same( + "#error: Unable to pass parameters to action 'Test:product', missing corresponding method TestPresenter::renderProduct().", + $this->link('product', 1), + ); Assert::same('/index.php?a=1&action=product&presenter=Test', $this->link('product', ['a' => 1])); - Assert::same('#error: Passed more parameters than method TestPresenter::actionParams() expects.', $this->link('params', 1, 2, 3, 4, 5)); + Assert::same( + '#error: Passed more parameters than method TestPresenter::actionParams() expects.', + $this->link('params', 1, 2, 3, 4, 5), + ); // fragment via args Assert::same('/index.php?action=product&presenter=Test#fragment', $this->link('product', ['#' => 'fragment'])); @@ -96,7 +124,10 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=product&presenter=Test', $this->link('product', ['#' => ''])); // empty fragment omitted Assert::same('/index.php?action=product&presenter=Test', $this->link('product', ['#' => null])); Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'product'], $this->getLastCreatedRequest()->getParameters()); // # not propagated to request - Assert::same('http://localhost/index.php?x=1&action=product&presenter=Test#fragment', $this->link('//product', ['x' => 1, '#' => 'fragment'])); + Assert::same( + 'http://localhost/index.php?x=1&action=product&presenter=Test#fragment', + $this->link('//product', ['x' => 1, '#' => 'fragment']), + ); Assert::same('/index.php?action=product&presenter=Test#hello%20world%20%26%20co', $this->link('product', ['#' => 'hello world & co'])); // URL encoding Assert::same('/index.php?action=product&presenter=Test#42', $this->link('product', ['#' => 42])); // scalar coercion Assert::same("#error: Value of '#' must be scalar, array given.", $this->link('product', ['#' => [1, 2]])); @@ -105,28 +136,67 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=product&presenter=Test#frag', (string) new Application\UI\Link($this, 'product', ['#' => 'frag'])); // Link class // absolute - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product#fragment', ['x' => 1, 'y' => 2])); + Assert::same( + 'http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', + $this->link('//product#fragment', ['x' => 1, 'y' => 2]), + ); $this->absoluteUrls = true; - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('product#fragment', ['x' => 1, 'y' => 2])); - Assert::same('http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', $this->link('//product#fragment', ['x' => 1, 'y' => 2])); + Assert::same( + 'http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', + $this->link('product#fragment', ['x' => 1, 'y' => 2]), + ); + Assert::same( + 'http://localhost/index.php?x=1&y=2&action=product&presenter=Test#fragment', + $this->link('//product#fragment', ['x' => 1, 'y' => 2]), + ); $this->absoluteUrls = false; // persistent params - Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); - Assert::same('/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]])); + Assert::same('/index.php?p=&action=params&presenter=Test', $this->link('params', ['pint' => $this->pint, 'p' => ''])); + Assert::same( + '/index.php?pint=20&parr%5B0%5D=1&action=params&presenter=Test', + $this->link('params', ['pint' => $this->pint * 2, 'pbool' => true, 'parr' => [1]]), + ); Assert::same(['pint' => 20, 'pbool' => null, 'parr' => [1], 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?pint=1&pbool=0&action=params&presenter=Test', $this->link('params', ['pint' => true, 'pbool' => '0', 'parr' => []])); - Assert::same('/index.php?pint=0&pbool=0&p=0&action=params&presenter=Test', $this->link('params', ['pint' => false, 'pbool' => false, 'p' => false, 'parr' => null])); - Assert::same("#error: Value passed to persistent parameter 'pbool' in presenter Test must be bool, string given.", $this->link('this', ['p' => null, 'pbool' => 'a'])); - Assert::same("#error: Value passed to persistent parameter 'p' in presenter Test must be scalar, array given.", $this->link('this', ['p' => [1], 'pbool' => false])); + Assert::same( + '/index.php?pint=1&pbool=0&action=params&presenter=Test', + $this->link('params', ['pint' => true, 'pbool' => '0', 'parr' => []]), + ); + Assert::same( + '/index.php?pint=0&pbool=0&p=0&action=params&presenter=Test', + $this->link('params', ['pint' => false, 'pbool' => false, 'p' => false, 'parr' => null]), + ); + Assert::same( + "#error: Value passed to persistent parameter 'pbool' in presenter Test must be bool, string given.", + $this->link('this', ['p' => null, 'pbool' => 'a']), + ); + Assert::same( + "#error: Value passed to persistent parameter 'p' in presenter Test must be scalar, array given.", + $this->link('this', ['p' => [1], 'pbool' => false]), + ); Assert::same('/index.php?action=persistent&presenter=Test', $this->link('persistent')); - Assert::same('/index.php?pbooln=1&parrn%5B0%5D=1&action=params&presenter=Test', $this->link('params', ['pbooln' => true, 'parrn' => [1]])); - Assert::same(['pbooln' => true, 'parrn' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + '/index.php?pbooln=1&parrn%5B0%5D=1&action=params&presenter=Test', + $this->link('params', ['pbooln' => true, 'parrn' => [1]]), + ); + Assert::same( + ['pbooln' => true, 'parrn' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); Assert::same('/index.php?pbooln=0&action=params&presenter=Test', $this->link('params', ['pbooln' => '0', 'parrn' => []])); - Assert::same(['pbooln' => false, 'parrn' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same("#error: Value passed to persistent parameter 'pbooln' in presenter Test must be ?bool, string given.", $this->link('params', ['pbooln' => 'a'])); - Assert::same("#error: Value passed to persistent parameter 'parrn' in presenter Test must be array, string given.", $this->link('params', ['parrn' => 'a'])); + Assert::same( + ['pbooln' => false, 'parrn' => [], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + "#error: Value passed to persistent parameter 'pbooln' in presenter Test must be ?bool, string given.", + $this->link('params', ['pbooln' => 'a']), + ); + Assert::same( + "#error: Value passed to persistent parameter 'parrn' in presenter Test must be array, string given.", + $this->link('params', ['parrn' => 'a']), + ); // Other presenter & action link Assert::same('/index.php?action=product&presenter=Other', $this->link('Other:product', ['p' => $this->p])); @@ -141,94 +211,333 @@ class TestPresenter extends Application\UI\Presenter Assert::same('/index.php?action=default&do=signal&presenter=Test', $this->link('signal!', ['p' => $this->p])); Assert::same('/index.php?p=0&action=default&do=signal&presenter=Test', $this->link('signal!', [1, 'p' => $this->p * 2])); Assert::same('/index.php?y=2&action=default&do=signal&presenter=Test', $this->link('signal!', 1, 2)); - Assert::same(['x' => null, 'y' => 2, 'pint' => null, 'parr' => null, 'pbool' => null, 'mycontrol-order' => null, 'mycontrol-round' => null, 'action' => 'default', 'do' => 'signal'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + [ + 'x' => null, + 'y' => 2, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'mycontrol-order' => null, + 'mycontrol-round' => null, + 'action' => 'default', + 'do' => 'signal', + ], + $this->getLastCreatedRequest()->getParameters(), + ); // Component link Assert::same("#error: Invalid destination ''.", $this['mycontrol']->link('', 0, 1)); - Assert::same('/index.php?mycontrol-x=0&mycontrol-y=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', 0, 1)); - Assert::same('/index.php?mycontrol-x=0a&mycontrol-y=1a&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', '0a', '1a')); + Assert::same( + '/index.php?mycontrol-x=0&mycontrol-y=1&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', 0, 1), + ); + Assert::same( + '/index.php?mycontrol-x=0a&mycontrol-y=1a&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', '0a', '1a'), + ); Assert::same('/index.php?mycontrol-x=1&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', [1])); - Assert::same('#error: Argument $x passed to TestControl::handleClick() must be scalar, array given.', $this['mycontrol']->link('click', [1], (object) [1])); - Assert::same('/index.php?mycontrol-x=1&mycontrol-y=0&action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', true, false)); - Assert::same('/index.php?action=default&do=mycontrol-click&presenter=Test', $this['mycontrol']->link('click', null, '')); - Assert::same('#error: Passed more parameters than method TestControl::handleClick() expects.', $this['mycontrol']->link('click', 1, 2, 3)); - Assert::same('http://localhost/index.php?mycontrol-x=1&mycontrol-round=1&action=default&presenter=Test#frag', $this['mycontrol']->link('//this#frag', ['x' => 1, 'round' => 1])); - Assert::same('/index.php?mycontrol-x=1&mycontrol-y=2&action=default&do=mycontrol-click&presenter=Test', $this->link('mycontrol:click!', ['x' => 1, 'y' => 2, 'round' => 0])); - Assert::same(['mycontrol-x' => 1, 'mycontrol-y' => 2, 'mycontrol-round' => null, 'mycontrol-order' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'default', 'do' => 'mycontrol-click'], $this->getLastCreatedRequest()->getParameters()); + Assert::same( + '#error: Argument $x passed to TestControl::handleClick() must be scalar, array given.', + $this['mycontrol']->link('click', [1], (object) [1]), + ); + Assert::same( + '/index.php?mycontrol-x=1&mycontrol-y=0&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', true, false), + ); + Assert::same( + '/index.php?mycontrol-y=&action=default&do=mycontrol-click&presenter=Test', + $this['mycontrol']->link('click', null, ''), + ); + Assert::same( + '#error: Passed more parameters than method TestControl::handleClick() expects.', + $this['mycontrol']->link('click', 1, 2, 3), + ); + Assert::same( + 'http://localhost/index.php?mycontrol-x=1&mycontrol-round=1&action=default&presenter=Test#frag', + $this['mycontrol']->link('//this#frag', ['x' => 1, 'round' => 1]), + ); + Assert::same( + '/index.php?mycontrol-x=1&mycontrol-y=2&action=default&do=mycontrol-click&presenter=Test', + $this->link('mycontrol:click!', ['x' => 1, 'y' => 2, 'round' => 0]), + ); + Assert::same( + [ + 'mycontrol-x' => 1, + 'mycontrol-y' => 2, + 'mycontrol-round' => null, + 'mycontrol-order' => null, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'default', + 'do' => 'mycontrol-click', + ], + $this->getLastCreatedRequest()->getParameters(), + ); // Component link type checking - Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", $this['mycontrol']->link('click', ['order' => 1])); - Assert::same("#error: Value passed to persistent parameter 'round' in component 'mycontrol' must be int, array given.", $this['mycontrol']->link('click', ['round' => []])); + Assert::same( + "#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", + $this['mycontrol']->link('click', ['order' => 1]), + ); + Assert::same( + "#error: Value passed to persistent parameter 'round' in component 'mycontrol' must be int, array given.", + $this['mycontrol']->link('click', ['round' => []]), + ); $this['mycontrol']->order = 1; - Assert::same("#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", $this['mycontrol']->link('click')); + Assert::same( + "#error: Value passed to persistent parameter 'order' in component 'mycontrol' must be array, int given.", + $this['mycontrol']->link('click'), + ); $this['mycontrol']->order = null; // type checking Assert::same('/index.php?action=params&presenter=Test', $this->link('params', [])); - Assert::same(['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?int=1&bool=1&str=abc&arr=1&action=params&presenter=Test', $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1'])); - Assert::same('/index.php?int=0&bool=0&action=params&presenter=Test', $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => ''])); + Assert::same( + ['pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'params'], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?action=params&presenter=Test', + $this->link('params', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + [ + 'int' => null, + 'bool' => null, + 'str' => null, + 'arr' => null, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'params', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr=1&action=params&presenter=Test', + $this->link('params', ['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => '1']), + ); + Assert::same( + '/index.php?int=0&bool=0&str=&arr=&action=params&presenter=Test', + $this->link('params', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=params&presenter=Test', $this->link('params', ['int' => new stdClass])); Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', [])); - Assert::same('#error: Missing parameter $int required by TestPresenter::actionHints()', $this->link('hints', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hints&presenter=Test', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hints&presenter=Test', $this->link('hints', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, string given.', $this->link('hints', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, stdClass given.', $this->link('hints', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHints() must be int, array given.', $this->link('hints', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHints() must be bool, string given.', $this->link('hints', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHints() must be array, string given.', $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '#error: Missing parameter $int required by TestPresenter::actionHints()', + $this->link('hints', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hints&presenter=Test', + $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hints&presenter=Test', + $this->link('hints', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, string given.', + $this->link('hints', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, stdClass given.', + $this->link('hints', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHints() must be int, array given.', + $this->link('hints', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHints() must be bool, string given.', + $this->link('hints', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHints() must be array, string given.', + $this->link('hints', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', [])); - Assert::same('/index.php?action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hintsNulls&presenter=Test', $this->link('hintsNulls', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, string given.', $this->link('hintsNulls', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, stdClass given.', $this->link('hintsNulls', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, array given.', $this->link('hintsNulls', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNulls() must be ?bool, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNulls() must be ?array, string given.', $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hintsNulls&presenter=Test', + $this->link('hintsNulls', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, string given.', + $this->link('hintsNulls', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, stdClass given.', + $this->link('hintsNulls', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNulls() must be ?int, array given.', + $this->link('hintsNulls', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsNulls() must be ?bool, string given.', + $this->link('hintsNulls', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsNulls() must be ?array, string given.', + $this->link('hintsNulls', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', [])); - Assert::same('/index.php?action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same('/index.php?int=0&bool=0&action=hintsNullable&presenter=Test', $this->link('hintsNullable', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, string given.', $this->link('hintsNullable', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, stdClass given.', $this->link('hintsNullable', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, array given.', $this->link('hintsNullable', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsNullable() must be ?bool, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsNullable() must be ?array, string given.', $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + '/index.php?int=0&bool=0&action=hintsNullable&presenter=Test', + $this->link('hintsNullable', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, string given.', + $this->link('hintsNullable', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, stdClass given.', + $this->link('hintsNullable', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsNullable() must be ?int, array given.', + $this->link('hintsNullable', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsNullable() must be ?bool, string given.', + $this->link('hintsNullable', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsNullable() must be ?array, string given.', + $this->link('hintsNullable', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', [])); - Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]])); - Assert::same(['int' => 1, 'bool' => true, 'str' => 'abc', 'arr' => [1], 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'hintsDefaults'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?action=hintsDefaults&presenter=Test', $this->link('hintsDefaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, string given.', $this->link('hintsDefaults', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, stdClass given.', $this->link('hintsDefaults', ['int' => new stdClass])); - Assert::same('#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, array given.', $this->link('hintsDefaults', ['int' => []])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionHintsDefaults() must be bool, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionHintsDefaults() must be array, string given.', $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); + Assert::same( + '/index.php?action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?int=1&bool=1&str=abc&arr%5B0%5D=1&action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => 'abc', 'arr' => [1]]), + ); + Assert::same( + [ + 'int' => 1, + 'bool' => true, + 'str' => 'abc', + 'arr' => [1], + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'hintsDefaults', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?action=hintsDefaults&presenter=Test', + $this->link('hintsDefaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, string given.', + $this->link('hintsDefaults', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, stdClass given.', + $this->link('hintsDefaults', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionHintsDefaults() must be int, array given.', + $this->link('hintsDefaults', ['int' => []]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionHintsDefaults() must be bool, string given.', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionHintsDefaults() must be array, string given.', + $this->link('hintsDefaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', [])); - Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null])); - Assert::same('/index.php?action=defaults&presenter=Test', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => 'a', 'arr' => [1]])); - Assert::same(['int' => null, 'bool' => null, 'str' => null, 'arr' => null, 'pint' => null, 'parr' => null, 'pbool' => null, 'action' => 'defaults'], $this->getLastCreatedRequest()->getParameters()); - Assert::same('/index.php?int=0&bool=0&str=&action=defaults&presenter=Test', $this->link('defaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, string given.', $this->link('defaults', ['int' => ''])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, array given.', $this->link('defaults', ['int' => []])); - Assert::same('#error: Argument $int passed to TestPresenter::actionDefaults() must be int, stdClass given.', $this->link('defaults', ['int' => new stdClass])); - Assert::same('#error: Argument $bool passed to TestPresenter::actionDefaults() must be bool, string given.', $this->link('defaults', ['int' => '1', 'bool' => ''])); - Assert::same('#error: Argument $arr passed to TestPresenter::actionDefaults() must be array, string given.', $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => ''])); - - Assert::same('/index.php?action=objects&presenter=Test', $this->link('objects', ['req' => new stdClass, 'nullable' => new stdClass, 'opt' => new stdClass])); + Assert::same( + '/index.php?action=defaults&presenter=Test', + $this->link('defaults', ['int' => null, 'bool' => null, 'str' => null, 'arr' => null]), + ); + Assert::same( + '/index.php?action=defaults&presenter=Test', + $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => 'a', 'arr' => [1]]), + ); + Assert::same( + [ + 'int' => null, + 'bool' => null, + 'str' => null, + 'arr' => null, + 'pint' => null, + 'parr' => null, + 'pbool' => null, + 'action' => 'defaults', + ], + $this->getLastCreatedRequest()->getParameters(), + ); + Assert::same( + '/index.php?int=0&bool=0&str=&action=defaults&presenter=Test', + $this->link('defaults', ['int' => 0, 'bool' => false, 'str' => '', 'arr' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, string given.', + $this->link('defaults', ['int' => '']), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, array given.', + $this->link('defaults', ['int' => []]), + ); + Assert::same( + '#error: Argument $int passed to TestPresenter::actionDefaults() must be int, stdClass given.', + $this->link('defaults', ['int' => new stdClass]), + ); + Assert::same( + '#error: Argument $bool passed to TestPresenter::actionDefaults() must be bool, string given.', + $this->link('defaults', ['int' => '1', 'bool' => '']), + ); + Assert::same( + '#error: Argument $arr passed to TestPresenter::actionDefaults() must be array, string given.', + $this->link('defaults', ['int' => '1', 'bool' => '1', 'str' => '', 'arr' => '']), + ); + + Assert::same( + '/index.php?action=objects&presenter=Test', + $this->link('objects', ['req' => new stdClass, 'nullable' => new stdClass, 'opt' => new stdClass]), + ); Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', [])); - Assert::same('#error: Missing parameter $req required by TestPresenter::actionObjects()', $this->link('objects', ['req' => null, 'nullable' => null, 'opt' => null])); - Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, Exception given.', $this->link('objects', ['req' => new Exception, 'opt' => null])); - Assert::same('#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, array given.', $this->link('objects', ['req' => []])); + Assert::same( + '#error: Missing parameter $req required by TestPresenter::actionObjects()', + $this->link('objects', ['req' => null, 'nullable' => null, 'opt' => null]), + ); + Assert::same( + '#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, Exception given.', + $this->link('objects', ['req' => new Exception, 'opt' => null]), + ); + Assert::same( + '#error: Argument $req passed to TestPresenter::actionObjects() must be stdClass, array given.', + $this->link('objects', ['req' => []]), + ); // silent invalid link mode $this->invalidLinkMode = self::InvalidLinkSilent; @@ -326,5 +635,5 @@ $presenter->injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; $presenter->autoCanonicalize = false; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); diff --git a/tests/UI/Presenter.paramChecking.phpt b/tests/UI/Presenter.paramChecking.phpt index 3cf341cf4..04f7fe318 100644 --- a/tests/UI/Presenter.paramChecking.phpt +++ b/tests/UI/Presenter.paramChecking.phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @require __DIR__ . '/fixtures/ParamPresenter.php'; // @ Required parameter $c follows optional parameter $b diff --git a/tests/UI/Presenter.redirects.phpt b/tests/UI/Presenter.redirects.phpt index 51476ade1..8744d26f7 100644 --- a/tests/UI/Presenter.redirects.phpt +++ b/tests/UI/Presenter.redirects.phpt @@ -8,7 +8,6 @@ use Nette\{Application, Http}; use Nette\Application\Attributes\Persistent; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; require __DIR__ . '/functions.php'; diff --git a/tests/UI/Presenter.selfLinkInvalidParam.phpt b/tests/UI/Presenter.selfLinkInvalidParam.phpt index 95bd4e599..193e27a2f 100644 --- a/tests/UI/Presenter.selfLinkInvalidParam.phpt +++ b/tests/UI/Presenter.selfLinkInvalidParam.phpt @@ -9,7 +9,6 @@ use Nette\{Application, Http}; use Nette\Application\UI\{InvalidLinkException, InvalidRequestParameterException, Presenter}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; diff --git a/tests/UI/Presenter.storeRequest().phpt b/tests/UI/Presenter.storeRequest().phpt index a409f9ec9..c5bcb7979 100644 --- a/tests/UI/Presenter.storeRequest().phpt +++ b/tests/UI/Presenter.storeRequest().phpt @@ -7,7 +7,6 @@ use Nette\{Application, Http}; use Tester\Assert; - require __DIR__ . '/../bootstrap.php'; @@ -62,7 +61,7 @@ test('request storage with user context', function () { $userMock, ); - $applicationRequest = new Application\Request('', '', []); + $applicationRequest = new Application\Request('', ''); $presenter->run($applicationRequest); $expiration = '+1 year'; @@ -113,7 +112,7 @@ test('request storage without user context', function () { $sessionMock, ); - $applicationRequest = new Application\Request('', '', []); + $applicationRequest = new Application\Request('', ''); $presenter->run($applicationRequest); $expiration = '+1 year'; diff --git a/tests/UI/Presenter.switch.loop.phpt b/tests/UI/Presenter.switch.loop.phpt new file mode 100644 index 000000000..f0ee94304 --- /dev/null +++ b/tests/UI/Presenter.switch.loop.phpt @@ -0,0 +1,34 @@ +switch('bar'); + } + + + public function actionBar(): void + { + $this->switch('foo'); + } +} + + +$presenter = createPresenter(SwitchLoopPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['action' => 'foo'])), + LogicException::class, + 'Infinite loop of switch() calls detected.', +); diff --git a/tests/UI/Requires.combinations.phpt b/tests/UI/Requires.combinations.phpt index 07f60a8ae..942f27414 100644 --- a/tests/UI/Requires.combinations.phpt +++ b/tests/UI/Requires.combinations.phpt @@ -236,7 +236,7 @@ test('forward requirement satisfied', function () { $presenter = createPresenter(ForwardPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('Test', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('Test', Application\Request::Forward)), ); }); diff --git a/tests/UI/Requires.forward.link().phpt b/tests/UI/Requires.forward.link().phpt index 7d11fdf69..0c4221284 100644 --- a/tests/UI/Requires.forward.link().phpt +++ b/tests/UI/Requires.forward.link().phpt @@ -61,7 +61,7 @@ $presenter->injectPrimary( $presenter->invalidLinkMode = TestPresenter::InvalidLinkWarning; -$request = new Application\Request('Test', Http\Request::Get, []); +$request = new Application\Request('Test', Http\Request::Get); $presenter->run($request); Assert::error( @@ -90,7 +90,24 @@ Assert::error( Assert::noError(fn() => $presenter->link('Test:')); +Assert::false($presenter->isLinkCurrent('foo')); +Assert::false($presenter->isLinkCurrent('Forward:')); +Assert::false($presenter->isLinkCurrent('Forward:*')); + Assert::error( // no error fn() => $presenter->forward('Forward:'), Application\AbortException::class, ); + + +$presenter = new ForwardPresenter; +$presenter->injectPrimary( + new Http\Request($url), + new Http\Response, + $presenterFactory, + new Application\Routers\SimpleRouter, +); +$presenter->run(new Application\Request('Forward', Application\Request::Forward)); + +Assert::true($presenter->isLinkCurrent('this')); +Assert::true($presenter->isLinkCurrent('Forward:*')); diff --git a/tests/UI/Requires.forward.phpt b/tests/UI/Requires.forward.phpt index f275663d2..3ebbce54a 100644 --- a/tests/UI/Requires.forward.phpt +++ b/tests/UI/Requires.forward.phpt @@ -57,7 +57,7 @@ class TestSwitchViewPresenter extends Nette\Application\UI\Presenter // forwarded request $presenter = createPresenter(TestForwardPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( diff --git a/tests/UI/Requires.inheritance.phpt b/tests/UI/Requires.inheritance.phpt new file mode 100644 index 000000000..d4483e814 --- /dev/null +++ b/tests/UI/Requires.inheritance.phpt @@ -0,0 +1,85 @@ +terminate(); + } +} + +#[Requires(forward: false)] +class OverridingRequiresPresenter extends BaseRequiresPresenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + +#[Requires(methods: ['POST'])] +class CombiningRequiresPresenter extends BaseRequiresPresenter +{ + public function actionDefault(): never + { + $this->terminate(); + } +} + + +// rule inherited from ancestor is enforced +$presenter = createPresenter(InheritingRequiresPresenter::class); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), +); + +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Forwarded request is required by InheritingRequiresPresenter', +); + + +// rule explicitly set in descendant overrides the ancestor +$presenter = createPresenter(OverridingRequiresPresenter::class); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); + + +// descendant rule combines with a different inherited rule +$presenter = createPresenter(CombiningRequiresPresenter::class, method: 'POST'); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Forwarded request is required by CombiningRequiresPresenter', +); + +$presenter = createPresenter(CombiningRequiresPresenter::class, method: 'POST'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), +); + +$presenter = createPresenter(CombiningRequiresPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + Application\BadRequestException::class, + 'Method GET is not allowed by CombiningRequiresPresenter', +); diff --git a/tests/UI/Requires.locations.phpt b/tests/UI/Requires.locations.phpt index 4a907dec1..6699735ca 100644 --- a/tests/UI/Requires.locations.phpt +++ b/tests/UI/Requires.locations.phpt @@ -80,11 +80,10 @@ class TestMethodCreateComponentControl extends Nette\Application\UI\Control } - // class-level attribute $presenter = createPresenter(TestClassPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -97,7 +96,7 @@ Assert::exception( // method action() $presenter = createPresenter(TestMethodActionPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -110,7 +109,7 @@ Assert::exception( // method render() $presenter = createPresenter(TestMethodRenderPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD)), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward)), ); Assert::exception( @@ -123,7 +122,7 @@ Assert::exception( // method handle() $presenter = createPresenter(TestMethodHandlePresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['do' => 'foo'])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['do' => 'foo'])), ); Assert::exception( @@ -136,7 +135,7 @@ Assert::exception( // method createComponent() $presenter = createPresenter(TestMethodCreateComponentPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['foo-a' => 1])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['foo-a' => 1])), ); Assert::exception( @@ -149,7 +148,7 @@ Assert::exception( // method createComponent() in component $presenter = createPresenter(TestMethodCreateComponentPresenter::class); Assert::noError( - fn() => $presenter->run(new Application\Request('', Application\Request::FORWARD, ['bar-foo-a' => 1])), + fn() => $presenter->run(new Application\Request('', Application\Request::Forward, ['bar-foo-a' => 1])), ); Assert::exception( diff --git a/tests/UI/Requires.methods.phpt b/tests/UI/Requires.methods.phpt index c32ae5e03..36a5dd191 100644 --- a/tests/UI/Requires.methods.phpt +++ b/tests/UI/Requires.methods.phpt @@ -32,6 +32,31 @@ class TestAllMethodsPresenter extends Nette\Application\UI\Presenter } +class TestActionMethodsPresenter extends Nette\Application\UI\Presenter +{ + #[Requires(methods: ['OPTIONS'])] + public function actionDefault(): never + { + $this->terminate(); + } +} + + +class TestRenderMethodsPresenter extends Nette\Application\UI\Presenter +{ + public function actionDefault(): void + { + } + + + #[Requires(methods: ['OPTIONS'])] + public function renderDefault(): never + { + $this->terminate(); + } +} + + $presenter = createPresenter(TestMethodsPresenter::class, method: 'OPTIONS'); Assert::noError( fn() => $presenter->run(new Application\Request('', Http\Request::Get)), @@ -50,3 +75,25 @@ $presenter = createPresenter(TestAllMethodsPresenter::class, method: 'OPTIONS'); Assert::noError( fn() => $presenter->run(new Application\Request('', Http\Request::Get)), ); + + +// attribute on the action method can allow methods outside the default whitelist +$presenter = createPresenter(TestActionMethodsPresenter::class, method: 'OPTIONS'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); + + +$presenter = createPresenter(TestActionMethodsPresenter::class); +Assert::exception( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), + Application\BadRequestException::class, + 'Method GET is not allowed by TestActionMethodsPresenter::actionDefault()', +); + + +// attribute on the render method counts as well +$presenter = createPresenter(TestRenderMethodsPresenter::class, method: 'OPTIONS'); +Assert::noError( + fn() => $presenter->run(new Application\Request('', Http\Request::Get)), +); diff --git a/tests/UI/fixtures/ParamPresenter.php b/tests/UI/fixtures/ParamPresenter.php index 39ad7bc77..235fb8b2d 100644 --- a/tests/UI/fixtures/ParamPresenter.php +++ b/tests/UI/fixtures/ParamPresenter.php @@ -6,7 +6,7 @@ class ParamPresenter extends Nette\Application\UI\Presenter public $bool = true; - public function actionDefault($a, $b = null, array $c, array $d = null, $e = 1, $f = 1.0, $g = false) + public function actionDefault($a, $b = null, array $c, ?array $d = null, $e = 1, $f = 1.0, $g = false) { } } diff --git a/tests/UI/functions.php b/tests/UI/functions.php index 048130af4..d80383b88 100644 --- a/tests/UI/functions.php +++ b/tests/UI/functions.php @@ -1,6 +1,5 @@ |null, fragment: string}', + 'array{absolute: bool, path: string, signal: bool, args: array|null, fragment: string}', LinkGenerator::parseDestination('Presenter:action'), ); } @@ -153,7 +155,7 @@ function testParseDestination(): void function testTemplateFactoryCreateTemplate(TemplateFactory $factory): void { $template = $factory->createTemplate(); - assertType('Nette\Bridges\ApplicationLatte\Template', $template); + assertType(Nette\Bridges\ApplicationLatte\Template::class, $template); }