diff --git a/.gitattributes b/.gitattributes
index 9670e954..88cfd880 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,9 +1,11 @@
-.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
+CLAUDE.md 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/.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/static-analysis.yml b/.github/workflows/static-analysis.yml
index 401b6c88..94d41706 100644
--- a/.github/workflows/static-analysis.yml
+++ b/.github/workflows/static-analysis.yml
@@ -1,9 +1,6 @@
name: Static Analysis
-on:
- push:
- branches:
- - master
+on: [push, pull_request]
jobs:
phpstan:
@@ -13,9 +10,8 @@ jobs:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
- php-version: 8.1
+ php-version: 8.5
coverage: none
- run: composer install --no-progress --prefer-dist
- run: composer phpstan
- continue-on-error: true # is only informative
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/.gitignore b/.gitignore
index de4a392c..d49bcd46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
/vendor
/composer.lock
+tests/lock
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..62b16e62
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,328 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## Project Overview
+
+**Nette HTTP Component** - A standalone PHP library providing HTTP abstraction for request/response handling, URL manipulation, session management, and file uploads. Part of the Nette Framework ecosystem but usable independently.
+
+- **PHP Version**: 8.1 - 8.5
+- **Package**: `nette/http`
+
+## Essential Commands
+
+### Running Tests
+
+```bash
+# Run all tests
+vendor/bin/tester tests -s
+
+# Run specific test file
+php tests/Http/Request.files.phpt
+
+# Run tests in specific directory
+vendor/bin/tester tests/Http -s
+```
+
+### Static Analysis
+
+```bash
+# Run PHPStan
+composer phpstan
+
+# Or directly
+vendor/bin/phpstan analyse
+```
+
+## Core Architecture
+
+### Immutability Pattern
+
+The codebase uses a sophisticated immutability strategy:
+
+- **`Request`** - Immutable HTTP request with single wither method `withUrl()`
+- **`Response`** - Mutable for managing response state (headers, cookies, status)
+- **`UrlImmutable`** - Immutable URL with wither methods (`withHost()`, `withPath()`, etc.)
+- **`Url`** - Mutable URL with setters for flexible building
+- **`UrlScript`** - Extends UrlImmutable with script path information
+
+**Design principle**: Data objects (Request) are immutable for integrity; state managers (Response, Session) are mutable for practical management.
+
+### Security-First Design
+
+Input sanitization is mandatory, not optional:
+
+1. **RequestFactory** sanitizes ALL input:
+ - Removes invalid UTF-8 sequences
+ - Strips control characters (except tab, newline, carriage return)
+ - Validates with regex: `[\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]`
+
+2. **Secure defaults everywhere**:
+ - Cookies are `httpOnly` by default
+ - `SameSite=Lax` by default
+ - Session uses strict mode and one-time cookies only
+ - HTTPS auto-detection via proxy configuration
+
+3. **FileUpload** security:
+ - Content-based MIME detection (not client-provided)
+ - `getSanitizedName()` removes dangerous characters
+ - Documentation warns against trusting `getUntrustedName()`
+
+### Key Components
+
+#### Request (`src/Http/Request.php`)
+- Immutable HTTP request representation
+- Type-safe access to GET/POST/COOKIE/FILES/headers
+- AJAX detection, same-site checking (`_nss` cookie, formerly `nette-samesite`), language detection
+- Sanitized by RequestFactory before construction
+- **Origin detection**: `getOrigin()` returns scheme + host + port for CORS validation
+- **Basic Auth**: `getBasicCredentials()` returns `[user, password]` array
+- **File access**: `getFile(['my-form', 'details', 'avatar'])` accepts array of keys for nested structures
+- **Warning**: Browsers don't send URL fragments to the server (`$url->getFragment()` returns empty string)
+
+#### Response (`src/Http/Response.php`)
+- Mutable HTTP response management
+- Header manipulation (set/add/delete)
+- Cookie handling with security defaults (use `Response::SameSiteLax`, `SameSiteStrict`, `SameSiteNone` constants)
+- Redirect, cache control, content-type helpers
+- **Download support**: `sendAsFile('invoice.pdf')` triggers browser download dialog
+- Checks `isSent()` to prevent modification after output starts
+- **Cookie domain**: If specified, includes subdomains; if omitted, excludes subdomains
+
+#### RequestFactory (`src/Http/RequestFactory.php`)
+- Creates Request from PHP superglobals (`$_GET`, `$_POST`, etc.)
+- Configurable proxy support (RFC 7239 Forwarded header + X-Forwarded-*)
+- URL filters for cleaning malformed URLs
+- File upload normalization into FileUpload objects
+
+#### URL Classes
+- **`Url`** - Mutable URL builder with setters, supports `appendQuery()` to add parameters
+- **`UrlImmutable`** - Immutable URL with wither methods
+ - `resolve($reference)` - Resolves relative URLs like a browser (v3.3.2+)
+ - `withoutUserInfo()` - Removes user and password
+- **`UrlScript`** - Request URL with virtual components:
+ - `baseUrl` - Base URL including domain and path to app root
+ - `basePath` - Path to application root directory
+ - `scriptPath` - Path to current script
+ - `relativePath` - Script name relative to basePath
+ - `relativeUrl` - Everything after baseUrl (query + fragment)
+ - `pathInfo` - Rarely used part after script name
+- **Static helpers**:
+ - `Url::isAbsolute($url)` - Checks if URL has scheme (v3.3.2+)
+ - `Url::removeDotSegments($path)` - Normalizes path by removing `.` and `..` (v3.3.2+)
+- All support IDN (via `ext-intl`), canonicalization, query manipulation
+
+#### Session (`src/Http/Session.php` + `SessionSection.php`)
+- **Auto-start modes**:
+ - `smart` - Start only when session data is accessed (default)
+ - `always` - Start immediately with application
+ - `never` - Manual start required
+- Namespaced sections to prevent naming conflicts
+- **SessionSection API**: Use explicit methods instead of magic properties:
+ - `$section->set('userName', 'john')` - Write variable
+ - `$section->get('userName')` - Read variable (returns null if missing)
+ - `$section->remove('userName')` - Delete variable
+ - `$section->set('flash', $message, '30 seconds')` - Third parameter sets expiration
+- Per-section or per-variable expiration
+- Custom session handler support
+- **Events**: `$onStart`, `$onBeforeWrite` - Callbacks invoked after session starts or before write to disk
+- **Session ID management**: `regenerateId()` generates new ID (e.g., after login for security)
+
+#### FileUpload (`src/Http/FileUpload.php`)
+- Safe file upload handling
+- Content-based MIME detection (requires `ext-fileinfo`)
+- Image validation and conversion (supports JPEG, PNG, GIF, WebP, AVIF)
+- Sanitized filename generation
+- **Filename methods**:
+ - `getUntrustedName()` - Original browser-provided name (⚠️ never trust!)
+ - `getSanitizedName()` - Safe ASCII-only name `[a-zA-Z0-9.-]` with correct extension
+ - `getSuggestedExtension()` - Extension based on MIME type (v3.2.4+)
+ - `getUntrustedFullPath()` - Full path for directory uploads (PHP 8.1+, ⚠️ never trust!)
+
+### Nette DI Integration
+
+Two extensions provide auto-wiring:
+
+1. **HttpExtension** (`src/Bridges/HttpDI/HttpExtension.php`)
+ - **Registers**: `http.requestFactory`, `http.request`, `http.response`
+ - **Configuration**: proxy IPs, headers, CSP, X-Frame-Options, cookie defaults
+ - **CSP with nonce**: Automatically generates nonce for inline scripts
+ ```neon
+ http:
+ csp:
+ script-src: [nonce, strict-dynamic, self]
+ ```
+ Use in templates: `` - nonce filled automatically
+ - **Cookie defaults**: `cookiePath`, `cookieDomain: domain` (includes subdomains), `cookieSecure: auto`
+ - **X-Frame-Options**: `frames: SAMEORIGIN` (default) or `frames: true` to allow all
+
+2. **SessionExtension** (`src/Bridges/HttpDI/SessionExtension.php`)
+ - **Registers**: `session.session`
+ - **Configuration**: `autoStart: smart|always|never`, expiration, handler, all PHP `session.*` directives in camelCase
+ - **Tracy debugger panel**: Enable with `debugger: true` in config
+ - **Session cookie**: Configure separately with `cookieDomain`, `cookieSamesite: Strict|Lax|None`
+
+## Code Conventions
+
+### Strict PHP Standards
+
+Every file must start with:
+```php
+declare(strict_types=1);
+```
+
+### Modern PHP Features
+
+Heavily uses PHP 8.1+ features:
+
+```php
+// Constructor property promotion with readonly
+public function __construct(
+ private readonly UrlScript $url,
+ private readonly array $post = [],
+ private readonly string $method = 'GET',
+) {
+}
+
+// Named arguments
+setcookie($name, $value, [
+ 'expires' => $expire ? (int) DateTime::from($expire)->format('U') : 0,
+ 'httponly' => $httpOnly ?? true,
+ 'samesite' => $sameSite ?? self::SameSiteLax,
+]);
+
+// First-class callables
+Nette\Utils\Callback::invokeSafe(
+ 'session_start',
+ [['read_and_close' => $this->readAndClose]],
+ fn(string $message) => throw new Exception($message)
+);
+```
+
+### Property Documentation with SmartObject
+
+Uses `@property-read` magic properties with Nette's SmartObject trait:
+
+```php
+/**
+ * @property-read UrlScript $url
+ * @property-read array $query
+ * @property-read string $method
+ */
+class Request implements IRequest
+{
+ use Nette\SmartObject;
+}
+```
+
+### Testing with Nette Tester
+
+Test files use `.phpt` extension and follow this pattern:
+
+```php
+canonicalize();
+ Assert::same('http://example.com/path/file.txt', (string) $url);
+});
+
+test('Url handles IDN domains', function () {
+ $url = new Url('https://xn--tst-qla.de/');
+ $url->canonicalize();
+ Assert::same('https://täst.de/', (string) $url);
+});
+```
+
+The `test()` helper is defined in `tests/bootstrap.php`.
+
+## Development Guidelines
+
+### When Adding Features
+
+1. **Read existing code first** - Understand patterns before modifying
+2. **Security first** - Consider injection, XSS, CSRF implications
+3. **Maintain immutability contracts** - Don't add setters to immutable classes
+4. **Test thoroughly** - Add `.phpt` test files in `tests/Http/`
+5. **Check Windows compatibility** - Tests run on Windows in CI
+
+### Common Patterns
+
+**Proxy detection** - Use RequestFactory's `setProxy()` for trusted proxy IPs:
+```php
+$factory->setProxy(['127.0.0.1', '::1']);
+```
+
+**URL filtering** - Clean malformed URLs via urlFilters:
+```php
+// Remove spaces from path
+$factory->urlFilters['path']['%20'] = '';
+
+// Remove trailing punctuation
+$factory->urlFilters['url']['[.,)]$'] = '';
+```
+
+**Cookie security** - Response uses secure defaults:
+```php
+// httpOnly=true, sameSite=Lax by default
+$response->setCookie('name', 'value', '1 hour');
+```
+
+**Session sections** - Namespace session data with explicit methods:
+```php
+$section = $session->getSection('cart');
+$section->set('items', []);
+$section->setExpiration('20 minutes');
+
+// Per-variable expiration
+$section->set('flash', $message, '30 seconds');
+
+// Events
+$session->onBeforeWrite[] = function () use ($section) {
+ $section->set('lastSaved', time());
+};
+```
+
+## CI/CD Pipeline
+
+GitHub Actions runs:
+
+1. **Tests** (`.github/workflows/tests.yml`):
+ - Matrix: Ubuntu/Windows/macOS × PHP 8.1-8.5 × php/php-cgi
+ - Lowest dependencies test
+ - Code coverage with Coveralls
+
+2. **Static Analysis** (`.github/workflows/static-analysis.yml`):
+ - PHPStan level 5 (informative only)
+
+3. **Coding Style** (`.github/workflows/coding-style.yml`):
+ - Nette Coding Standard (PSR-12 based)
+
+## Architecture Principles
+
+- **Single Responsibility** - Each class has one clear purpose
+- **Dependency Injection** - Constructor injection, no service locators
+- **Type Safety** - Everything typed (properties, parameters, returns)
+- **Fail Fast** - Validation at boundaries, exceptions for errors
+- **Framework Optional** - Works standalone or with Nette DI
+
+## Important Notes
+
+- **Browser behavior** - Browsers don't send URL fragments or Origin header for same-origin GET requests
+- **Proxy configuration critical** - Required for correct IP detection and HTTPS detection
+- **Session auto-start modes**:
+ - `smart` - Starts only when `$section->get()`/`set()` is called (default)
+ - `always` - Starts immediately on application bootstrap
+ - `never` - Must call `$session->start()` manually
+- **URL encoding nuances** - Respects PHP's `arg_separator.input` for query parsing
+- **FileUpload validation** - Always check `hasFile()` and `isOk()` before processing
+- **UrlScript virtual components** - Generated by RequestFactory, understand baseUrl vs basePath distinction
+- **CSP nonce in templates** - Use `