From 26d298e46eb6150c6c43db3f02f1db7eae20b7f9 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 29 Dec 2025 23:17:48 +0100 Subject: [PATCH 01/13] updated .gitattributes & .gitignore --- .gitattributes | 17 +++++++++-------- .gitignore | 1 + 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.gitattributes b/.gitattributes index 9670e954..433a2de9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,9 +1,10 @@ -.gitattributes export-ignore -.gitignore export-ignore -.github export-ignore -ncs.* export-ignore -phpstan.neon export-ignore -tests/ export-ignore +.gitattributes export-ignore +.github/ export-ignore +.gitignore export-ignore +ncs.* export-ignore +phpstan*.neon export-ignore +src/**/*.latte export-ignore +tests/ export-ignore -*.sh eol=lf -*.php* diff=php linguist-language=PHP +*.php* diff=php +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index de4a392c..d49bcd46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /vendor /composer.lock +tests/lock From 07cb42ebd00fd73d5395ec767d4625a9f955da37 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Feb 2026 16:14:30 +0100 Subject: [PATCH 02/13] improved tests - removed slow getRemoteHost() reverse DNS lookups assertions --- .github/workflows/coding-style.yml | 6 +++--- .github/workflows/tests.yml | 9 +++++---- composer.json | 2 +- tests/Http/RequestFactory.proxy.forwarded.phpt | 2 -- tests/Http/RequestFactory.proxy.x-forwarded.phpt | 5 ----- tests/Http/SessionSection.setExpiration().phpt | 1 + tests/bootstrap.php | 7 +------ 7 files changed, 11 insertions(+), 21 deletions(-) diff --git a/.github/workflows/coding-style.yml b/.github/workflows/coding-style.yml index b5c5acc4..6d911046 100644 --- a/.github/workflows/coding-style.yml +++ b/.github/workflows/coding-style.yml @@ -10,11 +10,11 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 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 + - run: php temp/code-checker/code-checker --strict-types --no-progress --ignore *.latte nette_cs: @@ -24,7 +24,7 @@ jobs: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 with: - php-version: 8.1 + php-version: 8.3 coverage: none - run: composer create-project nette/coding-standard temp/coding-standard ^3 --no-progress diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d1d9074c..b15efc86 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: Tests on: [push, pull_request] env: - php-options: -C -d opcache.enable=0 + php-options: -d opcache.enable=0 php-extensions: fileinfo, intl, gd jobs: @@ -27,7 +27,7 @@ jobs: extensions: ${{ env.php-extensions }} - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -p ${{ matrix.sapi }} -s ${{ env.php-options }} + - run: composer tester -- -p ${{ matrix.sapi }} ${{ env.php-options }} - if: failure() uses: actions/upload-artifact@v4 with: @@ -47,12 +47,13 @@ jobs: extensions: ${{ env.php-extensions }} - run: composer update --no-progress --prefer-dist --prefer-lowest --prefer-stable - - run: vendor/bin/tester tests -s ${{ env.php-options }} + - run: composer tester -- ${{ env.php-options }} code_coverage: name: Code Coverage runs-on: ubuntu-latest + continue-on-error: true steps: - uses: actions/checkout@v4 - uses: shivammathur/setup-php@v2 @@ -62,7 +63,7 @@ jobs: extensions: ${{ env.php-extensions }} - run: composer install --no-progress --prefer-dist - - run: vendor/bin/tester tests -p phpdbg -s ${{ env.php-options }} --coverage ./coverage.xml --coverage-src ./src + - run: composer tester -- -p phpdbg ${{ env.php-options }} --coverage ./coverage.xml --coverage-src ./src - run: wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.4.3/php-coveralls.phar - env: COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/composer.json b/composer.json index b23213fc..3c2ae3f4 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require-dev": { "nette/di": "^3.0", - "nette/tester": "^2.4", + "nette/tester": "^2.6", "nette/security": "^3.0", "tracy/tracy": "^2.8", "phpstan/phpstan-nette": "^2.0@stable" diff --git a/tests/Http/RequestFactory.proxy.forwarded.phpt b/tests/Http/RequestFactory.proxy.forwarded.phpt index d911c385..21d32840 100644 --- a/tests/Http/RequestFactory.proxy.forwarded.phpt +++ b/tests/Http/RequestFactory.proxy.forwarded.phpt @@ -23,7 +23,6 @@ test('forwarded header handling with proxy', function () { $factory->setProxy('127.0.0.1/8'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('http', $url->getScheme()); @@ -41,7 +40,6 @@ test('forwarded header with port numbers', function () { $factory->setProxy('127.0.0.3'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same(8080, $url->getPort()); diff --git a/tests/Http/RequestFactory.proxy.x-forwarded.phpt b/tests/Http/RequestFactory.proxy.x-forwarded.phpt index 0dac6a21..c7b2efd1 100644 --- a/tests/Http/RequestFactory.proxy.x-forwarded.phpt +++ b/tests/Http/RequestFactory.proxy.x-forwarded.phpt @@ -25,7 +25,6 @@ test('X-Forwarded headers handling with proxy', function () { $factory->setProxy('127.0.0.1/8'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('otherhost', $url->getHost()); @@ -43,7 +42,6 @@ test('X-Forwarded-Host with port', function () { $factory = new RequestFactory; $factory->setProxy('127.0.0.3'); Assert::same('23.75.45.200', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('a23-75-45-200.deploy.static.akamaitechnologies.com', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('otherhost', $url->getHost()); @@ -79,7 +77,6 @@ test('multiple proxies in X-Forwarded headers', function () { $factory = new RequestFactory; $factory->setProxy('10.0.0.0/24'); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); @@ -87,7 +84,6 @@ test('multiple proxies in X-Forwarded headers', function () { $factory->setProxy(['10.0.0.1', '10.0.0.2']); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); @@ -105,7 +101,6 @@ test('X-Forwarded-Host with multiple entries and port', function () { $factory = new RequestFactory; $factory->setProxy(['10.0.0.1', '10.0.0.2']); Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteAddress()); - Assert::same('172.16.0.1', $factory->fromGlobals()->getRemoteHost()); $url = $factory->fromGlobals()->getUrl(); Assert::same('real', $url->getHost()); diff --git a/tests/Http/SessionSection.setExpiration().phpt b/tests/Http/SessionSection.setExpiration().phpt index 2e7ffbd9..08a118ea 100644 --- a/tests/Http/SessionSection.setExpiration().phpt +++ b/tests/Http/SessionSection.setExpiration().phpt @@ -10,6 +10,7 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +ob_start(); $session = new Session(new Nette\Http\Request(new Nette\Http\UrlScript), new Nette\Http\Response); $session->setExpiration('+10 seconds'); diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 6007eabd..e4464789 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -10,6 +10,7 @@ Tester\Environment::setup(); +Tester\Environment::setupFunctions(); date_default_timezone_set('Europe/Prague'); @@ -35,9 +36,3 @@ function getTempDir(): string ini_set('session.save_path', getTempDir()); - - -function test(string $title, Closure $function): void -{ - $function(); -} From 7c2169a3cc445aab890f9d31a42df234f3a6489b Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 27 Dec 2025 01:30:08 +0100 Subject: [PATCH 03/13] cs --- src/Bridges/HttpDI/HttpExtension.php | 4 ++-- src/Http/FileUpload.php | 5 ++--- src/Http/Helpers.php | 2 +- src/Http/Request.php | 5 +++-- src/Http/RequestFactory.php | 7 ++++--- src/Http/Response.php | 4 ++-- src/Http/Session.php | 21 +++++++++------------ src/Http/SessionSection.php | 18 +++++++++--------- src/Http/Url.php | 3 ++- src/Http/UserStorage.php | 12 ++++++------ 10 files changed, 40 insertions(+), 41 deletions(-) diff --git a/src/Bridges/HttpDI/HttpExtension.php b/src/Bridges/HttpDI/HttpExtension.php index 782539db..0f3978e3 100644 --- a/src/Bridges/HttpDI/HttpExtension.php +++ b/src/Bridges/HttpDI/HttpExtension.php @@ -9,7 +9,7 @@ use Nette; use Nette\Schema\Expect; -use function is_array; +use function is_array, strval; /** @@ -91,7 +91,7 @@ public function loadConfiguration(): void private function sendHeaders(): void { $config = $this->config; - $headers = array_map('strval', $config->headers); + $headers = array_map(strval(...), $config->headers); if (isset($config->frames) && $config->frames !== true && !isset($headers['X-Frame-Options'])) { $frames = $config->frames; diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index 8d2841fb..de8ac9d4 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -10,7 +10,6 @@ use Nette; use Nette\Utils\Image; use function array_intersect_key, array_map, basename, chmod, dirname, file_get_contents, filesize, finfo_file, finfo_open, getimagesize, image_type_to_extension, in_array, is_string, is_uploaded_file, preg_replace, str_replace, trim, unlink; -use const FILEINFO_EXTENSION, FILEINFO_MIME_TYPE, UPLOAD_ERR_NO_FILE, UPLOAD_ERR_OK; /** @@ -138,7 +137,7 @@ public function getSuggestedExtension(): ?string } [, , $type] = Nette\Utils\Helpers::falseToNull(@getimagesize($this->tmpName)); // @ - files smaller than 12 bytes causes read error if ($type) { - return $this->extension = image_type_to_extension($type, false); + return $this->extension = image_type_to_extension($type, include_dot: false); } $this->extension = false; } @@ -229,7 +228,7 @@ function (string $message) use ($dest): void { */ public function isImage(): bool { - $types = array_map(fn($type) => Image::typeToMimeType($type), Image::getSupportedTypes()); + $types = array_map(Image::typeToMimeType(...), Image::getSupportedTypes()); return in_array($this->getContentType(), $types, strict: true); } diff --git a/src/Http/Helpers.php b/src/Http/Helpers.php index 0d19bfde..08dce378 100644 --- a/src/Http/Helpers.php +++ b/src/Http/Helpers.php @@ -54,7 +54,7 @@ public static function ipMatch(string $ip, string $mask): bool } - public static function initCookie(IRequest $request, IResponse $response) + public static function initCookie(IRequest $request, IResponse $response): void { $response->setCookie(self::StrictCookieName, '1', 0, '/', sameSite: IResponse::SameSiteStrict); } diff --git a/src/Http/Request.php b/src/Http/Request.php index 857806dc..49bc773f 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -9,7 +9,6 @@ use Nette; use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, preg_match, preg_match_all, rsort, strcasecmp, strtolower, strtr; -use const CASE_LOWER; /** @@ -33,6 +32,7 @@ class Request implements IRequest { use Nette\SmartObject; + /** @var string[] */ private readonly array $headers; private readonly ?\Closure $rawBodyCallback; @@ -176,6 +176,7 @@ public function getHeader(string $header): ?string /** * Returns all HTTP headers as associative array. + * @return string[] */ public function getHeaders(): array { @@ -205,7 +206,7 @@ public function getOrigin(): ?UrlImmutable return $header === 'null' ? null : new UrlImmutable($header); - } catch (Nette\InvalidArgumentException $e) { + } catch (Nette\InvalidArgumentException) { return null; } } diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index 8cb87858..f9c29551 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -11,7 +11,7 @@ use Nette\Utils\Arrays; use Nette\Utils\Strings; use function array_filter, base64_encode, count, end, explode, file_get_contents, filter_input_array, filter_var, function_exists, get_debug_type, in_array, ini_get, is_array, is_string, key, min, preg_last_error, preg_match, preg_replace, preg_split, rtrim, sprintf, str_contains, strcasecmp, strlen, strncmp, strpos, strrpos, strtolower, strtr, substr, trim; -use const FILTER_UNSAFE_RAW, FILTER_VALIDATE_IP, INPUT_COOKIE, INPUT_POST, PHP_SAPI, UPLOAD_ERR_NO_FILE; +use const PHP_SAPI; /** @@ -22,6 +22,7 @@ class RequestFactory /** @internal */ private const ValidChars = '\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}'; + /** @var array */ public array $urlFilters = [ 'path' => ['#//#' => '/'], // '%20' => '' 'url' => [], // '#[.,)]$#D' => '' @@ -130,7 +131,7 @@ private function getScriptPath(Url $url): string private function getGetPostCookie(Url $url): array { - $useFilter = (!in_array((string) ini_get('filter.default'), ['', 'unsafe_raw'], true) || ini_get('filter.default_flags')); + $useFilter = (!in_array((string) ini_get('filter.default'), ['', 'unsafe_raw'], strict: true) || ini_get('filter.default_flags')); $query = $url->getQueryParameters(); $post = $useFilter @@ -233,7 +234,7 @@ private function getHeaders(): array } else { $headers = []; foreach ($_SERVER as $k => $v) { - if (strncmp($k, 'HTTP_', 5) === 0) { + if (str_starts_with($k, 'HTTP_')) { $k = substr($k, 5); } elseif (strncmp($k, 'CONTENT_', 8)) { continue; diff --git a/src/Http/Response.php b/src/Http/Response.php index 474933a4..9d21e98a 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -10,7 +10,7 @@ use Nette; use Nette\Utils\DateTime; use function array_filter, header, header_remove, headers_list, headers_sent, htmlspecialchars, http_response_code, ini_get, is_int, ltrim, ob_get_length, ob_get_status, preg_match, rawurlencode, setcookie, str_replace, strcasecmp, strlen, strncasecmp, strpos, substr, time; -use const ENT_IGNORE, ENT_QUOTES, PHP_SAPI; +use const PHP_SAPI; /** @@ -274,7 +274,7 @@ private function checkHeaders(): void } elseif ( $this->warnOnBuffer && ob_get_length() && - !array_filter(ob_get_status(true), fn(array $i): bool => !$i['chunk_size']) + !array_filter(ob_get_status(full_status: true), fn(array $i): bool => !$i['chunk_size']) ) { trigger_error('Possible problem: you are sending a HTTP header while already having some data in output buffer. Try Tracy\OutputDebugger or send cookies/start session earlier.'); } diff --git a/src/Http/Session.php b/src/Http/Session.php index 77ec69f8..988b3b17 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -44,19 +44,16 @@ class Session 'cookie_lifetime' => 0, // for a maximum of 3 hours or until the browser is closed 'gc_maxlifetime' => self::DefaultFileLifetime, // 3 hours ]; - - private readonly IRequest $request; - private readonly IResponse $response; private ?\SessionHandlerInterface $handler = null; private bool $readAndClose = false; private bool $fileExists = true; private bool $autoStart = true; - public function __construct(IRequest $request, IResponse $response) - { - $this->request = $request; - $this->response = $response; + public function __construct( + private readonly IRequest $request, + private readonly IResponse $response, + ) { $this->options['cookie_path'] = &$this->response->cookiePath; $this->options['cookie_domain'] = &$this->response->cookieDomain; $this->options['cookie_secure'] = &$this->response->cookieSecure; @@ -73,7 +70,7 @@ public function start(): void } - private function doStart($mustExists = false): void + private function doStart(bool $mustExists = false): void { if (session_status() === PHP_SESSION_ACTIVE) { // adapt an existing session if (!$this->started) { @@ -252,7 +249,7 @@ public function regenerateId(): void throw new Nette\InvalidStateException('Cannot regenerate session ID after HTTP headers have been sent' . ($file ? " (output started at $file:$line)." : '.')); } - session_regenerate_id(true); + session_regenerate_id(delete_old_session: true); } else { session_id(session_create_id()); } @@ -316,7 +313,7 @@ public function getSection(string $section, string $class = SessionSection::clas public function hasSection(string $section): bool { if ($this->exists() && !$this->started) { - $this->autoStart(false); + $this->autoStart(forWrite: false); } return !empty($_SESSION['__NF']['DATA'][$section]); @@ -327,7 +324,7 @@ public function hasSection(string $section): bool public function getSectionNames(): array { if ($this->exists() && !$this->started) { - $this->autoStart(false); + $this->autoStart(forWrite: false); } return array_keys($_SESSION['__NF']['DATA'] ?? []); @@ -440,7 +437,7 @@ private function configure(array $config): void if ($value === null || ini_get("session.$key") == $value) { // intentionally == continue; - } elseif (strncmp($key, 'cookie_', 7) === 0) { + } elseif (str_starts_with($key, 'cookie_')) { $cookie[substr($key, 7)] = $value; } else { diff --git a/src/Http/SessionSection.php b/src/Http/SessionSection.php index 79bd647f..ed2cbeeb 100644 --- a/src/Http/SessionSection.php +++ b/src/Http/SessionSection.php @@ -8,7 +8,7 @@ namespace Nette\Http; use Nette; -use function array_key_exists, func_num_args, ini_get, is_array, time; +use function array_key_exists, func_num_args, ini_get, is_array, is_string, time; /** @@ -34,7 +34,7 @@ public function __construct( */ public function getIterator(): \Iterator { - $this->session->autoStart(false); + $this->session->autoStart(forWrite: false); return new \ArrayIterator($this->getData() ?? []); } @@ -47,7 +47,7 @@ public function set(string $name, mixed $value, ?string $expire = null): void if ($value === null) { $this->remove($name); } else { - $this->session->autoStart(true); + $this->session->autoStart(forWrite: true); $this->getData()[$name] = $value; $this->setExpiration($expire, $name); } @@ -63,7 +63,7 @@ public function get(string $name): mixed throw new \ArgumentCountError(__METHOD__ . '() expects 1 arguments, given more.'); } - $this->session->autoStart(false); + $this->session->autoStart(forWrite: false); return $this->getData()[$name] ?? null; } @@ -74,7 +74,7 @@ public function get(string $name): mixed */ public function remove(string|array|null $name = null): void { - $this->session->autoStart(false); + $this->session->autoStart(forWrite: false); if (func_num_args() > 1) { throw new \ArgumentCountError(__METHOD__ . '() expects at most 1 arguments, given more.'); @@ -94,9 +94,9 @@ public function remove(string|array|null $name = null): void * Sets a variable in this session section. * @deprecated use set() instead */ - public function __set(string $name, $value): void + public function __set(string $name, mixed $value): void { - $this->session->autoStart(true); + $this->session->autoStart(forWrite: true); $this->getData()[$name] = $value; } @@ -107,7 +107,7 @@ public function __set(string $name, $value): void */ public function &__get(string $name): mixed { - $this->session->autoStart(true); + $this->session->autoStart(forWrite: true); $data = &$this->getData(); if ($this->warnOnUndefined && !array_key_exists($name, $data ?? [])) { trigger_error("The variable '$name' does not exist in session section"); @@ -123,7 +123,7 @@ public function &__get(string $name): mixed */ public function __isset(string $name): bool { - $this->session->autoStart(false); + $this->session->autoStart(forWrite: false); return isset($this->getData()[$name]); } diff --git a/src/Http/Url.php b/src/Http/Url.php index 887371f4..6374884c 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -9,7 +9,7 @@ use Nette; use function array_pop, array_slice, bin2hex, chunk_split, defined, explode, function_exists, http_build_query, idn_to_utf8, implode, ini_get, ip2long, is_array, is_string, ksort, parse_str, parse_url, preg_match, preg_quote, preg_replace, preg_replace_callback, rawurldecode, rawurlencode, rtrim, str_contains, str_replace, str_starts_with, strcasecmp, strlen, strrpos, strtolower, strtoupper, substr; -use const IDNA_DEFAULT, INTL_IDNA_VARIANT_UTS46, PHP_QUERY_RFC3986; +use const PHP_QUERY_RFC3986; /** @@ -45,6 +45,7 @@ class Url implements \JsonSerializable { use Nette\SmartObject; + /** @var array */ public static array $defaultPorts = [ 'http' => 80, 'https' => 443, diff --git a/src/Http/UserStorage.php b/src/Http/UserStorage.php index 670d4d34..2b4ae899 100644 --- a/src/Http/UserStorage.php +++ b/src/Http/UserStorage.php @@ -35,7 +35,7 @@ public function __construct( */ public function setAuthenticated(bool $state): self { - $section = $this->getSessionSection(true); + $section = $this->getSessionSection(need: true); $section->authenticated = $state; // Session Fixation defence @@ -59,7 +59,7 @@ public function setAuthenticated(bool $state): self */ public function isAuthenticated(): bool { - $session = $this->getSessionSection(false); + $session = $this->getSessionSection(need: false); return $session && $session->authenticated; } @@ -69,7 +69,7 @@ public function isAuthenticated(): bool */ public function setIdentity(?IIdentity $identity): self { - $this->getSessionSection(true)->identity = $identity; + $this->getSessionSection(need: true)->identity = $identity; return $this; } @@ -79,7 +79,7 @@ public function setIdentity(?IIdentity $identity): self */ public function getIdentity(): ?Nette\Security\IIdentity { - $session = $this->getSessionSection(false); + $session = $this->getSessionSection(need: false); return $session ? $session->identity : null; } @@ -112,7 +112,7 @@ public function getNamespace(): string */ public function setExpiration(?string $time, int $flags = 0): self { - $section = $this->getSessionSection(true); + $section = $this->getSessionSection(need: true); if ($time) { $time = Nette\Utils\DateTime::from($time)->format('U'); $section->expireTime = $time; @@ -133,7 +133,7 @@ public function setExpiration(?string $time, int $flags = 0): self */ public function getLogoutReason(): ?int { - $session = $this->getSessionSection(false); + $session = $this->getSessionSection(need: false); return $session ? $session->reason : null; } From 266733047879dd8ed958532ed3bca6163d43f1a7 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Mar 2026 04:23:07 +0100 Subject: [PATCH 04/13] improved PHPDoc descriptions --- src/Http/Context.php | 3 ++- src/Http/FileUpload.php | 8 ++++---- src/Http/Helpers.php | 7 +++++-- src/Http/IRequest.php | 34 ++++++++++++++++------------------ src/Http/IResponse.php | 15 ++++++++------- src/Http/Request.php | 30 +++++++++++++++--------------- src/Http/RequestFactory.php | 12 ++++++++++-- src/Http/Response.php | 17 ++++++++--------- src/Http/Session.php | 15 +++++++++------ src/Http/SessionSection.php | 15 +++++++++------ src/Http/Url.php | 28 ++++++++++++++++++++++------ src/Http/UrlImmutable.php | 20 +++++++++++++++----- src/Http/UrlScript.php | 2 +- 13 files changed, 124 insertions(+), 82 deletions(-) diff --git a/src/Http/Context.php b/src/Http/Context.php index 553101f5..fed899c5 100644 --- a/src/Http/Context.php +++ b/src/Http/Context.php @@ -21,7 +21,8 @@ public function __construct( /** - * Attempts to cache the sent entity by its last modification date. + * Checks whether the response has been modified since the client's cached version. + * Sets Last-Modified and ETag headers if provided. Returns false and sends 304 Not Modified if unchanged. */ public function isModified(string|int|\DateTimeInterface|null $lastModified = null, ?string $etag = null): bool { diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index de8ac9d4..362897ce 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -174,7 +174,7 @@ public function __toString(): string /** - * Returns the error code. It has to be one of UPLOAD_ERR_XXX constants. + * Returns the upload error code (one of the UPLOAD_ERR_XXX constants). * @see http://php.net/manual/en/features.file-upload.errors.php */ public function getError(): int @@ -223,8 +223,8 @@ function (string $message) use ($dest): void { /** - * Returns true if the uploaded file is an image and the format is supported by PHP, so it can be loaded using the toImage() method. - * Detection is based on its signature, the integrity of the file is not checked. Requires PHP extensions fileinfo & gd. + * Checks whether the uploaded file is an image in a format supported by PHP (detectable via fileinfo, loadable via GD). + * Detection is based on file signature; full integrity is not verified. */ public function isImage(): bool { @@ -244,7 +244,7 @@ public function toImage(): Image /** - * Returns a pair of [width, height] with dimensions of the uploaded image. + * Returns the [width, height] dimensions of the uploaded image, or null if it is not a valid image. */ public function getImageSize(): ?array { diff --git a/src/Http/Helpers.php b/src/Http/Helpers.php index 08dce378..c0465243 100644 --- a/src/Http/Helpers.php +++ b/src/Http/Helpers.php @@ -27,7 +27,7 @@ final class Helpers /** - * Returns HTTP valid date format. + * Formats a date and time in the HTTP date format (RFC 7231), e.g. 'Mon, 23 Jan 1978 10:00:00 GMT'. */ public static function formatDate(string|int|\DateTimeInterface $time): string { @@ -37,7 +37,7 @@ public static function formatDate(string|int|\DateTimeInterface $time): string /** - * Is IP address in CIDR block? + * Checks whether an IP address falls within a CIDR block (e.g. '192.168.1.0/24'). */ public static function ipMatch(string $ip, string $mask): bool { @@ -54,6 +54,9 @@ public static function ipMatch(string $ip, string $mask): bool } + /** + * Sends the strict same-site cookie used to detect same-site requests. + */ public static function initCookie(IRequest $request, IResponse $response): void { $response->setCookie(self::StrictCookieName, '1', 0, '/', sameSite: IResponse::SameSiteStrict); diff --git a/src/Http/IRequest.php b/src/Http/IRequest.php index f3ef8375..d9672732 100644 --- a/src/Http/IRequest.php +++ b/src/Http/IRequest.php @@ -9,9 +9,9 @@ /** - * HTTP request provides access scheme for request sent via HTTP. - * @method UrlImmutable|null getReferer() Returns referrer. - * @method bool isSameSite() Is the request sent from the same origin? + * HTTP request contract providing access to URL, headers, cookies, uploaded files, and body. + * @method ?UrlImmutable getReferer() Returns the referrer URL. + * @method bool isSameSite() Checks whether the request is coming from the same site. */ interface IRequest { @@ -47,63 +47,61 @@ interface IRequest public const OPTIONS = self::Options; /** - * Returns URL object. + * Returns the request URL. */ function getUrl(): UrlScript; /********************* query, post, files & cookies ****************d*g**/ /** - * Returns variable provided to the script via URL query ($_GET). - * If no key is passed, returns the entire array. + * Returns a URL query parameter, or all parameters as an array if no key is given. * @return mixed */ function getQuery(?string $key = null); /** - * Returns variable provided to the script via POST method ($_POST). - * If no key is passed, returns the entire array. + * Returns a POST parameter, or all POST parameters as an array if no key is given. * @return mixed */ function getPost(?string $key = null); /** - * Returns uploaded file. + * Returns the uploaded file for the given key, or null if not present. + * Accepts a string key or an array of keys for nested file structures (e.g. ['form', 'avatar']). * @return FileUpload|array|null */ function getFile(string $key); /** - * Returns uploaded files. + * Returns the tree of uploaded files, with each leaf being a FileUpload instance. */ function getFiles(): array; /** - * Returns variable provided to the script via HTTP cookies. + * Returns a cookie value, or null if it does not exist. * @return mixed */ function getCookie(string $key); /** - * Returns variables provided to the script via HTTP cookies. + * Returns all cookies. */ function getCookies(): array; /********************* method & headers ****************d*g**/ /** - * Returns HTTP request method (GET, POST, HEAD, PUT, ...). The method is case-sensitive. + * Returns the HTTP request method (GET, POST, HEAD, PUT, ...). */ function getMethod(): string; /** - * Checks HTTP request method. + * Checks the HTTP request method. The comparison is case-insensitive. */ function isMethod(string $method): bool; /** - * Return the value of the HTTP header. Pass the header name as the - * plain, HTTP-specified header name (e.g. 'Accept-Encoding'). + * Returns the value of an HTTP header, or null if it does not exist. The name is case-insensitive. */ function getHeader(string $header): ?string; @@ -113,12 +111,12 @@ function getHeader(string $header): ?string; function getHeaders(): array; /** - * Is the request sent via secure channel (https)? + * Checks whether the request was sent via a secure channel (HTTPS). */ function isSecured(): bool; /** - * Is AJAX request? + * Checks whether the request was made via AJAX (X-Requested-With: XMLHttpRequest). */ function isAjax(): bool; diff --git a/src/Http/IResponse.php b/src/Http/IResponse.php index 1286ef32..f3deae82 100644 --- a/src/Http/IResponse.php +++ b/src/Http/IResponse.php @@ -9,7 +9,7 @@ /** - * HTTP response interface. + * HTTP response contract for setting status code, headers, cookies, and redirects. * @method self deleteHeader(string $name) */ interface IResponse @@ -345,13 +345,13 @@ function setCode(int $code, ?string $reason = null); function getCode(): int; /** - * Sends a HTTP header and replaces a previous one. + * Sends an HTTP header, replacing any previously sent header with the same name. * @return static */ function setHeader(string $name, string $value); /** - * Adds HTTP header. + * Adds an HTTP header without replacing a previously sent header with the same name. * @return static */ function addHeader(string $name, string $value); @@ -368,23 +368,24 @@ function setContentType(string $type, ?string $charset = null); function redirect(string $url, int $code = self::S302_Found): void; /** - * Sets the time (like '20 minutes') before a page cached on a browser expires, null means "must-revalidate". + * Sets the Cache-Control and Expires headers. Pass a time string (e.g. '20 minutes') to enable caching, + * or null to disable it. * @return static */ function setExpiration(?string $expire); /** - * Checks if headers have been sent. + * Checks whether HTTP headers have already been sent. */ function isSent(): bool; /** - * Returns value of an HTTP header. + * Returns the value of a sent HTTP header, or null if it does not exist. */ function getHeader(string $header): ?string; /** - * Returns an associative array of headers to sent. + * Returns all sent HTTP headers as an associative array. */ function getHeaders(): array; diff --git a/src/Http/Request.php b/src/Http/Request.php index 49bc773f..a0abd435 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -12,7 +12,7 @@ /** - * HttpRequest provides access scheme for request sent via HTTP. + * Immutable representation of an HTTP request with access to URL, headers, cookies, uploaded files, and body. * * @property-read UrlScript $url * @property-read array $query @@ -78,8 +78,7 @@ public function getUrl(): UrlScript /** - * Returns variable provided to the script via URL query ($_GET). - * If no key is passed, returns the entire array. + * Returns a URL query parameter, or all parameters as an array if no key is given. */ public function getQuery(?string $key = null): mixed { @@ -92,8 +91,7 @@ public function getQuery(?string $key = null): mixed /** - * Returns variable provided to the script via POST method ($_POST). - * If no key is passed, returns the entire array. + * Returns a POST parameter, or all POST parameters as an array if no key is given. */ public function getPost(?string $key = null): mixed { @@ -106,7 +104,8 @@ public function getPost(?string $key = null): mixed /** - * Returns uploaded file. + * Returns the uploaded file for the given key, or null if not present. + * Accepts a string key or an array of keys for nested file structures (e.g. ['form', 'avatar']). * @param string|string[] $key */ public function getFile($key): ?FileUpload @@ -117,7 +116,7 @@ public function getFile($key): ?FileUpload /** - * Returns tree of upload files in a normalized structure, with each leaf an instance of Nette\Http\FileUpload. + * Returns the tree of uploaded files, with each leaf being a FileUpload instance. */ public function getFiles(): array { @@ -185,8 +184,8 @@ public function getHeaders(): array /** - * What URL did the user come from? Beware, it is not reliable at all. - * @deprecated deprecated in favor of the getOrigin() + * Returns the referrer URL from the Referer header. Unreliable - clients may omit or spoof it. + * @deprecated use getOrigin() */ public function getReferer(): ?UrlImmutable { @@ -197,7 +196,7 @@ public function getReferer(): ?UrlImmutable /** - * What origin did the user come from? It contains scheme, hostname and port. + * Returns the request origin (scheme + host + port) from the Origin header, or null if absent or invalid. */ public function getOrigin(): ?UrlImmutable { @@ -213,7 +212,7 @@ public function getOrigin(): ?UrlImmutable /** - * Is the request sent via secure channel (https)? + * Checks whether the request was sent via a secure channel (HTTPS). */ public function isSecured(): bool { @@ -222,7 +221,7 @@ public function isSecured(): bool /** - * Is the request coming from the same site and is initiated by clicking on a link? + * Checks whether the request is coming from the same site and was initiated by clicking on a link. */ public function isSameSite(): bool { @@ -231,7 +230,7 @@ public function isSameSite(): bool /** - * Is it an AJAX request? + * Checks whether the request was made via AJAX (X-Requested-With: XMLHttpRequest). */ public function isAjax(): bool { @@ -290,8 +289,9 @@ public function getBasicCredentials(): ?array /** - * Returns the most preferred language by browser. Uses the `Accept-Language` header. If no match is reached, it returns `null`. - * @param string[] $langs supported languages + * Returns the most preferred language from the Accept-Language header that matches one of the supported languages, + * or null if no match is found. + * @param array $langs supported language codes (e.g. ['en', 'cs', 'de']) */ public function detectLanguage(array $langs): ?string { diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index f9c29551..ccceebb3 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -22,7 +22,11 @@ class RequestFactory /** @internal */ private const ValidChars = '\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}'; - /** @var array */ + /** + * Regex-based filters applied to the URL before parsing. 'path' filters run on the path component only; + * 'url' filters run on the full request URI. + * @var array> + */ public array $urlFilters = [ 'path' => ['#//#' => '/'], // '%20' => '' 'url' => [], // '#[.,)]$#D' => '' @@ -34,6 +38,9 @@ class RequestFactory private array $proxies = []; + /** + * Disables sanitization of request data (GET, POST, cookies, file names) for binary-safe handling. + */ public function setBinary(bool $binary = true): static { $this->binary = $binary; @@ -42,7 +49,8 @@ public function setBinary(bool $binary = true): static /** - * @param string|string[] $proxy + * Sets the trusted proxy IP addresses or CIDR blocks used to resolve the real client IP and URL scheme. + * @param string|list $proxy */ public function setProxy($proxy): static { diff --git a/src/Http/Response.php b/src/Http/Response.php index 9d21e98a..0496565e 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -14,7 +14,7 @@ /** - * HttpResponse class. + * Mutable HTTP response for setting status code, headers, cookies, and redirects. * * @property-read array $headers */ @@ -31,7 +31,7 @@ final class Response implements IResponse /** Whether the cookie is available only through HTTPS */ public bool $cookieSecure = false; - /** Whether warn on possible problem with data in output buffer */ + /** Whether to warn when there is data in the output buffer before sending headers */ public bool $warnOnBuffer = true; /** HTTP response code */ @@ -76,7 +76,7 @@ public function getCode(): int /** - * Sends an HTTP header and overwrites previously sent header of the same name. + * Sends an HTTP header, replacing any previously sent header with the same name. * @throws Nette\InvalidStateException if HTTP headers have been sent */ public function setHeader(string $name, ?string $value): static @@ -95,7 +95,7 @@ public function setHeader(string $name, ?string $value): static /** - * Sends an HTTP header and doesn't overwrite previously sent header of the same name. + * Adds an HTTP header without replacing a previously sent header with the same name. * @throws Nette\InvalidStateException if HTTP headers have been sent */ public function addHeader(string $name, string $value): static @@ -130,7 +130,7 @@ public function setContentType(string $type, ?string $charset = null): static /** - * Response should be downloaded with 'Save as' dialog. + * Triggers a browser download dialog for the response body with the given filename. * @throws Nette\InvalidStateException if HTTP headers have been sent */ public function sendAsFile(string $fileName): static @@ -160,8 +160,8 @@ public function redirect(string $url, int $code = self::S302_Found): void /** - * Sets the expiration of the HTTP document using the `Cache-Control` and `Expires` headers. - * The parameter is either a time interval (as text) or `null`, which disables caching. + * Sets the Cache-Control and Expires headers. Pass a time string (e.g. '20 minutes') to enable caching, + * or null to disable it entirely. * @throws Nette\InvalidStateException if HTTP headers have been sent */ public function setExpiration(?string $expire): static @@ -181,8 +181,7 @@ public function setExpiration(?string $expire): static /** - * Returns whether headers have already been sent from the server to the browser, - * so it is no longer possible to send headers or change the response code. + * Checks whether HTTP headers have already been sent, making it impossible to modify them. */ public function isSent(): bool { diff --git a/src/Http/Session.php b/src/Http/Session.php index 988b3b17..b8e76827 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -183,7 +183,7 @@ public function __destruct() /** - * Has been session started? + * Checks whether the session has been started. */ public function isStarted(): bool { @@ -259,7 +259,7 @@ public function regenerateId(): void /** - * Returns the current session ID. Don't make dependencies, can be changed for each request. + * Returns the current session ID. Avoid relying on the value - it may change between requests. */ public function getId(): string { @@ -268,7 +268,7 @@ public function getId(): string /** - * Sets the session name to a specified one. + * Sets the session name. */ public function setName(string $name): static { @@ -320,7 +320,10 @@ public function hasSection(string $section): bool } - /** @return string[] */ + /** + * Returns the names of all existing session sections. + * @return list + */ public function getSectionNames(): array { if ($this->exists() && !$this->started) { @@ -472,8 +475,8 @@ private function configure(array $config): void /** - * Sets the amount of time (like '20 minutes') allowed between requests before the session will be terminated, - * null means "for a maximum of 3 hours or until the browser is closed". + * Sets the session lifetime as a time string (e.g. '20 minutes'), or null to revert to the default + * (up to 3 hours or until the browser is closed). */ public function setExpiration(?string $expire): static { diff --git a/src/Http/SessionSection.php b/src/Http/SessionSection.php index ed2cbeeb..a3a9f4ae 100644 --- a/src/Http/SessionSection.php +++ b/src/Http/SessionSection.php @@ -16,6 +16,7 @@ */ class SessionSection implements \IteratorAggregate, \ArrayAccess { + /** Emits a warning when accessing an undefined variable in this section */ public bool $warnOnUndefined = false; @@ -40,7 +41,8 @@ public function getIterator(): \Iterator /** - * Sets a variable in this session section. + * Sets a variable in this session section. Passing null removes it. + * The optional $expire sets per-variable expiration as a time string (e.g. '30 seconds'). */ public function set(string $name, mixed $value, ?string $expire = null): void { @@ -69,7 +71,7 @@ public function get(string $name): mixed /** - * Removes a variable or whole section. + * Removes a variable or a list of variables from this section. With no argument, removes the entire section. * @param string|string[]|null $name */ public function remove(string|array|null $name = null): void @@ -179,8 +181,9 @@ public function offsetUnset($name): void /** - * Sets the expiration of the section or specific variables. - * @param string|string[]|null $variables list of variables / single variable to expire + * Sets the expiration time for the whole section or for specific variables. + * Pass null to clear the expiration. + * @param string|string[]|null $variables variable name(s) to apply the expiration to; null applies to the whole section */ public function setExpiration(?string $expire, string|array|null $variables = null): static { @@ -206,8 +209,8 @@ public function setExpiration(?string $expire, string|array|null $variables = nu /** - * Removes the expiration from the section or specific variables. - * @param string|string[]|null $variables list of variables / single variable to expire + * Removes the expiration from the whole section or from specific variables. + * @param string|string[]|null $variables variable name(s) to clear expiration for; null applies to the whole section */ public function removeExpiration(string|array|null $variables = null): void { diff --git a/src/Http/Url.php b/src/Http/Url.php index 6374884c..59bfcc9b 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -146,7 +146,8 @@ public function getHost(): string /** - * Returns the part of domain. + * Returns the specified number of rightmost domain labels (e.g. level 2 of 'www.nette.org' -> 'nette.org'). + * Negative values trim from the right instead. */ public function getDomain(int $level = 2): string { @@ -167,18 +168,27 @@ public function setPort(int $port): static } + /** + * Returns the port number, falling back to the default port for the scheme if not explicitly set. + */ public function getPort(): ?int { return $this->port ?: $this->getDefaultPort(); } + /** + * Returns the default port for the current scheme, or null if the scheme is not recognized. + */ public function getDefaultPort(): ?int { return self::$defaultPorts[$this->scheme] ?? null; } + /** + * Sets the path. Automatically prepends a leading slash when a host is set. + */ public function setPath(string $path): static { $this->path = $path; @@ -203,6 +213,11 @@ public function setQuery(string|array $query): static } + /** + * Merges query parameters into the existing query. Array values use union (existing keys are preserved); + * string values are appended and reparsed. + * @param string|mixed[] $query + */ public function appendQuery(string|array $query): static { $this->query = is_array($query) @@ -308,7 +323,7 @@ public function getRelativeUrl(): string /** - * URL comparison. + * Checks whether two URLs are equal, ignoring query parameter order and trailing dots in hostnames. */ public function isEqual(string|self|UrlImmutable $url): bool { @@ -332,7 +347,8 @@ public function isEqual(string|self|UrlImmutable $url): bool /** - * Transforms URL to canonical form. + * Normalizes the URL to canonical form: percent-encodes path, lowercases and trims the host, + * and converts IDN ASCII to Unicode. */ public function canonicalize(): static { @@ -384,7 +400,7 @@ private static function idnHostToUnicode(string $host): string /** - * Similar to rawurldecode, but preserves reserved chars encoded. + * Decodes percent-encoded characters, but keeps reserved characters (specified in $reserved) encoded. */ public static function unescape(string $s, string $reserved = '%;/?:@&=+$,'): string { @@ -417,7 +433,7 @@ public static function parseQuery(string $s): array /** - * Determines if URL is absolute, ie if it starts with a scheme followed by colon. + * Checks whether the URL is absolute, i.e. starts with a scheme followed by a colon. */ public static function isAbsolute(string $url): bool { @@ -426,7 +442,7 @@ public static function isAbsolute(string $url): bool /** - * Normalizes a path by handling and removing relative path references like '.', '..' and directory traversal. + * Resolves dot segments (. and ..) in a URL path, as per RFC 3986. */ public static function removeDotSegments(string $path): string { diff --git a/src/Http/UrlImmutable.php b/src/Http/UrlImmutable.php index fb506ae4..40c2dd69 100644 --- a/src/Http/UrlImmutable.php +++ b/src/Http/UrlImmutable.php @@ -137,6 +137,10 @@ public function getHost(): string } + /** + * Returns the specified number of rightmost domain labels (e.g. level 2 of 'www.nette.org' -> 'nette.org'). + * Negative values trim from the right instead. + */ public function getDomain(int $level = 2): string { $parts = ip2long($this->host) @@ -158,12 +162,18 @@ public function withPort(int $port): static } + /** + * Returns the port number, falling back to the default port for the scheme if not explicitly set. + */ public function getPort(): ?int { return $this->port ?: $this->getDefaultPort(); } + /** + * Returns the default port for the current scheme, or null if the scheme is not recognized. + */ public function getDefaultPort(): ?int { return Url::$defaultPorts[$this->scheme] ?? null; @@ -237,9 +247,6 @@ public function getFragment(): string } - /** - * Returns the entire URI including query string and fragment. - */ public function getAbsoluteUrl(): string { return $this->getHostUrl() . $this->path @@ -281,6 +288,9 @@ public function __toString(): string } + /** + * Checks whether two URLs are equal, ignoring query parameter order and trailing dots in hostnames. + */ public function isEqual(string|Url|self $url): bool { return (new Url($this))->isEqual($url); @@ -288,8 +298,8 @@ public function isEqual(string|Url|self $url): bool /** - * Resolves relative URLs in the same way as browser. If path is relative, it is resolved against - * base URL, if begins with /, it is resolved against the host root. + * Resolves a URI reference against this URL the same way a browser would. + * Relative paths are resolved against the current path; paths starting with / are resolved against the host root. */ public function resolve(string $reference): self { diff --git a/src/Http/UrlScript.php b/src/Http/UrlScript.php index 074c2d92..fefcf13e 100644 --- a/src/Http/UrlScript.php +++ b/src/Http/UrlScript.php @@ -97,7 +97,7 @@ public function getRelativeUrl(): string /** - * Returns the additional path information. + * Returns the path segment after the script name (PATH_INFO), or an empty string if not present. */ public function getPathInfo(): string { From 1ef71c5cbfc781154fed913b1be0dbca70be35f1 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Mon, 9 Mar 2026 04:24:01 +0100 Subject: [PATCH 05/13] improved phpDoc types --- src/Bridges/HttpDI/HttpExtension.php | 1 + src/Http/FileUpload.php | 6 ++++-- src/Http/IRequest.php | 5 ++++- src/Http/IResponse.php | 2 ++ src/Http/Request.php | 32 ++++++++++++++++++---------- src/Http/RequestFactory.php | 6 +++++- src/Http/Response.php | 4 +++- src/Http/Session.php | 11 ++++++---- src/Http/SessionSection.php | 9 ++++++-- src/Http/Url.php | 7 +++++- src/Http/UrlImmutable.php | 7 +++++- 11 files changed, 66 insertions(+), 24 deletions(-) diff --git a/src/Bridges/HttpDI/HttpExtension.php b/src/Bridges/HttpDI/HttpExtension.php index 0f3978e3..1de4e6a5 100644 --- a/src/Bridges/HttpDI/HttpExtension.php +++ b/src/Bridges/HttpDI/HttpExtension.php @@ -141,6 +141,7 @@ private function sendHeaders(): void } + /** @param array|scalar|null> $config */ private static function buildPolicy(array $config): string { $nonQuoted = ['require-sri-for' => 1, 'sandbox' => 1]; diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index 362897ce..a916f2be 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -18,12 +18,12 @@ * @property-read string $name * @property-read string $sanitizedName * @property-read string $untrustedFullPath - * @property-read string|null $contentType + * @property-read ?string $contentType * @property-read int $size * @property-read string $temporaryFile * @property-read int $error * @property-read bool $ok - * @property-read string|null $contents + * @property-read ?string $contents */ final class FileUpload { @@ -41,6 +41,7 @@ final class FileUpload private readonly int $error; + /** @param array{name?: string, full_path?: string, size?: int, tmp_name?: string, error?: int, type?: string}|string|null $value */ public function __construct(array|string|null $value) { if (is_string($value)) { @@ -245,6 +246,7 @@ public function toImage(): Image /** * Returns the [width, height] dimensions of the uploaded image, or null if it is not a valid image. + * @return ?array{int, int} */ public function getImageSize(): ?array { diff --git a/src/Http/IRequest.php b/src/Http/IRequest.php index d9672732..f3799516 100644 --- a/src/Http/IRequest.php +++ b/src/Http/IRequest.php @@ -68,12 +68,13 @@ function getPost(?string $key = null); /** * Returns the uploaded file for the given key, or null if not present. * Accepts a string key or an array of keys for nested file structures (e.g. ['form', 'avatar']). - * @return FileUpload|array|null + * @return ?FileUpload */ function getFile(string $key); /** * Returns the tree of uploaded files, with each leaf being a FileUpload instance. + * @return mixed[] */ function getFiles(): array; @@ -85,6 +86,7 @@ function getCookie(string $key); /** * Returns all cookies. + * @return array */ function getCookies(): array; @@ -107,6 +109,7 @@ function getHeader(string $header): ?string; /** * Returns all HTTP headers. + * @return array */ function getHeaders(): array; diff --git a/src/Http/IResponse.php b/src/Http/IResponse.php index f3deae82..800e1c8e 100644 --- a/src/Http/IResponse.php +++ b/src/Http/IResponse.php @@ -386,6 +386,7 @@ function getHeader(string $header): ?string; /** * Returns all sent HTTP headers as an associative array. + * @return array */ function getHeaders(): array; @@ -405,6 +406,7 @@ function setCookie( /** * Deletes a cookie. + * @return void */ function deleteCookie(string $name, ?string $path = null, ?string $domain = null, ?bool $secure = null); } diff --git a/src/Http/Request.php b/src/Http/Request.php index a0abd435..fe0d6a42 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -15,33 +15,41 @@ * Immutable representation of an HTTP request with access to URL, headers, cookies, uploaded files, and body. * * @property-read UrlScript $url - * @property-read array $query - * @property-read array $post - * @property-read array $files - * @property-read array $cookies + * @property-read array $query + * @property-read array $post + * @property-read array $files + * @property-read array $cookies * @property-read string $method - * @property-read array $headers - * @property-read UrlImmutable|null $referer + * @property-read array $headers + * @property-read ?UrlImmutable $referer * @property-read bool $secured * @property-read bool $ajax - * @property-read string|null $remoteAddress - * @property-read string|null $remoteHost - * @property-read string|null $rawBody + * @property-read ?string $remoteAddress + * @property-read ?string $remoteHost + * @property-read ?string $rawBody */ class Request implements IRequest { use Nette\SmartObject; - /** @var string[] */ + /** @var array */ private readonly array $headers; + /** @var (\Closure(): string)|null */ private readonly ?\Closure $rawBodyCallback; + /** + * @param array $headers + * @param ?(callable(): string) $rawBodyCallback + */ public function __construct( private UrlScript $url, + /** @var mixed[] */ private readonly array $post = [], + /** @var mixed[] */ private readonly array $files = [], + /** @var array */ private readonly array $cookies = [], array $headers = [], private readonly string $method = 'GET', @@ -117,6 +125,7 @@ public function getFile($key): ?FileUpload /** * Returns the tree of uploaded files, with each leaf being a FileUpload instance. + * @return mixed[] */ public function getFiles(): array { @@ -135,6 +144,7 @@ public function getCookie(string $key): mixed /** * Returns all cookies. + * @return array */ public function getCookies(): array { @@ -175,7 +185,7 @@ public function getHeader(string $header): ?string /** * Returns all HTTP headers as associative array. - * @return string[] + * @return array */ public function getHeaders(): array { diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index ccceebb3..13cc7482 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -34,7 +34,7 @@ class RequestFactory private bool $binary = false; - /** @var string[] */ + /** @var list */ private array $proxies = []; @@ -137,6 +137,7 @@ private function getScriptPath(Url $url): string } + /** @return array{mixed[], mixed[]} */ private function getGetPostCookie(Url $url): array { $useFilter = (!in_array((string) ini_get('filter.default'), ['', 'unsafe_raw'], strict: true) || ini_get('filter.default_flags')); @@ -179,6 +180,7 @@ private function getGetPostCookie(Url $url): array } + /** @return mixed[] */ private function getFiles(): array { $reChars = '#^[' . self::ValidChars . ']*+$#Du'; @@ -235,6 +237,7 @@ private function getFiles(): array } + /** @return array */ private function getHeaders(): array { if (function_exists('apache_request_headers')) { @@ -278,6 +281,7 @@ private function getMethod(): string } + /** @return array{?string, ?string} [remoteAddr, remoteHost] */ private function getClient(Url $url): array { $remoteAddr = !empty($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; diff --git a/src/Http/Response.php b/src/Http/Response.php index 0496565e..8332be60 100644 --- a/src/Http/Response.php +++ b/src/Http/Response.php @@ -16,7 +16,7 @@ /** * Mutable HTTP response for setting status code, headers, cookies, and redirects. * - * @property-read array $headers + * @property-read array $headers */ final class Response implements IResponse { @@ -208,6 +208,7 @@ public function getHeader(string $header): ?string /** * Returns all sent HTTP headers as associative array. + * @return array */ public function getHeaders(): array { @@ -223,6 +224,7 @@ public function getHeaders(): array /** * Sends a cookie. + * @param self::SameSite*|null $sameSite * @throws Nette\InvalidStateException if HTTP headers have been sent */ public function setCookie( diff --git a/src/Http/Session.php b/src/Http/Session.php index b8e76827..5ab1b197 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -29,16 +29,16 @@ class Session 'cookie_httponly' => true, // must be enabled to prevent Session Hijacking ]; - /** @var array Occurs when the session is started */ + /** @var list Occurs when the session is started */ public array $onStart = []; - /** @var array Occurs before the session is written to disk */ + /** @var list Occurs before the session is written to disk */ public array $onBeforeWrite = []; private bool $regenerated = false; private bool $started = false; - /** default configuration */ + /** @var array default configuration */ private array $options = [ 'cookie_samesite' => IResponse::SameSiteLax, 'cookie_lifetime' => 0, // for a maximum of 3 hours or until the browser is closed @@ -298,7 +298,7 @@ public function getName(): string /** * Returns specified session section. * @template T of SessionSection - * @param class-string $class + * @param class-string $class * @return T */ public function getSection(string $section, string $class = SessionSection::class): SessionSection @@ -371,6 +371,7 @@ private function clean(): void /** * Sets session options. + * @param array $options * @throws Nette\NotSupportedException * @throws Nette\InvalidStateException */ @@ -421,6 +422,7 @@ public function setOptions(array $options): static /** * Returns all session options. + * @return array */ public function getOptions(): array { @@ -430,6 +432,7 @@ public function getOptions(): array /** * Configures session environment. + * @param array $config */ private function configure(array $config): void { diff --git a/src/Http/SessionSection.php b/src/Http/SessionSection.php index a3a9f4ae..9557e6bf 100644 --- a/src/Http/SessionSection.php +++ b/src/Http/SessionSection.php @@ -13,6 +13,8 @@ /** * Session section. + * @implements \IteratorAggregate + * @implements \ArrayAccess */ class SessionSection implements \IteratorAggregate, \ArrayAccess { @@ -32,6 +34,7 @@ public function __construct( /** * Returns an iterator over all section variables. + * @return \Iterator */ public function getIterator(): \Iterator { @@ -218,13 +221,15 @@ public function removeExpiration(string|array|null $variables = null): void } - private function &getData() + /** @return ?array */ + private function &getData(): ?array { return $_SESSION['__NF']['DATA'][$this->name]; } - private function &getMeta() + /** @return ?array */ + private function &getMeta(): ?array { return $_SESSION['__NF']['META'][$this->name]; } diff --git a/src/Http/Url.php b/src/Http/Url.php index 59bfcc9b..ab04c110 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -39,7 +39,7 @@ * @property-read string $basePath * @property-read string $baseUrl * @property-read string $relativeUrl - * @property-read array $queryParameters + * @property-read array $queryParameters */ class Url implements \JsonSerializable { @@ -58,6 +58,8 @@ class Url implements \JsonSerializable private string $host = ''; private ?int $port = null; private string $path = ''; + + /** @var mixed[] */ private array $query = []; private string $fragment = ''; @@ -206,6 +208,7 @@ public function getPath(): string } + /** @param string|mixed[] $query */ public function setQuery(string|array $query): static { $this->query = is_array($query) ? $query : self::parseQuery($query); @@ -233,6 +236,7 @@ public function getQuery(): string } + /** @return mixed[] */ public function getQueryParameters(): array { return $this->query; @@ -421,6 +425,7 @@ public static function unescape(string $s, string $reserved = '%;/?:@&=+$,'): st /** * Parses query string. Is affected by directive arg_separator.input. + * @return mixed[] */ public static function parseQuery(string $s): array { diff --git a/src/Http/UrlImmutable.php b/src/Http/UrlImmutable.php index 40c2dd69..309e3263 100644 --- a/src/Http/UrlImmutable.php +++ b/src/Http/UrlImmutable.php @@ -36,7 +36,7 @@ * @property-read string $absoluteUrl * @property-read string $authority * @property-read string $hostUrl - * @property-read array $queryParameters + * @property-read array $queryParameters */ class UrlImmutable implements \JsonSerializable { @@ -48,6 +48,8 @@ class UrlImmutable implements \JsonSerializable private string $host = ''; private ?int $port = null; private string $path = ''; + + /** @var mixed[] */ private array $query = []; private string $fragment = ''; private ?string $authority = null; @@ -199,6 +201,7 @@ public function getPath(): string } + /** @param string|mixed[] $query */ public function withQuery(string|array $query): static { $dolly = clone $this; @@ -221,12 +224,14 @@ public function withQueryParameter(string $name, mixed $value): static } + /** @return mixed[] */ public function getQueryParameters(): array { return $this->query; } + /** @return mixed[]|string|null */ public function getQueryParameter(string $name): array|string|null { return $this->query[$name] ?? null; From acc11f05dfa26edcaa84221905bad5a1ef0a3b95 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sat, 27 Dec 2025 20:04:14 +0100 Subject: [PATCH 06/13] Request::getOrigin() accepts only URL as defined in RFC 6454 --- src/Http/Request.php | 9 +++------ tests/Http/Request.getOrigin.phpt | 8 ++++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Http/Request.php b/src/Http/Request.php index fe0d6a42..48744c66 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -210,14 +210,11 @@ public function getReferer(): ?UrlImmutable */ public function getOrigin(): ?UrlImmutable { - $header = $this->headers['origin'] ?? 'null'; - try { - return $header === 'null' - ? null - : new UrlImmutable($header); - } catch (Nette\InvalidArgumentException) { + $header = $this->headers['origin'] ?? ''; + if (!preg_match('~^[a-z][a-z0-9+.-]*://[^/]+$~i', $header)) { return null; } + return new UrlImmutable($header); } diff --git a/tests/Http/Request.getOrigin.phpt b/tests/Http/Request.getOrigin.phpt index 8f58dfa1..db0480df 100644 --- a/tests/Http/Request.getOrigin.phpt +++ b/tests/Http/Request.getOrigin.phpt @@ -27,3 +27,11 @@ test('valid Origin header', function () { ]); Assert::equal(new UrlImmutable('https://nette.org'), $request->getOrigin()); }); + + +test('invalid Origin header', function () { + $request = new Http\Request(new Http\UrlScript, headers: [ + 'Origin' => 'https://nette.org/path', + ]); + Assert::null($request->getOrigin()); +}); From d5cef6ad18667cdf03b1171eeabf2dd4cba35c8e Mon Sep 17 00:00:00 2001 From: David Grudl Date: Tue, 6 Jan 2026 09:24:24 +0100 Subject: [PATCH 07/13] added RequestFactory::setForceHttps() --- src/Bridges/HttpDI/HttpExtension.php | 7 ++++++- src/Http/RequestFactory.php | 16 ++++++++++++++++ tests/Http/RequestFactory.scheme.phpt | 12 ++++++++++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/Bridges/HttpDI/HttpExtension.php b/src/Bridges/HttpDI/HttpExtension.php index 1de4e6a5..454c35c2 100644 --- a/src/Bridges/HttpDI/HttpExtension.php +++ b/src/Bridges/HttpDI/HttpExtension.php @@ -27,6 +27,7 @@ public function getConfigSchema(): Nette\Schema\Schema { return Expect::structure([ 'proxy' => Expect::anyOf(Expect::arrayOf('string'), Expect::string()->castTo('array'))->firstIsDefault()->dynamic(), + 'forceHttps' => Expect::bool(false)->dynamic(), 'headers' => Expect::arrayOf('scalar|null')->default([ 'X-Powered-By' => 'Nette Framework 3', 'Content-Type' => 'text/html; charset=utf-8', @@ -48,10 +49,14 @@ public function loadConfiguration(): void $builder = $this->getContainerBuilder(); $config = $this->config; - $builder->addDefinition($this->prefix('requestFactory')) + $requestFactory = $builder->addDefinition($this->prefix('requestFactory')) ->setFactory(Nette\Http\RequestFactory::class) ->addSetup('setProxy', [$config->proxy]); + if ($config->forceHttps) { + $requestFactory->addSetup('setForceHttps'); + } + $request = $builder->addDefinition($this->prefix('request')) ->setFactory('@Nette\Http\RequestFactory::fromGlobals'); diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index 13cc7482..326eb883 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -37,6 +37,8 @@ class RequestFactory /** @var list */ private array $proxies = []; + private bool $forceHttps = false; + /** * Disables sanitization of request data (GET, POST, cookies, file names) for binary-safe handling. @@ -59,6 +61,16 @@ public function setProxy($proxy): static } + /** + * Forces the request scheme to HTTPS regardless of the server environment. + */ + public function setForceHttps(bool $forceHttps = true): static + { + $this->forceHttps = $forceHttps; + return $this; + } + + /** * Returns new Request instance, using values from superglobals. */ @@ -70,6 +82,10 @@ public function fromGlobals(): Request [$post, $cookies] = $this->getGetPostCookie($url); [$remoteAddr, $remoteHost] = $this->getClient($url); + if ($this->forceHttps) { + $url->setScheme('https'); + } + return new Request( new UrlScript($url, $this->getScriptPath($url)), $post, diff --git a/tests/Http/RequestFactory.scheme.phpt b/tests/Http/RequestFactory.scheme.phpt index 4b5319f4..85107065 100644 --- a/tests/Http/RequestFactory.scheme.phpt +++ b/tests/Http/RequestFactory.scheme.phpt @@ -73,6 +73,18 @@ class RequestFactorySchemeTest extends Tester\TestCase ['https', 80, ['SERVER_NAME' => 'localhost:80', 'HTTPS' => 'off', 'HTTP_X_FORWARDED_PROTO' => 'https', 'HTTP_X_FORWARDED_PORT' => '80']], ]; } + + + public function testForceHttps() + { + $_SERVER = ['SERVER_NAME' => 'localhost:80']; + + $factory = new Nette\Http\RequestFactory; + $factory->setForceHttps(); + $url = $factory->fromGlobals()->getUrl(); + + Assert::same('https', $url->getScheme()); + } } From 43447b3970174d5584359927470fed22191820ce Mon Sep 17 00:00:00 2001 From: David Grudl Date: Sun, 28 Dec 2025 04:12:07 +0100 Subject: [PATCH 08/13] added Request::isFrom() WIP --- src/Http/IRequest.php | 1 + src/Http/Request.php | 26 ++++++++- tests/Http/Request.isFrom.phpt | 96 ++++++++++++++++++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 tests/Http/Request.isFrom.phpt diff --git a/src/Http/IRequest.php b/src/Http/IRequest.php index f3799516..506de5b3 100644 --- a/src/Http/IRequest.php +++ b/src/Http/IRequest.php @@ -12,6 +12,7 @@ * HTTP request contract providing access to URL, headers, cookies, uploaded files, and body. * @method ?UrlImmutable getReferer() Returns the referrer URL. * @method bool isSameSite() Checks whether the request is coming from the same site. + * @method bool isFrom(string|list|null $site = null, string|list|null $initiator = null) */ interface IRequest { diff --git a/src/Http/Request.php b/src/Http/Request.php index 48744c66..01f1891b 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -8,7 +8,7 @@ namespace Nette\Http; use Nette; -use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, preg_match, preg_match_all, rsort, strcasecmp, strtolower, strtr; +use function array_change_key_case, base64_decode, count, explode, func_num_args, gethostbyaddr, implode, in_array, preg_match, preg_match_all, rsort, strcasecmp, strtr; /** @@ -236,6 +236,30 @@ public function isSameSite(): bool } + /** + * Checks whether the request origin and initiator match the given Sec-Fetch-Site and Sec-Fetch-Dest values. + * Falls back to the Origin header for browsers that don't send Sec-Fetch headers (Safari < 16.4). + * @param string|list|null $site expected Sec-Fetch-Site values (e.g. 'same-origin', 'cross-site') + * @param string|list|null $initiator expected Sec-Fetch-Dest values (e.g. 'document', 'empty') + */ + public function isFrom(string|array|null $site = null, string|array|null $initiator = null): bool + { + $actualSite = $this->headers['sec-fetch-site'] ?? null; + $actualDest = $this->headers['sec-fetch-dest'] ?? null; + + if ($actualSite === null && ($origin = $this->getOrigin())) { // fallback for Safari < 16.4 + $actualSite = strcasecmp($origin->getScheme(), $this->url->getScheme()) === 0 + && strcasecmp(rtrim($origin->getHost(), '.'), rtrim($this->url->getHost(), '.')) === 0 + && $origin->getPort() === $this->url->getPort() + ? 'same-origin' + : 'cross-site'; + } + + return ($site === null || ($actualSite !== null && in_array($actualSite, (array) $site, strict: true))) + && ($initiator === null || ($actualDest !== null && in_array($actualDest, (array) $initiator, strict: true))); + } + + /** * Checks whether the request was made via AJAX (X-Requested-With: XMLHttpRequest). */ diff --git a/tests/Http/Request.isFrom.phpt b/tests/Http/Request.isFrom.phpt new file mode 100644 index 00000000..2d162dec --- /dev/null +++ b/tests/Http/Request.isFrom.phpt @@ -0,0 +1,96 @@ + 'same-origin', + 'Sec-Fetch-Dest' => 'document', + ]); + + Assert::true($request->isFrom('same-origin', 'document')); +}); + + +test('fails when expected header missing', function () { + $request = new Http\Request(new Http\UrlScript, headers: [ + 'Sec-Fetch-Site' => 'same-origin', + ]); + + Assert::false($request->isFrom('same-origin', 'document')); +}); + + +test('accepts multiple expected values', function () { + $request = new Http\Request(new Http\UrlScript, headers: [ + 'Sec-Fetch-Site' => 'cross-site', + 'Sec-Fetch-Dest' => 'image', + ]); + + Assert::true($request->isFrom(['same-origin', 'cross-site'], ['document', 'image'])); + Assert::false($request->isFrom(['cross-site'], ['Document'])); + Assert::false($request->isFrom(['Cross-Site'], ['image'])); +}); + + +test('fallback same-origin from Origin header', function () { + $url = new Http\UrlScript('https://nette.org/app/'); + $request = new Http\Request($url, headers: [ + 'Origin' => 'https://nette.org', + ]); + + Assert::true($request->isFrom('same-origin')); +}); + + +test('fallback cross-site from Origin header', function () { + $url = new Http\UrlScript('https://nette.org/'); + $request = new Http\Request($url, headers: [ + 'Origin' => 'https://example.com', + ]); + + Assert::true($request->isFrom('cross-site')); +}); + + +test('fallback missing without Origin header', function () { + $url = new Http\UrlScript('https://nette.org/'); + $request = new Http\Request($url); + + Assert::false($request->isFrom('same-origin')); +}); + + +test('fallback not used when header present', function () { + $url = new Http\UrlScript('https://nette.org/'); + $request = new Http\Request($url, headers: [ + 'Sec-Fetch-Site' => 'none', + 'Origin' => 'https://nette.org', + ]); + + Assert::false($request->isFrom('same-origin')); +}); + + +test('fallback cross-site when port differs', function () { + $url = new Http\UrlScript('https://nette.org:443'); + $request = new Http\Request($url, headers: [ + 'Origin' => 'https://nette.org:444', + ]); + + Assert::true($request->isFrom('cross-site')); +}); + + +test('fallback ignored for invalid Origin', function () { + $url = new Http\UrlScript('https://nette.org/'); + $request = new Http\Request($url, headers: [ + 'Origin' => 'null', + ]); + + Assert::false($request->isFrom('same-origin')); +}); From 481f110218a0055dddbae2cc9cf3a0c80a5f90ff Mon Sep 17 00:00:00 2001 From: David Grudl Date: Wed, 21 Jan 2026 07:51:17 +0100 Subject: [PATCH 09/13] uses nette/phpstan-rules --- composer.json | 9 ++++++++- tests/types/TypesTest.phpt | 7 +++++++ tests/types/http-types.php | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 tests/types/TypesTest.phpt create mode 100644 tests/types/http-types.php diff --git a/composer.json b/composer.json index 3c2ae3f4..e177612f 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,9 @@ "nette/tester": "^2.6", "nette/security": "^3.0", "tracy/tracy": "^2.8", - "phpstan/phpstan-nette": "^2.0@stable" + "phpstan/phpstan": "^2.1@stable", + "phpstan/extension-installer": "^1.4@stable", + "nette/phpstan-rules": "^1.0" }, "conflict": { "nette/di": "<3.0.3", @@ -50,5 +52,10 @@ "branch-alias": { "dev-master": "3.3-dev" } + }, + "config": { + "allow-plugins": { + "phpstan/extension-installer": true + } } } diff --git a/tests/types/TypesTest.phpt b/tests/types/TypesTest.phpt new file mode 100644 index 00000000..b91310f3 --- /dev/null +++ b/tests/types/TypesTest.phpt @@ -0,0 +1,7 @@ + $value) { + assertType('string', $key); + assertType('mixed', $value); + } +} + + +function testSessionSectionArrayAccess(SessionSection $section): void +{ + $section->remove(); + $value = $section['key']; + assertType('mixed', $value); +} + + +function testFileUploadGetImageSize(FileUpload $upload): void +{ + $size = $upload->getImageSize(); + assertType('array{int, int}|null', $size); +} From 8c270510da27538be272864c1ede8912b0ca3067 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 26 Feb 2026 06:15:08 +0100 Subject: [PATCH 10/13] fixed PHPStan errors --- phpstan-baseline.neon | 55 +++++++++++++++++++++++++ phpstan.neon | 12 +++++- src/Bridges/HttpDI/HttpExtension.php | 2 + src/Bridges/HttpDI/SessionExtension.php | 1 + src/Bridges/HttpTracy/dist/panel.phtml | 2 +- src/Bridges/HttpTracy/panel.latte | 1 + src/Http/FileUpload.php | 20 ++++----- src/Http/Helpers.php | 10 ++++- src/Http/Request.php | 8 ++-- src/Http/RequestFactory.php | 2 +- src/Http/Response.php | 8 ++-- src/Http/Session.php | 8 ++-- src/Http/SessionSection.php | 4 +- src/Http/Url.php | 9 ++-- 14 files changed, 112 insertions(+), 30 deletions(-) create mode 100644 phpstan-baseline.neon diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..59680809 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,55 @@ +parameters: + ignoreErrors: + - + message: '#^Unknown parameter \$sameSite in call to method Nette\\Http\\IResponse\:\:setCookie\(\)\.$#' + identifier: argument.unknown + count: 1 + path: src/Http/Helpers.php + + - + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue + count: 1 + path: src/Http/Request.php + + - + message: '#^Strict comparison using \=\=\= between 2 and 2 will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: src/Http/Request.php + + - + message: '#^Offset ''name'' on non\-empty\-array in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset + count: 1 + path: src/Http/RequestFactory.php + + - + message: '#^Variable \$_FILES on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable + count: 1 + path: src/Http/RequestFactory.php + + - + message: '#^Method Nette\\Http\\IResponse\:\:setCookie\(\) invoked with 8 parameters, 3\-7 required\.$#' + identifier: arguments.count + count: 1 + path: src/Http/Session.php + + - + message: '#^Offset ''samesite'' on array\{lifetime\: int\<0, max\>, path\: non\-falsy\-string, domain\: string, secure\: bool, httponly\: bool, samesite\: ''Lax''\|''lax''\|''None''\|''none''\|''Strict''\|''strict''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: src/Http/Session.php + + - + message: '#^Method Nette\\Http\\Url\:\:export\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Http/Url.php + + - + message: '#^Method Nette\\Http\\UrlImmutable\:\:export\(\) return type has no value type specified in iterable type array\.$#' + identifier: missingType.iterableValue + count: 1 + path: src/Http/UrlImmutable.php diff --git a/phpstan.neon b/phpstan.neon index a0846930..0d34d240 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,5 +1,15 @@ parameters: - level: 5 + level: 8 paths: - src + + excludePaths: + - src/Http/UserStorage.php # deprecated, implements interface from optional nette/security + + fileExtensions: + - php + - phtml + +includes: + - phpstan-baseline.neon diff --git a/src/Bridges/HttpDI/HttpExtension.php b/src/Bridges/HttpDI/HttpExtension.php index 454c35c2..0f3dcb32 100644 --- a/src/Bridges/HttpDI/HttpExtension.php +++ b/src/Bridges/HttpDI/HttpExtension.php @@ -48,6 +48,7 @@ public function loadConfiguration(): void { $builder = $this->getContainerBuilder(); $config = $this->config; + \assert($config instanceof \stdClass); $requestFactory = $builder->addDefinition($this->prefix('requestFactory')) ->setFactory(Nette\Http\RequestFactory::class) @@ -96,6 +97,7 @@ public function loadConfiguration(): void private function sendHeaders(): void { $config = $this->config; + \assert($config instanceof \stdClass); $headers = array_map(strval(...), $config->headers); if (isset($config->frames) && $config->frames !== true && !isset($headers['X-Frame-Options'])) { diff --git a/src/Bridges/HttpDI/SessionExtension.php b/src/Bridges/HttpDI/SessionExtension.php index a85f171c..5bd4e5f6 100644 --- a/src/Bridges/HttpDI/SessionExtension.php +++ b/src/Bridges/HttpDI/SessionExtension.php @@ -42,6 +42,7 @@ public function loadConfiguration(): void { $builder = $this->getContainerBuilder(); $config = $this->config; + \assert($config instanceof \stdClass); $session = $builder->addDefinition($this->prefix('session')) ->setFactory(Nette\Http\Session::class); diff --git a/src/Bridges/HttpTracy/dist/panel.phtml b/src/Bridges/HttpTracy/dist/panel.phtml index 07650e33..6d1547fa 100644 --- a/src/Bridges/HttpTracy/dist/panel.phtml +++ b/src/Bridges/HttpTracy/dist/panel.phtml @@ -1,5 +1,5 @@ -