diff --git a/.editorconfig b/.editorconfig index 677e36e2..7c3e4a28 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,6 @@ indent_size = 4 indent_style = space insert_final_newline = true trim_trailing_whitespace = true + +[{Makefile, *.mk}] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index b72a8a90..687c48fb 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,7 +1,13 @@ -/tests export-ignore -.editorconfig export-ignore -.gitattributes export-ignore -.gitignore export-ignore -.travis.yml export-ignore -Makefile export-ignore -phpunit.xml.dist export-ignore +.editorconfig export-ignore +.gitattributes export-ignore +/.github/ export-ignore +.gitignore export-ignore +.php-cs-fixer.dist.php export-ignore +/Makefile export-ignore +/phpstan-baseline.neon export-ignore +/phpstan.neon.dist export-ignore +/phpunit.xml.dist export-ignore +/psalm-baseline.xml export-ignore +/psalm.xml export-ignore +/tests/ export-ignore +/vendor-bin/ export-ignore diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..7d222c58 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: [Nyholm, GrahamCampbell] +tidelift: "packagist/guzzlehttp/psr7" diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..53faa71b --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,14 @@ +daysUntilStale: 120 +daysUntilClose: 14 +exemptLabels: + - lifecycle/keep-open + - lifecycle/ready-for-merge +# Label to use when marking an issue as stale +staleLabel: lifecycle/stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed after 2 weeks if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml deleted file mode 100644 index 9eaedbce..00000000 --- a/.github/workflows/bc.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: BC Check - -on: - pull_request: - -jobs: - roave-bc-check: - name: Roave BC Check - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Roave BC Check - uses: docker://nyholm/roave-bc-check-ga diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml new file mode 100644 index 00000000..a4a2ea0f --- /dev/null +++ b/.github/workflows/checks.yml @@ -0,0 +1,19 @@ +name: Checks + +on: + push: + branches: + - master + pull_request: + +jobs: + composer-normalize: + name: Composer Normalize + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Composer normalize + uses: docker://ergebnis/composer-normalize-action diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da7414e5..15fed93e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,16 +1,40 @@ name: CI on: + push: + branches: + - master pull_request: jobs: + build-lowest-version: + name: Build lowest version + runs-on: ubuntu-latest + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.2' + coverage: 'none' + extensions: mbstring + + - name: Checkout code + uses: actions/checkout@v3 + + - name: Install dependencies + run: composer update --no-interaction --prefer-stable --prefer-lowest --no-progress + + - name: Run tests + run: make test + build: name: Build runs-on: ubuntu-latest strategy: max-parallel: 10 matrix: - php: ['5.6', '7.0', '7.1', '7.2', '7.3', '7.4'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1'] steps: - name: Set up PHP @@ -21,10 +45,14 @@ jobs: extensions: mbstring - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 + + - name: Mimic PHP 8.0 + run: composer config platform.php 8.0.999 + if: matrix.php > 8 - name: Install dependencies - run: composer update --no-interaction --no-progress --prefer-dist + run: composer update --no-interaction --no-progress - name: Run tests run: make test diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b6e0eb24..6003e890 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,6 +1,9 @@ name: Integration on: + push: + branches: + - master pull_request: jobs: @@ -11,7 +14,7 @@ jobs: strategy: max-parallel: 10 matrix: - php: ['7.2', '7.3', '7.4', '8.0'] + php: ['7.2', '7.3', '7.4', '8.0', '8.1'] steps: - name: Set up PHP @@ -21,12 +24,14 @@ jobs: coverage: none - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - - name: Download dependencies - uses: ramsey/composer-install@v1 - with: - composer-options: --no-interaction --prefer-dist --optimize-autoloader + - name: Mimic PHP 8.0 + run: composer config platform.php 8.0.999 + if: matrix.php > 8 + + - name: Install dependencies + run: composer update --no-interaction --no-progress - name: Start server run: php -S 127.0.0.1:10002 tests/Integration/server.php & diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ab4d68ba..8aea52bb 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -1,16 +1,44 @@ name: Static analysis on: + push: + branches: + - master pull_request: jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + extensions: mbstring + tools: composer + + - name: Download dependencies + run: composer update --no-interaction --no-progress + + - name: Download PHPStan + run: composer bin phpstan update --no-interaction --no-progress + + - name: Execute PHPStan + run: vendor/bin/phpstan analyze --no-progress + php-cs-fixer: name: PHP-CS-Fixer runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -18,12 +46,38 @@ jobs: php-version: '7.4' coverage: none extensions: mbstring + tools: composer - name: Download dependencies run: composer update --no-interaction --no-progress - name: Download PHP CS Fixer - run: composer require "friendsofphp/php-cs-fixer:2.18.4" + run: composer bin php-cs-fixer update --no-interaction --no-progress - name: Execute PHP CS Fixer - run: vendor/bin/php-cs-fixer fix --diff-format udiff --dry-run + run: vendor/bin/php-cs-fixer fix --diff --dry-run + + psalm: + name: Psalm + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + coverage: none + extensions: mbstring + tools: composer + + - name: Download dependencies + run: composer update --no-interaction --no-progress + + - name: Download Psalm + run: composer bin psalm update --no-interaction --no-progress + + - name: Execute Psalm + run: vendor/bin/psalm.phar --no-progress --output-format=github diff --git a/.gitignore b/.gitignore index 6a926bd6..d9fe6a30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -artifacts/ -vendor/ -.php_cs -.php_cs.cache +.php-cs-fixer.php +.php-cs-fixer.cache .phpunit.result.cache composer.lock -phpunit.xml +vendor/ +/phpstan.neon +/phpunit.xml diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 68% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index e4f0bd53..b6936c5f 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,17 +1,16 @@ setRiskyAllowed(true) ->setRules([ '@PSR2' => true, 'array_syntax' => ['syntax' => 'short'], 'concat_space' => ['spacing' => 'one'], - 'declare_strict_types' => false, - 'final_static_access' => true, + 'declare_strict_types' => true, 'fully_qualified_strict_types' => true, + 'function_to_constant' => ['functions' => ['php_sapi_name']], 'header_comment' => false, - 'is_null' => ['use_yoda_style' => true], - 'list_syntax' => ['syntax' => 'long'], + 'list_syntax' => ['syntax' => 'short'], 'lowercase_cast' => true, 'magic_method_casing' => true, 'modernize_types_casting' => true, @@ -24,16 +23,22 @@ 'no_empty_statement' => true, 'no_extra_blank_lines' => true, 'no_leading_import_slash' => true, + 'no_superfluous_phpdoc_tags' => ['allow_mixed' => true], 'no_trailing_comma_in_singleline_array' => true, 'no_unset_cast' => true, 'no_unused_imports' => true, 'no_whitespace_in_blank_line' => true, 'ordered_imports' => true, - 'php_unit_ordered_covers' => true, + 'php_unit_dedicate_assert_internal_type' => ['target' => 'newest'], + 'php_unit_expectation' => ['target' => 'newest'], + 'php_unit_mock' => ['target' => 'newest'], + 'php_unit_mock_short_will_return' => true, + 'php_unit_no_expectation_annotation' => ['target' => 'newest'], 'php_unit_test_annotation' => ['style' => 'prefix'], 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], 'phpdoc_align' => ['align' => 'vertical'], 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_order_by_value' => ['annotations' => ['covers']], 'phpdoc_scalar' => true, 'phpdoc_separation' => true, 'phpdoc_single_line_var_spacing' => true, @@ -42,8 +47,14 @@ 'phpdoc_types' => true, 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 'phpdoc_var_without_name' => true, + 'self_static_accessor' => true, 'single_trait_insert_per_statement' => true, 'standardize_not_equals' => true, + 'ternary_to_null_coalescing' => true, + 'visibility_required' => true, + 'void_return' => true, + 'yoda_style' => false, + // 'native_function_invocation' => true, ]) ->setFinder( PhpCsFixer\Finder::create() diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a3219eba..00000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: php - -matrix: - include: - - php: hhvm-3.24 - dist: trusty - - php: 5.4 - dist: trusty - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" - - php: 5.4 - dist: trusty - - php: 5.5.9 - dist: trusty - env: COMPOSER_FLAGS="--prefer-stable --prefer-lowest" - - php: 5.5 - dist: trusty - fast_finish: true - -before_install: - - if [[ "$TRAVIS_PHP_VERSION" != "hhvm-3.24" ]]; then echo "xdebug.overload_var_dump = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi - - if [[ "$TRAVIS_PHP_VERSION" == "hhvm-3.24" ]]; then travis_retry composer require "phpunit/phpunit:^5.7.27" --dev --no-update -n; fi - -install: - - if [[ "$TRAVIS_PHP_VERSION" != "nightly" ]]; then travis_retry composer update --prefer-dist; fi - - if [[ "$TRAVIS_PHP_VERSION" == "nightly" ]]; then travis_retry composer update --prefer-dist --ignore-platform-reqs; fi - -script: - - make test diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a6b7bf9..14637cbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,80 @@ # Change Log - All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). - +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased -## 1.8.2 - 2021-04-26 +## 2.2.1 - 2022-03-20 + +### Fixed + +- Correct header value validation + +## 2.2.0 - 2022-03-20 + +### Added + +- A more compressive list of mime types +- Add JsonSerializable to Uri +- Missing return types + +### Fixed + +- Bug MultipartStream no `uri` metadata +- Bug MultipartStream with filename for `data://` streams +- Fixed new line handling in MultipartStream +- Reduced RAM usage when copying streams +- Updated parsing in `Header::normalize()` + +## 2.1.1 - 2022-03-20 + +### Fixed + +- Validate header values properly + +## 2.1.0 - 2021-10-06 + +### Changed + +- Attempting to create a `Uri` object from a malformed URI will no longer throw a generic + `InvalidArgumentException`, but rather a `MalformedUriException`, which inherits from the former + for backwards compatibility. Callers relying on the exception being thrown to detect invalid + URIs should catch the new exception. + +### Fixed + +- Return `null` in caching stream size if remote size is `null` + +## 2.0.0 - 2021-06-30 + +Identical to the RC release. + +## 2.0.0@RC-1 - 2021-04-29 ### Fixed - Handle possibly unset `url` in `stream_get_meta_data` +## 2.0.0@beta-1 - 2021-03-21 + +### Added + +- PSR-17 factories +- Made classes final +- PHP7 type hints + +### Changed + +- When building a query string, booleans are represented as 1 and 0. + +### Removed + +- PHP < 7.2 support +- All functions in the Guzzle\Psr7 namespace + ## 1.8.1 - 2021-03-21 ### Fixed diff --git a/LICENSE b/LICENSE index 581d95f9..51c7ec81 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,11 @@ -Copyright (c) 2015 Michael Dowling, https://github.com/mtdowling +The MIT License (MIT) + +Copyright (c) 2015 Michael Dowling +Copyright (c) 2015 Márk Sági-Kazár +Copyright (c) 2015 Graham Campbell +Copyright (c) 2016 Tobias Schultze +Copyright (c) 2016 George Mponos +Copyright (c) 2018 Tobias Nyholm Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/Makefile b/Makefile index 8b00b43e..b3bfef65 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,6 @@ -all: clean test - test: vendor/bin/phpunit $(TEST) -coverage: - vendor/bin/phpunit --coverage-html=artifacts/coverage $(TEST) - -view-coverage: - open artifacts/coverage/index.html - check-tag: $(if $(TAG),,$(error TAG is not defined. Pass via "make tag TAG=4.2.1")) @@ -24,6 +16,3 @@ tag: check-tag release: check-tag git push origin master git push origin $(TAG) - -clean: - rm -rf artifacts/* diff --git a/README.md b/README.md index acfabfdc..ed81c927 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ This repository contains a full [PSR-7](http://www.php-fig.org/psr/psr-7/) message implementation, several stream decorators, and some helpful functionality like query string parsing. - -[![Build Status](https://travis-ci.org/guzzle/psr7.svg?branch=master)](https://travis-ci.org/guzzle/psr7) +![CI](https://github.com/guzzle/psr7/workflows/CI/badge.svg) +![Static analysis](https://github.com/guzzle/psr7/workflows/Static%20analysis/badge.svg) # Stream implementation @@ -130,10 +130,9 @@ $fnStream->rewind(); `GuzzleHttp\Psr7\InflateStream` -Uses PHP's zlib.inflate filter to inflate deflate or gzipped content. +Uses PHP's zlib.inflate filter to inflate zlib (HTTP deflate, RFC1950) or gzipped (RFC1952) content. -This stream decorator skips the first 10 bytes of the given stream to remove -the gzip header, converts the provided stream to a PHP stream resource, +This stream decorator converts the provided stream to a PHP stream resource, then appends the zlib.inflate filter. The stream is then converted back to a Guzzle stream resource to be used as a Guzzle stream. @@ -555,7 +554,7 @@ Maps a file extensions to a mimetype. ## Upgrading from Function API -The static API was first introduced in 1.7.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API will be removed in 2.0.0. A migration table has been provided here for your convenience: +The static API was first introduced in 1.7.0, in order to mitigate problems with functions conflicting between global and local copies of the package. The function API was removed in 2.0.0. A migration table has been provided here for your convenience: | Original Function | Replacement Method | |----------------|----------------| @@ -807,3 +806,18 @@ Whether two URIs can be considered equivalent. Both URIs are normalized automati `$normalizations` bitmask. The method also accepts relative URI references and returns true when they are equivalent. This of course assumes they will be resolved against the same base URI. If this is not the case, determination of equivalence or difference of relative references does not mean anything. + + +## Security + +If you discover a security vulnerability within this package, please send an email to security@tidelift.com. All security vulnerabilities will be promptly addressed. Please do not disclose security-related issues publicly until a fix has been announced. Please see [Security Policy](https://github.com/guzzle/psr7/security/policy) for more information. + +## License + +Guzzle is made available under the MIT License (MIT). Please see [License File](LICENSE) for more information. + +## For Enterprise + +Available as part of the Tidelift Subscription + +The maintainers of Guzzle and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source dependencies you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact dependencies you use. [Learn more.](https://tidelift.com/subscription/pkg/packagist-guzzlehttp-psr7?utm_source=packagist-guzzlehttp-psr7&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) diff --git a/composer.json b/composer.json index 58dcb07e..1aed3ed6 100644 --- a/composer.json +++ b/composer.json @@ -1,40 +1,76 @@ { "name": "guzzlehttp/psr7", - "type": "library", "description": "PSR-7 message implementation that also provides common utility methods", - "keywords": ["request", "response", "message", "stream", "http", "uri", "url", "psr-7"], + "keywords": [ + "request", + "response", + "message", + "stream", + "http", + "uri", + "url", + "psr-7" + ], "license": "MIT", "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" } ], "require": { - "php": ">=5.4.0", - "psr/http-message": "~1.0", - "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" - }, - "require-dev": { - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10", - "ext-zlib": "*" + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0", + "ralouphie/getallheaders": "^3.0" }, "provide": { + "psr/http-factory-implementation": "1.0", "psr/http-message-implementation": "1.0" }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.8 || ^9.3.10" + }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "autoload": { "psr-4": { "GuzzleHttp\\Psr7\\": "src/" - }, - "files": ["src/functions_include.php"] + } }, "autoload-dev": { "psr-4": { @@ -43,7 +79,17 @@ }, "extra": { "branch-alias": { - "dev-master": "1.7-dev" + "dev-master": "2.2-dev" + } + }, + "config": { + "allow-plugins": { + "bamarni/composer-bin-plugin": true + }, + "preferred-install": "dist", + "sort-packages": true, + "allow-plugins": { + "bamarni/composer-bin-plugin": true } } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..bc2f5a0e --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,332 @@ +parameters: + ignoreErrors: + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/AppendStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/CachingStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/DroppingStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn___toString\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_close\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_detach\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_eof\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_getContents\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_getMetadata\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_getSize\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_isReadable\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_isSeekable\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_isWritable\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_read\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_rewind\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_seek\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_tell\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Access to an undefined property GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:\\$_fn_write\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:__toString\\(\\) should return string but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:detach\\(\\) should return resource\\|null but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:eof\\(\\) should return bool but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:getContents\\(\\) should return string but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:getSize\\(\\) should return int\\|null but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:isReadable\\(\\) should return bool but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:isSeekable\\(\\) should return bool but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:isWritable\\(\\) should return bool but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:read\\(\\) should return string but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:tell\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\FnStream\\:\\:write\\(\\) should return int but returns mixed\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/FnStream.php + + - + message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" + count: 2 + path: src/Header.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/InflateStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/LazyOpenStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/LimitStream.php + + - + message: "#^Parameter \\#1 \\$haystack of function substr_count expects string, string\\|null given\\.$#" + count: 1 + path: src/Message.php + + - + message: "#^Parameter \\#2 \\$subject of function preg_match expects string, string\\|null given\\.$#" + count: 1 + path: src/Message.php + + - + message: "#^Parameter \\#2 \\$subject of function preg_match_all expects string, string\\|null given\\.$#" + count: 1 + path: src/Message.php + + - + message: "#^Variable \\$headerLines in PHPDoc tag @var does not match assigned variable \\$count\\.$#" + count: 1 + path: src/Message.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\MultipartStream\\:\\:getHeader\\(\\) has no return type specified\\.$#" + count: 1 + path: src/MultipartStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/MultipartStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/NoSeekStream.php + + - + message: "#^Parameter \\#1 \\$string of function strlen expects string, mixed given\\.$#" + count: 1 + path: src/PumpStream.php + + - + message: "#^Parameter \\#1 \\$string of method GuzzleHttp\\\\Psr7\\\\BufferStream\\:\\:write\\(\\) expects string, mixed given\\.$#" + count: 1 + path: src/PumpStream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/PumpStream.php + + - + message: "#^Parameter \\#2 \\$subject of function preg_match expects string, mixed given\\.$#" + count: 1 + path: src/Request.php + + - + message: "#^Property GuzzleHttp\\\\Psr7\\\\Request\\:\\:\\$requestTarget \\(string\\|null\\) does not accept mixed\\.$#" + count: 1 + path: src/Request.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\ServerRequest\\:\\:normalizeNestedFileSpec\\(\\) should return array\\ but returns array\\\\|Psr\\\\Http\\\\Message\\\\UploadedFileInterface\\>\\.$#" + count: 1 + path: src/ServerRequest.php + + - + message: "#^Offset 'size' on array\\{0\\: int, 1\\: int, 2\\: int, 3\\: int, 4\\: int, 5\\: int, 6\\: int, 7\\: int, \\.\\.\\.\\} in isset\\(\\) always exists and is not nullable\\.$#" + count: 1 + path: src/Stream.php + + - + message: "#^Property GuzzleHttp\\\\Psr7\\\\Stream\\:\\:\\$stream \\(resource\\) in isset\\(\\) is not nullable\\.$#" + count: 10 + path: src/Stream.php + + - + message: "#^Property GuzzleHttp\\\\Psr7\\\\Stream\\:\\:\\$uri \\(string\\|null\\) does not accept mixed\\.$#" + count: 1 + path: src/Stream.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/Stream.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\StreamWrapper\\:\\:getResource\\(\\) should return resource but returns resource\\|false\\.$#" + count: 1 + path: src/StreamWrapper.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\UploadedFile\\:\\:isStringNotEmpty\\(\\) has parameter \\$param with no type specified\\.$#" + count: 1 + path: src/UploadedFile.php + + - + message: "#^Cannot cast mixed to int\\.$#" + count: 1 + path: src/Uri.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\Uri\\:\\:filterPath\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Uri.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\Uri\\:\\:filterQueryAndFragment\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Uri.php + + - + message: "#^Method GuzzleHttp\\\\Psr7\\\\Uri\\:\\:filterUserInfoComponent\\(\\) should return string but returns string\\|null\\.$#" + count: 1 + path: src/Uri.php + + - + message: "#^Parameter \\#1 \\$callback of function array_map expects \\(callable\\(int\\|string\\)\\: mixed\\)\\|null, 'urldecode' given\\.$#" + count: 1 + path: src/Uri.php + + - + message: "#^Parameter \\#1 \\$path of method Psr\\\\Http\\\\Message\\\\UriInterface\\:\\:withPath\\(\\) expects string, string\\|null given\\.$#" + count: 3 + path: src/UriNormalizer.php + + - + message: "#^Parameter \\#1 \\$query of method Psr\\\\Http\\\\Message\\\\UriInterface\\:\\:withQuery\\(\\) expects string, string\\|null given\\.$#" + count: 2 + path: src/UriNormalizer.php + + - + message: "#^Strict comparison using \\=\\=\\= between '' and non\\-empty\\-string will always evaluate to false\\.$#" + count: 1 + path: src/UriResolver.php + + - + message: "#^Offset 'uri' on array\\{timed_out\\: bool, blocked\\: bool, eof\\: bool, unread_bytes\\: int, stream_type\\: string, wrapper_type\\: string, wrapper_data\\: mixed, mode\\: string, \\.\\.\\.\\} on left side of \\?\\? always exists and is not nullable\\.$#" + count: 1 + path: src/Utils.php + + - + message: "#^Parameter \\#1 \\$keys of static method GuzzleHttp\\\\Psr7\\\\Utils\\:\\:caselessRemove\\(\\) expects array\\, array\\ given\\.$#" + count: 1 + path: src/Utils.php + + - + message: "#^Parameter \\#1 \\$source of class GuzzleHttp\\\\Psr7\\\\PumpStream constructor expects callable\\(int\\)\\: string\\|false\\|null, Closure\\(\\)\\: mixed given\\.$#" + count: 1 + path: src/Utils.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/Utils.php + + - + message: "#^Variable \\$handle might not be defined\\.$#" + count: 1 + path: src/Utils.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..ce60b792 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,9 @@ +includes: + - phpstan-baseline.neon + - vendor-bin/phpstan/vendor/phpstan/phpstan-deprecation-rules/rules.neon + +parameters: + checkMissingIterableValueType: false + level: max + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e2819aa1..bbce3f54 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,19 +6,29 @@ beStrictAboutTestsThatDoNotTestAnything="true" bootstrap="vendor/autoload.php" > - - - tests/ - tests/Integration - - - tests/Integration - - - - - - src/ - - + + + tests + tests/Integration + + + tests/Integration + + + ./vendor/http-interop/http-factory-tests/test + + + + + src + + + + + + + + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 00000000..cdc2655c --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,134 @@ + + + + + $this->stream + + + + + $this->stream + + + + + call_user_func($this->_fn___toString) + call_user_func($this->_fn_close) + call_user_func($this->_fn_detach) + call_user_func($this->_fn_eof) + call_user_func($this->_fn_getContents) + call_user_func($this->_fn_getMetadata, $key) + call_user_func($this->_fn_getSize) + call_user_func($this->_fn_isReadable) + call_user_func($this->_fn_isSeekable) + call_user_func($this->_fn_isWritable) + call_user_func($this->_fn_read, $length) + call_user_func($this->_fn_rewind) + call_user_func($this->_fn_seek, $offset, $whence) + call_user_func($this->_fn_tell) + call_user_func($this->_fn_write, $string) + + + + + $file + + + + + $this->stream + + + + + $this->stream + + + + + MessageInterface + MessageInterface + MessageInterface + MessageInterface + MessageInterface + array + array + + + $header + $header + $header + $header + $header + $header + + + + + $filename + $filename + + + $this->stream + + + + + isset($this->headerNames['host']) + + + + + $normalizedFiles + + + UploadedFileInterface[] + + + $attribute + $attribute + $attribute + + + + + $this->stream + + + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + isset($this->stream) + + + + + $this->stream + $this->stream + + + + + $result + + + + + '' === $relativePath + + + + + throw $ex; + + + $handle + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..04d8486f --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/AppendStream.php b/src/AppendStream.php index fa9153d7..967925f3 100644 --- a/src/AppendStream.php +++ b/src/AppendStream.php @@ -1,5 +1,7 @@ rewind(); return $this->getContents(); - } catch (\Exception $e) { + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } @@ -48,7 +57,7 @@ public function __toString() * * @throws \InvalidArgumentException if the stream is not readable */ - public function addStream(StreamInterface $stream) + public function addStream(StreamInterface $stream): void { if (!$stream->isReadable()) { throw new \InvalidArgumentException('Each stream must be readable'); @@ -62,17 +71,15 @@ public function addStream(StreamInterface $stream) $this->streams[] = $stream; } - public function getContents() + public function getContents(): string { return Utils::copyToString($this); } /** * Closes each attached stream. - * - * {@inheritdoc} */ - public function close() + public function close(): void { $this->pos = $this->current = 0; $this->seekable = true; @@ -88,8 +95,6 @@ public function close() * Detaches each attached stream. * * Returns null as it's not clear which underlying stream resource to return. - * - * {@inheritdoc} */ public function detach() { @@ -105,7 +110,7 @@ public function detach() return null; } - public function tell() + public function tell(): int { return $this->pos; } @@ -115,10 +120,8 @@ public function tell() * * If any of the streams do not return a valid number, then the size of the * append stream cannot be determined and null is returned. - * - * {@inheritdoc} */ - public function getSize() + public function getSize(): ?int { $size = 0; @@ -133,24 +136,22 @@ public function getSize() return $size; } - public function eof() + public function eof(): bool { return !$this->streams || ($this->current >= count($this->streams) - 1 && $this->streams[$this->current]->eof()); } - public function rewind() + public function rewind(): void { $this->seek(0); } /** * Attempts to seek to the given position. Only supports SEEK_SET. - * - * {@inheritdoc} */ - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { if (!$this->seekable) { throw new \RuntimeException('This AppendStream is not seekable'); @@ -181,10 +182,8 @@ public function seek($offset, $whence = SEEK_SET) /** * Reads from all of the appended streams until the length is met or EOF. - * - * {@inheritdoc} */ - public function read($length) + public function read($length): string { $buffer = ''; $total = count($this->streams) - 1; @@ -204,8 +203,7 @@ public function read($length) $result = $this->streams[$this->current]->read($remaining); - // Using a loose comparison here to match on '', false, and null - if ($result == null) { + if ($result === '') { $progressToNext = true; continue; } @@ -219,26 +217,31 @@ public function read($length) return $buffer; } - public function isReadable() + public function isReadable(): bool { return true; } - public function isWritable() + public function isWritable(): bool { return false; } - public function isSeekable() + public function isSeekable(): bool { return $this->seekable; } - public function write($string) + public function write($string): int { throw new \RuntimeException('Cannot write to an AppendStream'); } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { return $key ? null : []; diff --git a/src/BufferStream.php b/src/BufferStream.php index 783859c1..21be8c0a 100644 --- a/src/BufferStream.php +++ b/src/BufferStream.php @@ -1,5 +1,7 @@ hwm = $hwm; } - public function __toString() + public function __toString(): string { return $this->getContents(); } - public function getContents() + public function getContents(): string { $buffer = $this->buffer; $this->buffer = ''; @@ -44,7 +47,7 @@ public function getContents() return $buffer; } - public function close() + public function close(): void { $this->buffer = ''; } @@ -56,42 +59,42 @@ public function detach() return null; } - public function getSize() + public function getSize(): ?int { return strlen($this->buffer); } - public function isReadable() + public function isReadable(): bool { return true; } - public function isWritable() + public function isWritable(): bool { return true; } - public function isSeekable() + public function isSeekable(): bool { return false; } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { throw new \RuntimeException('Cannot seek a BufferStream'); } - public function eof() + public function eof(): bool { return strlen($this->buffer) === 0; } - public function tell() + public function tell(): int { throw new \RuntimeException('Cannot determine the position of a BufferStream'); } @@ -99,7 +102,7 @@ public function tell() /** * Reads data from the buffer. */ - public function read($length) + public function read($length): string { $currentLength = strlen($this->buffer); @@ -119,21 +122,25 @@ public function read($length) /** * Writes data to the buffer. */ - public function write($string) + public function write($string): int { $this->buffer .= $string; - // TODO: What should happen here? if (strlen($this->buffer) >= $this->hwm) { - return false; + return 0; } return strlen($string); } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { - if ($key == 'hwm') { + if ($key === 'hwm') { return $this->hwm; } diff --git a/src/CachingStream.php b/src/CachingStream.php index fe749e98..7a70ee94 100644 --- a/src/CachingStream.php +++ b/src/CachingStream.php @@ -1,5 +1,7 @@ stream = $target ?: new Stream(Utils::tryFopen('php://temp', 'r+')); } - public function getSize() + public function getSize(): ?int { - return max($this->stream->getSize(), $this->remoteStream->getSize()); + $remoteSize = $this->remoteStream->getSize(); + + if (null === $remoteSize) { + return null; + } + + return max($this->stream->getSize(), $remoteSize); } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { - if ($whence == SEEK_SET) { + if ($whence === SEEK_SET) { $byte = $offset; - } elseif ($whence == SEEK_CUR) { + } elseif ($whence === SEEK_CUR) { $byte = $offset + $this->tell(); - } elseif ($whence == SEEK_END) { + } elseif ($whence === SEEK_END) { $size = $this->remoteStream->getSize(); if ($size === null) { $size = $this->cacheEntireStream(); @@ -75,7 +81,7 @@ public function seek($offset, $whence = SEEK_SET) } } - public function read($length) + public function read($length): string { // Perform a regular read on any previously read data from the buffer $data = $this->stream->read($length); @@ -104,7 +110,7 @@ public function read($length) return $data; } - public function write($string) + public function write($string): int { // When appending to the end of the currently read stream, you'll want // to skip bytes from being read from the remote stream to emulate @@ -118,7 +124,7 @@ public function write($string) return $this->stream->write($string); } - public function eof() + public function eof(): bool { return $this->stream->eof() && $this->remoteStream->eof(); } @@ -126,12 +132,13 @@ public function eof() /** * Close both the remote stream and buffer stream */ - public function close() + public function close(): void { - $this->remoteStream->close() && $this->stream->close(); + $this->remoteStream->close(); + $this->stream->close(); } - private function cacheEntireStream() + private function cacheEntireStream(): int { $target = new FnStream(['write' => 'strlen']); Utils::copyToStream($this, $target); diff --git a/src/DroppingStream.php b/src/DroppingStream.php index 9f7420c4..d78070ae 100644 --- a/src/DroppingStream.php +++ b/src/DroppingStream.php @@ -1,5 +1,7 @@ stream = $stream; $this->maxLength = $maxLength; } - public function write($string) + public function write($string): int { $diff = $this->maxLength - $this->stream->getSize(); diff --git a/src/Exception/MalformedUriException.php b/src/Exception/MalformedUriException.php new file mode 100644 index 00000000..3a084779 --- /dev/null +++ b/src/Exception/MalformedUriException.php @@ -0,0 +1,14 @@ + */ + private $methods; /** - * @param array $methods Hash of method name to a callable. + * @param array $methods Hash of method name to a callable. */ public function __construct(array $methods) { @@ -40,7 +41,7 @@ public function __construct(array $methods) * * @throws \BadMethodCallException */ - public function __get($name) + public function __get(string $name): void { throw new \BadMethodCallException(str_replace('_fn_', '', $name) . '() is not implemented in the FnStream'); @@ -61,7 +62,7 @@ public function __destruct() * * @throws \LogicException */ - public function __wakeup() + public function __wakeup(): void { throw new \LogicException('FnStream should never be unserialized'); } @@ -70,8 +71,8 @@ public function __wakeup() * Adds custom functionality to an underlying stream by intercepting * specific method calls. * - * @param StreamInterface $stream Stream to decorate - * @param array $methods Hash of method name to a closure + * @param StreamInterface $stream Stream to decorate + * @param array $methods Hash of method name to a closure * * @return FnStream */ @@ -79,21 +80,31 @@ public static function decorate(StreamInterface $stream, array $methods) { // If any of the required methods were not provided, then simply // proxy to the decorated stream. - foreach (array_diff(self::$slots, array_keys($methods)) as $diff) { - $methods[$diff] = [$stream, $diff]; + foreach (array_diff(self::SLOTS, array_keys($methods)) as $diff) { + /** @var callable $callable */ + $callable = [$stream, $diff]; + $methods[$diff] = $callable; } return new self($methods); } - public function __toString() + public function __toString(): string { - return call_user_func($this->_fn___toString); + try { + return call_user_func($this->_fn___toString); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); + return ''; + } } - public function close() + public function close(): void { - return call_user_func($this->_fn_close); + call_user_func($this->_fn_close); } public function detach() @@ -101,61 +112,66 @@ public function detach() return call_user_func($this->_fn_detach); } - public function getSize() + public function getSize(): ?int { return call_user_func($this->_fn_getSize); } - public function tell() + public function tell(): int { return call_user_func($this->_fn_tell); } - public function eof() + public function eof(): bool { return call_user_func($this->_fn_eof); } - public function isSeekable() + public function isSeekable(): bool { return call_user_func($this->_fn_isSeekable); } - public function rewind() + public function rewind(): void { call_user_func($this->_fn_rewind); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { call_user_func($this->_fn_seek, $offset, $whence); } - public function isWritable() + public function isWritable(): bool { return call_user_func($this->_fn_isWritable); } - public function write($string) + public function write($string): int { return call_user_func($this->_fn_write, $string); } - public function isReadable() + public function isReadable(): bool { return call_user_func($this->_fn_isReadable); } - public function read($length) + public function read($length): string { return call_user_func($this->_fn_read, $length); } - public function getContents() + public function getContents(): string { return call_user_func($this->_fn_getContents); } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { return call_user_func($this->_fn_getMetadata, $key); diff --git a/src/Header.php b/src/Header.php index 865d7421..b219b87b 100644 --- a/src/Header.php +++ b/src/Header.php @@ -1,5 +1,7 @@ getSize(); + } + + return new UploadedFile($stream, $size, $error, $clientFilename, $clientMediaType); + } + + public function createStream(string $content = ''): StreamInterface + { + return Utils::streamFor($content); + } + + public function createStreamFromFile(string $file, string $mode = 'r'): StreamInterface + { + try { + $resource = Utils::tryFopen($file, $mode); + } catch (\RuntimeException $e) { + if ('' === $mode || false === \in_array($mode[0], ['r', 'w', 'a', 'x', 'c'], true)) { + throw new \InvalidArgumentException(sprintf('Invalid file opening mode "%s"', $mode), 0, $e); + } + + throw $e; + } + + return Utils::streamFor($resource); + } + + public function createStreamFromResource($resource): StreamInterface + { + return Utils::streamFor($resource); + } + + public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface + { + if (empty($method)) { + if (!empty($serverParams['REQUEST_METHOD'])) { + $method = $serverParams['REQUEST_METHOD']; + } else { + throw new \InvalidArgumentException('Cannot determine HTTP method'); + } + } + + return new ServerRequest($method, $uri, [], null, '1.1', $serverParams); + } + + public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface + { + return new Response($code, [], null, '1.1', $reasonPhrase); + } + + public function createRequest(string $method, $uri): RequestInterface + { + return new Request($method, $uri); + } + + public function createUri(string $uri = ''): UriInterface + { + return new Uri($uri); + } +} diff --git a/src/InflateStream.php b/src/InflateStream.php index 0cbd2cce..8e3cf171 100644 --- a/src/InflateStream.php +++ b/src/InflateStream.php @@ -1,56 +1,34 @@ read(10); - $filenameHeaderLength = $this->getLengthOfPossibleFilenameHeader($stream, $header); - // Skip the header, that is 10 + length of filename + 1 (nil) bytes - $stream = new LimitStream($stream, -1, 10 + $filenameHeaderLength); $resource = StreamWrapper::getResource($stream); - stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ); + // Specify window=15+32, so zlib will use header detection to both gzip (with header) and zlib data + // See http://www.zlib.net/manual.html#Advanced definition of inflateInit2 + // "Add 32 to windowBits to enable zlib and gzip decoding with automatic header detection" + // Default window size is 15. + stream_filter_append($resource, 'zlib.inflate', STREAM_FILTER_READ, ['window' => 15 + 32]); $this->stream = $stream->isSeekable() ? new Stream($resource) : new NoSeekStream(new Stream($resource)); } - - /** - * @param StreamInterface $stream - * @param $header - * - * @return int - */ - private function getLengthOfPossibleFilenameHeader(StreamInterface $stream, $header) - { - $filename_header_length = 0; - - if (substr(bin2hex($header), 6, 2) === '08') { - // we have a filename, read until nil - $filename_header_length = 1; - while ($stream->read(1) !== chr(0)) { - $filename_header_length++; - } - } - - return $filename_header_length; - } } diff --git a/src/LazyOpenStream.php b/src/LazyOpenStream.php index 911e127d..6b604296 100644 --- a/src/LazyOpenStream.php +++ b/src/LazyOpenStream.php @@ -1,5 +1,7 @@ filename = $filename; $this->mode = $mode; @@ -32,10 +32,8 @@ public function __construct($filename, $mode) /** * Creates the underlying stream lazily when required. - * - * @return StreamInterface */ - protected function createStream() + protected function createStream(): StreamInterface { return Utils::streamFor(Utils::tryFopen($this->filename, $this->mode)); } diff --git a/src/LimitStream.php b/src/LimitStream.php index 1173ec40..9762d38a 100644 --- a/src/LimitStream.php +++ b/src/LimitStream.php @@ -1,15 +1,15 @@ stream = $stream; $this->setLimit($limit); $this->setOffset($offset); } - public function eof() + public function eof(): bool { // Always return true if the underlying stream is EOF if ($this->stream->eof()) { @@ -44,7 +44,7 @@ public function eof() } // No limit and the underlying stream is not at EOF - if ($this->limit == -1) { + if ($this->limit === -1) { return false; } @@ -53,13 +53,12 @@ public function eof() /** * Returns the size of the limited subset of data - * {@inheritdoc} */ - public function getSize() + public function getSize(): ?int { if (null === ($length = $this->stream->getSize())) { return null; - } elseif ($this->limit == -1) { + } elseif ($this->limit === -1) { return $length - $this->offset; } else { return min($this->limit, $length - $this->offset); @@ -68,9 +67,8 @@ public function getSize() /** * Allow for a bounded seek on the read limited stream - * {@inheritdoc} */ - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { if ($whence !== SEEK_SET || $offset < 0) { throw new \RuntimeException(sprintf( @@ -93,9 +91,8 @@ public function seek($offset, $whence = SEEK_SET) /** * Give a relative tell() - * {@inheritdoc} */ - public function tell() + public function tell(): int { return $this->stream->tell() - $this->offset; } @@ -107,7 +104,7 @@ public function tell() * * @throws \RuntimeException if the stream cannot be seeked. */ - public function setOffset($offset) + public function setOffset(int $offset): void { $current = $this->stream->tell(); @@ -132,14 +129,14 @@ public function setOffset($offset) * @param int $limit Number of bytes to allow to be read from the stream. * Use -1 for no limit. */ - public function setLimit($limit) + public function setLimit(int $limit): void { $this->limit = $limit; } - public function read($length) + public function read($length): string { - if ($this->limit == -1) { + if ($this->limit === -1) { return $this->stream->read($length); } diff --git a/src/Message.php b/src/Message.php index 516d1cb8..9b825b30 100644 --- a/src/Message.php +++ b/src/Message.php @@ -1,5 +1,7 @@ getMethod() . ' ' @@ -52,10 +52,8 @@ public static function toString(MessageInterface $message) * * @param MessageInterface $message The message to get the body summary * @param int $truncateAt The maximum allowed size of the summary - * - * @return string|null */ - public static function bodySummary(MessageInterface $message, $truncateAt = 120) + public static function bodySummary(MessageInterface $message, int $truncateAt = 120): ?string { $body = $message->getBody(); @@ -95,7 +93,7 @@ public static function bodySummary(MessageInterface $message, $truncateAt = 120) * * @throws \RuntimeException */ - public static function rewindBody(MessageInterface $message) + public static function rewindBody(MessageInterface $message): void { $body = $message->getBody(); @@ -112,10 +110,8 @@ public static function rewindBody(MessageInterface $message) * array values, and a "body" key containing the body of the message. * * @param string $message HTTP request or response to parse. - * - * @return array */ - public static function parseMessage($message) + public static function parseMessage(string $message): array { if (!$message) { throw new \InvalidArgumentException('Invalid message'); @@ -129,7 +125,7 @@ public static function parseMessage($message) throw new \InvalidArgumentException('Invalid message: Missing header delimiter'); } - list($rawHeaders, $body) = $messageParts; + [$rawHeaders, $body] = $messageParts; $rawHeaders .= "\r\n"; // Put back the delimiter we split previously $headerParts = preg_split("/\r?\n/", $rawHeaders, 2); @@ -137,7 +133,7 @@ public static function parseMessage($message) throw new \InvalidArgumentException('Invalid message: Missing status line'); } - list($startLine, $rawHeaders) = $headerParts; + [$startLine, $rawHeaders] = $headerParts; if (preg_match("/(?:^HTTP\/|^[A-Z]+ \S+ HTTP\/)(\d+(?:\.\d+)?)/i", $startLine, $matches) && $matches[1] === '1.0') { // Header folding is deprecated for HTTP/1.1, but allowed in HTTP/1.0 @@ -175,10 +171,8 @@ public static function parseMessage($message) * * @param string $path Path from the start-line * @param array $headers Array of headers (each value an array). - * - * @return string */ - public static function parseRequestUri($path, array $headers) + public static function parseRequestUri(string $path, array $headers): string { $hostKey = array_filter(array_keys($headers), function ($k) { return strtolower($k) === 'host'; @@ -199,10 +193,8 @@ public static function parseRequestUri($path, array $headers) * Parses a request message string into a request object. * * @param string $message Request message string. - * - * @return Request */ - public static function parseRequest($message) + public static function parseRequest(string $message): RequestInterface { $data = self::parseMessage($message); $matches = []; @@ -227,10 +219,8 @@ public static function parseRequest($message) * Parses a response message string into a response object. * * @param string $message Response message string. - * - * @return Response */ - public static function parseResponse($message) + public static function parseResponse(string $message): ResponseInterface { $data = self::parseMessage($message); // According to https://tools.ietf.org/html/rfc7230#section-3.1.2 the space @@ -246,7 +236,7 @@ public static function parseResponse($message) $data['headers'], $data['body'], explode('/', $parts[0])[1], - isset($parts[2]) ? $parts[2] : null + $parts[2] ?? null ); } } diff --git a/src/MessageTrait.php b/src/MessageTrait.php index 99203bb4..a8696b98 100644 --- a/src/MessageTrait.php +++ b/src/MessageTrait.php @@ -1,7 +1,10 @@ array of values */ + /** @var array Map of all registered headers, as original name => array of values */ private $headers = []; - /** @var array Map of lowercase header name => original name at registration */ + /** @var array Map of lowercase header name => original name at registration */ private $headerNames = []; /** @var string */ @@ -21,12 +24,12 @@ trait MessageTrait /** @var StreamInterface|null */ private $stream; - public function getProtocolVersion() + public function getProtocolVersion(): string { return $this->protocol; } - public function withProtocolVersion($version) + public function withProtocolVersion($version): MessageInterface { if ($this->protocol === $version) { return $this; @@ -37,17 +40,17 @@ public function withProtocolVersion($version) return $new; } - public function getHeaders() + public function getHeaders(): array { return $this->headers; } - public function hasHeader($header) + public function hasHeader($header): bool { return isset($this->headerNames[strtolower($header)]); } - public function getHeader($header) + public function getHeader($header): array { $header = strtolower($header); @@ -60,12 +63,12 @@ public function getHeader($header) return $this->headers[$header]; } - public function getHeaderLine($header) + public function getHeaderLine($header): string { return implode(', ', $this->getHeader($header)); } - public function withHeader($header, $value) + public function withHeader($header, $value): MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); @@ -81,7 +84,7 @@ public function withHeader($header, $value) return $new; } - public function withAddedHeader($header, $value) + public function withAddedHeader($header, $value): MessageInterface { $this->assertHeader($header); $value = $this->normalizeHeaderValue($value); @@ -99,7 +102,7 @@ public function withAddedHeader($header, $value) return $new; } - public function withoutHeader($header) + public function withoutHeader($header): MessageInterface { $normalized = strtolower($header); @@ -115,7 +118,7 @@ public function withoutHeader($header) return $new; } - public function getBody() + public function getBody(): StreamInterface { if (!$this->stream) { $this->stream = Utils::streamFor(''); @@ -124,7 +127,7 @@ public function getBody() return $this->stream; } - public function withBody(StreamInterface $body) + public function withBody(StreamInterface $body): MessageInterface { if ($body === $this->stream) { return $this; @@ -135,7 +138,10 @@ public function withBody(StreamInterface $body) return $new; } - private function setHeaders(array $headers) + /** + * @param array $headers + */ + private function setHeaders(array $headers): void { $this->headerNames = $this->headers = []; foreach ($headers as $header => $value) { @@ -157,17 +163,22 @@ private function setHeaders(array $headers) } } - private function normalizeHeaderValue($value) + /** + * @param mixed $value + * + * @return string[] + */ + private function normalizeHeaderValue($value): array { if (!is_array($value)) { - return $this->trimHeaderValues([$value]); + return $this->trimAndValidateHeaderValues([$value]); } if (count($value) === 0) { throw new \InvalidArgumentException('Header value can not be an empty array.'); } - return $this->trimHeaderValues($value); + return $this->trimAndValidateHeaderValues($value); } /** @@ -178,13 +189,13 @@ private function normalizeHeaderValue($value) * header-field = field-name ":" OWS field-value OWS * OWS = *( SP / HTAB ) * - * @param string[] $values Header values + * @param mixed[] $values Header values * * @return string[] Trimmed header values * * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 */ - private function trimHeaderValues(array $values) + private function trimAndValidateHeaderValues(array $values): array { return array_map(function ($value) { if (!is_scalar($value) && null !== $value) { @@ -194,11 +205,19 @@ private function trimHeaderValues(array $values) )); } - return trim((string) $value, " \t"); + $trimmed = trim((string) $value, " \t"); + $this->assertValue($trimmed); + + return $trimmed; }, array_values($values)); } - private function assertHeader($header) + /** + * @see https://tools.ietf.org/html/rfc7230#section-3.2 + * + * @param mixed $header + */ + private function assertHeader($header): void { if (!is_string($header)) { throw new \InvalidArgumentException(sprintf( @@ -207,8 +226,41 @@ private function assertHeader($header) )); } - if ($header === '') { - throw new \InvalidArgumentException('Header name can not be empty.'); + if (! preg_match('/^[a-zA-Z0-9\'`#$%&*+.^_|~!-]+$/', $header)) { + throw new \InvalidArgumentException( + sprintf( + '"%s" is not valid header name', + $header + ) + ); + } + } + + /** + * @see https://tools.ietf.org/html/rfc7230#section-3.2 + * + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * VCHAR = %x21-7E + * obs-text = %x80-FF + * obs-fold = CRLF 1*( SP / HTAB ) + */ + private function assertValue(string $value): void + { + // The regular expression intentionally does not support the obs-fold production, because as + // per RFC 7230#3.2.4: + // + // A sender MUST NOT generate a message that includes + // line folding (i.e., that has any field-value that contains a match to + // the obs-fold rule) unless the message is intended for packaging + // within the message/http media type. + // + // Clients must not send a request with line folding and a server sending folded headers is + // likely very rare. Line folding is a fairly obscure feature of HTTP/1.1 and thus not accepting + // folding is not likely to break any legitimate use case. + if (! preg_match('/^[\x20\x09\x21-\x7E\x80-\xFF]*$/', $value)) { + throw new \InvalidArgumentException(sprintf('"%s" is not valid header value', $value)); } } } diff --git a/src/MimeType.php b/src/MimeType.php index 205c7b1f..3bcb07ae 100644 --- a/src/MimeType.php +++ b/src/MimeType.php @@ -1,17 +1,1217 @@ 'application/vnd.1000minds.decision-model+xml', + '3dml' => 'text/vnd.in3d.3dml', + '3ds' => 'image/x-3ds', + '3g2' => 'video/3gpp2', + '3gp' => 'video/3gp', + '3gpp' => 'video/3gpp', + '3mf' => 'model/3mf', + '7z' => 'application/x-7z-compressed', + '7zip' => 'application/x-7z-compressed', + '123' => 'application/vnd.lotus-1-2-3', + 'aab' => 'application/x-authorware-bin', + 'aac' => 'audio/x-acc', + 'aam' => 'application/x-authorware-map', + 'aas' => 'application/x-authorware-seg', + 'abw' => 'application/x-abiword', + 'ac' => 'application/vnd.nokia.n-gage.ac+xml', + 'ac3' => 'audio/ac3', + 'acc' => 'application/vnd.americandynamics.acc', + 'ace' => 'application/x-ace-compressed', + 'acu' => 'application/vnd.acucobol', + 'acutc' => 'application/vnd.acucorp', + 'adp' => 'audio/adpcm', + 'aep' => 'application/vnd.audiograph', + 'afm' => 'application/x-font-type1', + 'afp' => 'application/vnd.ibm.modcap', + 'ahead' => 'application/vnd.ahead.space', + 'ai' => 'application/pdf', + 'aif' => 'audio/x-aiff', + 'aifc' => 'audio/x-aiff', + 'aiff' => 'audio/x-aiff', + 'air' => 'application/vnd.adobe.air-application-installer-package+zip', + 'ait' => 'application/vnd.dvb.ait', + 'ami' => 'application/vnd.amiga.ami', + 'amr' => 'audio/amr', + 'apk' => 'application/vnd.android.package-archive', + 'apng' => 'image/apng', + 'appcache' => 'text/cache-manifest', + 'application' => 'application/x-ms-application', + 'apr' => 'application/vnd.lotus-approach', + 'arc' => 'application/x-freearc', + 'arj' => 'application/x-arj', + 'asc' => 'application/pgp-signature', + 'asf' => 'video/x-ms-asf', + 'asm' => 'text/x-asm', + 'aso' => 'application/vnd.accpac.simply.aso', + 'asx' => 'video/x-ms-asf', + 'atc' => 'application/vnd.acucorp', + 'atom' => 'application/atom+xml', + 'atomcat' => 'application/atomcat+xml', + 'atomdeleted' => 'application/atomdeleted+xml', + 'atomsvc' => 'application/atomsvc+xml', + 'atx' => 'application/vnd.antix.game-component', + 'au' => 'audio/x-au', + 'avi' => 'video/x-msvideo', + 'avif' => 'image/avif', + 'aw' => 'application/applixware', + 'azf' => 'application/vnd.airzip.filesecure.azf', + 'azs' => 'application/vnd.airzip.filesecure.azs', + 'azv' => 'image/vnd.airzip.accelerator.azv', + 'azw' => 'application/vnd.amazon.ebook', + 'b16' => 'image/vnd.pco.b16', + 'bat' => 'application/x-msdownload', + 'bcpio' => 'application/x-bcpio', + 'bdf' => 'application/x-font-bdf', + 'bdm' => 'application/vnd.syncml.dm+wbxml', + 'bdoc' => 'application/x-bdoc', + 'bed' => 'application/vnd.realvnc.bed', + 'bh2' => 'application/vnd.fujitsu.oasysprs', + 'bin' => 'application/octet-stream', + 'blb' => 'application/x-blorb', + 'blorb' => 'application/x-blorb', + 'bmi' => 'application/vnd.bmi', + 'bmml' => 'application/vnd.balsamiq.bmml+xml', + 'bmp' => 'image/bmp', + 'book' => 'application/vnd.framemaker', + 'box' => 'application/vnd.previewsystems.box', + 'boz' => 'application/x-bzip2', + 'bpk' => 'application/octet-stream', + 'bpmn' => 'application/octet-stream', + 'bsp' => 'model/vnd.valve.source.compiled-map', + 'btif' => 'image/prs.btif', + 'buffer' => 'application/octet-stream', + 'bz' => 'application/x-bzip', + 'bz2' => 'application/x-bzip2', + 'c' => 'text/x-c', + 'c4d' => 'application/vnd.clonk.c4group', + 'c4f' => 'application/vnd.clonk.c4group', + 'c4g' => 'application/vnd.clonk.c4group', + 'c4p' => 'application/vnd.clonk.c4group', + 'c4u' => 'application/vnd.clonk.c4group', + 'c11amc' => 'application/vnd.cluetrust.cartomobile-config', + 'c11amz' => 'application/vnd.cluetrust.cartomobile-config-pkg', + 'cab' => 'application/vnd.ms-cab-compressed', + 'caf' => 'audio/x-caf', + 'cap' => 'application/vnd.tcpdump.pcap', + 'car' => 'application/vnd.curl.car', + 'cat' => 'application/vnd.ms-pki.seccat', + 'cb7' => 'application/x-cbr', + 'cba' => 'application/x-cbr', + 'cbr' => 'application/x-cbr', + 'cbt' => 'application/x-cbr', + 'cbz' => 'application/x-cbr', + 'cc' => 'text/x-c', + 'cco' => 'application/x-cocoa', + 'cct' => 'application/x-director', + 'ccxml' => 'application/ccxml+xml', + 'cdbcmsg' => 'application/vnd.contact.cmsg', + 'cdf' => 'application/x-netcdf', + 'cdfx' => 'application/cdfx+xml', + 'cdkey' => 'application/vnd.mediastation.cdkey', + 'cdmia' => 'application/cdmi-capability', + 'cdmic' => 'application/cdmi-container', + 'cdmid' => 'application/cdmi-domain', + 'cdmio' => 'application/cdmi-object', + 'cdmiq' => 'application/cdmi-queue', + 'cdr' => 'application/cdr', + 'cdx' => 'chemical/x-cdx', + 'cdxml' => 'application/vnd.chemdraw+xml', + 'cdy' => 'application/vnd.cinderella', + 'cer' => 'application/pkix-cert', + 'cfs' => 'application/x-cfs-compressed', + 'cgm' => 'image/cgm', + 'chat' => 'application/x-chat', + 'chm' => 'application/vnd.ms-htmlhelp', + 'chrt' => 'application/vnd.kde.kchart', + 'cif' => 'chemical/x-cif', + 'cii' => 'application/vnd.anser-web-certificate-issue-initiation', + 'cil' => 'application/vnd.ms-artgalry', + 'cjs' => 'application/node', + 'cla' => 'application/vnd.claymore', + 'class' => 'application/octet-stream', + 'clkk' => 'application/vnd.crick.clicker.keyboard', + 'clkp' => 'application/vnd.crick.clicker.palette', + 'clkt' => 'application/vnd.crick.clicker.template', + 'clkw' => 'application/vnd.crick.clicker.wordbank', + 'clkx' => 'application/vnd.crick.clicker', + 'clp' => 'application/x-msclip', + 'cmc' => 'application/vnd.cosmocaller', + 'cmdf' => 'chemical/x-cmdf', + 'cml' => 'chemical/x-cml', + 'cmp' => 'application/vnd.yellowriver-custom-menu', + 'cmx' => 'image/x-cmx', + 'cod' => 'application/vnd.rim.cod', + 'coffee' => 'text/coffeescript', + 'com' => 'application/x-msdownload', + 'conf' => 'text/plain', + 'cpio' => 'application/x-cpio', + 'cpp' => 'text/x-c', + 'cpt' => 'application/mac-compactpro', + 'crd' => 'application/x-mscardfile', + 'crl' => 'application/pkix-crl', + 'crt' => 'application/x-x509-ca-cert', + 'crx' => 'application/x-chrome-extension', + 'cryptonote' => 'application/vnd.rig.cryptonote', + 'csh' => 'application/x-csh', + 'csl' => 'application/vnd.citationstyles.style+xml', + 'csml' => 'chemical/x-csml', + 'csp' => 'application/vnd.commonspace', + 'csr' => 'application/octet-stream', + 'css' => 'text/css', + 'cst' => 'application/x-director', + 'csv' => 'text/csv', + 'cu' => 'application/cu-seeme', + 'curl' => 'text/vnd.curl', + 'cww' => 'application/prs.cww', + 'cxt' => 'application/x-director', + 'cxx' => 'text/x-c', + 'dae' => 'model/vnd.collada+xml', + 'daf' => 'application/vnd.mobius.daf', + 'dart' => 'application/vnd.dart', + 'dataless' => 'application/vnd.fdsn.seed', + 'davmount' => 'application/davmount+xml', + 'dbf' => 'application/vnd.dbf', + 'dbk' => 'application/docbook+xml', + 'dcr' => 'application/x-director', + 'dcurl' => 'text/vnd.curl.dcurl', + 'dd2' => 'application/vnd.oma.dd2+xml', + 'ddd' => 'application/vnd.fujixerox.ddd', + 'ddf' => 'application/vnd.syncml.dmddf+xml', + 'dds' => 'image/vnd.ms-dds', + 'deb' => 'application/x-debian-package', + 'def' => 'text/plain', + 'deploy' => 'application/octet-stream', + 'der' => 'application/x-x509-ca-cert', + 'dfac' => 'application/vnd.dreamfactory', + 'dgc' => 'application/x-dgc-compressed', + 'dic' => 'text/x-c', + 'dir' => 'application/x-director', + 'dis' => 'application/vnd.mobius.dis', + 'disposition-notification' => 'message/disposition-notification', + 'dist' => 'application/octet-stream', + 'distz' => 'application/octet-stream', + 'djv' => 'image/vnd.djvu', + 'djvu' => 'image/vnd.djvu', + 'dll' => 'application/octet-stream', + 'dmg' => 'application/x-apple-diskimage', + 'dmn' => 'application/octet-stream', + 'dmp' => 'application/vnd.tcpdump.pcap', + 'dms' => 'application/octet-stream', + 'dna' => 'application/vnd.dna', + 'doc' => 'application/msword', + 'docm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'dot' => 'application/msword', + 'dotm' => 'application/vnd.ms-word.template.macroEnabled.12', + 'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template', + 'dp' => 'application/vnd.osgi.dp', + 'dpg' => 'application/vnd.dpgraph', + 'dra' => 'audio/vnd.dra', + 'drle' => 'image/dicom-rle', + 'dsc' => 'text/prs.lines.tag', + 'dssc' => 'application/dssc+der', + 'dtb' => 'application/x-dtbook+xml', + 'dtd' => 'application/xml-dtd', + 'dts' => 'audio/vnd.dts', + 'dtshd' => 'audio/vnd.dts.hd', + 'dump' => 'application/octet-stream', + 'dvb' => 'video/vnd.dvb.file', + 'dvi' => 'application/x-dvi', + 'dwd' => 'application/atsc-dwd+xml', + 'dwf' => 'model/vnd.dwf', + 'dwg' => 'image/vnd.dwg', + 'dxf' => 'image/vnd.dxf', + 'dxp' => 'application/vnd.spotfire.dxp', + 'dxr' => 'application/x-director', + 'ear' => 'application/java-archive', + 'ecelp4800' => 'audio/vnd.nuera.ecelp4800', + 'ecelp7470' => 'audio/vnd.nuera.ecelp7470', + 'ecelp9600' => 'audio/vnd.nuera.ecelp9600', + 'ecma' => 'application/ecmascript', + 'edm' => 'application/vnd.novadigm.edm', + 'edx' => 'application/vnd.novadigm.edx', + 'efif' => 'application/vnd.picsel', + 'ei6' => 'application/vnd.pg.osasli', + 'elc' => 'application/octet-stream', + 'emf' => 'image/emf', + 'eml' => 'message/rfc822', + 'emma' => 'application/emma+xml', + 'emotionml' => 'application/emotionml+xml', + 'emz' => 'application/x-msmetafile', + 'eol' => 'audio/vnd.digital-winds', + 'eot' => 'application/vnd.ms-fontobject', + 'eps' => 'application/postscript', + 'epub' => 'application/epub+zip', + 'es' => 'application/ecmascript', + 'es3' => 'application/vnd.eszigno3+xml', + 'esa' => 'application/vnd.osgi.subsystem', + 'esf' => 'application/vnd.epson.esf', + 'et3' => 'application/vnd.eszigno3+xml', + 'etx' => 'text/x-setext', + 'eva' => 'application/x-eva', + 'evy' => 'application/x-envoy', + 'exe' => 'application/octet-stream', + 'exi' => 'application/exi', + 'exp' => 'application/express', + 'exr' => 'image/aces', + 'ext' => 'application/vnd.novadigm.ext', + 'ez' => 'application/andrew-inset', + 'ez2' => 'application/vnd.ezpix-album', + 'ez3' => 'application/vnd.ezpix-package', + 'f' => 'text/x-fortran', + 'f4v' => 'video/mp4', + 'f77' => 'text/x-fortran', + 'f90' => 'text/x-fortran', + 'fbs' => 'image/vnd.fastbidsheet', + 'fcdt' => 'application/vnd.adobe.formscentral.fcdt', + 'fcs' => 'application/vnd.isac.fcs', + 'fdf' => 'application/vnd.fdf', + 'fdt' => 'application/fdt+xml', + 'fe_launch' => 'application/vnd.denovo.fcselayout-link', + 'fg5' => 'application/vnd.fujitsu.oasysgp', + 'fgd' => 'application/x-director', + 'fh' => 'image/x-freehand', + 'fh4' => 'image/x-freehand', + 'fh5' => 'image/x-freehand', + 'fh7' => 'image/x-freehand', + 'fhc' => 'image/x-freehand', + 'fig' => 'application/x-xfig', + 'fits' => 'image/fits', + 'flac' => 'audio/x-flac', + 'fli' => 'video/x-fli', + 'flo' => 'application/vnd.micrografx.flo', + 'flv' => 'video/x-flv', + 'flw' => 'application/vnd.kde.kivio', + 'flx' => 'text/vnd.fmi.flexstor', + 'fly' => 'text/vnd.fly', + 'fm' => 'application/vnd.framemaker', + 'fnc' => 'application/vnd.frogans.fnc', + 'fo' => 'application/vnd.software602.filler.form+xml', + 'for' => 'text/x-fortran', + 'fpx' => 'image/vnd.fpx', + 'frame' => 'application/vnd.framemaker', + 'fsc' => 'application/vnd.fsc.weblaunch', + 'fst' => 'image/vnd.fst', + 'ftc' => 'application/vnd.fluxtime.clip', + 'fti' => 'application/vnd.anser-web-funds-transfer-initiation', + 'fvt' => 'video/vnd.fvt', + 'fxp' => 'application/vnd.adobe.fxp', + 'fxpl' => 'application/vnd.adobe.fxp', + 'fzs' => 'application/vnd.fuzzysheet', + 'g2w' => 'application/vnd.geoplan', + 'g3' => 'image/g3fax', + 'g3w' => 'application/vnd.geospace', + 'gac' => 'application/vnd.groove-account', + 'gam' => 'application/x-tads', + 'gbr' => 'application/rpki-ghostbusters', + 'gca' => 'application/x-gca-compressed', + 'gdl' => 'model/vnd.gdl', + 'gdoc' => 'application/vnd.google-apps.document', + 'geo' => 'application/vnd.dynageo', + 'geojson' => 'application/geo+json', + 'gex' => 'application/vnd.geometry-explorer', + 'ggb' => 'application/vnd.geogebra.file', + 'ggt' => 'application/vnd.geogebra.tool', + 'ghf' => 'application/vnd.groove-help', + 'gif' => 'image/gif', + 'gim' => 'application/vnd.groove-identity-message', + 'glb' => 'model/gltf-binary', + 'gltf' => 'model/gltf+json', + 'gml' => 'application/gml+xml', + 'gmx' => 'application/vnd.gmx', + 'gnumeric' => 'application/x-gnumeric', + 'gpg' => 'application/gpg-keys', + 'gph' => 'application/vnd.flographit', + 'gpx' => 'application/gpx+xml', + 'gqf' => 'application/vnd.grafeq', + 'gqs' => 'application/vnd.grafeq', + 'gram' => 'application/srgs', + 'gramps' => 'application/x-gramps-xml', + 'gre' => 'application/vnd.geometry-explorer', + 'grv' => 'application/vnd.groove-injector', + 'grxml' => 'application/srgs+xml', + 'gsf' => 'application/x-font-ghostscript', + 'gsheet' => 'application/vnd.google-apps.spreadsheet', + 'gslides' => 'application/vnd.google-apps.presentation', + 'gtar' => 'application/x-gtar', + 'gtm' => 'application/vnd.groove-tool-message', + 'gtw' => 'model/vnd.gtw', + 'gv' => 'text/vnd.graphviz', + 'gxf' => 'application/gxf', + 'gxt' => 'application/vnd.geonext', + 'gz' => 'application/gzip', + 'gzip' => 'application/gzip', + 'h' => 'text/x-c', + 'h261' => 'video/h261', + 'h263' => 'video/h263', + 'h264' => 'video/h264', + 'hal' => 'application/vnd.hal+xml', + 'hbci' => 'application/vnd.hbci', + 'hbs' => 'text/x-handlebars-template', + 'hdd' => 'application/x-virtualbox-hdd', + 'hdf' => 'application/x-hdf', + 'heic' => 'image/heic', + 'heics' => 'image/heic-sequence', + 'heif' => 'image/heif', + 'heifs' => 'image/heif-sequence', + 'hej2' => 'image/hej2k', + 'held' => 'application/atsc-held+xml', + 'hh' => 'text/x-c', + 'hjson' => 'application/hjson', + 'hlp' => 'application/winhlp', + 'hpgl' => 'application/vnd.hp-hpgl', + 'hpid' => 'application/vnd.hp-hpid', + 'hps' => 'application/vnd.hp-hps', + 'hqx' => 'application/mac-binhex40', + 'hsj2' => 'image/hsj2', + 'htc' => 'text/x-component', + 'htke' => 'application/vnd.kenameaapp', + 'htm' => 'text/html', + 'html' => 'text/html', + 'hvd' => 'application/vnd.yamaha.hv-dic', + 'hvp' => 'application/vnd.yamaha.hv-voice', + 'hvs' => 'application/vnd.yamaha.hv-script', + 'i2g' => 'application/vnd.intergeo', + 'icc' => 'application/vnd.iccprofile', + 'ice' => 'x-conference/x-cooltalk', + 'icm' => 'application/vnd.iccprofile', + 'ico' => 'image/x-icon', + 'ics' => 'text/calendar', + 'ief' => 'image/ief', + 'ifb' => 'text/calendar', + 'ifm' => 'application/vnd.shana.informed.formdata', + 'iges' => 'model/iges', + 'igl' => 'application/vnd.igloader', + 'igm' => 'application/vnd.insors.igm', + 'igs' => 'model/iges', + 'igx' => 'application/vnd.micrografx.igx', + 'iif' => 'application/vnd.shana.informed.interchange', + 'img' => 'application/octet-stream', + 'imp' => 'application/vnd.accpac.simply.imp', + 'ims' => 'application/vnd.ms-ims', + 'in' => 'text/plain', + 'ini' => 'text/plain', + 'ink' => 'application/inkml+xml', + 'inkml' => 'application/inkml+xml', + 'install' => 'application/x-install-instructions', + 'iota' => 'application/vnd.astraea-software.iota', + 'ipfix' => 'application/ipfix', + 'ipk' => 'application/vnd.shana.informed.package', + 'irm' => 'application/vnd.ibm.rights-management', + 'irp' => 'application/vnd.irepository.package+xml', + 'iso' => 'application/x-iso9660-image', + 'itp' => 'application/vnd.shana.informed.formtemplate', + 'its' => 'application/its+xml', + 'ivp' => 'application/vnd.immervision-ivp', + 'ivu' => 'application/vnd.immervision-ivu', + 'jad' => 'text/vnd.sun.j2me.app-descriptor', + 'jade' => 'text/jade', + 'jam' => 'application/vnd.jam', + 'jar' => 'application/java-archive', + 'jardiff' => 'application/x-java-archive-diff', + 'java' => 'text/x-java-source', + 'jhc' => 'image/jphc', + 'jisp' => 'application/vnd.jisp', + 'jls' => 'image/jls', + 'jlt' => 'application/vnd.hp-jlyt', + 'jng' => 'image/x-jng', + 'jnlp' => 'application/x-java-jnlp-file', + 'joda' => 'application/vnd.joost.joda-archive', + 'jp2' => 'image/jp2', + 'jpe' => 'image/jpeg', + 'jpeg' => 'image/jpeg', + 'jpf' => 'image/jpx', + 'jpg' => 'image/jpeg', + 'jpg2' => 'image/jp2', + 'jpgm' => 'video/jpm', + 'jpgv' => 'video/jpeg', + 'jph' => 'image/jph', + 'jpm' => 'video/jpm', + 'jpx' => 'image/jpx', + 'js' => 'application/javascript', + 'json' => 'application/json', + 'json5' => 'application/json5', + 'jsonld' => 'application/ld+json', + 'jsonml' => 'application/jsonml+json', + 'jsx' => 'text/jsx', + 'jxr' => 'image/jxr', + 'jxra' => 'image/jxra', + 'jxrs' => 'image/jxrs', + 'jxs' => 'image/jxs', + 'jxsc' => 'image/jxsc', + 'jxsi' => 'image/jxsi', + 'jxss' => 'image/jxss', + 'kar' => 'audio/midi', + 'karbon' => 'application/vnd.kde.karbon', + 'kdb' => 'application/octet-stream', + 'kdbx' => 'application/x-keepass2', + 'key' => 'application/x-iwork-keynote-sffkey', + 'kfo' => 'application/vnd.kde.kformula', + 'kia' => 'application/vnd.kidspiration', + 'kml' => 'application/vnd.google-earth.kml+xml', + 'kmz' => 'application/vnd.google-earth.kmz', + 'kne' => 'application/vnd.kinar', + 'knp' => 'application/vnd.kinar', + 'kon' => 'application/vnd.kde.kontour', + 'kpr' => 'application/vnd.kde.kpresenter', + 'kpt' => 'application/vnd.kde.kpresenter', + 'kpxx' => 'application/vnd.ds-keypoint', + 'ksp' => 'application/vnd.kde.kspread', + 'ktr' => 'application/vnd.kahootz', + 'ktx' => 'image/ktx', + 'ktx2' => 'image/ktx2', + 'ktz' => 'application/vnd.kahootz', + 'kwd' => 'application/vnd.kde.kword', + 'kwt' => 'application/vnd.kde.kword', + 'lasxml' => 'application/vnd.las.las+xml', + 'latex' => 'application/x-latex', + 'lbd' => 'application/vnd.llamagraphics.life-balance.desktop', + 'lbe' => 'application/vnd.llamagraphics.life-balance.exchange+xml', + 'les' => 'application/vnd.hhe.lesson-player', + 'less' => 'text/less', + 'lgr' => 'application/lgr+xml', + 'lha' => 'application/octet-stream', + 'link66' => 'application/vnd.route66.link66+xml', + 'list' => 'text/plain', + 'list3820' => 'application/vnd.ibm.modcap', + 'listafp' => 'application/vnd.ibm.modcap', + 'litcoffee' => 'text/coffeescript', + 'lnk' => 'application/x-ms-shortcut', + 'log' => 'text/plain', + 'lostxml' => 'application/lost+xml', + 'lrf' => 'application/octet-stream', + 'lrm' => 'application/vnd.ms-lrm', + 'ltf' => 'application/vnd.frogans.ltf', + 'lua' => 'text/x-lua', + 'luac' => 'application/x-lua-bytecode', + 'lvp' => 'audio/vnd.lucent.voice', + 'lwp' => 'application/vnd.lotus-wordpro', + 'lzh' => 'application/octet-stream', + 'm1v' => 'video/mpeg', + 'm2a' => 'audio/mpeg', + 'm2v' => 'video/mpeg', + 'm3a' => 'audio/mpeg', + 'm3u' => 'text/plain', + 'm3u8' => 'application/vnd.apple.mpegurl', + 'm4a' => 'audio/x-m4a', + 'm4p' => 'application/mp4', + 'm4s' => 'video/iso.segment', + 'm4u' => 'application/vnd.mpegurl', + 'm4v' => 'video/x-m4v', + 'm13' => 'application/x-msmediaview', + 'm14' => 'application/x-msmediaview', + 'm21' => 'application/mp21', + 'ma' => 'application/mathematica', + 'mads' => 'application/mads+xml', + 'maei' => 'application/mmt-aei+xml', + 'mag' => 'application/vnd.ecowin.chart', + 'maker' => 'application/vnd.framemaker', + 'man' => 'text/troff', + 'manifest' => 'text/cache-manifest', + 'map' => 'application/json', + 'mar' => 'application/octet-stream', + 'markdown' => 'text/markdown', + 'mathml' => 'application/mathml+xml', + 'mb' => 'application/mathematica', + 'mbk' => 'application/vnd.mobius.mbk', + 'mbox' => 'application/mbox', + 'mc1' => 'application/vnd.medcalcdata', + 'mcd' => 'application/vnd.mcd', + 'mcurl' => 'text/vnd.curl.mcurl', + 'md' => 'text/markdown', + 'mdb' => 'application/x-msaccess', + 'mdi' => 'image/vnd.ms-modi', + 'mdx' => 'text/mdx', + 'me' => 'text/troff', + 'mesh' => 'model/mesh', + 'meta4' => 'application/metalink4+xml', + 'metalink' => 'application/metalink+xml', + 'mets' => 'application/mets+xml', + 'mfm' => 'application/vnd.mfmp', + 'mft' => 'application/rpki-manifest', + 'mgp' => 'application/vnd.osgeo.mapguide.package', + 'mgz' => 'application/vnd.proteus.magazine', + 'mid' => 'audio/midi', + 'midi' => 'audio/midi', + 'mie' => 'application/x-mie', + 'mif' => 'application/vnd.mif', + 'mime' => 'message/rfc822', + 'mj2' => 'video/mj2', + 'mjp2' => 'video/mj2', + 'mjs' => 'application/javascript', + 'mk3d' => 'video/x-matroska', + 'mka' => 'audio/x-matroska', + 'mkd' => 'text/x-markdown', + 'mks' => 'video/x-matroska', + 'mkv' => 'video/x-matroska', + 'mlp' => 'application/vnd.dolby.mlp', + 'mmd' => 'application/vnd.chipnuts.karaoke-mmd', + 'mmf' => 'application/vnd.smaf', + 'mml' => 'text/mathml', + 'mmr' => 'image/vnd.fujixerox.edmics-mmr', + 'mng' => 'video/x-mng', + 'mny' => 'application/x-msmoney', + 'mobi' => 'application/x-mobipocket-ebook', + 'mods' => 'application/mods+xml', + 'mov' => 'video/quicktime', + 'movie' => 'video/x-sgi-movie', + 'mp2' => 'audio/mpeg', + 'mp2a' => 'audio/mpeg', + 'mp3' => 'audio/mpeg', + 'mp4' => 'video/mp4', + 'mp4a' => 'audio/mp4', + 'mp4s' => 'application/mp4', + 'mp4v' => 'video/mp4', + 'mp21' => 'application/mp21', + 'mpc' => 'application/vnd.mophun.certificate', + 'mpd' => 'application/dash+xml', + 'mpe' => 'video/mpeg', + 'mpeg' => 'video/mpeg', + 'mpg' => 'video/mpeg', + 'mpg4' => 'video/mp4', + 'mpga' => 'audio/mpeg', + 'mpkg' => 'application/vnd.apple.installer+xml', + 'mpm' => 'application/vnd.blueice.multipass', + 'mpn' => 'application/vnd.mophun.application', + 'mpp' => 'application/vnd.ms-project', + 'mpt' => 'application/vnd.ms-project', + 'mpy' => 'application/vnd.ibm.minipay', + 'mqy' => 'application/vnd.mobius.mqy', + 'mrc' => 'application/marc', + 'mrcx' => 'application/marcxml+xml', + 'ms' => 'text/troff', + 'mscml' => 'application/mediaservercontrol+xml', + 'mseed' => 'application/vnd.fdsn.mseed', + 'mseq' => 'application/vnd.mseq', + 'msf' => 'application/vnd.epson.msf', + 'msg' => 'application/vnd.ms-outlook', + 'msh' => 'model/mesh', + 'msi' => 'application/x-msdownload', + 'msl' => 'application/vnd.mobius.msl', + 'msm' => 'application/octet-stream', + 'msp' => 'application/octet-stream', + 'msty' => 'application/vnd.muvee.style', + 'mtl' => 'model/mtl', + 'mts' => 'model/vnd.mts', + 'mus' => 'application/vnd.musician', + 'musd' => 'application/mmt-usd+xml', + 'musicxml' => 'application/vnd.recordare.musicxml+xml', + 'mvb' => 'application/x-msmediaview', + 'mvt' => 'application/vnd.mapbox-vector-tile', + 'mwf' => 'application/vnd.mfer', + 'mxf' => 'application/mxf', + 'mxl' => 'application/vnd.recordare.musicxml', + 'mxmf' => 'audio/mobile-xmf', + 'mxml' => 'application/xv+xml', + 'mxs' => 'application/vnd.triscape.mxs', + 'mxu' => 'video/vnd.mpegurl', + 'n-gage' => 'application/vnd.nokia.n-gage.symbian.install', + 'n3' => 'text/n3', + 'nb' => 'application/mathematica', + 'nbp' => 'application/vnd.wolfram.player', + 'nc' => 'application/x-netcdf', + 'ncx' => 'application/x-dtbncx+xml', + 'nfo' => 'text/x-nfo', + 'ngdat' => 'application/vnd.nokia.n-gage.data', + 'nitf' => 'application/vnd.nitf', + 'nlu' => 'application/vnd.neurolanguage.nlu', + 'nml' => 'application/vnd.enliven', + 'nnd' => 'application/vnd.noblenet-directory', + 'nns' => 'application/vnd.noblenet-sealer', + 'nnw' => 'application/vnd.noblenet-web', + 'npx' => 'image/vnd.net-fpx', + 'nq' => 'application/n-quads', + 'nsc' => 'application/x-conference', + 'nsf' => 'application/vnd.lotus-notes', + 'nt' => 'application/n-triples', + 'ntf' => 'application/vnd.nitf', + 'numbers' => 'application/x-iwork-numbers-sffnumbers', + 'nzb' => 'application/x-nzb', + 'oa2' => 'application/vnd.fujitsu.oasys2', + 'oa3' => 'application/vnd.fujitsu.oasys3', + 'oas' => 'application/vnd.fujitsu.oasys', + 'obd' => 'application/x-msbinder', + 'obgx' => 'application/vnd.openblox.game+xml', + 'obj' => 'model/obj', + 'oda' => 'application/oda', + 'odb' => 'application/vnd.oasis.opendocument.database', + 'odc' => 'application/vnd.oasis.opendocument.chart', + 'odf' => 'application/vnd.oasis.opendocument.formula', + 'odft' => 'application/vnd.oasis.opendocument.formula-template', + 'odg' => 'application/vnd.oasis.opendocument.graphics', + 'odi' => 'application/vnd.oasis.opendocument.image', + 'odm' => 'application/vnd.oasis.opendocument.text-master', + 'odp' => 'application/vnd.oasis.opendocument.presentation', + 'ods' => 'application/vnd.oasis.opendocument.spreadsheet', + 'odt' => 'application/vnd.oasis.opendocument.text', + 'oga' => 'audio/ogg', + 'ogex' => 'model/vnd.opengex', + 'ogg' => 'audio/ogg', + 'ogv' => 'video/ogg', + 'ogx' => 'application/ogg', + 'omdoc' => 'application/omdoc+xml', + 'onepkg' => 'application/onenote', + 'onetmp' => 'application/onenote', + 'onetoc' => 'application/onenote', + 'onetoc2' => 'application/onenote', + 'opf' => 'application/oebps-package+xml', + 'opml' => 'text/x-opml', + 'oprc' => 'application/vnd.palm', + 'opus' => 'audio/ogg', + 'org' => 'text/x-org', + 'osf' => 'application/vnd.yamaha.openscoreformat', + 'osfpvg' => 'application/vnd.yamaha.openscoreformat.osfpvg+xml', + 'osm' => 'application/vnd.openstreetmap.data+xml', + 'otc' => 'application/vnd.oasis.opendocument.chart-template', + 'otf' => 'font/otf', + 'otg' => 'application/vnd.oasis.opendocument.graphics-template', + 'oth' => 'application/vnd.oasis.opendocument.text-web', + 'oti' => 'application/vnd.oasis.opendocument.image-template', + 'otp' => 'application/vnd.oasis.opendocument.presentation-template', + 'ots' => 'application/vnd.oasis.opendocument.spreadsheet-template', + 'ott' => 'application/vnd.oasis.opendocument.text-template', + 'ova' => 'application/x-virtualbox-ova', + 'ovf' => 'application/x-virtualbox-ovf', + 'owl' => 'application/rdf+xml', + 'oxps' => 'application/oxps', + 'oxt' => 'application/vnd.openofficeorg.extension', + 'p' => 'text/x-pascal', + 'p7a' => 'application/x-pkcs7-signature', + 'p7b' => 'application/x-pkcs7-certificates', + 'p7c' => 'application/pkcs7-mime', + 'p7m' => 'application/pkcs7-mime', + 'p7r' => 'application/x-pkcs7-certreqresp', + 'p7s' => 'application/pkcs7-signature', + 'p8' => 'application/pkcs8', + 'p10' => 'application/x-pkcs10', + 'p12' => 'application/x-pkcs12', + 'pac' => 'application/x-ns-proxy-autoconfig', + 'pages' => 'application/x-iwork-pages-sffpages', + 'pas' => 'text/x-pascal', + 'paw' => 'application/vnd.pawaafile', + 'pbd' => 'application/vnd.powerbuilder6', + 'pbm' => 'image/x-portable-bitmap', + 'pcap' => 'application/vnd.tcpdump.pcap', + 'pcf' => 'application/x-font-pcf', + 'pcl' => 'application/vnd.hp-pcl', + 'pclxl' => 'application/vnd.hp-pclxl', + 'pct' => 'image/x-pict', + 'pcurl' => 'application/vnd.curl.pcurl', + 'pcx' => 'image/x-pcx', + 'pdb' => 'application/x-pilot', + 'pde' => 'text/x-processing', + 'pdf' => 'application/pdf', + 'pem' => 'application/x-x509-user-cert', + 'pfa' => 'application/x-font-type1', + 'pfb' => 'application/x-font-type1', + 'pfm' => 'application/x-font-type1', + 'pfr' => 'application/font-tdpfr', + 'pfx' => 'application/x-pkcs12', + 'pgm' => 'image/x-portable-graymap', + 'pgn' => 'application/x-chess-pgn', + 'pgp' => 'application/pgp', + 'php' => 'application/x-httpd-php', + 'php3' => 'application/x-httpd-php', + 'php4' => 'application/x-httpd-php', + 'phps' => 'application/x-httpd-php-source', + 'phtml' => 'application/x-httpd-php', + 'pic' => 'image/x-pict', + 'pkg' => 'application/octet-stream', + 'pki' => 'application/pkixcmp', + 'pkipath' => 'application/pkix-pkipath', + 'pkpass' => 'application/vnd.apple.pkpass', + 'pl' => 'application/x-perl', + 'plb' => 'application/vnd.3gpp.pic-bw-large', + 'plc' => 'application/vnd.mobius.plc', + 'plf' => 'application/vnd.pocketlearn', + 'pls' => 'application/pls+xml', + 'pm' => 'application/x-perl', + 'pml' => 'application/vnd.ctc-posml', + 'png' => 'image/png', + 'pnm' => 'image/x-portable-anymap', + 'portpkg' => 'application/vnd.macports.portpkg', + 'pot' => 'application/vnd.ms-powerpoint', + 'potm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template', + 'ppa' => 'application/vnd.ms-powerpoint', + 'ppam' => 'application/vnd.ms-powerpoint.addin.macroEnabled.12', + 'ppd' => 'application/vnd.cups-ppd', + 'ppm' => 'image/x-portable-pixmap', + 'pps' => 'application/vnd.ms-powerpoint', + 'ppsm' => 'application/vnd.ms-powerpoint.slideshow.macroEnabled.12', + 'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow', + 'ppt' => 'application/powerpoint', + 'pptm' => 'application/vnd.ms-powerpoint.presentation.macroEnabled.12', + 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + 'pqa' => 'application/vnd.palm', + 'prc' => 'application/x-pilot', + 'pre' => 'application/vnd.lotus-freelance', + 'prf' => 'application/pics-rules', + 'provx' => 'application/provenance+xml', + 'ps' => 'application/postscript', + 'psb' => 'application/vnd.3gpp.pic-bw-small', + 'psd' => 'application/x-photoshop', + 'psf' => 'application/x-font-linux-psf', + 'pskcxml' => 'application/pskc+xml', + 'pti' => 'image/prs.pti', + 'ptid' => 'application/vnd.pvi.ptid1', + 'pub' => 'application/x-mspublisher', + 'pvb' => 'application/vnd.3gpp.pic-bw-var', + 'pwn' => 'application/vnd.3m.post-it-notes', + 'pya' => 'audio/vnd.ms-playready.media.pya', + 'pyv' => 'video/vnd.ms-playready.media.pyv', + 'qam' => 'application/vnd.epson.quickanime', + 'qbo' => 'application/vnd.intu.qbo', + 'qfx' => 'application/vnd.intu.qfx', + 'qps' => 'application/vnd.publishare-delta-tree', + 'qt' => 'video/quicktime', + 'qwd' => 'application/vnd.quark.quarkxpress', + 'qwt' => 'application/vnd.quark.quarkxpress', + 'qxb' => 'application/vnd.quark.quarkxpress', + 'qxd' => 'application/vnd.quark.quarkxpress', + 'qxl' => 'application/vnd.quark.quarkxpress', + 'qxt' => 'application/vnd.quark.quarkxpress', + 'ra' => 'audio/x-realaudio', + 'ram' => 'audio/x-pn-realaudio', + 'raml' => 'application/raml+yaml', + 'rapd' => 'application/route-apd+xml', + 'rar' => 'application/x-rar', + 'ras' => 'image/x-cmu-raster', + 'rcprofile' => 'application/vnd.ipunplugged.rcprofile', + 'rdf' => 'application/rdf+xml', + 'rdz' => 'application/vnd.data-vision.rdz', + 'relo' => 'application/p2p-overlay+xml', + 'rep' => 'application/vnd.businessobjects', + 'res' => 'application/x-dtbresource+xml', + 'rgb' => 'image/x-rgb', + 'rif' => 'application/reginfo+xml', + 'rip' => 'audio/vnd.rip', + 'ris' => 'application/x-research-info-systems', + 'rl' => 'application/resource-lists+xml', + 'rlc' => 'image/vnd.fujixerox.edmics-rlc', + 'rld' => 'application/resource-lists-diff+xml', + 'rm' => 'audio/x-pn-realaudio', + 'rmi' => 'audio/midi', + 'rmp' => 'audio/x-pn-realaudio-plugin', + 'rms' => 'application/vnd.jcp.javame.midlet-rms', + 'rmvb' => 'application/vnd.rn-realmedia-vbr', + 'rnc' => 'application/relax-ng-compact-syntax', + 'rng' => 'application/xml', + 'roa' => 'application/rpki-roa', + 'roff' => 'text/troff', + 'rp9' => 'application/vnd.cloanto.rp9', + 'rpm' => 'audio/x-pn-realaudio-plugin', + 'rpss' => 'application/vnd.nokia.radio-presets', + 'rpst' => 'application/vnd.nokia.radio-preset', + 'rq' => 'application/sparql-query', + 'rs' => 'application/rls-services+xml', + 'rsa' => 'application/x-pkcs7', + 'rsat' => 'application/atsc-rsat+xml', + 'rsd' => 'application/rsd+xml', + 'rsheet' => 'application/urc-ressheet+xml', + 'rss' => 'application/rss+xml', + 'rtf' => 'text/rtf', + 'rtx' => 'text/richtext', + 'run' => 'application/x-makeself', + 'rusd' => 'application/route-usd+xml', + 'rv' => 'video/vnd.rn-realvideo', + 's' => 'text/x-asm', + 's3m' => 'audio/s3m', + 'saf' => 'application/vnd.yamaha.smaf-audio', + 'sass' => 'text/x-sass', + 'sbml' => 'application/sbml+xml', + 'sc' => 'application/vnd.ibm.secure-container', + 'scd' => 'application/x-msschedule', + 'scm' => 'application/vnd.lotus-screencam', + 'scq' => 'application/scvp-cv-request', + 'scs' => 'application/scvp-cv-response', + 'scss' => 'text/x-scss', + 'scurl' => 'text/vnd.curl.scurl', + 'sda' => 'application/vnd.stardivision.draw', + 'sdc' => 'application/vnd.stardivision.calc', + 'sdd' => 'application/vnd.stardivision.impress', + 'sdkd' => 'application/vnd.solent.sdkm+xml', + 'sdkm' => 'application/vnd.solent.sdkm+xml', + 'sdp' => 'application/sdp', + 'sdw' => 'application/vnd.stardivision.writer', + 'sea' => 'application/octet-stream', + 'see' => 'application/vnd.seemail', + 'seed' => 'application/vnd.fdsn.seed', + 'sema' => 'application/vnd.sema', + 'semd' => 'application/vnd.semd', + 'semf' => 'application/vnd.semf', + 'senmlx' => 'application/senml+xml', + 'sensmlx' => 'application/sensml+xml', + 'ser' => 'application/java-serialized-object', + 'setpay' => 'application/set-payment-initiation', + 'setreg' => 'application/set-registration-initiation', + 'sfd-hdstx' => 'application/vnd.hydrostatix.sof-data', + 'sfs' => 'application/vnd.spotfire.sfs', + 'sfv' => 'text/x-sfv', + 'sgi' => 'image/sgi', + 'sgl' => 'application/vnd.stardivision.writer-global', + 'sgm' => 'text/sgml', + 'sgml' => 'text/sgml', + 'sh' => 'application/x-sh', + 'shar' => 'application/x-shar', + 'shex' => 'text/shex', + 'shf' => 'application/shf+xml', + 'shtml' => 'text/html', + 'sid' => 'image/x-mrsid-image', + 'sieve' => 'application/sieve', + 'sig' => 'application/pgp-signature', + 'sil' => 'audio/silk', + 'silo' => 'model/mesh', + 'sis' => 'application/vnd.symbian.install', + 'sisx' => 'application/vnd.symbian.install', + 'sit' => 'application/x-stuffit', + 'sitx' => 'application/x-stuffitx', + 'siv' => 'application/sieve', + 'skd' => 'application/vnd.koan', + 'skm' => 'application/vnd.koan', + 'skp' => 'application/vnd.koan', + 'skt' => 'application/vnd.koan', + 'sldm' => 'application/vnd.ms-powerpoint.slide.macroenabled.12', + 'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide', + 'slim' => 'text/slim', + 'slm' => 'text/slim', + 'sls' => 'application/route-s-tsid+xml', + 'slt' => 'application/vnd.epson.salt', + 'sm' => 'application/vnd.stepmania.stepchart', + 'smf' => 'application/vnd.stardivision.math', + 'smi' => 'application/smil', + 'smil' => 'application/smil', + 'smv' => 'video/x-smv', + 'smzip' => 'application/vnd.stepmania.package', + 'snd' => 'audio/basic', + 'snf' => 'application/x-font-snf', + 'so' => 'application/octet-stream', + 'spc' => 'application/x-pkcs7-certificates', + 'spdx' => 'text/spdx', + 'spf' => 'application/vnd.yamaha.smaf-phrase', + 'spl' => 'application/x-futuresplash', + 'spot' => 'text/vnd.in3d.spot', + 'spp' => 'application/scvp-vp-response', + 'spq' => 'application/scvp-vp-request', + 'spx' => 'audio/ogg', + 'sql' => 'application/x-sql', + 'src' => 'application/x-wais-source', + 'srt' => 'application/x-subrip', + 'sru' => 'application/sru+xml', + 'srx' => 'application/sparql-results+xml', + 'ssdl' => 'application/ssdl+xml', + 'sse' => 'application/vnd.kodak-descriptor', + 'ssf' => 'application/vnd.epson.ssf', + 'ssml' => 'application/ssml+xml', + 'sst' => 'application/octet-stream', + 'st' => 'application/vnd.sailingtracker.track', + 'stc' => 'application/vnd.sun.xml.calc.template', + 'std' => 'application/vnd.sun.xml.draw.template', + 'stf' => 'application/vnd.wt.stf', + 'sti' => 'application/vnd.sun.xml.impress.template', + 'stk' => 'application/hyperstudio', + 'stl' => 'model/stl', + 'stpx' => 'model/step+xml', + 'stpxz' => 'model/step-xml+zip', + 'stpz' => 'model/step+zip', + 'str' => 'application/vnd.pg.format', + 'stw' => 'application/vnd.sun.xml.writer.template', + 'styl' => 'text/stylus', + 'stylus' => 'text/stylus', + 'sub' => 'text/vnd.dvb.subtitle', + 'sus' => 'application/vnd.sus-calendar', + 'susp' => 'application/vnd.sus-calendar', + 'sv4cpio' => 'application/x-sv4cpio', + 'sv4crc' => 'application/x-sv4crc', + 'svc' => 'application/vnd.dvb.service', + 'svd' => 'application/vnd.svd', + 'svg' => 'image/svg+xml', + 'svgz' => 'image/svg+xml', + 'swa' => 'application/x-director', + 'swf' => 'application/x-shockwave-flash', + 'swi' => 'application/vnd.aristanetworks.swi', + 'swidtag' => 'application/swid+xml', + 'sxc' => 'application/vnd.sun.xml.calc', + 'sxd' => 'application/vnd.sun.xml.draw', + 'sxg' => 'application/vnd.sun.xml.writer.global', + 'sxi' => 'application/vnd.sun.xml.impress', + 'sxm' => 'application/vnd.sun.xml.math', + 'sxw' => 'application/vnd.sun.xml.writer', + 't' => 'text/troff', + 't3' => 'application/x-t3vm-image', + 't38' => 'image/t38', + 'taglet' => 'application/vnd.mynfc', + 'tao' => 'application/vnd.tao.intent-module-archive', + 'tap' => 'image/vnd.tencent.tap', + 'tar' => 'application/x-tar', + 'tcap' => 'application/vnd.3gpp2.tcap', + 'tcl' => 'application/x-tcl', + 'td' => 'application/urc-targetdesc+xml', + 'teacher' => 'application/vnd.smart.teacher', + 'tei' => 'application/tei+xml', + 'teicorpus' => 'application/tei+xml', + 'tex' => 'application/x-tex', + 'texi' => 'application/x-texinfo', + 'texinfo' => 'application/x-texinfo', + 'text' => 'text/plain', + 'tfi' => 'application/thraud+xml', + 'tfm' => 'application/x-tex-tfm', + 'tfx' => 'image/tiff-fx', + 'tga' => 'image/x-tga', + 'tgz' => 'application/x-tar', + 'thmx' => 'application/vnd.ms-officetheme', + 'tif' => 'image/tiff', + 'tiff' => 'image/tiff', + 'tk' => 'application/x-tcl', + 'tmo' => 'application/vnd.tmobile-livetv', + 'toml' => 'application/toml', + 'torrent' => 'application/x-bittorrent', + 'tpl' => 'application/vnd.groove-tool-template', + 'tpt' => 'application/vnd.trid.tpt', + 'tr' => 'text/troff', + 'tra' => 'application/vnd.trueapp', + 'trig' => 'application/trig', + 'trm' => 'application/x-msterminal', + 'ts' => 'video/mp2t', + 'tsd' => 'application/timestamped-data', + 'tsv' => 'text/tab-separated-values', + 'ttc' => 'font/collection', + 'ttf' => 'font/ttf', + 'ttl' => 'text/turtle', + 'ttml' => 'application/ttml+xml', + 'twd' => 'application/vnd.simtech-mindmapper', + 'twds' => 'application/vnd.simtech-mindmapper', + 'txd' => 'application/vnd.genomatix.tuxedo', + 'txf' => 'application/vnd.mobius.txf', + 'txt' => 'text/plain', + 'u8dsn' => 'message/global-delivery-status', + 'u8hdr' => 'message/global-headers', + 'u8mdn' => 'message/global-disposition-notification', + 'u8msg' => 'message/global', + 'u32' => 'application/x-authorware-bin', + 'ubj' => 'application/ubjson', + 'udeb' => 'application/x-debian-package', + 'ufd' => 'application/vnd.ufdl', + 'ufdl' => 'application/vnd.ufdl', + 'ulx' => 'application/x-glulx', + 'umj' => 'application/vnd.umajin', + 'unityweb' => 'application/vnd.unity', + 'uoml' => 'application/vnd.uoml+xml', + 'uri' => 'text/uri-list', + 'uris' => 'text/uri-list', + 'urls' => 'text/uri-list', + 'usdz' => 'model/vnd.usdz+zip', + 'ustar' => 'application/x-ustar', + 'utz' => 'application/vnd.uiq.theme', + 'uu' => 'text/x-uuencode', + 'uva' => 'audio/vnd.dece.audio', + 'uvd' => 'application/vnd.dece.data', + 'uvf' => 'application/vnd.dece.data', + 'uvg' => 'image/vnd.dece.graphic', + 'uvh' => 'video/vnd.dece.hd', + 'uvi' => 'image/vnd.dece.graphic', + 'uvm' => 'video/vnd.dece.mobile', + 'uvp' => 'video/vnd.dece.pd', + 'uvs' => 'video/vnd.dece.sd', + 'uvt' => 'application/vnd.dece.ttml+xml', + 'uvu' => 'video/vnd.uvvu.mp4', + 'uvv' => 'video/vnd.dece.video', + 'uvva' => 'audio/vnd.dece.audio', + 'uvvd' => 'application/vnd.dece.data', + 'uvvf' => 'application/vnd.dece.data', + 'uvvg' => 'image/vnd.dece.graphic', + 'uvvh' => 'video/vnd.dece.hd', + 'uvvi' => 'image/vnd.dece.graphic', + 'uvvm' => 'video/vnd.dece.mobile', + 'uvvp' => 'video/vnd.dece.pd', + 'uvvs' => 'video/vnd.dece.sd', + 'uvvt' => 'application/vnd.dece.ttml+xml', + 'uvvu' => 'video/vnd.uvvu.mp4', + 'uvvv' => 'video/vnd.dece.video', + 'uvvx' => 'application/vnd.dece.unspecified', + 'uvvz' => 'application/vnd.dece.zip', + 'uvx' => 'application/vnd.dece.unspecified', + 'uvz' => 'application/vnd.dece.zip', + 'vbox' => 'application/x-virtualbox-vbox', + 'vbox-extpack' => 'application/x-virtualbox-vbox-extpack', + 'vcard' => 'text/vcard', + 'vcd' => 'application/x-cdlink', + 'vcf' => 'text/x-vcard', + 'vcg' => 'application/vnd.groove-vcard', + 'vcs' => 'text/x-vcalendar', + 'vcx' => 'application/vnd.vcx', + 'vdi' => 'application/x-virtualbox-vdi', + 'vds' => 'model/vnd.sap.vds', + 'vhd' => 'application/x-virtualbox-vhd', + 'vis' => 'application/vnd.visionary', + 'viv' => 'video/vnd.vivo', + 'vlc' => 'application/videolan', + 'vmdk' => 'application/x-virtualbox-vmdk', + 'vob' => 'video/x-ms-vob', + 'vor' => 'application/vnd.stardivision.writer', + 'vox' => 'application/x-authorware-bin', + 'vrml' => 'model/vrml', + 'vsd' => 'application/vnd.visio', + 'vsf' => 'application/vnd.vsf', + 'vss' => 'application/vnd.visio', + 'vst' => 'application/vnd.visio', + 'vsw' => 'application/vnd.visio', + 'vtf' => 'image/vnd.valve.source.texture', + 'vtt' => 'text/vtt', + 'vtu' => 'model/vnd.vtu', + 'vxml' => 'application/voicexml+xml', + 'w3d' => 'application/x-director', + 'wad' => 'application/x-doom', + 'wadl' => 'application/vnd.sun.wadl+xml', + 'war' => 'application/java-archive', + 'wasm' => 'application/wasm', + 'wav' => 'audio/x-wav', + 'wax' => 'audio/x-ms-wax', + 'wbmp' => 'image/vnd.wap.wbmp', + 'wbs' => 'application/vnd.criticaltools.wbs+xml', + 'wbxml' => 'application/wbxml', + 'wcm' => 'application/vnd.ms-works', + 'wdb' => 'application/vnd.ms-works', + 'wdp' => 'image/vnd.ms-photo', + 'weba' => 'audio/webm', + 'webapp' => 'application/x-web-app-manifest+json', + 'webm' => 'video/webm', + 'webmanifest' => 'application/manifest+json', + 'webp' => 'image/webp', + 'wg' => 'application/vnd.pmi.widget', + 'wgt' => 'application/widget', + 'wks' => 'application/vnd.ms-works', + 'wm' => 'video/x-ms-wm', + 'wma' => 'audio/x-ms-wma', + 'wmd' => 'application/x-ms-wmd', + 'wmf' => 'image/wmf', + 'wml' => 'text/vnd.wap.wml', + 'wmlc' => 'application/wmlc', + 'wmls' => 'text/vnd.wap.wmlscript', + 'wmlsc' => 'application/vnd.wap.wmlscriptc', + 'wmv' => 'video/x-ms-wmv', + 'wmx' => 'video/x-ms-wmx', + 'wmz' => 'application/x-msmetafile', + 'woff' => 'font/woff', + 'woff2' => 'font/woff2', + 'word' => 'application/msword', + 'wpd' => 'application/vnd.wordperfect', + 'wpl' => 'application/vnd.ms-wpl', + 'wps' => 'application/vnd.ms-works', + 'wqd' => 'application/vnd.wqd', + 'wri' => 'application/x-mswrite', + 'wrl' => 'model/vrml', + 'wsc' => 'message/vnd.wfa.wsc', + 'wsdl' => 'application/wsdl+xml', + 'wspolicy' => 'application/wspolicy+xml', + 'wtb' => 'application/vnd.webturbo', + 'wvx' => 'video/x-ms-wvx', + 'x3d' => 'model/x3d+xml', + 'x3db' => 'model/x3d+fastinfoset', + 'x3dbz' => 'model/x3d+binary', + 'x3dv' => 'model/x3d-vrml', + 'x3dvz' => 'model/x3d+vrml', + 'x3dz' => 'model/x3d+xml', + 'x32' => 'application/x-authorware-bin', + 'x_b' => 'model/vnd.parasolid.transmit.binary', + 'x_t' => 'model/vnd.parasolid.transmit.text', + 'xaml' => 'application/xaml+xml', + 'xap' => 'application/x-silverlight-app', + 'xar' => 'application/vnd.xara', + 'xav' => 'application/xcap-att+xml', + 'xbap' => 'application/x-ms-xbap', + 'xbd' => 'application/vnd.fujixerox.docuworks.binder', + 'xbm' => 'image/x-xbitmap', + 'xca' => 'application/xcap-caps+xml', + 'xcs' => 'application/calendar+xml', + 'xdf' => 'application/xcap-diff+xml', + 'xdm' => 'application/vnd.syncml.dm+xml', + 'xdp' => 'application/vnd.adobe.xdp+xml', + 'xdssc' => 'application/dssc+xml', + 'xdw' => 'application/vnd.fujixerox.docuworks', + 'xel' => 'application/xcap-el+xml', + 'xenc' => 'application/xenc+xml', + 'xer' => 'application/patch-ops-error+xml', + 'xfdf' => 'application/vnd.adobe.xfdf', + 'xfdl' => 'application/vnd.xfdl', + 'xht' => 'application/xhtml+xml', + 'xhtml' => 'application/xhtml+xml', + 'xhvml' => 'application/xv+xml', + 'xif' => 'image/vnd.xiff', + 'xl' => 'application/excel', + 'xla' => 'application/vnd.ms-excel', + 'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12', + 'xlc' => 'application/vnd.ms-excel', + 'xlf' => 'application/xliff+xml', + 'xlm' => 'application/vnd.ms-excel', + 'xls' => 'application/vnd.ms-excel', + 'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12', + 'xlsm' => 'application/vnd.ms-excel.sheet.macroEnabled.12', + 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + 'xlt' => 'application/vnd.ms-excel', + 'xltm' => 'application/vnd.ms-excel.template.macroEnabled.12', + 'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template', + 'xlw' => 'application/vnd.ms-excel', + 'xm' => 'audio/xm', + 'xml' => 'application/xml', + 'xns' => 'application/xcap-ns+xml', + 'xo' => 'application/vnd.olpc-sugar', + 'xop' => 'application/xop+xml', + 'xpi' => 'application/x-xpinstall', + 'xpl' => 'application/xproc+xml', + 'xpm' => 'image/x-xpixmap', + 'xpr' => 'application/vnd.is-xpr', + 'xps' => 'application/vnd.ms-xpsdocument', + 'xpw' => 'application/vnd.intercon.formnet', + 'xpx' => 'application/vnd.intercon.formnet', + 'xsd' => 'application/xml', + 'xsl' => 'application/xml', + 'xslt' => 'application/xslt+xml', + 'xsm' => 'application/vnd.syncml+xml', + 'xspf' => 'application/xspf+xml', + 'xul' => 'application/vnd.mozilla.xul+xml', + 'xvm' => 'application/xv+xml', + 'xvml' => 'application/xv+xml', + 'xwd' => 'image/x-xwindowdump', + 'xyz' => 'chemical/x-xyz', + 'xz' => 'application/x-xz', + 'yaml' => 'text/yaml', + 'yang' => 'application/yang', + 'yin' => 'application/yin+xml', + 'yml' => 'text/yaml', + 'ymp' => 'text/x-suse-ymp', + 'z' => 'application/x-compress', + 'z1' => 'application/x-zmachine', + 'z2' => 'application/x-zmachine', + 'z3' => 'application/x-zmachine', + 'z4' => 'application/x-zmachine', + 'z5' => 'application/x-zmachine', + 'z6' => 'application/x-zmachine', + 'z7' => 'application/x-zmachine', + 'z8' => 'application/x-zmachine', + 'zaz' => 'application/vnd.zzazz.deck+xml', + 'zip' => 'application/zip', + 'zir' => 'application/vnd.zul', + 'zirz' => 'application/vnd.zul', + 'zmm' => 'application/vnd.handheld-entertainment+xml', + 'zsh' => 'text/x-scriptzsh', + ]; + /** * Determines the mimetype of a file by looking at its extension. * - * @param string $filename - * - * @return string|null + * @link https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ - public static function fromFilename($filename) + public static function fromFilename(string $filename): ?string { return self::fromExtension(pathinfo($filename, PATHINFO_EXTENSION)); } @@ -19,122 +1219,10 @@ public static function fromFilename($filename) /** * Maps a file extensions to a mimetype. * - * @param string $extension string The file extension. - * - * @return string|null - * - * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types + * @link https://raw.githubusercontent.com/jshttp/mime-db/master/db.json */ - public static function fromExtension($extension) + public static function fromExtension(string $extension): ?string { - static $mimetypes = [ - '3gp' => 'video/3gpp', - '7z' => 'application/x-7z-compressed', - 'aac' => 'audio/x-aac', - 'ai' => 'application/postscript', - 'aif' => 'audio/x-aiff', - 'asc' => 'text/plain', - 'asf' => 'video/x-ms-asf', - 'atom' => 'application/atom+xml', - 'avi' => 'video/x-msvideo', - 'bmp' => 'image/bmp', - 'bz2' => 'application/x-bzip2', - 'cer' => 'application/pkix-cert', - 'crl' => 'application/pkix-crl', - 'crt' => 'application/x-x509-ca-cert', - 'css' => 'text/css', - 'csv' => 'text/csv', - 'cu' => 'application/cu-seeme', - 'deb' => 'application/x-debian-package', - 'doc' => 'application/msword', - 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', - 'dvi' => 'application/x-dvi', - 'eot' => 'application/vnd.ms-fontobject', - 'eps' => 'application/postscript', - 'epub' => 'application/epub+zip', - 'etx' => 'text/x-setext', - 'flac' => 'audio/flac', - 'flv' => 'video/x-flv', - 'gif' => 'image/gif', - 'gz' => 'application/gzip', - 'htm' => 'text/html', - 'html' => 'text/html', - 'ico' => 'image/x-icon', - 'ics' => 'text/calendar', - 'ini' => 'text/plain', - 'iso' => 'application/x-iso9660-image', - 'jar' => 'application/java-archive', - 'jpe' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpg' => 'image/jpeg', - 'js' => 'text/javascript', - 'json' => 'application/json', - 'latex' => 'application/x-latex', - 'log' => 'text/plain', - 'm4a' => 'audio/mp4', - 'm4v' => 'video/mp4', - 'mid' => 'audio/midi', - 'midi' => 'audio/midi', - 'mov' => 'video/quicktime', - 'mkv' => 'video/x-matroska', - 'mp3' => 'audio/mpeg', - 'mp4' => 'video/mp4', - 'mp4a' => 'audio/mp4', - 'mp4v' => 'video/mp4', - 'mpe' => 'video/mpeg', - 'mpeg' => 'video/mpeg', - 'mpg' => 'video/mpeg', - 'mpg4' => 'video/mp4', - 'oga' => 'audio/ogg', - 'ogg' => 'audio/ogg', - 'ogv' => 'video/ogg', - 'ogx' => 'application/ogg', - 'pbm' => 'image/x-portable-bitmap', - 'pdf' => 'application/pdf', - 'pgm' => 'image/x-portable-graymap', - 'png' => 'image/png', - 'pnm' => 'image/x-portable-anymap', - 'ppm' => 'image/x-portable-pixmap', - 'ppt' => 'application/vnd.ms-powerpoint', - 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', - 'ps' => 'application/postscript', - 'qt' => 'video/quicktime', - 'rar' => 'application/x-rar-compressed', - 'ras' => 'image/x-cmu-raster', - 'rss' => 'application/rss+xml', - 'rtf' => 'application/rtf', - 'sgm' => 'text/sgml', - 'sgml' => 'text/sgml', - 'svg' => 'image/svg+xml', - 'swf' => 'application/x-shockwave-flash', - 'tar' => 'application/x-tar', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'torrent' => 'application/x-bittorrent', - 'ttf' => 'application/x-font-ttf', - 'txt' => 'text/plain', - 'wav' => 'audio/x-wav', - 'webm' => 'video/webm', - 'webp' => 'image/webp', - 'wma' => 'audio/x-ms-wma', - 'wmv' => 'video/x-ms-wmv', - 'woff' => 'application/x-font-woff', - 'wsdl' => 'application/wsdl+xml', - 'xbm' => 'image/x-xbitmap', - 'xls' => 'application/vnd.ms-excel', - 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', - 'xml' => 'application/xml', - 'xpm' => 'image/x-xpixmap', - 'xwd' => 'image/x-xwindowdump', - 'yaml' => 'text/yaml', - 'yml' => 'text/yaml', - 'zip' => 'application/zip', - ]; - - $extension = strtolower($extension); - - return isset($mimetypes[$extension]) - ? $mimetypes[$extension] - : null; + return self::MIME_TYPES[strtolower($extension)] ?? null; } } diff --git a/src/MultipartStream.php b/src/MultipartStream.php index 5a6079a8..c2517228 100644 --- a/src/MultipartStream.php +++ b/src/MultipartStream.php @@ -1,5 +1,7 @@ boundary = $boundary ?: sha1(uniqid('', true)); $this->stream = $this->createStream($elements); } - /** - * Get the boundary - * - * @return string - */ - public function getBoundary() + public function getBoundary(): string { return $this->boundary; } - public function isWritable() + public function isWritable(): bool { return false; } /** * Get the headers needed before transferring the content of a POST file + * + * @param array $headers */ - private function getHeaders(array $headers) + private function getHeaders(array $headers): string { $str = ''; foreach ($headers as $key => $value) { @@ -65,7 +63,7 @@ private function getHeaders(array $headers) /** * Create the aggregate stream that will be used to upload the POST data */ - protected function createStream(array $elements) + protected function createStream(array $elements = []): StreamInterface { $stream = new AppendStream(); @@ -79,7 +77,7 @@ protected function createStream(array $elements) return $stream; } - private function addElement(AppendStream $stream, array $element) + private function addElement(AppendStream $stream, array $element): void { foreach (['contents', 'name'] as $key) { if (!array_key_exists($key, $element)) { @@ -91,16 +89,16 @@ private function addElement(AppendStream $stream, array $element) if (empty($element['filename'])) { $uri = $element['contents']->getMetadata('uri'); - if (substr($uri, 0, 6) !== 'php://') { + if ($uri && \is_string($uri) && \substr($uri, 0, 6) !== 'php://' && \substr($uri, 0, 7) !== 'data://') { $element['filename'] = $uri; } } - list($body, $headers) = $this->createElement( + [$body, $headers] = $this->createElement( $element['name'], $element['contents'], - isset($element['filename']) ? $element['filename'] : null, - isset($element['headers']) ? $element['headers'] : [] + $element['filename'] ?? null, + $element['headers'] ?? [] ); $stream->addStream(Utils::streamFor($this->getHeaders($headers))); @@ -108,10 +106,7 @@ private function addElement(AppendStream $stream, array $element) $stream->addStream(Utils::streamFor("\r\n")); } - /** - * @return array - */ - private function createElement($name, StreamInterface $stream, $filename, array $headers) + private function createElement(string $name, StreamInterface $stream, ?string $filename, array $headers): array { // Set a default content-disposition header if one was no provided $disposition = $this->getHeader($headers, 'content-disposition'); @@ -144,7 +139,7 @@ private function createElement($name, StreamInterface $stream, $filename, array return [$stream, $headers]; } - private function getHeader(array $headers, $key) + private function getHeader(array $headers, string $key) { $lowercaseHeader = strtolower($key); foreach ($headers as $k => $v) { diff --git a/src/NoSeekStream.php b/src/NoSeekStream.php index d66bdde4..99e25b9e 100644 --- a/src/NoSeekStream.php +++ b/src/NoSeekStream.php @@ -1,24 +1,24 @@ source = $source; - $this->size = isset($options['size']) ? $options['size'] : null; - $this->metadata = isset($options['metadata']) ? $options['metadata'] : []; + $this->size = $options['size'] ?? null; + $this->metadata = $options['metadata'] ?? []; $this->buffer = new BufferStream(); } - public function __toString() + public function __toString(): string { try { return Utils::copyToString($this); - } catch (\Exception $e) { + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } - public function close() + public function close(): void { $this->detach(); } public function detach() { - $this->tellPos = false; + $this->tellPos = 0; $this->source = null; return null; } - public function getSize() + public function getSize(): ?int { return $this->size; } - public function tell() + public function tell(): int { return $this->tellPos; } - public function eof() + public function eof(): bool { - return !$this->source; + return $this->source === null; } - public function isSeekable() + public function isSeekable(): bool { return false; } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { throw new \RuntimeException('Cannot seek a PumpStream'); } - public function isWritable() + public function isWritable(): bool { return false; } - public function write($string) + public function write($string): int { throw new \RuntimeException('Cannot write to a PumpStream'); } - public function isReadable() + public function isReadable(): bool { return true; } - public function read($length) + public function read($length): string { $data = $this->buffer->read($length); $readLen = strlen($data); @@ -134,7 +138,7 @@ public function read($length) return $data; } - public function getContents() + public function getContents(): string { $result = ''; while (!$this->eof()) { @@ -144,16 +148,21 @@ public function getContents() return $result; } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { if (!$key) { return $this->metadata; } - return isset($this->metadata[$key]) ? $this->metadata[$key] : null; + return $this->metadata[$key] ?? null; } - private function pump($length) + private function pump(int $length): void { if ($this->source) { do { diff --git a/src/Query.php b/src/Query.php index 5a7cc035..2faab3a8 100644 --- a/src/Query.php +++ b/src/Query.php @@ -1,5 +1,7 @@ $v) { - $k = $encoder($k); + $k = $encoder((string) $k); if (!is_array($v)) { $qs .= $k; + $v = is_bool($v) ? (int) $v : $v; if ($v !== null) { - $qs .= '=' . $encoder($v); + $qs .= '=' . $encoder((string) $v); } $qs .= '&'; } else { foreach ($v as $vv) { $qs .= $k; + $vv = is_bool($vv) ? (int) $vv : $vv; if ($vv !== null) { - $qs .= '=' . $encoder($vv); + $qs .= '=' . $encoder((string) $vv); } $qs .= '&'; } diff --git a/src/Request.php b/src/Request.php index c1cdaebf..b17af66a 100644 --- a/src/Request.php +++ b/src/Request.php @@ -1,5 +1,7 @@ $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version */ public function __construct( - $method, + string $method, $uri, array $headers = [], $body = null, - $version = '1.1' + string $version = '1.1' ) { $this->assertMethod($method); if (!($uri instanceof UriInterface)) { @@ -56,14 +58,14 @@ public function __construct( } } - public function getRequestTarget() + public function getRequestTarget(): string { if ($this->requestTarget !== null) { return $this->requestTarget; } $target = $this->uri->getPath(); - if ($target == '') { + if ($target === '') { $target = '/'; } if ($this->uri->getQuery() != '') { @@ -73,7 +75,7 @@ public function getRequestTarget() return $target; } - public function withRequestTarget($requestTarget) + public function withRequestTarget($requestTarget): RequestInterface { if (preg_match('#\s#', $requestTarget)) { throw new InvalidArgumentException( @@ -86,12 +88,12 @@ public function withRequestTarget($requestTarget) return $new; } - public function getMethod() + public function getMethod(): string { return $this->method; } - public function withMethod($method) + public function withMethod($method): RequestInterface { $this->assertMethod($method); $new = clone $this; @@ -99,12 +101,12 @@ public function withMethod($method) return $new; } - public function getUri() + public function getUri(): UriInterface { return $this->uri; } - public function withUri(UriInterface $uri, $preserveHost = false) + public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface { if ($uri === $this->uri) { return $this; @@ -120,7 +122,7 @@ public function withUri(UriInterface $uri, $preserveHost = false) return $new; } - private function updateHostFromUri() + private function updateHostFromUri(): void { $host = $this->uri->getHost(); @@ -143,10 +145,13 @@ private function updateHostFromUri() $this->headers = [$header => [$host]] + $this->headers; } - private function assertMethod($method) + /** + * @param mixed $method + */ + private function assertMethod($method): void { if (!is_string($method) || $method === '') { - throw new \InvalidArgumentException('Method must be a non-empty string.'); + throw new InvalidArgumentException('Method must be a non-empty string.'); } } } diff --git a/src/Response.php b/src/Response.php index 8c01a0f5..4c6ee6f0 100644 --- a/src/Response.php +++ b/src/Response.php @@ -1,5 +1,7 @@ 'Continue', 101 => 'Switching Protocols', 102 => 'Processing', @@ -34,6 +36,7 @@ class Response implements ResponseInterface 305 => 'Use Proxy', 306 => 'Switch Proxy', 307 => 'Temporary Redirect', + 308 => 'Permanent Redirect', 400 => 'Bad Request', 401 => 'Unauthorized', 402 => 'Payment Required', @@ -71,31 +74,30 @@ class Response implements ResponseInterface 506 => 'Variant Also Negotiates', 507 => 'Insufficient Storage', 508 => 'Loop Detected', + 510 => 'Not Extended', 511 => 'Network Authentication Required', ]; /** @var string */ - private $reasonPhrase = ''; + private $reasonPhrase; /** @var int */ - private $statusCode = 200; + private $statusCode; /** * @param int $status Status code - * @param array $headers Response headers + * @param array $headers Response headers * @param string|resource|StreamInterface|null $body Response body * @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( - $status = 200, + int $status = 200, array $headers = [], $body = null, - $version = '1.1', - $reason = null + string $version = '1.1', + string $reason = null ) { - $this->assertStatusCodeIsInteger($status); - $status = (int) $status; $this->assertStatusCodeRange($status); $this->statusCode = $status; @@ -105,8 +107,8 @@ public function __construct( } $this->setHeaders($headers); - if ($reason == '' && isset(self::$phrases[$this->statusCode])) { - $this->reasonPhrase = self::$phrases[$this->statusCode]; + if ($reason == '' && isset(self::PHRASES[$this->statusCode])) { + $this->reasonPhrase = self::PHRASES[$this->statusCode]; } else { $this->reasonPhrase = (string) $reason; } @@ -114,17 +116,17 @@ public function __construct( $this->protocol = $version; } - public function getStatusCode() + public function getStatusCode(): int { return $this->statusCode; } - public function getReasonPhrase() + public function getReasonPhrase(): string { return $this->reasonPhrase; } - public function withStatus($code, $reasonPhrase = '') + public function withStatus($code, $reasonPhrase = ''): ResponseInterface { $this->assertStatusCodeIsInteger($code); $code = (int) $code; @@ -132,21 +134,24 @@ public function withStatus($code, $reasonPhrase = '') $new = clone $this; $new->statusCode = $code; - if ($reasonPhrase == '' && isset(self::$phrases[$new->statusCode])) { - $reasonPhrase = self::$phrases[$new->statusCode]; + if ($reasonPhrase == '' && isset(self::PHRASES[$new->statusCode])) { + $reasonPhrase = self::PHRASES[$new->statusCode]; } $new->reasonPhrase = (string) $reasonPhrase; return $new; } - private function assertStatusCodeIsInteger($statusCode) + /** + * @param mixed $statusCode + */ + private function assertStatusCodeIsInteger($statusCode): void { if (filter_var($statusCode, FILTER_VALIDATE_INT) === false) { throw new \InvalidArgumentException('Status code must be an integer value.'); } } - private function assertStatusCodeRange($statusCode) + private function assertStatusCodeRange(int $statusCode): void { if ($statusCode < 100 || $statusCode >= 600) { throw new \InvalidArgumentException('Status code must be an integer value between 1xx and 5xx.'); diff --git a/src/Rfc7230.php b/src/Rfc7230.php index 51b571f2..30224018 100644 --- a/src/Rfc7230.php +++ b/src/Rfc7230.php @@ -1,12 +1,16 @@ @,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; - const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; + public const HEADER_REGEX = "(^([^()<>@,;:\\\"/[\]?={}\x01-\x20\x7F]++):[ \t]*+((?:[ \t]*+[\x21-\x7E\x80-\xFF]++)*+)[ \t]*+\r?\n)m"; + public const HEADER_FOLD_REGEX = "(\r?\n[ \t]++)"; } diff --git a/src/ServerRequest.php b/src/ServerRequest.php index e6d26f5f..43cbb502 100644 --- a/src/ServerRequest.php +++ b/src/ServerRequest.php @@ -1,5 +1,7 @@ $headers Request headers * @param string|resource|StreamInterface|null $body Request body * @param string $version Protocol version * @param array $serverParams Typically the $_SERVER superglobal */ public function __construct( - $method, + string $method, $uri, array $headers = [], $body = null, - $version = '1.1', + string $version = '1.1', array $serverParams = [] ) { $this->serverParams = $serverParams; @@ -78,13 +80,11 @@ public function __construct( /** * Return an UploadedFile instance array. * - * @param array $files A array which respect $_FILES structure - * - * @return array + * @param array $files An array which respect $_FILES structure * * @throws InvalidArgumentException for unrecognized values */ - public static function normalizeFiles(array $files) + public static function normalizeFiles(array $files): array { $normalized = []; @@ -112,7 +112,7 @@ public static function normalizeFiles(array $files) * * @param array $value $_FILES struct * - * @return array|UploadedFileInterface + * @return UploadedFileInterface|UploadedFileInterface[] */ private static function createUploadedFileFromSpec(array $value) { @@ -135,11 +135,9 @@ private static function createUploadedFileFromSpec(array $value) * Loops through all nested files and returns a normalized array of * UploadedFileInterface instances. * - * @param array $files - * * @return UploadedFileInterface[] */ - private static function normalizeNestedFileSpec(array $files = []) + private static function normalizeNestedFileSpec(array $files = []): array { $normalizedFiles = []; @@ -164,12 +162,10 @@ private static function normalizeNestedFileSpec(array $files = []) * $_COOKIE * $_FILES * $_SERVER - * - * @return ServerRequestInterface */ - public static function fromGlobals() + public static function fromGlobals(): ServerRequestInterface { - $method = isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : 'GET'; + $method = $_SERVER['REQUEST_METHOD'] ?? 'GET'; $headers = getallheaders(); $uri = self::getUriFromGlobals(); $body = new CachingStream(new LazyOpenStream('php://input', 'r+')); @@ -184,7 +180,7 @@ public static function fromGlobals() ->withUploadedFiles(self::normalizeFiles($_FILES)); } - private static function extractHostAndPortFromAuthority($authority) + private static function extractHostAndPortFromAuthority(string $authority): array { $uri = 'http://' . $authority; $parts = parse_url($uri); @@ -192,18 +188,16 @@ private static function extractHostAndPortFromAuthority($authority) return [null, null]; } - $host = isset($parts['host']) ? $parts['host'] : null; - $port = isset($parts['port']) ? $parts['port'] : null; + $host = $parts['host'] ?? null; + $port = $parts['port'] ?? null; return [$host, $port]; } /** * Get a Uri populated with values from $_SERVER. - * - * @return UriInterface */ - public static function getUriFromGlobals() + public static function getUriFromGlobals(): UriInterface { $uri = new Uri(''); @@ -211,7 +205,7 @@ public static function getUriFromGlobals() $hasPort = false; if (isset($_SERVER['HTTP_HOST'])) { - list($host, $port) = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); + [$host, $port] = self::extractHostAndPortFromAuthority($_SERVER['HTTP_HOST']); if ($host !== null) { $uri = $uri->withHost($host); } @@ -247,26 +241,17 @@ public static function getUriFromGlobals() return $uri; } - /** - * {@inheritdoc} - */ - public function getServerParams() + public function getServerParams(): array { return $this->serverParams; } - /** - * {@inheritdoc} - */ - public function getUploadedFiles() + public function getUploadedFiles(): array { return $this->uploadedFiles; } - /** - * {@inheritdoc} - */ - public function withUploadedFiles(array $uploadedFiles) + public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface { $new = clone $this; $new->uploadedFiles = $uploadedFiles; @@ -274,18 +259,12 @@ public function withUploadedFiles(array $uploadedFiles) return $new; } - /** - * {@inheritdoc} - */ - public function getCookieParams() + public function getCookieParams(): array { return $this->cookieParams; } - /** - * {@inheritdoc} - */ - public function withCookieParams(array $cookies) + public function withCookieParams(array $cookies): ServerRequestInterface { $new = clone $this; $new->cookieParams = $cookies; @@ -293,18 +272,12 @@ public function withCookieParams(array $cookies) return $new; } - /** - * {@inheritdoc} - */ - public function getQueryParams() + public function getQueryParams(): array { return $this->queryParams; } - /** - * {@inheritdoc} - */ - public function withQueryParams(array $query) + public function withQueryParams(array $query): ServerRequestInterface { $new = clone $this; $new->queryParams = $query; @@ -314,16 +287,15 @@ public function withQueryParams(array $query) /** * {@inheritdoc} + * + * @return array|object|null */ public function getParsedBody() { return $this->parsedBody; } - /** - * {@inheritdoc} - */ - public function withParsedBody($data) + public function withParsedBody($data): ServerRequestInterface { $new = clone $this; $new->parsedBody = $data; @@ -331,16 +303,15 @@ public function withParsedBody($data) return $new; } - /** - * {@inheritdoc} - */ - public function getAttributes() + public function getAttributes(): array { return $this->attributes; } /** * {@inheritdoc} + * + * @return mixed */ public function getAttribute($attribute, $default = null) { @@ -351,10 +322,7 @@ public function getAttribute($attribute, $default = null) return $this->attributes[$attribute]; } - /** - * {@inheritdoc} - */ - public function withAttribute($attribute, $value) + public function withAttribute($attribute, $value): ServerRequestInterface { $new = clone $this; $new->attributes[$attribute] = $value; @@ -362,10 +330,7 @@ public function withAttribute($attribute, $value) return $new; } - /** - * {@inheritdoc} - */ - public function withoutAttribute($attribute) + public function withoutAttribute($attribute): ServerRequestInterface { if (false === array_key_exists($attribute, $this->attributes)) { return $this; diff --git a/src/Stream.php b/src/Stream.php index 3865d6d6..d389427c 100644 --- a/src/Stream.php +++ b/src/Stream.php @@ -1,33 +1,36 @@ size = $options['size']; } - $this->customMetadata = isset($options['metadata']) - ? $options['metadata'] - : []; - + $this->customMetadata = $options['metadata'] ?? []; $this->stream = $stream; $meta = stream_get_meta_data($this->stream); $this->seekable = $meta['seekable']; @@ -74,19 +74,23 @@ public function __destruct() $this->close(); } - public function __toString() + public function __toString(): string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); - } catch (\Exception $e) { + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } - public function getContents() + public function getContents(): string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); @@ -101,7 +105,7 @@ public function getContents() return $contents; } - public function close() + public function close(): void { if (isset($this->stream)) { if (is_resource($this->stream)) { @@ -125,7 +129,7 @@ public function detach() return $result; } - public function getSize() + public function getSize(): ?int { if ($this->size !== null) { return $this->size; @@ -141,7 +145,7 @@ public function getSize() } $stats = fstat($this->stream); - if (isset($stats['size'])) { + if (is_array($stats) && isset($stats['size'])) { $this->size = $stats['size']; return $this->size; } @@ -149,22 +153,22 @@ public function getSize() return null; } - public function isReadable() + public function isReadable(): bool { return $this->readable; } - public function isWritable() + public function isWritable(): bool { return $this->writable; } - public function isSeekable() + public function isSeekable(): bool { return $this->seekable; } - public function eof() + public function eof(): bool { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); @@ -173,7 +177,7 @@ public function eof() return feof($this->stream); } - public function tell() + public function tell(): int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); @@ -188,12 +192,12 @@ public function tell() return $result; } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { $whence = (int) $whence; @@ -209,7 +213,7 @@ public function seek($offset, $whence = SEEK_SET) } } - public function read($length) + public function read($length): string { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); @@ -233,7 +237,7 @@ public function read($length) return $string; } - public function write($string) + public function write($string): int { if (!isset($this->stream)) { throw new \RuntimeException('Stream is detached'); @@ -253,6 +257,11 @@ public function write($string) return $result; } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { if (!isset($this->stream)) { @@ -265,6 +274,6 @@ public function getMetadata($key = null) $meta = stream_get_meta_data($this->stream); - return isset($meta[$key]) ? $meta[$key] : null; + return $meta[$key] ?? null; } } diff --git a/src/StreamDecoratorTrait.php b/src/StreamDecoratorTrait.php index 5025dd67..56d4104d 100644 --- a/src/StreamDecoratorTrait.php +++ b/src/StreamDecoratorTrait.php @@ -1,5 +1,7 @@ stream = $this->createStream(); return $this->stream; } @@ -37,22 +37,23 @@ public function __get($name) throw new \UnexpectedValueException("$name not found on class"); } - public function __toString() + public function __toString(): string { try { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); - } catch (\Exception $e) { - // Really, PHP? https://bugs.php.net/bug.php?id=53648 - trigger_error('StreamDecorator::__toString exception: ' - . (string) $e, E_USER_ERROR); + } catch (\Throwable $e) { + if (\PHP_VERSION_ID >= 70400) { + throw $e; + } + trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); return ''; } } - public function getContents() + public function getContents(): string { return Utils::copyToString($this); } @@ -60,24 +61,28 @@ public function getContents() /** * Allow decorators to implement custom methods * - * @param string $method Missing method name - * @param array $args Method arguments - * * @return mixed */ - public function __call($method, array $args) + public function __call(string $method, array $args) { - $result = call_user_func_array([$this->stream, $method], $args); + /** @var callable $callable */ + $callable = [$this->stream, $method]; + $result = call_user_func_array($callable, $args); // Always return the wrapped object if the result is a return $this return $result === $this->stream ? $this : $result; } - public function close() + public function close(): void { $this->stream->close(); } + /** + * {@inheritdoc} + * + * @return mixed + */ public function getMetadata($key = null) { return $this->stream->getMetadata($key); @@ -88,52 +93,52 @@ public function detach() return $this->stream->detach(); } - public function getSize() + public function getSize(): ?int { return $this->stream->getSize(); } - public function eof() + public function eof(): bool { return $this->stream->eof(); } - public function tell() + public function tell(): int { return $this->stream->tell(); } - public function isReadable() + public function isReadable(): bool { return $this->stream->isReadable(); } - public function isWritable() + public function isWritable(): bool { return $this->stream->isWritable(); } - public function isSeekable() + public function isSeekable(): bool { return $this->stream->isSeekable(); } - public function rewind() + public function rewind(): void { $this->seek(0); } - public function seek($offset, $whence = SEEK_SET) + public function seek($offset, $whence = SEEK_SET): void { $this->stream->seek($offset, $whence); } - public function read($length) + public function read($length): string { return $this->stream->read($length); } - public function write($string) + public function write($string): int { return $this->stream->write($string); } @@ -141,11 +146,9 @@ public function write($string) /** * Implement in subclasses to dynamically create streams when requested. * - * @return StreamInterface - * * @throws \BadMethodCallException */ - protected function createStream() + protected function createStream(): StreamInterface { throw new \BadMethodCallException('Not implemented'); } diff --git a/src/StreamWrapper.php b/src/StreamWrapper.php index fc7cb969..2a934640 100644 --- a/src/StreamWrapper.php +++ b/src/StreamWrapper.php @@ -1,5 +1,7 @@ context); @@ -83,41 +83,48 @@ public function stream_open($path, $mode, $options, &$opened_path) return true; } - public function stream_read($count) + public function stream_read(int $count): string { return $this->stream->read($count); } - public function stream_write($data) + public function stream_write(string $data): int { - return (int) $this->stream->write($data); + return $this->stream->write($data); } - public function stream_tell() + public function stream_tell(): int { return $this->stream->tell(); } - public function stream_eof() + public function stream_eof(): bool { return $this->stream->eof(); } - public function stream_seek($offset, $whence) + public function stream_seek(int $offset, int $whence): bool { $this->stream->seek($offset, $whence); return true; } - public function stream_cast($cast_as) + /** + * @return resource|false + */ + public function stream_cast(int $cast_as) { $stream = clone($this->stream); + $resource = $stream->detach(); - return $stream->detach(); + return $resource ?? false; } - public function stream_stat() + /** + * @return array + */ + public function stream_stat(): array { static $modeMap = [ 'r' => 33060, @@ -144,7 +151,10 @@ public function stream_stat() ]; } - public function url_stat($path, $flags) + /** + * @return array + */ + public function url_stat(string $path, int $flags): array { return [ 'dev' => 0, diff --git a/src/UploadedFile.php b/src/UploadedFile.php index bf342c4d..b1521bcf 100644 --- a/src/UploadedFile.php +++ b/src/UploadedFile.php @@ -1,5 +1,7 @@ setError($errorStatus); - $this->setSize($size); - $this->setClientFilename($clientFilename); - $this->setClientMediaType($clientMediaType); + $this->size = $size; + $this->clientFilename = $clientFilename; + $this->clientMediaType = $clientMediaType; if ($this->isOk()) { $this->setStreamOrFile($streamOrFile); @@ -85,11 +80,11 @@ public function __construct( /** * Depending on the value set file or stream variable * - * @param mixed $streamOrFile + * @param StreamInterface|string|resource $streamOrFile * * @throws InvalidArgumentException */ - private function setStreamOrFile($streamOrFile) + private function setStreamOrFile($streamOrFile): void { if (is_string($streamOrFile)) { $this->file = $streamOrFile; @@ -105,19 +100,11 @@ private function setStreamOrFile($streamOrFile) } /** - * @param int $error - * * @throws InvalidArgumentException */ - private function setError($error) + private function setError(int $error): void { - if (false === is_int($error)) { - throw new InvalidArgumentException( - 'Upload file error status must be an integer' - ); - } - - if (false === in_array($error, UploadedFile::$errors)) { + if (false === in_array($error, UploadedFile::ERRORS, true)) { throw new InvalidArgumentException( 'Invalid error status for UploadedFile' ); @@ -126,88 +113,20 @@ private function setError($error) $this->error = $error; } - /** - * @param int $size - * - * @throws InvalidArgumentException - */ - private function setSize($size) - { - if (false === is_int($size)) { - throw new InvalidArgumentException( - 'Upload file size must be an integer' - ); - } - - $this->size = $size; - } - - /** - * @param mixed $param - * - * @return bool - */ - private function isStringOrNull($param) - { - return in_array(gettype($param), ['string', 'NULL']); - } - - /** - * @param mixed $param - * - * @return bool - */ - private function isStringNotEmpty($param) + private function isStringNotEmpty($param): bool { return is_string($param) && false === empty($param); } - /** - * @param string|null $clientFilename - * - * @throws InvalidArgumentException - */ - private function setClientFilename($clientFilename) - { - if (false === $this->isStringOrNull($clientFilename)) { - throw new InvalidArgumentException( - 'Upload file client filename must be a string or null' - ); - } - - $this->clientFilename = $clientFilename; - } - - /** - * @param string|null $clientMediaType - * - * @throws InvalidArgumentException - */ - private function setClientMediaType($clientMediaType) - { - if (false === $this->isStringOrNull($clientMediaType)) { - throw new InvalidArgumentException( - 'Upload file client media type must be a string or null' - ); - } - - $this->clientMediaType = $clientMediaType; - } - /** * Return true if there is no upload error - * - * @return bool */ - private function isOk() + private function isOk(): bool { return $this->error === UPLOAD_ERR_OK; } - /** - * @return bool - */ - public function isMoved() + public function isMoved(): bool { return $this->moved; } @@ -215,7 +134,7 @@ public function isMoved() /** * @throws RuntimeException if is moved or not ok */ - private function validateActive() + private function validateActive(): void { if (false === $this->isOk()) { throw new RuntimeException('Cannot retrieve stream due to upload error'); @@ -226,12 +145,7 @@ private function validateActive() } } - /** - * {@inheritdoc} - * - * @throws RuntimeException if the upload was not successful. - */ - public function getStream() + public function getStream(): StreamInterface { $this->validateActive(); @@ -239,23 +153,13 @@ public function getStream() return $this->stream; } - return new LazyOpenStream($this->file, 'r+'); + /** @var string $file */ + $file = $this->file; + + return new LazyOpenStream($file, 'r+'); } - /** - * {@inheritdoc} - * - * @see http://php.net/is_uploaded_file - * @see http://php.net/move_uploaded_file - * - * @param string $targetPath Path to which to move the uploaded file. - * - * @throws RuntimeException if the upload was not successful. - * @throws InvalidArgumentException if the $path specified is invalid. - * @throws RuntimeException on any error during the move operation, or on - * the second or subsequent call to the method. - */ - public function moveTo($targetPath) + public function moveTo($targetPath): void { $this->validateActive(); @@ -266,7 +170,7 @@ public function moveTo($targetPath) } if ($this->file) { - $this->moved = php_sapi_name() == 'cli' + $this->moved = PHP_SAPI === 'cli' ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath); } else { @@ -285,43 +189,22 @@ public function moveTo($targetPath) } } - /** - * {@inheritdoc} - * - * @return int|null The file size in bytes or null if unknown. - */ - public function getSize() + public function getSize(): ?int { return $this->size; } - /** - * {@inheritdoc} - * - * @see http://php.net/manual/en/features.file-upload.errors.php - * - * @return int One of PHP's UPLOAD_ERR_XXX constants. - */ - public function getError() + public function getError(): int { return $this->error; } - /** - * {@inheritdoc} - * - * @return string|null The filename sent by the client or null if none - * was provided. - */ - public function getClientFilename() + public function getClientFilename(): ?string { return $this->clientFilename; } - /** - * {@inheritdoc} - */ - public function getClientMediaType() + public function getClientMediaType(): ?string { return $this->clientMediaType; } diff --git a/src/Uri.php b/src/Uri.php index 0f9f020d..5c6416ae 100644 --- a/src/Uri.php +++ b/src/Uri.php @@ -1,7 +1,10 @@ 80, 'https' => 443, 'ftp' => 21, @@ -35,9 +38,20 @@ class Uri implements UriInterface 'ldap' => 389, ]; - private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; - private static $charSubDelims = '!\$&\'\(\)\*\+,;='; - private static $replaceQuery = ['=' => '%3D', '&' => '%26']; + /** + * Unreserved characters for use in a regex. + * + * @link https://tools.ietf.org/html/rfc3986#section-2.3 + */ + private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; + + /** + * Sub-delims for use in a regex. + * + * @link https://tools.ietf.org/html/rfc3986#section-2.2 + */ + private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; + private const QUERY_SEPARATORS_REPLACEMENT = ['=' => '%3D', '&' => '%26']; /** @var string Uri scheme. */ private $scheme = ''; @@ -60,21 +74,19 @@ class Uri implements UriInterface /** @var string Uri fragment. */ private $fragment = ''; - /** - * @param string $uri URI to parse - */ - public function __construct($uri = '') + /** @var string|null String representation */ + private $composedComponents; + + public function __construct(string $uri = '') { - // weak type check to also accept null until we can add scalar type hints - if ($uri != '') { + if ($uri !== '') { $parts = self::parse($uri); if ($parts === false) { - throw new \InvalidArgumentException("Unable to parse URI: $uri"); + throw new MalformedUriException("Unable to parse URI: $uri"); } $this->applyParts($parts); } } - /** * UTF-8 aware \parse_url() replacement. * @@ -88,19 +100,19 @@ public function __construct($uri = '') * @see https://www.php.net/manual/en/function.parse-url.php#114817 * @see https://curl.haxx.se/libcurl/c/CURLOPT_URL.html#ENCODING * - * @param string $url - * * @return array|false */ - private static function parse($url) + private static function parse(string $url) { // If IPv6 $prefix = ''; if (preg_match('%^(.*://\[[0-9:a-f]+\])(.*?)$%', $url, $matches)) { + /** @var array{0:string, 1:string, 2:string} $matches */ $prefix = $matches[1]; $url = $matches[2]; } + /** @var string */ $encodedUrl = preg_replace_callback( '%[^:/@?&=#]+%usD', static function ($matches) { @@ -118,15 +130,19 @@ static function ($matches) { return array_map('urldecode', $result); } - public function __toString() + public function __toString(): string { - return self::composeComponents( - $this->scheme, - $this->getAuthority(), - $this->path, - $this->query, - $this->fragment - ); + if ($this->composedComponents === null) { + $this->composedComponents = self::composeComponents( + $this->scheme, + $this->getAuthority(), + $this->path, + $this->query, + $this->fragment + ); + } + + return $this->composedComponents; } /** @@ -145,17 +161,9 @@ public function __toString() * `file:///` is the more common syntax for the file scheme anyway (Chrome for example redirects to * that format). * - * @param string $scheme - * @param string $authority - * @param string $path - * @param string $query - * @param string $fragment - * - * @return string - * * @link https://tools.ietf.org/html/rfc3986#section-5.3 */ - public static function composeComponents($scheme, $authority, $path, $query, $fragment) + public static function composeComponents(?string $scheme, ?string $authority, string $path, ?string $query, ?string $fragment): string { $uri = ''; @@ -186,15 +194,11 @@ public static function composeComponents($scheme, $authority, $path, $query, $fr * * `Psr\Http\Message\UriInterface::getPort` may return null or the standard port. This method can be used * independently of the implementation. - * - * @param UriInterface $uri - * - * @return bool */ - public static function isDefaultPort(UriInterface $uri) + public static function isDefaultPort(UriInterface $uri): bool { return $uri->getPort() === null - || (isset(self::$defaultPorts[$uri->getScheme()]) && $uri->getPort() === self::$defaultPorts[$uri->getScheme()]); + || (isset(self::DEFAULT_PORTS[$uri->getScheme()]) && $uri->getPort() === self::DEFAULT_PORTS[$uri->getScheme()]); } /** @@ -207,16 +211,12 @@ public static function isDefaultPort(UriInterface $uri) * - absolute-path references, e.g. '/path' * - relative-path references, e.g. 'subpath' * - * @param UriInterface $uri - * - * @return bool - * * @see Uri::isNetworkPathReference * @see Uri::isAbsolutePathReference * @see Uri::isRelativePathReference * @link https://tools.ietf.org/html/rfc3986#section-4 */ - public static function isAbsolute(UriInterface $uri) + public static function isAbsolute(UriInterface $uri): bool { return $uri->getScheme() !== ''; } @@ -226,13 +226,9 @@ public static function isAbsolute(UriInterface $uri) * * A relative reference that begins with two slash characters is termed an network-path reference. * - * @param UriInterface $uri - * - * @return bool - * * @link https://tools.ietf.org/html/rfc3986#section-4.2 */ - public static function isNetworkPathReference(UriInterface $uri) + public static function isNetworkPathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() !== ''; } @@ -242,13 +238,9 @@ public static function isNetworkPathReference(UriInterface $uri) * * A relative reference that begins with a single slash character is termed an absolute-path reference. * - * @param UriInterface $uri - * - * @return bool - * * @link https://tools.ietf.org/html/rfc3986#section-4.2 */ - public static function isAbsolutePathReference(UriInterface $uri) + public static function isAbsolutePathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' @@ -261,13 +253,9 @@ public static function isAbsolutePathReference(UriInterface $uri) * * A relative reference that does not begin with a slash character is termed a relative-path reference. * - * @param UriInterface $uri - * - * @return bool - * * @link https://tools.ietf.org/html/rfc3986#section-4.2 */ - public static function isRelativePathReference(UriInterface $uri) + public static function isRelativePathReference(UriInterface $uri): bool { return $uri->getScheme() === '' && $uri->getAuthority() === '' @@ -284,11 +272,9 @@ public static function isRelativePathReference(UriInterface $uri) * @param UriInterface $uri The URI to check * @param UriInterface|null $base An optional base URI to compare against * - * @return bool - * * @link https://tools.ietf.org/html/rfc3986#section-4.4 */ - public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null) + public static function isSameDocumentReference(UriInterface $uri, UriInterface $base = null): bool { if ($base !== null) { $uri = UriResolver::resolve($base, $uri); @@ -302,41 +288,6 @@ public static function isSameDocumentReference(UriInterface $uri, UriInterface $ return $uri->getScheme() === '' && $uri->getAuthority() === '' && $uri->getPath() === '' && $uri->getQuery() === ''; } - /** - * Removes dot segments from a path and returns the new path. - * - * @param string $path - * - * @return string - * - * @deprecated since version 1.4. Use UriResolver::removeDotSegments instead. - * @see UriResolver::removeDotSegments - */ - public static function removeDotSegments($path) - { - return UriResolver::removeDotSegments($path); - } - - /** - * Converts the relative URI into a new URI that is resolved against the base URI. - * - * @param UriInterface $base Base URI - * @param string|UriInterface $rel Relative URI - * - * @return UriInterface - * - * @deprecated since version 1.4. Use UriResolver::resolve instead. - * @see UriResolver::resolve - */ - public static function resolve(UriInterface $base, $rel) - { - if (!($rel instanceof UriInterface)) { - $rel = new self($rel); - } - - return UriResolver::resolve($base, $rel); - } - /** * Creates a new URI with a specific query string value removed. * @@ -345,10 +296,8 @@ public static function resolve(UriInterface $base, $rel) * * @param UriInterface $uri URI to use as a base. * @param string $key Query string key to remove. - * - * @return UriInterface */ - public static function withoutQueryValue(UriInterface $uri, $key) + public static function withoutQueryValue(UriInterface $uri, string $key): UriInterface { $result = self::getFilteredQueryString($uri, [$key]); @@ -367,10 +316,8 @@ public static function withoutQueryValue(UriInterface $uri, $key) * @param UriInterface $uri URI to use as a base. * @param string $key Key to set. * @param string|null $value Value to set - * - * @return UriInterface */ - public static function withQueryValue(UriInterface $uri, $key, $value) + public static function withQueryValue(UriInterface $uri, string $key, ?string $value): UriInterface { $result = self::getFilteredQueryString($uri, [$key]); @@ -384,17 +331,15 @@ public static function withQueryValue(UriInterface $uri, $key, $value) * * It has the same behavior as withQueryValue() but for an associative array of key => value. * - * @param UriInterface $uri URI to use as a base. - * @param array $keyValueArray Associative array of key and values - * - * @return UriInterface + * @param UriInterface $uri URI to use as a base. + * @param array $keyValueArray Associative array of key and values */ - public static function withQueryValues(UriInterface $uri, array $keyValueArray) + public static function withQueryValues(UriInterface $uri, array $keyValueArray): UriInterface { $result = self::getFilteredQueryString($uri, array_keys($keyValueArray)); foreach ($keyValueArray as $key => $value) { - $result[] = self::generateQueryString($key, $value); + $result[] = self::generateQueryString((string) $key, $value !== null ? (string) $value : null); } return $uri->withQuery(implode('&', $result)); @@ -403,15 +348,11 @@ public static function withQueryValues(UriInterface $uri, array $keyValueArray) /** * Creates a URI from a hash of `parse_url` components. * - * @param array $parts - * - * @return UriInterface - * * @link http://php.net/manual/en/function.parse-url.php * - * @throws \InvalidArgumentException If the components do not form a valid URI. + * @throws MalformedUriException If the components do not form a valid URI. */ - public static function fromParts(array $parts) + public static function fromParts(array $parts): UriInterface { $uri = new self(); $uri->applyParts($parts); @@ -420,12 +361,12 @@ public static function fromParts(array $parts) return $uri; } - public function getScheme() + public function getScheme(): string { return $this->scheme; } - public function getAuthority() + public function getAuthority(): string { $authority = $this->host; if ($this->userInfo !== '') { @@ -439,37 +380,37 @@ public function getAuthority() return $authority; } - public function getUserInfo() + public function getUserInfo(): string { return $this->userInfo; } - public function getHost() + public function getHost(): string { return $this->host; } - public function getPort() + public function getPort(): ?int { return $this->port; } - public function getPath() + public function getPath(): string { return $this->path; } - public function getQuery() + public function getQuery(): string { return $this->query; } - public function getFragment() + public function getFragment(): string { return $this->fragment; } - public function withScheme($scheme) + public function withScheme($scheme): UriInterface { $scheme = $this->filterScheme($scheme); @@ -479,13 +420,14 @@ public function withScheme($scheme) $new = clone $this; $new->scheme = $scheme; + $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } - public function withUserInfo($user, $password = null) + public function withUserInfo($user, $password = null): UriInterface { $info = $this->filterUserInfoComponent($user); if ($password !== null) { @@ -498,12 +440,13 @@ public function withUserInfo($user, $password = null) $new = clone $this; $new->userInfo = $info; + $new->composedComponents = null; $new->validateState(); return $new; } - public function withHost($host) + public function withHost($host): UriInterface { $host = $this->filterHost($host); @@ -513,12 +456,13 @@ public function withHost($host) $new = clone $this; $new->host = $host; + $new->composedComponents = null; $new->validateState(); return $new; } - public function withPort($port) + public function withPort($port): UriInterface { $port = $this->filterPort($port); @@ -528,13 +472,14 @@ public function withPort($port) $new = clone $this; $new->port = $port; + $new->composedComponents = null; $new->removeDefaultPort(); $new->validateState(); return $new; } - public function withPath($path) + public function withPath($path): UriInterface { $path = $this->filterPath($path); @@ -544,12 +489,13 @@ public function withPath($path) $new = clone $this; $new->path = $path; + $new->composedComponents = null; $new->validateState(); return $new; } - public function withQuery($query) + public function withQuery($query): UriInterface { $query = $this->filterQueryAndFragment($query); @@ -559,11 +505,12 @@ public function withQuery($query) $new = clone $this; $new->query = $query; + $new->composedComponents = null; return $new; } - public function withFragment($fragment) + public function withFragment($fragment): UriInterface { $fragment = $this->filterQueryAndFragment($fragment); @@ -573,16 +520,22 @@ public function withFragment($fragment) $new = clone $this; $new->fragment = $fragment; + $new->composedComponents = null; return $new; } + public function jsonSerialize(): string + { + return $this->__toString(); + } + /** * Apply parse_url parts to a URI. * * @param array $parts Array of parse_url parts to apply. */ - private function applyParts(array $parts) + private function applyParts(array $parts): void { $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) @@ -613,13 +566,11 @@ private function applyParts(array $parts) } /** - * @param string $scheme - * - * @return string + * @param mixed $scheme * * @throws \InvalidArgumentException If the scheme is invalid. */ - private function filterScheme($scheme) + private function filterScheme($scheme): string { if (!is_string($scheme)) { throw new \InvalidArgumentException('Scheme must be a string'); @@ -629,33 +580,29 @@ private function filterScheme($scheme) } /** - * @param string $component - * - * @return string + * @param mixed $component * * @throws \InvalidArgumentException If the user info is invalid. */ - private function filterUserInfoComponent($component) + private function filterUserInfoComponent($component): string { if (!is_string($component)) { throw new \InvalidArgumentException('User info must be a string'); } return preg_replace_callback( - '/(?:[^%' . self::$charUnreserved . self::$charSubDelims . ']+|%(?![A-Fa-f0-9]{2}))/', + '/(?:[^%' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . ']+|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $component ); } /** - * @param string $host - * - * @return string + * @param mixed $host * * @throws \InvalidArgumentException If the host is invalid. */ - private function filterHost($host) + private function filterHost($host): string { if (!is_string($host)) { throw new \InvalidArgumentException('Host must be a string'); @@ -665,13 +612,11 @@ private function filterHost($host) } /** - * @param int|null $port - * - * @return int|null + * @param mixed $port * * @throws \InvalidArgumentException If the port is invalid. */ - private function filterPort($port) + private function filterPort($port): ?int { if ($port === null) { return null; @@ -688,12 +633,11 @@ private function filterPort($port) } /** - * @param UriInterface $uri - * @param array $keys + * @param string[] $keys * - * @return array + * @return string[] */ - private static function getFilteredQueryString(UriInterface $uri, array $keys) + private static function getFilteredQueryString(UriInterface $uri, array $keys): array { $current = $uri->getQuery(); @@ -708,27 +652,21 @@ private static function getFilteredQueryString(UriInterface $uri, array $keys) }); } - /** - * @param string $key - * @param string|null $value - * - * @return string - */ - private static function generateQueryString($key, $value) + private static function generateQueryString(string $key, ?string $value): string { // Query string separators ("=", "&") within the key or value need to be encoded // (while preventing double-encoding) before setting the query string. All other // chars that need percent-encoding will be encoded by withQuery(). - $queryString = strtr($key, self::$replaceQuery); + $queryString = strtr($key, self::QUERY_SEPARATORS_REPLACEMENT); if ($value !== null) { - $queryString .= '=' . strtr($value, self::$replaceQuery); + $queryString .= '=' . strtr($value, self::QUERY_SEPARATORS_REPLACEMENT); } return $queryString; } - private function removeDefaultPort() + private function removeDefaultPort(): void { if ($this->port !== null && self::isDefaultPort($this)) { $this->port = null; @@ -738,20 +676,18 @@ private function removeDefaultPort() /** * Filters the path of a URI * - * @param string $path - * - * @return string + * @param mixed $path * * @throws \InvalidArgumentException If the path is invalid. */ - private function filterPath($path) + private function filterPath($path): string { if (!is_string($path)) { throw new \InvalidArgumentException('Path must be a string'); } return preg_replace_callback( - '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', + '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $path ); @@ -760,31 +696,29 @@ private function filterPath($path) /** * Filters the query string or fragment of a URI. * - * @param string $str - * - * @return string + * @param mixed $str * * @throws \InvalidArgumentException If the query or fragment is invalid. */ - private function filterQueryAndFragment($str) + private function filterQueryAndFragment($str): string { if (!is_string($str)) { throw new \InvalidArgumentException('Query and fragment must be a string'); } return preg_replace_callback( - '/(?:[^' . self::$charUnreserved . self::$charSubDelims . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', + '/(?:[^' . self::CHAR_UNRESERVED . self::CHAR_SUB_DELIMS . '%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', [$this, 'rawurlencodeMatchZero'], $str ); } - private function rawurlencodeMatchZero(array $match) + private function rawurlencodeMatchZero(array $match): string { return rawurlencode($match[0]); } - private function validateState() + private function validateState(): void { if ($this->host === '' && ($this->scheme === 'http' || $this->scheme === 'https')) { $this->host = self::HTTP_DEFAULT_HOST; @@ -792,19 +726,13 @@ private function validateState() if ($this->getAuthority() === '') { if (0 === strpos($this->path, '//')) { - throw new \InvalidArgumentException('The path of a URI without an authority must not start with two slashes "//"'); + throw new MalformedUriException('The path of a URI without an authority must not start with two slashes "//"'); } if ($this->scheme === '' && false !== strpos(explode('/', $this->path, 2)[0], ':')) { - throw new \InvalidArgumentException('A relative URI must not have a path beginning with a segment containing a colon'); + throw new MalformedUriException('A relative URI must not have a path beginning with a segment containing a colon'); } } elseif (isset($this->path[0]) && $this->path[0] !== '/') { - @trigger_error( - 'The path of a URI with an authority must start with a slash "/" or be empty. Automagically fixing the URI ' . - 'by adding a leading slash to the path is deprecated since version 1.4 and will throw an exception instead.', - E_USER_DEPRECATED - ); - $this->path = '/' . $this->path; - //throw new \InvalidArgumentException('The path of a URI with an authority must start with a slash "/" or be empty'); + throw new MalformedUriException('The path of a URI with an authority must start with a slash "/" or be empty'); } } } diff --git a/src/UriNormalizer.php b/src/UriNormalizer.php index 81419ead..e12971ed 100644 --- a/src/UriNormalizer.php +++ b/src/UriNormalizer.php @@ -1,5 +1,7 @@ getScheme() !== '' && ($base->getScheme() !== $target->getScheme() || $target->getAuthority() === '' && $base->getAuthority() !== '') @@ -174,6 +162,7 @@ public static function relativize(UriInterface $base, UriInterface $target) // inherit the base query component when resolving. if ($target->getQuery() === '') { $segments = explode('/', $target->getPath()); + /** @var string $lastSegment */ $lastSegment = end($segments); return $emptyPathUri->withPath($lastSegment === '' ? './' : $lastSegment); @@ -182,7 +171,7 @@ public static function relativize(UriInterface $base, UriInterface $target) return $emptyPathUri; } - private static function getRelativePath(UriInterface $base, UriInterface $target) + private static function getRelativePath(UriInterface $base, UriInterface $target): string { $sourceSegments = explode('/', $base->getPath()); $targetSegments = explode('/', $target->getPath()); diff --git a/src/Utils.php b/src/Utils.php index 6b6c8cce..e590ad68 100644 --- a/src/Utils.php +++ b/src/Utils.php @@ -1,5 +1,7 @@ $keys - * - * @return array + * @param string[] $keys */ - public static function caselessRemove($keys, array $data) + public static function caselessRemove(array $keys, array $data): array { $result = []; @@ -25,7 +25,7 @@ public static function caselessRemove($keys, array $data) } foreach ($data as $k => $v) { - if (!in_array(strtolower($k), $keys)) { + if (!is_string($k) || !in_array(strtolower($k), $keys)) { $result[$k] = $v; } } @@ -44,7 +44,7 @@ public static function caselessRemove($keys, array $data) * * @throws \RuntimeException on error. */ - public static function copyToStream(StreamInterface $source, StreamInterface $dest, $maxLen = -1) + public static function copyToStream(StreamInterface $source, StreamInterface $dest, int $maxLen = -1): void { $bufferSize = 8192; @@ -76,19 +76,16 @@ public static function copyToStream(StreamInterface $source, StreamInterface $de * @param int $maxLen Maximum number of bytes to read. Pass -1 * to read the entire stream. * - * @return string - * * @throws \RuntimeException on error. */ - public static function copyToString(StreamInterface $stream, $maxLen = -1) + public static function copyToString(StreamInterface $stream, int $maxLen = -1): string { $buffer = ''; if ($maxLen === -1) { while (!$stream->eof()) { $buf = $stream->read(1048576); - // Using a loose equality here to match on '' and false. - if ($buf == null) { + if ($buf === '') { break; } $buffer .= $buf; @@ -99,8 +96,7 @@ public static function copyToString(StreamInterface $stream, $maxLen = -1) $len = 0; while (!$stream->eof() && $len < $maxLen) { $buf = $stream->read($maxLen - $len); - // Using a loose equality here to match on '' and false. - if ($buf == null) { + if ($buf === '') { break; } $buffer .= $buf; @@ -120,11 +116,9 @@ public static function copyToString(StreamInterface $stream, $maxLen = -1) * @param string $algo Hash algorithm (e.g. md5, crc32, etc) * @param bool $rawOutput Whether or not to use raw output * - * @return string Returns the hash of the stream - * * @throws \RuntimeException on error. */ - public static function hash(StreamInterface $stream, $algo, $rawOutput = false) + public static function hash(StreamInterface $stream, string $algo, bool $rawOutput = false): string { $pos = $stream->tell(); @@ -137,7 +131,7 @@ public static function hash(StreamInterface $stream, $algo, $rawOutput = false) hash_update($ctx, $stream->read(1048576)); } - $out = hash_final($ctx, (bool) $rawOutput); + $out = hash_final($ctx, $rawOutput); $stream->seek($pos); return $out; @@ -160,10 +154,8 @@ public static function hash(StreamInterface $stream, $algo, $rawOutput = false) * * @param RequestInterface $request Request to clone and modify. * @param array $changes Changes to apply. - * - * @return RequestInterface */ - public static function modifyRequest(RequestInterface $request, array $changes) + public static function modifyRequest(RequestInterface $request, array $changes): RequestInterface { if (!$changes) { return $request; @@ -204,13 +196,11 @@ public static function modifyRequest(RequestInterface $request, array $changes) if ($request instanceof ServerRequestInterface) { $new = (new ServerRequest( - isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $changes['method'] ?? $request->getMethod(), $uri, $headers, - isset($changes['body']) ? $changes['body'] : $request->getBody(), - isset($changes['version']) - ? $changes['version'] - : $request->getProtocolVersion(), + $changes['body'] ?? $request->getBody(), + $changes['version'] ?? $request->getProtocolVersion(), $request->getServerParams() )) ->withParsedBody($request->getParsedBody()) @@ -226,13 +216,11 @@ public static function modifyRequest(RequestInterface $request, array $changes) } return new Request( - isset($changes['method']) ? $changes['method'] : $request->getMethod(), + $changes['method'] ?? $request->getMethod(), $uri, $headers, - isset($changes['body']) ? $changes['body'] : $request->getBody(), - isset($changes['version']) - ? $changes['version'] - : $request->getProtocolVersion() + $changes['body'] ?? $request->getBody(), + $changes['version'] ?? $request->getProtocolVersion() ); } @@ -241,17 +229,14 @@ public static function modifyRequest(RequestInterface $request, array $changes) * * @param StreamInterface $stream Stream to read from * @param int|null $maxLength Maximum buffer length - * - * @return string */ - public static function readLine(StreamInterface $stream, $maxLength = null) + public static function readLine(StreamInterface $stream, ?int $maxLength = null): string { $buffer = ''; $size = 0; while (!$stream->eof()) { - // Using a loose equality here to match on '' and false. - if (null == ($byte = $stream->read(1))) { + if ('' === ($byte = $stream->read(1))) { return $buffer; } $buffer .= $byte; @@ -294,18 +279,16 @@ public static function readLine(StreamInterface $stream, $maxLength = null) * buffered and used in subsequent reads. * * @param resource|string|int|float|bool|StreamInterface|callable|\Iterator|null $resource Entity body data - * @param array $options Additional options - * - * @return StreamInterface + * @param array{size?: int, metadata?: array} $options Additional options * * @throws \InvalidArgumentException if the $resource arg is not valid. */ - public static function streamFor($resource = '', array $options = []) + public static function streamFor($resource = '', array $options = []): StreamInterface { if (is_scalar($resource)) { $stream = self::tryFopen('php://temp', 'r+'); if ($resource !== '') { - fwrite($stream, $resource); + fwrite($stream, (string) $resource); fseek($stream, 0); } return new Stream($stream, $options); @@ -317,15 +300,17 @@ public static function streamFor($resource = '', array $options = []) * The 'php://input' is a special stream with quirks and inconsistencies. * We avoid using that stream by reading it into php://temp */ - $metaData = \stream_get_meta_data($resource); - if (isset($metaData['uri']) && $metaData['uri'] === 'php://input') { + + /** @var resource $resource */ + if ((\stream_get_meta_data($resource)['uri'] ?? '') === 'php://input') { $stream = self::tryFopen('php://temp', 'w+'); - fwrite($stream, stream_get_contents($resource)); + stream_copy_to_stream($resource, $stream); fseek($stream, 0); $resource = $stream; } return new Stream($resource, $options); case 'object': + /** @var object $resource */ if ($resource instanceof StreamInterface) { return $resource; } elseif ($resource instanceof \Iterator) { @@ -338,7 +323,7 @@ public static function streamFor($resource = '', array $options = []) return $result; }, $options); } elseif (method_exists($resource, '__toString')) { - return Utils::streamFor((string) $resource, $options); + return self::streamFor((string) $resource, $options); } break; case 'NULL': @@ -365,21 +350,22 @@ public static function streamFor($resource = '', array $options = []) * * @throws \RuntimeException if the file cannot be opened */ - public static function tryFopen($filename, $mode) + public static function tryFopen(string $filename, string $mode) { $ex = null; - set_error_handler(function () use ($filename, $mode, &$ex) { + set_error_handler(static function (int $errno, string $errstr) use ($filename, $mode, &$ex): bool { $ex = new \RuntimeException(sprintf( 'Unable to open "%s" using mode "%s": %s', $filename, $mode, - func_get_args()[1] + $errstr )); return true; }); try { + /** @var resource $handle */ $handle = fopen($filename, $mode); } catch (\Throwable $e) { $ex = new \RuntimeException(sprintf( @@ -409,11 +395,9 @@ public static function tryFopen($filename, $mode) * * @param string|UriInterface $uri * - * @return UriInterface - * * @throws \InvalidArgumentException */ - public static function uriFor($uri) + public static function uriFor($uri): UriInterface { if ($uri instanceof UriInterface) { return $uri; diff --git a/src/functions.php b/src/functions.php deleted file mode 100644 index b0901fad..00000000 --- a/src/functions.php +++ /dev/null @@ -1,422 +0,0 @@ - '1', 'foo[b]' => '2'])`. - * - * @param string $str Query string to parse - * @param int|bool $urlEncoding How the query string is encoded - * - * @return array - * - * @deprecated parse_query will be removed in guzzlehttp/psr7:2.0. Use Query::parse instead. - */ -function parse_query($str, $urlEncoding = true) -{ - return Query::parse($str, $urlEncoding); -} - -/** - * Build a query string from an array of key value pairs. - * - * This function can use the return value of `parse_query()` to build a query - * string. This function does not modify the provided keys when an array is - * encountered (like `http_build_query()` would). - * - * @param array $params Query string parameters. - * @param int|false $encoding Set to false to not encode, PHP_QUERY_RFC3986 - * to encode using RFC3986, or PHP_QUERY_RFC1738 - * to encode using RFC1738. - * - * @return string - * - * @deprecated build_query will be removed in guzzlehttp/psr7:2.0. Use Query::build instead. - */ -function build_query(array $params, $encoding = PHP_QUERY_RFC3986) -{ - return Query::build($params, $encoding); -} - -/** - * Determines the mimetype of a file by looking at its extension. - * - * @param string $filename - * - * @return string|null - * - * @deprecated mimetype_from_filename will be removed in guzzlehttp/psr7:2.0. Use MimeType::fromFilename instead. - */ -function mimetype_from_filename($filename) -{ - return MimeType::fromFilename($filename); -} - -/** - * Maps a file extensions to a mimetype. - * - * @param $extension string The file extension. - * - * @return string|null - * - * @link http://svn.apache.org/repos/asf/httpd/httpd/branches/1.3.x/conf/mime.types - * @deprecated mimetype_from_extension will be removed in guzzlehttp/psr7:2.0. Use MimeType::fromExtension instead. - */ -function mimetype_from_extension($extension) -{ - return MimeType::fromExtension($extension); -} - -/** - * Parses an HTTP message into an associative array. - * - * The array contains the "start-line" key containing the start line of - * the message, "headers" key containing an associative array of header - * array values, and a "body" key containing the body of the message. - * - * @param string $message HTTP request or response to parse. - * - * @return array - * - * @internal - * - * @deprecated _parse_message will be removed in guzzlehttp/psr7:2.0. Use Message::parseMessage instead. - */ -function _parse_message($message) -{ - return Message::parseMessage($message); -} - -/** - * Constructs a URI for an HTTP request message. - * - * @param string $path Path from the start-line - * @param array $headers Array of headers (each value an array). - * - * @return string - * - * @internal - * - * @deprecated _parse_request_uri will be removed in guzzlehttp/psr7:2.0. Use Message::parseRequestUri instead. - */ -function _parse_request_uri($path, array $headers) -{ - return Message::parseRequestUri($path, $headers); -} - -/** - * Get a short summary of the message body. - * - * Will return `null` if the response is not printable. - * - * @param MessageInterface $message The message to get the body summary - * @param int $truncateAt The maximum allowed size of the summary - * - * @return string|null - * - * @deprecated get_message_body_summary will be removed in guzzlehttp/psr7:2.0. Use Message::bodySummary instead. - */ -function get_message_body_summary(MessageInterface $message, $truncateAt = 120) -{ - return Message::bodySummary($message, $truncateAt); -} - -/** - * Remove the items given by the keys, case insensitively from the data. - * - * @param iterable $keys - * - * @return array - * - * @internal - * - * @deprecated _caseless_remove will be removed in guzzlehttp/psr7:2.0. Use Utils::caselessRemove instead. - */ -function _caseless_remove($keys, array $data) -{ - return Utils::caselessRemove($keys, $data); -} diff --git a/src/functions_include.php b/src/functions_include.php deleted file mode 100644 index 96a4a83a..00000000 --- a/src/functions_include.php +++ /dev/null @@ -1,6 +0,0 @@ -getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isReadable']) - ->getMockForAbstractClass(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(false)); - - $this->expectExceptionGuzzle('InvalidArgumentException', 'Each stream must be readable'); - + ->willReturn(false); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Each stream must be readable'); $a->addStream($s); } - public function testValidatesSeekType() + public function testValidatesSeekType(): void { $a = new AppendStream(); - - $this->expectExceptionGuzzle('RuntimeException', 'The AppendStream can only seek with SEEK_SET'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The AppendStream can only seek with SEEK_SET'); $a->seek(100, SEEK_CUR); } - public function testTriesToRewindOnSeek() + public function testTriesToRewindOnSeek(): void { $a = new AppendStream(); - $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isReadable', 'rewind', 'isSeekable']) - ->getMockForAbstractClass(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(true)); + ->willReturn(true); $s->expects(self::once()) ->method('isSeekable') - ->will(self::returnValue(true)); + ->willReturn(true); $s->expects(self::once()) ->method('rewind') - ->will(self::throwException(new \RuntimeException())); + ->willThrowException(new \RuntimeException()); $a->addStream($s); - - $this->expectExceptionGuzzle('RuntimeException', 'Unable to seek stream 0 of the AppendStream'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to seek stream 0 of the AppendStream'); $a->seek(10); } - public function testSeeksToPositionByReading() + public function testSeeksToPositionByReading(): void { $a = new AppendStream([ Psr7\Utils::streamFor('foo'), @@ -70,7 +67,7 @@ public function testSeeksToPositionByReading() self::assertSame('baz', $a->read(3)); } - public function testDetachWithoutStreams() + public function testDetachWithoutStreams(): void { $s = new AppendStream(); $s->detach(); @@ -83,7 +80,7 @@ public function testDetachWithoutStreams() self::assertFalse($s->isWritable()); } - public function testDetachesEachStream() + public function testDetachesEachStream(): void { $handle = fopen('php://temp', 'r'); @@ -101,11 +98,11 @@ public function testDetachesEachStream() self::assertFalse($a->isWritable()); self::assertNull($s1->detach()); - $this->assertInternalTypeGuzzle('resource', $handle, 'resource is not closed when detaching'); + self::assertIsResource($handle, 'resource is not closed when detaching'); fclose($handle); } - public function testClosesEachStream() + public function testClosesEachStream(): void { $handle = fopen('php://temp', 'r'); @@ -125,25 +122,24 @@ public function testClosesEachStream() self::assertFalse(is_resource($handle)); } - public function testIsNotWritable() + public function testIsNotWritable(): void { $a = new AppendStream([Psr7\Utils::streamFor('foo')]); self::assertFalse($a->isWritable()); self::assertTrue($a->isSeekable()); self::assertTrue($a->isReadable()); - - $this->expectExceptionGuzzle('RuntimeException', 'Cannot write to an AppendStream'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot write to an AppendStream'); $a->write('foo'); } - public function testDoesNotNeedStreams() + public function testDoesNotNeedStreams(): void { $a = new AppendStream(); self::assertSame('', (string) $a); } - public function testCanReadFromMultipleStreams() + public function testCanReadFromMultipleStreams(): void { $a = new AppendStream([ Psr7\Utils::streamFor('foo'), @@ -161,7 +157,7 @@ public function testCanReadFromMultipleStreams() self::assertSame('foobarbaz', (string) $a); } - public function testCanDetermineSizeFromMultipleStreams() + public function testCanDetermineSizeFromMultipleStreams(): void { $a = new AppendStream([ Psr7\Utils::streamFor('foo'), @@ -169,42 +165,53 @@ public function testCanDetermineSizeFromMultipleStreams() ]); self::assertSame(6, $a->getSize()); - $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isSeekable', 'isReadable']) - ->getMockForAbstractClass(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::once()) ->method('isSeekable') - ->will(self::returnValue(null)); + ->willReturn(false); $s->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(true)); + ->willReturn(true); $a->addStream($s); self::assertNull($a->getSize()); } - public function testCatchesExceptionsWhenCastingToString() + /** + * @requires PHP < 7.4 + */ + public function testCatchesExceptionsWhenCastingToString(): void { - $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isSeekable', 'read', 'isReadable', 'eof']) - ->getMockForAbstractClass(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::once()) ->method('isSeekable') - ->will(self::returnValue(true)); + ->willReturn(true); $s->expects(self::once()) ->method('read') - ->will(self::throwException(new \RuntimeException('foo'))); + ->willThrowException(new \RuntimeException('foo')); $s->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(true)); + ->willReturn(true); $s->expects(self::any()) ->method('eof') - ->will(self::returnValue(false)); + ->willReturn(false); $a = new AppendStream([$s]); self::assertFalse($a->eof()); - self::assertSame('', (string) $a); + + $errors = []; + set_error_handler(static function (int $errorNumber, string $errorMessage) use (&$errors): bool { + $errors[] = ['number' => $errorNumber, 'message' => $errorMessage]; + return true; + }); + (string) $a; + + restore_error_handler(); + + self::assertCount(1, $errors); + self::assertSame(E_USER_ERROR, $errors[0]['number']); + self::assertStringStartsWith('GuzzleHttp\Psr7\AppendStream::__toString exception:', $errors[0]['message']); } - public function testReturnsEmptyMetadata() + public function testReturnsEmptyMetadata(): void { $s = new AppendStream(); self::assertSame([], $s->getMetadata()); diff --git a/tests/BaseTest.php b/tests/BaseTest.php deleted file mode 100644 index f0ad99c5..00000000 --- a/tests/BaseTest.php +++ /dev/null @@ -1,139 +0,0 @@ -setExpectedException($exception, $message); - } else { - $this->expectException($exception); - if (null !== $message) { - $this->expectExceptionMessage($message); - } - } - } - - public function expectWarningGuzzle() - { - if (method_exists($this, 'expectWarning')) { - $this->expectWarning(); - } elseif (class_exists('PHPUnit\Framework\Error\Warning')) { - $this->expectExceptionGuzzle('PHPUnit\Framework\Error\Warning'); - } else { - $this->expectExceptionGuzzle('PHPUnit_Framework_Error_Warning'); - } - } - - /** - * @param string $type - * @param mixed $input - */ - public function assertInternalTypeGuzzle($type, $input) - { - switch ($type) { - case 'array': - if (method_exists($this, 'assertIsArray')) { - self::assertIsArray($input); - } else { - self::assertInternalType('array', $input); - } - break; - case 'bool': - case 'boolean': - if (method_exists($this, 'assertIsBool')) { - self::assertIsBool($input); - } else { - self::assertInternalType('bool', $input); - } - break; - case 'double': - case 'float': - case 'real': - if (method_exists($this, 'assertIsFloat')) { - self::assertIsFloat($input); - } else { - self::assertInternalType('float', $input); - } - break; - case 'int': - case 'integer': - if (method_exists($this, 'assertIsInt')) { - self::assertIsInt($input); - } else { - self::assertInternalType('int', $input); - } - break; - case 'numeric': - if (method_exists($this, 'assertIsNumeric')) { - self::assertIsNumeric($input); - } else { - self::assertInternalType('numeric', $input); - } - break; - case 'object': - if (method_exists($this, 'assertIsObject')) { - self::assertIsObject($input); - } else { - self::assertInternalType('object', $input); - } - break; - case 'resource': - if (method_exists($this, 'assertIsResource')) { - self::assertIsResource($input); - } else { - self::assertInternalType('resource', $input); - } - break; - case 'string': - if (method_exists($this, 'assertIsString')) { - self::assertIsString($input); - } else { - self::assertInternalType('string', $input); - } - break; - case 'scalar': - if (method_exists($this, 'assertIsScalar')) { - self::assertIsScalar($input); - } else { - self::assertInternalType('scalar', $input); - } - break; - case 'callable': - if (method_exists($this, 'assertIsCallable')) { - self::assertIsCallable($input); - } else { - self::assertInternalType('callable', $input); - } - break; - case 'iterable': - if (method_exists($this, 'assertIsIterable')) { - self::assertIsIterable($input); - } else { - self::assertInternalType('iterable', $input); - } - break; - } - } - - /** - * @param string $needle - * @param string $haystack - */ - public function assertStringContainsStringGuzzle($needle, $haystack) - { - if (method_exists($this, 'assertStringContainsString')) { - self::assertStringContainsString($needle, $haystack); - } else { - self::assertContains($needle, $haystack); - } - } -} diff --git a/tests/BufferStreamTest.php b/tests/BufferStreamTest.php index 804a4260..987f2947 100644 --- a/tests/BufferStreamTest.php +++ b/tests/BufferStreamTest.php @@ -1,23 +1,26 @@ isReadable()); self::assertTrue($b->isWritable()); self::assertFalse($b->isSeekable()); - self::assertSame(null, $b->getMetadata('foo')); + self::assertNull($b->getMetadata('foo')); self::assertSame(10, $b->getMetadata('hwm')); self::assertSame([], $b->getMetadata()); } - public function testRemovesReadDataFromBuffer() + public function testRemovesReadDataFromBuffer(): void { $b = new BufferStream(); self::assertSame(3, $b->write('foo')); @@ -28,7 +31,7 @@ public function testRemovesReadDataFromBuffer() self::assertSame('', $b->read(10)); } - public function testCanCastToStringOrGetContents() + public function testCanCastToStringOrGetContents(): void { $b = new BufferStream(); $b->write('foo'); @@ -36,13 +39,12 @@ public function testCanCastToStringOrGetContents() self::assertSame('foo', $b->read(3)); $b->write('bar'); self::assertSame('bazbar', (string) $b); - - $this->expectExceptionGuzzle('RuntimeException', 'Cannot determine the position of a BufferStream'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot determine the position of a BufferStream'); $b->tell(); } - public function testDetachClearsBuffer() + public function testDetachClearsBuffer(): void { $b = new BufferStream(); $b->write('foo'); @@ -52,11 +54,11 @@ public function testDetachClearsBuffer() self::assertSame('abc', $b->read(10)); } - public function testExceedingHighwaterMarkReturnsFalseButStillBuffers() + public function testExceedingHighwaterMarkReturnsFalseButStillBuffers(): void { $b = new BufferStream(5); self::assertSame(3, $b->write('hi ')); - self::assertFalse($b->write('hello')); + self::assertSame(0, $b->write('hello')); self::assertSame('hi hello', (string) $b); self::assertSame(4, $b->write('test')); } diff --git a/tests/CachingStreamTest.php b/tests/CachingStreamTest.php index c760f5db..919cb1ca 100644 --- a/tests/CachingStreamTest.php +++ b/tests/CachingStreamTest.php @@ -1,47 +1,53 @@ decorated = Psr7\Utils::streamFor('testing'); $this->body = new CachingStream($this->decorated); } - /** - * @after - */ - public function tearDownTest() + protected function tearDown(): void { $this->decorated->close(); $this->body->close(); } - public function testUsesRemoteSizeIfPossible() + public function testUsesRemoteSizeIfAvailable(): void { $body = Psr7\Utils::streamFor('test'); $caching = new CachingStream($body); self::assertSame(4, $caching->getSize()); } - public function testReadsUntilCachedToByte() + public function testUsesRemoteSizeIfNotAvailable(): void + { + $body = new Psr7\PumpStream(function () { + return 'a'; + }); + $caching = new CachingStream($body); + self::assertNull($caching->getSize()); + } + + public function testReadsUntilCachedToByte(): void { $this->body->seek(5); self::assertSame('n', $this->body->read(1)); @@ -49,7 +55,7 @@ public function testReadsUntilCachedToByte() self::assertSame('t', $this->body->read(1)); } - public function testCanSeekNearEndWithSeekEnd() + public function testCanSeekNearEndWithSeekEnd(): void { $baseStream = Psr7\Utils::streamFor(implode('', range('a', 'z'))); $cached = new CachingStream($baseStream); @@ -59,7 +65,7 @@ public function testCanSeekNearEndWithSeekEnd() self::assertSame(26, $cached->getSize()); } - public function testCanSeekToEndWithSeekEnd() + public function testCanSeekToEndWithSeekEnd(): void { $baseStream = Psr7\Utils::streamFor(implode('', range('a', 'z'))); $cached = new CachingStream($baseStream); @@ -69,7 +75,7 @@ public function testCanSeekToEndWithSeekEnd() self::assertSame(26, $cached->getSize()); } - public function testCanUseSeekEndWithUnknownSize() + public function testCanUseSeekEndWithUnknownSize(): void { $baseStream = Psr7\Utils::streamFor('testing'); $decorated = Psr7\FnStream::decorate($baseStream, [ @@ -82,21 +88,16 @@ public function testCanUseSeekEndWithUnknownSize() self::assertSame('g', $cached->read(1)); } - public function testRewindUsesSeek() + public function testRewind(): void { $a = Psr7\Utils::streamFor('foo'); - $d = $this->getMockBuilder('GuzzleHttp\Psr7\CachingStream') - ->setMethods(['seek']) - ->setConstructorArgs([$a]) - ->getMock(); - $d->expects(self::once()) - ->method('seek') - ->with(0) - ->will(self::returnValue(true)); - $d->seek(0); + $d = new CachingStream($a); + self::assertSame('foo', $d->read(3)); + $d->rewind(); + self::assertSame('foo', $d->read(3)); } - public function testCanSeekToReadBytes() + public function testCanSeekToReadBytes(): void { self::assertSame('te', $this->body->read(2)); $this->body->seek(0); @@ -109,20 +110,20 @@ public function testCanSeekToReadBytes() self::assertSame('ing', $this->body->read(3)); } - public function testCanSeekToReadBytesWithPartialBodyReturned() + public function testCanSeekToReadBytesWithPartialBodyReturned(): void { $stream = fopen('php://temp', 'r+'); fwrite($stream, 'testing'); fseek($stream, 0); - $this->decorated = $this->getMockBuilder('\GuzzleHttp\Psr7\Stream') + $this->decorated = $this->getMockBuilder(Stream::class) ->setConstructorArgs([$stream]) - ->setMethods(['read']) + ->onlyMethods(['read']) ->getMock(); $this->decorated->expects(self::exactly(2)) ->method('read') - ->willReturnCallback(function ($length) use ($stream) { + ->willReturnCallback(function (int $length) use ($stream) { return fread($stream, 2); }); @@ -136,7 +137,7 @@ public function testCanSeekToReadBytesWithPartialBodyReturned() self::assertSame('test', $this->body->read(4)); } - public function testWritesToBufferStream() + public function testWritesToBufferStream(): void { $this->body->read(2); $this->body->write('hi'); @@ -144,11 +145,11 @@ public function testWritesToBufferStream() self::assertSame('tehiing', (string) $this->body); } - public function testSkipsOverwrittenBytes() + public function testSkipsOverwrittenBytes(): void { $decorated = Psr7\Utils::streamFor( implode("\n", array_map(function ($n) { - return str_pad($n, 4, '0', STR_PAD_LEFT); + return str_pad((string)$n, 4, '0', STR_PAD_LEFT); }, range(0, 25))) ); @@ -158,34 +159,30 @@ public function testSkipsOverwrittenBytes() self::assertSame("0001\n", Psr7\Utils::readLine($body)); // Write over part of the body yet to be read, so skip some bytes self::assertSame(5, $body->write("TEST\n")); - self::assertSame(5, Helpers::readObjectAttribute($body, 'skipReadBytes')); // Read, which skips bytes, then reads self::assertSame("0003\n", Psr7\Utils::readLine($body)); - self::assertSame(0, Helpers::readObjectAttribute($body, 'skipReadBytes')); self::assertSame("0004\n", Psr7\Utils::readLine($body)); self::assertSame("0005\n", Psr7\Utils::readLine($body)); // Overwrite part of the cached body (so don't skip any bytes) $body->seek(5); self::assertSame(5, $body->write("ABCD\n")); - self::assertSame(0, Helpers::readObjectAttribute($body, 'skipReadBytes')); self::assertSame("TEST\n", Psr7\Utils::readLine($body)); self::assertSame("0003\n", Psr7\Utils::readLine($body)); self::assertSame("0004\n", Psr7\Utils::readLine($body)); self::assertSame("0005\n", Psr7\Utils::readLine($body)); self::assertSame("0006\n", Psr7\Utils::readLine($body)); self::assertSame(5, $body->write("1234\n")); - self::assertSame(5, Helpers::readObjectAttribute($body, 'skipReadBytes')); // Seek to 0 and ensure the overwritten bit is replaced $body->seek(0); self::assertSame("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", $body->read(50)); // Ensure that casting it to a string does not include the bit that was overwritten - $this->assertStringContainsStringGuzzle("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", (string) $body); + self::assertStringContainsString("0000\nABCD\nTEST\n0003\n0004\n0005\n0006\n1234\n0008\n0009\n", (string) $body); } - public function testClosesBothStreams() + public function testClosesBothStreams(): void { $s = fopen('php://temp', 'r'); $a = Psr7\Utils::streamFor($s); @@ -194,10 +191,10 @@ public function testClosesBothStreams() self::assertFalse(is_resource($s)); } - public function testEnsuresValidWhence() + public function testEnsuresValidWhence(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid whence'); $this->body->seek(10, -123456); } } diff --git a/tests/DroppingStreamTest.php b/tests/DroppingStreamTest.php index 15dd3015..d70bc9cb 100644 --- a/tests/DroppingStreamTest.php +++ b/tests/DroppingStreamTest.php @@ -1,13 +1,16 @@ expectExceptionGuzzle('BadMethodCallException', 'seek() is not implemented in the FnStream'); - + $this->expectException(\BadMethodCallException::class); + $this->expectExceptionMessage('seek() is not implemented in the FnStream'); (new FnStream([]))->seek(1); } - public function testProxiesToFunction() + public function testProxiesToFunction(): void { $s = new FnStream([ 'read' => function ($len) { @@ -29,11 +32,11 @@ public function testProxiesToFunction() self::assertSame('foo', $s->read(3)); } - public function testCanCloseOnDestruct() + public function testCanCloseOnDestruct(): void { $called = false; $s = new FnStream([ - 'close' => function () use (&$called) { + 'close' => function () use (&$called): void { $called = true; } ]); @@ -41,14 +44,14 @@ public function testCanCloseOnDestruct() self::assertTrue($called); } - public function testDoesNotRequireClose() + public function testDoesNotRequireClose(): void { $s = new FnStream([]); unset($s); self::assertTrue(true); // strict mode requires an assertion } - public function testDecoratesStream() + public function testDecoratesStream(): void { $a = Psr7\Utils::streamFor('foo'); $b = FnStream::decorate($a, []); @@ -70,11 +73,11 @@ public function testDecoratesStream() $b->seek(0, SEEK_END); $b->write('bar'); self::assertSame('foobar', (string) $b); - $this->assertInternalTypeGuzzle('resource', $b->detach()); + self::assertIsResource($b->detach()); $b->close(); } - public function testDecoratesWithCustomizations() + public function testDecoratesWithCustomizations(): void { $called = false; $a = Psr7\Utils::streamFor('foo'); @@ -88,11 +91,36 @@ public function testDecoratesWithCustomizations() self::assertTrue($called); } - public function testDoNotAllowUnserialization() + public function testDoNotAllowUnserialization(): void { $a = new FnStream([]); $b = serialize($a); - $this->expectExceptionGuzzle('\LogicException', 'FnStream should never be unserialized'); + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('FnStream should never be unserialized'); unserialize($b); } + + /** + * @requires PHP < 7.4 + */ + public function testThatConvertingStreamToStringWillTriggerErrorAndWillReturnEmptyString(): void + { + $a = new FnStream([ + '__toString' => function (): void { + throw new \Exception(); + }, + ]); + + $errors = []; + set_error_handler(function (int $errorNumber, string $errorMessage) use (&$errors): void { + $errors[] = ['number' => $errorNumber, 'message' => $errorMessage]; + }); + (string) $a; + + restore_error_handler(); + + self::assertCount(1, $errors); + self::assertSame(E_USER_ERROR, $errors[0]['number']); + self::assertStringStartsWith('GuzzleHttp\Psr7\FnStream::__toString exception:', $errors[0]['message']); + } } diff --git a/tests/HasToString.php b/tests/HasToString.php index 7a9d7374..bcc503bd 100644 --- a/tests/HasToString.php +++ b/tests/HasToString.php @@ -1,10 +1,12 @@ ; rel=\"first\",\n; rel=\"next\",\n; rel=\"prev\",\n; rel=\"last\",", + ['; rel="first"', '; rel="next"', '; rel="prev"', '; rel="last"'], + ], + ]; + } + + /** + * @dataProvider normalizeProvider + */ + public function testNormalize($header, $result): void { - $header = ['a, b', 'c', 'd, e']; - self::assertSame(['a', 'b', 'c', 'd', 'e'], Psr7\Header::normalize($header)); + self::assertSame($result, Psr7\Header::normalize([$header])); + self::assertSame($result, Psr7\Header::normalize($header)); } } diff --git a/tests/Helpers.php b/tests/Helpers.php deleted file mode 100644 index 6ff31a69..00000000 --- a/tests/Helpers.php +++ /dev/null @@ -1,35 +0,0 @@ -getProperty($attributeName); - - if (!$attribute || $attribute->isPublic()) { - return $object->$attributeName; - } - - $attribute->setAccessible(true); - - return $attribute->getValue($object); - } catch (\ReflectionException $e) { - // do nothing - } - } while ($reflector = $reflector->getParentClass()); - - throw new \Exception( - sprintf('Attribute "%s" not found in object.', $attributeName) - ); - } -} diff --git a/tests/InflateStreamTest.php b/tests/InflateStreamTest.php index a97b2874..2726d983 100644 --- a/tests/InflateStreamTest.php +++ b/tests/InflateStreamTest.php @@ -1,14 +1,20 @@ getGzipStringWithFilename('test'); $a = Psr7\Utils::streamFor($content); @@ -24,14 +30,54 @@ public function testInflatesStreamsWithFilename() self::assertSame('test', (string) $b); } - public function testInflatesStreamsPreserveSeekable() + public function testInflatesRfc1950Streams(): void { - $content = $this->getGzipStringWithFilename('test'); + $content = gzcompress('test'); + $a = Psr7\Utils::streamFor($content); + $b = new InflateStream($a); + self::assertSame('test', (string) $b); + } + + public function testInflatesRfc1952StreamsWithExtraFlags(): void + { + $content = gzdeflate('test'); // RFC 1951. Raw deflate. No header. + + // +---+---+---+---+---+---+---+---+---+---+ + // |ID1|ID2|CM |FLG| MTIME |XFL|OS | (more-->) + // +---+---+---+---+---+---+---+---+---+---+ + $header = "\x1f\x8B\x08"; + // set flags FHCRC, FEXTRA, FNAME and FCOMMENT + $header .= chr(0b00011110); + $header .= "\x00\x00\x00\x00"; // MTIME + $header .= "\x02\x03"; // XFL, OS + // 4 byte extra data + $header .= "\x04\x00" /* XLEN */ . "\x41\x70\x00\x00" /*EXTRA*/; + // file name (2 bytes + terminator) + $header .= "\x41\x70\x00"; + // file comment (3 bytes + terminator) + $header .= "\x41\x42\x43\x00"; + + // crc16 + $header .= pack('v', crc32($header)); + + $a = Psr7\Utils::streamFor($header . $content); + $b = new InflateStream($a); + self::assertSame('test', (string) $b); + } + + public function testInflatesStreamsPreserveSeekable(): void + { + $content = gzencode('test'); $seekable = Psr7\Utils::streamFor($content); - $nonSeekable = new NoSeekStream(Psr7\Utils::streamFor($content)); - self::assertTrue((new InflateStream($seekable))->isSeekable()); - self::assertFalse((new InflateStream($nonSeekable))->isSeekable()); + $seekableInflate = new InflateStream($seekable); + self::assertTrue($seekableInflate->isSeekable()); + self::assertSame('test', (string) $seekableInflate); + + $nonSeekable = new NoSeekStream(Psr7\Utils::streamFor($content)); + $nonSeekableInflate = new InflateStream($nonSeekable); + self::assertFalse($nonSeekableInflate->isSeekable()); + self::assertSame('test', (string) $nonSeekableInflate); } private function getGzipStringWithFilename($original_string) diff --git a/tests/Integration/ServerRequestFromGlobalsTest.php b/tests/Integration/ServerRequestFromGlobalsTest.php index ff95b73e..e36a3fc5 100644 --- a/tests/Integration/ServerRequestFromGlobalsTest.php +++ b/tests/Integration/ServerRequestFromGlobalsTest.php @@ -1,15 +1,14 @@ getServerUri()) { self::markTestSkipped(); @@ -17,7 +16,7 @@ protected function setUpTest() parent::setUp(); } - public function testBodyExists() + public function testBodyExists(): void { $curl = curl_init(); @@ -40,6 +39,6 @@ public function testBodyExists() private function getServerUri() { - return isset($_SERVER['TEST_SERVER']) ? $_SERVER['TEST_SERVER'] : false; + return $_SERVER['TEST_SERVER'] ?? false; } } diff --git a/tests/Integration/server.php b/tests/Integration/server.php index 9539e626..1dc4523c 100644 --- a/tests/Integration/server.php +++ b/tests/Integration/server.php @@ -1,5 +1,7 @@ fname = tempnam(sys_get_temp_dir(), 'tfile'); @@ -20,27 +20,24 @@ public function setUpTest() } } - /** - * @after - */ - public function tearDownTest() + protected function tearDown(): void { if (file_exists($this->fname)) { unlink($this->fname); } } - public function testOpensLazily() + public function testOpensLazily(): void { $l = new LazyOpenStream($this->fname, 'w+'); $l->write('foo'); - $this->assertInternalTypeGuzzle('array', $l->getMetadata()); + self::assertIsArray($l->getMetadata()); self::assertFileExists($this->fname); self::assertSame('foo', file_get_contents($this->fname)); self::assertSame('foo', (string) $l); } - public function testProxiesToFile() + public function testProxiesToFile(): void { file_put_contents($this->fname, 'foo'); $l = new LazyOpenStream($this->fname, 'r'); @@ -54,16 +51,16 @@ public function testProxiesToFile() self::assertSame('oo', $l->getContents()); self::assertSame('foo', (string) $l); self::assertSame(3, $l->getSize()); - $this->assertInternalTypeGuzzle('array', $l->getMetadata()); + self::assertIsArray($l->getMetadata()); $l->close(); } - public function testDetachesUnderlyingStream() + public function testDetachesUnderlyingStream(): void { file_put_contents($this->fname, 'foo'); $l = new LazyOpenStream($this->fname, 'r'); $r = $l->detach(); - $this->assertInternalTypeGuzzle('resource', $r); + self::assertIsResource($r); fseek($r, 0); self::assertSame('foo', stream_get_contents($r)); fclose($r); diff --git a/tests/LimitStreamTest.php b/tests/LimitStreamTest.php index 62dc0b26..b5cb22b6 100644 --- a/tests/LimitStreamTest.php +++ b/tests/LimitStreamTest.php @@ -1,5 +1,7 @@ decorated = Psr7\Utils::streamFor(fopen(__FILE__, 'r')); $this->body = new LimitStream($this->decorated, 10, 3); } - public function testReturnsSubset() + public function testReturnsSubset(): void { $body = new LimitStream(Psr7\Utils::streamFor('foo'), -1, 1); self::assertSame('oo', (string) $body); @@ -40,34 +40,34 @@ public function testReturnsSubset() self::assertTrue($body->eof()); } - public function testReturnsSubsetWhenCastToString() + public function testReturnsSubsetWhenCastToString(): void { $body = Psr7\Utils::streamFor('foo_baz_bar'); $limited = new LimitStream($body, 3, 4); self::assertSame('baz', (string) $limited); } - public function testReturnsSubsetOfEmptyBodyWhenCastToString() + public function testReturnsSubsetOfEmptyBodyWhenCastToString(): void { $body = Psr7\Utils::streamFor('01234567891234'); $limited = new LimitStream($body, 0, 10); self::assertSame('', (string) $limited); } - public function testReturnsSpecificSubsetOBodyWhenCastToString() + public function testReturnsSpecificSubsetOBodyWhenCastToString(): void { $body = Psr7\Utils::streamFor('0123456789abcdef'); $limited = new LimitStream($body, 3, 10); self::assertSame('abc', (string) $limited); } - public function testSeeksWhenConstructed() + public function testSeeksWhenConstructed(): void { self::assertSame(0, $this->body->tell()); self::assertSame(3, $this->decorated->tell()); } - public function testAllowsBoundedSeek() + public function testAllowsBoundedSeek(): void { $this->body->seek(100); self::assertSame(10, $this->body->tell()); @@ -93,7 +93,7 @@ public function testAllowsBoundedSeek() } } - public function testReadsOnlySubsetOfData() + public function testReadsOnlySubsetOfData(): void { $data = $this->body->read(100); self::assertSame(10, strlen($data)); @@ -105,19 +105,18 @@ public function testReadsOnlySubsetOfData() self::assertNotSame($data, $newData); } - public function testThrowsWhenCurrentGreaterThanOffsetSeek() + public function testThrowsWhenCurrentGreaterThanOffsetSeek(): void { $a = Psr7\Utils::streamFor('foo_bar'); $b = new NoSeekStream($a); $c = new LimitStream($b); $a->getContents(); - - $this->expectExceptionGuzzle('RuntimeException', 'Could not seek to stream offset 2'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Could not seek to stream offset 2'); $c->setOffset(2); } - public function testCanGetContentsWithoutSeeking() + public function testCanGetContentsWithoutSeeking(): void { $a = Psr7\Utils::streamFor('foo_bar'); $b = new NoSeekStream($a); @@ -125,25 +124,25 @@ public function testCanGetContentsWithoutSeeking() self::assertSame('foo_bar', $c->getContents()); } - public function testClaimsConsumedWhenReadLimitIsReached() + public function testClaimsConsumedWhenReadLimitIsReached(): void { self::assertFalse($this->body->eof()); $this->body->read(1000); self::assertTrue($this->body->eof()); } - public function testContentLengthIsBounded() + public function testContentLengthIsBounded(): void { self::assertSame(10, $this->body->getSize()); } - public function testGetContentsIsBasedOnSubset() + public function testGetContentsIsBasedOnSubset(): void { $body = new LimitStream(Psr7\Utils::streamFor('foobazbar'), 3, 3); self::assertSame('baz', $body->getContents()); } - public function testReturnsNullIfSizeCannotBeDetermined() + public function testReturnsNullIfSizeCannotBeDetermined(): void { $a = new FnStream([ 'getSize' => function () { @@ -157,7 +156,7 @@ public function testReturnsNullIfSizeCannotBeDetermined() self::assertNull($b->getSize()); } - public function testLengthLessOffsetWhenNoLimitSize() + public function testLengthLessOffsetWhenNoLimitSize(): void { $a = Psr7\Utils::streamFor('foo_bar'); $b = new LimitStream($a, -1, 4); diff --git a/tests/MessageTest.php b/tests/MessageTest.php index 339b3ab3..08708cf5 100644 --- a/tests/MessageTest.php +++ b/tests/MessageTest.php @@ -1,13 +1,16 @@ 'bar', @@ -19,7 +22,7 @@ public function testConvertsRequestsToStrings() ); } - public function testConvertsResponsesToStrings() + public function testConvertsResponsesToStrings(): void { $response = new Psr7\Response(200, [ 'Baz' => 'bar', @@ -31,7 +34,7 @@ public function testConvertsResponsesToStrings() ); } - public function testCorrectlyRendersSetCookieHeadersToString() + public function testCorrectlyRendersSetCookieHeadersToString(): void { $response = new Psr7\Response(200, [ 'Set-Cookie' => ['bar','baz','qux'] @@ -42,7 +45,7 @@ public function testCorrectlyRendersSetCookieHeadersToString() ); } - public function testRewindsBody() + public function testRewindsBody(): void { $body = Psr7\Utils::streamFor('abc'); $res = new Psr7\Response(200, [], $body); @@ -53,23 +56,23 @@ public function testRewindsBody() self::assertSame(0, $body->tell()); } - public function testThrowsWhenBodyCannotBeRewound() + public function testThrowsWhenBodyCannotBeRewound(): void { $body = Psr7\Utils::streamFor('abc'); $body->read(1); $body = FnStream::decorate($body, [ - 'rewind' => function () { + 'rewind' => function (): void { throw new \RuntimeException('a'); }, ]); $res = new Psr7\Response(200, [], $body); - $this->expectExceptionGuzzle('RuntimeException'); + $this->expectException(\RuntimeException::class); Psr7\Message::rewindBody($res); } - public function testParsesRequestMessages() + public function testParsesRequestMessages(): void { $req = "GET /abc HTTP/1.0\r\nHost: foo.com\r\nFoo: Bar\r\nBaz: Bam\r\nBaz: Qux\r\n\r\nTest"; $request = Psr7\Message::parseRequest($req); @@ -83,7 +86,7 @@ public function testParsesRequestMessages() self::assertSame('http://foo.com/abc', (string)$request->getUri()); } - public function testParsesRequestMessagesWithHttpsScheme() + public function testParsesRequestMessagesWithHttpsScheme(): void { $req = "PUT /abc?baz=bar HTTP/1.1\r\nHost: foo.com:443\r\n\r\n"; $request = Psr7\Message::parseRequest($req); @@ -95,7 +98,7 @@ public function testParsesRequestMessagesWithHttpsScheme() self::assertSame('https://foo.com/abc?baz=bar', (string)$request->getUri()); } - public function testParsesRequestMessagesWithUriWhenHostIsNotFirst() + public function testParsesRequestMessagesWithUriWhenHostIsNotFirst(): void { $req = "PUT / HTTP/1.1\r\nFoo: Bar\r\nHost: foo.com\r\n\r\n"; $request = Psr7\Message::parseRequest($req); @@ -104,7 +107,7 @@ public function testParsesRequestMessagesWithUriWhenHostIsNotFirst() self::assertSame('http://foo.com/', (string)$request->getUri()); } - public function testParsesRequestMessagesWithFullUri() + public function testParsesRequestMessagesWithFullUri(): void { $req = "GET https://www.google.com:443/search?q=foobar HTTP/1.1\r\nHost: www.google.com\r\n\r\n"; $request = Psr7\Message::parseRequest($req); @@ -116,14 +119,14 @@ public function testParsesRequestMessagesWithFullUri() self::assertSame('https://www.google.com/search?q=foobar', (string)$request->getUri()); } - public function testParsesRequestMessagesWithCustomMethod() + public function testParsesRequestMessagesWithCustomMethod(): void { $req = "GET_DATA / HTTP/1.1\r\nFoo: Bar\r\nHost: foo.com\r\n\r\n"; $request = Psr7\Message::parseRequest($req); self::assertSame('GET_DATA', $request->getMethod()); } - public function testParsesRequestMessagesWithFoldedHeadersOnHttp10() + public function testParsesRequestMessagesWithFoldedHeadersOnHttp10(): void { $req = "PUT / HTTP/1.0\r\nFoo: Bar\r\n Bam\r\n\r\n"; $request = Psr7\Message::parseRequest($req); @@ -132,14 +135,15 @@ public function testParsesRequestMessagesWithFoldedHeadersOnHttp10() self::assertSame('Bar Bam', $request->getHeaderLine('Foo')); } - public function testRequestParsingFailsWithFoldedHeadersOnHttp11() + public function testRequestParsingFailsWithFoldedHeadersOnHttp11(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Invalid header syntax: Obsolete line folding'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid header syntax: Obsolete line folding'); Psr7\Message::parseResponse("GET_DATA / HTTP/1.1\r\nFoo: Bar\r\n Biz: Bam\r\n\r\n"); } - public function testParsesRequestMessagesWhenHeaderDelimiterIsOnlyALineFeed() + public function testParsesRequestMessagesWhenHeaderDelimiterIsOnlyALineFeed(): void { $req = "PUT / HTTP/1.0\nFoo: Bar\nBaz: Bam\n\n"; $request = Psr7\Message::parseRequest($req); @@ -149,14 +153,14 @@ public function testParsesRequestMessagesWhenHeaderDelimiterIsOnlyALineFeed() self::assertSame('Bam', $request->getHeaderLine('Baz')); } - public function testValidatesRequestMessages() + public function testValidatesRequestMessages(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Psr7\Message::parseRequest("HTTP/1.1 200 OK\r\n\r\n"); } - public function testParsesResponseMessages() + public function testParsesResponseMessages(): void { $res = "HTTP/1.0 200 OK\r\nFoo: Bar\r\nBaz: Bam\r\nBaz: Qux\r\n\r\nTest"; $response = Psr7\Message::parseResponse($res); @@ -168,7 +172,7 @@ public function testParsesResponseMessages() self::assertSame('Test', (string)$response->getBody()); } - public function testParsesResponseWithoutReason() + public function testParsesResponseWithoutReason(): void { $res = "HTTP/1.0 200\r\nFoo: Bar\r\nBaz: Bam\r\nBaz: Qux\r\n\r\nTest"; $response = Psr7\Message::parseResponse($res); @@ -180,7 +184,7 @@ public function testParsesResponseWithoutReason() self::assertSame('Test', (string)$response->getBody()); } - public function testParsesResponseWithLeadingDelimiter() + public function testParsesResponseWithLeadingDelimiter(): void { $res = "\r\nHTTP/1.0 200\r\nFoo: Bar\r\n\r\nTest"; $response = Psr7\Message::parseResponse($res); @@ -191,7 +195,7 @@ public function testParsesResponseWithLeadingDelimiter() self::assertSame('Test', (string)$response->getBody()); } - public function testParsesResponseWithFoldedHeadersOnHttp10() + public function testParsesResponseWithFoldedHeadersOnHttp10(): void { $res = "HTTP/1.0 200\r\nFoo: Bar\r\n Bam\r\n\r\nTest"; $response = Psr7\Message::parseResponse($res); @@ -202,14 +206,14 @@ public function testParsesResponseWithFoldedHeadersOnHttp10() self::assertSame('Test', (string)$response->getBody()); } - public function testResponseParsingFailsWithFoldedHeadersOnHttp11() + public function testResponseParsingFailsWithFoldedHeadersOnHttp11(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Invalid header syntax: Obsolete line folding'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid header syntax: Obsolete line folding'); Psr7\Message::parseResponse("HTTP/1.1 200\r\nFoo: Bar\r\n Biz: Bam\r\nBaz: Qux\r\n\r\nTest"); } - public function testParsesResponseWhenHeaderDelimiterIsOnlyALineFeed() + public function testParsesResponseWhenHeaderDelimiterIsOnlyALineFeed(): void { $res = "HTTP/1.0 200\nFoo: Bar\nBaz: Bam\n\nTest\n\nOtherTest"; $response = Psr7\Message::parseResponse($res); @@ -221,45 +225,44 @@ public function testParsesResponseWhenHeaderDelimiterIsOnlyALineFeed() self::assertSame("Test\n\nOtherTest", (string)$response->getBody()); } - public function testResponseParsingFailsWithoutHeaderDelimiter() + public function testResponseParsingFailsWithoutHeaderDelimiter(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Invalid message: Missing header delimiter'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid message: Missing header delimiter'); Psr7\Message::parseResponse("HTTP/1.0 200\r\nFoo: Bar\r\n Baz: Bam\r\nBaz: Qux\r\n"); } - public function testValidatesResponseMessages() + public function testValidatesResponseMessages(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); Psr7\Message::parseResponse("GET / HTTP/1.1\r\n\r\n"); } - public function testMessageBodySummaryWithSmallBody() + public function testMessageBodySummaryWithSmallBody(): void { $message = new Psr7\Response(200, [], 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); self::assertSame('Lorem ipsum dolor sit amet, consectetur adipiscing elit.', Psr7\Message::bodySummary($message)); } - public function testMessageBodySummaryWithLargeBody() + public function testMessageBodySummaryWithLargeBody(): void { $message = new Psr7\Response(200, [], 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); self::assertSame('Lorem ipsu (truncated...)', Psr7\Message::bodySummary($message, 10)); } - public function testMessageBodySummaryWithSpecialUTF8Characters() + public function testMessageBodySummaryWithSpecialUTF8Characters(): void { $message = new Psr7\Response(200, [], '’é€௵ဪ‱'); self::assertSame('’é€௵ဪ‱', Psr7\Message::bodySummary($message)); } - public function testMessageBodySummaryWithEmptyBody() + public function testMessageBodySummaryWithEmptyBody(): void { $message = new Psr7\Response(200, [], ''); self::assertNull(Psr7\Message::bodySummary($message)); } - public function testGetResponseBodySummaryOfNonReadableStream() + public function testGetResponseBodySummaryOfNonReadableStream(): void { self::assertNull(Psr7\Message::bodySummary(new Psr7\Response(500, [], new ReadSeekOnlyStream()))); } diff --git a/tests/MimeTypeTest.php b/tests/MimeTypeTest.php index 169e20b5..3eca5e58 100644 --- a/tests/MimeTypeTest.php +++ b/tests/MimeTypeTest.php @@ -1,18 +1,21 @@ getBoundary()); } - public function testCanProvideBoundary() + public function testCanProvideBoundary(): void { $b = new MultipartStream([], 'foo'); self::assertSame('foo', $b->getBoundary()); } - public function testIsNotWritable() + public function testIsNotWritable(): void { $b = new MultipartStream(); self::assertFalse($b->isWritable()); } - public function testCanCreateEmptyStream() + public function testCanCreateEmptyStream(): void { $b = new MultipartStream(); $boundary = $b->getBoundary(); @@ -33,21 +36,19 @@ public function testCanCreateEmptyStream() self::assertSame(strlen($boundary) + 6, $b->getSize()); } - public function testValidatesFilesArrayElement() + public function testValidatesFilesArrayElement(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); new MultipartStream([['foo' => 'bar']]); } - public function testEnsuresFileHasName() + public function testEnsuresFileHasName(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); new MultipartStream([['contents' => 'bar']]); } - public function testSerializesFields() + public function testSerializesFields(): void { $b = new MultipartStream([ [ @@ -59,15 +60,25 @@ public function testSerializesFields() 'contents' => 'bam' ] ], 'boundary'); - self::assertSame( - "--boundary\r\nContent-Disposition: form-data; name=\"foo\"\r\nContent-Length: 3\r\n\r\n" - . "bar\r\n--boundary\r\nContent-Disposition: form-data; name=\"baz\"\r\nContent-Length: 3" - . "\r\n\r\nbam\r\n--boundary--\r\n", - (string) $b - ); + + $expected = \implode('', [ + "--boundary\r\n", + "Content-Disposition: form-data; name=\"foo\"\r\n", + "Content-Length: 3\r\n", + "\r\n", + "bar\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=\"baz\"\r\n", + "Content-Length: 3\r\n", + "\r\n", + "bam\r\n", + "--boundary--\r\n", + ]); + + self::assertSame($expected, (string) $b); } - public function testSerializesNonStringFields() + public function testSerializesNonStringFields(): void { $b = new MultipartStream([ [ @@ -87,32 +98,51 @@ public function testSerializesNonStringFields() 'contents' => (float) 1.1 ] ], 'boundary'); - self::assertSame( - "--boundary\r\nContent-Disposition: form-data; name=\"int\"\r\nContent-Length: 1\r\n\r\n" - . "1\r\n--boundary\r\nContent-Disposition: form-data; name=\"bool\"\r\n\r\n\r\n--boundary" - . "\r\nContent-Disposition: form-data; name=\"bool2\"\r\nContent-Length: 1\r\n\r\n" - . "1\r\n--boundary\r\nContent-Disposition: form-data; name=\"float\"\r\nContent-Length: 3" - . "\r\n\r\n1.1\r\n--boundary--\r\n", - (string) $b - ); + + $expected = \implode('', [ + "--boundary\r\n", + "Content-Disposition: form-data; name=\"int\"\r\n", + "Content-Length: 1\r\n", + "\r\n", + "1\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=\"bool\"\r\n", + "\r\n", + "\r\n", + "--boundary", + "\r\n", + "Content-Disposition: form-data; name=\"bool2\"\r\n", + "Content-Length: 1\r\n", + "\r\n", + "1\r\n", + "--boundary\r\n", + "Content-Disposition: form-data; name=\"float\"\r\n", + "Content-Length: 3\r\n", + "\r\n", + "1.1\r\n", + "--boundary--\r\n", + "", + ]); + + self::assertSame($expected, (string) $b); } - public function testSerializesFiles() + public function testSerializesFiles(): void { $f1 = Psr7\FnStream::decorate(Psr7\Utils::streamFor('foo'), [ - 'getMetadata' => function () { + 'getMetadata' => static function (): string { return '/foo/bar.txt'; } ]); $f2 = Psr7\FnStream::decorate(Psr7\Utils::streamFor('baz'), [ - 'getMetadata' => function () { + 'getMetadata' => static function (): string { return '/foo/baz.jpg'; } ]); $f3 = Psr7\FnStream::decorate(Psr7\Utils::streamFor('bar'), [ - 'getMetadata' => function () { + 'getMetadata' => static function (): string { return '/foo/bar.gif'; } ]); @@ -132,36 +162,68 @@ public function testSerializesFiles() ], ], 'boundary'); - $expected = << static function (): string { + return '/foo/newlines.txt'; + } + ]); -bar ---boundary-- + $b = new MultipartStream([ + [ + 'name' => 'newlines', + 'contents' => $f1 + ], + ], 'boundary'); -EOT; + $expected = \implode('', [ + "--boundary\r\n", + "Content-Disposition: form-data; name=\"newlines\"; filename=\"newlines.txt\"\r\n", + "Content-Length: {$contentLength}\r\n", + "Content-Type: text/plain\r\n", + "\r\n", + "{$content}\r\n", + "--boundary--\r\n", + ]); - self::assertSame($expected, str_replace("\r", '', $b)); + // Do not perform newline normalization in the assertion! The `$content` must + // be embedded as-is in the payload. + self::assertSame($expected, (string) $b); } - public function testSerializesFilesWithCustomHeaders() + public function testSerializesFilesWithCustomHeaders(): void { $f1 = Psr7\FnStream::decorate(Psr7\Utils::streamFor('foo'), [ - 'getMetadata' => function () { + 'getMetadata' => static function (): string { return '/foo/bar.txt'; } ]); @@ -177,31 +239,30 @@ public function testSerializesFilesWithCustomHeaders() ] ], 'boundary'); - $expected = << function () { + 'getMetadata' => static function (): string { return '/foo/bar.txt'; } ]); $f2 = Psr7\FnStream::decorate(Psr7\Utils::streamFor('baz'), [ - 'getMetadata' => function () { + 'getMetadata' => static function (): string { return '/foo/baz.jpg'; } ]); @@ -222,24 +283,48 @@ public function testSerializesFilesWithCustomHeadersAndMultipleValues() ] ], 'boundary'); - $expected = << 'foo', + 'contents' => $b, + ], + ], 'boundary'); -EOT; + $expected = \implode('', [ + "--boundary\r\n", + "Content-Disposition: form-data; name=\"foo\"\r\n", + "\r\n", + $str . "\r\n", + "--boundary--\r\n", + ]); - self::assertSame($expected, str_replace("\r", '', $b)); + self::assertSame($expected, (string)$c); } } diff --git a/tests/NoSeekStreamTest.php b/tests/NoSeekStreamTest.php index 7960764a..c0585e27 100644 --- a/tests/NoSeekStreamTest.php +++ b/tests/NoSeekStreamTest.php @@ -1,31 +1,32 @@ getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isSeekable', 'seek']) - ->getMockForAbstractClass(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::never())->method('seek'); $s->expects(self::never())->method('isSeekable'); $wrapped = new NoSeekStream($s); self::assertFalse($wrapped->isSeekable()); - - $this->expectExceptionGuzzle('RuntimeException', 'Cannot seek a NoSeekStream'); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Cannot seek a NoSeekStream'); $wrapped->seek(2); } - public function testToStringDoesNotSeek() + public function testToStringDoesNotSeek(): void { $s = \GuzzleHttp\Psr7\Utils::streamFor('foo'); $s->seek(1); diff --git a/tests/PumpStreamTest.php b/tests/PumpStreamTest.php index 31d2cba9..e357018e 100644 --- a/tests/PumpStreamTest.php +++ b/tests/PumpStreamTest.php @@ -1,16 +1,19 @@ ['foo' => 'bar'], 'size' => 100 @@ -21,7 +24,7 @@ public function testHasMetadataAndSize() self::assertSame(100, $p->getSize()); } - public function testCanReadFromCallable() + public function testCanReadFromCallable(): void { $p = Psr7\Utils::streamFor(function ($size) { return 'a'; @@ -32,7 +35,7 @@ public function testCanReadFromCallable() self::assertSame(6, $p->tell()); } - public function testStoresExcessDataInBuffer() + public function testStoresExcessDataInBuffer(): void { $called = []; $p = Psr7\Utils::streamFor(function ($size) use (&$called) { @@ -46,7 +49,7 @@ public function testStoresExcessDataInBuffer() self::assertSame([1, 9, 3], $called); } - public function testInifiniteStreamWrappedInLimitStream() + public function testInifiniteStreamWrappedInLimitStream(): void { $p = Psr7\Utils::streamFor(function () { return 'a'; @@ -55,9 +58,9 @@ public function testInifiniteStreamWrappedInLimitStream() self::assertSame('aaaaa', (string) $s); } - public function testDescribesCapabilities() + public function testDescribesCapabilities(): void { - $p = Psr7\Utils::streamFor(function () { + $p = Psr7\Utils::streamFor(function (): void { }); self::assertTrue($p->isReadable()); self::assertFalse($p->isSeekable()); @@ -75,4 +78,27 @@ public function testDescribesCapabilities() } catch (\RuntimeException $e) { } } + + /** + * @requires PHP < 7.4 + */ + public function testThatConvertingStreamToStringWillTriggerErrorAndWillReturnEmptyString(): void + { + $p = Psr7\Utils::streamFor(function ($size): void { + throw new \Exception(); + }); + self::assertInstanceOf(PumpStream::class, $p); + + $errors = []; + set_error_handler(function (int $errorNumber, string $errorMessage) use (&$errors): void { + $errors[] = ['number' => $errorNumber, 'message' => $errorMessage]; + }); + (string) $p; + + restore_error_handler(); + + self::assertCount(1, $errors); + self::assertSame(E_USER_ERROR, $errors[0]['number']); + self::assertStringStartsWith('GuzzleHttp\Psr7\PumpStream::__toString exception:', $errors[0]['message']); + } } diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 6047fb02..16f46501 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -1,10 +1,13 @@ ['a', 'b', 'c']]], + // Keeps first null when parsing mult-values + ['q&q=&q=a', ['q' => [null, '', 'a']]], ]; } /** * @dataProvider parseQueryProvider */ - public function testParsesQueries($input, $output) + public function testParsesQueries($input, $output): void { $result = Psr7\Query::parse($input); self::assertSame($output, $result); } - public function testDoesNotDecode() + public function testDoesNotDecode(): void { $str = 'foo%20=bar'; $data = Psr7\Query::parse($str, false); @@ -64,35 +69,50 @@ public function testDoesNotDecode() /** * @dataProvider parseQueryProvider */ - public function testParsesAndBuildsQueries($input) + public function testParsesAndBuildsQueries($input): void { $result = Psr7\Query::parse($input, false); self::assertSame($input, Psr7\Query::build($result, false)); } - public function testEncodesWithRfc1738() + public function testEncodesWithRfc1738(): void { $str = Psr7\Query::build(['foo bar' => 'baz+'], PHP_QUERY_RFC1738); self::assertSame('foo+bar=baz%2B', $str); } - public function testEncodesWithRfc3986() + public function testEncodesWithRfc3986(): void { $str = Psr7\Query::build(['foo bar' => 'baz+'], PHP_QUERY_RFC3986); self::assertSame('foo%20bar=baz%2B', $str); } - public function testDoesNotEncode() + public function testDoesNotEncode(): void { $str = Psr7\Query::build(['foo bar' => 'baz+'], false); self::assertSame('foo bar=baz+', $str); } - public function testCanControlDecodingType() + public function testCanControlDecodingType(): void { $result = Psr7\Query::parse('var=foo+bar', PHP_QUERY_RFC3986); self::assertSame('foo+bar', $result['var']); $result = Psr7\Query::parse('var=foo+bar', PHP_QUERY_RFC1738); self::assertSame('foo bar', $result['var']); } + + public function testBuildBooleans(): void + { + $data = [ + 'true' => true, + 'false' => false + ]; + self::assertEquals(http_build_query($data), Psr7\Query::build($data)); + + $data = [ + 'foo' => [true, 'true'], + 'bar' => [false, 'false'] + ]; + self::assertEquals('foo=1&foo=true&bar=0&bar=false', Psr7\Query::build($data, PHP_QUERY_RFC1738)); + } } diff --git a/tests/ReadSeekOnlyStream.php b/tests/ReadSeekOnlyStream.php index 27409ece..a93afb15 100644 --- a/tests/ReadSeekOnlyStream.php +++ b/tests/ReadSeekOnlyStream.php @@ -1,22 +1,23 @@ -getUri()); } - public function testRequestUriMayBeUri() + public function testRequestUriMayBeUri(): void { $uri = new Uri('/'); $r = new Request('GET', $uri); self::assertSame($uri, $r->getUri()); } - public function testValidateRequestUri() + public function testValidateRequestUri(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); new Request('GET', '///'); } - public function testCanConstructWithBody() + public function testCanConstructWithBody(): void { $r = new Request('GET', '/', [], 'baz'); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('baz', (string) $r->getBody()); } - public function testNullBody() + public function testNullBody(): void { $r = new Request('GET', '/', [], null); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('', (string) $r->getBody()); } - public function testFalseyBody() + public function testFalseyBody(): void { $r = new Request('GET', '/', [], '0'); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('0', (string) $r->getBody()); } - public function testConstructorDoesNotReadStreamBody() + public function testConstructorDoesNotReadStreamBody(): void { $streamIsRead = false; $body = Psr7\FnStream::decorate(Psr7\Utils::streamFor(''), [ @@ -68,19 +71,19 @@ public function testConstructorDoesNotReadStreamBody() self::assertSame($body, $r->getBody()); } - public function testCapitalizesMethod() + public function testCapitalizesMethod(): void { $r = new Request('get', '/'); self::assertSame('GET', $r->getMethod()); } - public function testCapitalizesWithMethod() + public function testCapitalizesWithMethod(): void { $r = new Request('GET', '/'); self::assertSame('PUT', $r->withMethod('put')->getMethod()); } - public function testWithUri() + public function testWithUri(): void { $r1 = new Request('GET', '/'); $u1 = $r1->getUri(); @@ -94,23 +97,23 @@ public function testWithUri() /** * @dataProvider invalidMethodsProvider */ - public function testConstructWithInvalidMethods($method) + public function testConstructWithInvalidMethods($method): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\TypeError::class); new Request($method, '/'); } /** * @dataProvider invalidMethodsProvider */ - public function testWithInvalidMethods($method) + public function testWithInvalidMethods($method): void { $r = new Request('get', '/'); - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $r->withMethod($method); } - public function invalidMethodsProvider() + public function invalidMethodsProvider(): iterable { return [ [null], @@ -120,14 +123,14 @@ public function invalidMethodsProvider() ]; } - public function testSameInstanceWhenSameUri() + public function testSameInstanceWhenSameUri(): void { $r1 = new Request('GET', 'http://foo.com'); $r2 = $r1->withUri($r1->getUri()); self::assertSame($r1, $r2); } - public function testWithRequestTarget() + public function testWithRequestTarget(): void { $r1 = new Request('GET', '/'); $r2 = $r1->withRequestTarget('*'); @@ -135,15 +138,14 @@ public function testWithRequestTarget() self::assertSame('/', $r1->getRequestTarget()); } - public function testRequestTargetDoesNotAllowSpaces() + public function testRequestTargetDoesNotAllowSpaces(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - $r1 = new Request('GET', '/'); + $this->expectException(\InvalidArgumentException::class); $r1->withRequestTarget('/foo bar'); } - public function testRequestTargetDefaultsToSlash() + public function testRequestTargetDefaultsToSlash(): void { $r1 = new Request('GET', ''); self::assertSame('/', $r1->getRequestTarget()); @@ -153,19 +155,19 @@ public function testRequestTargetDefaultsToSlash() self::assertSame('/bar%20baz/', $r3->getRequestTarget()); } - public function testBuildsRequestTarget() + public function testBuildsRequestTarget(): void { $r1 = new Request('GET', 'http://foo.com/baz?bar=bam'); self::assertSame('/baz?bar=bam', $r1->getRequestTarget()); } - public function testBuildsRequestTargetWithFalseyQuery() + public function testBuildsRequestTargetWithFalseyQuery(): void { $r1 = new Request('GET', 'http://foo.com/baz?0'); self::assertSame('/baz?0', $r1->getRequestTarget()); } - public function testHostIsAddedFirst() + public function testHostIsAddedFirst(): void { $r = new Request('GET', 'http://foo.com/baz?bar=bam', ['Foo' => 'Bar']); self::assertSame([ @@ -174,7 +176,18 @@ public function testHostIsAddedFirst() ], $r->getHeaders()); } - public function testCanGetHeaderAsCsv() + public function testHeaderValueWithWhitespace(): void + { + $r = new Request('GET', 'https://example.com/', [ + 'User-Agent' => 'Linux f0f489981e90 5.10.104-linuxkit 1 SMP Wed Mar 9 19:05:23 UTC 2022 x86_64' + ]); + self::assertSame([ + 'Host' => ['example.com'], + 'User-Agent' => ['Linux f0f489981e90 5.10.104-linuxkit 1 SMP Wed Mar 9 19:05:23 UTC 2022 x86_64'] + ], $r->getHeaders()); + } + + public function testCanGetHeaderAsCsv(): void { $r = new Request('GET', 'http://foo.com/baz?bar=bam', [ 'Foo' => ['a', 'b', 'c'] @@ -183,7 +196,69 @@ public function testCanGetHeaderAsCsv() self::assertSame('', $r->getHeaderLine('Bar')); } - public function testHostIsNotOverwrittenWhenPreservingHost() + /** + * @dataProvider provideHeadersContainingNotAllowedChars + */ + public function testContainsNotAllowedCharsOnHeaderField($header): void + { + $this->expectExceptionMessage( + sprintf( + '"%s" is not valid header name', + $header + ) + ); + $r = new Request( + 'GET', + 'http://foo.com/baz?bar=bam', + [ + $header => 'value' + ] + ); + } + + public function provideHeadersContainingNotAllowedChars(): iterable + { + return [[' key '], ['key '], [' key'], ['key/'], ['key('], ['key\\'], [' ']]; + } + + /** + * @dataProvider provideHeadersContainsAllowedChar + */ + public function testContainsAllowedCharsOnHeaderField($header): void + { + $r = new Request( + 'GET', + 'http://foo.com/baz?bar=bam', + [ + $header => 'value' + ] + ); + self::assertArrayHasKey($header, $r->getHeaders()); + } + + public function provideHeadersContainsAllowedChar(): iterable + { + return [ + ['key'], + ['key#'], + ['key$'], + ['key%'], + ['key&'], + ['key*'], + ['key+'], + ['key.'], + ['key^'], + ['key_'], + ['key|'], + ['key~'], + ['key!'], + ['key-'], + ["key'"], + ['key`'] + ]; + } + + public function testHostIsNotOverwrittenWhenPreservingHost(): void { $r = new Request('GET', 'http://foo.com/baz?bar=bam', ['Host' => 'a.com']); self::assertSame(['Host' => ['a.com']], $r->getHeaders()); @@ -191,7 +266,7 @@ public function testHostIsNotOverwrittenWhenPreservingHost() self::assertSame('a.com', $r2->getHeaderLine('Host')); } - public function testWithUriSetsHostIfNotSet() + public function testWithUriSetsHostIfNotSet(): void { $r = (new Request('GET', 'http://foo.com/baz?bar=bam'))->withoutHeader('Host'); self::assertSame([], $r->getHeaders()); @@ -199,7 +274,7 @@ public function testWithUriSetsHostIfNotSet() self::assertSame('www.baz.com', $r2->getHeaderLine('Host')); } - public function testOverridesHostWithUri() + public function testOverridesHostWithUri(): void { $r = new Request('GET', 'http://foo.com/baz?bar=bam'); self::assertSame(['Host' => ['foo.com']], $r->getHeaders()); @@ -207,7 +282,7 @@ public function testOverridesHostWithUri() self::assertSame('www.baz.com', $r2->getHeaderLine('Host')); } - public function testAggregatesHeaders() + public function testAggregatesHeaders(): void { $r = new Request('GET', '', [ 'ZOO' => 'zoobar', @@ -217,16 +292,65 @@ public function testAggregatesHeaders() self::assertSame('zoobar, foobar, zoobar', $r->getHeaderLine('zoo')); } - public function testAddsPortToHeader() + public function testAddsPortToHeader(): void { $r = new Request('GET', 'http://foo.com:8124/bar'); self::assertSame('foo.com:8124', $r->getHeaderLine('host')); } - public function testAddsPortToHeaderAndReplacePreviousPort() + public function testAddsPortToHeaderAndReplacePreviousPort(): void { $r = new Request('GET', 'http://foo.com:8124/bar'); $r = $r->withUri(new Uri('http://foo.com:8125/bar')); self::assertSame('foo.com:8125', $r->getHeaderLine('host')); } + + /** + * @dataProvider provideHeaderValuesContainingNotAllowedChars + */ + public function testContainsNotAllowedCharsOnHeaderValue(string $value): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(sprintf('"%s" is not valid header value', $value)); + + $r = new Request( + 'GET', + 'http://foo.com/baz?bar=bam', + [ + 'testing' => $value + ] + ); + } + + public function provideHeaderValuesContainingNotAllowedChars(): iterable + { + // Explicit tests for newlines as the most common exploit vector. + $tests = [ + ["new\nline"], + ["new\r\nline"], + ["new\rline"], + // Line folding is technically allowed, but deprecated. + // We don't support it. + ["new\r\n line"], + ]; + + for ($i = 0; $i <= 0xff; $i++) { + if (\chr($i) == "\t") { + continue; + } + if (\chr($i) == " ") { + continue; + } + if ($i >= 0x21 && $i <= 0x7e) { + continue; + } + if ($i >= 0x80) { + continue; + } + + $tests[] = ["foo" . \chr($i) . "bar"]; + } + + return $tests; + } } diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php index c9f06034..9accb4f8 100644 --- a/tests/ResponseTest.php +++ b/tests/ResponseTest.php @@ -1,35 +1,39 @@ getStatusCode()); self::assertSame('1.1', $r->getProtocolVersion()); self::assertSame('OK', $r->getReasonPhrase()); self::assertSame([], $r->getHeaders()); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('', (string) $r->getBody()); } - public function testCanConstructWithStatusCode() + public function testCanConstructWithStatusCode(): void { $r = new Response(404); self::assertSame(404, $r->getStatusCode()); self::assertSame('Not Found', $r->getReasonPhrase()); } - public function testConstructorDoesNotReadStreamBody() + public function testConstructorDoesNotReadStreamBody(): void { $streamIsRead = false; $body = Psr7\FnStream::decorate(Psr7\Utils::streamFor(''), [ @@ -44,17 +48,15 @@ public function testConstructorDoesNotReadStreamBody() self::assertSame($body, $r->getBody()); } - public function testStatusCanBeNumericString() + public function testStatusCanBeNumericString(): void { - $r = new Response('404'); - $r2 = $r->withStatus('201'); - self::assertSame(404, $r->getStatusCode()); - self::assertSame('Not Found', $r->getReasonPhrase()); - self::assertSame(201, $r2->getStatusCode()); - self::assertSame('Created', $r2->getReasonPhrase()); + $r = (new Response())->withStatus('201'); + + self::assertSame(201, $r->getStatusCode()); + self::assertSame('Created', $r->getReasonPhrase()); } - public function testCanConstructWithHeaders() + public function testCanConstructWithHeaders(): void { $r = new Response(200, ['Foo' => 'Bar']); self::assertSame(['Foo' => ['Bar']], $r->getHeaders()); @@ -62,7 +64,7 @@ public function testCanConstructWithHeaders() self::assertSame(['Bar'], $r->getHeader('Foo')); } - public function testCanConstructWithHeadersAsArray() + public function testCanConstructWithHeadersAsArray(): void { $r = new Response(200, [ 'Foo' => ['baz', 'bar'] @@ -72,28 +74,28 @@ public function testCanConstructWithHeadersAsArray() self::assertSame(['baz', 'bar'], $r->getHeader('Foo')); } - public function testCanConstructWithBody() + public function testCanConstructWithBody(): void { $r = new Response(200, [], 'baz'); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('baz', (string) $r->getBody()); } - public function testNullBody() + public function testNullBody(): void { $r = new Response(200, [], null); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('', (string) $r->getBody()); } - public function testFalseyBody() + public function testFalseyBody(): void { $r = new Response(200, [], '0'); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('0', (string) $r->getBody()); } - public function testCanConstructWithReason() + public function testCanConstructWithReason(): void { $r = new Response(200, [], null, '1.1', 'bar'); self::assertSame('bar', $r->getReasonPhrase()); @@ -102,20 +104,20 @@ public function testCanConstructWithReason() self::assertSame('0', $r->getReasonPhrase(), 'Falsey reason works'); } - public function testCanConstructWithProtocolVersion() + public function testCanConstructWithProtocolVersion(): void { $r = new Response(200, [], null, '1000'); self::assertSame('1000', $r->getProtocolVersion()); } - public function testWithStatusCodeAndNoReason() + public function testWithStatusCodeAndNoReason(): void { $r = (new Response())->withStatus(201); self::assertSame(201, $r->getStatusCode()); self::assertSame('Created', $r->getReasonPhrase()); } - public function testWithStatusCodeAndReason() + public function testWithStatusCodeAndReason(): void { $r = (new Response())->withStatus(201, 'Foo'); self::assertSame(201, $r->getStatusCode()); @@ -126,34 +128,34 @@ public function testWithStatusCodeAndReason() self::assertSame('0', $r->getReasonPhrase(), 'Falsey reason works'); } - public function testWithProtocolVersion() + public function testWithProtocolVersion(): void { $r = (new Response())->withProtocolVersion('1000'); self::assertSame('1000', $r->getProtocolVersion()); } - public function testSameInstanceWhenSameProtocol() + public function testSameInstanceWhenSameProtocol(): void { $r = new Response(); self::assertSame($r, $r->withProtocolVersion('1.1')); } - public function testWithBody() + public function testWithBody(): void { $b = Psr7\Utils::streamFor('0'); $r = (new Response())->withBody($b); - self::assertInstanceOf('Psr\Http\Message\StreamInterface', $r->getBody()); + self::assertInstanceOf(StreamInterface::class, $r->getBody()); self::assertSame('0', (string) $r->getBody()); } - public function testSameInstanceWhenSameBody() + public function testSameInstanceWhenSameBody(): void { $r = new Response(); $b = $r->getBody(); self::assertSame($r, $r->withBody($b)); } - public function testWithHeader() + public function testWithHeader(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withHeader('baZ', 'Bam'); @@ -163,13 +165,13 @@ public function testWithHeader() self::assertSame(['Bam'], $r2->getHeader('baz')); } - public function testNumericHeaderValue() + public function testNumericHeaderValue(): void { $r = (new Response())->withHeader('Api-Version', 1); self::assertSame(['Api-Version' => ['1']], $r->getHeaders()); } - public function testWithHeaderAsArray() + public function testWithHeaderAsArray(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withHeader('baZ', ['Bam', 'Bar']); @@ -179,7 +181,7 @@ public function testWithHeaderAsArray() self::assertSame(['Bam', 'Bar'], $r2->getHeader('baz')); } - public function testWithHeaderReplacesDifferentCase() + public function testWithHeaderReplacesDifferentCase(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withHeader('foO', 'Bam'); @@ -189,7 +191,7 @@ public function testWithHeaderReplacesDifferentCase() self::assertSame(['Bam'], $r2->getHeader('foo')); } - public function testWithAddedHeader() + public function testWithAddedHeader(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withAddedHeader('foO', 'Baz'); @@ -199,7 +201,7 @@ public function testWithAddedHeader() self::assertSame(['Bar', 'Baz'], $r2->getHeader('foo')); } - public function testWithAddedHeaderAsArray() + public function testWithAddedHeaderAsArray(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withAddedHeader('foO', ['Baz', 'Bam']); @@ -209,7 +211,7 @@ public function testWithAddedHeaderAsArray() self::assertSame(['Bar', 'Baz', 'Bam'], $r2->getHeader('foo')); } - public function testWithAddedHeaderThatDoesNotExist() + public function testWithAddedHeaderThatDoesNotExist(): void { $r = new Response(200, ['Foo' => 'Bar']); $r2 = $r->withAddedHeader('nEw', 'Baz'); @@ -219,7 +221,7 @@ public function testWithAddedHeaderThatDoesNotExist() self::assertSame(['Baz'], $r2->getHeader('new')); } - public function testWithoutHeaderThatExists() + public function testWithoutHeaderThatExists(): void { $r = new Response(200, ['Foo' => 'Bar', 'Baz' => 'Bam']); $r2 = $r->withoutHeader('foO'); @@ -229,7 +231,7 @@ public function testWithoutHeaderThatExists() self::assertSame(['Baz' => ['Bam']], $r2->getHeaders()); } - public function testWithoutHeaderThatDoesNotExist() + public function testWithoutHeaderThatDoesNotExist(): void { $r = new Response(200, ['Baz' => 'Bam']); $r2 = $r->withoutHeader('foO'); @@ -238,13 +240,13 @@ public function testWithoutHeaderThatDoesNotExist() self::assertSame(['Baz' => ['Bam']], $r2->getHeaders()); } - public function testSameInstanceWhenRemovingMissingHeader() + public function testSameInstanceWhenRemovingMissingHeader(): void { $r = new Response(); self::assertSame($r, $r->withoutHeader('foo')); } - public function testPassNumericHeaderNameInConstructor() + public function testPassNumericHeaderNameInConstructor(): void { $r = new Response(200, ['Location' => 'foo', '123' => 'bar']); self::assertSame('bar', $r->getHeaderLine('123')); @@ -253,41 +255,42 @@ public function testPassNumericHeaderNameInConstructor() /** * @dataProvider invalidHeaderProvider */ - public function testConstructResponseInvalidHeader($header, $headerValue, $expectedMessage) + public function testConstructResponseInvalidHeader($header, $headerValue, $expectedMessage): void { - $this->expectExceptionGuzzle('InvalidArgumentException', $expectedMessage); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); new Response(200, [$header => $headerValue]); } - public function invalidHeaderProvider() + public function invalidHeaderProvider(): iterable { return [ ['foo', [], 'Header value can not be an empty array.'], - ['', '', 'Header name can not be empty.'], - ['foo', new \stdClass(), 'Header value must be scalar or null but stdClass provided.'], + ['', '', '"" is not valid header name'], + ['foo', new \stdClass(), 'Header value must be scalar or null but stdClass provided.'], ]; } /** * @dataProvider invalidWithHeaderProvider */ - public function testWithInvalidHeader($header, $headerValue, $expectedMessage) + public function testWithInvalidHeader($header, $headerValue, $expectedMessage): void { $r = new Response(); - $this->expectExceptionGuzzle('InvalidArgumentException', $expectedMessage); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage($expectedMessage); $r->withHeader($header, $headerValue); } - public function invalidWithHeaderProvider() + public function invalidWithHeaderProvider(): iterable { - return array_merge($this->invalidHeaderProvider(), [ - [[], 'foo', 'Header name must be a string but array provided.'], - [false, 'foo', 'Header name must be a string but boolean provided.'], - [new \stdClass(), 'foo', 'Header name must be a string but stdClass provided.'], - ]); + yield from $this->invalidHeaderProvider(); + yield [[], 'foo', 'Header name must be a string but array provided.']; + yield [false, 'foo', 'Header name must be a string but boolean provided.']; + yield [new \stdClass(), 'foo', 'Header name must be a string but stdClass provided.']; } - public function testHeaderValuesAreTrimmed() + public function testHeaderValuesAreTrimmed(): void { $r1 = new Response(200, ['OWS' => " \t \tFoo\t \t "]); $r2 = (new Response())->withHeader('OWS', " \t \tFoo\t \t "); @@ -300,7 +303,7 @@ public function testHeaderValuesAreTrimmed() } } - public function testWithAddedHeaderArrayValueAndKeys() + public function testWithAddedHeaderArrayValueAndKeys(): void { $message = (new Response())->withAddedHeader('list', ['foo' => 'one']); $message = $message->withAddedHeader('list', ['foo' => 'two', 'bar' => 'three']); @@ -314,9 +317,9 @@ public function testWithAddedHeaderArrayValueAndKeys() * * @param mixed $invalidValues */ - public function testConstructResponseWithNonIntegerStatusCode($invalidValues) + public function testConstructResponseWithNonIntegerStatusCode($invalidValues): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Status code must be an integer value.'); + $this->expectException(\TypeError::class); new Response($invalidValues); } @@ -325,14 +328,15 @@ public function testConstructResponseWithNonIntegerStatusCode($invalidValues) * * @param mixed $invalidValues */ - public function testResponseChangeStatusCodeWithNonInteger($invalidValues) + public function testResponseChangeStatusCodeWithNonInteger($invalidValues): void { $response = new Response(); - $this->expectExceptionGuzzle('InvalidArgumentException', 'Status code must be an integer value.'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Status code must be an integer value.'); $response->withStatus($invalidValues); } - public function nonIntegerStatusCodeProvider() + public function nonIntegerStatusCodeProvider(): iterable { return [ ['whatever'], @@ -347,9 +351,10 @@ public function nonIntegerStatusCodeProvider() * * @param mixed $invalidValues */ - public function testConstructResponseWithInvalidRangeStatusCode($invalidValues) + public function testConstructResponseWithInvalidRangeStatusCode($invalidValues): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Status code must be an integer value between 1xx and 5xx.'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Status code must be an integer value between 1xx and 5xx.'); new Response($invalidValues); } @@ -358,14 +363,15 @@ public function testConstructResponseWithInvalidRangeStatusCode($invalidValues) * * @param mixed $invalidValues */ - public function testResponseChangeStatusCodeWithWithInvalidRange($invalidValues) + public function testResponseChangeStatusCodeWithWithInvalidRange($invalidValues): void { $response = new Response(); - $this->expectExceptionGuzzle('InvalidArgumentException', 'Status code must be an integer value between 1xx and 5xx.'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Status code must be an integer value between 1xx and 5xx.'); $response->withStatus($invalidValues); } - public function invalidStatusCodeRangeProvider() + public function invalidStatusCodeRangeProvider(): iterable { return [ [600], diff --git a/tests/ServerRequestTest.php b/tests/ServerRequestTest.php index 10f46b4a..f7605120 100644 --- a/tests/ServerRequestTest.php +++ b/tests/ServerRequestTest.php @@ -1,17 +1,20 @@ [ @@ -258,20 +261,21 @@ public function dataNormalizeFiles() /** * @dataProvider dataNormalizeFiles */ - public function testNormalizeFiles($files, $expected) + public function testNormalizeFiles($files, $expected): void { $result = ServerRequest::normalizeFiles($files); self::assertEquals($expected, $result); } - public function testNormalizeFilesRaisesException() + public function testNormalizeFilesRaisesException(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Invalid value in files specification'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value in files specification'); ServerRequest::normalizeFiles(['test' => 'something']); } - public function dataGetUriFromGlobals() + public function dataGetUriFromGlobals(): iterable { $server = [ 'REQUEST_URI' => '/blog/article.php?id=10&user=foo', @@ -350,14 +354,14 @@ public function dataGetUriFromGlobals() /** * @dataProvider dataGetUriFromGlobals */ - public function testGetUriFromGlobals($expected, $serverParams) + public function testGetUriFromGlobals($expected, $serverParams): void { $_SERVER = $serverParams; self::assertEquals(new Uri($expected), ServerRequest::getUriFromGlobals()); } - public function testFromGlobals() + public function testFromGlobals(): void { $_SERVER = [ 'REQUEST_URI' => '/blog/article.php?id=10&user=foo', @@ -439,7 +443,7 @@ public function testFromGlobals() self::assertEquals($expectedFiles, $server->getUploadedFiles()); } - public function testUploadedFiles() + public function testUploadedFiles(): void { $request1 = new ServerRequest('GET', '/'); @@ -454,7 +458,7 @@ public function testUploadedFiles() self::assertSame($files, $request2->getUploadedFiles()); } - public function testServerParams() + public function testServerParams(): void { $params = ['name' => 'value']; @@ -462,7 +466,7 @@ public function testServerParams() self::assertSame($params, $request->getServerParams()); } - public function testCookieParams() + public function testCookieParams(): void { $request1 = new ServerRequest('GET', '/'); @@ -475,7 +479,7 @@ public function testCookieParams() self::assertSame($params, $request2->getCookieParams()); } - public function testQueryParams() + public function testQueryParams(): void { $request1 = new ServerRequest('GET', '/'); @@ -488,7 +492,7 @@ public function testQueryParams() self::assertSame($params, $request2->getQueryParams()); } - public function testParsedBody() + public function testParsedBody(): void { $request1 = new ServerRequest('GET', '/'); @@ -501,7 +505,7 @@ public function testParsedBody() self::assertSame($params, $request2->getParsedBody()); } - public function testAttributes() + public function testAttributes(): void { $request1 = new ServerRequest('GET', '/'); @@ -525,11 +529,11 @@ public function testAttributes() self::assertSame('value', $request2->getAttribute('name')); self::assertSame(['name' => 'value'], $request2->getAttributes()); - self::assertEquals(['name' => 'value', 'other' => 'otherValue'], $request3->getAttributes()); + self::assertSame(['name' => 'value', 'other' => 'otherValue'], $request3->getAttributes()); self::assertSame(['name' => 'value'], $request4->getAttributes()); } - public function testNullAttribute() + public function testNullAttribute(): void { $request = (new ServerRequest('GET', '/'))->withAttribute('name', null); diff --git a/tests/StreamDecoratorTraitTest.php b/tests/StreamDecoratorTraitTest.php index 365e7f37..4e5a9092 100644 --- a/tests/StreamDecoratorTraitTest.php +++ b/tests/StreamDecoratorTraitTest.php @@ -1,9 +1,12 @@ c = fopen('php://temp', 'r+'); fwrite($this->c, 'foo'); @@ -35,46 +35,47 @@ public function setUpTest() $this->b = new Str($this->a); } - public function testCatchesExceptionsWhenCastingToString() + /** + * @requires PHP < 7.4 + */ + public function testCatchesExceptionsWhenCastingToString(): void { - $s = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['read']) - ->getMockForAbstractClass(); + $s = $this->createMock(Str::class); $s->expects(self::once()) ->method('read') - ->will(self::throwException(new \Exception('foo'))); + ->willThrowException(new \RuntimeException('foo')); $msg = ''; - set_error_handler(function ($errNo, $str) use (&$msg) { + set_error_handler(function (int $errNo, string $str) use (&$msg): void { $msg = $str; }); echo new Str($s); restore_error_handler(); - $this->assertStringContainsStringGuzzle('foo', $msg); + self::assertStringContainsString('foo', $msg); } - public function testToString() + public function testToString(): void { self::assertSame('foo', (string) $this->b); } - public function testHasSize() + public function testHasSize(): void { self::assertSame(3, $this->b->getSize()); } - public function testReads() + public function testReads(): void { self::assertSame('foo', $this->b->read(10)); } - public function testCheckMethods() + public function testCheckMethods(): void { self::assertSame($this->a->isReadable(), $this->b->isReadable()); self::assertSame($this->a->isWritable(), $this->b->isWritable()); self::assertSame($this->a->isSeekable(), $this->b->isSeekable()); } - public function testSeeksAndTells() + public function testSeeksAndTells(): void { $this->b->seek(1); self::assertSame(1, $this->a->tell()); @@ -87,7 +88,7 @@ public function testSeeksAndTells() self::assertSame(3, $this->b->tell()); } - public function testGetsContents() + public function testGetsContents(): void { self::assertSame('foo', $this->b->getContents()); self::assertSame('', $this->b->getContents()); @@ -95,44 +96,41 @@ public function testGetsContents() self::assertSame('oo', $this->b->getContents()); } - public function testCloses() + public function testCloses(): void { $this->b->close(); self::assertFalse(is_resource($this->c)); } - public function testDetaches() + public function testDetaches(): void { $this->b->detach(); self::assertFalse($this->b->isReadable()); } - public function testWrapsMetadata() + public function testWrapsMetadata(): void { self::assertSame($this->b->getMetadata(), $this->a->getMetadata()); self::assertSame($this->b->getMetadata('uri'), $this->a->getMetadata('uri')); } - public function testWrapsWrites() + public function testWrapsWrites(): void { $this->b->seek(0, SEEK_END); $this->b->write('foo'); self::assertSame('foofoo', (string) $this->a); } - public function testThrowsWithInvalidGetter() + public function testThrowsWithInvalidGetter(): void { - $this->expectExceptionGuzzle('UnexpectedValueException'); - + $this->expectException(\UnexpectedValueException::class); $this->b->foo; } - public function testThrowsWhenGetterNotImplemented() + public function testThrowsWhenGetterNotImplemented(): void { + $this->expectException(\BadMethodCallException::class); $s = new BadStream(); - - $this->expectExceptionGuzzle('BadMethodCallException'); - $s->stream; } } diff --git a/tests/StreamTest.php b/tests/StreamTest.php index 3cd2a504..906078cb 100644 --- a/tests/StreamTest.php +++ b/tests/StreamTest.php @@ -1,24 +1,26 @@ expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); new Stream(true); } - public function testConstructorInitializesProperties() + public function testConstructorInitializesProperties(): void { $handle = fopen('php://temp', 'r+'); fwrite($handle, 'data'); @@ -27,13 +29,13 @@ public function testConstructorInitializesProperties() self::assertTrue($stream->isWritable()); self::assertTrue($stream->isSeekable()); self::assertSame('php://temp', $stream->getMetadata('uri')); - $this->assertInternalTypeGuzzle('array', $stream->getMetadata()); + self::assertIsArray($stream->getMetadata()); self::assertSame(4, $stream->getSize()); self::assertFalse($stream->eof()); $stream->close(); } - public function testConstructorInitializesPropertiesWithRbPlus() + public function testConstructorInitializesPropertiesWithRbPlus(): void { $handle = fopen('php://temp', 'rb+'); fwrite($handle, 'data'); @@ -42,13 +44,13 @@ public function testConstructorInitializesPropertiesWithRbPlus() self::assertTrue($stream->isWritable()); self::assertTrue($stream->isSeekable()); self::assertSame('php://temp', $stream->getMetadata('uri')); - $this->assertInternalTypeGuzzle('array', $stream->getMetadata()); + self::assertIsArray($stream->getMetadata()); self::assertSame(4, $stream->getSize()); self::assertFalse($stream->eof()); $stream->close(); } - public function testStreamClosesHandleOnDestruct() + public function testStreamClosesHandleOnDestruct(): void { $handle = fopen('php://temp', 'r'); $stream = new Stream($handle); @@ -56,7 +58,7 @@ public function testStreamClosesHandleOnDestruct() self::assertFalse(is_resource($handle)); } - public function testConvertsToString() + public function testConvertsToString(): void { $handle = fopen('php://temp', 'w+'); fwrite($handle, 'data'); @@ -66,24 +68,16 @@ public function testConvertsToString() $stream->close(); } - public function testConvertsToStringNonSeekableStream() + public function testConvertsToStringNonSeekableStream(): void { - if (defined('HHVM_VERSION')) { - self::markTestSkipped('This does not work on HHVM.'); - } - $handle = popen('echo foo', 'r'); $stream = new Stream($handle); self::assertFalse($stream->isSeekable()); self::assertSame('foo', trim((string) $stream)); } - public function testConvertsToStringNonSeekablePartiallyReadStream() + public function testConvertsToStringNonSeekablePartiallyReadStream(): void { - if (defined('HHVM_VERSION')) { - self::markTestSkipped('This does not work on HHVM.'); - } - $handle = popen('echo bar', 'r'); $stream = new Stream($handle); $firstLetter = $stream->read(1); @@ -92,7 +86,7 @@ public function testConvertsToStringNonSeekablePartiallyReadStream() self::assertSame('ar', trim((string) $stream)); } - public function testGetsContents() + public function testGetsContents(): void { $handle = fopen('php://temp', 'w+'); fwrite($handle, 'data'); @@ -104,7 +98,7 @@ public function testGetsContents() $stream->close(); } - public function testChecksEof() + public function testChecksEof(): void { $handle = fopen('php://temp', 'w+'); fwrite($handle, 'data'); @@ -116,7 +110,7 @@ public function testChecksEof() $stream->close(); } - public function testGetSize() + public function testGetSize(): void { $size = filesize(__FILE__); $handle = fopen(__FILE__, 'r'); @@ -127,7 +121,7 @@ public function testGetSize() $stream->close(); } - public function testEnsuresSizeIsConsistent() + public function testEnsuresSizeIsConsistent(): void { $h = fopen('php://temp', 'w+'); self::assertSame(3, fwrite($h, 'foo')); @@ -139,7 +133,7 @@ public function testEnsuresSizeIsConsistent() $stream->close(); } - public function testProvidesStreamPosition() + public function testProvidesStreamPosition(): void { $handle = fopen('php://temp', 'w+'); $stream = new Stream($handle); @@ -152,12 +146,12 @@ public function testProvidesStreamPosition() $stream->close(); } - public function testDetachStreamAndClearProperties() + public function testDetachStreamAndClearProperties(): void { $handle = fopen('php://temp', 'r'); $stream = new Stream($handle); self::assertSame($handle, $stream->detach()); - $this->assertInternalTypeGuzzle('resource', $handle, 'Stream is not closed'); + self::assertIsResource($handle, 'Stream is not closed'); self::assertNull($stream->detach()); $this->assertStreamStateAfterClosedOrDetached($stream); @@ -165,7 +159,7 @@ public function testDetachStreamAndClearProperties() $stream->close(); } - public function testCloseResourceAndClearProperties() + public function testCloseResourceAndClearProperties(): void { $handle = fopen('php://temp', 'r'); $stream = new Stream($handle); @@ -176,7 +170,7 @@ public function testCloseResourceAndClearProperties() $this->assertStreamStateAfterClosedOrDetached($stream); } - private function assertStreamStateAfterClosedOrDetached(Stream $stream) + private function assertStreamStateAfterClosedOrDetached(Stream $stream): void { self::assertFalse($stream->isReadable()); self::assertFalse($stream->isWritable()); @@ -185,11 +179,11 @@ private function assertStreamStateAfterClosedOrDetached(Stream $stream) self::assertSame([], $stream->getMetadata()); self::assertNull($stream->getMetadata('foo')); - $throws = function (callable $fn) { + $throws = function (callable $fn): void { try { $fn(); } catch (\Exception $e) { - $this->assertStringContainsStringGuzzle('Stream is detached', $e->getMessage()); + $this->assertStringContainsString('Stream is detached', $e->getMessage()); return; } @@ -197,28 +191,44 @@ private function assertStreamStateAfterClosedOrDetached(Stream $stream) $this->fail('Exception should be thrown after the stream is detached.'); }; - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->read(10); }); - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->write('bar'); }); - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->seek(10); }); - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->tell(); }); - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->eof(); }); - $throws(function () use ($stream) { + $throws(function () use ($stream): void { $stream->getContents(); }); - self::assertSame('', (string) $stream); + + if (\PHP_VERSION_ID >= 70400) { + $throws(function () use ($stream): void { + (string) $stream; + }); + } else { + $errors = []; + set_error_handler(function (int $errorNumber, string $errorMessage) use (&$errors): void { + $errors[] = ['message' => $errorMessage, 'number' => $errorNumber]; + }); + self::assertSame('', (string) $stream); + restore_error_handler(); + + self::assertCount(1, $errors); + self::assertStringStartsWith('GuzzleHttp\Psr7\Stream::__toString exception', $errors[0]['message']); + self::assertSame(E_USER_ERROR, $errors[0]['number']); + } } - public function testStreamReadingWithZeroLength() + public function testStreamReadingWithZeroLength(): void { $r = fopen('php://temp', 'r'); $stream = new Stream($r); @@ -228,12 +238,12 @@ public function testStreamReadingWithZeroLength() $stream->close(); } - public function testStreamReadingWithNegativeLength() + public function testStreamReadingWithNegativeLength(): void { $r = fopen('php://temp', 'r'); $stream = new Stream($r); - - $this->expectExceptionGuzzle('RuntimeException', 'Length parameter cannot be negative'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Length parameter cannot be negative'); try { $stream->read(-1); @@ -245,13 +255,13 @@ public function testStreamReadingWithNegativeLength() $stream->close(); } - public function testStreamReadingFreadError() + public function testStreamReadingFreadError(): void { self::$isFReadError = true; $r = fopen('php://temp', 'r'); $stream = new Stream($r); - - $this->expectExceptionGuzzle('RuntimeException', 'Unable to read from stream'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to read from stream'); try { $stream->read(1); @@ -266,18 +276,12 @@ public function testStreamReadingFreadError() } /** - * @dataProvider gzipModeProvider + * @requires extension zlib * - * @param string $mode - * @param bool $readable - * @param bool $writable + * @dataProvider gzipModeProvider */ - public function testGzipStreamModes($mode, $readable, $writable) + public function testGzipStreamModes(string $mode, bool $readable, bool $writable): void { - if (defined('HHVM_VERSION')) { - self::markTestSkipped('This does not work on HHVM.'); - } - $r = gzopen('php://temp', $mode); $stream = new Stream($r); @@ -287,7 +291,7 @@ public function testGzipStreamModes($mode, $readable, $writable) $stream->close(); } - public function gzipModeProvider() + public function gzipModeProvider(): iterable { return [ ['mode' => 'rb9', 'readable' => true, 'writable' => false], @@ -297,10 +301,8 @@ public function gzipModeProvider() /** * @dataProvider readableModeProvider - * - * @param string $mode */ - public function testReadableStream($mode) + public function testReadableStream(string $mode): void { $r = fopen('php://temp', $mode); $stream = new Stream($r); @@ -310,7 +312,7 @@ public function testReadableStream($mode) $stream->close(); } - public function readableModeProvider() + public function readableModeProvider(): iterable { return [ ['r'], @@ -333,7 +335,7 @@ public function readableModeProvider() ]; } - public function testWriteOnlyStreamIsNotReadable() + public function testWriteOnlyStreamIsNotReadable(): void { $r = fopen('php://output', 'w'); $stream = new Stream($r); @@ -345,10 +347,8 @@ public function testWriteOnlyStreamIsNotReadable() /** * @dataProvider writableModeProvider - * - * @param string $mode */ - public function testWritableStream($mode) + public function testWritableStream(string $mode): void { $r = fopen('php://temp', $mode); $stream = new Stream($r); @@ -358,7 +358,7 @@ public function testWritableStream($mode) $stream->close(); } - public function writableModeProvider() + public function writableModeProvider(): iterable { return [ ['w'], @@ -382,7 +382,7 @@ public function writableModeProvider() ]; } - public function testReadOnlyStreamIsNotWritable() + public function testReadOnlyStreamIsNotWritable(): void { $r = fopen('php://input', 'r'); $stream = new Stream($r); diff --git a/tests/StreamWrapperTest.php b/tests/StreamWrapperTest.php index e6a8ae64..1f052c61 100644 --- a/tests/StreamWrapperTest.php +++ b/tests/StreamWrapperTest.php @@ -1,16 +1,20 @@ 0, - 1 => 0, - 2 => 33206, - 3 => 0, - 4 => 0, - 5 => 0, - 6 => 0, - 7 => 6, - 8 => 0, - 9 => 0, - 10 => 0, - 11 => $stBlksize, - 12 => $stBlksize, - 'dev' => 0, - 'ino' => 0, - 'mode' => 33206, - 'nlink' => 0, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => 6, - 'atime' => 0, - 'mtime' => 0, - 'ctime' => 0, - 'blksize' => $stBlksize, - 'blocks' => $stBlksize, - ], fstat($handle)); - } + self::assertEquals([ + 'dev' => 0, + 'ino' => 0, + 'mode' => 33206, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 6, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => $stBlksize, + 'blocks' => $stBlksize, + 0 => 0, + 1 => 0, + 2 => 33206, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + 7 => 6, + 8 => 0, + 9 => 0, + 10 => 0, + 11 => $stBlksize, + 12 => $stBlksize, + ], fstat($handle)); self::assertTrue(fclose($handle)); self::assertSame('foobar', (string) $stream); } - public function testStreamContext() + public function testStreamContext(): void { $stream = Psr7\Utils::streamFor('foo'); self::assertSame('foo', file_get_contents('guzzle://stream', false, StreamWrapper::createStreamContext($stream))); } - public function testStreamCast() + public function testStreamCast(): void { $streams = [ StreamWrapper::getResource(Psr7\Utils::streamFor('foo')), @@ -75,94 +76,87 @@ public function testStreamCast() ]; $write = null; $except = null; - $this->assertInternalTypeGuzzle('integer', stream_select($streams, $write, $except, 0)); + self::assertIsInt(stream_select($streams, $write, $except, 0)); } - public function testValidatesStream() + public function testValidatesStream(): void { - $stream = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isReadable', 'isWritable']) - ->getMockForAbstractClass(); + $stream = $this->createMock(StreamInterface::class); $stream->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(false)); + ->willReturn(false); $stream->expects(self::once()) ->method('isWritable') - ->will(self::returnValue(false)); - - $this->expectExceptionGuzzle('InvalidArgumentException'); + ->willReturn(false); + $this->expectException(\InvalidArgumentException::class); StreamWrapper::getResource($stream); } - public function testReturnsFalseWhenStreamDoesNotExist() + public function testReturnsFalseWhenStreamDoesNotExist(): void { - $this->expectWarningGuzzle(); - + $this->expectWarning(); fopen('guzzle://foo', 'r'); } - public function testCanOpenReadonlyStream() + public function testCanOpenReadonlyStream(): void { - $stream = $this->getMockBuilder('Psr\Http\Message\StreamInterface') - ->setMethods(['isReadable', 'isWritable']) - ->getMockForAbstractClass(); + $stream = $this->createMock(StreamInterface::class); $stream->expects(self::once()) ->method('isReadable') - ->will(self::returnValue(false)); + ->willReturn(false); $stream->expects(self::once()) ->method('isWritable') - ->will(self::returnValue(true)); + ->willReturn(true); $r = StreamWrapper::getResource($stream); - $this->assertInternalTypeGuzzle('resource', $r); + self::assertIsResource($r); fclose($r); } - public function testUrlStat() + public function testUrlStat(): void { StreamWrapper::register(); - $stBlksize = defined('PHP_WINDOWS_VERSION_BUILD') ? -1 : 0; + $stBlksize = defined('PHP_WINDOWS_VERSION_BUILD') ? -1 : 0; - self::assertSame([ - 0 => 0, - 1 => 0, - 2 => 0, - 3 => 0, - 4 => 0, - 5 => 0, - 6 => 0, - 7 => 0, - 8 => 0, - 9 => 0, - 10 => 0, - 11 => $stBlksize, - 12 => $stBlksize, - 'dev' => 0, - 'ino' => 0, - 'mode' => 0, - 'nlink' => 0, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => 0, - 'atime' => 0, - 'mtime' => 0, - 'ctime' => 0, - 'blksize' => $stBlksize, - 'blocks' => $stBlksize, - ], stat('guzzle://stream')); + self::assertEquals( + [ + 'dev' => 0, + 'ino' => 0, + 'mode' => 0, + 'nlink' => 0, + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => 0, + 'atime' => 0, + 'mtime' => 0, + 'ctime' => 0, + 'blksize' => $stBlksize, + 'blocks' => $stBlksize, + 0 => 0, + 1 => 0, + 2 => 0, + 3 => 0, + 4 => 0, + 5 => 0, + 6 => 0, + 7 => 0, + 8 => 0, + 9 => 0, + 10 => 0, + 11 => $stBlksize, + 12 => $stBlksize, + ], + stat('guzzle://stream') + ); } - public function testXmlReaderWithStream() + /** + * @requires extension xmlreader + */ + public function testXmlReaderWithStream(): void { - if (!class_exists('XMLReader')) { - self::markTestSkipped('XML Reader is not available.'); - } - if (defined('HHVM_VERSION')) { - self::markTestSkipped('This does not work on HHVM.'); - } - $stream = Psr7\Utils::streamFor(''); StreamWrapper::register(); @@ -174,15 +168,11 @@ public function testXmlReaderWithStream() self::assertSame('foo', $reader->name); } - public function testXmlWriterWithStream() + /** + * @requires extension xmlreader + */ + public function testXmlWriterWithStream(): void { - if (!class_exists('XMLWriter')) { - self::markTestSkipped('XML Writer is not available.'); - } - if (defined('HHVM_VERSION')) { - self::markTestSkipped('This does not work on HHVM.'); - } - $stream = Psr7\Utils::streamFor(fopen('php://memory', 'wb')); StreamWrapper::register(); diff --git a/tests/UploadedFileTest.php b/tests/UploadedFileTest.php index d41dee52..a162001c 100644 --- a/tests/UploadedFileTest.php +++ b/tests/UploadedFileTest.php @@ -1,30 +1,27 @@ cleanup = []; } - /** - * @after - */ - public function tearDownTest() + protected function tearDown(): void { foreach ($this->cleanup as $file) { if (is_scalar($file) && file_exists($file)) { @@ -49,91 +46,14 @@ public function invalidStreams() /** * @dataProvider invalidStreams */ - public function testRaisesExceptionOnInvalidStreamOrFile($streamOrFile) + public function testRaisesExceptionOnInvalidStreamOrFile($streamOrFile): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); new UploadedFile($streamOrFile, 0, UPLOAD_ERR_OK); } - public function invalidSizes() - { - return [ - 'null' => [null], - 'float' => [1.1], - 'array' => [[1]], - 'object' => [(object) [1]], - ]; - } - - /** - * @dataProvider invalidSizes - */ - public function testRaisesExceptionOnInvalidSize($size) - { - $this->expectExceptionGuzzle('InvalidArgumentException', 'size'); - - new UploadedFile(fopen('php://temp', 'wb+'), $size, UPLOAD_ERR_OK); - } - - public function invalidErrorStatuses() - { - return [ - 'null' => [null], - 'true' => [true], - 'false' => [false], - 'float' => [1.1], - 'string' => ['1'], - 'array' => [[1]], - 'object' => [(object) [1]], - 'negative' => [-1], - 'too-big' => [9], - ]; - } - - /** - * @dataProvider invalidErrorStatuses - */ - public function testRaisesExceptionOnInvalidErrorStatus($status) - { - $this->expectExceptionGuzzle('InvalidArgumentException', 'status'); - - new UploadedFile(fopen('php://temp', 'wb+'), 0, $status); - } - - public function invalidFilenamesAndMediaTypes() - { - return [ - 'true' => [true], - 'false' => [false], - 'int' => [1], - 'float' => [1.1], - 'array' => [['string']], - 'object' => [(object) ['string']], - ]; - } - - /** - * @dataProvider invalidFilenamesAndMediaTypes - */ - public function testRaisesExceptionOnInvalidClientFilename($filename) - { - $this->expectExceptionGuzzle('InvalidArgumentException', 'filename'); - - new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, $filename); - } - - /** - * @dataProvider invalidFilenamesAndMediaTypes - */ - public function testRaisesExceptionOnInvalidClientMediaType($mediaType) - { - $this->expectExceptionGuzzle('InvalidArgumentException', 'media type'); - - new UploadedFile(fopen('php://temp', 'wb+'), 0, UPLOAD_ERR_OK, 'foobar.baz', $mediaType); - } - - public function testGetStreamReturnsOriginalStreamObject() + public function testGetStreamReturnsOriginalStreamObject(): void { $stream = new Stream(fopen('php://temp', 'r')); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); @@ -141,7 +61,7 @@ public function testGetStreamReturnsOriginalStreamObject() self::assertSame($stream, $upload->getStream()); } - public function testGetStreamReturnsWrappedPhpStream() + public function testGetStreamReturnsWrappedPhpStream(): void { $stream = fopen('php://temp', 'wb+'); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); @@ -150,7 +70,7 @@ public function testGetStreamReturnsWrappedPhpStream() self::assertSame($stream, $uploadStream); } - public function testGetStreamReturnsStreamForFile() + public function testGetStreamReturnsStreamForFile(): void { $this->cleanup[] = $stream = tempnam(sys_get_temp_dir(), 'stream_file'); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); @@ -161,7 +81,7 @@ public function testGetStreamReturnsStreamForFile() self::assertSame($stream, $r->getValue($uploadStream)); } - public function testSuccessful() + public function testSuccessful(): void { $stream = \GuzzleHttp\Psr7\Utils::streamFor('Foo bar!'); $upload = new UploadedFile($stream, $stream->getSize(), UPLOAD_ERR_OK, 'filename.txt', 'text/plain'); @@ -176,7 +96,7 @@ public function testSuccessful() self::assertSame($stream->__toString(), file_get_contents($to)); } - public function invalidMovePaths() + public function invalidMovePaths(): iterable { return [ 'null' => [null], @@ -193,18 +113,17 @@ public function invalidMovePaths() /** * @dataProvider invalidMovePaths */ - public function testMoveRaisesExceptionForInvalidPath($path) + public function testMoveRaisesExceptionForInvalidPath($path): void { $stream = \GuzzleHttp\Psr7\Utils::streamFor('Foo bar!'); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); - $this->cleanup[] = $path; - - $this->expectExceptionGuzzle('InvalidArgumentException', 'path'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('path'); $upload->moveTo($path); } - public function testMoveCannotBeCalledMoreThanOnce() + public function testMoveCannotBeCalledMoreThanOnce(): void { $stream = \GuzzleHttp\Psr7\Utils::streamFor('Foo bar!'); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); @@ -213,11 +132,12 @@ public function testMoveCannotBeCalledMoreThanOnce() $upload->moveTo($to); self::assertFileExists($to); - $this->expectExceptionGuzzle('RuntimeException', 'moved'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('moved'); $upload->moveTo($to); } - public function testCannotRetrieveStreamAfterMove() + public function testCannotRetrieveStreamAfterMove(): void { $stream = \GuzzleHttp\Psr7\Utils::streamFor('Foo bar!'); $upload = new UploadedFile($stream, 0, UPLOAD_ERR_OK); @@ -226,11 +146,12 @@ public function testCannotRetrieveStreamAfterMove() $upload->moveTo($to); self::assertFileExists($to); - $this->expectExceptionGuzzle('RuntimeException', 'moved'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('moved'); $upload->getStream(); } - public function nonOkErrorStatus() + public function nonOkErrorStatus(): iterable { return [ 'UPLOAD_ERR_INI_SIZE' => [ UPLOAD_ERR_INI_SIZE ], @@ -246,7 +167,7 @@ public function nonOkErrorStatus() /** * @dataProvider nonOkErrorStatus */ - public function testConstructorDoesNotRaiseExceptionForInvalidStreamWhenErrorStatusPresent($status) + public function testConstructorDoesNotRaiseExceptionForInvalidStreamWhenErrorStatusPresent($status): void { $uploadedFile = new UploadedFile('not ok', 0, $status); self::assertSame($status, $uploadedFile->getError()); @@ -255,24 +176,26 @@ public function testConstructorDoesNotRaiseExceptionForInvalidStreamWhenErrorSta /** * @dataProvider nonOkErrorStatus */ - public function testMoveToRaisesExceptionWhenErrorStatusPresent($status) + public function testMoveToRaisesExceptionWhenErrorStatusPresent($status): void { $uploadedFile = new UploadedFile('not ok', 0, $status); - $this->expectExceptionGuzzle('RuntimeException', 'upload error'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('upload error'); $uploadedFile->moveTo(__DIR__ . '/' . sha1(uniqid('', true))); } /** * @dataProvider nonOkErrorStatus */ - public function testGetStreamRaisesExceptionWhenErrorStatusPresent($status) + public function testGetStreamRaisesExceptionWhenErrorStatusPresent($status): void { $uploadedFile = new UploadedFile('not ok', 0, $status); - $this->expectExceptionGuzzle('RuntimeException', 'upload error'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('upload error'); $uploadedFile->getStream(); } - public function testMoveToCreatesStreamIfOnlyAFilenameWasProvided() + public function testMoveToCreatesStreamIfOnlyAFilenameWasProvided(): void { $this->cleanup[] = $from = tempnam(sys_get_temp_dir(), 'copy_from'); $this->cleanup[] = $to = tempnam(sys_get_temp_dir(), 'copy_to'); diff --git a/tests/UriNormalizerTest.php b/tests/UriNormalizerTest.php index e284558b..741c17d1 100644 --- a/tests/UriNormalizerTest.php +++ b/tests/UriNormalizerTest.php @@ -1,16 +1,20 @@ getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); - $uri->expects(self::any())->method('getScheme')->will(self::returnValue('http')); - $uri->expects(self::any())->method('getPort')->will(self::returnValue(80)); - $uri->expects(self::once())->method('withPort')->with(null)->will(self::returnValue(new Uri('http://example.org'))); + $uri = $this->createMock(UriInterface::class); + $uri->expects(self::any())->method('getScheme')->willReturn('http'); + $uri->expects(self::any())->method('getPort')->willReturn(80); + $uri->expects(self::once())->method('withPort')->with(null)->willReturn(new Uri('http://example.org')); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::REMOVE_DEFAULT_PORT); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertNull($normalizedUri->getPort()); } - public function testRemoveDotSegments() + public function testRemoveDotSegments(): void { $uri = new Uri('http://example.org/../a/b/../c/./d.html'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::REMOVE_DOT_SEGMENTS); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('http://example.org/a/c/d.html', (string) $normalizedUri); } - public function testRemoveDotSegmentsOfAbsolutePathReference() + public function testRemoveDotSegmentsOfAbsolutePathReference(): void { $uri = new Uri('/../a/b/../c/./d.html'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::REMOVE_DOT_SEGMENTS); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('/a/c/d.html', (string) $normalizedUri); } - public function testRemoveDotSegmentsOfRelativePathReference() + public function testRemoveDotSegmentsOfRelativePathReference(): void { $uri = new Uri('../c/./d.html'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::REMOVE_DOT_SEGMENTS); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('../c/./d.html', (string) $normalizedUri); } - public function testRemoveDuplicateSlashes() + public function testRemoveDuplicateSlashes(): void { $uri = new Uri('http://example.org//foo///bar/bam.html'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::REMOVE_DUPLICATE_SLASHES); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('http://example.org/foo/bar/bam.html', (string) $normalizedUri); } - public function testSortQueryParameters() + public function testSortQueryParameters(): void { $uri = new Uri('?lang=en&article=fred'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::SORT_QUERY_PARAMETERS); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('?article=fred&lang=en', (string) $normalizedUri); } - public function testSortQueryParametersWithSameKeys() + public function testSortQueryParametersWithSameKeys(): void { $uri = new Uri('?a=b&b=c&a=a&a&b=a&b=b&a=d&a=c'); $normalizedUri = UriNormalizer::normalize($uri, UriNormalizer::SORT_QUERY_PARAMETERS); - self::assertInstanceOf('Psr\Http\Message\UriInterface', $normalizedUri); + self::assertInstanceOf(UriInterface::class, $normalizedUri); self::assertSame('?a&a=a&a=b&a=c&a=d&b=a&b=b&b=c', (string) $normalizedUri); } /** * @dataProvider getEquivalentTestCases */ - public function testIsEquivalent($uri1, $uri2, $expected) + public function testIsEquivalent(string $uri1, string $uri2, bool $expected): void { $equivalent = UriNormalizer::isEquivalent(new Uri($uri1), new Uri($uri2)); self::assertSame($expected, $equivalent); } - public function getEquivalentTestCases() + public function getEquivalentTestCases(): iterable { return [ ['http://example.org', 'http://example.org', true], diff --git a/tests/UriResolverTest.php b/tests/UriResolverTest.php index 6f41409b..9381b7f7 100644 --- a/tests/UriResolverTest.php +++ b/tests/UriResolverTest.php @@ -1,26 +1,30 @@ withScheme('https') @@ -49,7 +54,7 @@ public function testCanTransformAndRetrievePartsIndividually() /** * @dataProvider getValidUris */ - public function testValidUrisStayValid($input) + public function testValidUrisStayValid(string $input): void { $uri = new Uri($input); @@ -59,14 +64,14 @@ public function testValidUrisStayValid($input) /** * @dataProvider getValidUris */ - public function testFromParts($input) + public function testFromParts(string $input): void { $uri = Uri::fromParts(parse_url($input)); self::assertSame($input, (string) $uri); } - public function getValidUris() + public function getValidUris(): iterable { return [ ['urn:path-rootless'], @@ -98,14 +103,13 @@ public function getValidUris() /** * @dataProvider getInvalidUris */ - public function testInvalidUrisThrowException($invalidUri) + public function testInvalidUrisThrowException(string $invalidUri): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Unable to parse URI'); - + $this->expectException(MalformedUriException::class); new Uri($invalidUri); } - public function getInvalidUris() + public function getInvalidUris(): iterable { return [ // parse_url() requires the host component which makes sense for http(s) @@ -116,63 +120,56 @@ public function getInvalidUris() ]; } - public function testPortMustBeValid() + public function testPortMustBeValid(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Must be between 0 and 65535'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid port: 100000. Must be between 0 and 65535'); (new Uri())->withPort(100000); } - public function testWithPortCannotBeNegative() + public function testWithPortCannotBeNegative(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Invalid port: -1. Must be between 0 and 65535'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid port: -1. Must be between 0 and 65535'); (new Uri())->withPort(-1); } - public function testParseUriPortCannotBeNegative() + public function testParseUriPortCannotBeNegative(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'Unable to parse URI'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Unable to parse URI'); new Uri('//example.com:-1'); } - public function testSchemeMustHaveCorrectType() + public function testSchemeMustHaveCorrectType(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); (new Uri())->withScheme([]); } - public function testHostMustHaveCorrectType() + public function testHostMustHaveCorrectType(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); (new Uri())->withHost([]); } - - public function testPathMustHaveCorrectType() + public function testPathMustHaveCorrectType(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); (new Uri())->withPath([]); } - - public function testQueryMustHaveCorrectType() + public function testQueryMustHaveCorrectType(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); (new Uri())->withQuery([]); } - public function testFragmentMustHaveCorrectType() + public function testFragmentMustHaveCorrectType(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); - + $this->expectException(\InvalidArgumentException::class); (new Uri())->withFragment([]); } - public function testCanParseFalseyUriParts() + public function testCanParseFalseyUriParts(): void { $uri = new Uri('0://0:0@0/0?0#0'); @@ -186,7 +183,7 @@ public function testCanParseFalseyUriParts() self::assertSame('0://0:0@0/0?0#0', (string) $uri); } - public function testCanConstructFalseyUriParts() + public function testCanConstructFalseyUriParts(): void { $uri = (new Uri()) ->withScheme('0') @@ -209,16 +206,16 @@ public function testCanConstructFalseyUriParts() /** * @dataProvider getPortTestCases */ - public function testIsDefaultPort($scheme, $port, $isDefaultPort) + public function testIsDefaultPort(string $scheme, ?int $port, bool $isDefaultPort): void { - $uri = $this->getMockBuilder('Psr\Http\Message\UriInterface')->getMock(); - $uri->expects(self::any())->method('getScheme')->will(self::returnValue($scheme)); - $uri->expects(self::any())->method('getPort')->will(self::returnValue($port)); + $uri = $this->createMock(UriInterface::class); + $uri->expects(self::any())->method('getScheme')->willReturn($scheme); + $uri->expects(self::any())->method('getPort')->willReturn($port); self::assertSame($isDefaultPort, Uri::isDefaultPort($uri)); } - public function getPortTestCases() + public function getPortTestCases(): iterable { return [ ['http', null, true], @@ -239,7 +236,7 @@ public function getPortTestCases() ]; } - public function testIsAbsolute() + public function testIsAbsolute(): void { self::assertTrue(Uri::isAbsolute(new Uri('http://example.org'))); self::assertFalse(Uri::isAbsolute(new Uri('//example.org'))); @@ -247,7 +244,7 @@ public function testIsAbsolute() self::assertFalse(Uri::isAbsolute(new Uri('rel-path'))); } - public function testIsNetworkPathReference() + public function testIsNetworkPathReference(): void { self::assertFalse(Uri::isNetworkPathReference(new Uri('http://example.org'))); self::assertTrue(Uri::isNetworkPathReference(new Uri('//example.org'))); @@ -255,7 +252,7 @@ public function testIsNetworkPathReference() self::assertFalse(Uri::isNetworkPathReference(new Uri('rel-path'))); } - public function testIsAbsolutePathReference() + public function testIsAbsolutePathReference(): void { self::assertFalse(Uri::isAbsolutePathReference(new Uri('http://example.org'))); self::assertFalse(Uri::isAbsolutePathReference(new Uri('//example.org'))); @@ -264,7 +261,7 @@ public function testIsAbsolutePathReference() self::assertFalse(Uri::isAbsolutePathReference(new Uri('rel-path'))); } - public function testIsRelativePathReference() + public function testIsRelativePathReference(): void { self::assertFalse(Uri::isRelativePathReference(new Uri('http://example.org'))); self::assertFalse(Uri::isRelativePathReference(new Uri('//example.org'))); @@ -273,7 +270,7 @@ public function testIsRelativePathReference() self::assertTrue(Uri::isRelativePathReference(new Uri(''))); } - public function testIsSameDocumentReference() + public function testIsSameDocumentReference(): void { self::assertFalse(Uri::isSameDocumentReference(new Uri('http://example.org'))); self::assertFalse(Uri::isSameDocumentReference(new Uri('//example.org'))); @@ -300,7 +297,7 @@ public function testIsSameDocumentReference() self::assertFalse(Uri::isSameDocumentReference(new Uri('urn:/path'), new Uri('urn://example.com/path'))); } - public function testAddAndRemoveQueryValues() + public function testAddAndRemoveQueryValues(): void { $uri = new Uri(); $uri = Uri::withQueryValue($uri, 'a', 'b'); @@ -316,13 +313,20 @@ public function testAddAndRemoveQueryValues() self::assertSame('', $uri->getQuery()); } - public function testNumericQueryValue() + public function testScalarQueryValues(): void { - $uri = Uri::withQueryValue(new Uri(), 'version', 1); - self::assertSame('version=1', $uri->getQuery()); + $uri = new Uri(); + $uri = Uri::withQueryValues($uri, [ + 2 => 2, + 1 => true, + 'false' => false, + 'float' => 3.1 + ]); + + self::assertSame('2=2&1=1&false=&float=3.1', $uri->getQuery()); } - public function testWithQueryValues() + public function testWithQueryValues(): void { $uri = new Uri(); $uri = Uri::withQueryValues($uri, [ @@ -333,7 +337,7 @@ public function testWithQueryValues() self::assertSame('key1=value1&key2=value2', $uri->getQuery()); } - public function testWithQueryValuesReplacesSameKeys() + public function testWithQueryValuesReplacesSameKeys(): void { $uri = new Uri(); @@ -349,7 +353,7 @@ public function testWithQueryValuesReplacesSameKeys() self::assertSame('key1=value1&key2=newvalue', $uri->getQuery()); } - public function testWithQueryValueReplacesSameKeys() + public function testWithQueryValueReplacesSameKeys(): void { $uri = new Uri(); $uri = Uri::withQueryValue($uri, 'a', 'b'); @@ -358,14 +362,14 @@ public function testWithQueryValueReplacesSameKeys() self::assertSame('c=d&a=e', $uri->getQuery()); } - public function testWithoutQueryValueRemovesAllSameKeys() + public function testWithoutQueryValueRemovesAllSameKeys(): void { $uri = (new Uri())->withQuery('a=b&c=d&a=e'); $uri = Uri::withoutQueryValue($uri, 'a'); self::assertSame('c=d', $uri->getQuery()); } - public function testRemoveNonExistingQueryValue() + public function testRemoveNonExistingQueryValue(): void { $uri = new Uri(); $uri = Uri::withQueryValue($uri, 'a', 'b'); @@ -373,7 +377,7 @@ public function testRemoveNonExistingQueryValue() self::assertSame('a=b', $uri->getQuery()); } - public function testWithQueryValueHandlesEncoding() + public function testWithQueryValueHandlesEncoding(): void { $uri = new Uri(); $uri = Uri::withQueryValue($uri, 'E=mc^2', 'ein&stein'); @@ -384,7 +388,7 @@ public function testWithQueryValueHandlesEncoding() self::assertSame('E%3Dmc%5e2=ein%26stein', $uri->getQuery(), 'Encoded key/value do not get double-encoded'); } - public function testWithoutQueryValueHandlesEncoding() + public function testWithoutQueryValueHandlesEncoding(): void { // It also tests that the case of the percent-encoding does not matter, // i.e. both lowercase "%3d" and uppercase "%5E" can be removed. @@ -397,7 +401,7 @@ public function testWithoutQueryValueHandlesEncoding() self::assertSame('foo=bar', $uri->getQuery(), 'Handles key in encoded form'); } - public function testSchemeIsNormalizedToLowercase() + public function testSchemeIsNormalizedToLowercase(): void { $uri = new Uri('HTTP://example.com'); @@ -410,7 +414,7 @@ public function testSchemeIsNormalizedToLowercase() self::assertSame('http://example.com', (string) $uri); } - public function testHostIsNormalizedToLowercase() + public function testHostIsNormalizedToLowercase(): void { $uri = new Uri('//eXaMpLe.CoM'); @@ -423,7 +427,7 @@ public function testHostIsNormalizedToLowercase() self::assertSame('//example.com', (string) $uri); } - public function testPortIsNullIfStandardPortForScheme() + public function testPortIsNullIfStandardPortForScheme(): void { // HTTPS standard port $uri = new Uri('https://example.com:443'); @@ -444,7 +448,7 @@ public function testPortIsNullIfStandardPortForScheme() self::assertSame('example.com', $uri->getAuthority()); } - public function testPortIsReturnedIfSchemeUnknown() + public function testPortIsReturnedIfSchemeUnknown(): void { $uri = (new Uri('//example.com'))->withPort(80); @@ -452,7 +456,7 @@ public function testPortIsReturnedIfSchemeUnknown() self::assertSame('example.com:80', $uri->getAuthority()); } - public function testStandardPortIsNullIfSchemeChanges() + public function testStandardPortIsNullIfSchemeChanges(): void { $uri = new Uri('http://example.com:443'); self::assertSame('http', $uri->getScheme()); @@ -462,7 +466,7 @@ public function testStandardPortIsNullIfSchemeChanges() self::assertNull($uri->getPort()); } - public function testPortPassedAsStringIsCastedToInt() + public function testPortPassedAsStringIsCastedToInt(): void { $uri = (new Uri('//example.com'))->withPort('8080'); @@ -470,7 +474,7 @@ public function testPortPassedAsStringIsCastedToInt() self::assertSame('example.com:8080', $uri->getAuthority()); } - public function testPortCanBeRemoved() + public function testPortCanBeRemoved(): void { $uri = (new Uri('http://example.com:8080'))->withPort(null); @@ -482,7 +486,7 @@ public function testPortCanBeRemoved() * In RFC 8986 the host is optional and the authority can only * consist of the user info and port. */ - public function testAuthorityWithUserInfoOrPortButWithoutHost() + public function testAuthorityWithUserInfoOrPortButWithoutHost(): void { $uri = (new Uri())->withUserInfo('user', 'pass'); @@ -498,7 +502,7 @@ public function testAuthorityWithUserInfoOrPortButWithoutHost() self::assertSame(':8080', $uri->getAuthority()); } - public function testHostInHttpUriDefaultsToLocalhost() + public function testHostInHttpUriDefaultsToLocalhost(): void { $uri = (new Uri())->withScheme('http'); @@ -507,7 +511,7 @@ public function testHostInHttpUriDefaultsToLocalhost() self::assertSame('http://localhost', (string) $uri); } - public function testHostInHttpsUriDefaultsToLocalhost() + public function testHostInHttpsUriDefaultsToLocalhost(): void { $uri = (new Uri())->withScheme('https'); @@ -516,7 +520,7 @@ public function testHostInHttpsUriDefaultsToLocalhost() self::assertSame('https://localhost', (string) $uri); } - public function testFileSchemeWithEmptyHostReconstruction() + public function testFileSchemeWithEmptyHostReconstruction(): void { $uri = new Uri('file:///tmp/filename.ext'); @@ -525,7 +529,7 @@ public function testFileSchemeWithEmptyHostReconstruction() self::assertSame('file:///tmp/filename.ext', (string) $uri); } - public function uriComponentsEncodingProvider() + public function uriComponentsEncodingProvider(): iterable { $unreserved = 'a-zA-Z0-9.-_~!$&\'()*+,;=:@'; @@ -550,7 +554,7 @@ public function uriComponentsEncodingProvider() /** * @dataProvider uriComponentsEncodingProvider */ - public function testUriComponentsGetEncodedProperly($input, $path, $query, $fragment, $output) + public function testUriComponentsGetEncodedProperly(string $input, string $path, string $query, string $fragment, string $output): void { $uri = new Uri($input); self::assertSame($path, $uri->getPath()); @@ -559,7 +563,7 @@ public function testUriComponentsGetEncodedProperly($input, $path, $query, $frag self::assertSame($output, (string) $uri); } - public function testWithPathEncodesProperly() + public function testWithPathEncodesProperly(): void { $uri = (new Uri())->withPath('/baz?#€/b%61r'); // Query and fragment delimiters and multibyte chars are encoded. @@ -567,7 +571,7 @@ public function testWithPathEncodesProperly() self::assertSame('/baz%3F%23%E2%82%AC/b%61r', (string) $uri); } - public function testWithQueryEncodesProperly() + public function testWithQueryEncodesProperly(): void { $uri = (new Uri())->withQuery('?=#&€=/&b%61r'); // A query starting with a "?" is valid and must not be magically removed. Otherwise it would be impossible to @@ -576,7 +580,7 @@ public function testWithQueryEncodesProperly() self::assertSame('??=%23&%E2%82%AC=/&b%61r', (string) $uri); } - public function testWithFragmentEncodesProperly() + public function testWithFragmentEncodesProperly(): void { $uri = (new Uri())->withFragment('#€?/b%61r'); // A fragment starting with a "#" is valid and must not be magically removed. Otherwise it would be impossible to @@ -585,57 +589,57 @@ public function testWithFragmentEncodesProperly() self::assertSame('#%23%E2%82%AC?/b%61r', (string) $uri); } - public function testAllowsForRelativeUri() + public function testAllowsForRelativeUri(): void { $uri = (new Uri)->withPath('foo'); self::assertSame('foo', $uri->getPath()); self::assertSame('foo', (string) $uri); } - public function testRelativePathAndAuhorityIsAutomagicallyFixed() + public function testRelativePathAndAuthorityThrowsException(): void { // concatenating a relative path with a host doesn't work: "//example.comfoo" would be wrong - $uri = (new Uri)->withPath('foo')->withHost('example.com'); - self::assertSame('/foo', $uri->getPath()); - self::assertSame('//example.com/foo', (string) $uri); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The path of a URI with an authority must start with a slash "/" or be empty'); + (new Uri)->withHost('example.com')->withPath('foo'); } - public function testPathStartingWithTwoSlashesAndNoAuthorityIsInvalid() + public function testPathStartingWithTwoSlashesAndNoAuthorityIsInvalid(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'The path of a URI without an authority must not start with two slashes "//"'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The path of a URI without an authority must not start with two slashes "//"'); // URI "//foo" would be interpreted as network reference and thus change the original path to the host (new Uri)->withPath('//foo'); } - public function testPathStartingWithTwoSlashes() + public function testPathStartingWithTwoSlashes(): void { $uri = new Uri('http://example.org//path-not-host.com'); self::assertSame('//path-not-host.com', $uri->getPath()); $uri = $uri->withScheme(''); self::assertSame('//example.org//path-not-host.com', (string) $uri); // This is still valid - $this->expectExceptionGuzzle('\InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); $uri->withHost(''); // Now it becomes invalid } - public function testRelativeUriWithPathBeginngWithColonSegmentIsInvalid() + public function testRelativeUriWithPathBeginngWithColonSegmentIsInvalid(): void { - $this->expectExceptionGuzzle('InvalidArgumentException', 'A relative URI must not have a path beginning with a segment containing a colon'); - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('A relative URI must not have a path beginning with a segment containing a colon'); (new Uri)->withPath('mailto:foo'); } - public function testRelativeUriWithPathHavingColonSegment() + public function testRelativeUriWithPathHavingColonSegment(): void { $uri = (new Uri('urn:/mailto:foo'))->withScheme(''); self::assertSame('/mailto:foo', $uri->getPath()); - $this->expectExceptionGuzzle('\InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); (new Uri('urn:mailto:foo'))->withScheme(''); } - public function testDefaultReturnValuesOfGetters() + public function testDefaultReturnValuesOfGetters(): void { $uri = new Uri(); @@ -649,7 +653,7 @@ public function testDefaultReturnValuesOfGetters() self::assertSame('', $uri->getFragment()); } - public function testImmutability() + public function testImmutability(): void { $uri = new Uri(); @@ -662,18 +666,18 @@ public function testImmutability() self::assertNotSame($uri, $uri->withFragment('test')); } - public function testExtendingClassesInstantiates() + public function testExtendingClassesInstantiates(): void { // The non-standard port triggers a cascade of private methods which // should not use late static binding to access private static members. // If they do, this will fatal. self::assertInstanceOf( - 'GuzzleHttp\Tests\Psr7\ExtendedUriTest', + ExtendedUriTest::class, new ExtendedUriTest('http://h:9/') ); } - public function testSpecialCharsOfUserInfo() + public function testSpecialCharsOfUserInfo(): void { // The `userInfo` must always be URL-encoded. $uri = (new Uri)->withUserInfo('foo@bar.com', 'pass#word'); @@ -684,7 +688,7 @@ public function testSpecialCharsOfUserInfo() self::assertSame('foo%40bar.com:pass%23word', $uri->getUserInfo()); } - public function testInternationalizedDomainName() + public function testInternationalizedDomainName(): void { $uri = new Uri('https://яндекс.рф'); self::assertSame('яндекс.рф', $uri->getHost()); @@ -693,7 +697,7 @@ public function testInternationalizedDomainName() self::assertSame('яндекaс.рф', $uri->getHost()); } - public function testIPv6Host() + public function testIPv6Host(): void { $uri = new Uri('https://[2a00:f48:1008::212:183:10]'); self::assertSame('[2a00:f48:1008::212:183:10]', $uri->getHost()); @@ -703,6 +707,13 @@ public function testIPv6Host() self::assertSame(56, $uri->getPort()); self::assertSame('foo=bar', $uri->getQuery()); } + + public function testJsonSerializable(): void + { + $uri = new Uri('https://example.com'); + + self::assertSame('{"uri":"https:\/\/example.com"}', \json_encode(['uri'=> $uri])); + } } class ExtendedUriTest extends Uri diff --git a/tests/UtilsTest.php b/tests/UtilsTest.php index 4f62e3a3..f2d3dcac 100644 --- a/tests/UtilsTest.php +++ b/tests/UtilsTest.php @@ -1,14 +1,18 @@ getMockBuilder('GuzzleHttp\Psr7\Stream') - ->setMethods(['read', 'eof']) - ->disableOriginalConstructor() - ->getMock(); + $s = $this->createMock(StreamInterface::class); $s->expects(self::exactly(2)) ->method('read') - ->will(self::returnCallback(function () { - static $c = false; - if ($c) { - return false; + ->willReturnCallback(function () { + static $called = false; + if ($called) { + return ''; } - $c = true; + $called = true; + return 'h'; - })); + }); $s->expects(self::exactly(2)) ->method('eof') - ->will(self::returnValue(false)); + ->willReturn(false); self::assertSame('h', Psr7\Utils::readLine($s)); } - public function testCalculatesHash() + public function testCalculatesHash(): void { $s = Psr7\Utils::streamFor('foobazbar'); self::assertSame(md5('foobazbar'), Psr7\Utils::hash($s, 'md5')); } - public function testCalculatesHashThrowsWhenSeekFails() + public function testCalculatesHashThrowsWhenSeekFails(): void { $s = new NoSeekStream(Psr7\Utils::streamFor('foobazbar')); $s->read(2); - $this->expectExceptionGuzzle('RuntimeException'); + $this->expectException(\RuntimeException::class); Psr7\Utils::hash($s, 'md5'); } - public function testCalculatesHashSeeksToOriginalPosition() + public function testCalculatesHashSeeksToOriginalPosition(): void { $s = Psr7\Utils::streamFor('foobazbar'); $s->seek(4); @@ -175,28 +177,30 @@ public function testCalculatesHashSeeksToOriginalPosition() self::assertSame(4, $s->tell()); } - public function testOpensFilesSuccessfully() + public function testOpensFilesSuccessfully(): void { $r = Psr7\Utils::tryFopen(__FILE__, 'r'); - $this->assertInternalTypeGuzzle('resource', $r); + self::assertIsResource($r); fclose($r); } - public function testThrowsExceptionNotWarning() + public function testThrowsExceptionNotWarning(): void { - $this->expectExceptionGuzzle('RuntimeException', 'Unable to open "/path/to/does/not/exist" using mode "r"'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to open "/path/to/does/not/exist" using mode "r"'); Psr7\Utils::tryFopen('/path/to/does/not/exist', 'r'); } - public function testThrowsExceptionNotValueError() + public function testThrowsExceptionNotValueError(): void { - $this->expectExceptionGuzzle('RuntimeException', 'Unable to open "" using mode "r"'); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to open "" using mode "r"'); Psr7\Utils::tryFopen('', 'r'); } - public function testCreatesUriForValue() + public function testCreatesUriForValue(): void { self::assertInstanceOf('GuzzleHttp\Psr7\Uri', Psr7\Utils::uriFor('/foo')); self::assertInstanceOf( @@ -205,14 +209,14 @@ public function testCreatesUriForValue() ); } - public function testValidatesUri() + public function testValidatesUri(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Psr7\Utils::uriFor([]); } - public function testKeepsPositionOfResource() + public function testKeepsPositionOfResource(): void { $h = fopen(__FILE__, 'r'); fseek($h, 10); @@ -221,7 +225,7 @@ public function testKeepsPositionOfResource() $stream->close(); } - public function testCreatesWithFactory() + public function testCreatesWithFactory(): void { $stream = Psr7\Utils::streamFor('foo'); self::assertInstanceOf('GuzzleHttp\Psr7\Stream', $stream); @@ -229,19 +233,19 @@ public function testCreatesWithFactory() $stream->close(); } - public function testFactoryCreatesFromEmptyString() + public function testFactoryCreatesFromEmptyString(): void { $s = Psr7\Utils::streamFor(); self::assertInstanceOf('GuzzleHttp\Psr7\Stream', $s); } - public function testFactoryCreatesFromNull() + public function testFactoryCreatesFromNull(): void { $s = Psr7\Utils::streamFor(null); self::assertInstanceOf('GuzzleHttp\Psr7\Stream', $s); } - public function testFactoryCreatesFromResource() + public function testFactoryCreatesFromResource(): void { $r = fopen(__FILE__, 'r'); $s = Psr7\Utils::streamFor($r); @@ -249,7 +253,7 @@ public function testFactoryCreatesFromResource() self::assertSame(file_get_contents(__FILE__), (string)$s); } - public function testFactoryCreatesFromObjectWithToString() + public function testFactoryCreatesFromObjectWithToString(): void { $r = new HasToString(); $s = Psr7\Utils::streamFor($r); @@ -257,33 +261,33 @@ public function testFactoryCreatesFromObjectWithToString() self::assertSame('foo', (string)$s); } - public function testCreatePassesThrough() + public function testCreatePassesThrough(): void { $s = Psr7\Utils::streamFor('foo'); self::assertSame($s, Psr7\Utils::streamFor($s)); } - public function testThrowsExceptionForUnknown() + public function testThrowsExceptionForUnknown(): void { - $this->expectExceptionGuzzle('InvalidArgumentException'); + $this->expectException(\InvalidArgumentException::class); Psr7\Utils::streamFor(new \stdClass()); } - public function testReturnsCustomMetadata() + public function testReturnsCustomMetadata(): void { $s = Psr7\Utils::streamFor('foo', ['metadata' => ['hwm' => 3]]); self::assertSame(3, $s->getMetadata('hwm')); self::assertArrayHasKey('hwm', $s->getMetadata()); } - public function testCanSetSize() + public function testCanSetSize(): void { $s = Psr7\Utils::streamFor('', ['size' => 10]); self::assertSame(10, $s->getSize()); } - public function testCanCreateIteratorBasedStream() + public function testCanCreateIteratorBasedStream(): void { $a = new \ArrayIterator(['foo', 'bar', '123']); $p = Psr7\Utils::streamFor($a); @@ -299,7 +303,7 @@ public function testCanCreateIteratorBasedStream() self::assertSame(9, $p->tell()); } - public function testConvertsRequestsToStrings() + public function testConvertsRequestsToStrings(): void { $request = new Psr7\Request('PUT', 'http://foo.com/hi?123', [ 'Baz' => 'bar', @@ -311,7 +315,7 @@ public function testConvertsRequestsToStrings() ); } - public function testConvertsResponsesToStrings() + public function testConvertsResponsesToStrings(): void { $response = new Psr7\Response(200, [ 'Baz' => 'bar', @@ -323,7 +327,7 @@ public function testConvertsResponsesToStrings() ); } - public function testCorrectlyRendersSetCookieHeadersToString() + public function testCorrectlyRendersSetCookieHeadersToString(): void { $response = new Psr7\Response(200, [ 'Set-Cookie' => ['bar','baz','qux'] @@ -334,7 +338,7 @@ public function testCorrectlyRendersSetCookieHeadersToString() ); } - public function testCanModifyRequestWithUri() + public function testCanModifyRequestWithUri(): void { $r1 = new Psr7\Request('GET', 'http://foo.com'); $r2 = Psr7\Utils::modifyRequest($r1, [ @@ -344,7 +348,7 @@ public function testCanModifyRequestWithUri() self::assertSame('www.foo.com', (string)$r2->getHeaderLine('host')); } - public function testCanModifyRequestWithUriAndPort() + public function testCanModifyRequestWithUriAndPort(): void { $r1 = new Psr7\Request('GET', 'http://foo.com:8000'); $r2 = Psr7\Utils::modifyRequest($r1, [ @@ -354,7 +358,7 @@ public function testCanModifyRequestWithUriAndPort() self::assertSame('www.foo.com:8000', (string)$r2->getHeaderLine('host')); } - public function testCanModifyRequestWithCaseInsensitiveHeader() + public function testCanModifyRequestWithCaseInsensitiveHeader(): void { $r1 = new Psr7\Request('GET', 'http://foo.com', ['User-Agent' => 'foo']); $r2 = Psr7\Utils::modifyRequest($r1, ['set_headers' => ['User-agent' => 'bar']]); @@ -362,7 +366,7 @@ public function testCanModifyRequestWithCaseInsensitiveHeader() self::assertSame('bar', $r2->getHeaderLine('User-agent')); } - public function testReturnsAsIsWhenNoChanges() + public function testReturnsAsIsWhenNoChanges(): void { $r1 = new Psr7\Request('GET', 'http://foo.com'); $r2 = Psr7\Utils::modifyRequest($r1, []); @@ -373,7 +377,7 @@ public function testReturnsAsIsWhenNoChanges() self::assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $r2); } - public function testReturnsUriAsIsWhenNoChanges() + public function testReturnsUriAsIsWhenNoChanges(): void { $r1 = new Psr7\Request('GET', 'http://foo.com'); $r2 = Psr7\Utils::modifyRequest($r1, ['set_headers' => ['foo' => 'bar']]); @@ -381,7 +385,7 @@ public function testReturnsUriAsIsWhenNoChanges() self::assertSame('bar', $r2->getHeaderLine('foo')); } - public function testRemovesHeadersFromMessage() + public function testRemovesHeadersFromMessage(): void { $r1 = new Psr7\Request('GET', 'http://foo.com', ['foo' => 'bar']); $r2 = Psr7\Utils::modifyRequest($r1, ['remove_headers' => ['foo']]); @@ -389,7 +393,7 @@ public function testRemovesHeadersFromMessage() self::assertFalse($r2->hasHeader('foo')); } - public function testAddsQueryToUri() + public function testAddsQueryToUri(): void { $r1 = new Psr7\Request('GET', 'http://foo.com'); $r2 = Psr7\Utils::modifyRequest($r1, ['query' => 'foo=bar']); @@ -397,7 +401,7 @@ public function testAddsQueryToUri() self::assertSame('foo=bar', $r2->getUri()->getQuery()); } - public function testModifyRequestKeepInstanceOfRequest() + public function testModifyRequestKeepInstanceOfRequest(): void { $r1 = new Psr7\Request('GET', 'http://foo.com'); $r2 = Psr7\Utils::modifyRequest($r1, ['remove_headers' => ['non-existent']]); @@ -408,7 +412,7 @@ public function testModifyRequestKeepInstanceOfRequest() self::assertInstanceOf('Psr\Http\Message\ServerRequestInterface', $r2); } - public function testModifyServerRequestWithUploadedFiles() + public function testModifyServerRequestWithUploadedFiles(): void { $request = new Psr7\ServerRequest('GET', 'http://example.com/bla'); $file = new Psr7\UploadedFile('Test', 100, \UPLOAD_ERR_OK); @@ -423,7 +427,7 @@ public function testModifyServerRequestWithUploadedFiles() self::assertInstanceOf('GuzzleHttp\Psr7\UploadedFile', $files[0]); } - public function testModifyServerRequestWithCookies() + public function testModifyServerRequestWithCookies(): void { $request = (new Psr7\ServerRequest('GET', 'http://example.com/bla')) ->withCookieParams(['name' => 'value']); @@ -434,7 +438,7 @@ public function testModifyServerRequestWithCookies() self::assertSame(['name' => 'value'], $modifiedRequest->getCookieParams()); } - public function testModifyServerRequestParsedBody() + public function testModifyServerRequestParsedBody(): void { $request = (new Psr7\ServerRequest('GET', 'http://example.com/bla')) ->withParsedBody(['name' => 'value']); @@ -445,7 +449,7 @@ public function testModifyServerRequestParsedBody() self::assertSame(['name' => 'value'], $modifiedRequest->getParsedBody()); } - public function testModifyServerRequestQueryParams() + public function testModifyServerRequestQueryParams(): void { $request = (new Psr7\ServerRequest('GET', 'http://example.com/bla')) ->withQueryParams(['name' => 'value']); @@ -456,7 +460,7 @@ public function testModifyServerRequestQueryParams() self::assertSame(['name' => 'value'], $modifiedRequest->getQueryParams()); } - public function testModifyServerRequestRetainsAttributes() + public function testModifyServerRequestRetainsAttributes(): void { $request = (new Psr7\ServerRequest('GET', 'http://example.com/bla')) ->withAttribute('foo', 'bar'); @@ -466,4 +470,38 @@ public function testModifyServerRequestRetainsAttributes() self::assertSame(['foo' => 'bar'], $modifiedRequest->getAttributes()); } + + /** + * @return list + */ + public function providesCaselessRemoveCases(): array + { + return [ + [ + ['foo-bar'], + ['Foo-Bar' => 'hello'], + [] + ], + [ + ['foo-bar'], + ['hello'], + ['hello'] + ], + [ + ['foo-Bar'], + ['Foo-Bar' => 'hello', 123 => '', 'Foo-BAR' => 'hello123', 'foobar' => 'baz'], + [123 => '', 'foobar' => 'baz'], + ], + ]; + } + + /** + * @dataProvider providesCaselessRemoveCases + * + * @param string[] $keys + */ + public function testCaselessRemove(array $keys, array $data, array $expected): void + { + self::assertSame($expected, Psr7\Utils::caselessRemove($keys, $data)); + } } diff --git a/vendor-bin/php-cs-fixer/composer.json b/vendor-bin/php-cs-fixer/composer.json new file mode 100644 index 00000000..5c4b374f --- /dev/null +++ b/vendor-bin/php-cs-fixer/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "php": "^7.4 || ^8.0", + "friendsofphp/php-cs-fixer": "3.7.0" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/vendor-bin/phpstan/composer.json b/vendor-bin/phpstan/composer.json new file mode 100644 index 00000000..12065e27 --- /dev/null +++ b/vendor-bin/phpstan/composer.json @@ -0,0 +1,10 @@ +{ + "require": { + "php": "^7.4 || ^8.0", + "phpstan/phpstan": "1.4.9", + "phpstan/phpstan-deprecation-rules": "1.0.0" + }, + "config": { + "preferred-install": "dist" + } +} diff --git a/vendor-bin/psalm/composer.json b/vendor-bin/psalm/composer.json new file mode 100644 index 00000000..0c0ac2c3 --- /dev/null +++ b/vendor-bin/psalm/composer.json @@ -0,0 +1,9 @@ +{ + "require": { + "php": "^7.4 || ^8.0", + "psalm/phar": "4.22.0" + }, + "config": { + "preferred-install": "dist" + } +}