diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e8275301..6730be9b 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -8,7 +8,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] - php: ['7.1', '7.2', '7.3', '7.4', '8.0'] + php: ['7.2', '7.3', '7.4', '8.0'] # sapi: ['php', 'php-cgi'] fail-fast: false @@ -38,7 +38,7 @@ jobs: - uses: actions/checkout@v2 - uses: shivammathur/setup-php@v2 with: - php-version: 7.1 + php-version: 7.2 coverage: none extensions: fileinfo, intl diff --git a/.travis.yml b/.travis.yml index cb943ce6..a8a8869f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: php php: - - 7.1 - 7.2 - 7.3 - 7.4 diff --git a/appveyor.yml b/appveyor.yml index 0c9820f0..a672440d 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,7 @@ install: # Install PHP - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - IF %PHP%==1 cd c:\php - - IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.1.0-Win32-VC14-x64.zip --output php.zip + - IF %PHP%==1 curl https://windows.php.net/downloads/releases/archives/php-7.2.28-Win32-VC15-x64.zip --output php.zip - IF %PHP%==1 7z x php.zip >nul - IF %PHP%==1 echo extension_dir=ext >> php.ini - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini diff --git a/composer.json b/composer.json index d193eab3..9ec06fb8 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.1 <8.1", + "php": ">=7.2 <8.1", "nette/utils": "^3.1" }, "require-dev": { @@ -42,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "3.1-dev" } } } diff --git a/readme.md b/readme.md index 383eee0d..478c90a4 100644 --- a/readme.md +++ b/readme.md @@ -23,7 +23,7 @@ Installation: composer require nette/http ``` -It requires PHP version 7.1 and supports PHP up to 8.0. +It requires PHP version 7.2 and supports PHP up to 8.0. HTTP Request diff --git a/src/Bridges/HttpDI/HttpExtension.php b/src/Bridges/HttpDI/HttpExtension.php index d4e0cd4a..c5dbc163 100644 --- a/src/Bridges/HttpDI/HttpExtension.php +++ b/src/Bridges/HttpDI/HttpExtension.php @@ -40,7 +40,8 @@ public function getConfigSchema(): Nette\Schema\Schema 'csp' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy 'cspReportOnly' => Expect::arrayOf('array|scalar|null'), // Content-Security-Policy-Report-Only 'featurePolicy' => Expect::arrayOf('array|scalar|null'), // Feature-Policy - 'cookieSecure' => Expect::anyOf(null, true, false, 'auto'), // true|false|auto Whether the cookie is available only through HTTPS + 'cookieSecure' => Expect::anyOf(null, true, false, 'auto')->default('auto'), // true|false|auto Whether the cookie is available only through HTTPS + 'cookieNameStrict' => Expect::anyOf(Expect::string(), Expect::bool(), null)->default('nette-samesite'), ]); } @@ -55,7 +56,8 @@ public function loadConfiguration() ->addSetup('setProxy', [$config->proxy]); $builder->addDefinition($this->prefix('request')) - ->setFactory('@Nette\Http\RequestFactory::fromGlobals'); + ->setFactory('@Nette\Http\RequestFactory::fromGlobals') + ->addSetup('setCookieNameStrict', [$config->cookieNameStrict]); $response = $builder->addDefinition($this->prefix('response')) ->setFactory(Nette\Http\Response::class); @@ -121,8 +123,8 @@ private function sendHeaders() } $this->initialization->addBody( - 'Nette\Http\Helpers::initCookie($this->getService(?), $response);', - [$this->prefix('request')] + 'Nette\Http\Helpers::initCookie($this->getService(?), $response, ?);', + [$this->prefix('request'), $config->cookieNameStrict] ); } diff --git a/src/Http/FileUpload.php b/src/Http/FileUpload.php index e0d149cb..490ad882 100644 --- a/src/Http/FileUpload.php +++ b/src/Http/FileUpload.php @@ -62,9 +62,7 @@ public function __construct(?array $value) /** - * Returns the original file name as submitted by the browser. Do not trust the value returned by this method. - * A client could send a malicious filename with the intention to corrupt or hack your application. - * Alias for getUntrustedName() + * @deprecated use getUntrustedName() */ public function getName(): string { diff --git a/src/Http/Helpers.php b/src/Http/Helpers.php index 02332bbf..82a5ef7c 100644 --- a/src/Http/Helpers.php +++ b/src/Http/Helpers.php @@ -52,10 +52,10 @@ 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, $cookieName = self::STRICT_COOKIE_NAME) { - if (!$request->getCookie(self::STRICT_COOKIE_NAME)) { - $response->setCookie(self::STRICT_COOKIE_NAME, '1', 0, '/', null, null, true, 'Strict'); + if (!$request->getCookie($cookieName)) { + $response->setCookie($cookieName, '1', 0, '/', null, null, true, 'Strict'); } } } diff --git a/src/Http/Request.php b/src/Http/Request.php index 802c1742..b7a2519d 100644 --- a/src/Http/Request.php +++ b/src/Http/Request.php @@ -60,6 +60,9 @@ class Request implements IRequest /** @var callable|null */ private $rawBodyCallback; + /** @var string */ + private $cookieNameStrict; + public function __construct( UrlScript $url, @@ -81,6 +84,17 @@ public function __construct( $this->remoteAddress = $remoteAddress; $this->remoteHost = $remoteHost; $this->rawBodyCallback = $rawBodyCallback; + $this->cookieNameStrict = Helpers::STRICT_COOKIE_NAME; + } + + + /** + * Setter for cookieNameStrict + * @param string $name + */ + public function setCookieNameStrict(string $name) + { + $this->cookieNameStrict = $name; } @@ -142,11 +156,15 @@ public function getPost(string $key = null) /** * Returns uploaded file. - * @return FileUpload|array|null + * @param string|string[] $key + * @return ?FileUpload */ - public function getFile(string $key) + public function getFile($key) { - return $this->files[$key] ?? null; + $res = Nette\Utils\Arrays::get($this->files, $key, null); + return $res instanceof FileUpload + ? $res + : null; } @@ -249,7 +267,7 @@ public function isSecured(): bool */ public function isSameSite(): bool { - return isset($this->cookies[Helpers::STRICT_COOKIE_NAME]); + return isset($this->cookies[$this->cookieNameStrict]); } diff --git a/src/Http/RequestFactory.php b/src/Http/RequestFactory.php index 76e057bc..71d8d161 100644 --- a/src/Http/RequestFactory.php +++ b/src/Http/RequestFactory.php @@ -163,8 +163,11 @@ private function getGetPostCookie(Url $url): array $list[$key][$k] = $v; $list[] = &$list[$key][$k]; - } else { + } elseif (is_string($v)) { $list[$key][$k] = (string) preg_replace('#[^' . self::CHARS . ']+#u', '', $v); + + } else { + throw new Nette\InvalidStateException(sprintf('Invalid value in $_POST/$_COOKIE in key %s, expected string, %s given.', "'$k'", gettype($v))); } } } diff --git a/src/Http/Session.php b/src/Http/Session.php index db98c6a6..bb9fc310 100644 --- a/src/Http/Session.php +++ b/src/Http/Session.php @@ -466,6 +466,7 @@ public function setCookieParameters( /** @deprecated */ public function getCookieParameters(): array { + trigger_error(__METHOD__ . '() is deprecated.', E_USER_DEPRECATED); return session_get_cookie_params(); } diff --git a/src/Http/Url.php b/src/Http/Url.php index e282a486..64ee5fee 100644 --- a/src/Http/Url.php +++ b/src/Http/Url.php @@ -16,13 +16,13 @@ * Mutable representation of a URL. * *
- * scheme  user  password  host  port  basePath   relativeUrl
- *   |      |      |        |      |    |             |
- * /--\   /--\ /------\ /-------\ /--\/--\/----------------------------\
+ * scheme  user  password  host  port      path        query    fragment
+ *   |      |      |        |      |        |            |         |
+ * /--\   /--\ /------\ /-------\ /--\/------------\ /--------\ /------\
  * http://john:x0y17575@nette.org:8042/en/manual.php?name=param#fragment  <-- absoluteUrl
