From 4a6c95a5d0e64850058747533d4164b924e8cdcf Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 16 Feb 2019 19:47:05 +0100 Subject: [PATCH 01/84] Typo (#110) --- .scrutinizer.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scrutinizer.yml b/.scrutinizer.yml index f7a088e..4304ab1 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,5 +1,5 @@ filter: - excluded_paths: [vendor/*, Tests/*] + excluded_paths: [vendor/*, tests/*] checks: php: code_rating: true From 2f22fca397ca0a9e6f4faf7833116cc42c3180bc Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Mon, 8 Apr 2019 21:11:19 +0200 Subject: [PATCH 02/84] Typo (#112) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25237d2..eb95b6b 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ If you are using Symfony Flex then you get all message factories registered as s ## Usage -The PSR-7 objects do not contain any other public methods then those defined in +The PSR-7 objects do not contain any other public methods than those defined in the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). ### Create objects From f3d331dca621d1e60dbcf7ae11e25f06369a0d83 Mon Sep 17 00:00:00 2001 From: Martijn van der Ven Date: Mon, 8 Apr 2019 21:13:12 +0200 Subject: [PATCH 03/84] Clean-up usage section (#114) --- README.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index eb95b6b..c3ff5e4 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). Use the PSR-17 factory to create requests, streams, URIs etc. ```php -$factory = new \Nyholm\Psr7\Factory\Psr17Factory(); -$request = $factory->createRequest('GET', 'http://tnyholm.se'); -$stream = $factory->createStream('foobar'); +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); +$stream = $psr17Factory->createStream('foobar'); ``` ### Sending a request @@ -58,9 +58,9 @@ composer require kriswallsmith/buzz ```php $psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); -$psr18Client = new Buzz\Client\Curl($psr17Factory); +$psr18Client = new \Buzz\Client\Curl($psr17Factory); -$request = (new Psr17Factory())->createRequest('GET', 'http://tnyholm.se'); +$request = $psr17Factory->createRequest('GET', 'http://tnyholm.se'); $response = $psr18Client->sendRequest($request); ``` @@ -74,9 +74,7 @@ composer require nyholm/psr7-server ``` ```php -use Nyholm\Psr7\Factory\Psr17Factory; - -$psr17Factory = new Psr17Factory(); +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); $creator = new ServerRequestCreator( $psr17Factory, // ServerRequestFactory @@ -95,7 +93,10 @@ composer require zendframework/zend-httphandlerrunner ``` ```php -$response = (new Psr17Factory())->createReponse('200', 'Hello world'); +$psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); + +$responseBody = $psr17Factory->createStream('Hello world'); +$response = $psr17Factory->createResponse(200)->withBody($responseBody); (new \Zend\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); ``` From 8d1625cef1f82bcd2352bf113a491b4821196a2d Mon Sep 17 00:00:00 2001 From: Kamal Khan Date: Wed, 22 May 2019 12:41:36 +0500 Subject: [PATCH 04/84] Fix namespace in readme (#119) Change `ServerRequestCreator` to `\Nyholm\Psr7Server\ServerRequestCreator` --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c3ff5e4..d626853 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ composer require nyholm/psr7-server ```php $psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); -$creator = new ServerRequestCreator( +$creator = new \Nyholm\Psr7Server\ServerRequestCreator( $psr17Factory, // ServerRequestFactory $psr17Factory, // UriFactory $psr17Factory, // UploadedFileFactory From 53205ddef01ab582a94e7fac59c1e5eb3f8cc909 Mon Sep 17 00:00:00 2001 From: Tracerneo <660285+Tracerneo@users.noreply.github.com> Date: Wed, 22 May 2019 10:51:08 +0200 Subject: [PATCH 05/84] Change minimal port number to 0 (unix socket) (#115) --- src/Uri.php | 4 ++-- tests/UriTest.php | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Uri.php b/src/Uri.php index 1775360..d67c078 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -278,8 +278,8 @@ private function filterPort($port): ?int } $port = (int) $port; - if (1 > $port || 0xffff < $port) { - throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port)); + if (0 > $port || 0xffff < $port) { + throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return self::isNonStandardPort($this->scheme, $port) ? $port : null; diff --git a/tests/UriTest.php b/tests/UriTest.php index cb50d3a..2d37623 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -113,17 +113,17 @@ public function getInvalidUris() public function testPortMustBeValid() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid port: 100000. Must be between 1 and 65535'); + $this->expectExceptionMessage('Invalid port: 100000. Must be between 0 and 65535'); (new Uri())->withPort(100000); } - public function testWithPortCannotBeZero() + public function testWithPortCannotBeNegative() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Invalid port: 0. Must be between 1 and 65535'); + $this->expectExceptionMessage('Invalid port: -1. Must be between 0 and 65535'); - (new Uri())->withPort(0); + (new Uri())->withPort(-1); } public function testParseUriPortCannotBeZero() From cd09dbed2d57e7e519530cceb1477ccede72d5fb Mon Sep 17 00:00:00 2001 From: Martijn van der Ven Date: Fri, 24 May 2019 17:37:33 +0200 Subject: [PATCH 06/84] Fix null reason phrase resulting from Response constructor (#120) * Add breaking test where reason phrase is null * Make sure internal representation of reasonPhrase stays a string --- src/Response.php | 2 +- tests/ResponseTest.php | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Response.php b/src/Response.php index f50f188..a75e93c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -49,7 +49,7 @@ public function __construct(int $status = 200, array $headers = [], $body = null if (null === $reason && isset(self::PHRASES[$this->statusCode])) { $this->reasonPhrase = self::PHRASES[$status]; } else { - $this->reasonPhrase = $reason; + $this->reasonPhrase = $reason ?? ''; } $this->protocol = $version; diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 150da53..58af622 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -30,6 +30,13 @@ public function testCanConstructWithStatusCode() $this->assertSame('Not Found', $r->getReasonPhrase()); } + public function testCanConstructWithUndefinedStatusCode() + { + $r = new Response(999); + $this->assertSame(999, $r->getStatusCode()); + $this->assertSame('', $r->getReasonPhrase()); + } + public function testConstructorDoesNotReadStreamBody() { $body = $this->getMockBuilder(StreamInterface::class)->getMock(); From ad18d9bb5f7a6ed3e703c1c944e97d5866b24cb4 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 21 Jul 2019 10:06:02 +0200 Subject: [PATCH 07/84] Fix checking for seekable stream resources (#121) Userland stream wrappers always return true for the "seekable" metadata, yet this can be wrong. Here is a more accurate check. --- src/Stream.php | 2 +- tests/StreamTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Stream.php b/src/Stream.php index e877e5d..a72ce0a 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -76,7 +76,7 @@ public static function create($body = ''): StreamInterface $new = new self(); $new->stream = $body; $meta = \stream_get_meta_data($new->stream); - $new->seekable = $meta['seekable']; + $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR); $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); $new->uri = $new->getMetadata('uri'); diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 74767fa..5a6d01e 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -160,4 +160,34 @@ public function testCloseClearProperties() $this->assertNull($stream->getSize()); $this->assertEmpty($stream->getMetadata()); } + + public function testUnseekableStreamWrapper() + { + stream_wrapper_register('nyholm-psr7-test', TestStreamWrapper::class); + $handle = fopen('nyholm-psr7-test://', 'r'); + stream_wrapper_unregister('nyholm-psr7-test'); + + $stream = Stream::create($handle); + $this->assertFalse($stream->isSeekable()); + } +} + +class TestStreamWrapper +{ + public $context; + + public function stream_open() + { + return true; + } + + public function stream_seek(int $offset, int $whence = SEEK_SET) + { + return false; + } + + public function stream_eof() + { + return true; + } } From 8e64aec534ca8efd57fa4ce973871beaa8d47653 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 15 Aug 2019 11:02:32 +0200 Subject: [PATCH 08/84] Added test to make sure we can create response with empty reason phrase (#122) * Added test to make sure we can create response with empty reason phrase * Allow use of PSR17 factory to get response phrase * Updated tests --- src/Factory/Psr17Factory.php | 5 ++++ tests/Factory/Psr17FactoryTest.php | 38 ++++++++++++++++++++++++++++++ tests/ResponseTest.php | 7 ++++++ 3 files changed, 50 insertions(+) create mode 100644 tests/Factory/Psr17FactoryTest.php diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 33130ce..08caf85 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -20,6 +20,11 @@ public function createRequest(string $method, $uri): RequestInterface public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface { + if (2 > \func_num_args()) { + // This will make the Response class to use a custom reasonPhrase + $reasonPhrase = null; + } + return new Response($code, [], null, '1.1', $reasonPhrase); } diff --git a/tests/Factory/Psr17FactoryTest.php b/tests/Factory/Psr17FactoryTest.php new file mode 100644 index 0000000..9ba03de --- /dev/null +++ b/tests/Factory/Psr17FactoryTest.php @@ -0,0 +1,38 @@ +createResponse(200); + $this->assertEquals('OK', $r->getReasonPhrase()); + + $r = $factory->createResponse(200, ''); + $this->assertEquals('', $r->getReasonPhrase()); + + $r = $factory->createResponse(200, 'Foo'); + $this->assertEquals('Foo', $r->getReasonPhrase()); + + /* + * Test for non-standard response codes + */ + $r = $factory->createResponse(567); + $this->assertEquals('', $r->getReasonPhrase()); + + $r = $factory->createResponse(567, ''); + $this->assertEquals(567, $r->getStatusCode()); + $this->assertEquals('', $r->getReasonPhrase()); + + $r = $factory->createResponse(567, 'Foo'); + $this->assertEquals(567, $r->getStatusCode()); + $this->assertEquals('Foo', $r->getReasonPhrase()); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 58af622..e6e109e 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -37,6 +37,13 @@ public function testCanConstructWithUndefinedStatusCode() $this->assertSame('', $r->getReasonPhrase()); } + public function testCanConstructWithStatusCodeAndEmptyReason() + { + $r = new Response(404, [], null, '1.1', ''); + $this->assertSame(404, $r->getStatusCode()); + $this->assertSame('', $r->getReasonPhrase()); + } + public function testConstructorDoesNotReadStreamBody() { $body = $this->getMockBuilder(StreamInterface::class)->getMock(); From 508c9472df9466ee2fe768cfbe3396d79c2572af Mon Sep 17 00:00:00 2001 From: Nyholm Date: Thu, 22 Aug 2019 16:23:42 +0200 Subject: [PATCH 09/84] Added Github Action --- .github/workflows/bc.yml | 10 ++++++++++ .github/workflows/static.yml | 22 ++++++++++++++++++++++ .travis.yml | 6 ------ 3 files changed, 32 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/bc.yml create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml new file mode 100644 index 0000000..685675d --- /dev/null +++ b/.github/workflows/bc.yml @@ -0,0 +1,10 @@ +on: [push] +name: BC +jobs: + roave_bc_check: + name: Roave BC Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Roave BC Check + uses: docker://nyholm/roave-bc-check-ga \ No newline at end of file diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 0000000..be575c9 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,22 @@ +on: [push] +name: Static analysis +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + with: + args: analyze --no-progress + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: PHP-CS-Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --dry-run --diff-format udiff diff --git a/.travis.yml b/.travis.yml index 46e0afa..68c4184 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,15 +24,9 @@ matrix: - php: nightly env: COMPOSER_FLAGS="--ignore-platform-reqs" include: - - php: 7.2 - name: Backward compatibillity check - env: DEPENDENCIES="roave/backward-compatibility-check" TEST_COMMAND="./vendor/bin/roave-backward-compatibility-check" - php: 7.2 name: Lowest version of dependencies env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - - php: 7.2 - name: PHPStan - env: DEPENDENCIES="phpstan/phpstan" TEST_COMMAND="./vendor/bin/phpstan" - php: nightly name: PHP 8.0 env: COMPOSER_FLAGS="--ignore-platform-reqs" From b859bb665e603a54c8e81db9b80051873c6aea34 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Thu, 22 Aug 2019 16:34:47 +0200 Subject: [PATCH 10/84] Fixed PHPStan issue --- phpstan.neon.dist | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/phpstan.neon.dist b/phpstan.neon.dist index df24667..fc19869 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -23,3 +23,7 @@ parameters: - message: '#Result of && is always false#' path: %currentWorkingDirectory%/src/ServerRequest.php + + - + message: '#Result of && is always false#' + path: %currentWorkingDirectory%/src/UploadedFile.php From 147c383fb3f75a8ac87f9a270f7e06af94aa7f2b Mon Sep 17 00:00:00 2001 From: Nyholm Date: Thu, 22 Aug 2019 16:35:57 +0200 Subject: [PATCH 11/84] Rename --- .github/workflows/bc.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 685675d..4547023 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -1,10 +1,10 @@ on: [push] -name: BC +name: Roave jobs: roave_bc_check: - name: Roave BC Check + name: BC Check runs-on: ubuntu-latest steps: - uses: actions/checkout@master - name: Roave BC Check - uses: docker://nyholm/roave-bc-check-ga \ No newline at end of file + uses: docker://nyholm/roave-bc-check-ga From 27c828a165ff936f9fe9056baa9cabe0c70a423c Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 22 Aug 2019 20:22:08 +0200 Subject: [PATCH 12/84] Prepare 1.2.0 (#123) --- CHANGELOG.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fa09be0..fe68b75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.2.0 + +### Changed + +- Change minimal port number to 0 (unix socket) +- Updated `Psr17Factory::createResponse` to respect the specification. If second + argument is not used, a standard reason phrase. If an empty string is passed, + then the reason phrase will be empty. + +### Fixed + +- Check for seekable on the stream resource. +- Fixed the `Response::$reason` should never be null. + ## 1.1.0 ### Added From 713642941e9bbfd39403a4c6eb4f7f66752f43f7 Mon Sep 17 00:00:00 2001 From: Sam Reed Date: Thu, 5 Sep 2019 14:18:17 +0100 Subject: [PATCH 13/84] Add .github to .gitattributes (#126) Remove . prefix from phpstan.neon.dist Fixes https://github.com/Nyholm/psr7/issues/125 --- .gitattributes | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index f235654..8266b04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ +.github/ export-ignore tests/ export-ignore .editorconfig export-ignore .gitattributes export-ignore @@ -5,5 +6,5 @@ tests/ export-ignore .php_cs export-ignore .scrutinizer.yml export-ignore .travis.yml export-ignore -.phpstan.neon.dist export-ignore +phpstan.neon.dist export-ignore phpunit.xml.dist export-ignore From 55ff6b76573f5b242554c9775792bd59fb52e11c Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 5 Sep 2019 15:24:16 +0200 Subject: [PATCH 14/84] Added changelog for 1.2.1 (#127) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe68b75..28f17af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.2.1 + +### Changed + +- Added `.github` and `phpstan.neon.dist` to `.gitattributes`. + ## 1.2.0 ### Changed From d38874cbc289d25a309cefd04379e24352ced608 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 30 Jan 2020 15:38:31 +0100 Subject: [PATCH 15/84] Create FUNDING.yml --- .github/FUNDING.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..11255a0 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,3 @@ +# These are supported funding model platforms + +github: [nyholm, Zegnat] From 38638110ec7a0a7103b2b9f0b2bc6f0d22ef3c9b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 21 Apr 2020 20:26:29 +0200 Subject: [PATCH 16/84] Trying to fix CI on master (#145) * Trying to fix CI on master * cs fix --- composer.json | 2 +- src/Stream.php | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/composer.json b/composer.json index 569cf14..33ba50f 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ }, "require-dev": { "phpunit/phpunit": "^7.5", - "php-http/psr7-integration-tests": "dev-master", + "php-http/psr7-integration-tests": "^1.0", "http-interop/http-factory-tests": "dev-master" }, "provide": { diff --git a/src/Stream.php b/src/Stream.php index a72ce0a..c82c93c 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -56,8 +56,6 @@ private function __construct() * * @param string|resource|StreamInterface $body * - * @return StreamInterface - * * @throws \InvalidArgumentException */ public static function create($body = ''): StreamInterface From 47d6826e1a5007e6637cd05436f5c57f00c1031b Mon Sep 17 00:00:00 2001 From: Andrey Bolonin Date: Tue, 21 Apr 2020 21:27:02 +0300 Subject: [PATCH 17/84] add php 7.4 (#130) * add php 7.4 * upd php 7.4 image Co-Authored-By: Vincent Co-authored-by: Vincent --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 68c4184..2902a35 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,6 +9,7 @@ php: - 7.1 - 7.2 - 7.3 + - 7.4 env: global: From 89ca021ba859be1950245369737bfc124dd37170 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 20 Apr 2020 22:27:22 +0200 Subject: [PATCH 18/84] Add locale-independent lowercasing --- src/LowercaseTrait.php | 20 ++++++++++++++++++++ src/MessageTrait.php | 12 +++++++----- src/Uri.php | 10 ++++++---- 3 files changed, 33 insertions(+), 9 deletions(-) create mode 100644 src/LowercaseTrait.php diff --git a/src/LowercaseTrait.php b/src/LowercaseTrait.php new file mode 100644 index 0000000..1a16ebd --- /dev/null +++ b/src/LowercaseTrait.php @@ -0,0 +1,20 @@ + + * + * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise + */ +trait LowercaseTrait +{ + private static function lowercase(string $value): string + { + return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + } +} diff --git a/src/MessageTrait.php b/src/MessageTrait.php index d1e93cc..0f7635d 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -17,6 +17,8 @@ */ trait MessageTrait { + use LowercaseTrait; + /** @var array Map of all registered headers, as original name => array of values */ private $headers = []; @@ -53,12 +55,12 @@ public function getHeaders(): array public function hasHeader($header): bool { - return isset($this->headerNames[\strtolower($header)]); + return isset($this->headerNames[self::lowercase($header)]); } public function getHeader($header): array { - $header = \strtolower($header); + $header = self::lowercase($header); if (!isset($this->headerNames[$header])) { return []; } @@ -76,7 +78,7 @@ public function getHeaderLine($header): string public function withHeader($header, $value): self { $value = $this->validateAndTrimHeader($header, $value); - $normalized = \strtolower($header); + $normalized = self::lowercase($header); $new = clone $this; if (isset($new->headerNames[$normalized])) { @@ -102,7 +104,7 @@ public function withAddedHeader($header, $value): self public function withoutHeader($header): self { - $normalized = \strtolower($header); + $normalized = self::lowercase($header); if (!isset($this->headerNames[$normalized])) { return $this; } @@ -139,7 +141,7 @@ private function setHeaders(array $headers): void { foreach ($headers as $header => $value) { $value = $this->validateAndTrimHeader($header, $value); - $normalized = \strtolower($header); + $normalized = self::lowercase($header); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = \array_merge($this->headers[$header], $value); diff --git a/src/Uri.php b/src/Uri.php index d67c078..d8bb2ed 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -17,6 +17,8 @@ */ final class Uri implements UriInterface { + use LowercaseTrait; + private const SCHEMES = ['http' => 80, 'https' => 443]; private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; @@ -52,9 +54,9 @@ public function __construct(string $uri = '') } // Apply parse_url parts to a URI. - $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : ''; + $this->scheme = isset($parts['scheme']) ? self::lowercase($parts['scheme']) : ''; $this->userInfo = $parts['user'] ?? ''; - $this->host = isset($parts['host']) ? \strtolower($parts['host']) : ''; + $this->host = isset($parts['host']) ? self::lowercase($parts['host']) : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; @@ -129,7 +131,7 @@ public function withScheme($scheme): self throw new \InvalidArgumentException('Scheme must be a string'); } - if ($this->scheme === $scheme = \strtolower($scheme)) { + if ($this->scheme === $scheme = self::lowercase($scheme)) { return $this; } @@ -163,7 +165,7 @@ public function withHost($host): self throw new \InvalidArgumentException('Host must be a string'); } - if ($this->host === $host = \strtolower($host)) { + if ($this->host === $host = self::lowercase($host)) { return $this; } From e6bd518c8871d9d1bd506aee9d1554c48e78b693 Mon Sep 17 00:00:00 2001 From: Chris Seufert Date: Fri, 4 Oct 2019 09:00:38 +1000 Subject: [PATCH 19/84] Added test for UTF-8 hostnames --- tests/UriTest.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/UriTest.php b/tests/UriTest.php index 2d37623..6a8be12 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -471,4 +471,13 @@ public function testImmutability() $this->assertNotSame($uri, $uri->withQuery('q=abc')); $this->assertNotSame($uri, $uri->withFragment('test')); } + + public function testUtf8Host() + { + $uri = new Uri('http://ουτοπία.δπθ.gr/'); + $this->assertSame('ουτοπία.δπθ.gr', $uri->getHost()); + $new = $uri->withHost('程式设计.com'); + $this->assertSame('程式设计.com', $new->getHost()); + } + } From 45de4ac0988f5548771ea8de01b98d3226743e59 Mon Sep 17 00:00:00 2001 From: Martijn van der Ven Date: Tue, 22 Oct 2019 20:00:33 +0200 Subject: [PATCH 20/84] Introduce breaking test for unicode hostnames Using an old ICANN test domain name for internationalized domain names, hopefully not clashing with anything that actually exists. Systems that use a C or POSIX locale seem unable to reproduce this, so try to set a more common locale on these. Continuation of #131. --- tests/UriTest.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/UriTest.php b/tests/UriTest.php index 6a8be12..fc7cfe7 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -295,6 +295,23 @@ public function testHostIsNormalizedToLowercase() $this->assertSame('//example.com', (string) $uri); } + public function testHostNormalizationLeavesUnicodeIntact() + { + $resetLocale = false; + if (\in_array($currentLocale = \setlocale(\LC_CTYPE, '0'), ['POSIX', 'C'])) { + $resetLocale = \setlocale(\LC_CTYPE, ['en_US.UTF-8', 'en_US.US-ASCII', 'en_US']); + } + + $testDomain = 'παράδειγμα.δοκιμή'; + $uri = (new Uri())->withHost($testDomain); + $this->assertSame($testDomain, $uri->getHost()); + $this->assertSame('//' . $testDomain, (string) $uri); + + if (false !== $resetLocale) { + \setlocale(\LC_CTYPE, $currentLocale); + } + } + public function testPortIsNullIfStandardPortForScheme() { // HTTPS standard port From be30ccfbc669b51630757113d732f83fa323bf11 Mon Sep 17 00:00:00 2001 From: Nyholm Date: Tue, 21 Apr 2020 20:35:17 +0200 Subject: [PATCH 21/84] Moved tests and fixed CS --- src/LowercaseTrait.php | 2 +- tests/UriTest.php | 25 ++++++------------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/src/LowercaseTrait.php b/src/LowercaseTrait.php index 1a16ebd..dfc5031 100644 --- a/src/LowercaseTrait.php +++ b/src/LowercaseTrait.php @@ -15,6 +15,6 @@ trait LowercaseTrait { private static function lowercase(string $value): string { - return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + return \strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); } } diff --git a/tests/UriTest.php b/tests/UriTest.php index fc7cfe7..d5f52fa 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -295,23 +295,6 @@ public function testHostIsNormalizedToLowercase() $this->assertSame('//example.com', (string) $uri); } - public function testHostNormalizationLeavesUnicodeIntact() - { - $resetLocale = false; - if (\in_array($currentLocale = \setlocale(\LC_CTYPE, '0'), ['POSIX', 'C'])) { - $resetLocale = \setlocale(\LC_CTYPE, ['en_US.UTF-8', 'en_US.US-ASCII', 'en_US']); - } - - $testDomain = 'παράδειγμα.δοκιμή'; - $uri = (new Uri())->withHost($testDomain); - $this->assertSame($testDomain, $uri->getHost()); - $this->assertSame('//' . $testDomain, (string) $uri); - - if (false !== $resetLocale) { - \setlocale(\LC_CTYPE, $currentLocale); - } - } - public function testPortIsNullIfStandardPortForScheme() { // HTTPS standard port @@ -488,13 +471,17 @@ public function testImmutability() $this->assertNotSame($uri, $uri->withQuery('q=abc')); $this->assertNotSame($uri, $uri->withFragment('test')); } - + public function testUtf8Host() { $uri = new Uri('http://ουτοπία.δπθ.gr/'); $this->assertSame('ουτοπία.δπθ.gr', $uri->getHost()); $new = $uri->withHost('程式设计.com'); $this->assertSame('程式设计.com', $new->getHost()); - } + $testDomain = 'παράδειγμα.δοκιμή'; + $uri = (new Uri())->withHost($testDomain); + $this->assertSame($testDomain, $uri->getHost()); + $this->assertSame('//' . $testDomain, (string) $uri); + } } From 9f68527d39f9bdd9d38b1257c68e33fd53eaf09d Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 21 Apr 2020 20:44:06 +0200 Subject: [PATCH 22/84] Make __toString() compatible with throwing (#144) --- composer.json | 3 ++- src/Stream.php | 22 ++++++++++++++++++++-- tests/StreamTest.php | 17 ++++++++++++++++- 3 files changed, 38 insertions(+), 4 deletions(-) diff --git a/composer.json b/composer.json index 33ba50f..d4c2f59 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ "require-dev": { "phpunit/phpunit": "^7.5", "php-http/psr7-integration-tests": "^1.0", - "http-interop/http-factory-tests": "dev-master" + "http-interop/http-factory-tests": "dev-master", + "symfony/error-handler": "^4.4" }, "provide": { "psr/http-message-implementation": "1.0", diff --git a/src/Stream.php b/src/Stream.php index c82c93c..0dd8a2b 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -5,6 +5,8 @@ namespace Nyholm\Psr7; use Psr\Http\Message\StreamInterface; +use Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler; +use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler; /** * @author Michael Dowling and contributors to guzzlehttp/psr7 @@ -93,7 +95,10 @@ public function __destruct() $this->close(); } - public function __toString(): string + /** + * @return string + */ + public function __toString() { try { if ($this->isSeekable()) { @@ -101,7 +106,20 @@ public function __toString(): string } return $this->getContents(); - } catch (\Exception $e) { + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + + if (\is_array($errorHandler = \set_error_handler('var_dump'))) { + $errorHandler = $errorHandler[0] ?? null; + } + \restore_error_handler(); + + if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { + return \trigger_error((string) $e, \E_USER_ERROR); + } + return ''; } } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 5a6d01e..74898e9 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -4,6 +4,7 @@ use Nyholm\Psr7\Stream; use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler; /** * @covers \Nyholm\Psr7\Stream @@ -144,7 +145,21 @@ public function testCanDetachStream() $throws(function ($stream) { $stream->getContents(); }); - $this->assertSame('', (string) $stream); + if (\PHP_VERSION_ID >= 70400) { + $throws(function ($stream) { + (string) $stream; + }); + } else { + $this->assertSame('', (string) $stream); + + SymfonyErrorHandler::register(); + $throws(function ($stream) { + (string) $stream; + }); + restore_error_handler(); + restore_exception_handler(); + } + $stream->close(); } From a4a26bb156f88586c8cccf469040d3096c5f79e2 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 21 Apr 2020 21:45:08 +0200 Subject: [PATCH 23/84] Run CI on pull_requests too (#146) * Run CI on pull_requests to * Moved ignoredErrors to seperate file * typo --- .github/workflows/bc.yml | 2 +- .github/workflows/static.yml | 4 +++- phpstan.baseline.dist | 42 ++++++++++++++++++++++++++++++++++++ phpstan.neon.dist | 28 +++--------------------- 4 files changed, 49 insertions(+), 27 deletions(-) create mode 100644 phpstan.baseline.dist diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 4547023..2d288ed 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -1,4 +1,4 @@ -on: [push] +on: [push, pull_request] name: Roave jobs: roave_bc_check: diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index be575c9..5b84712 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,4 +1,4 @@ -on: [push] +on: [push, pull_request] name: Static analysis jobs: phpstan: @@ -8,6 +8,8 @@ jobs: - uses: actions/checkout@master - name: PHPStan uses: docker://oskarstark/phpstan-ga + env: + REQUIRE_DEV: true with: args: analyze --no-progress diff --git a/phpstan.baseline.dist b/phpstan.baseline.dist new file mode 100644 index 0000000..56a1c07 --- /dev/null +++ b/phpstan.baseline.dist @@ -0,0 +1,42 @@ +parameters: + ignoreErrors: + - + message: "#^Result of && is always false\\.$#" + count: 1 + path: src/Response.php + + - + message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#" + count: 1 + path: src/Response.php + + - + message: "#^Result of && is always false\\.$#" + count: 1 + path: src/ServerRequest.php + + - + message: "#^Strict comparison using \\!\\=\\= between null and null will always evaluate to false\\.$#" + count: 1 + path: src/ServerRequest.php + + - + message: "#^Parameter \\#1 \\$error_handler of function set_error_handler expects \\(callable\\(int, string, string, int, array\\)\\: bool\\)\\|null, 'var_dump' given\\.$#" + count: 1 + path: src/Stream.php + + - + message: "#^Method Nyholm\\\\Psr7\\\\Stream\\:\\:__toString\\(\\) should return string but returns bool\\.$#" + count: 1 + path: src/Stream.php + + - + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" + count: 2 + path: src/UploadedFile.php + + - + message: "#^Result of && is always false\\.$#" + count: 2 + path: src/UploadedFile.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist index fc19869..0628e93 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,29 +1,7 @@ +includes: + - phpstan.baseline.dist + parameters: level: 5 paths: - src - - ignoreErrors: - - - message: '#Strict comparison using === between false and true will always evaluate to false#' - path: %currentWorkingDirectory%/src/UploadedFile.php - - - - message: '#Strict comparison using === between null and string will always evaluate to false#' - path: %currentWorkingDirectory%/src/Response.php - - - - message: '#Result of && is always false.#' - path: %currentWorkingDirectory%/src/Response.php - - - - message: '#Strict comparison using !== between null and null will always evaluate to false#' - path: %currentWorkingDirectory%/src/ServerRequest.php - - - - message: '#Result of && is always false#' - path: %currentWorkingDirectory%/src/ServerRequest.php - - - - message: '#Result of && is always false#' - path: %currentWorkingDirectory%/src/UploadedFile.php From 3f71c198de0b5e8fb976847c951c938624f37ba2 Mon Sep 17 00:00:00 2001 From: Nicolai Cornelis Date: Tue, 28 Apr 2020 18:06:58 +0200 Subject: [PATCH 24/84] Added support for numeric header names (#149) * Added support for numeric header names * Added assertions that test getHeader and getHeaderLine for HTTP_0 header case Co-authored-by: Nicolai Cornelis --- src/MessageTrait.php | 5 +++++ tests/RequestTest.php | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 0f7635d..2da949d 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -140,6 +140,11 @@ public function withBody(StreamInterface $body): self private function setHeaders(array $headers): void { foreach ($headers as $header => $value) { + if (\is_int($header)) { + // If a header name was set to a numeric string, PHP will cast the key to an int. + // We must cast it back to a string in order to comply with validation. + $header = (string) $header; + } $value = $this->validateAndTrimHeader($header, $value); $normalized = self::lowercase($header); if (isset($this->headerNames[$normalized])) { diff --git a/tests/RequestTest.php b/tests/RequestTest.php index e357e03..ddac6d2 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -202,6 +202,51 @@ public function testSupportNumericHeaders() $this->assertSame('200', $r->getHeaderLine('Content-Length')); } + public function testSupportNumericHeaderNames() + { + $r = new Request( + 'GET', '', [ + '200' => 'NumericHeaderValue', + '0' => 'NumericHeaderValueZero', + ] + ); + + $this->assertSame( + [ + '200' => ['NumericHeaderValue'], + '0' => ['NumericHeaderValueZero'], + ], + $r->getHeaders() + ); + + $this->assertSame(['NumericHeaderValue'], $r->getHeader('200')); + $this->assertSame('NumericHeaderValue', $r->getHeaderLine('200')); + + $this->assertSame(['NumericHeaderValueZero'], $r->getHeader('0')); + $this->assertSame('NumericHeaderValueZero', $r->getHeaderLine('0')); + + $r = $r->withHeader('300', 'NumericHeaderValue2') + ->withAddedHeader('200', ['A', 'B']); + + $this->assertSame( + [ + '200' => ['NumericHeaderValue', 'A', 'B'], + '0' => ['NumericHeaderValueZero'], + '300' => ['NumericHeaderValue2'], + ], + $r->getHeaders() + ); + + $r = $r->withoutHeader('300'); + $this->assertSame( + [ + '200' => ['NumericHeaderValue', 'A', 'B'], + '0' => ['NumericHeaderValueZero'], + ], + $r->getHeaders() + ); + } + public function testAddsPortToHeader() { $r = new Request('GET', 'http://foo.com:8124/bar'); From c17f4f73985f62054a331cbc4ffdf9868c4ef256 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 23 May 2020 13:29:07 +0200 Subject: [PATCH 25/84] Prepare 1.3.0 (#147) * Prepare 1.3.0 * Mention fix of numeric headers in CHANGELOG Co-authored-by: Martijn van der Ven --- CHANGELOG.md | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28f17af..ec6795e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.3.0 + +### Added + +- Make Stream::__toString() compatible with throwing exceptions on PHP 7.4. + +### Fixed + +- Support for UTF-8 hostnames +- Support for numeric header names + ## 1.2.1 ### Changed @@ -13,9 +24,9 @@ All notable changes to this project will be documented in this file, in reverse ### Changed - Change minimal port number to 0 (unix socket) -- Updated `Psr17Factory::createResponse` to respect the specification. If second - argument is not used, a standard reason phrase. If an empty string is passed, - then the reason phrase will be empty. +- Updated `Psr17Factory::createResponse` to respect the specification. If second + argument is not used, a standard reason phrase. If an empty string is passed, + then the reason phrase will be empty. ### Fixed @@ -38,7 +49,7 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed - Handle `fopen` failing in createStreamFromFile according to PSR-7. -- Reduce execution path to speed up performance. +- Reduce execution path to speed up performance. - Fixed typos. - Code style. @@ -47,10 +58,10 @@ All notable changes to this project will be documented in this file, in reverse ### Added - Support for final PSR-17 (HTTP factories). (`Psr17Factory`) -- Support for numeric header values. -- Support for empty header values. +- Support for numeric header values. +- Support for empty header values. - All classes are final -- `HttplugFactory` that implements factory interfaces from HTTPlug. +- `HttplugFactory` that implements factory interfaces from HTTPlug. ### Changed @@ -59,25 +70,25 @@ All notable changes to this project will be documented in this file, in reverse ### Removed - The HTTPlug discovery strategy was removed since it is included in php-http/discovery 1.4. -- `UploadedFileFactory()` was removed in favor for `Psr17Factory`. -- `ServerRequestFactory()` was removed in favor for `Psr17Factory`. -- `StreamFactory`, `UriFactory`, abd `MessageFactory`. Use `HttplugFactory` instead. -- `ServerRequestFactory::createServerRequestFromArray`, `ServerRequestFactory::createServerRequestFromArrays` and - `ServerRequestFactory::createServerRequestFromGlobals`. Please use the new `nyholm/psr7-server` instead. +- `UploadedFileFactory()` was removed in favor for `Psr17Factory`. +- `ServerRequestFactory()` was removed in favor for `Psr17Factory`. +- `StreamFactory`, `UriFactory`, abd `MessageFactory`. Use `HttplugFactory` instead. +- `ServerRequestFactory::createServerRequestFromArray`, `ServerRequestFactory::createServerRequestFromArrays` and + `ServerRequestFactory::createServerRequestFromGlobals`. Please use the new `nyholm/psr7-server` instead. ## 0.3.0 ### Added - Return types. -- Many `InvalidArgumentException`s are thrown when you use invalid arguments. +- Many `InvalidArgumentException`s are thrown when you use invalid arguments. - Integration tests for `UploadedFile` and `ServerRequest`. ### Changed -- We dropped PHP7.0 support. -- PSR-17 factories have been marked as internal. They do not fall under our BC promise until PSR-17 is accepted. -- `UploadedFileFactory::createUploadedFile` does not accept a string file path. +- We dropped PHP7.0 support. +- PSR-17 factories have been marked as internal. They do not fall under our BC promise until PSR-17 is accepted. +- `UploadedFileFactory::createUploadedFile` does not accept a string file path. ## 0.2.3 From 21b71a31eab5c0c2caf967b9c0fd97020254ed75 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 13 Jun 2020 17:59:10 +0200 Subject: [PATCH 26/84] Allow the package to be installed on PHP8 (#151) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index d4c2f59..d8772f8 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": "^7.1", + "php": ">=7.1", "psr/http-message": "^1.0", "php-http/message-factory": "^1.0", "psr/http-factory": "^1.0" From d1d899fe05d3de5a57f440ef40fd2f3f9c440f8d Mon Sep 17 00:00:00 2001 From: Hugo Date: Fri, 25 Sep 2020 11:02:25 +0200 Subject: [PATCH 27/84] fix throw exception on read stream (#154) * fix throw exception on read stream * remove test on read return false --- src/Stream.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Stream.php b/src/Stream.php index 0dd8a2b..2b62e1c 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -240,7 +240,11 @@ public function read($length): string throw new \RuntimeException('Cannot read from non-readable stream'); } - return \fread($this->stream, $length); + if (false === $result = \fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream'); + } + + return $result; } public function getContents(): string From f4b597dd940d0bf567897d3e8a50cd4d94a2d5cf Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 1 Oct 2020 15:56:52 +0200 Subject: [PATCH 28/84] Improve exception message (#157) * Improve error message * cs --- src/Response.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Response.php b/src/Response.php index a75e93c..c2020f4 100644 --- a/src/Response.php +++ b/src/Response.php @@ -73,7 +73,7 @@ public function withStatus($code, $reasonPhrase = ''): self $code = (int) $code; if ($code < 100 || $code > 599) { - throw new \InvalidArgumentException('Status code has to be an integer between 100 and 599'); + throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); } $new = clone $this; From ed734c4553c1d57b85b6af0824c8ebca02c50313 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 1 Oct 2020 15:57:17 +0200 Subject: [PATCH 29/84] Added psalm action (#155) --- .github/workflows/static.yml | 33 +++++++++++++++++++++++++++------ psalm.xml | 15 +++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 psalm.xml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5b84712..a19492b 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -4,21 +4,42 @@ jobs: phpstan: name: PHPStan runs-on: ubuntu-latest + steps: - - uses: actions/checkout@master + - name: Checkout code + uses: actions/checkout@v2 + + - name: Download dependencies + run: | + composer update --no-interaction --prefer-dist --optimize-autoloader + - name: PHPStan - uses: docker://oskarstark/phpstan-ga - env: - REQUIRE_DEV: true + uses: docker://oskarstark/phpstan-ga:0.12.44 with: + entrypoint: /composer/vendor/bin/phpstan args: analyze --no-progress php-cs-fixer: name: PHP-CS-Fixer runs-on: ubuntu-latest + steps: - - uses: actions/checkout@master + - name: Checkout code + uses: actions/checkout@v2 + - name: PHP-CS-Fixer - uses: docker://oskarstark/php-cs-fixer-ga + uses: OskarStark/php-cs-fixer-ga@2.16.4.1 with: args: --dry-run --diff-format udiff + + psalm: + name: Psalm + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Psalm + uses: psalm/psalm-github-actions@master + with: + args: --no-progress --show-info=false --stats diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..d234691 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,15 @@ + + + + + + + + + From 66e6f494374289d858bf75c7bc51550d0a80b908 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 14 Nov 2020 13:01:11 +0100 Subject: [PATCH 30/84] Make sure CI is green (#159) * ci fixes * minors * cs * fix * Test that port can be zero * Version fix --- .github/workflows/static.yml | 10 +++++++--- composer.json | 2 +- tests/UriTest.php | 14 ++++++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index a19492b..0089206 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -14,7 +14,9 @@ jobs: composer update --no-interaction --prefer-dist --optimize-autoloader - name: PHPStan - uses: docker://oskarstark/phpstan-ga:0.12.44 + uses: docker://oskarstark/phpstan-ga:0.12.48 + env: + REQUIRE_DEV: true with: entrypoint: /composer/vendor/bin/phpstan args: analyze --no-progress @@ -28,7 +30,7 @@ jobs: uses: actions/checkout@v2 - name: PHP-CS-Fixer - uses: OskarStark/php-cs-fixer-ga@2.16.4.1 + uses: OskarStark/php-cs-fixer-ga@2.16.4 with: args: --dry-run --diff-format udiff @@ -40,6 +42,8 @@ jobs: uses: actions/checkout@v2 - name: Psalm - uses: psalm/psalm-github-actions@master + uses: docker://vimeo/psalm-github-actions:3.17.2 + env: + REQUIRE_DEV: true with: args: --no-progress --show-info=false --stats diff --git a/composer.json b/composer.json index d8772f8..c016cb9 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require-dev": { "phpunit/phpunit": "^7.5", "php-http/psr7-integration-tests": "^1.0", - "http-interop/http-factory-tests": "dev-master", + "http-interop/http-factory-tests": "^0.8", "symfony/error-handler": "^4.4" }, "provide": { diff --git a/tests/UriTest.php b/tests/UriTest.php index d5f52fa..bb165e8 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -126,12 +126,22 @@ public function testWithPortCannotBeNegative() (new Uri())->withPort(-1); } - public function testParseUriPortCannotBeZero() + public function testParseUriPortCannotBeNegative() { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Unable to parse URI'); - new Uri('//example.com:0'); + new Uri('//example.com:-1'); + } + + public function testParseUriPortCanBeZero() + { + if (version_compare(PHP_VERSION, '7.4.12') < 0) { + self::markTestSkipped('Skipping this on low PHP versions.'); + } + + $uri = new Uri('//example.com:0'); + $this->assertEquals(0, $uri->getPort()); } public function testSchemeMustHaveCorrectType() From 1e712e8c926c52265c5fd21069991de9f23cffe8 Mon Sep 17 00:00:00 2001 From: DavidPrevot Date: Sat, 14 Nov 2020 08:08:07 -0400 Subject: [PATCH 31/84] Compatibility with recent PHPUnit (8) (#132) * Compatibility with recent PHPUnit * Allow phpunit 8 Co-authored-by: Nyholm --- composer.json | 2 +- tests/UploadedFileTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/composer.json b/composer.json index c016cb9..3468db5 100644 --- a/composer.json +++ b/composer.json @@ -21,7 +21,7 @@ "psr/http-factory": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.5", + "phpunit/phpunit": "^7.5 || 8.5", "php-http/psr7-integration-tests": "^1.0", "http-interop/http-factory-tests": "^0.8", "symfony/error-handler": "^4.4" diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 4d43352..085967c 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -14,12 +14,12 @@ class UploadedFileTest extends TestCase { protected $cleanup; - public function setUp() + public function setUp(): void { $this->cleanup = []; } - public function tearDown() + public function tearDown(): void { foreach ($this->cleanup as $file) { if (is_scalar($file) && file_exists($file)) { From d0cddd63f0b1a901b3cca26b225e47316b9a5c90 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 14 Nov 2020 13:29:45 +0100 Subject: [PATCH 32/84] Add Github action to automatically update branch alias (#160) --- .github/workflows/branch-alias.yml | 75 ++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 .github/workflows/branch-alias.yml diff --git a/.github/workflows/branch-alias.yml b/.github/workflows/branch-alias.yml new file mode 100644 index 0000000..9c08d7f --- /dev/null +++ b/.github/workflows/branch-alias.yml @@ -0,0 +1,75 @@ +name: Update branch alias + +on: + push: + tags: ['*'] + +jobs: + branch-alias: + name: Update branch alias + runs-on: ubuntu-latest + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + coverage: none + + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: main + + - name: Find branch alias + id: find_alias + run: | + TAG=$(echo $GITHUB_REF | cut -d'/' -f 3) + echo "Last tag was $TAG" + ARR=(${TAG//./ }) + ARR[1]=$((${ARR[1]}+1)) + echo ::set-output name=alias::${ARR[0]}.${ARR[1]} + + - name: Update branch alias + run: | + CURRENT_ALIAS=$(composer config extra.branch-alias.dev-main | cut -d'-' -f 1) + + # If there is a current value on the branch alias + if [ ! -z $CURRENT_ALIAS ]; then + NEW_ALIAS=${{ steps.find_alias.outputs.alias }} + CURRENT_ARR=(${CURRENT_ALIAS//./ }) + NEW_ARR=(${NEW_ALIAS//./ }) + + if [ ${CURRENT_ARR[0]} -gt ${NEW_ARR[0]} ]; then + echo "The current value for major version is larger" + exit 1; + fi + + if [ ${CURRENT_ARR[0]} -eq ${NEW_ARR[0]} ] && [ ${CURRENT_ARR[1]} -gt ${NEW_ARR[1]} ]; then + echo "The current value for minor version is larger" + exit 1; + fi + fi + + composer config extra.branch-alias.dev-main ${{ steps.find_alias.outputs.alias }}-dev + + - name: Commit & push the new files + run: | + echo "::group::git status" + git status + echo "::endgroup::" + + git add -N . + if [[ $(git diff --numstat | wc -l) -eq 0 ]]; then + echo "No changes found. Exiting." + exit 0; + fi + + git config --local user.email "noreply@github.com" + git config --local user.name "GitHub" + + echo "::group::git push" + git add . + git commit -m "Update branch alias" + git push + echo "::endgroup::" From ef9492e7ad73b894ef8d8def23c201aa7c20f107 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 14 Nov 2020 13:44:14 +0100 Subject: [PATCH 33/84] Changes to meta files and support for PHPUnit 9 (#161) * Only support PHPUnit 8.5 and 9.4 * Updated meta files * Make sure we support PHP 7.1 * MIgrate config * Cleanup .travis.yml and cure my OCD =) --- .gitattributes | 21 +++++++++++---------- .gitignore | 10 ++++++---- .travis.yml | 20 +++++++------------- composer.json | 8 ++------ phpunit.xml.dist | 47 ++++++++++++++++++++++------------------------- 5 files changed, 48 insertions(+), 58 deletions(-) diff --git a/.gitattributes b/.gitattributes index 8266b04..fa51164 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,10 +1,11 @@ -.github/ export-ignore -tests/ export-ignore -.editorconfig export-ignore -.gitattributes export-ignore -.gitignore export-ignore -.php_cs export-ignore -.scrutinizer.yml export-ignore -.travis.yml export-ignore -phpstan.neon.dist export-ignore -phpunit.xml.dist export-ignore +.github/ export-ignore +tests/ export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +.gitignore export-ignore +.php_cs export-ignore +.scrutinizer.yml export-ignore +.travis.yml export-ignore +phpstan.neon.dist export-ignore +phpstan.baseline.dist export-ignore +phpunit.xml.dist export-ignore diff --git a/.gitignore b/.gitignore index ed486bb..8cf0c7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ -/composer.lock -/phpstan.neon -/phpunit.xml -/vendor/ +composer.lock +phpstan.neon +phpunit.xml +vendor +.php_cs.cache +.phpunit.result.cache diff --git a/.travis.yml b/.travis.yml index 2902a35..908de13 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ php: env: global: - - TEST_COMMAND="composer test" + - TEST_COMMAND="./vendor/bin/phpunit" branches: except: @@ -21,19 +21,13 @@ branches: matrix: fast_finish: true - allow_failures: - - php: nightly - env: COMPOSER_FLAGS="--ignore-platform-reqs" include: - - php: 7.2 - name: Lowest version of dependencies - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="composer test-ci" - - php: nightly - name: PHP 8.0 - env: COMPOSER_FLAGS="--ignore-platform-reqs" - -before_install: - - if ! [ -v "$DEPENDENCIES" ]; then composer require --no-update ${DEPENDENCIES}; fi; + - name: "PHP: 8.0" + php: nightly + + - name: "Lowest version of dependencies" + php: 7.2 + env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="./vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" install: - composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction diff --git a/composer.json b/composer.json index 3468db5..a5b9dc0 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "A fast PHP7 implementation of PSR-7", "license": "MIT", "keywords": ["psr-7", "psr-17"], - "homepage": "http://tnyholm.se", + "homepage": "https://tnyholm.se", "authors": [ { "name": "Tobias Nyholm", @@ -21,7 +21,7 @@ "psr/http-factory": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || 8.5", + "phpunit/phpunit": "^7.5 || 8.5 || 9.4", "php-http/psr7-integration-tests": "^1.0", "http-interop/http-factory-tests": "^0.8", "symfony/error-handler": "^4.4" @@ -40,10 +40,6 @@ "Tests\\Nyholm\\Psr7\\": "tests/" } }, - "scripts": { - "test": "vendor/bin/phpunit", - "test-ci": "vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" - }, "extra": { "branch-alias": { "dev-master": "1.0-dev" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 100a46c..1e5ba2a 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,27 +1,24 @@ - - - - tests/ - - - - ./vendor/http-interop/http-factory-tests/test - - - - - - src/ - - - - - - - - - - - + + + + src/ + + + + + tests/ + + + ./vendor/http-interop/http-factory-tests/test + + + + + + + + + + From 329ee4a715293e721d34775b58bc35c18394c1af Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 14 Nov 2020 13:44:26 +0100 Subject: [PATCH 34/84] Removed PHP7 line (#162) --- README.md | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index d626853..4c53bdf 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,6 @@ A super lightweight PSR-7 implementation. Very strict and very fast. | Description | Guzzle | Zend | Slim | Nyholm | | ---- | ------ | ---- | ---- | ------ | | Lines of code | 3 000 | 3 000 | 1 700 | 1 000 | -| PHP7 | No | Yes | No | Yes | | PSR-7* | 66% | 100% | 75% | 100% | | PSR-17 | No | Yes | Yes | Yes | | HTTPlug | No | No | No | Yes | @@ -30,16 +29,16 @@ A super lightweight PSR-7 implementation. Very strict and very fast. composer require nyholm/psr7 ``` -If you are using Symfony Flex then you get all message factories registered as services. +If you are using Symfony Flex then you get all message factories registered as services. ## Usage The PSR-7 objects do not contain any other public methods than those defined in -the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). +the [PSR-7 specification](https://www.php-fig.org/psr/psr-7/). ### Create objects -Use the PSR-17 factory to create requests, streams, URIs etc. +Use the PSR-17 factory to create requests, streams, URIs etc. ```php $psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); @@ -49,8 +48,8 @@ $stream = $psr17Factory->createStream('foobar'); ### Sending a request -With [HTTPlug](http://httplug.io/) or any other PSR-18 (HTTP client) you may send -requests like: +With [HTTPlug](http://httplug.io/) or any other PSR-18 (HTTP client) you may send +requests like: ```bash composer require kriswallsmith/buzz @@ -66,7 +65,7 @@ $response = $psr18Client->sendRequest($request); ### Create server requests -The [`nyholm/psr7-server`](https://github.com/Nyholm/psr7-server) package can be used +The [`nyholm/psr7-server`](https://github.com/Nyholm/psr7-server) package can be used to create server requests from PHP superglobals. ```bash @@ -102,10 +101,10 @@ $response = $psr17Factory->createResponse(200)->withBody($responseBody); ## Our goal -This package is currently maintained by [Tobias Nyholm](http://nyholm.se) and +This package is currently maintained by [Tobias Nyholm](http://nyholm.se) and [Martijn van der Ven](https://vanderven.se/martijn/). They have decided that the -goal of this library should be to provide a super strict implementation of -[PSR-7](https://www.php-fig.org/psr/psr-7/) that is blazing fast. +goal of this library should be to provide a super strict implementation of +[PSR-7](https://www.php-fig.org/psr/psr-7/) that is blazing fast. The package will never include any extra features nor helper methods. All our classes -and functions exist because they are required to fulfill the PSR-7 specification. +and functions exist because they are required to fulfill the PSR-7 specification. From a272953743c454ac4af9626634daaf5ab3ce1173 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 14 Nov 2020 18:35:34 +0100 Subject: [PATCH 35/84] Prepare release 1.3.2 (#158) * Prepare 1.3.1 * Update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ec6795e..2e9cc9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.3.2 + +### Fixed + +- `Stream::read()` must not return boolean. +- Improved exception message when using wrong HTTP status code. + +## 1.3.1 + +### Fixed + +- Allow installation on PHP8 + ## 1.3.0 ### Added From e3f55e44f99647cf5f0c380c578fa3aed00c4b48 Mon Sep 17 00:00:00 2001 From: Marvin Lukaschek Date: Sat, 30 Jan 2021 13:48:32 +0100 Subject: [PATCH 36/84] Rename Zend to Laminas (#165) Rename Zend to Laminas in README.md In January 2020 Zend has moved on to become the Laminas project. (See https://framework.zend.com/blog/2019-04-17-announcing-laminas.html for further reference) This commit renames Zend to Laminas (and updates Packages / Namespaces) in the README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4c53bdf..abe3248 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ A super lightweight PSR-7 implementation. Very strict and very fast. -| Description | Guzzle | Zend | Slim | Nyholm | +| Description | Guzzle | Laminas | Slim | Nyholm | | ---- | ------ | ---- | ---- | ------ | | Lines of code | 3 000 | 3 000 | 1 700 | 1 000 | | PSR-7* | 66% | 100% | 75% | 100% | @@ -88,7 +88,7 @@ $serverRequest = $creator->fromGlobals(); ### Emitting a response ```bash -composer require zendframework/zend-httphandlerrunner +composer require laminas/laminas-httphandlerrunner ``` ```php @@ -96,7 +96,7 @@ $psr17Factory = new \Nyholm\Psr7\Factory\Psr17Factory(); $responseBody = $psr17Factory->createStream('Hello world'); $response = $psr17Factory->createResponse(200)->withBody($responseBody); -(new \Zend\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); +(new \Laminas\HttpHandlerRunner\Emitter\SapiEmitter())->emit($response); ``` ## Our goal From a36fb363bc9f478733bb98e0d42f91a68ad9a274 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 10 Feb 2021 12:06:11 +0100 Subject: [PATCH 37/84] Use github actions for PHPUnit (#170) * Use github actions * Use constant PHP_EOL * minor * Push on master * Push on master * Fixed branch alias --- .github/workflows/bc.yml | 7 ++- .github/workflows/branch-alias.yml | 31 ++++------- .github/workflows/static.yml | 7 ++- .github/workflows/tests.yml | 82 ++++++++++++++++++++++++++++++ .travis.yml | 40 --------------- composer.json | 2 +- phpunit.xml.dist | 5 -- tests/UploadedFileTest.php | 2 +- 8 files changed, 107 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 2d288ed..18b35c8 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -1,4 +1,9 @@ -on: [push, pull_request] +on: + pull_request: ~ + push: + branches: + - "master" + name: Roave jobs: roave_bc_check: diff --git a/.github/workflows/branch-alias.yml b/.github/workflows/branch-alias.yml index 9c08d7f..377cd29 100644 --- a/.github/workflows/branch-alias.yml +++ b/.github/workflows/branch-alias.yml @@ -53,23 +53,14 @@ jobs: composer config extra.branch-alias.dev-main ${{ steps.find_alias.outputs.alias }}-dev - - name: Commit & push the new files - run: | - echo "::group::git status" - git status - echo "::endgroup::" - - git add -N . - if [[ $(git diff --numstat | wc -l) -eq 0 ]]; then - echo "No changes found. Exiting." - exit 0; - fi - - git config --local user.email "noreply@github.com" - git config --local user.name "GitHub" - - echo "::group::git push" - git add . - git commit -m "Update branch alias" - git push - echo "::endgroup::" + - name: Create Pull Request + uses: peter-evans/create-pull-request@v3 + with: + base: main + branch: branch-alias-update + author: GitHub + committer: GitHub + commit-message: Updating branch alias to ${{ steps.find_alias.outputs.alias }} + title: Update branch alias + body: | + Since we just tagged a new version, we need to update composer.json branch alias. diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 0089206..11b808e 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,4 +1,9 @@ -on: [push, pull_request] +on: + pull_request: ~ + push: + branches: + - "master" + name: Static analysis jobs: phpstan: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..bcf0f4e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,82 @@ +on: + pull_request: ~ + push: + branches: + - "master" + +name: Tests + +jobs: + phpunit: + name: PHPUnit with PHP ${{ matrix.php-version }} on ${{ matrix.operating-system }} + + strategy: + matrix: + operating-system: + - 'ubuntu-latest' + php-version: + - '7.1' + - '7.2' + - '7.3' + - '7.4' + - '8.0' + include: + - operating-system: 'windows-latest' + php-version: '7.4' + + runs-on: ${{ matrix.operating-system }} + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: ${{ matrix.php-version }} + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: composer-${{ runner.os }}-${{ matrix.php-version }}-${{ hashFiles('composer.*') }} + restore-keys: | + composer-${{ runner.os }}-${{ matrix.php-version }}- + composer-${{ runner.os }}- + composer- + + - name: Download dependencies + run: composer update --no-interaction --no-progress --optimize-autoloader + + - name: Run PHPUnit + run: ./vendor/bin/phpunit + + phpunit-lowest: + name: PHPUnit lowest dependencies + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Install PHP with extensions + uses: shivammathur/setup-php@v2 + with: + coverage: none + php-version: 7.4 + + - name: Download dependencies + run: composer update --no-interaction --no-progress --optimize-autoloader --prefer-stable --prefer-lowest + + - name: Run PHPUnit + run: ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + + - name: Upload coverage + run: | + wget https://scrutinizer-ci.com/ocular.phar + php ocular.phar code-coverage:upload --format=php-clover coverage.xml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 908de13..0000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: php -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -php: - - 7.1 - - 7.2 - - 7.3 - - 7.4 - -env: - global: - - TEST_COMMAND="./vendor/bin/phpunit" - -branches: - except: - - /^patch-.*$/ - -matrix: - fast_finish: true - include: - - name: "PHP: 8.0" - php: nightly - - - name: "Lowest version of dependencies" - php: 7.2 - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" COVERAGE=true TEST_COMMAND="./vendor/bin/phpunit --coverage-text --coverage-clover=build/coverage.xml" - -install: - - composer update ${COMPOSER_FLAGS} --prefer-source --no-interaction - -script: - - $TEST_COMMAND - -after_success: - - if [[ "$COVERAGE" = true ]]; then wget https://scrutinizer-ci.com/ocular.phar; fi - - if [[ "$COVERAGE" = true ]]; then php ocular.phar code-coverage:upload --format=php-clover build/coverage.xml; fi diff --git a/composer.json b/composer.json index a5b9dc0..0741e74 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "1.4-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1e5ba2a..51ebcd4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,10 +1,5 @@ - - - src/ - - tests/ diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 085967c..42f2201 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -134,7 +134,7 @@ public function testGetStream() $upload = new UploadedFile(__DIR__.'/Resources/foo.txt', 0, UPLOAD_ERR_OK); $stream = $upload->getStream(); $this->assertInstanceOf(StreamInterface::class, $stream); - $this->assertEquals("Foobar\n", $stream->__toString()); + $this->assertEquals('Foobar'.PHP_EOL, $stream->__toString()); } public function testSuccessful() From 87bc153623847ffd2b4ded609b1f13e873db5119 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 18 Feb 2021 16:41:14 +0100 Subject: [PATCH 38/84] Remove LowercaseTrait (#172) * Remove LowercaseTrait * Fixed tests * cs fixes * minor * Reverted some automated CS changes * Update PHP-cs-fixer rules --- .github/workflows/static.yml | 2 +- .php_cs | 1 + src/LowercaseTrait.php | 20 -------------------- src/MessageTrait.php | 12 +++++------- src/Stream.php | 16 ++++++++++++---- src/Uri.php | 10 ++++------ 6 files changed, 23 insertions(+), 38 deletions(-) delete mode 100644 src/LowercaseTrait.php diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 11b808e..09c99d2 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -35,7 +35,7 @@ jobs: uses: actions/checkout@v2 - name: PHP-CS-Fixer - uses: OskarStark/php-cs-fixer-ga@2.16.4 + uses: OskarStark/php-cs-fixer-ga@2.17.3 with: args: --dry-run --diff-format udiff diff --git a/.php_cs b/.php_cs index 31a2cf2..945d0b2 100644 --- a/.php_cs +++ b/.php_cs @@ -7,6 +7,7 @@ $config = PhpCsFixer\Config::create() '@Symfony:risky' => true, 'array_syntax' => array('syntax' => 'short'), 'native_function_invocation' => true, + 'native_constant_invocation' => true, 'ordered_imports' => true, 'declare_strict_types' => true, 'single_import_per_statement' => false, diff --git a/src/LowercaseTrait.php b/src/LowercaseTrait.php deleted file mode 100644 index dfc5031..0000000 --- a/src/LowercaseTrait.php +++ /dev/null @@ -1,20 +0,0 @@ - - * - * @internal should not be used outside of Nyholm/Psr7 as it does not fall under our BC promise - */ -trait LowercaseTrait -{ - private static function lowercase(string $value): string - { - return \strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); - } -} diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 2da949d..595258b 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -17,8 +17,6 @@ */ trait MessageTrait { - use LowercaseTrait; - /** @var array Map of all registered headers, as original name => array of values */ private $headers = []; @@ -55,12 +53,12 @@ public function getHeaders(): array public function hasHeader($header): bool { - return isset($this->headerNames[self::lowercase($header)]); + return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); } public function getHeader($header): array { - $header = self::lowercase($header); + $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$header])) { return []; } @@ -78,7 +76,7 @@ public function getHeaderLine($header): string public function withHeader($header, $value): self { $value = $this->validateAndTrimHeader($header, $value); - $normalized = self::lowercase($header); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $new = clone $this; if (isset($new->headerNames[$normalized])) { @@ -104,7 +102,7 @@ public function withAddedHeader($header, $value): self public function withoutHeader($header): self { - $normalized = self::lowercase($header); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$normalized])) { return $this; } @@ -146,7 +144,7 @@ private function setHeaders(array $headers): void $header = (string) $header; } $value = $this->validateAndTrimHeader($header, $value); - $normalized = self::lowercase($header); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; $this->headers[$header] = \array_merge($this->headers[$header], $value); diff --git a/src/Stream.php b/src/Stream.php index 2b62e1c..151b4f6 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -27,7 +27,7 @@ final class Stream implements StreamInterface /** @var bool */ private $writable; - /** @var array|mixed|void|null */ + /** @var array|mixed|void|bool|null */ private $uri; /** @var int|null */ @@ -79,7 +79,6 @@ public static function create($body = ''): StreamInterface $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR); $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); - $new->uri = $new->getMetadata('uri'); return $new; } @@ -148,6 +147,15 @@ public function detach() return $result; } + private function getUri() + { + if (false !== $this->uri) { + $this->uri = $this->getMetadata('uri') ?? false; + } + + return $this->uri; + } + public function getSize(): ?int { if (null !== $this->size) { @@ -159,8 +167,8 @@ public function getSize(): ?int } // Clear the stat cache if the stream has a URI - if ($this->uri) { - \clearstatcache(true, $this->uri); + if ($uri = $this->getUri()) { + \clearstatcache(true, $uri); } $stats = \fstat($this->stream); diff --git a/src/Uri.php b/src/Uri.php index d8bb2ed..95027e1 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -17,8 +17,6 @@ */ final class Uri implements UriInterface { - use LowercaseTrait; - private const SCHEMES = ['http' => 80, 'https' => 443]; private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; @@ -54,9 +52,9 @@ public function __construct(string $uri = '') } // Apply parse_url parts to a URI. - $this->scheme = isset($parts['scheme']) ? self::lowercase($parts['scheme']) : ''; + $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->userInfo = $parts['user'] ?? ''; - $this->host = isset($parts['host']) ? self::lowercase($parts['host']) : ''; + $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; @@ -131,7 +129,7 @@ public function withScheme($scheme): self throw new \InvalidArgumentException('Scheme must be a string'); } - if ($this->scheme === $scheme = self::lowercase($scheme)) { + if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } @@ -165,7 +163,7 @@ public function withHost($host): self throw new \InvalidArgumentException('Host must be a string'); } - if ($this->host === $host = self::lowercase($host)) { + if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } From 23ae1f00fbc6a886cbe3062ca682391b9cc7c37b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 18 Feb 2021 16:41:32 +0100 Subject: [PATCH 39/84] Remove final keyword (#169) * Remove final keyword * Added changelog * Added docs to final * Update doc/final.md Co-authored-by: Martijn van der Ven * Added link to decorator pattern Co-authored-by: Martijn van der Ven --- CHANGELOG.md | 6 ++++++ doc/final.md | 20 ++++++++++++++++++++ src/Factory/HttplugFactory.php | 4 +++- src/Factory/Psr17Factory.php | 4 +++- src/Request.php | 4 +++- src/Response.php | 4 +++- src/ServerRequest.php | 4 +++- src/Stream.php | 4 +++- src/UploadedFile.php | 4 +++- src/Uri.php | 4 +++- 10 files changed, 50 insertions(+), 8 deletions(-) create mode 100644 doc/final.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e9cc9e..759bdbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.4.0 + +### Removed + +The `final` keyword was replaced by `@final` annotation. + ## 1.3.2 ### Fixed diff --git a/doc/final.md b/doc/final.md new file mode 100644 index 0000000..b5b1942 --- /dev/null +++ b/doc/final.md @@ -0,0 +1,20 @@ +# Final classes + +The `final` keyword was removed in version 1.4.0. It was replaced by `@final` annotation. +This was done due popular demand, not because it is a good technical reason to +extend the classes. + +This document will show the correct way to work with PSR-7 classes. The "correct way" +refers to best practices and good software design. I strongly believe that one should +be aware of how a problem *should* be solved, however, it is not needed to always +implement that solution. + +## Extending classes + +You should never extend the classes, you should rather use composition or implement +the interface yourself. Please refer to the [decorator pattern](https://refactoring.guru/design-patterns/decorator). + +## Mocking classes + +The PSR-7 classes are all value objects and they can be used without mocking. If +one really needs to create a special scenario, one can mock the interface instead. diff --git a/src/Factory/HttplugFactory.php b/src/Factory/HttplugFactory.php index a296541..cc64780 100644 --- a/src/Factory/HttplugFactory.php +++ b/src/Factory/HttplugFactory.php @@ -11,8 +11,10 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class HttplugFactory implements MessageFactory, StreamFactory, UriFactory +class HttplugFactory implements MessageFactory, StreamFactory, UriFactory { public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1') { diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 08caf85..0da5acd 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -10,8 +10,10 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface +class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { diff --git a/src/Request.php b/src/Request.php index 84a9f2a..d50744e 100644 --- a/src/Request.php +++ b/src/Request.php @@ -9,8 +9,10 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class Request implements RequestInterface +class Request implements RequestInterface { use MessageTrait; use RequestTrait; diff --git a/src/Response.php b/src/Response.php index c2020f4..9a26d2c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -10,8 +10,10 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class Response implements ResponseInterface +class Response implements ResponseInterface { use MessageTrait; diff --git a/src/ServerRequest.php b/src/ServerRequest.php index aff8721..1ad8792 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -10,8 +10,10 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class ServerRequest implements ServerRequestInterface +class ServerRequest implements ServerRequestInterface { use MessageTrait; use RequestTrait; diff --git a/src/Stream.php b/src/Stream.php index 151b4f6..bcc59ba 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -12,8 +12,10 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class Stream implements StreamInterface +class Stream implements StreamInterface { /** @var resource|null A resource reference */ private $stream; diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 757e70e..415bf87 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -10,8 +10,10 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class UploadedFile implements UploadedFileInterface +class UploadedFile implements UploadedFileInterface { /** @var array */ private const ERRORS = [ diff --git a/src/Uri.php b/src/Uri.php index 95027e1..eee304f 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -14,8 +14,10 @@ * @author Matthew Weier O'Phinney * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ -final class Uri implements UriInterface +class Uri implements UriInterface { private const SCHEMES = ['http' => 80, 'https' => 443]; From 902515f50a970235f4930527b48d5c4e67e6c4c9 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Thu, 18 Feb 2021 17:19:46 +0100 Subject: [PATCH 40/84] Update the readme with better benchmarks (#173) * Update readme with better benchmarks * Remove httpsoft --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index abe3248..9f92933 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ A super lightweight PSR-7 implementation. Very strict and very fast. | Description | Guzzle | Laminas | Slim | Nyholm | | ---- | ------ | ---- | ---- | ------ | -| Lines of code | 3 000 | 3 000 | 1 700 | 1 000 | +| Lines of code | 3.300 | 3.100 | 1.900 | 1.000 | | PSR-7* | 66% | 100% | 75% | 100% | | PSR-17 | No | Yes | Yes | Yes | | HTTPlug | No | No | No | Yes | -| Performance** | 1.34x | 1x | 1.16x | 1.75x | +| Performance (runs per second)** | 14.553 | 14.703 | 13.416 | 17.734 | \* Percent of completed tests in https://github.com/php-http/psr7-integration-tests -\** See benchmark at https://github.com/Nyholm/http-client-benchmark (higher is better) +\** Benchmark with 50.000 runs. See https://github.com/devanych/psr-http-benchmark (higher is better) ## Installation From d9371730dc3e2718f542207d884722807f402b6d Mon Sep 17 00:00:00 2001 From: David Buchmann Date: Mon, 10 May 2021 22:00:20 +0200 Subject: [PATCH 41/84] rewind in-memory stream to have expected behaviour (#177) --- CHANGELOG.md | 6 ++++++ src/Stream.php | 1 + tests/StreamTest.php | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 759bdbf..2a761d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## Unreleased + +### Fixed + +- `Stream::create` with a string needs to rewind the created memory stream. + ## 1.4.0 ### Removed diff --git a/src/Stream.php b/src/Stream.php index bcc59ba..fee0c63 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -71,6 +71,7 @@ public static function create($body = ''): StreamInterface if (\is_string($body)) { $resource = \fopen('php://temp', 'rw+'); \fwrite($resource, $body); + \rewind($resource); $body = $resource; } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 74898e9..d3ac5aa 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -44,6 +44,13 @@ public function testConvertsToString() $stream->close(); } + public function testBuildFromString() + { + $stream = Stream::create('data'); + $this->assertEquals('data', $stream->getContents()); + $stream->close(); + } + public function testGetsContents() { $handle = fopen('php://temp', 'w+'); From a4a362944244ed20a6bbbecacc882abc8045585a Mon Sep 17 00:00:00 2001 From: Martijn van der Ven Date: Mon, 10 May 2021 22:13:32 +0200 Subject: [PATCH 42/84] Wrap fopen for PHP 8 (#174) * Wrap fopen to not leak PHP 8 throwing ValueError * Dont expand the public API Co-authored-by: Nyholm --- CHANGELOG.md | 5 +++-- composer.json | 2 +- src/Factory/Psr17Factory.php | 9 +++++++-- src/UploadedFile.php | 17 ++++++++++++----- 4 files changed, 23 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a761d7..7a626f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,13 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. -## Unreleased +## 1.4.1 ### Fixed - `Stream::create` with a string needs to rewind the created memory stream. +- `Psr17Factory::createStreamFromFile`, `UploadedFile::moveTo`, and + `UploadedFile::getStream` no longer throw `ValueError` in PHP 8. ## 1.4.0 @@ -118,4 +120,3 @@ The `final` keyword was replaced by `@final` annotation. ## 0.2.3 No changelog before this release - diff --git a/composer.json b/composer.json index 0741e74..fffdec0 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "require-dev": { "phpunit/phpunit": "^7.5 || 8.5 || 9.4", "php-http/psr7-integration-tests": "^1.0", - "http-interop/http-factory-tests": "^0.8", + "http-interop/http-factory-tests": "^0.9", "symfony/error-handler": "^4.4" }, "provide": { diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 0da5acd..0726b68 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -37,9 +37,14 @@ public function createStream(string $content = ''): StreamInterface public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface { - $resource = @\fopen($filename, $mode); + try { + $resource = @\fopen($filename, $mode); + } catch (\Throwable $e) { + throw new \RuntimeException('The file ' . $filename . ' cannot be opened.'); + } + if (false === $resource) { - if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'])) { + if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.'); } diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 415bf87..3c0d098 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -114,9 +114,11 @@ public function getStream(): StreamInterface return $this->stream; } - $resource = \fopen($this->file, 'r'); - - return Stream::create($resource); + try { + return Stream::create(\fopen($this->file, 'r')); + } catch (\Throwable $e) { + throw new \RuntimeException('The file ' . $this->file . ' cannot be opened.'); + } } public function moveTo($targetPath): void @@ -135,8 +137,13 @@ public function moveTo($targetPath): void $stream->rewind(); } - // Copy the contents of a stream into another stream until end-of-file. - $dest = Stream::create(\fopen($targetPath, 'w')); + try { + // Copy the contents of a stream into another stream until end-of-file. + $dest = Stream::create(\fopen($targetPath, 'w')); + } catch (\Throwable $e) { + throw new \RuntimeException('The file ' . $targetPath . ' cannot be opened.'); + } + while (!$stream->eof()) { if (!$dest->write($stream->read(1048576))) { break; From 2dc58be5319542a437e5f61b69ebebe76d15ceca Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sat, 19 Jun 2021 09:59:27 +0200 Subject: [PATCH 43/84] Make more clear exception messages with quotes (#179) * Make more clear exception messages with quotes * cs * Fixed tests --- src/Factory/Psr17Factory.php | 6 +++--- src/Stream.php | 2 +- src/UploadedFile.php | 6 +++--- src/Uri.php | 2 +- tests/RequestTest.php | 2 +- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 0726b68..f304baa 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -40,15 +40,15 @@ public function createStreamFromFile(string $filename, string $mode = 'r'): Stre try { $resource = @\fopen($filename, $mode); } catch (\Throwable $e) { - throw new \RuntimeException('The file ' . $filename . ' cannot be opened.'); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $filename)); } if (false === $resource) { if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { - throw new \InvalidArgumentException('The mode ' . $mode . ' is invalid.'); + throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode)); } - throw new \RuntimeException('The file ' . $filename . ' cannot be opened.'); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $filename)); } return Stream::create($resource); diff --git a/src/Stream.php b/src/Stream.php index fee0c63..f1dc804 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -210,7 +210,7 @@ public function seek($offset, $whence = \SEEK_SET): void } if (-1 === \fseek($this->stream, $offset, $whence)) { - throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true)); + throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, true)); } } diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 3c0d098..198cd33 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -117,7 +117,7 @@ public function getStream(): StreamInterface try { return Stream::create(\fopen($this->file, 'r')); } catch (\Throwable $e) { - throw new \RuntimeException('The file ' . $this->file . ' cannot be opened.'); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $this->file)); } } @@ -141,7 +141,7 @@ public function moveTo($targetPath): void // Copy the contents of a stream into another stream until end-of-file. $dest = Stream::create(\fopen($targetPath, 'w')); } catch (\Throwable $e) { - throw new \RuntimeException('The file ' . $targetPath . ' cannot be opened.'); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $targetPath)); } while (!$stream->eof()) { @@ -154,7 +154,7 @@ public function moveTo($targetPath): void } if (false === $this->moved) { - throw new \RuntimeException(\sprintf('Uploaded file could not be moved to %s', $targetPath)); + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s"', $targetPath)); } } diff --git a/src/Uri.php b/src/Uri.php index eee304f..13fbf72 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -50,7 +50,7 @@ public function __construct(string $uri = '') { if ('' !== $uri) { if (false === $parts = \parse_url($uri)) { - throw new \InvalidArgumentException("Unable to parse URI: $uri"); + throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); } // Apply parse_url parts to a URI. diff --git a/tests/RequestTest.php b/tests/RequestTest.php index ddac6d2..392b032 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -28,7 +28,7 @@ public function testRequestUriMayBeUri() public function testValidateRequestUri() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Unable to parse URI: ///'); + $this->expectExceptionMessage('Unable to parse URI: "///"'); new Request('GET', '///'); } From 2212385b47153ea71b1c1b1374f8cb5e4f7892ec Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Fri, 2 Jul 2021 10:32:20 +0200 Subject: [PATCH 44/84] Revert #177 (#182) * Revert #177 * update changelog --- CHANGELOG.md | 1 - src/Stream.php | 1 - tests/StreamTest.php | 3 ++- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a626f2..07238bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,6 @@ All notable changes to this project will be documented in this file, in reverse ### Fixed -- `Stream::create` with a string needs to rewind the created memory stream. - `Psr17Factory::createStreamFromFile`, `UploadedFile::moveTo`, and `UploadedFile::getStream` no longer throw `ValueError` in PHP 8. diff --git a/src/Stream.php b/src/Stream.php index f1dc804..1a7f8c1 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -71,7 +71,6 @@ public static function create($body = ''): StreamInterface if (\is_string($body)) { $resource = \fopen('php://temp', 'rw+'); \fwrite($resource, $body); - \rewind($resource); $body = $resource; } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index d3ac5aa..f487203 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -47,7 +47,8 @@ public function testConvertsToString() public function testBuildFromString() { $stream = Stream::create('data'); - $this->assertEquals('data', $stream->getContents()); + $this->assertEquals('', $stream->getContents()); + $this->assertEquals('data', $stream->__toString()); $stream->close(); } From 83a384e18af23c8b2547836876c5d87dce7ba97e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 16 Jul 2021 19:37:44 +0300 Subject: [PATCH 45/84] Improve error handling (#185) --- src/Factory/Psr17Factory.php | 10 ++++------ src/Stream.php | 36 ++++++++++++++++++++++++++---------- src/UploadedFile.php | 29 ++++++++++++++--------------- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index f304baa..440bec3 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -37,18 +37,16 @@ public function createStream(string $content = ''): StreamInterface public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface { - try { - $resource = @\fopen($filename, $mode); - } catch (\Throwable $e) { - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $filename)); + if ('' === $filename) { + throw new \RuntimeException('Path cannot be empty'); } - if (false === $resource) { + if (false === $resource = @\fopen($filename, $mode)) { if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode)); } - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $filename)); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? '')); } return Stream::create($resource); diff --git a/src/Stream.php b/src/Stream.php index 1a7f8c1..372af92 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -185,8 +185,12 @@ public function getSize(): ?int public function tell(): int { - if (false === $result = \ftell($this->stream)) { - throw new \RuntimeException('Unable to determine stream position'); + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + + if (false === $result = @\ftell($this->stream)) { + throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -194,7 +198,7 @@ public function tell(): int public function eof(): bool { - return !$this->stream || \feof($this->stream); + return !isset($this->stream) || \feof($this->stream); } public function isSeekable(): bool @@ -204,6 +208,10 @@ public function isSeekable(): bool public function seek($offset, $whence = \SEEK_SET): void { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->seekable) { throw new \RuntimeException('Stream is not seekable'); } @@ -225,6 +233,10 @@ public function isWritable(): bool public function write($string): int { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->writable) { throw new \RuntimeException('Cannot write to a non-writable stream'); } @@ -232,8 +244,8 @@ public function write($string): int // We can't know the size after writing anything $this->size = null; - if (false === $result = \fwrite($this->stream, $string)) { - throw new \RuntimeException('Unable to write to stream'); + if (false === $result = @\fwrite($this->stream, $string)) { + throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -246,12 +258,16 @@ public function isReadable(): bool public function read($length): string { + if (!isset($this->stream)) { + throw new \RuntimeException('Stream is detached'); + } + if (!$this->readable) { throw new \RuntimeException('Cannot read from non-readable stream'); } - if (false === $result = \fread($this->stream, $length)) { - throw new \RuntimeException('Unable to read from stream'); + if (false === $result = @\fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -260,11 +276,11 @@ public function read($length): string public function getContents(): string { if (!isset($this->stream)) { - throw new \RuntimeException('Unable to read stream contents'); + throw new \RuntimeException('Stream is detached'); } - if (false === $contents = \stream_get_contents($this->stream)) { - throw new \RuntimeException('Unable to read stream contents'); + if (false === $contents = @\stream_get_contents($this->stream)) { + throw new \RuntimeException('Unable to read stream contents: ' . (\error_get_last()['message'] ?? '')); } return $contents; diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 198cd33..bf47b78 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -80,7 +80,7 @@ public function __construct($streamOrFile, $size, $errorStatus, $clientFilename if (\UPLOAD_ERR_OK === $this->error) { // Depending on the value set file or stream variable. - if (\is_string($streamOrFile)) { + if (\is_string($streamOrFile) && '' !== $streamOrFile) { $this->file = $streamOrFile; } elseif (\is_resource($streamOrFile)) { $this->stream = Stream::create($streamOrFile); @@ -114,11 +114,11 @@ public function getStream(): StreamInterface return $this->stream; } - try { - return Stream::create(\fopen($this->file, 'r')); - } catch (\Throwable $e) { - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $this->file)); + if (false === $resource = @\fopen($this->file, 'r')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? '')); } + + return Stream::create($resource); } public function moveTo($targetPath): void @@ -130,20 +130,23 @@ public function moveTo($targetPath): void } if (null !== $this->file) { - $this->moved = 'cli' === \PHP_SAPI ? \rename($this->file, $targetPath) : \move_uploaded_file($this->file, $targetPath); + $this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath); + + if (false === $this->moved) { + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? '')); + } } else { $stream = $this->getStream(); if ($stream->isSeekable()) { $stream->rewind(); } - try { - // Copy the contents of a stream into another stream until end-of-file. - $dest = Stream::create(\fopen($targetPath, 'w')); - } catch (\Throwable $e) { - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened.', $targetPath)); + if (false === $resource = @\fopen($targetPath, 'w')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? '')); } + $dest = Stream::create($resource); + while (!$stream->eof()) { if (!$dest->write($stream->read(1048576))) { break; @@ -152,10 +155,6 @@ public function moveTo($targetPath): void $this->moved = true; } - - if (false === $this->moved) { - throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s"', $targetPath)); - } } public function getSize(): int From bbc97cf23c1339441e78bbcc2c8494b4864e75b7 Mon Sep 17 00:00:00 2001 From: Nico Rickenbach Date: Fri, 16 Jul 2021 18:38:11 +0200 Subject: [PATCH 46/84] add doc/ and psalm.xml to .gitattributes (#184) --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index fa51164..fc3ad5c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,5 @@ .github/ export-ignore +doc/ export-ignore tests/ export-ignore .editorconfig export-ignore .gitattributes export-ignore @@ -9,3 +10,4 @@ tests/ export-ignore phpstan.neon.dist export-ignore phpstan.baseline.dist export-ignore phpunit.xml.dist export-ignore +psalm.xml export-ignore From fafc2faf8d49b979f922fe416dc8805462dc1fe8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 10 Aug 2021 17:20:44 +0200 Subject: [PATCH 47/84] Add explicit `@return mixed` phpdoc (#187) --- src/ServerRequest.php | 3 +++ src/Stream.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 1ad8792..bde46cd 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -133,6 +133,9 @@ public function getAttributes(): array return $this->attributes; } + /** + * @return mixed + */ public function getAttribute($attribute, $default = null) { if (false === \array_key_exists($attribute, $this->attributes)) { diff --git a/src/Stream.php b/src/Stream.php index 372af92..71515e5 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -286,6 +286,9 @@ public function getContents(): string return $contents; } + /** + * @return mixed + */ public function getMetadata($key = null) { if (!isset($this->stream)) { From 28e9c474672d3474705227f6276b8f4d19297880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabriel=20Ostroluck=C3=BD?= Date: Mon, 13 Dec 2021 23:47:27 +0100 Subject: [PATCH 48/84] Add return types to HttplugFactory (#188) This is needed to do now because symfony/error-handler triggers deprecation notices such these 1x: Method "Http\Message\RequestFactory::createRequest()" might add "RequestInterface" as a native return type declaration in the future. Do the same in implementation "Nyholm\Psr7\Factory\HttplugFactory" now to avoid errors or add an explicit @return annotation to suppress this message. 1x in DiscoveredClientsTest::testForcedDiscovery from Http\HttplugBundle\Tests\Functional 1x: Method "Http\Message\ResponseFactory::createResponse()" might add "ResponseInterface" as a native return type declaration in the future. Do the same in implementation "Nyholm\Psr7\Factory\HttplugFactory" now to avoid errors or add an explicit @return annotation to suppress this message. 1x in DiscoveredClientsTest::testForcedDiscovery from Http\HttplugBundle\Tests\Functional 1x: Method "Http\Message\StreamFactory::createStream()" might add "StreamInterface" as a native return type declaration in the future. Do the same in implementation "Nyholm\Psr7\Factory\HttplugFactory" now to avoid errors or add an explicit @return annotation to suppress this message. 1x in DiscoveredClientsTest::testForcedDiscovery from Http\HttplugBundle\Tests\Functional --- src/Factory/HttplugFactory.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Factory/HttplugFactory.php b/src/Factory/HttplugFactory.php index cc64780..4cf8e27 100644 --- a/src/Factory/HttplugFactory.php +++ b/src/Factory/HttplugFactory.php @@ -6,6 +6,9 @@ use Http\Message\{MessageFactory, StreamFactory, UriFactory}; use Nyholm\Psr7\{Request, Response, Stream, Uri}; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; /** @@ -16,17 +19,17 @@ */ class HttplugFactory implements MessageFactory, StreamFactory, UriFactory { - public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1') + public function createRequest($method, $uri, array $headers = [], $body = null, $protocolVersion = '1.1'): RequestInterface { return new Request($method, $uri, $headers, $body, $protocolVersion); } - public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1') + public function createResponse($statusCode = 200, $reasonPhrase = null, array $headers = [], $body = null, $version = '1.1'): ResponseInterface { return new Response((int) $statusCode, $headers, $body, $version, $reasonPhrase); } - public function createStream($body = null) + public function createStream($body = null): StreamInterface { return Stream::create($body ?? ''); } From 71ddee018f25e9267ded7bbc4e27802e5553454b Mon Sep 17 00:00:00 2001 From: Petri Haikonen Date: Wed, 2 Feb 2022 20:27:08 +0200 Subject: [PATCH 49/84] PHP 8.1 (#192) --- .github/workflows/tests.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index bcf0f4e..b916c7a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -20,6 +20,7 @@ jobs: - '7.3' - '7.4' - '8.0' + - '8.1' include: - operating-system: 'windows-latest' php-version: '7.4' From 1461e07a0f2a975a52082ca3b769ca912b816226 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 2 Feb 2022 10:37:57 -0800 Subject: [PATCH 50/84] Release 1.5.0 (#194) --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 07238bd..78a633c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.5.0 + +### Added + +- Add explicit `@return mixed` +- Add explicit return types to HttplugFactory + +### Fixed + +- Improve error handling with streams + ## 1.4.1 ### Fixed From f88a9e564781304289dbe16ee2ac810f1a728532 Mon Sep 17 00:00:00 2001 From: Andrii Dembitskyi Date: Wed, 22 Jun 2022 10:08:22 +0300 Subject: [PATCH 51/84] Resolve symfony/error-handler deprecations (#197) * Fixed deprecation messages for PHP 8.1 (#195) * Fixed deprecation messages for PHP 8.1 and `symfony/error-handler` with BC (#195) * Fixed deprecation messages for PHP 8.1 and `symfony/error-handler` in tests Co-authored-by: Luis Fernando do Nascimento --- src/ServerRequest.php | 15 +++++++++++++++ tests/Integration/RequestTest.php | 3 ++- tests/Integration/ResponseTest.php | 3 ++- tests/Integration/ServerRequestTest.php | 3 ++- tests/Integration/StreamTest.php | 3 ++- tests/Integration/UploadedFileTest.php | 3 ++- tests/Integration/UriTest.php | 3 ++- 7 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/ServerRequest.php b/src/ServerRequest.php index bde46cd..5fc3f2b 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -77,6 +77,9 @@ public function getUploadedFiles(): array return $this->uploadedFiles; } + /** + * @return static + */ public function withUploadedFiles(array $uploadedFiles) { $new = clone $this; @@ -90,6 +93,9 @@ public function getCookieParams(): array return $this->cookieParams; } + /** + * @return static + */ public function withCookieParams(array $cookies) { $new = clone $this; @@ -103,6 +109,9 @@ public function getQueryParams(): array return $this->queryParams; } + /** + * @return static + */ public function withQueryParams(array $query) { $new = clone $this; @@ -111,11 +120,17 @@ public function withQueryParams(array $query) return $new; } + /** + * @return array|object|null + */ public function getParsedBody() { return $this->parsedBody; } + /** + * @return static + */ public function withParsedBody($data) { if (!\is_array($data) && !\is_object($data) && null !== $data) { diff --git a/tests/Integration/RequestTest.php b/tests/Integration/RequestTest.php index 50a99dc..8b06719 100644 --- a/tests/Integration/RequestTest.php +++ b/tests/Integration/RequestTest.php @@ -2,12 +2,13 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\RequestInterface; use Http\Psr7Test\RequestIntegrationTest; use Nyholm\Psr7\Request; class RequestTest extends RequestIntegrationTest { - public function createSubject() + public function createSubject(): RequestInterface { return new Request('GET', '/'); } diff --git a/tests/Integration/ResponseTest.php b/tests/Integration/ResponseTest.php index 3a8c23c..aa15272 100644 --- a/tests/Integration/ResponseTest.php +++ b/tests/Integration/ResponseTest.php @@ -2,12 +2,13 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\ResponseInterface; use Http\Psr7Test\ResponseIntegrationTest; use Nyholm\Psr7\Response; class ResponseTest extends ResponseIntegrationTest { - public function createSubject() + public function createSubject(): ResponseInterface { return new Response(); } diff --git a/tests/Integration/ServerRequestTest.php b/tests/Integration/ServerRequestTest.php index 38c19f8..f801bbc 100644 --- a/tests/Integration/ServerRequestTest.php +++ b/tests/Integration/ServerRequestTest.php @@ -2,12 +2,13 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\ServerRequestInterface; use Http\Psr7Test\ServerRequestIntegrationTest; use Nyholm\Psr7\ServerRequest; class ServerRequestTest extends ServerRequestIntegrationTest { - public function createSubject() + public function createSubject(): ServerRequestInterface { $_SERVER['REQUEST_METHOD'] = 'GET'; diff --git a/tests/Integration/StreamTest.php b/tests/Integration/StreamTest.php index 5c7cb14..e79bbd9 100644 --- a/tests/Integration/StreamTest.php +++ b/tests/Integration/StreamTest.php @@ -2,12 +2,13 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\StreamInterface; use Http\Psr7Test\StreamIntegrationTest; use Nyholm\Psr7\Stream; class StreamTest extends StreamIntegrationTest { - public function createStream($data) + public function createStream($data): StreamInterface { return Stream::create($data); } diff --git a/tests/Integration/UploadedFileTest.php b/tests/Integration/UploadedFileTest.php index d5b4ce4..28d256e 100644 --- a/tests/Integration/UploadedFileTest.php +++ b/tests/Integration/UploadedFileTest.php @@ -2,13 +2,14 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\UploadedFileInterface; use Http\Psr7Test\UploadedFileIntegrationTest; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Stream; class UploadedFileTest extends UploadedFileIntegrationTest { - public function createSubject() + public function createSubject(): UploadedFileInterface { return (new Psr17Factory())->createUploadedFile(Stream::create('writing to tempfile')); } diff --git a/tests/Integration/UriTest.php b/tests/Integration/UriTest.php index d96c26b..2adafea 100644 --- a/tests/Integration/UriTest.php +++ b/tests/Integration/UriTest.php @@ -2,12 +2,13 @@ namespace Tests\Nyholm\Psr7\Integration; +use Psr\Http\Message\UriInterface; use Http\Psr7Test\UriIntegrationTest; use Nyholm\Psr7\Uri; class UriTest extends UriIntegrationTest { - public function createUri($uri) + public function createUri($uri): UriInterface { return new Uri($uri); } From f734364e38a876a23be4d906a2a089e1315be18a Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 22 Jun 2022 09:13:36 +0200 Subject: [PATCH 52/84] Release 1.5.1 (#200) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78a633c..a9f8154 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.5.1 + +### Fixed + +- Fixed deprecations on PHP 8.1 + ## 1.5.0 ### Added From e22012d3595099877531300e6aa023a11d940d46 Mon Sep 17 00:00:00 2001 From: Vano Devium Date: Fri, 31 Mar 2023 14:01:05 +0300 Subject: [PATCH 53/84] Uri: normalization leading slashes for getPath() (#211) --- src/Uri.php | 15 ++++++++++++++- tests/UriTest.php | 4 ++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Uri.php b/src/Uri.php index 13fbf72..130d852 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -112,7 +112,20 @@ public function getPort(): ?int public function getPath(): string { - return $this->path; + $path = $this->path; + + if ('' !== $path && '/' !== $path[0]) { + if ('' !== $this->host) { + // If the path is rootless and an authority is present, the path MUST be prefixed by "/" + $path = '/' . $path; + } + } elseif (isset($path[1]) && '/' === $path[1]) { + // If the path is starting with more than one "/", the + // starting slashes MUST be reduced to one. + $path = '/' . \ltrim($path, '/'); + } + + return $path; } public function getQuery(): string diff --git a/tests/UriTest.php b/tests/UriTest.php index bb165e8..cb9a9d4 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -440,7 +440,7 @@ public function testAddsSlashForRelativeUriStringWithHost() // If the path is rootless and an authority is present, the path MUST // be prefixed by "/". $uri = (new Uri())->withPath('foo')->withHost('example.com'); - $this->assertSame('foo', $uri->getPath()); + $this->assertSame('/foo', $uri->getPath()); // concatenating a relative path with a host doesn't work: "//example.comfoo" would be wrong $this->assertSame('//example.com/foo', (string) $uri); } @@ -450,7 +450,7 @@ public function testRemoveExtraSlashesWihoutHost() // If the path is starting with more than one "/" and no authority is // present, the starting slashes MUST be reduced to one. $uri = (new Uri())->withPath('//foo'); - $this->assertSame('//foo', $uri->getPath()); + $this->assertSame('/foo', $uri->getPath()); // URI "//foo" would be interpreted as network reference and thus change the original path to the host $this->assertSame('/foo', (string) $uri); } From 53a07582bd6466fcc3d66fed4f0f27f5baa81fe8 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 13:08:33 +0200 Subject: [PATCH 54/84] Encode userinfo (but don't double-encode) (#213) --- composer.json | 1 + src/Uri.php | 4 ++-- tests/UriTest.php | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index fffdec0..7c88d31 100644 --- a/composer.json +++ b/composer.json @@ -27,6 +27,7 @@ "symfony/error-handler": "^4.4" }, "provide": { + "php-http/message-factory-implementation": "1.0", "psr/http-message-implementation": "1.0", "psr/http-factory-implementation": "1.0" }, diff --git a/src/Uri.php b/src/Uri.php index 130d852..bb7f602 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -157,9 +157,9 @@ public function withScheme($scheme): self public function withUserInfo($user, $password = null): self { - $info = $user; + $info = \rawurlencode(\rawurldecode($user)); if (null !== $password && '' !== $password) { - $info .= ':' . $password; + $info .= ':' . \rawurlencode(\rawurldecode($password)); } if ($this->userInfo === $info) { diff --git a/tests/UriTest.php b/tests/UriTest.php index cb9a9d4..2d964b7 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -31,7 +31,7 @@ public function testCanTransformAndRetrievePartsIndividually() { $uri = (new Uri()) ->withScheme('https') - ->withUserInfo('user', 'pass') + ->withUserInfo('user%3D=', 'pass%3D=') ->withHost('example.com') ->withPort(8080) ->withPath('/path/123') @@ -39,14 +39,14 @@ public function testCanTransformAndRetrievePartsIndividually() ->withFragment('test'); $this->assertSame('https', $uri->getScheme()); - $this->assertSame('user:pass@example.com:8080', $uri->getAuthority()); - $this->assertSame('user:pass', $uri->getUserInfo()); + $this->assertSame('user%3D%3D:pass%3D%3D@example.com:8080', $uri->getAuthority()); + $this->assertSame('user%3D%3D:pass%3D%3D', $uri->getUserInfo()); $this->assertSame('example.com', $uri->getHost()); $this->assertSame(8080, $uri->getPort()); $this->assertSame('/path/123', $uri->getPath()); $this->assertSame('q=abc', $uri->getQuery()); $this->assertSame('test', $uri->getFragment()); - $this->assertSame('https://user:pass@example.com:8080/path/123?q=abc#test', (string) $uri); + $this->assertSame('https://user%3D%3D:pass%3D%3D@example.com:8080/path/123?q=abc#test', (string) $uri); } /** From 133794405fe1e7b80055d1b0cdecee90f30cd579 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 13:10:28 +0200 Subject: [PATCH 55/84] Improve extensibility by removing `@final` and making Stream's constructor public (#203) --- doc/final.md | 20 -------------------- src/Factory/HttplugFactory.php | 2 -- src/Factory/Psr17Factory.php | 2 -- src/Request.php | 2 -- src/Response.php | 2 -- src/ServerRequest.php | 2 -- src/Stream.php | 29 ++++++++++++++++------------- src/UploadedFile.php | 2 -- src/Uri.php | 2 -- 9 files changed, 16 insertions(+), 47 deletions(-) delete mode 100644 doc/final.md diff --git a/doc/final.md b/doc/final.md deleted file mode 100644 index b5b1942..0000000 --- a/doc/final.md +++ /dev/null @@ -1,20 +0,0 @@ -# Final classes - -The `final` keyword was removed in version 1.4.0. It was replaced by `@final` annotation. -This was done due popular demand, not because it is a good technical reason to -extend the classes. - -This document will show the correct way to work with PSR-7 classes. The "correct way" -refers to best practices and good software design. I strongly believe that one should -be aware of how a problem *should* be solved, however, it is not needed to always -implement that solution. - -## Extending classes - -You should never extend the classes, you should rather use composition or implement -the interface yourself. Please refer to the [decorator pattern](https://refactoring.guru/design-patterns/decorator). - -## Mocking classes - -The PSR-7 classes are all value objects and they can be used without mocking. If -one really needs to create a special scenario, one can mock the interface instead. diff --git a/src/Factory/HttplugFactory.php b/src/Factory/HttplugFactory.php index 4cf8e27..cfdeeb8 100644 --- a/src/Factory/HttplugFactory.php +++ b/src/Factory/HttplugFactory.php @@ -14,8 +14,6 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class HttplugFactory implements MessageFactory, StreamFactory, UriFactory { diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 440bec3..07b3611 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -10,8 +10,6 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface { diff --git a/src/Request.php b/src/Request.php index d50744e..f1c92c5 100644 --- a/src/Request.php +++ b/src/Request.php @@ -9,8 +9,6 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Request implements RequestInterface { diff --git a/src/Response.php b/src/Response.php index 9a26d2c..2610536 100644 --- a/src/Response.php +++ b/src/Response.php @@ -10,8 +10,6 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Response implements ResponseInterface { diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 5fc3f2b..fce68d8 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -10,8 +10,6 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class ServerRequest implements ServerRequestInterface { diff --git a/src/Stream.php b/src/Stream.php index 71515e5..e711656 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -12,8 +12,6 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Stream implements StreamInterface { @@ -51,8 +49,20 @@ class Stream implements StreamInterface ], ]; - private function __construct() + /** + * @param resource $body + */ + public function __construct($body) { + if (!\is_resource($body)) { + throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource.'); + } + + $this->stream = $body; + $meta = \stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR); + $this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); + $this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); } /** @@ -74,18 +84,11 @@ public static function create($body = ''): StreamInterface $body = $resource; } - if (\is_resource($body)) { - $new = new self(); - $new->stream = $body; - $meta = \stream_get_meta_data($new->stream); - $new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR); - $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); - $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); - - return $new; + if (!\is_resource($body)) { + throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); } - throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); + return new self($body); } /** diff --git a/src/UploadedFile.php b/src/UploadedFile.php index bf47b78..d93b395 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -10,8 +10,6 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class UploadedFile implements UploadedFileInterface { diff --git a/src/Uri.php b/src/Uri.php index bb7f602..230d0b2 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -14,8 +14,6 @@ * @author Matthew Weier O'Phinney * @author Tobias Nyholm * @author Martijn van der Ven - * - * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Uri implements UriInterface { From 3b10897c4080c43ba2b085af943942407cd47703 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Fri, 31 Mar 2023 12:11:33 +0100 Subject: [PATCH 56/84] Test on PHP 8.2 (#212) --- .github/workflows/tests.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b916c7a..1453255 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,6 +21,7 @@ jobs: - '7.4' - '8.0' - '8.1' + - '8.2' include: - operating-system: 'windows-latest' php-version: '7.4' @@ -29,7 +30,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 @@ -63,7 +64,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Install PHP with extensions uses: shivammathur/setup-php@v2 From 7ff2b9908847ece3edc23d240cbc6502f1d451d3 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 16:07:24 +0200 Subject: [PATCH 57/84] Fix permission on BC check job (#215) --- .github/workflows/bc.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index 18b35c8..f4658e0 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -10,6 +10,8 @@ jobs: name: BC Check runs-on: ubuntu-latest steps: + - name: Permissions + run: git config --global --add safe.directory /github/workspace - uses: actions/checkout@master - name: Roave BC Check uses: docker://nyholm/roave-bc-check-ga From 6a33acf28df8500e5ac01b55efa958def6269e9e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 16:14:42 +0200 Subject: [PATCH 58/84] Populate ServerRequest::getQueryParams() with query string passed to constructor (#214) --- src/ServerRequest.php | 1 + tests/ServerRequestTest.php | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ServerRequest.php b/src/ServerRequest.php index fce68d8..a60a15d 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -54,6 +54,7 @@ public function __construct(string $method, $uri, array $headers = [], $body = n $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; + \parse_str($uri->getQuery(), $this->queryParams); if (!$this->hasHeader('Host')) { $this->updateHostFromUri(); diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index c26b228..b607827 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -49,14 +49,14 @@ public function testCookieParams() public function testQueryParams() { - $request1 = new ServerRequest('GET', '/'); + $request1 = new ServerRequest('GET', '/?foo=bar'); $params = ['name' => 'value']; $request2 = $request1->withQueryParams($params); $this->assertNotSame($request2, $request1); - $this->assertEmpty($request1->getQueryParams()); + $this->assertSame(['foo' => 'bar'], $request1->getQueryParams()); $this->assertSame($params, $request2->getQueryParams()); } From fcfda65a61419d06d6f58e60f549396072ecae2e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 16:15:53 +0200 Subject: [PATCH 59/84] Revert "Fix permissions on BC check job" (#216) --- .github/workflows/bc.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml index f4658e0..18b35c8 100644 --- a/.github/workflows/bc.yml +++ b/.github/workflows/bc.yml @@ -10,8 +10,6 @@ jobs: name: BC Check runs-on: ubuntu-latest steps: - - name: Permissions - run: git config --global --add safe.directory /github/workspace - uses: actions/checkout@master - name: Roave BC Check uses: docker://nyholm/roave-bc-check-ga From 0ebdcc7b3a24d4c846777e541a392b30ff64da5f Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 16:29:32 +0200 Subject: [PATCH 60/84] Stream: seek to the begining of the string on initialization (#217) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Grégoire Pineau --- .gitignore | 1 + src/Stream.php | 1 + tests/ResponseTest.php | 8 ++++++++ tests/StreamTest.php | 16 +++++++++++++++- 4 files changed, 25 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8cf0c7b..cae3602 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ phpunit.xml vendor .php_cs.cache .phpunit.result.cache +.tmp diff --git a/src/Stream.php b/src/Stream.php index e711656..b1337f8 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -81,6 +81,7 @@ public static function create($body = ''): StreamInterface if (\is_string($body)) { $resource = \fopen('php://temp', 'rw+'); \fwrite($resource, $body); + \fseek($resource, 0); $body = $resource; } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index e6e109e..1c5f7af 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -23,6 +23,14 @@ public function testDefaultConstructor() $this->assertSame('', (string) $r->getBody()); } + public function testDefaultConstructorSeekBody() + { + $r = new Response(200, [], 'Hello'); + $this->assertSame(200, $r->getStatusCode()); + $this->assertInstanceOf(StreamInterface::class, $r->getBody()); + $this->assertSame('Hello', $r->getBody()->getContents()); + } + public function testCanConstructWithStatusCode() { $r = new Response(404); diff --git a/tests/StreamTest.php b/tests/StreamTest.php index f487203..345a55c 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -26,6 +26,20 @@ public function testConstructorInitializesProperties() $stream->close(); } + public function testConstructorSeekWithStringContent() + { + $stream = Stream::create('Hello'); + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isWritable()); + $this->assertTrue($stream->isSeekable()); + $this->assertEquals('php://temp', $stream->getMetadata('uri')); + $this->assertTrue(\is_array($stream->getMetadata())); + $this->assertSame(5, $stream->getSize()); + $this->assertFalse($stream->eof()); + $this->assertSame('Hello', $stream->getContents()); + $stream->close(); + } + public function testStreamClosesHandleOnDestruct() { $handle = fopen('php://temp', 'r'); @@ -47,7 +61,7 @@ public function testConvertsToString() public function testBuildFromString() { $stream = Stream::create('data'); - $this->assertEquals('', $stream->getContents()); + $this->assertEquals('data', $stream->getContents()); $this->assertEquals('data', $stream->__toString()); $stream->close(); } From 834cdce1c4b9824a4de27093040898b8a288dd83 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 17:56:08 +0200 Subject: [PATCH 61/84] Add some missing type checks on arguments (#220) --- phpstan.baseline.dist | 5 +++++ src/MessageTrait.php | 24 ++++++++++++++++++------ src/RequestTrait.php | 4 ++++ src/ServerRequest.php | 12 ++++++++++++ src/Stream.php | 8 ++++++-- src/UploadedFile.php | 2 +- src/Uri.php | 8 ++++++++ tests/RequestTest.php | 2 +- 8 files changed, 55 insertions(+), 10 deletions(-) diff --git a/phpstan.baseline.dist b/phpstan.baseline.dist index 56a1c07..9432f7a 100644 --- a/phpstan.baseline.dist +++ b/phpstan.baseline.dist @@ -40,3 +40,8 @@ parameters: count: 2 path: src/UploadedFile.php + - + message: "#^Result of && is always false\\.$#" + count: 1 + path: src/Stream.php + diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 595258b..2544fa4 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -36,12 +36,16 @@ public function getProtocolVersion(): string public function withProtocolVersion($version): self { + if (!\is_scalar($version)) { + throw new \InvalidArgumentException('Protocol version must be a string'); + } + if ($this->protocol === $version) { return $this; } $new = clone $this; - $new->protocol = $version; + $new->protocol = (string) $version; return $new; } @@ -58,6 +62,10 @@ public function hasHeader($header): bool public function getHeader($header): array { + if (!\is_string($header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$header])) { return []; @@ -91,7 +99,7 @@ public function withHeader($header, $value): self public function withAddedHeader($header, $value): self { if (!\is_string($header) || '' === $header) { - throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } $new = clone $this; @@ -102,6 +110,10 @@ public function withAddedHeader($header, $value): self public function withoutHeader($header): self { + if (!\is_string($header)) { + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); + } + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$normalized])) { return $this; @@ -176,27 +188,27 @@ private function setHeaders(array $headers): void private function validateAndTrimHeader($header, $values): array { if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { - throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); + throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } if (!\is_array($values)) { // This is simple, just one value. if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { - throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } return [\trim((string) $values, " \t")]; } if (empty($values)) { - throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given.'); + throw new \InvalidArgumentException('Header values must be a string or an array of strings, empty array given'); } // Assert Non empty array $returnValues = []; foreach ($values as $v) { if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { - throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings.'); + throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } $returnValues[] = \trim((string) $v, " \t"); diff --git a/src/RequestTrait.php b/src/RequestTrait.php index f39993a..7c39bbb 100644 --- a/src/RequestTrait.php +++ b/src/RequestTrait.php @@ -42,6 +42,10 @@ public function getRequestTarget(): string public function withRequestTarget($requestTarget): self { + if (!\is_string($requestTarget)) { + throw new \InvalidArgumentException('Request target must be a string'); + } + if (\preg_match('#\s#', $requestTarget)) { throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index a60a15d..9268030 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -152,6 +152,10 @@ public function getAttributes(): array */ public function getAttribute($attribute, $default = null) { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + if (false === \array_key_exists($attribute, $this->attributes)) { return $default; } @@ -161,6 +165,10 @@ public function getAttribute($attribute, $default = null) public function withAttribute($attribute, $value): self { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + $new = clone $this; $new->attributes[$attribute] = $value; @@ -169,6 +177,10 @@ public function withAttribute($attribute, $value): self public function withoutAttribute($attribute): self { + if (!\is_string($attribute)) { + throw new \InvalidArgumentException('Attribute name must be a string'); + } + if (false === \array_key_exists($attribute, $this->attributes)) { return $this; } diff --git a/src/Stream.php b/src/Stream.php index b1337f8..33e1818 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -55,7 +55,7 @@ class Stream implements StreamInterface public function __construct($body) { if (!\is_resource($body)) { - throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource.'); + throw new \InvalidArgumentException('First argument to Stream::__construct() must be resource'); } $this->stream = $body; @@ -86,7 +86,7 @@ public static function create($body = ''): StreamInterface } if (!\is_resource($body)) { - throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); + throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface'); } return new self($body); @@ -295,6 +295,10 @@ public function getContents(): string */ public function getMetadata($key = null) { + if (null !== $key && !\is_string($key)) { + throw new \InvalidArgumentException('Metadata key must be a string'); + } + if (!isset($this->stream)) { return $key ? null : []; } diff --git a/src/UploadedFile.php b/src/UploadedFile.php index d93b395..f5323cd 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -56,7 +56,7 @@ class UploadedFile implements UploadedFileInterface public function __construct($streamOrFile, $size, $errorStatus, $clientFilename = null, $clientMediaType = null) { if (false === \is_int($errorStatus) || !isset(self::ERRORS[$errorStatus])) { - throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants.'); + throw new \InvalidArgumentException('Upload file error status must be an integer value and one of the "UPLOAD_ERR_*" constants'); } if (false === \is_int($size)) { diff --git a/src/Uri.php b/src/Uri.php index 230d0b2..1b1a682 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -155,8 +155,16 @@ public function withScheme($scheme): self public function withUserInfo($user, $password = null): self { + if (!\is_string($user)) { + throw new \InvalidArgumentException('User must be a string'); + } + $info = \rawurlencode(\rawurldecode($user)); if (null !== $password && '' !== $password) { + if (!\is_string($password)) { + throw new \InvalidArgumentException('Password must be a string'); + } + $info .= ':' . \rawurlencode(\rawurldecode($password)); } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index 392b032..dabc077 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -263,7 +263,7 @@ public function testAddsPortToHeaderAndReplacePreviousPort() public function testCannotHaveHeaderWithEmptyName() { $this->expectException(\InvalidArgumentException::class); - $this->expectExceptionMessage('Header name must be an RFC 7230 compatible string.'); + $this->expectExceptionMessage('Header name must be an RFC 7230 compatible string'); $r = new Request('GET', 'https://example.com/'); $r->withHeader('', 'Bar'); } From c9b58270d5448ec27b73f897f0a1c725f4ce8fa1 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 31 Mar 2023 18:26:33 +0200 Subject: [PATCH 62/84] Encode only reserved characters in user-info (#221) --- src/Uri.php | 6 ++++-- tests/UriTest.php | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Uri.php b/src/Uri.php index 1b1a682..11be3a8 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -23,6 +23,8 @@ class Uri implements UriInterface private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + private const CHAR_GEN_DELIMS = ':\/\?#\[\]@'; + /** @var string Uri scheme. */ private $scheme = ''; @@ -159,13 +161,13 @@ public function withUserInfo($user, $password = null): self throw new \InvalidArgumentException('User must be a string'); } - $info = \rawurlencode(\rawurldecode($user)); + $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); if (null !== $password && '' !== $password) { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string'); } - $info .= ':' . \rawurlencode(\rawurldecode($password)); + $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); } if ($this->userInfo === $info) { diff --git a/tests/UriTest.php b/tests/UriTest.php index 2d964b7..a8f5366 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -31,7 +31,7 @@ public function testCanTransformAndRetrievePartsIndividually() { $uri = (new Uri()) ->withScheme('https') - ->withUserInfo('user%3D=', 'pass%3D=') + ->withUserInfo('foo\user%3D=', 'pass%3D=') ->withHost('example.com') ->withPort(8080) ->withPath('/path/123') @@ -39,14 +39,14 @@ public function testCanTransformAndRetrievePartsIndividually() ->withFragment('test'); $this->assertSame('https', $uri->getScheme()); - $this->assertSame('user%3D%3D:pass%3D%3D@example.com:8080', $uri->getAuthority()); - $this->assertSame('user%3D%3D:pass%3D%3D', $uri->getUserInfo()); + $this->assertSame('foo\user%3D%3D:pass%3D%3D@example.com:8080', $uri->getAuthority()); + $this->assertSame('foo\user%3D%3D:pass%3D%3D', $uri->getUserInfo()); $this->assertSame('example.com', $uri->getHost()); $this->assertSame(8080, $uri->getPort()); $this->assertSame('/path/123', $uri->getPath()); $this->assertSame('q=abc', $uri->getQuery()); $this->assertSame('test', $uri->getFragment()); - $this->assertSame('https://user%3D%3D:pass%3D%3D@example.com:8080/path/123?q=abc#test', (string) $uri); + $this->assertSame('https://foo\user%3D%3D:pass%3D%3D@example.com:8080/path/123?q=abc#test', (string) $uri); } /** From 50b69ed63759c9cac1ac7c00da325178aacdd0ac Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 3 Apr 2023 17:07:18 +0100 Subject: [PATCH 63/84] Fixed incorrect branch alias (#222) --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7c88d31..5c33c75 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } } } From 72535f5d31c0b1258a4550a89dcb6059fd0d7d3a Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Wed, 5 Apr 2023 10:26:34 +0200 Subject: [PATCH 64/84] refactor(phpunit): make data providers static (#224) --- tests/ResponseTest.php | 2 +- tests/UploadedFileTest.php | 10 +++++----- tests/UriTest.php | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index 1c5f7af..c60246d 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -256,7 +256,7 @@ public function testSameInstanceWhenRemovingMissingHeader() $this->assertSame($r, $r->withoutHeader('foo')); } - public function trimmedHeaderValues() + public static function trimmedHeaderValues() { return [ [new Response(200, ['OWS' => " \t \tFoo\t \t "])], diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 42f2201..30fced3 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -28,7 +28,7 @@ public function tearDown(): void } } - public function invalidStreams() + public static function invalidStreams() { return [ 'null' => [null], @@ -52,7 +52,7 @@ public function testRaisesExceptionOnInvalidStreamOrFile($streamOrFile) new UploadedFile($streamOrFile, 0, UPLOAD_ERR_OK); } - public function invalidErrorStatuses() + public static function invalidErrorStatuses() { return [ 'null' => [null], @@ -78,7 +78,7 @@ public function testRaisesExceptionOnInvalidErrorStatus($status) new UploadedFile(fopen('php://temp', 'wb+'), 0, $status); } - public function invalidFilenamesAndMediaTypes() + public static function invalidFilenamesAndMediaTypes() { return [ 'true' => [true], @@ -152,7 +152,7 @@ public function testSuccessful() $this->assertEquals($stream->__toString(), file_get_contents($to)); } - public function invalidMovePaths() + public static function invalidMovePaths() { return [ 'null' => [null], @@ -209,7 +209,7 @@ public function testCannotRetrieveStreamAfterMove() $upload->getStream(); } - public function nonOkErrorStatus() + public static function nonOkErrorStatus() { return [ 'UPLOAD_ERR_INI_SIZE' => [UPLOAD_ERR_INI_SIZE], diff --git a/tests/UriTest.php b/tests/UriTest.php index a8f5366..da122b4 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -59,7 +59,7 @@ public function testValidUrisStayValid($input) $this->assertSame($input, (string) $uri); } - public function getValidUris() + public static function getValidUris() { return [ ['urn:path-rootless'], @@ -99,7 +99,7 @@ public function testInvalidUrisThrowException($invalidUri) new Uri($invalidUri); } - public function getInvalidUris() + public static function getInvalidUris() { return [ // parse_url() requires the host component which makes sense for http(s) @@ -368,7 +368,7 @@ public function testAuthorityWithUserInfoButWithoutHost() $this->assertSame('', $uri->getAuthority()); } - public function uriComponentsEncodingProvider() + public static function uriComponentsEncodingProvider() { $unreserved = 'a-zA-Z0-9.-_~!$&\'()*+,;=:@'; From c06c9e0f03ca36a2c4e1823c34a8937eec90ed11 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Wed, 5 Apr 2023 13:28:44 +0200 Subject: [PATCH 65/84] Revert removals of `@final` annotations (#225) --- doc/final.md | 20 ++++++++++++++++++++ src/Factory/HttplugFactory.php | 2 ++ src/Factory/Psr17Factory.php | 2 ++ src/Request.php | 2 ++ src/Response.php | 2 ++ src/ServerRequest.php | 2 ++ src/Stream.php | 2 ++ src/UploadedFile.php | 2 ++ src/Uri.php | 2 ++ 9 files changed, 36 insertions(+) create mode 100644 doc/final.md diff --git a/doc/final.md b/doc/final.md new file mode 100644 index 0000000..b5b1942 --- /dev/null +++ b/doc/final.md @@ -0,0 +1,20 @@ +# Final classes + +The `final` keyword was removed in version 1.4.0. It was replaced by `@final` annotation. +This was done due popular demand, not because it is a good technical reason to +extend the classes. + +This document will show the correct way to work with PSR-7 classes. The "correct way" +refers to best practices and good software design. I strongly believe that one should +be aware of how a problem *should* be solved, however, it is not needed to always +implement that solution. + +## Extending classes + +You should never extend the classes, you should rather use composition or implement +the interface yourself. Please refer to the [decorator pattern](https://refactoring.guru/design-patterns/decorator). + +## Mocking classes + +The PSR-7 classes are all value objects and they can be used without mocking. If +one really needs to create a special scenario, one can mock the interface instead. diff --git a/src/Factory/HttplugFactory.php b/src/Factory/HttplugFactory.php index cfdeeb8..4cf8e27 100644 --- a/src/Factory/HttplugFactory.php +++ b/src/Factory/HttplugFactory.php @@ -14,6 +14,8 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class HttplugFactory implements MessageFactory, StreamFactory, UriFactory { diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 07b3611..440bec3 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -10,6 +10,8 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Psr17Factory implements RequestFactoryInterface, ResponseFactoryInterface, ServerRequestFactoryInterface, StreamFactoryInterface, UploadedFileFactoryInterface, UriFactoryInterface { diff --git a/src/Request.php b/src/Request.php index f1c92c5..d50744e 100644 --- a/src/Request.php +++ b/src/Request.php @@ -9,6 +9,8 @@ /** * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Request implements RequestInterface { diff --git a/src/Response.php b/src/Response.php index 2610536..9a26d2c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -10,6 +10,8 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Response implements ResponseInterface { diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 9268030..7f5022e 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -10,6 +10,8 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class ServerRequest implements ServerRequestInterface { diff --git a/src/Stream.php b/src/Stream.php index 33e1818..d173f35 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -12,6 +12,8 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Stream implements StreamInterface { diff --git a/src/UploadedFile.php b/src/UploadedFile.php index f5323cd..c77dca4 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -10,6 +10,8 @@ * @author Michael Dowling and contributors to guzzlehttp/psr7 * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class UploadedFile implements UploadedFileInterface { diff --git a/src/Uri.php b/src/Uri.php index 11be3a8..70ad634 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -14,6 +14,8 @@ * @author Matthew Weier O'Phinney * @author Tobias Nyholm * @author Martijn van der Ven + * + * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md */ class Uri implements UriInterface { From c3c62f4071b0ce66eb8e84e97067b13bce32fc57 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 9 Apr 2023 10:28:14 +0200 Subject: [PATCH 66/84] Added some more tests (#228) --- tests/ServerRequestTest.php | 5 +++-- tests/UriTest.php | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index b607827..4a3db77 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -49,10 +49,11 @@ public function testCookieParams() public function testQueryParams() { - $request1 = new ServerRequest('GET', '/?foo=bar'); + $request0 = new ServerRequest('GET', '/'); + $this->assertEmpty($request0->getQueryParams()); + $request1 = new ServerRequest('GET', '/?foo=bar'); $params = ['name' => 'value']; - $request2 = $request1->withQueryParams($params); $this->assertNotSame($request2, $request1); diff --git a/tests/UriTest.php b/tests/UriTest.php index da122b4..be3beb0 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -28,6 +28,28 @@ public function testParsesProvidedUri() } public function testCanTransformAndRetrievePartsIndividually() + { + $uri = (new Uri()) + ->withScheme('https') + ->withUserInfo('user', 'pass') + ->withHost('example.com') + ->withPort(8080) + ->withPath('/path/123') + ->withQuery('q=abc') + ->withFragment('test'); + + $this->assertSame('https', $uri->getScheme()); + $this->assertSame('user:pass@example.com:8080', $uri->getAuthority()); + $this->assertSame('user:pass', $uri->getUserInfo()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame(8080, $uri->getPort()); + $this->assertSame('/path/123', $uri->getPath()); + $this->assertSame('q=abc', $uri->getQuery()); + $this->assertSame('test', $uri->getFragment()); + $this->assertSame('https://user:pass@example.com:8080/path/123?q=abc#test', (string) $uri); + } + + public function testSupportsUrlEncodedValues() { $uri = (new Uri()) ->withScheme('https') From bf4aebd170fadf5fd808c70b90535de327e81a50 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Sun, 9 Apr 2023 10:34:27 +0200 Subject: [PATCH 67/84] Release 1.6.0 (#218) --- CHANGELOG.md | 11 +++++++++++ composer.json | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9f8154..462b677 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.6.0 + +### Changed + +- Seek to the begining of the string when using Stream::create() +- Populate ServerRequest::getQueryParams() on instantiation +- Encode [reserved characters](https://www.rfc-editor.org/rfc/rfc3986#appendix-A) in userinfo in Uri +- Normalize leading slashes for Uri::getPath() +- Make Stream's constructor public +- Add some missing type checks on arguments + ## 1.5.1 ### Fixed diff --git a/composer.json b/composer.json index 5c33c75..680459c 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.5-dev" + "dev-master": "1.6-dev" } } } From be5f75a97b5a911cde547d9eb0a5e1bff5cf6dea Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 11 Apr 2023 18:36:14 +0200 Subject: [PATCH 68/84] Remove scutinizer (#231) --- .github/workflows/tests.yml | 5 ----- .scrutinizer.yml | 8 -------- README.md | 2 -- 3 files changed, 15 deletions(-) delete mode 100644 .scrutinizer.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 1453255..4d4e52d 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -77,8 +77,3 @@ jobs: - name: Run PHPUnit run: ./vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml - - - name: Upload coverage - run: | - wget https://scrutinizer-ci.com/ocular.phar - php ocular.phar code-coverage:upload --format=php-clover coverage.xml diff --git a/.scrutinizer.yml b/.scrutinizer.yml deleted file mode 100644 index 4304ab1..0000000 --- a/.scrutinizer.yml +++ /dev/null @@ -1,8 +0,0 @@ -filter: - excluded_paths: [vendor/*, tests/*] -checks: - php: - code_rating: true - duplication: true -tools: - external_code_coverage: true diff --git a/README.md b/README.md index 9f92933..9369f73 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,6 @@ [![Latest Version](https://img.shields.io/github/release/Nyholm/psr7.svg?style=flat-square)](https://github.com/Nyholm/psr7/releases) [![Build Status](https://img.shields.io/travis/Nyholm/psr7/master.svg?style=flat-square)](https://travis-ci.org/Nyholm/psr7) -[![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/Nyholm/psr7.svg?style=flat-square)](https://scrutinizer-ci.com/g/Nyholm/psr7) -[![Quality Score](https://img.shields.io/scrutinizer/g/Nyholm/psr7.svg?style=flat-square)](https://scrutinizer-ci.com/g/Nyholm/psr7) [![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7) [![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) From 1cb99829352428036ed3f4a06cb63020bf723a1c Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Tue, 11 Apr 2023 18:36:32 +0200 Subject: [PATCH 69/84] [CI] Just some code style changes (#232) * Update php-cs-fixer * CS fixes * CS native_function_invocation * CS native_constant_invocation * CS ordered_imports * CS declare_strict_types * CS linebreak_after_opening_tag * fix * update meta files --- .github/workflows/static.yml | 60 ++++++++++++------- .gitignore | 2 +- .php-cs-fixer.dist.php | 26 ++++++++ .php_cs | 24 -------- ...tan.baseline.dist => phpstan-baseline.neon | 14 ++--- phpstan.neon.dist | 2 +- psalm.baseline.xml | 8 +++ psalm.xml | 2 + src/Factory/Psr17Factory.php | 6 +- src/MessageTrait.php | 24 ++++---- src/RequestTrait.php | 2 +- src/Response.php | 2 +- src/ServerRequest.php | 2 +- src/Stream.php | 46 +++++++------- src/UploadedFile.php | 12 ++-- src/Uri.php | 30 +++++----- tests/Integration/RequestTest.php | 4 +- tests/Integration/ResponseTest.php | 4 +- tests/Integration/ServerRequestTest.php | 4 +- tests/Integration/StreamTest.php | 4 +- tests/Integration/UploadedFileTest.php | 4 +- tests/Integration/UriTest.php | 4 +- tests/RequestTest.php | 10 ++-- tests/ServerRequestTest.php | 4 +- tests/StreamTest.php | 6 +- tests/UploadedFileTest.php | 46 +++++++------- tests/UriTest.php | 6 +- 27 files changed, 190 insertions(+), 168 deletions(-) create mode 100644 .php-cs-fixer.dist.php delete mode 100644 .php_cs rename phpstan.baseline.dist => phpstan-baseline.neon (73%) create mode 100644 psalm.baseline.xml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 09c99d2..1af3110 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -8,47 +8,63 @@ name: Static analysis jobs: phpstan: name: PHPStan - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: 8.1 + coverage: none + tools: phpstan:1.8.11, cs2pr - name: Download dependencies - run: | - composer update --no-interaction --prefer-dist --optimize-autoloader + uses: ramsey/composer-install@v2 - name: PHPStan - uses: docker://oskarstark/phpstan-ga:0.12.48 - env: - REQUIRE_DEV: true - with: - entrypoint: /composer/vendor/bin/phpstan - args: analyze --no-progress + run: phpstan analyze --no-progress --error-format=checkstyle | cs2pr php-cs-fixer: name: PHP-CS-Fixer - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: PHP-CS-Fixer - uses: OskarStark/php-cs-fixer-ga@2.17.3 + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - args: --dry-run --diff-format udiff + php-version: 8.1 + coverage: none + tools: php-cs-fixer:3.15, cs2pr + + - name: Display PHP-CS-Fixer version + run: php-cs-fixer --version + + - name: PHP-CS-Fixer + run: php-cs-fixer fix --dry-run --format=checkstyle | cs2pr psalm: name: Psalm - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Psalm - uses: docker://vimeo/psalm-github-actions:3.17.2 - env: - REQUIRE_DEV: true + - name: Setup PHP + uses: shivammathur/setup-php@v2 with: - args: --no-progress --show-info=false --stats + php-version: 8.1 + extensions: apcu, redis + coverage: none + tools: vimeo/psalm:4.29.0 + + - name: Download dependencies + uses: ramsey/composer-install@v2 + + - name: Psalm + run: psalm --no-progress --output-format=github diff --git a/.gitignore b/.gitignore index cae3602..d401b18 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ composer.lock phpstan.neon phpunit.xml vendor -.php_cs.cache +.php-cs-fixer.cache .phpunit.result.cache .tmp diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..f35538b --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,26 @@ +in(__DIR__.'/src') + ->in(__DIR__.'/tests'); + +$config = new PhpCsFixer\Config(); + +return $config->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + 'array_syntax' => array('syntax' => 'short'), + 'native_function_invocation' => true, + 'native_constant_invocation' => true, + 'ordered_imports' => true, + 'declare_strict_types' => false, + 'linebreak_after_opening_tag' => false, + 'single_import_per_statement' => false, + 'blank_line_after_opening_tag' => false, + 'concat_space' => ['spacing'=>'one'], + 'phpdoc_align' => ['align'=>'left'], +]) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/.php_cs b/.php_cs deleted file mode 100644 index 945d0b2..0000000 --- a/.php_cs +++ /dev/null @@ -1,24 +0,0 @@ -setRiskyAllowed(true) - ->setRules([ - '@Symfony' => true, - '@Symfony:risky' => true, - 'array_syntax' => array('syntax' => 'short'), - 'native_function_invocation' => true, - 'native_constant_invocation' => true, - 'ordered_imports' => true, - 'declare_strict_types' => true, - 'single_import_per_statement' => false, - 'concat_space' => ['spacing'=>'one'], - 'phpdoc_align' => ['align'=>'left'], - ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->in(__DIR__.'/src') - ->name('*.php') - ) -; - -return $config; diff --git a/phpstan.baseline.dist b/phpstan-baseline.neon similarity index 73% rename from phpstan.baseline.dist rename to phpstan-baseline.neon index 9432f7a..80991e8 100644 --- a/phpstan.baseline.dist +++ b/phpstan-baseline.neon @@ -21,27 +21,21 @@ parameters: path: src/ServerRequest.php - - message: "#^Parameter \\#1 \\$error_handler of function set_error_handler expects \\(callable\\(int, string, string, int, array\\)\\: bool\\)\\|null, 'var_dump' given\\.$#" + message: "#^Parameter \\#1 \\$callback of function set_error_handler expects \\(callable\\(int, string, string, int\\)\\: bool\\)\\|null, 'var_dump' given\\.$#" count: 1 path: src/Stream.php - - message: "#^Method Nyholm\\\\Psr7\\\\Stream\\:\\:__toString\\(\\) should return string but returns bool\\.$#" + message: "#^Result of && is always false\\.$#" count: 1 path: src/Stream.php - - message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" + message: "#^Result of && is always false\\.$#" count: 2 path: src/UploadedFile.php - - message: "#^Result of && is always false\\.$#" + message: "#^Strict comparison using \\=\\=\\= between false and true will always evaluate to false\\.$#" count: 2 path: src/UploadedFile.php - - - - message: "#^Result of && is always false\\.$#" - count: 1 - path: src/Stream.php - diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0628e93..b9fec19 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,5 +1,5 @@ includes: - - phpstan.baseline.dist + - phpstan-baseline.neon parameters: level: 5 diff --git a/psalm.baseline.xml b/psalm.baseline.xml new file mode 100644 index 0000000..8cdb7fa --- /dev/null +++ b/psalm.baseline.xml @@ -0,0 +1,8 @@ + + + + + return trigger_error((string) $e, \E_USER_ERROR); + + + diff --git a/psalm.xml b/psalm.xml index d234691..de59250 100644 --- a/psalm.xml +++ b/psalm.xml @@ -5,6 +5,8 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd" + + errorBaseline="psalm.baseline.xml" > diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 440bec3..326e517 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -41,12 +41,12 @@ public function createStreamFromFile(string $filename, string $mode = 'r'): Stre throw new \RuntimeException('Path cannot be empty'); } - if (false === $resource = @\fopen($filename, $mode)) { + if (false === $resource = @fopen($filename, $mode)) { if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { - throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode)); + throw new \InvalidArgumentException(sprintf('The mode "%s" is invalid.', $mode)); } - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? '')); + throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $filename, error_get_last()['message'] ?? '')); } return Stream::create($resource); diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 2544fa4..e8ec75f 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -57,7 +57,7 @@ public function getHeaders(): array public function hasHeader($header): bool { - return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); + return isset($this->headerNames[strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); } public function getHeader($header): array @@ -66,7 +66,7 @@ public function getHeader($header): array throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } - $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $header = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$header])) { return []; } @@ -78,13 +78,13 @@ public function getHeader($header): array public function getHeaderLine($header): string { - return \implode(', ', $this->getHeader($header)); + return implode(', ', $this->getHeader($header)); } public function withHeader($header, $value): self { $value = $this->validateAndTrimHeader($header, $value); - $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $new = clone $this; if (isset($new->headerNames[$normalized])) { @@ -114,7 +114,7 @@ public function withoutHeader($header): self throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } - $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$normalized])) { return $this; } @@ -156,10 +156,10 @@ private function setHeaders(array $headers): void $header = (string) $header; } $value = $this->validateAndTrimHeader($header, $value); - $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; - $this->headers[$header] = \array_merge($this->headers[$header], $value); + $this->headers[$header] = array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; @@ -187,17 +187,17 @@ private function setHeaders(array $headers): void */ private function validateAndTrimHeader($header, $values): array { - if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { + if (!\is_string($header) || 1 !== preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } if (!\is_array($values)) { // This is simple, just one value. - if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { + if ((!is_numeric($values) && !\is_string($values)) || 1 !== preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } - return [\trim((string) $values, " \t")]; + return [trim((string) $values, " \t")]; } if (empty($values)) { @@ -207,11 +207,11 @@ private function validateAndTrimHeader($header, $values): array // Assert Non empty array $returnValues = []; foreach ($values as $v) { - if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { + if ((!is_numeric($v) && !\is_string($v)) || 1 !== preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } - $returnValues[] = \trim((string) $v, " \t"); + $returnValues[] = trim((string) $v, " \t"); } return $returnValues; diff --git a/src/RequestTrait.php b/src/RequestTrait.php index 7c39bbb..597874d 100644 --- a/src/RequestTrait.php +++ b/src/RequestTrait.php @@ -46,7 +46,7 @@ public function withRequestTarget($requestTarget): self throw new \InvalidArgumentException('Request target must be a string'); } - if (\preg_match('#\s#', $requestTarget)) { + if (preg_match('#\s#', $requestTarget)) { throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); } diff --git a/src/Response.php b/src/Response.php index 9a26d2c..3062b3b 100644 --- a/src/Response.php +++ b/src/Response.php @@ -75,7 +75,7 @@ public function withStatus($code, $reasonPhrase = ''): self $code = (int) $code; if ($code < 100 || $code > 599) { - throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + throw new \InvalidArgumentException(sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); } $new = clone $this; diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 7f5022e..1e47a06 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -56,7 +56,7 @@ public function __construct(string $method, $uri, array $headers = [], $body = n $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; - \parse_str($uri->getQuery(), $this->queryParams); + parse_str($uri->getQuery(), $this->queryParams); if (!$this->hasHeader('Host')) { $this->updateHostFromUri(); diff --git a/src/Stream.php b/src/Stream.php index d173f35..2c08b9f 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -61,8 +61,8 @@ public function __construct($body) } $this->stream = $body; - $meta = \stream_get_meta_data($this->stream); - $this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR); + $meta = stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable'] && 0 === fseek($this->stream, 0, \SEEK_CUR); $this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); $this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); } @@ -81,9 +81,9 @@ public static function create($body = ''): StreamInterface } if (\is_string($body)) { - $resource = \fopen('php://temp', 'rw+'); - \fwrite($resource, $body); - \fseek($resource, 0); + $resource = fopen('php://temp', 'rw+'); + fwrite($resource, $body); + fseek($resource, 0); $body = $resource; } @@ -118,13 +118,13 @@ public function __toString() throw $e; } - if (\is_array($errorHandler = \set_error_handler('var_dump'))) { + if (\is_array($errorHandler = set_error_handler('var_dump'))) { $errorHandler = $errorHandler[0] ?? null; } - \restore_error_handler(); + restore_error_handler(); if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { - return \trigger_error((string) $e, \E_USER_ERROR); + return trigger_error((string) $e, \E_USER_ERROR); } return ''; @@ -135,7 +135,7 @@ public function close(): void { if (isset($this->stream)) { if (\is_resource($this->stream)) { - \fclose($this->stream); + fclose($this->stream); } $this->detach(); } @@ -176,10 +176,10 @@ public function getSize(): ?int // Clear the stat cache if the stream has a URI if ($uri = $this->getUri()) { - \clearstatcache(true, $uri); + clearstatcache(true, $uri); } - $stats = \fstat($this->stream); + $stats = fstat($this->stream); if (isset($stats['size'])) { $this->size = $stats['size']; @@ -195,8 +195,8 @@ public function tell(): int throw new \RuntimeException('Stream is detached'); } - if (false === $result = @\ftell($this->stream)) { - throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? '')); + if (false === $result = @ftell($this->stream)) { + throw new \RuntimeException('Unable to determine stream position: ' . (error_get_last()['message'] ?? '')); } return $result; @@ -204,7 +204,7 @@ public function tell(): int public function eof(): bool { - return !isset($this->stream) || \feof($this->stream); + return !isset($this->stream) || feof($this->stream); } public function isSeekable(): bool @@ -222,8 +222,8 @@ public function seek($offset, $whence = \SEEK_SET): void throw new \RuntimeException('Stream is not seekable'); } - if (-1 === \fseek($this->stream, $offset, $whence)) { - throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, true)); + if (-1 === fseek($this->stream, $offset, $whence)) { + throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . var_export($whence, true)); } } @@ -250,8 +250,8 @@ public function write($string): int // We can't know the size after writing anything $this->size = null; - if (false === $result = @\fwrite($this->stream, $string)) { - throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? '')); + if (false === $result = @fwrite($this->stream, $string)) { + throw new \RuntimeException('Unable to write to stream: ' . (error_get_last()['message'] ?? '')); } return $result; @@ -272,8 +272,8 @@ public function read($length): string throw new \RuntimeException('Cannot read from non-readable stream'); } - if (false === $result = @\fread($this->stream, $length)) { - throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? '')); + if (false === $result = @fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream: ' . (error_get_last()['message'] ?? '')); } return $result; @@ -285,8 +285,8 @@ public function getContents(): string throw new \RuntimeException('Stream is detached'); } - if (false === $contents = @\stream_get_contents($this->stream)) { - throw new \RuntimeException('Unable to read stream contents: ' . (\error_get_last()['message'] ?? '')); + if (false === $contents = @stream_get_contents($this->stream)) { + throw new \RuntimeException('Unable to read stream contents: ' . (error_get_last()['message'] ?? '')); } return $contents; @@ -305,7 +305,7 @@ public function getMetadata($key = null) return $key ? null : []; } - $meta = \stream_get_meta_data($this->stream); + $meta = stream_get_meta_data($this->stream); if (null === $key) { return $meta; diff --git a/src/UploadedFile.php b/src/UploadedFile.php index c77dca4..0789943 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -114,8 +114,8 @@ public function getStream(): StreamInterface return $this->stream; } - if (false === $resource = @\fopen($this->file, 'r')) { - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? '')); + if (false === $resource = @fopen($this->file, 'r')) { + throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $this->file, error_get_last()['message'] ?? '')); } return Stream::create($resource); @@ -130,10 +130,10 @@ public function moveTo($targetPath): void } if (null !== $this->file) { - $this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath); + $this->moved = 'cli' === \PHP_SAPI ? @rename($this->file, $targetPath) : @move_uploaded_file($this->file, $targetPath); if (false === $this->moved) { - throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? '')); + throw new \RuntimeException(sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, error_get_last()['message'] ?? '')); } } else { $stream = $this->getStream(); @@ -141,8 +141,8 @@ public function moveTo($targetPath): void $stream->rewind(); } - if (false === $resource = @\fopen($targetPath, 'w')) { - throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? '')); + if (false === $resource = @fopen($targetPath, 'w')) { + throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $targetPath, error_get_last()['message'] ?? '')); } $dest = Stream::create($resource); diff --git a/src/Uri.php b/src/Uri.php index 70ad634..3946765 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -51,14 +51,14 @@ class Uri implements UriInterface public function __construct(string $uri = '') { if ('' !== $uri) { - if (false === $parts = \parse_url($uri)) { - throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); + if (false === $parts = parse_url($uri)) { + throw new \InvalidArgumentException(sprintf('Unable to parse URI: "%s"', $uri)); } // Apply parse_url parts to a URI. - $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->scheme = isset($parts['scheme']) ? strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->userInfo = $parts['user'] ?? ''; - $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->host = isset($parts['host']) ? strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; @@ -124,7 +124,7 @@ public function getPath(): string } elseif (isset($path[1]) && '/' === $path[1]) { // If the path is starting with more than one "/", the // starting slashes MUST be reduced to one. - $path = '/' . \ltrim($path, '/'); + $path = '/' . ltrim($path, '/'); } return $path; @@ -146,7 +146,7 @@ public function withScheme($scheme): self throw new \InvalidArgumentException('Scheme must be a string'); } - if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + if ($this->scheme === $scheme = strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } @@ -163,13 +163,13 @@ public function withUserInfo($user, $password = null): self throw new \InvalidArgumentException('User must be a string'); } - $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); + $info = preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); if (null !== $password && '' !== $password) { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string'); } - $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); + $info .= ':' . preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); } if ($this->userInfo === $info) { @@ -188,7 +188,7 @@ public function withHost($host): self throw new \InvalidArgumentException('Host must be a string'); } - if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + if ($this->host === $host = strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } @@ -270,7 +270,7 @@ private static function createUriString(string $scheme, string $authority, strin if ('' === $authority) { // If the path is starting with more than one "/" and no authority is present, the // starting slashes MUST be reduced to one. - $path = '/' . \ltrim($path, '/'); + $path = '/' . ltrim($path, '/'); } } @@ -303,8 +303,8 @@ private function filterPort($port): ?int } $port = (int) $port; - if (0 > $port || 0xffff < $port) { - throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); + if (0 > $port || 0xFFFF < $port) { + throw new \InvalidArgumentException(sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return self::isNonStandardPort($this->scheme, $port) ? $port : null; @@ -316,7 +316,7 @@ private function filterPath($path): string throw new \InvalidArgumentException('Path must be a string'); } - return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); + return preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); } private function filterQueryAndFragment($str): string @@ -325,11 +325,11 @@ private function filterQueryAndFragment($str): string throw new \InvalidArgumentException('Query and fragment must be a string'); } - return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); + return preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); } private static function rawurlencodeMatchZero(array $match): string { - return \rawurlencode($match[0]); + return rawurlencode($match[0]); } } diff --git a/tests/Integration/RequestTest.php b/tests/Integration/RequestTest.php index 8b06719..1f0c880 100644 --- a/tests/Integration/RequestTest.php +++ b/tests/Integration/RequestTest.php @@ -1,10 +1,10 @@ - 'NumericHeaderValue', - '0' => 'NumericHeaderValueZero', + '0' => 'NumericHeaderValueZero', ] ); $this->assertSame( [ '200' => ['NumericHeaderValue'], - '0' => ['NumericHeaderValueZero'], + '0' => ['NumericHeaderValueZero'], ], $r->getHeaders() ); @@ -231,7 +231,7 @@ public function testSupportNumericHeaderNames() $this->assertSame( [ '200' => ['NumericHeaderValue', 'A', 'B'], - '0' => ['NumericHeaderValueZero'], + '0' => ['NumericHeaderValueZero'], '300' => ['NumericHeaderValue2'], ], $r->getHeaders() @@ -241,7 +241,7 @@ public function testSupportNumericHeaderNames() $this->assertSame( [ '200' => ['NumericHeaderValue', 'A', 'B'], - '0' => ['NumericHeaderValueZero'], + '0' => ['NumericHeaderValueZero'], ], $r->getHeaders() ); diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index 4a3db77..2fabcb5 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -1,4 +1,4 @@ - new UploadedFile('test', 123, UPLOAD_ERR_OK), + 'file' => new UploadedFile('test', 123, \UPLOAD_ERR_OK), ]; $request2 = $request1->withUploadedFiles($files); diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 345a55c..abeeb17 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -1,4 +1,4 @@ -assertFalse(is_resource($handle)); + $this->assertFalse(\is_resource($handle)); } public function testConvertsToString() @@ -218,7 +218,7 @@ public function stream_open() return true; } - public function stream_seek(int $offset, int $whence = SEEK_SET) + public function stream_seek(int $offset, int $whence = \SEEK_SET) { return false; } diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 30fced3..9c1f35e 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -14,15 +14,15 @@ class UploadedFileTest extends TestCase { protected $cleanup; - public function setUp(): void + protected function setUp(): void { $this->cleanup = []; } - public function tearDown(): void + protected function tearDown(): void { foreach ($this->cleanup as $file) { - if (is_scalar($file) && file_exists($file)) { + if (\is_scalar($file) && file_exists($file)) { unlink($file); } } @@ -49,7 +49,7 @@ public function testRaisesExceptionOnInvalidStreamOrFile($streamOrFile) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('Invalid stream or file provided for UploadedFile'); - new UploadedFile($streamOrFile, 0, UPLOAD_ERR_OK); + new UploadedFile($streamOrFile, 0, \UPLOAD_ERR_OK); } public static function invalidErrorStatuses() @@ -98,7 +98,7 @@ public function testRaisesExceptionOnInvalidClientFilename($filename) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('filename'); - new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, $filename); + new UploadedFile(fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, $filename); } /** @@ -109,13 +109,13 @@ public function testRaisesExceptionOnInvalidClientMediaType($mediaType) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('media type'); - new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, 'foobar.baz', $mediaType); + new UploadedFile(fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, 'foobar.baz', $mediaType); } public function testGetStreamReturnsOriginalStreamObject() { $stream = Stream::create(''); - $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); + $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $this->assertSame($stream, $upload->getStream()); } @@ -123,7 +123,7 @@ public function testGetStreamReturnsOriginalStreamObject() public function testGetStreamReturnsWrappedPhpStream() { $stream = fopen('php://temp', 'wb+'); - $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); + $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $uploadStream = $upload->getStream()->detach(); $this->assertSame($stream, $uploadStream); @@ -131,16 +131,16 @@ public function testGetStreamReturnsWrappedPhpStream() public function testGetStream() { - $upload = new UploadedFile(__DIR__.'/Resources/foo.txt', 0, UPLOAD_ERR_OK); + $upload = new UploadedFile(__DIR__ . '/Resources/foo.txt', 0, \UPLOAD_ERR_OK); $stream = $upload->getStream(); $this->assertInstanceOf(StreamInterface::class, $stream); - $this->assertEquals('Foobar'.PHP_EOL, $stream->__toString()); + $this->assertEquals('Foobar' . \PHP_EOL, $stream->__toString()); } public function testSuccessful() { $stream = Stream::create('Foo bar!'); - $upload = new UploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'filename.txt', 'text/plain'); + $upload = new UploadedFile($stream, $stream->getSize(), \UPLOAD_ERR_OK, 'filename.txt', 'text/plain'); $this->assertEquals($stream->getSize(), $upload->getSize()); $this->assertEquals('filename.txt', $upload->getClientFilename()); @@ -172,7 +172,7 @@ public static function invalidMovePaths() public function testMoveRaisesExceptionForInvalidPath($path) { $stream = (new \Nyholm\Psr7\Factory\Psr17Factory())->createStream('Foo bar!'); - $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); + $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $this->cleanup[] = $path; @@ -184,7 +184,7 @@ public function testMoveRaisesExceptionForInvalidPath($path) public function testMoveCannotBeCalledMoreThanOnce() { $stream = (new \Nyholm\Psr7\Factory\Psr17Factory())->createStream('Foo bar!'); - $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); + $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'diac'); $upload->moveTo($to); @@ -198,7 +198,7 @@ public function testMoveCannotBeCalledMoreThanOnce() public function testCannotRetrieveStreamAfterMove() { $stream = (new \Nyholm\Psr7\Factory\Psr17Factory())->createStream('Foo bar!'); - $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); + $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'diac'); $upload->moveTo($to); @@ -212,13 +212,13 @@ public function testCannotRetrieveStreamAfterMove() public static function nonOkErrorStatus() { return [ - 'UPLOAD_ERR_INI_SIZE' => [UPLOAD_ERR_INI_SIZE], - 'UPLOAD_ERR_FORM_SIZE' => [UPLOAD_ERR_FORM_SIZE], - 'UPLOAD_ERR_PARTIAL' => [UPLOAD_ERR_PARTIAL], - 'UPLOAD_ERR_NO_FILE' => [UPLOAD_ERR_NO_FILE], - 'UPLOAD_ERR_NO_TMP_DIR' => [UPLOAD_ERR_NO_TMP_DIR], - 'UPLOAD_ERR_CANT_WRITE' => [UPLOAD_ERR_CANT_WRITE], - 'UPLOAD_ERR_EXTENSION' => [UPLOAD_ERR_EXTENSION], + 'UPLOAD_ERR_INI_SIZE' => [\UPLOAD_ERR_INI_SIZE], + 'UPLOAD_ERR_FORM_SIZE' => [\UPLOAD_ERR_FORM_SIZE], + 'UPLOAD_ERR_PARTIAL' => [\UPLOAD_ERR_PARTIAL], + 'UPLOAD_ERR_NO_FILE' => [\UPLOAD_ERR_NO_FILE], + 'UPLOAD_ERR_NO_TMP_DIR' => [\UPLOAD_ERR_NO_TMP_DIR], + 'UPLOAD_ERR_CANT_WRITE' => [\UPLOAD_ERR_CANT_WRITE], + 'UPLOAD_ERR_EXTENSION' => [\UPLOAD_ERR_EXTENSION], ]; } @@ -239,7 +239,7 @@ public function testMoveToRaisesExceptionWhenErrorStatusPresent($status) $uploadedFile = new UploadedFile('not ok', 0, $status); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('upload error'); - $uploadedFile->moveTo(__DIR__.'/'.uniqid()); + $uploadedFile->moveTo(__DIR__ . '/' . uniqid()); } /** @@ -260,7 +260,7 @@ public function testMoveToCreatesStreamIfOnlyAFilenameWasProvided() copy(__FILE__, $from); - $uploadedFile = new UploadedFile($from, 100, UPLOAD_ERR_OK, basename($from), 'text/plain'); + $uploadedFile = new UploadedFile($from, 100, \UPLOAD_ERR_OK, basename($from), 'text/plain'); $uploadedFile->moveTo($to); $this->assertFileEquals(__FILE__, $to); diff --git a/tests/UriTest.php b/tests/UriTest.php index be3beb0..0c80f5f 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -1,4 +1,4 @@ - Date: Wed, 12 Apr 2023 09:24:58 +0200 Subject: [PATCH 70/84] [CI] Remove badge for Travis (#233) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 9369f73..0ca2671 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # PSR-7 implementation [![Latest Version](https://img.shields.io/github/release/Nyholm/psr7.svg?style=flat-square)](https://github.com/Nyholm/psr7/releases) -[![Build Status](https://img.shields.io/travis/Nyholm/psr7/master.svg?style=flat-square)](https://travis-ci.org/Nyholm/psr7) [![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7) [![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) From 52887ca631ea10155313b9a5e14591bffdbd8f94 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Sun, 16 Apr 2023 09:42:07 +0200 Subject: [PATCH 71/84] [CS] native_function_invocation (#235) * [CS] native_function_invocation * fixed psalm baseline --- .php-cs-fixer.dist.php | 3 +-- psalm.baseline.xml | 2 +- src/Factory/Psr17Factory.php | 6 ++--- src/MessageTrait.php | 24 +++++++++---------- src/RequestTrait.php | 2 +- src/Response.php | 2 +- src/ServerRequest.php | 2 +- src/Stream.php | 46 ++++++++++++++++++------------------ src/UploadedFile.php | 12 +++++----- src/Uri.php | 28 +++++++++++----------- tests/StreamTest.php | 44 +++++++++++++++++----------------- tests/UploadedFileTest.php | 32 ++++++++++++------------- tests/UriTest.php | 2 +- 13 files changed, 102 insertions(+), 103 deletions(-) diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index f35538b..04765de 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -11,8 +11,7 @@ return $config->setRules([ '@Symfony' => true, '@Symfony:risky' => true, - 'array_syntax' => array('syntax' => 'short'), - 'native_function_invocation' => true, + 'native_function_invocation' => ['include'=> ['@all']], 'native_constant_invocation' => true, 'ordered_imports' => true, 'declare_strict_types' => false, diff --git a/psalm.baseline.xml b/psalm.baseline.xml index 8cdb7fa..fe5b92e 100644 --- a/psalm.baseline.xml +++ b/psalm.baseline.xml @@ -2,7 +2,7 @@ - return trigger_error((string) $e, \E_USER_ERROR); + return \trigger_error((string) $e, \E_USER_ERROR); diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 326e517..440bec3 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -41,12 +41,12 @@ public function createStreamFromFile(string $filename, string $mode = 'r'): Stre throw new \RuntimeException('Path cannot be empty'); } - if (false === $resource = @fopen($filename, $mode)) { + if (false === $resource = @\fopen($filename, $mode)) { if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { - throw new \InvalidArgumentException(sprintf('The mode "%s" is invalid.', $mode)); + throw new \InvalidArgumentException(\sprintf('The mode "%s" is invalid.', $mode)); } - throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $filename, error_get_last()['message'] ?? '')); + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $filename, \error_get_last()['message'] ?? '')); } return Stream::create($resource); diff --git a/src/MessageTrait.php b/src/MessageTrait.php index e8ec75f..2544fa4 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -57,7 +57,7 @@ public function getHeaders(): array public function hasHeader($header): bool { - return isset($this->headerNames[strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); + return isset($this->headerNames[\strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')]); } public function getHeader($header): array @@ -66,7 +66,7 @@ public function getHeader($header): array throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } - $header = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $header = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$header])) { return []; } @@ -78,13 +78,13 @@ public function getHeader($header): array public function getHeaderLine($header): string { - return implode(', ', $this->getHeader($header)); + return \implode(', ', $this->getHeader($header)); } public function withHeader($header, $value): self { $value = $this->validateAndTrimHeader($header, $value); - $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); $new = clone $this; if (isset($new->headerNames[$normalized])) { @@ -114,7 +114,7 @@ public function withoutHeader($header): self throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } - $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (!isset($this->headerNames[$normalized])) { return $this; } @@ -156,10 +156,10 @@ private function setHeaders(array $headers): void $header = (string) $header; } $value = $this->validateAndTrimHeader($header, $value); - $normalized = strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); + $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); if (isset($this->headerNames[$normalized])) { $header = $this->headerNames[$normalized]; - $this->headers[$header] = array_merge($this->headers[$header], $value); + $this->headers[$header] = \array_merge($this->headers[$header], $value); } else { $this->headerNames[$normalized] = $header; $this->headers[$header] = $value; @@ -187,17 +187,17 @@ private function setHeaders(array $headers): void */ private function validateAndTrimHeader($header, $values): array { - if (!\is_string($header) || 1 !== preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { + if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } if (!\is_array($values)) { // This is simple, just one value. - if ((!is_numeric($values) && !\is_string($values)) || 1 !== preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { + if ((!\is_numeric($values) && !\is_string($values)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $values)) { throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } - return [trim((string) $values, " \t")]; + return [\trim((string) $values, " \t")]; } if (empty($values)) { @@ -207,11 +207,11 @@ private function validateAndTrimHeader($header, $values): array // Assert Non empty array $returnValues = []; foreach ($values as $v) { - if ((!is_numeric($v) && !\is_string($v)) || 1 !== preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { + if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } - $returnValues[] = trim((string) $v, " \t"); + $returnValues[] = \trim((string) $v, " \t"); } return $returnValues; diff --git a/src/RequestTrait.php b/src/RequestTrait.php index 597874d..7c39bbb 100644 --- a/src/RequestTrait.php +++ b/src/RequestTrait.php @@ -46,7 +46,7 @@ public function withRequestTarget($requestTarget): self throw new \InvalidArgumentException('Request target must be a string'); } - if (preg_match('#\s#', $requestTarget)) { + if (\preg_match('#\s#', $requestTarget)) { throw new \InvalidArgumentException('Invalid request target provided; cannot contain whitespace'); } diff --git a/src/Response.php b/src/Response.php index 3062b3b..9a26d2c 100644 --- a/src/Response.php +++ b/src/Response.php @@ -75,7 +75,7 @@ public function withStatus($code, $reasonPhrase = ''): self $code = (int) $code; if ($code < 100 || $code > 599) { - throw new \InvalidArgumentException(sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); + throw new \InvalidArgumentException(\sprintf('Status code has to be an integer between 100 and 599. A status code of %d was given', $code)); } $new = clone $this; diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 1e47a06..7f5022e 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -56,7 +56,7 @@ public function __construct(string $method, $uri, array $headers = [], $body = n $this->uri = $uri; $this->setHeaders($headers); $this->protocol = $version; - parse_str($uri->getQuery(), $this->queryParams); + \parse_str($uri->getQuery(), $this->queryParams); if (!$this->hasHeader('Host')) { $this->updateHostFromUri(); diff --git a/src/Stream.php b/src/Stream.php index 2c08b9f..d173f35 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -61,8 +61,8 @@ public function __construct($body) } $this->stream = $body; - $meta = stream_get_meta_data($this->stream); - $this->seekable = $meta['seekable'] && 0 === fseek($this->stream, 0, \SEEK_CUR); + $meta = \stream_get_meta_data($this->stream); + $this->seekable = $meta['seekable'] && 0 === \fseek($this->stream, 0, \SEEK_CUR); $this->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); $this->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); } @@ -81,9 +81,9 @@ public static function create($body = ''): StreamInterface } if (\is_string($body)) { - $resource = fopen('php://temp', 'rw+'); - fwrite($resource, $body); - fseek($resource, 0); + $resource = \fopen('php://temp', 'rw+'); + \fwrite($resource, $body); + \fseek($resource, 0); $body = $resource; } @@ -118,13 +118,13 @@ public function __toString() throw $e; } - if (\is_array($errorHandler = set_error_handler('var_dump'))) { + if (\is_array($errorHandler = \set_error_handler('var_dump'))) { $errorHandler = $errorHandler[0] ?? null; } - restore_error_handler(); + \restore_error_handler(); if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { - return trigger_error((string) $e, \E_USER_ERROR); + return \trigger_error((string) $e, \E_USER_ERROR); } return ''; @@ -135,7 +135,7 @@ public function close(): void { if (isset($this->stream)) { if (\is_resource($this->stream)) { - fclose($this->stream); + \fclose($this->stream); } $this->detach(); } @@ -176,10 +176,10 @@ public function getSize(): ?int // Clear the stat cache if the stream has a URI if ($uri = $this->getUri()) { - clearstatcache(true, $uri); + \clearstatcache(true, $uri); } - $stats = fstat($this->stream); + $stats = \fstat($this->stream); if (isset($stats['size'])) { $this->size = $stats['size']; @@ -195,8 +195,8 @@ public function tell(): int throw new \RuntimeException('Stream is detached'); } - if (false === $result = @ftell($this->stream)) { - throw new \RuntimeException('Unable to determine stream position: ' . (error_get_last()['message'] ?? '')); + if (false === $result = @\ftell($this->stream)) { + throw new \RuntimeException('Unable to determine stream position: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -204,7 +204,7 @@ public function tell(): int public function eof(): bool { - return !isset($this->stream) || feof($this->stream); + return !isset($this->stream) || \feof($this->stream); } public function isSeekable(): bool @@ -222,8 +222,8 @@ public function seek($offset, $whence = \SEEK_SET): void throw new \RuntimeException('Stream is not seekable'); } - if (-1 === fseek($this->stream, $offset, $whence)) { - throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . var_export($whence, true)); + if (-1 === \fseek($this->stream, $offset, $whence)) { + throw new \RuntimeException('Unable to seek to stream position "' . $offset . '" with whence ' . \var_export($whence, true)); } } @@ -250,8 +250,8 @@ public function write($string): int // We can't know the size after writing anything $this->size = null; - if (false === $result = @fwrite($this->stream, $string)) { - throw new \RuntimeException('Unable to write to stream: ' . (error_get_last()['message'] ?? '')); + if (false === $result = @\fwrite($this->stream, $string)) { + throw new \RuntimeException('Unable to write to stream: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -272,8 +272,8 @@ public function read($length): string throw new \RuntimeException('Cannot read from non-readable stream'); } - if (false === $result = @fread($this->stream, $length)) { - throw new \RuntimeException('Unable to read from stream: ' . (error_get_last()['message'] ?? '')); + if (false === $result = @\fread($this->stream, $length)) { + throw new \RuntimeException('Unable to read from stream: ' . (\error_get_last()['message'] ?? '')); } return $result; @@ -285,8 +285,8 @@ public function getContents(): string throw new \RuntimeException('Stream is detached'); } - if (false === $contents = @stream_get_contents($this->stream)) { - throw new \RuntimeException('Unable to read stream contents: ' . (error_get_last()['message'] ?? '')); + if (false === $contents = @\stream_get_contents($this->stream)) { + throw new \RuntimeException('Unable to read stream contents: ' . (\error_get_last()['message'] ?? '')); } return $contents; @@ -305,7 +305,7 @@ public function getMetadata($key = null) return $key ? null : []; } - $meta = stream_get_meta_data($this->stream); + $meta = \stream_get_meta_data($this->stream); if (null === $key) { return $meta; diff --git a/src/UploadedFile.php b/src/UploadedFile.php index 0789943..c77dca4 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -114,8 +114,8 @@ public function getStream(): StreamInterface return $this->stream; } - if (false === $resource = @fopen($this->file, 'r')) { - throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $this->file, error_get_last()['message'] ?? '')); + if (false === $resource = @\fopen($this->file, 'r')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $this->file, \error_get_last()['message'] ?? '')); } return Stream::create($resource); @@ -130,10 +130,10 @@ public function moveTo($targetPath): void } if (null !== $this->file) { - $this->moved = 'cli' === \PHP_SAPI ? @rename($this->file, $targetPath) : @move_uploaded_file($this->file, $targetPath); + $this->moved = 'cli' === \PHP_SAPI ? @\rename($this->file, $targetPath) : @\move_uploaded_file($this->file, $targetPath); if (false === $this->moved) { - throw new \RuntimeException(sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, error_get_last()['message'] ?? '')); + throw new \RuntimeException(\sprintf('Uploaded file could not be moved to "%s": %s', $targetPath, \error_get_last()['message'] ?? '')); } } else { $stream = $this->getStream(); @@ -141,8 +141,8 @@ public function moveTo($targetPath): void $stream->rewind(); } - if (false === $resource = @fopen($targetPath, 'w')) { - throw new \RuntimeException(sprintf('The file "%s" cannot be opened: %s', $targetPath, error_get_last()['message'] ?? '')); + if (false === $resource = @\fopen($targetPath, 'w')) { + throw new \RuntimeException(\sprintf('The file "%s" cannot be opened: %s', $targetPath, \error_get_last()['message'] ?? '')); } $dest = Stream::create($resource); diff --git a/src/Uri.php b/src/Uri.php index 3946765..0d2c975 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -51,14 +51,14 @@ class Uri implements UriInterface public function __construct(string $uri = '') { if ('' !== $uri) { - if (false === $parts = parse_url($uri)) { - throw new \InvalidArgumentException(sprintf('Unable to parse URI: "%s"', $uri)); + if (false === $parts = \parse_url($uri)) { + throw new \InvalidArgumentException(\sprintf('Unable to parse URI: "%s"', $uri)); } // Apply parse_url parts to a URI. - $this->scheme = isset($parts['scheme']) ? strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->scheme = isset($parts['scheme']) ? \strtr($parts['scheme'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->userInfo = $parts['user'] ?? ''; - $this->host = isset($parts['host']) ? strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; + $this->host = isset($parts['host']) ? \strtr($parts['host'], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') : ''; $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; @@ -124,7 +124,7 @@ public function getPath(): string } elseif (isset($path[1]) && '/' === $path[1]) { // If the path is starting with more than one "/", the // starting slashes MUST be reduced to one. - $path = '/' . ltrim($path, '/'); + $path = '/' . \ltrim($path, '/'); } return $path; @@ -146,7 +146,7 @@ public function withScheme($scheme): self throw new \InvalidArgumentException('Scheme must be a string'); } - if ($this->scheme === $scheme = strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + if ($this->scheme === $scheme = \strtr($scheme, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } @@ -163,13 +163,13 @@ public function withUserInfo($user, $password = null): self throw new \InvalidArgumentException('User must be a string'); } - $info = preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); + $info = \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $user); if (null !== $password && '' !== $password) { if (!\is_string($password)) { throw new \InvalidArgumentException('Password must be a string'); } - $info .= ':' . preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); + $info .= ':' . \preg_replace_callback('/[' . self::CHAR_GEN_DELIMS . self::CHAR_SUB_DELIMS . ']++/', [__CLASS__, 'rawurlencodeMatchZero'], $password); } if ($this->userInfo === $info) { @@ -188,7 +188,7 @@ public function withHost($host): self throw new \InvalidArgumentException('Host must be a string'); } - if ($this->host === $host = strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { + if ($this->host === $host = \strtr($host, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')) { return $this; } @@ -270,7 +270,7 @@ private static function createUriString(string $scheme, string $authority, strin if ('' === $authority) { // If the path is starting with more than one "/" and no authority is present, the // starting slashes MUST be reduced to one. - $path = '/' . ltrim($path, '/'); + $path = '/' . \ltrim($path, '/'); } } @@ -304,7 +304,7 @@ private function filterPort($port): ?int $port = (int) $port; if (0 > $port || 0xFFFF < $port) { - throw new \InvalidArgumentException(sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); + throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 0 and 65535', $port)); } return self::isNonStandardPort($this->scheme, $port) ? $port : null; @@ -316,7 +316,7 @@ private function filterPath($path): string throw new \InvalidArgumentException('Path must be a string'); } - return preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $path); } private function filterQueryAndFragment($str): string @@ -325,11 +325,11 @@ private function filterQueryAndFragment($str): string throw new \InvalidArgumentException('Query and fragment must be a string'); } - return preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); + return \preg_replace_callback('/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [__CLASS__, 'rawurlencodeMatchZero'], $str); } private static function rawurlencodeMatchZero(array $match): string { - return rawurlencode($match[0]); + return \rawurlencode($match[0]); } } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index abeeb17..a77d5c4 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -13,8 +13,8 @@ class StreamTest extends TestCase { public function testConstructorInitializesProperties() { - $handle = fopen('php://temp', 'r+'); - fwrite($handle, 'data'); + $handle = \fopen('php://temp', 'r+'); + \fwrite($handle, 'data'); $stream = Stream::create($handle); $this->assertTrue($stream->isReadable()); $this->assertTrue($stream->isWritable()); @@ -42,7 +42,7 @@ public function testConstructorSeekWithStringContent() public function testStreamClosesHandleOnDestruct() { - $handle = fopen('php://temp', 'r'); + $handle = \fopen('php://temp', 'r'); $stream = Stream::create($handle); unset($stream); $this->assertFalse(\is_resource($handle)); @@ -50,8 +50,8 @@ public function testStreamClosesHandleOnDestruct() public function testConvertsToString() { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, 'data'); + $handle = \fopen('php://temp', 'w+'); + \fwrite($handle, 'data'); $stream = Stream::create($handle); $this->assertEquals('data', (string) $stream); $this->assertEquals('data', (string) $stream); @@ -68,8 +68,8 @@ public function testBuildFromString() public function testGetsContents() { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, 'data'); + $handle = \fopen('php://temp', 'w+'); + \fwrite($handle, 'data'); $stream = Stream::create($handle); $this->assertEquals('', $stream->getContents()); $stream->seek(0); @@ -79,8 +79,8 @@ public function testGetsContents() public function testChecksEof() { - $handle = fopen('php://temp', 'w+'); - fwrite($handle, 'data'); + $handle = \fopen('php://temp', 'w+'); + \fwrite($handle, 'data'); $stream = Stream::create($handle); $this->assertFalse($stream->eof()); $stream->read(4); @@ -90,8 +90,8 @@ public function testChecksEof() public function testGetSize() { - $size = filesize(__FILE__); - $handle = fopen(__FILE__, 'r'); + $size = \filesize(__FILE__); + $handle = \fopen(__FILE__, 'r'); $stream = Stream::create($handle); $this->assertEquals($size, $stream->getSize()); // Load from cache @@ -101,8 +101,8 @@ public function testGetSize() public function testEnsuresSizeIsConsistent() { - $h = fopen('php://temp', 'w+'); - $this->assertEquals(3, fwrite($h, 'foo')); + $h = \fopen('php://temp', 'w+'); + $this->assertEquals(3, \fwrite($h, 'foo')); $stream = Stream::create($h); $this->assertEquals(3, $stream->getSize()); $this->assertEquals(4, $stream->write('test')); @@ -113,20 +113,20 @@ public function testEnsuresSizeIsConsistent() public function testProvidesStreamPosition() { - $handle = fopen('php://temp', 'w+'); + $handle = \fopen('php://temp', 'w+'); $stream = Stream::create($handle); $this->assertEquals(0, $stream->tell()); $stream->write('foo'); $this->assertEquals(3, $stream->tell()); $stream->seek(1); $this->assertEquals(1, $stream->tell()); - $this->assertSame(ftell($handle), $stream->tell()); + $this->assertSame(\ftell($handle), $stream->tell()); $stream->close(); } public function testCanDetachStream() { - $r = fopen('php://temp', 'w+'); + $r = \fopen('php://temp', 'w+'); $stream = Stream::create($r); $stream->write('foo'); $this->assertTrue($stream->isReadable()); @@ -178,8 +178,8 @@ public function testCanDetachStream() $throws(function ($stream) { (string) $stream; }); - restore_error_handler(); - restore_exception_handler(); + \restore_error_handler(); + \restore_exception_handler(); } $stream->close(); @@ -187,7 +187,7 @@ public function testCanDetachStream() public function testCloseClearProperties() { - $handle = fopen('php://temp', 'r+'); + $handle = \fopen('php://temp', 'r+'); $stream = Stream::create($handle); $stream->close(); @@ -200,9 +200,9 @@ public function testCloseClearProperties() public function testUnseekableStreamWrapper() { - stream_wrapper_register('nyholm-psr7-test', TestStreamWrapper::class); - $handle = fopen('nyholm-psr7-test://', 'r'); - stream_wrapper_unregister('nyholm-psr7-test'); + \stream_wrapper_register('nyholm-psr7-test', TestStreamWrapper::class); + $handle = \fopen('nyholm-psr7-test://', 'r'); + \stream_wrapper_unregister('nyholm-psr7-test'); $stream = Stream::create($handle); $this->assertFalse($stream->isSeekable()); diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index 9c1f35e..8a8f9f8 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -22,8 +22,8 @@ protected function setUp(): void protected function tearDown(): void { foreach ($this->cleanup as $file) { - if (\is_scalar($file) && file_exists($file)) { - unlink($file); + if (\is_scalar($file) && \file_exists($file)) { + \unlink($file); } } } @@ -75,7 +75,7 @@ public function testRaisesExceptionOnInvalidErrorStatus($status) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('status'); - new UploadedFile(fopen('php://temp', 'wb+'), 0, $status); + new UploadedFile(\fopen('php://temp', 'wb+'), 0, $status); } public static function invalidFilenamesAndMediaTypes() @@ -98,7 +98,7 @@ public function testRaisesExceptionOnInvalidClientFilename($filename) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('filename'); - new UploadedFile(fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, $filename); + new UploadedFile(\fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, $filename); } /** @@ -109,7 +109,7 @@ public function testRaisesExceptionOnInvalidClientMediaType($mediaType) $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('media type'); - new UploadedFile(fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, 'foobar.baz', $mediaType); + new UploadedFile(\fopen('php://temp', 'wb+'), 0, \UPLOAD_ERR_OK, 'foobar.baz', $mediaType); } public function testGetStreamReturnsOriginalStreamObject() @@ -122,7 +122,7 @@ public function testGetStreamReturnsOriginalStreamObject() public function testGetStreamReturnsWrappedPhpStream() { - $stream = fopen('php://temp', 'wb+'); + $stream = \fopen('php://temp', 'wb+'); $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); $uploadStream = $upload->getStream()->detach(); @@ -146,10 +146,10 @@ public function testSuccessful() $this->assertEquals('filename.txt', $upload->getClientFilename()); $this->assertEquals('text/plain', $upload->getClientMediaType()); - $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'successful'); + $this->cleanup[] = $to = \tempnam(\sys_get_temp_dir(), 'successful'); $upload->moveTo($to); $this->assertFileExists($to); - $this->assertEquals($stream->__toString(), file_get_contents($to)); + $this->assertEquals($stream->__toString(), \file_get_contents($to)); } public static function invalidMovePaths() @@ -186,9 +186,9 @@ public function testMoveCannotBeCalledMoreThanOnce() $stream = (new \Nyholm\Psr7\Factory\Psr17Factory())->createStream('Foo bar!'); $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); - $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'diac'); + $this->cleanup[] = $to = \tempnam(\sys_get_temp_dir(), 'diac'); $upload->moveTo($to); - $this->assertTrue(file_exists($to)); + $this->assertTrue(\file_exists($to)); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('moved'); @@ -200,7 +200,7 @@ public function testCannotRetrieveStreamAfterMove() $stream = (new \Nyholm\Psr7\Factory\Psr17Factory())->createStream('Foo bar!'); $upload = new UploadedFile($stream, 0, \UPLOAD_ERR_OK); - $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'diac'); + $this->cleanup[] = $to = \tempnam(\sys_get_temp_dir(), 'diac'); $upload->moveTo($to); $this->assertFileExists($to); @@ -239,7 +239,7 @@ public function testMoveToRaisesExceptionWhenErrorStatusPresent($status) $uploadedFile = new UploadedFile('not ok', 0, $status); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('upload error'); - $uploadedFile->moveTo(__DIR__ . '/' . uniqid()); + $uploadedFile->moveTo(__DIR__ . '/' . \uniqid()); } /** @@ -255,12 +255,12 @@ public function testGetStreamRaisesExceptionWhenErrorStatusPresent($status) public function testMoveToCreatesStreamIfOnlyAFilenameWasProvided() { - $this->cleanup[] = $from = tempnam(sys_get_temp_dir(), 'copy_from'); - $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'copy_to'); + $this->cleanup[] = $from = \tempnam(\sys_get_temp_dir(), 'copy_from'); + $this->cleanup[] = $to = \tempnam(\sys_get_temp_dir(), 'copy_to'); - copy(__FILE__, $from); + \copy(__FILE__, $from); - $uploadedFile = new UploadedFile($from, 100, \UPLOAD_ERR_OK, basename($from), 'text/plain'); + $uploadedFile = new UploadedFile($from, 100, \UPLOAD_ERR_OK, \basename($from), 'text/plain'); $uploadedFile->moveTo($to); $this->assertFileEquals(__FILE__, $to); diff --git a/tests/UriTest.php b/tests/UriTest.php index 0c80f5f..4aed446 100644 --- a/tests/UriTest.php +++ b/tests/UriTest.php @@ -158,7 +158,7 @@ public function testParseUriPortCannotBeNegative() public function testParseUriPortCanBeZero() { - if (version_compare(\PHP_VERSION, '7.4.12') < 0) { + if (\version_compare(\PHP_VERSION, '7.4.12') < 0) { self::markTestSkipped('Skipping this on low PHP versions.'); } From 1029a2671cbdd3e075a21952082c2be7c8018426 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 17 Apr 2023 18:00:04 +0200 Subject: [PATCH 72/84] Merge pull request from GHSA-wjfc-pgfp-pv9c Improper Input Validation in headers --- src/MessageTrait.php | 4 ++-- tests/RequestTest.php | 46 ++++++++++++++++++++++++++++++++++++++++++ tests/ResponseTest.php | 31 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 2544fa4..da34e58 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -187,7 +187,7 @@ private function setHeaders(array $headers): void */ private function validateAndTrimHeader($header, $values): array { - if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $header)) { + if (!\is_string($header) || 1 !== \preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@D", $header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); } @@ -207,7 +207,7 @@ private function validateAndTrimHeader($header, $values): array // Assert Non empty array $returnValues = []; foreach ($values as $v) { - if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@", (string) $v)) { + if ((!\is_numeric($v) && !\is_string($v)) || 1 !== \preg_match("@^[ \t\x21-\x7E\x80-\xFF]*$@D", (string) $v)) { throw new \InvalidArgumentException('Header values must be RFC 7230 compatible strings'); } diff --git a/tests/RequestTest.php b/tests/RequestTest.php index ad6926c..43dfd93 100644 --- a/tests/RequestTest.php +++ b/tests/RequestTest.php @@ -294,4 +294,50 @@ public function testUpdateHostFromUri() $request = $request->withUri(new Uri('https://nyholm.tech:443')); $this->assertEquals('nyholm.tech', $request->getHeaderLine('Host')); } + + /** + * @dataProvider provideHeaderValuesContainingNotAllowedChars + */ + public function testCannotHaveHeaderWithInvalidValue(string $name) + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Header name must be an RFC 7230 compatible string'); + $r = new Request('GET', 'https://example.com/'); + $r->withHeader($name, 'Bar'); + } + + public static function provideHeaderValuesContainingNotAllowedChars(): array + { + // Explicit tests for newlines as the most common exploit vector. + $tests = [ + ["new\nline"], + ["new\r\nline"], + ["new\rline"], + ["new\r\n line"], + ["newline\n"], + ["\nnewline"], + ["newline\r\n"], + ["\n\rnewline"], + ]; + + for ($i = 0; $i <= 0xFF; ++$i) { + if ("\t" == \chr($i)) { + continue; + } + if (' ' == \chr($i)) { + continue; + } + if ($i >= 0x21 && $i <= 0x7E) { + continue; + } + if ($i >= 0x80) { + continue; + } + + $tests[] = ['foo' . \chr($i) . 'bar']; + $tests[] = ['foo' . \chr($i)]; + } + + return $tests; + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index c60246d..7f20828 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -274,4 +274,35 @@ public function testHeaderValuesAreTrimmed($r) $this->assertSame('Foo', $r->getHeaderLine('OWS')); $this->assertSame(['Foo'], $r->getHeader('OWS')); } + + /** + * @dataProvider invalidWithHeaderProvider + */ + public function testWithInvalidHeader($header, $headerValue, $expectedMessage): void + { + $r = new Response(); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); + $r->withHeader($header, $headerValue); + } + + public function invalidWithHeaderProvider(): iterable + { + return [ + ['foo', [], 'Header values must be a string or an array of strings, empty array given'], + ['foo', new \stdClass(), 'Header values must be RFC 7230 compatible strings'], + [[], 'foo', 'Header name must be an RFC 7230 compatible string'], + [false, 'foo', 'Header name must be an RFC 7230 compatible string'], + [new \stdClass(), 'foo', 'Header name must be an RFC 7230 compatible string'], + ['', 'foo', 'Header name must be an RFC 7230 compatible string'], + ["Content-Type\r\n\r\n", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["Content-Type\r\n", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["Content-Type\n", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["\r\nContent-Type", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["\nContent-Type", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["\n", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["\r\n", 'foo', 'Header name must be an RFC 7230 compatible string'], + ["\t", 'foo', 'Header name must be an RFC 7230 compatible string'], + ]; + } } From e874c8c4286a1e010fb4f385f3a55ac56a05cc93 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 17 Apr 2023 18:03:48 +0200 Subject: [PATCH 73/84] Added changelog (#236) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 462b677..fecd1b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.6.1 + +- Security fix: CVE-2023-29197 + ## 1.6.0 ### Changed From 07c455ab0563b67b98d16e838d43cbd6dcffdb9c Mon Sep 17 00:00:00 2001 From: Simon Podlipsky Date: Mon, 17 Apr 2023 18:06:53 +0200 Subject: [PATCH 74/84] chore: drop php 7.1 support (#226) --- .github/workflows/tests.yml | 1 - composer.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 4d4e52d..80e93be 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,7 +15,6 @@ jobs: operating-system: - 'ubuntu-latest' php-version: - - '7.1' - '7.2' - '7.3' - '7.4' diff --git a/composer.json b/composer.json index 680459c..0f45a24 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,7 @@ } ], "require": { - "php": ">=7.1", + "php": ">=7.2", "psr/http-message": "^1.0", "php-http/message-factory": "^1.0", "psr/http-factory": "^1.0" From 6a31c2723c713dc63ffbb26e7a2e1b34ee00aade Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 20 Apr 2023 08:58:27 +0200 Subject: [PATCH 75/84] Allow psr/http-message v2 (#234) --- .github/workflows/static.yml | 2 +- composer.json | 4 +-- phpstan-baseline.neon | 5 ---- src/MessageTrait.php | 26 ++++++++++++---- src/RequestTrait.php | 16 ++++++++-- src/Response.php | 5 +++- src/ServerRequest.php | 18 ++++++++---- src/Stream.php | 33 ++------------------- src/StreamTrait.php | 57 ++++++++++++++++++++++++++++++++++++ src/Uri.php | 35 +++++++++++++++++----- tests/StreamTest.php | 3 +- 11 files changed, 142 insertions(+), 62 deletions(-) create mode 100644 src/StreamTrait.php diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 1af3110..f3c88b7 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -61,7 +61,7 @@ jobs: php-version: 8.1 extensions: apcu, redis coverage: none - tools: vimeo/psalm:4.29.0 + tools: vimeo/psalm:5.9 - name: Download dependencies uses: ramsey/composer-install@v2 diff --git a/composer.json b/composer.json index 0f45a24..a676df5 100644 --- a/composer.json +++ b/composer.json @@ -16,13 +16,13 @@ ], "require": { "php": ">=7.2", - "psr/http-message": "^1.0", + "psr/http-message": "^1.1 || ^2.0", "php-http/message-factory": "^1.0", "psr/http-factory": "^1.0" }, "require-dev": { "phpunit/phpunit": "^7.5 || 8.5 || 9.4", - "php-http/psr7-integration-tests": "^1.0", + "php-http/psr7-integration-tests": "^1.0@dev", "http-interop/http-factory-tests": "^0.9", "symfony/error-handler": "^4.4" }, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 80991e8..9a0cf11 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -20,11 +20,6 @@ parameters: count: 1 path: src/ServerRequest.php - - - message: "#^Parameter \\#1 \\$callback of function set_error_handler expects \\(callable\\(int, string, string, int\\)\\: bool\\)\\|null, 'var_dump' given\\.$#" - count: 1 - path: src/Stream.php - - message: "#^Result of && is always false\\.$#" count: 1 diff --git a/src/MessageTrait.php b/src/MessageTrait.php index da34e58..7d02383 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -4,6 +4,7 @@ namespace Nyholm\Psr7; +use Psr\Http\Message\MessageInterface; use Psr\Http\Message\StreamInterface; /** @@ -34,7 +35,10 @@ public function getProtocolVersion(): string return $this->protocol; } - public function withProtocolVersion($version): self + /** + * @return static + */ + public function withProtocolVersion($version): MessageInterface { if (!\is_scalar($version)) { throw new \InvalidArgumentException('Protocol version must be a string'); @@ -81,7 +85,10 @@ public function getHeaderLine($header): string return \implode(', ', $this->getHeader($header)); } - public function withHeader($header, $value): self + /** + * @return static + */ + public function withHeader($header, $value): MessageInterface { $value = $this->validateAndTrimHeader($header, $value); $normalized = \strtr($header, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); @@ -96,7 +103,10 @@ public function withHeader($header, $value): self return $new; } - public function withAddedHeader($header, $value): self + /** + * @return static + */ + public function withAddedHeader($header, $value): MessageInterface { if (!\is_string($header) || '' === $header) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); @@ -108,7 +118,10 @@ public function withAddedHeader($header, $value): self return $new; } - public function withoutHeader($header): self + /** + * @return static + */ + public function withoutHeader($header): MessageInterface { if (!\is_string($header)) { throw new \InvalidArgumentException('Header name must be an RFC 7230 compatible string'); @@ -135,7 +148,10 @@ public function getBody(): StreamInterface return $this->stream; } - public function withBody(StreamInterface $body): self + /** + * @return static + */ + public function withBody(StreamInterface $body): MessageInterface { if ($body === $this->stream) { return $this; diff --git a/src/RequestTrait.php b/src/RequestTrait.php index 7c39bbb..2dbb3ab 100644 --- a/src/RequestTrait.php +++ b/src/RequestTrait.php @@ -4,6 +4,7 @@ namespace Nyholm\Psr7; +use Psr\Http\Message\RequestInterface; use Psr\Http\Message\UriInterface; /** @@ -40,7 +41,10 @@ public function getRequestTarget(): string return $target; } - public function withRequestTarget($requestTarget): self + /** + * @return static + */ + public function withRequestTarget($requestTarget): RequestInterface { if (!\is_string($requestTarget)) { throw new \InvalidArgumentException('Request target must be a string'); @@ -61,7 +65,10 @@ public function getMethod(): string return $this->method; } - public function withMethod($method): self + /** + * @return static + */ + public function withMethod($method): RequestInterface { if (!\is_string($method)) { throw new \InvalidArgumentException('Method must be a string'); @@ -78,7 +85,10 @@ public function getUri(): UriInterface return $this->uri; } - public function withUri(UriInterface $uri, $preserveHost = false): self + /** + * @return static + */ + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface { if ($uri === $this->uri) { return $this; diff --git a/src/Response.php b/src/Response.php index 9a26d2c..f3e2097 100644 --- a/src/Response.php +++ b/src/Response.php @@ -67,7 +67,10 @@ public function getReasonPhrase(): string return $this->reasonPhrase; } - public function withStatus($code, $reasonPhrase = ''): self + /** + * @return static + */ + public function withStatus($code, $reasonPhrase = ''): ResponseInterface { if (!\is_int($code) && !\is_string($code)) { throw new \InvalidArgumentException('Status code has to be an integer'); diff --git a/src/ServerRequest.php b/src/ServerRequest.php index 7f5022e..a3c5ba9 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -81,7 +81,7 @@ public function getUploadedFiles(): array /** * @return static */ - public function withUploadedFiles(array $uploadedFiles) + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface { $new = clone $this; $new->uploadedFiles = $uploadedFiles; @@ -97,7 +97,7 @@ public function getCookieParams(): array /** * @return static */ - public function withCookieParams(array $cookies) + public function withCookieParams(array $cookies): ServerRequestInterface { $new = clone $this; $new->cookieParams = $cookies; @@ -113,7 +113,7 @@ public function getQueryParams(): array /** * @return static */ - public function withQueryParams(array $query) + public function withQueryParams(array $query): ServerRequestInterface { $new = clone $this; $new->queryParams = $query; @@ -132,7 +132,7 @@ public function getParsedBody() /** * @return static */ - public function withParsedBody($data) + public function withParsedBody($data): ServerRequestInterface { if (!\is_array($data) && !\is_object($data) && null !== $data) { throw new \InvalidArgumentException('First parameter to withParsedBody MUST be object, array or null'); @@ -165,7 +165,10 @@ public function getAttribute($attribute, $default = null) return $this->attributes[$attribute]; } - public function withAttribute($attribute, $value): self + /** + * @return static + */ + public function withAttribute($attribute, $value): ServerRequestInterface { if (!\is_string($attribute)) { throw new \InvalidArgumentException('Attribute name must be a string'); @@ -177,7 +180,10 @@ public function withAttribute($attribute, $value): self return $new; } - public function withoutAttribute($attribute): self + /** + * @return static + */ + public function withoutAttribute($attribute): ServerRequestInterface { if (!\is_string($attribute)) { throw new \InvalidArgumentException('Attribute name must be a string'); diff --git a/src/Stream.php b/src/Stream.php index d173f35..9ed9bec 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -5,8 +5,6 @@ namespace Nyholm\Psr7; use Psr\Http\Message\StreamInterface; -use Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler; -use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler; /** * @author Michael Dowling and contributors to guzzlehttp/psr7 @@ -17,6 +15,8 @@ */ class Stream implements StreamInterface { + use StreamTrait; + /** @var resource|null A resource reference */ private $stream; @@ -102,35 +102,6 @@ public function __destruct() $this->close(); } - /** - * @return string - */ - public function __toString() - { - try { - if ($this->isSeekable()) { - $this->seek(0); - } - - return $this->getContents(); - } catch (\Throwable $e) { - if (\PHP_VERSION_ID >= 70400) { - throw $e; - } - - if (\is_array($errorHandler = \set_error_handler('var_dump'))) { - $errorHandler = $errorHandler[0] ?? null; - } - \restore_error_handler(); - - if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { - return \trigger_error((string) $e, \E_USER_ERROR); - } - - return ''; - } - } - public function close(): void { if (isset($this->stream)) { diff --git a/src/StreamTrait.php b/src/StreamTrait.php new file mode 100644 index 0000000..41a3f9d --- /dev/null +++ b/src/StreamTrait.php @@ -0,0 +1,57 @@ += 70400 || (new \ReflectionMethod(StreamInterface::class, '__toString'))->hasReturnType()) { + /** + * @internal + */ + trait StreamTrait + { + public function __toString(): string + { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } + } +} else { + /** + * @internal + */ + trait StreamTrait + { + /** + * @return string + */ + public function __toString() + { + try { + if ($this->isSeekable()) { + $this->seek(0); + } + + return $this->getContents(); + } catch (\Throwable $e) { + if (\is_array($errorHandler = \set_error_handler('var_dump'))) { + $errorHandler = $errorHandler[0] ?? null; + } + \restore_error_handler(); + + if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { + return \trigger_error((string) $e, \E_USER_ERROR); + } + + return ''; + } + } + } +} diff --git a/src/Uri.php b/src/Uri.php index 0d2c975..621e2e7 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -140,7 +140,10 @@ public function getFragment(): string return $this->fragment; } - public function withScheme($scheme): self + /** + * @return static + */ + public function withScheme($scheme): UriInterface { if (!\is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); @@ -157,7 +160,10 @@ public function withScheme($scheme): self return $new; } - public function withUserInfo($user, $password = null): self + /** + * @return static + */ + public function withUserInfo($user, $password = null): UriInterface { if (!\is_string($user)) { throw new \InvalidArgumentException('User must be a string'); @@ -182,7 +188,10 @@ public function withUserInfo($user, $password = null): self return $new; } - public function withHost($host): self + /** + * @return static + */ + public function withHost($host): UriInterface { if (!\is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); @@ -198,7 +207,10 @@ public function withHost($host): self return $new; } - public function withPort($port): self + /** + * @return static + */ + public function withPort($port): UriInterface { if ($this->port === $port = $this->filterPort($port)) { return $this; @@ -210,7 +222,10 @@ public function withPort($port): self return $new; } - public function withPath($path): self + /** + * @return static + */ + public function withPath($path): UriInterface { if ($this->path === $path = $this->filterPath($path)) { return $this; @@ -222,7 +237,10 @@ public function withPath($path): self return $new; } - public function withQuery($query): self + /** + * @return static + */ + public function withQuery($query): UriInterface { if ($this->query === $query = $this->filterQueryAndFragment($query)) { return $this; @@ -234,7 +252,10 @@ public function withQuery($query): self return $new; } - public function withFragment($fragment): self + /** + * @return static + */ + public function withFragment($fragment): UriInterface { if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { return $this; diff --git a/tests/StreamTest.php b/tests/StreamTest.php index a77d5c4..c9973da 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -4,6 +4,7 @@ use Nyholm\Psr7\Stream; use PHPUnit\Framework\TestCase; +use Psr\Http\Message\StreamInterface; use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler; /** @@ -171,7 +172,7 @@ public function testCanDetachStream() $throws(function ($stream) { (string) $stream; }); - } else { + } elseif (!(new \ReflectionMethod(StreamInterface::class, '__toString'))->hasReturnType()) { $this->assertSame('', (string) $stream); SymfonyErrorHandler::register(); From 083ce3623dbb5b0e597255de4639eda5f9c06caf Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 20 Apr 2023 09:10:55 +0200 Subject: [PATCH 76/84] Use zval storage for Stream instances created from strings (#230) --- composer.json | 2 +- src/Stream.php | 105 +++++++++++++++++++++++++++++++++++++++++-- tests/StreamTest.php | 2 +- 3 files changed, 103 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index a676df5..470c937 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-master": "1.7-dev" } } } diff --git a/src/Stream.php b/src/Stream.php index 9ed9bec..65682ac 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -81,10 +81,7 @@ public static function create($body = ''): StreamInterface } if (\is_string($body)) { - $resource = \fopen('php://temp', 'rw+'); - \fwrite($resource, $body); - \fseek($resource, 0); - $body = $resource; + $body = self::openZvalStream($body); } if (!\is_resource($body)) { @@ -284,4 +281,104 @@ public function getMetadata($key = null) return $meta[$key] ?? null; } + + private static function openZvalStream(string $body) + { + static $wrapper; + + $wrapper ?? \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper = \get_class(new class() { + public $context; + + private $data; + private $position = 0; + + public function stream_open(): bool + { + $this->data = \stream_context_get_options($this->context)['Nyholm-Psr7-Zval']['data']; + \stream_context_set_option($this->context, 'Nyholm-Psr7-Zval', 'data', null); + + return true; + } + + public function stream_read(int $count): string + { + $result = \substr($this->data, $this->position, $count); + $this->position += \strlen($result); + + return $result; + } + + public function stream_write(string $data): int + { + $this->data = \substr_replace($this->data, $data, $this->position, \strlen($data)); + $this->position += \strlen($data); + + return \strlen($data); + } + + public function stream_tell(): int + { + return $this->position; + } + + public function stream_eof(): bool + { + return \strlen($this->data) <= $this->position; + } + + public function stream_stat(): array + { + return [ + 'mode' => 33206, // POSIX_S_IFREG | 0666 + 'nlink' => 1, + 'rdev' => -1, + 'size' => \strlen($this->data), + 'blksize' => -1, + 'blocks' => -1, + ]; + } + + public function stream_seek(int $offset, int $whence): bool + { + if (\SEEK_SET === $whence && (0 <= $offset && \strlen($this->data) >= $offset)) { + $this->position = $offset; + } elseif (\SEEK_CUR === $whence && 0 <= $offset) { + $this->position += $offset; + } elseif (\SEEK_END === $whence && (0 > $offset && 0 <= $offset = \strlen($this->data) + $offset)) { + $this->position = $offset; + } else { + return false; + } + + return true; + } + + public function stream_set_option(): bool + { + return true; + } + + public function stream_truncate(int $new_size): bool + { + if ($new_size) { + $this->data = \substr($this->data, 0, $new_size); + $this->position = \min($this->position, $new_size); + } else { + $this->data = ''; + $this->position = 0; + } + + return true; + } + })); + + $context = \stream_context_create(['Nyholm-Psr7-Zval' => ['data' => $body]]); + + if (!$stream = @\fopen('Nyholm-Psr7-Zval://', 'r+', false, $context)) { + \stream_wrapper_register('Nyholm-Psr7-Zval', $wrapper); + $stream = \fopen('Nyholm-Psr7-Zval://', 'r+', false, $context); + } + + return $stream; + } } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index c9973da..8f9c5bf 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -33,7 +33,7 @@ public function testConstructorSeekWithStringContent() $this->assertTrue($stream->isReadable()); $this->assertTrue($stream->isWritable()); $this->assertTrue($stream->isSeekable()); - $this->assertEquals('php://temp', $stream->getMetadata('uri')); + $this->assertEquals('Nyholm-Psr7-Zval://', $stream->getMetadata('uri')); $this->assertTrue(\is_array($stream->getMetadata())); $this->assertSame(5, $stream->getSize()); $this->assertFalse($stream->eof()); From 21644a7205f7ef3ccc14e26cbf0ac26af70937c9 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 20 Apr 2023 10:15:25 +0200 Subject: [PATCH 77/84] Improve performance of creating streams from strings (#241) --- src/Stream.php | 9 ++++++++- tests/StreamTest.php | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Stream.php b/src/Stream.php index 65682ac..d3bd78d 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -81,7 +81,14 @@ public static function create($body = ''): StreamInterface } if (\is_string($body)) { - $body = self::openZvalStream($body); + if (200000 <= \strlen($body)) { + $body = self::openZvalStream($body); + } else { + $resource = \fopen('php://memory', 'r+'); + \fwrite($resource, $body); + \fseek($resource, 0); + $body = $resource; + } } if (!\is_resource($body)) { diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 8f9c5bf..d3b3f63 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -29,16 +29,21 @@ public function testConstructorInitializesProperties() public function testConstructorSeekWithStringContent() { - $stream = Stream::create('Hello'); + $content = \str_repeat('Hello', 50000); + $stream = Stream::create($content); $this->assertTrue($stream->isReadable()); $this->assertTrue($stream->isWritable()); $this->assertTrue($stream->isSeekable()); $this->assertEquals('Nyholm-Psr7-Zval://', $stream->getMetadata('uri')); $this->assertTrue(\is_array($stream->getMetadata())); - $this->assertSame(5, $stream->getSize()); + $this->assertSame(250000, $stream->getSize()); $this->assertFalse($stream->eof()); - $this->assertSame('Hello', $stream->getContents()); + $this->assertSame($content, $stream->getContents()); $stream->close(); + + $stream = Stream::create('Hello'); + $this->assertEquals('php://memory', $stream->getMetadata('uri')); + $this->assertSame('Hello', $stream->getContents()); } public function testStreamClosesHandleOnDestruct() From ed7cf98f6562831dbc3c962406b5e49dc8179c8c Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 20 Apr 2023 10:38:48 +0200 Subject: [PATCH 78/84] Update changelog (#240) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fecd1b9..dcdedec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.7.0 + +- Bump to PHP 7.2 minimum +- Allow psr/http-message v2 +- Use copy-on-write for streams created from strings + ## 1.6.1 - Security fix: CVE-2023-29197 From 7f77c0eaefdb849630df611ba2204ce92dfe5ef5 Mon Sep 17 00:00:00 2001 From: Alexis Lefebvre Date: Thu, 20 Apr 2023 12:02:22 +0200 Subject: [PATCH 79/84] README.md: add badges for GH Actions (#237) --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0ca2671..7fc30bc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,8 @@ [![Total Downloads](https://poser.pugx.org/nyholm/psr7/downloads)](https://packagist.org/packages/nyholm/psr7) [![Monthly Downloads](https://poser.pugx.org/nyholm/psr7/d/monthly.png)](https://packagist.org/packages/nyholm/psr7) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) - +[![Static analysis](https://github.com/Nyholm/psr7/actions/workflows/static.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/static.yml?query=branch%3Amaster) +[![Tests](https://github.com/Nyholm/psr7/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/Nyholm/psr7/actions/workflows/tests.yml?query=branch%3Amaster) A super lightweight PSR-7 implementation. Very strict and very fast. From 3cb4d163b58589e47b35103e8e5e6a6a475b47be Mon Sep 17 00:00:00 2001 From: Murat Erkenov Date: Tue, 2 May 2023 14:26:24 +0300 Subject: [PATCH 80/84] Deprecate HttplugFactory and make dependency on php-http/message-factory optional (#243) --- CHANGELOG.md | 5 +++++ composer.json | 8 ++++---- src/Factory/HttplugFactory.php | 8 ++++++++ 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dcdedec..cddd363 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.8.0 + +- Deprecate HttplugFactory, use Psr17Factory instead +- Make depencendy on php-http/message-factory optional + ## 1.7.0 - Bump to PHP 7.2 minimum diff --git a/composer.json b/composer.json index 470c937..c607615 100644 --- a/composer.json +++ b/composer.json @@ -17,12 +17,12 @@ "require": { "php": ">=7.2", "psr/http-message": "^1.1 || ^2.0", - "php-http/message-factory": "^1.0", "psr/http-factory": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^7.5 || 8.5 || 9.4", - "php-http/psr7-integration-tests": "^1.0@dev", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", "http-interop/http-factory-tests": "^0.9", "symfony/error-handler": "^4.4" }, @@ -43,7 +43,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "1.8-dev" } } } diff --git a/src/Factory/HttplugFactory.php b/src/Factory/HttplugFactory.php index 4cf8e27..cc9285d 100644 --- a/src/Factory/HttplugFactory.php +++ b/src/Factory/HttplugFactory.php @@ -11,11 +11,19 @@ use Psr\Http\Message\StreamInterface; use Psr\Http\Message\UriInterface; +if (!\interface_exists(MessageFactory::class)) { + throw new \LogicException('You cannot use "Nyholm\Psr7\Factory\HttplugFactory" as the "php-http/message-factory" package is not installed. Try running "composer require php-http/message-factory". Note that this package is deprecated, use "psr/http-factory" instead'); +} + +@\trigger_error('Class "Nyholm\Psr7\Factory\HttplugFactory" is deprecated since version 1.8, use "Nyholm\Psr7\Factory\Psr17Factory" instead.', \E_USER_DEPRECATED); + /** * @author Tobias Nyholm * @author Martijn van der Ven * * @final This class should never be extended. See https://github.com/Nyholm/psr7/blob/master/doc/final.md + * + * @deprecated since version 1.8, use Psr17Factory instead */ class HttplugFactory implements MessageFactory, StreamFactory, UriFactory { From aa5fc277a4f5508013d571341ade0c3886d4d00e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Mon, 13 Nov 2023 10:31:12 +0100 Subject: [PATCH 81/84] Fix error handling in Stream::getContents() (#252) --- src/Stream.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Stream.php b/src/Stream.php index d3bd78d..63b7d6d 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -260,11 +260,19 @@ public function getContents(): string throw new \RuntimeException('Stream is detached'); } - if (false === $contents = @\stream_get_contents($this->stream)) { - throw new \RuntimeException('Unable to read stream contents: ' . (\error_get_last()['message'] ?? '')); + $exception = null; + + \set_error_handler(static function ($type, $message) use (&$exception) { + throw $exception = new \RuntimeException('Unable to read stream contents: ' . $message); + }); + + try { + return \stream_get_contents($this->stream); + } catch (\Throwable $e) { + throw $e === $exception ? $e : new \RuntimeException('Unable to read stream contents: ' . $e->getMessage(), 0, $e); + } finally { + \restore_error_handler(); } - - return $contents; } /** From fd12ffc87a1e4014b2a7485b88add81c96d105e8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 22 Nov 2023 12:09:15 +0100 Subject: [PATCH 82/84] Fix .gitattributes (#250) * Fix .gitattributes * Fix .gitattributes --- .gitattributes | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitattributes b/.gitattributes index fc3ad5c..323c441 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,10 +4,11 @@ tests/ export-ignore .editorconfig export-ignore .gitattributes export-ignore .gitignore export-ignore -.php_cs export-ignore +.php-cs-fixer.dist.php export-ignore .scrutinizer.yml export-ignore .travis.yml export-ignore phpstan.neon.dist export-ignore -phpstan.baseline.dist export-ignore +phpstan-baseline.neon export-ignore phpunit.xml.dist export-ignore psalm.xml export-ignore +psalm.baseline.xml export-ignore From 229484fc939a76a6579e331b55d86a77fb8e2863 Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Fri, 12 Apr 2024 12:59:04 +0530 Subject: [PATCH 83/84] [PHP 8.4] Fixes for implicit nullability deprecation (#255) Fixes all issues that emits a deprecation notice on PHP 8.4. See: - [RFC](https://wiki.php.net/rfc/deprecate-implicitly-nullable-types) - [PHP 8.4: Implicitly nullable parameter declarations deprecated](https://php.watch/versions/8.4/implicitly-marking-parameter-type-nullable-deprecated) --- src/Factory/Psr17Factory.php | 2 +- src/Response.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Factory/Psr17Factory.php b/src/Factory/Psr17Factory.php index 440bec3..2fa98be 100644 --- a/src/Factory/Psr17Factory.php +++ b/src/Factory/Psr17Factory.php @@ -57,7 +57,7 @@ public function createStreamFromResource($resource): StreamInterface return Stream::create($resource); } - public function createUploadedFile(StreamInterface $stream, int $size = null, int $error = \UPLOAD_ERR_OK, string $clientFilename = null, string $clientMediaType = null): UploadedFileInterface + public function createUploadedFile(StreamInterface $stream, ?int $size = null, int $error = \UPLOAD_ERR_OK, ?string $clientFilename = null, ?string $clientMediaType = null): UploadedFileInterface { if (null === $size) { $size = $stream->getSize(); diff --git a/src/Response.php b/src/Response.php index f3e2097..71eb2fa 100644 --- a/src/Response.php +++ b/src/Response.php @@ -39,7 +39,7 @@ class Response implements ResponseInterface * @param string $version Protocol version * @param string|null $reason Reason phrase (when empty a default will be used based on the status code) */ - public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', string $reason = null) + public function __construct(int $status = 200, array $headers = [], $body = null, string $version = '1.1', ?string $reason = null) { // If we got no body, defer initialization of the stream until Response::getBody() if ('' !== $body && null !== $body) { From a71f2b11690f4b24d099d6b16690a90ae14fc6f3 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 9 Sep 2024 09:06:30 +0200 Subject: [PATCH 84/84] Changelog for 1.8.2 (#258) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cddd363..17a819f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file, in reverse chronological order by release. +## 1.8.2 + +- Fix deprecation warnings in PHP 8.4 + +## 1.8.1 + +- Fix error handling in Stream::getContents() + ## 1.8.0 - Deprecate HttplugFactory, use Psr17Factory instead