- *        \__________________________/\____________/^\________/^\______/
- *                     |                     |           |         |
- *                 authority               path        query    fragment
+ * \______\__________________________/
+ *     |               |
+ *  hostUrl        authority
  * 
* * @property string $scheme @@ -316,6 +316,7 @@ public function getHostUrl(): string } + /** @deprecated */ public function getBasePath(): string { $pos = strrpos($this->path, '/'); @@ -323,12 +324,14 @@ public function getBasePath(): string } + /** @deprecated */ public function getBaseUrl(): string { return $this->getHostUrl() . $this->getBasePath(); } + /** @deprecated */ public function getRelativeUrl(): string { return substr($this->getAbsoluteUrl(), strlen($this->getBaseUrl())); @@ -360,6 +363,7 @@ public function isEqual($url): bool /** * Transforms URL to canonical form. * @return static + * @deprecated */ public function canonicalize() { diff --git a/src/Http/UrlScript.php b/src/Http/UrlScript.php index e43f17f8..fdc26fae 100644 --- a/src/Http/UrlScript.php +++ b/src/Http/UrlScript.php @@ -54,7 +54,8 @@ public function withPath(string $path, string $scriptPath = '') { $dolly = clone $this; $dolly->scriptPath = $scriptPath; - return call_user_func([$dolly, 'parent::withPath'], $path); + $parent = \Closure::fromCallable([UrlImmutable::class, 'withPath'])->bindTo($dolly); + return $parent($path); } diff --git a/tests/Http.DI/HttpExtension.sameSiteProtectionCustom.phpt b/tests/Http.DI/HttpExtension.sameSiteProtectionCustom.phpt new file mode 100644 index 00000000..a92c932c --- /dev/null +++ b/tests/Http.DI/HttpExtension.sameSiteProtectionCustom.phpt @@ -0,0 +1,38 @@ +addExtension('http', new HttpExtension); +$loader = new DI\Config\Loader; +$config = $loader->load(Tester\FileMock::create(<<<'EOD' +http: + cookieNameStrict: test-samesite +EOD +, 'neon')); + +// protection is enabled by default +eval($compiler->addConfig($config)->compile()); + +$container = new Container; +$container->initialize(); + +$headers = headers_list(); +Assert::contains( + PHP_VERSION_ID >= 70300 + ? 'Set-Cookie: test-samesite=1; path=/; HttpOnly; SameSite=Strict' + : 'Set-Cookie: test-samesite=1; path=/; SameSite=Strict; HttpOnly', + $headers +); diff --git a/tests/Http/Request.files.phpt b/tests/Http/Request.files.phpt index 5212508f..c9b1ee94 100644 --- a/tests/Http/Request.files.phpt +++ b/tests/Http/Request.files.phpt @@ -111,4 +111,4 @@ Assert::false(isset($request->files['file0'])); Assert::true(isset($request->files['file1'])); Assert::null($request->getFile('empty1')); -Assert::same([null], $request->getFile('empty2')); +Assert::null($request->getFile('empty2')); diff --git a/tests/Http/Request.invalidType.phpt b/tests/Http/Request.invalidType.phpt new file mode 100644 index 00000000..0dd6d6e4 --- /dev/null +++ b/tests/Http/Request.invalidType.phpt @@ -0,0 +1,34 @@ + 1, + ]; + + Assert::exception(function () { + (new Http\RequestFactory)->fromGlobals(); + }, Nette\InvalidStateException::class, 'Invalid value in $_POST/$_COOKIE in key \'int\', expected string, integer given.'); +}); + + +test('invalid COOKIE', function () { + $_POST = []; + $_COOKIE = ['x' => [1]]; + + Assert::exception(function () { + (new Http\RequestFactory)->fromGlobals(); + }, Nette\InvalidStateException::class, 'Invalid value in $_POST/$_COOKIE in key \'0\', expected string, integer given.'); +});