From e82b9c72f3a2d832bceb94045edf67c84eea9173 Mon Sep 17 00:00:00 2001 From: Paul Klimov Date: Wed, 6 Sep 2017 15:56:50 +0300 Subject: [PATCH] Fix #11328 PSR-7 HTTP Message Integration (#14701) Add PSR-7 'HTTP Message' --- composer.json | 1 + composer.lock | 338 +++++++------- framework/CHANGELOG.md | 1 + framework/UPGRADE.md | 11 + framework/captcha/CaptchaAction.php | 12 +- framework/classes.php | 8 +- framework/composer.json | 1 + framework/filters/Cors.php | 3 +- framework/filters/HttpCache.php | 8 +- framework/filters/PageCache.php | 23 +- framework/filters/RateLimiter.php | 7 +- framework/filters/VerbFilter.php | 2 +- framework/filters/auth/HttpBasicAuth.php | 2 +- framework/filters/auth/HttpBearerAuth.php | 4 +- framework/http/Cookie.php | 68 +++ framework/http/CookieCollection.php | 243 ++++++++++ framework/http/FileStream.php | 267 +++++++++++ framework/http/HeaderCollection.php | 236 ++++++++++ framework/http/MemoryStream.php | 205 +++++++++ framework/http/MessageTrait.php | 370 +++++++++++++++ framework/http/ResourceStream.php | 241 ++++++++++ framework/http/UploadedFile.php | 388 ++++++++++++++++ framework/http/Uri.php | 548 +++++++++++++++++++++++ framework/rest/Serializer.php | 11 +- framework/validators/FileValidator.php | 26 +- framework/validators/ImageValidator.php | 14 +- framework/web/Cookie.php | 68 --- framework/web/CookieCollection.php | 243 ---------- framework/web/ErrorHandler.php | 4 +- framework/web/HeaderCollection.php | 236 ---------- framework/web/HtmlResponseFormatter.php | 2 +- framework/web/JsonResponseFormatter.php | 4 +- framework/web/MultipartFormDataParser.php | 2 +- framework/web/Request.php | 231 ++++++++-- framework/web/Response.php | 283 +++++++----- framework/web/UploadedFile.php | 240 ---------- framework/web/User.php | 1 + framework/web/XmlResponseFormatter.php | 2 +- framework/widgets/Pjax.php | 5 +- tests/framework/captcha/CaptchaActionTest.php | 11 +- tests/framework/filters/HttpCacheTest.php | 13 +- tests/framework/filters/PageCacheTest.php | 20 +- tests/framework/filters/auth/AuthTest.php | 2 +- tests/framework/http/FileStreamTest.php | 250 +++++++++++ tests/framework/http/MemoryStreamTest.php | 133 ++++++ tests/framework/http/MessageTraitTest.php | 134 ++++++ tests/framework/http/ResourceStreamTest.php | 243 ++++++++++ tests/framework/http/UploadedFileTest.php | 113 +++++ tests/framework/http/UriTest.php | 203 +++++++++ tests/framework/rest/UrlRuleTest.php | 3 +- tests/framework/validators/FileValidatorTest.php | 54 +-- tests/framework/web/ControllerTest.php | 29 +- tests/framework/web/FormatterTest.php | 2 +- tests/framework/web/RequestTest.php | 17 +- tests/framework/web/ResponseTest.php | 40 +- tests/framework/web/UploadedFileTest.php | 75 ---- tests/framework/web/UrlManagerParseUrlTest.php | 10 +- tests/framework/web/UserTest.php | 6 +- tests/framework/web/stubs/VendorImage.php | 2 +- 59 files changed, 4372 insertions(+), 1347 deletions(-) create mode 100644 framework/http/Cookie.php create mode 100644 framework/http/CookieCollection.php create mode 100644 framework/http/FileStream.php create mode 100644 framework/http/HeaderCollection.php create mode 100644 framework/http/MemoryStream.php create mode 100644 framework/http/MessageTrait.php create mode 100644 framework/http/ResourceStream.php create mode 100644 framework/http/UploadedFile.php create mode 100644 framework/http/Uri.php delete mode 100644 framework/web/Cookie.php delete mode 100644 framework/web/CookieCollection.php delete mode 100644 framework/web/HeaderCollection.php delete mode 100644 framework/web/UploadedFile.php create mode 100644 tests/framework/http/FileStreamTest.php create mode 100644 tests/framework/http/MemoryStreamTest.php create mode 100644 tests/framework/http/MessageTraitTest.php create mode 100644 tests/framework/http/ResourceStreamTest.php create mode 100644 tests/framework/http/UploadedFileTest.php create mode 100644 tests/framework/http/UriTest.php delete mode 100644 tests/framework/web/UploadedFileTest.php diff --git a/composer.json b/composer.json index 25dfe0a..05abff4 100644 --- a/composer.json +++ b/composer.json @@ -75,6 +75,7 @@ "psr/log": "~1.0.2", "yiisoft/yii2-composer": "~2.0.4", "psr/simple-cache": "~1.0.0", + "psr/http-message": "~1.0.0", "ezyang/htmlpurifier": "~4.6", "cebe/markdown": "~1.0.0 | ~1.1.0", "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", diff --git a/composer.lock b/composer.lock index 6d98a6b..2ff56e1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,21 +4,21 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "81cb8a6aa01acac345059695e795fd68", - "content-hash": "b36161ae5a97ecec89951d7fd5b2aa6a", + "hash": "85aa1827f1afe6bc8ac2213443d06fbe", + "content-hash": "9fc9598aed2de7fd7f85836a8b1f8a44", "packages": [ { "name": "bower-asset/inputmask", - "version": "3.3.7", + "version": "3.3.8", "source": { "type": "git", "url": "https://github.com/RobinHerbots/Inputmask.git", - "reference": "9835731cb78cac749734d94a1cb5bd70da4d3b10" + "reference": "791d84990c4a98df1597e9d155be53a3725805dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/9835731cb78cac749734d94a1cb5bd70da4d3b10", - "reference": "9835731cb78cac749734d94a1cb5bd70da4d3b10", + "url": "https://api.github.com/repos/RobinHerbots/Inputmask/zipball/791d84990c4a98df1597e9d155be53a3725805dd", + "reference": "791d84990c4a98df1597e9d155be53a3725805dd", "shasum": null }, "require": { @@ -34,30 +34,18 @@ "version": "2.2.4", "source": { "type": "git", - "url": "https://github.com/components/jquery.git", - "reference": "981036fcb56668433a7eb0d1e71190324b4574df" + "url": "https://github.com/jquery/jquery-dist.git", + "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/components/jquery/zipball/981036fcb56668433a7eb0d1e71190324b4574df", - "reference": "981036fcb56668433a7eb0d1e71190324b4574df", - "shasum": "" - }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "dist/jquery.js", - "bower-asset-ignore": [ - "package.json" - ] + "url": "https://api.github.com/repos/jquery/jquery-dist/zipball/c0185ab7c75aab88762c5aae780b9d83b80eda72", + "reference": "c0185ab7c75aab88762c5aae780b9d83b80eda72", + "shasum": null }, + "type": "bower-asset", "license": [ "MIT" - ], - "keywords": [ - "browser", - "javascript", - "jquery", - "library" ] }, { @@ -72,21 +60,9 @@ "type": "zip", "url": "https://api.github.com/repos/bestiejs/punycode.js/zipball/38c8d3131a82567bfef18da09f7f4db68c84f8a3", "reference": "38c8d3131a82567bfef18da09f7f4db68c84f8a3", - "shasum": "" + "shasum": null }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "punycode.js", - "bower-asset-ignore": [ - "coverage", - "tests", - ".*", - "component.json", - "Gruntfile.js", - "node_modules", - "package.json" - ] - } + "type": "bower-asset" }, { "name": "bower-asset/yii2-pjax", @@ -100,24 +76,12 @@ "type": "zip", "url": "https://api.github.com/repos/yiisoft/jquery-pjax/zipball/60728da6ade5879e807a49ce59ef9a72039b8978", "reference": "60728da6ade5879e807a49ce59ef9a72039b8978", - "shasum": "" + "shasum": null }, "require": { "bower-asset/jquery": ">=1.8" }, - "type": "bower-asset-library", - "extra": { - "bower-asset-main": "./jquery.pjax.js", - "bower-asset-ignore": [ - ".travis.yml", - "Gemfile", - "Gemfile.lock", - "CONTRIBUTING.md", - "vendor/", - "script/", - "test/" - ] - }, + "type": "bower-asset", "license": [ "MIT" ] @@ -230,6 +194,56 @@ "time": "2017-06-03 02:28:16" }, { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06 14:39:51" + }, + { "name": "psr/simple-cache", "version": "1.0.0", "source": { @@ -540,16 +554,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.2.5", + "version": "v2.2.6", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "27c2cd9d4abd2178b5b585fa2c3cca656d377c69" + "reference": "c1cc52c242f17c4d52d9601159631da488fac7a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/27c2cd9d4abd2178b5b585fa2c3cca656d377c69", - "reference": "27c2cd9d4abd2178b5b585fa2c3cca656d377c69", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/c1cc52c242f17c4d52d9601159631da488fac7a4", + "reference": "c1cc52c242f17c4d52d9601159631da488fac7a4", "shasum": "" }, "require": { @@ -620,20 +634,20 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2017-07-18 15:16:38" + "time": "2017-08-22 14:08:16" }, { "name": "gecko-packages/gecko-php-unit", - "version": "v2.1", + "version": "v2.2", "source": { "type": "git", "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", - "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e" + "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/5b9e9622c7efd3b22655270b80c03f9e52878a6e", - "reference": "5b9e9622c7efd3b22655270b80c03f9e52878a6e", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/ab525fac9a9ffea219687f261b02008b18ebf2d1", + "reference": "ab525fac9a9ffea219687f261b02008b18ebf2d1", "shasum": "" }, "require": { @@ -642,24 +656,29 @@ "require-dev": { "phpunit/phpunit": "^4.8.35 || ^5.4.3" }, + "suggest": { + "ext-dom": "When testing with xml.", + "ext-libxml": "When testing with xml.", + "phpunit/phpunit": "This is an extension for it so make sure you have it some way." + }, "type": "library", "autoload": { "psr-4": { - "GeckoPackages\\PHPUnit\\": "src\\PHPUnit" + "GeckoPackages\\PHPUnit\\": "src/PHPUnit" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Additional PHPUnit tests.", + "description": "Additional PHPUnit asserts and constraints.", "homepage": "https://github.com/GeckoPackages", "keywords": [ "extension", "filesystem", "phpunit" ], - "time": "2017-06-20 11:22:48" + "time": "2017-08-23 07:39:54" }, { "name": "ircmaxell/password-compat", @@ -951,22 +970,22 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.2.2", + "version": "3.2.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157" + "reference": "86e24012a3139b42a7b71155cfaa325389f00f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157", - "reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/86e24012a3139b42a7b71155cfaa325389f00f1f", + "reference": "86e24012a3139b42a7b71155cfaa325389f00f1f", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^7.0", "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.3.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { @@ -992,20 +1011,20 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2017-08-08 06:39:58" + "time": "2017-08-29 19:37:41" }, { "name": "phpdocumentor/type-resolver", - "version": "0.3.0", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773", - "reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { @@ -1039,7 +1058,7 @@ "email": "me@mikevanriel.com" } ], - "time": "2017-06-03 08:32:36" + "time": "2017-07-14 14:27:02" }, { "name": "phpspec/prophecy", @@ -1307,16 +1326,16 @@ }, { "name": "phpunit/php-token-stream", - "version": "2.0.0", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b" + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", - "reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/9a02332089ac48e704c70f6cefed30c224e3c0b0", + "reference": "9a02332089ac48e704c70f6cefed30c224e3c0b0", "shasum": "" }, "require": { @@ -1352,7 +1371,7 @@ "keywords": [ "tokenizer" ], - "time": "2017-08-03 14:17:41" + "time": "2017-08-20 05:47:52" }, { "name": "phpunit/phpunit", @@ -2105,20 +2124,20 @@ }, { "name": "symfony/console", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b0878233cb5c4391347e5495089c7af11b8e6201" + "reference": "d6596cb5022b6a0bd940eae54a1de78646a5fda6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201", - "reference": "b0878233cb5c4391347e5495089c7af11b8e6201", + "url": "https://api.github.com/repos/symfony/console/zipball/d6596cb5022b6a0bd940eae54a1de78646a5fda6", + "reference": "d6596cb5022b6a0bd940eae54a1de78646a5fda6", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "symfony/debug": "~2.8|~3.0", "symfony/polyfill-mbstring": "~1.0" }, @@ -2131,7 +2150,6 @@ "symfony/dependency-injection": "~3.3", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/filesystem": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0", "symfony/process": "~2.8|~3.0" }, "suggest": { @@ -2170,24 +2188,24 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-07-29 21:27:59" + "time": "2017-08-27 14:52:21" }, { "name": "symfony/debug", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13" + "reference": "084d804fe35808eb2ef596ec83d85d9768aa6c9d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13", - "reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13", + "url": "https://api.github.com/repos/symfony/debug/zipball/084d804fe35808eb2ef596ec83d85d9768aa6c9d", + "reference": "084d804fe35808eb2ef596ec83d85d9768aa6c9d", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^5.5.9|>=7.0.8", "psr/log": "~1.0" }, "conflict": { @@ -2226,24 +2244,24 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-07-28 15:27:31" + "time": "2017-08-27 14:52:21" }, { "name": "symfony/event-dispatcher", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e" + "reference": "54ca9520a00386f83bca145819ad3b619aaa2485" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/67535f1e3fd662bdc68d7ba317c93eecd973617e", - "reference": "67535f1e3fd662bdc68d7ba317c93eecd973617e", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/54ca9520a00386f83bca145819ad3b619aaa2485", + "reference": "54ca9520a00386f83bca145819ad3b619aaa2485", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "conflict": { "symfony/dependency-injection": "<3.3" @@ -2289,24 +2307,24 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-06-09 14:53:08" + "time": "2017-07-29 21:54:42" }, { "name": "symfony/filesystem", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676" + "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/427987eb4eed764c3b6e38d52a0f87989e010676", - "reference": "427987eb4eed764c3b6e38d52a0f87989e010676", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/b32a0e5f928d0fa3d1dd03c78d020777e50c10cb", + "reference": "b32a0e5f928d0fa3d1dd03c78d020777e50c10cb", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2338,24 +2356,24 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-07-11 07:17:58" + "time": "2017-07-29 21:54:42" }, { "name": "symfony/finder", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", - "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", + "url": "https://api.github.com/repos/symfony/finder/zipball/b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", + "reference": "b2260dbc80f3c4198f903215f91a1ac7fe9fe09e", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2387,24 +2405,24 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-06-01 21:01:25" + "time": "2017-07-29 21:54:42" }, { "name": "symfony/options-resolver", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0" + "reference": "ee4e22978fe885b54ee5da8c7964f0a5301abfb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ff48982d295bcac1fd861f934f041ebc73ae40f0", - "reference": "ff48982d295bcac1fd861f934f041ebc73ae40f0", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/ee4e22978fe885b54ee5da8c7964f0a5301abfb6", + "reference": "ee4e22978fe885b54ee5da8c7964f0a5301abfb6", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2441,20 +2459,20 @@ "configuration", "options" ], - "time": "2017-04-12 14:14:56" + "time": "2017-07-29 21:54:42" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937" + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f29dca382a6485c3cbe6379f0c61230167681937", - "reference": "f29dca382a6485c3cbe6379f0c61230167681937", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/7c8fae0ac1d216eb54349e6a8baa57d515fe8803", + "reference": "7c8fae0ac1d216eb54349e6a8baa57d515fe8803", "shasum": "" }, "require": { @@ -2466,7 +2484,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2500,20 +2518,20 @@ "portable", "shim" ], - "time": "2017-06-09 14:24:12" + "time": "2017-06-14 15:44:48" }, { "name": "symfony/polyfill-php54", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php54.git", - "reference": "7dd1a8b9f0442273fdfeb1c4f5eaff6890a82789" + "reference": "b7763422a5334c914ef0298ed21b253d25913a6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/7dd1a8b9f0442273fdfeb1c4f5eaff6890a82789", - "reference": "7dd1a8b9f0442273fdfeb1c4f5eaff6890a82789", + "url": "https://api.github.com/repos/symfony/polyfill-php54/zipball/b7763422a5334c914ef0298ed21b253d25913a6e", + "reference": "b7763422a5334c914ef0298ed21b253d25913a6e", "shasum": "" }, "require": { @@ -2522,7 +2540,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2558,20 +2576,20 @@ "portable", "shim" ], - "time": "2017-06-09 08:25:21" + "time": "2017-06-14 15:44:48" }, { "name": "symfony/polyfill-php55", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "94566239a7720cde0820f15f0cc348ddb51ba51d" + "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/94566239a7720cde0820f15f0cc348ddb51ba51d", - "reference": "94566239a7720cde0820f15f0cc348ddb51ba51d", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/29b1381d66f16e0581aab0b9f678ccf073288f68", + "reference": "29b1381d66f16e0581aab0b9f678ccf073288f68", "shasum": "" }, "require": { @@ -2581,7 +2599,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2614,20 +2632,20 @@ "portable", "shim" ], - "time": "2017-06-09 08:25:21" + "time": "2017-06-14 15:44:48" }, { "name": "symfony/polyfill-php70", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874" + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/032fd647d5c11a9ceab8ee8747e13b5448e93874", - "reference": "032fd647d5c11a9ceab8ee8747e13b5448e93874", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/b6482e68974486984f59449ecea1fbbb22ff840f", + "reference": "b6482e68974486984f59449ecea1fbbb22ff840f", "shasum": "" }, "require": { @@ -2637,7 +2655,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2673,20 +2691,20 @@ "portable", "shim" ], - "time": "2017-06-09 14:24:12" + "time": "2017-06-14 15:44:48" }, { "name": "symfony/polyfill-php72", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "d3a71580c1e2cab33b6d705f0ec40e9015e14d5c" + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/d3a71580c1e2cab33b6d705f0ec40e9015e14d5c", - "reference": "d3a71580c1e2cab33b6d705f0ec40e9015e14d5c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/8abc9097f5001d310f0edba727469c988acc6ea7", + "reference": "8abc9097f5001d310f0edba727469c988acc6ea7", "shasum": "" }, "require": { @@ -2695,7 +2713,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { @@ -2728,24 +2746,24 @@ "portable", "shim" ], - "time": "2017-06-09 08:25:21" + "time": "2017-07-11 13:25:55" }, { "name": "symfony/process", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a" + "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/07432804942b9f6dd7b7377faf9920af5f95d70a", - "reference": "07432804942b9f6dd7b7377faf9920af5f95d70a", + "url": "https://api.github.com/repos/symfony/process/zipball/b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", + "reference": "b7666e9b438027a1ea0e1ee813ec5042d5d7f6f0", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2777,24 +2795,24 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-07-13 13:05:09" + "time": "2017-07-29 21:54:42" }, { "name": "symfony/stopwatch", - "version": "v3.3.6", + "version": "v3.3.8", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" + "reference": "9a5610a8d6a50985a7be485c0ba745c22607beeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", - "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/9a5610a8d6a50985a7be485c0ba745c22607beeb", + "reference": "9a5610a8d6a50985a7be485c0ba745c22607beeb", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { @@ -2826,7 +2844,7 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12 14:14:56" + "time": "2017-07-29 21:54:42" }, { "name": "theseer/tokenizer", diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 6f3a7c8..2387aa3 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -5,6 +5,7 @@ Yii Framework 2 Change Log ----------------------- - Enh #879: Caching implementation refactored according to PSR-16 'Simple Cache' specification (klimov-paul) +- Enh #11328: Added support for PSR-7 'HTTP Message' (klimov-paul) - Enh #13799: CAPTCHA rendering logic extracted into `yii\captcha\DriverInterface`, which instance is available via `yii\captcha\CaptchaAction::$driver` field (vladis84, klimov-paul) - Enh #9260: Mail view rendering encapsulated into `yii\mail\Template` class allowing rendering in isolation and access to `yii\mail\MessageInterface` instance via `$this->context->message` inside the view (klimov-paul) - Enh #11058: Add `$checkAjax` parameter to method `yii\web\Controller::redirect()` which controls redirection in AJAX and PJAX requests (ivanovyordan) diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index c2985cc..1c474e4 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -80,6 +80,17 @@ Upgrade from Yii 2.0.x * Profiling related functionality has been extracted into a separated component under `yii\profile\ProfilerInterface`. Profiling messages should be collection using `yii\base\Application::$profiler`. In case you wish to continue storing profiling messages along with the log ones, you may use `yii\profile\LogTarget` profiling target. +* Classes `yii\web\Request` and `yii\web\Response` have been updated to match interfaces `Psr\Http\Message\RequestInterface` + and `Psr\Http\Message\ResponseInterface` accordingly. Make sure you use their methods and properties correctly. + In particular: method `getHeaders()` and corresponding virtual property `$headers` are no longer return `HeaderCollection` + instance, you can use `getHeaderCollection()` in order to use old headers setup syntax; `Request|Response::$version` renamed + to `Request|Response::$protocolVersion`; `Response::$statusText` renamed `Response::$reasonPhrase`; +* `yii\web\Response::$stream` is no longer available, use `yii\web\Response::withBody()` to setup stream response. + You can use `Response::$bodyRange` to setup stream content range. +* Classes `yii\web\CookieCollection`, `yii\web\HeaderCollection` and `yii\web\UploadedFile` have been moved under + namespace `yii\http\*`. Make sure to refer to those classes using correct fully qualified name. +* Public interface of `UploadedFile` class has been changed according to `Psr\Http\Message\UploadedFileInterface`. + Make sure you refer to its properties and methods with correct names. * `yii\captcha\CaptchaAction` has been refactored. Rendering logic was extracted into `yii\captcha\DriverInterface`, which instance is available via `yii\captcha\CaptchaAction::$driver` field. All image settings now should be passed to the driver fields instead of action. Automatic detection of the rendering driver is no longer supported. diff --git a/framework/captcha/CaptchaAction.php b/framework/captcha/CaptchaAction.php index 6ae1e1a..bb8be3b 100644 --- a/framework/captcha/CaptchaAction.php +++ b/framework/captcha/CaptchaAction.php @@ -181,11 +181,11 @@ class CaptchaAction extends Action */ protected function setHttpHeaders() { - Yii::$app->getResponse()->getHeaders() - ->set('Pragma', 'public') - ->set('Expires', '0') - ->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->set('Content-Transfer-Encoding', 'binary') - ->set('Content-type', $this->driver->getImageMimeType()); + $response = Yii::$app->getResponse(); + $response->setHeader('Pragma', 'public'); + $response->setHeader('Expires', '0'); + $response->setHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0'); + $response->setHeader('Content-Transfer-Encoding', 'binary'); + $response->setHeader('Content-type', $this->driver->getImageMimeType()); } } diff --git a/framework/classes.php b/framework/classes.php index 765cad3..01048b5 100644 --- a/framework/classes.php +++ b/framework/classes.php @@ -279,15 +279,15 @@ return [ 'yii\web\CompositeUrlRule' => YII2_PATH . '/web/CompositeUrlRule.php', 'yii\web\ConflictHttpException' => YII2_PATH . '/web/ConflictHttpException.php', 'yii\web\Controller' => YII2_PATH . '/web/Controller.php', - 'yii\web\Cookie' => YII2_PATH . '/web/Cookie.php', - 'yii\web\CookieCollection' => YII2_PATH . '/web/CookieCollection.php', + 'yii\http\Cookie' => YII2_PATH . '/http/Cookie.php', + 'yii\http\CookieCollection' => YII2_PATH . '/http/CookieCollection.php', 'yii\web\DbSession' => YII2_PATH . '/web/DbSession.php', 'yii\web\ErrorAction' => YII2_PATH . '/web/ErrorAction.php', 'yii\web\ErrorHandler' => YII2_PATH . '/web/ErrorHandler.php', 'yii\web\ForbiddenHttpException' => YII2_PATH . '/web/ForbiddenHttpException.php', 'yii\web\GoneHttpException' => YII2_PATH . '/web/GoneHttpException.php', 'yii\web\GroupUrlRule' => YII2_PATH . '/web/GroupUrlRule.php', - 'yii\web\HeaderCollection' => YII2_PATH . '/web/HeaderCollection.php', + 'yii\http\HeaderCollection' => YII2_PATH . '/http/HeaderCollection.php', 'yii\web\HtmlResponseFormatter' => YII2_PATH . '/web/HtmlResponseFormatter.php', 'yii\web\HttpException' => YII2_PATH . '/web/HttpException.php', 'yii\web\IdentityInterface' => YII2_PATH . '/web/IdentityInterface.php', @@ -314,7 +314,7 @@ return [ 'yii\web\UnauthorizedHttpException' => YII2_PATH . '/web/UnauthorizedHttpException.php', 'yii\web\UnprocessableEntityHttpException' => YII2_PATH . '/web/UnprocessableEntityHttpException.php', 'yii\web\UnsupportedMediaTypeHttpException' => YII2_PATH . '/web/UnsupportedMediaTypeHttpException.php', - 'yii\web\UploadedFile' => YII2_PATH . '/web/UploadedFile.php', + 'yii\http\UploadedFile' => YII2_PATH . '/http/UploadedFile.php', 'yii\web\UrlManager' => YII2_PATH . '/web/UrlManager.php', 'yii\web\UrlNormalizer' => YII2_PATH . '/web/UrlNormalizer.php', 'yii\web\UrlNormalizerRedirectException' => YII2_PATH . '/web/UrlNormalizerRedirectException.php', diff --git a/framework/composer.json b/framework/composer.json index 7cb9103..05bfb3d 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -70,6 +70,7 @@ "psr/log": "~1.0.2", "yiisoft/yii2-composer": "~2.0.4", "psr/simple-cache": "~1.0.0", + "psr/http-message": "~1.0.0", "ezyang/htmlpurifier": "~4.6", "cebe/markdown": "~1.0.0 | ~1.1.0", "bower-asset/jquery": "2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", diff --git a/framework/filters/Cors.php b/framework/filters/Cors.php index 7177d13..0a43519 100644 --- a/framework/filters/Cors.php +++ b/framework/filters/Cors.php @@ -212,9 +212,8 @@ class Cors extends ActionFilter public function addCorsHeaders($response, $headers) { if (empty($headers) === false) { - $responseHeaders = $response->getHeaders(); foreach ($headers as $field => $value) { - $responseHeaders->set($field, $value); + $response->setHeader($field, $value); } } } diff --git a/framework/filters/HttpCache.php b/framework/filters/HttpCache.php index 1a419d7..ccb27d6 100644 --- a/framework/filters/HttpCache.php +++ b/framework/filters/HttpCache.php @@ -139,13 +139,13 @@ class HttpCache extends ActionFilter $response = Yii::$app->getResponse(); if ($etag !== null) { - $response->getHeaders()->set('Etag', $etag); + $response->setHeader('Etag', $etag); } $cacheValid = $this->validateCache($lastModified, $etag); // https://tools.ietf.org/html/rfc7232#section-4.1 if ($lastModified !== null && (!$cacheValid || ($cacheValid && $etag === null))) { - $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); + $response->setHeader('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); } if ($cacheValid) { $response->setStatusCode(304); @@ -192,10 +192,8 @@ class HttpCache extends ActionFilter session_cache_limiter($this->sessionCacheLimiter); } - $headers = Yii::$app->getResponse()->getHeaders(); - if ($this->cacheControlHeader !== null) { - $headers->set('Cache-Control', $this->cacheControlHeader); + Yii::$app->getResponse()->setHeader('Cache-Control', $this->cacheControlHeader); } } diff --git a/framework/filters/PageCache.php b/framework/filters/PageCache.php index d7e55d8..312e2f9 100644 --- a/framework/filters/PageCache.php +++ b/framework/filters/PageCache.php @@ -208,10 +208,15 @@ class PageCache extends ActionFilter */ protected function restoreResponse($response, $data) { - foreach (['format', 'version', 'statusCode', 'statusText', 'content'] as $name) { + foreach (['format', 'protocolVersion', 'statusCode', 'reasonPhrase', 'content'] as $name) { $response->{$name} = $data[$name]; } - foreach (['headers', 'cookies'] as $name) { + + if (isset($data['headers'])) { + $response->setHeaders($data['headers']); + } + + foreach (['cookies'] as $name) { if (isset($data[$name]) && is_array($data[$name])) { $response->{$name}->fromArray(array_merge($data[$name], $response->{$name}->toArray())); } @@ -256,9 +261,14 @@ class PageCache extends ActionFilter } $data['dynamicPlaceholders'] = $this->dynamicPlaceholders; - foreach (['format', 'version', 'statusCode', 'statusText'] as $name) { - $data[$name] = $response->{$name}; - } + + $data = array_merge($data, [ + 'format' => $response->format, + 'protocolVersion' => $response->getProtocolVersion(), + 'statusCode' => $response->getStatusCode(), + 'reasonPhrase' => $response->getReasonPhrase(), + ]); + $this->insertResponseCollectionIntoData($response, 'headers', $data); $this->insertResponseCollectionIntoData($response, 'cookies', $data); $this->cache->set($this->calculateCacheKey(), $data, $this->duration, $this->dependency); @@ -281,7 +291,8 @@ class PageCache extends ActionFilter return; } - $all = $response->{$collectionName}->toArray(); + $collection = $response->{$collectionName}; + $all = is_array($collection) ? $collection : $collection->toArray(); if (is_array($this->{$property})) { $filtered = []; foreach ($this->{$property} as $name) { diff --git a/framework/filters/RateLimiter.php b/framework/filters/RateLimiter.php index 608c9ce..481d611 100644 --- a/framework/filters/RateLimiter.php +++ b/framework/filters/RateLimiter.php @@ -136,10 +136,9 @@ class RateLimiter extends ActionFilter public function addRateLimitHeaders($response, $limit, $remaining, $reset) { if ($this->enableRateLimitHeaders) { - $response->getHeaders() - ->set('X-Rate-Limit-Limit', $limit) - ->set('X-Rate-Limit-Remaining', $remaining) - ->set('X-Rate-Limit-Reset', $reset); + $response->setHeader('X-Rate-Limit-Limit', $limit); + $response->setHeader('X-Rate-Limit-Remaining', $remaining); + $response->setHeader('X-Rate-Limit-Reset', $reset); } } } diff --git a/framework/filters/VerbFilter.php b/framework/filters/VerbFilter.php index 59b6c9d..6f663e2 100644 --- a/framework/filters/VerbFilter.php +++ b/framework/filters/VerbFilter.php @@ -101,7 +101,7 @@ class VerbFilter extends Behavior if (!in_array($verb, $allowed)) { $event->isValid = false; // https://tools.ietf.org/html/rfc2616#section-14.7 - Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed)); + Yii::$app->getResponse()->setHeader('Allow', implode(', ', $allowed)); throw new MethodNotAllowedHttpException('Method Not Allowed. This URL can only handle the following request methods: ' . implode(', ', $allowed) . '.'); } diff --git a/framework/filters/auth/HttpBasicAuth.php b/framework/filters/auth/HttpBasicAuth.php index 43882fc..f5f2ef9 100644 --- a/framework/filters/auth/HttpBasicAuth.php +++ b/framework/filters/auth/HttpBasicAuth.php @@ -113,6 +113,6 @@ class HttpBasicAuth extends AuthMethod */ public function challenge($response) { - $response->getHeaders()->set('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); + $response->setHeader('WWW-Authenticate', "Basic realm=\"{$this->realm}\""); } } diff --git a/framework/filters/auth/HttpBearerAuth.php b/framework/filters/auth/HttpBearerAuth.php index e6f8379..42335d2 100644 --- a/framework/filters/auth/HttpBearerAuth.php +++ b/framework/filters/auth/HttpBearerAuth.php @@ -39,7 +39,7 @@ class HttpBearerAuth extends AuthMethod */ public function authenticate($user, $request, $response) { - $authHeader = $request->getHeaders()->get('Authorization'); + $authHeader = $request->getHeaderLine('Authorization'); if ($authHeader !== null && preg_match('/^Bearer\s+(.*?)$/', $authHeader, $matches)) { $identity = $user->loginByAccessToken($matches[1], get_class($this)); if ($identity === null) { @@ -56,6 +56,6 @@ class HttpBearerAuth extends AuthMethod */ public function challenge($response) { - $response->getHeaders()->set('WWW-Authenticate', "Bearer realm=\"{$this->realm}\""); + $response->setHeader('WWW-Authenticate', "Bearer realm=\"{$this->realm}\""); } } diff --git a/framework/http/Cookie.php b/framework/http/Cookie.php new file mode 100644 index 0000000..afbc684 --- /dev/null +++ b/framework/http/Cookie.php @@ -0,0 +1,68 @@ + + * @since 2.0 + */ +class Cookie extends \yii\base\BaseObject +{ + /** + * @var string name of the cookie + */ + public $name; + /** + * @var string value of the cookie + */ + public $value = ''; + /** + * @var string domain of the cookie + */ + public $domain = ''; + /** + * @var int the timestamp at which the cookie expires. This is the server timestamp. + * Defaults to 0, meaning "until the browser is closed". + */ + public $expire = 0; + /** + * @var string the path on the server in which the cookie will be available on. The default is '/'. + */ + public $path = '/'; + /** + * @var bool whether cookie should be sent via secure connection + */ + public $secure = false; + /** + * @var bool whether the cookie should be accessible only through the HTTP protocol. + * By setting this property to true, the cookie will not be accessible by scripting languages, + * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. + */ + public $httpOnly = true; + + + /** + * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. + * + * ```php + * if (isset($request->cookies['name'])) { + * $value = (string) $request->cookies['name']; + * } + * ``` + * + * @return string The value of the cookie. If the value property is null, an empty string will be returned. + */ + public function __toString() + { + return (string) $this->value; + } +} diff --git a/framework/http/CookieCollection.php b/framework/http/CookieCollection.php new file mode 100644 index 0000000..e84aea3 --- /dev/null +++ b/framework/http/CookieCollection.php @@ -0,0 +1,243 @@ + + * @since 2.0 + */ +class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var bool whether this collection is read only. + */ + public $readOnly = false; + + /** + * @var Cookie[] the cookies in this collection (indexed by the cookie names) + */ + private $_cookies; + + + /** + * Constructor. + * @param array $cookies the cookies that this collection initially contains. This should be + * an array of name-value pairs. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($cookies = [], $config = []) + { + $this->_cookies = $cookies; + parent::__construct($config); + } + + /** + * Returns an iterator for traversing the cookies in the collection. + * This method is required by the SPL interface [[\IteratorAggregate]]. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the cookies in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_cookies); + } + + /** + * Returns the number of cookies in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return int the number of cookies in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of cookies in the collection. + * @return int the number of cookies in the collection. + */ + public function getCount() + { + return count($this->_cookies); + } + + /** + * Returns the cookie with the specified name. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name. Null if the named cookie does not exist. + * @see getValue() + */ + public function get($name) + { + return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; + } + + /** + * Returns the value of the named cookie. + * @param string $name the cookie name + * @param mixed $defaultValue the value that should be returned when the named cookie does not exist. + * @return mixed the value of the named cookie. + * @see get() + */ + public function getValue($name, $defaultValue = null) + { + return isset($this->_cookies[$name]) ? $this->_cookies[$name]->value : $defaultValue; + } + + /** + * Returns whether there is a cookie with the specified name. + * Note that if a cookie is marked for deletion from browser, this method will return false. + * @param string $name the cookie name + * @return bool whether the named cookie exists + * @see remove() + */ + public function has($name) + { + return isset($this->_cookies[$name]) && $this->_cookies[$name]->value !== '' + && ($this->_cookies[$name]->expire === null || $this->_cookies[$name]->expire >= time()); + } + + /** + * Adds a cookie to the collection. + * If there is already a cookie with the same name in the collection, it will be removed first. + * @param Cookie $cookie the cookie to be added + * @throws InvalidCallException if the cookie collection is read only + */ + public function add($cookie) + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + $this->_cookies[$cookie->name] = $cookie; + } + + /** + * Removes a cookie. + * If `$removeFromBrowser` is true, the cookie will be removed from the browser. + * In this case, a cookie with outdated expiry will be added to the collection. + * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. + * @param bool $removeFromBrowser whether to remove the cookie from browser + * @throws InvalidCallException if the cookie collection is read only + */ + public function remove($cookie, $removeFromBrowser = true) + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + if ($cookie instanceof Cookie) { + $cookie->expire = 1; + $cookie->value = ''; + } else { + $cookie = new Cookie([ + 'name' => $cookie, + 'expire' => 1, + ]); + } + if ($removeFromBrowser) { + $this->_cookies[$cookie->name] = $cookie; + } else { + unset($this->_cookies[$cookie->name]); + } + } + + /** + * Removes all cookies. + * @throws InvalidCallException if the cookie collection is read only + */ + public function removeAll() + { + if ($this->readOnly) { + throw new InvalidCallException('The cookie collection is read only.'); + } + $this->_cookies = []; + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are cookie names, and the array values are the corresponding cookie objects. + */ + public function toArray() + { + return $this->_cookies; + } + + /** + * Populates the cookie collection from an array. + * @param array $array the cookies to populate from + * @since 2.0.3 + */ + public function fromArray(array $array) + { + $this->_cookies = $array; + } + + /** + * Returns whether there is a cookie with the specified name. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the cookie name + * @return bool whether the named cookie exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the cookie with the specified name. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `$cookie = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the cookie name + * @return Cookie the cookie with the specified name, null if the named cookie does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the cookie to the collection. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `$collection[$name] = $cookie;`. + * This is equivalent to [[add()]]. + * @param string $name the cookie name + * @param Cookie $cookie the cookie to be added + */ + public function offsetSet($name, $cookie) + { + $this->add($cookie); + } + + /** + * Removes the named cookie. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the cookie name + */ + public function offsetUnset($name) + { + $this->remove($name); + } +} diff --git a/framework/http/FileStream.php b/framework/http/FileStream.php new file mode 100644 index 0000000..7acc68f --- /dev/null +++ b/framework/http/FileStream.php @@ -0,0 +1,267 @@ + '@app/files/items.txt', + * 'mode' => 'w+', + * ]); + * + * $stream->write('some content...'); + * $stream->close(); + * ``` + * + * @author Paul Klimov + * @since 2.1.0 + */ +class FileStream extends BaseObject implements StreamInterface +{ + /** + * @var string file or stream name. + * Path alias can be used here, for example: '@app/runtime/items.csv'. + * This field can also be PHP stream name, e.g. anything which can be passed to `fopen()`, for example: 'php://input'. + */ + public $filename; + /** + * @var string file open mode. + */ + public $mode = 'r'; + + /** + * @var resource|null stream resource + */ + private $_resource; + /** + * @var array a resource metadata. + */ + private $_metadata; + + + /** + * Destructor. + * Closes the stream resource when destroyed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * @return resource a file pointer resource. + * @throws InvalidConfigException if unable to open a resource. + */ + public function getResource() + { + if ($this->_resource === null) { + $resource = fopen(Yii::getAlias($this->filename), $this->mode); + if ($resource === false) { + throw new InvalidConfigException("Unable to open file '{$this->filename}' with mode '{$this->mode}'"); + } + $this->_resource = $resource; + } + return $this->_resource; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + $this->seek(0); + return $this->getContents(); + } catch (\Exception $e) { + ErrorHandler::convertExceptionToError($e); + return ''; + } + } + + /** + * {@inheritdoc} + */ + public function close() + { + if ($this->_resource !== null) { + fclose($this->_resource); + $this->_resource = null; + $this->_metadata = null; + } + } + + /** + * {@inheritdoc} + */ + public function detach() + { + if ($this->_resource === null) { + return null; + } + $result = $this->_resource; + $this->_resource = null; + $this->_metadata = null; + return $result; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + $uri = $this->getMetadata('uri'); + if (!empty($uri)) { + // clear the stat cache in case stream has a URI + clearstatcache(true, $uri); + } + + $stats = fstat($this->getResource()); + if (isset($stats['size'])) { + return $stats['size']; + } + return null; + } + + /** + * {@inheritdoc} + */ + public function tell() + { + $result = ftell($this->getResource()); + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return feof($this->getResource()); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return (bool)$this->getMetadata('seekable'); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (fseek($this->getResource(), $offset, $whence) === -1) { + throw new \RuntimeException("Unable to seek to stream position '{$offset}' with whence '{$whence}'"); + } + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->seek(0); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + $mode = $this->getMetadata('mode'); + foreach (['w', 'c', 'a', 'x', 'r+'] as $key) { + if (strpos($mode, $key) !== false) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + $result = fwrite($this->getResource(), $string); + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + $mode = $this->getMetadata('mode'); + foreach (['r', 'w+', 'a+', 'c+', 'x+'] as $key) { + if (strpos($mode, $key) !== false) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + $string = fread($this->getResource(), $length); + if ($string === false) { + throw new \RuntimeException('Unable to read from stream'); + } + return $string; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + $contents = stream_get_contents($this->getResource()); + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + return $contents; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + if ($this->_metadata === null) { + $this->_metadata = stream_get_meta_data($this->getResource()); + } + + if ($key === null) { + return $this->_metadata; + } + + return isset($this->_metadata[$key]) ? $this->_metadata[$key] : null; + } +} \ No newline at end of file diff --git a/framework/http/HeaderCollection.php b/framework/http/HeaderCollection.php new file mode 100644 index 0000000..a1fd1a4 --- /dev/null +++ b/framework/http/HeaderCollection.php @@ -0,0 +1,236 @@ + + * @since 2.0 + */ +class HeaderCollection extends BaseObject implements \IteratorAggregate, \ArrayAccess, \Countable +{ + /** + * @var array the headers in this collection (indexed by the header names) + */ + private $_headers = []; + + + /** + * Returns an iterator for traversing the headers in the collection. + * This method is required by the SPL interface [[\IteratorAggregate]]. + * It will be implicitly called when you use `foreach` to traverse the collection. + * @return ArrayIterator an iterator for traversing the headers in the collection. + */ + public function getIterator() + { + return new ArrayIterator($this->_headers); + } + + /** + * Returns the number of headers in the collection. + * This method is required by the SPL `Countable` interface. + * It will be implicitly called when you use `count($collection)`. + * @return int the number of headers in the collection. + */ + public function count() + { + return $this->getCount(); + } + + /** + * Returns the number of headers in the collection. + * @return int the number of headers in the collection. + */ + public function getCount() + { + return count($this->_headers); + } + + /** + * Returns the named header(s). + * @param string $name the name of the header to return + * @param mixed $default the value to return in case the named header does not exist + * @param bool $first whether to only return the first header of the specified name. + * If false, all headers of the specified name will be returned. + * @return string|array the named header(s). If `$first` is true, a string will be returned; + * If `$first` is false, an array will be returned. + */ + public function get($name, $default = null, $first = true) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; + } + + return $default; + } + + /** + * Adds a new header. + * If there is already a header with the same name, it will be replaced. + * @param string $name the name of the header + * @param string $value the value of the header + * @return $this the collection object itself + */ + public function set($name, $value = '') + { + $name = strtolower($name); + $this->_headers[$name] = (array) $value; + + return $this; + } + + /** + * Adds a new header. + * If there is already a header with the same name, the new one will + * be appended to it instead of replacing it. + * @param string $name the name of the header + * @param string $value the value of the header + * @return $this the collection object itself + */ + public function add($name, $value) + { + $name = strtolower($name); + $this->_headers[$name][] = $value; + + return $this; + } + + /** + * Sets a new header only if it does not exist yet. + * If there is already a header with the same name, the new one will be ignored. + * @param string $name the name of the header + * @param string $value the value of the header + * @return $this the collection object itself + */ + public function setDefault($name, $value) + { + $name = strtolower($name); + if (empty($this->_headers[$name])) { + $this->_headers[$name][] = $value; + } + + return $this; + } + + /** + * Returns a value indicating whether the named header exists. + * @param string $name the name of the header + * @return bool whether the named header exists + */ + public function has($name) + { + $name = strtolower($name); + + return isset($this->_headers[$name]); + } + + /** + * Removes a header. + * @param string $name the name of the header to be removed. + * @return array the value of the removed header. Null is returned if the header does not exist. + */ + public function remove($name) + { + $name = strtolower($name); + if (isset($this->_headers[$name])) { + $value = $this->_headers[$name]; + unset($this->_headers[$name]); + return $value; + } + + return null; + } + + /** + * Removes all headers. + */ + public function removeAll() + { + $this->_headers = []; + } + + /** + * Returns the collection as a PHP array. + * @return array the array representation of the collection. + * The array keys are header names, and the array values are the corresponding header values. + */ + public function toArray() + { + return $this->_headers; + } + + /** + * Populates the header collection from an array. + * @param array $array the headers to populate from + * @since 2.0.3 + */ + public function fromArray(array $array) + { + $this->_headers = $array; + } + + /** + * Returns whether there is a header with the specified name. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `isset($collection[$name])`. + * @param string $name the header name + * @return bool whether the named header exists + */ + public function offsetExists($name) + { + return $this->has($name); + } + + /** + * Returns the header with the specified name. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `$header = $collection[$name];`. + * This is equivalent to [[get()]]. + * @param string $name the header name + * @return string the header value with the specified name, null if the named header does not exist. + */ + public function offsetGet($name) + { + return $this->get($name); + } + + /** + * Adds the header to the collection. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `$collection[$name] = $header;`. + * This is equivalent to [[add()]]. + * @param string $name the header name + * @param string $value the header value to be added + */ + public function offsetSet($name, $value) + { + $this->set($name, $value); + } + + /** + * Removes the named header. + * This method is required by the SPL interface [[\ArrayAccess]]. + * It is implicitly called when you use something like `unset($collection[$name])`. + * This is equivalent to [[remove()]]. + * @param string $name the header name + */ + public function offsetUnset($name) + { + $this->remove($name); + } +} diff --git a/framework/http/MemoryStream.php b/framework/http/MemoryStream.php new file mode 100644 index 0000000..601209c --- /dev/null +++ b/framework/http/MemoryStream.php @@ -0,0 +1,205 @@ +write('some content...'); + * // ... + * $stream->rewind(); + * echo $stream->getContents(); + * ``` + * + * @author Paul Klimov + * @since 2.1.0 + */ +class MemoryStream extends BaseObject implements StreamInterface +{ + /** + * @var string internal content. + */ + private $buffer = ''; + /** + * @var int internal stream pointer. + */ + private $pointer = 0; + + + /** + * {@inheritdoc} + */ + public function __toString() + { + return $this->buffer; + } + + /** + * {@inheritdoc} + */ + public function close() + { + $this->buffer = ''; + $this->pointer = 0; + } + + /** + * {@inheritdoc} + */ + public function detach() + { + $this->close(); + return null; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + return strlen($this->buffer); + } + + /** + * {@inheritdoc} + */ + public function tell() + { + return $this->pointer; + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return $this->pointer >= $this->getSize(); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + switch ($whence) { + case SEEK_SET: + $this->pointer = $offset; + break; + case SEEK_CUR: + $this->pointer += $offset; + break; + case SEEK_END: + $this->pointer = $this->getSize() + $offset; + break; + default: + throw new InvalidArgumentException("Unknown seek whence: '{$whence}'."); + } + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->seek(0); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + $size = $this->getSize(); + $writeSize = strlen($string); + + if ($this->pointer >= $size) { + $this->buffer .= $string; + $this->pointer = $size + $writeSize; + return $writeSize; + } + + $begin = substr($this->buffer, 0, $this->pointer); + $end = substr($this->buffer, $this->pointer + $writeSize); + + $this->buffer = $begin . $string . $end; + $this->pointer += $writeSize; + return $writeSize; + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + return true; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + $data = substr($this->buffer, $this->pointer, $length); + $this->pointer += $length; + return $data; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + if ($this->pointer === 0) { + return $this->buffer; + } + return substr($this->buffer, $this->pointer); + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + $metadata = [ + 'mode' => 'rw', + 'seekable' => $this->isSeekable(), + ]; + + if ($key === null) { + return $metadata; + } + + return (isset($metadata[$key])) ? $metadata[$key] : null; + } +} \ No newline at end of file diff --git a/framework/http/MessageTrait.php b/framework/http/MessageTrait.php new file mode 100644 index 0000000..7706775 --- /dev/null +++ b/framework/http/MessageTrait.php @@ -0,0 +1,370 @@ + + * @since 2.1.0 + */ +trait MessageTrait +{ + /** + * @var string HTTP protocol version as a string. + */ + private $_protocolVersion; + /** + * @var HeaderCollection header collection, which is used for headers storage. + */ + private $_headerCollection; + /** + * @var StreamInterface the body of the message. + */ + private $_body; + + + /** + * Retrieves the HTTP protocol version as a string. + * @return string HTTP protocol version. + */ + public function getProtocolVersion() + { + if ($this->_protocolVersion === null) { + $this->_protocolVersion = $this->defaultProtocolVersion(); + } + return $this->_protocolVersion; + } + + /** + * Specifies HTTP protocol version. + * @param string $version HTTP protocol version + */ + public function setProtocolVersion($version) + { + $this->_protocolVersion = $version; + } + + /** + * Return an instance with the specified HTTP protocol version. + * + * This method retains the immutability of the message and returns an instance that has the + * new protocol version. + * + * @param string $version HTTP protocol version + * @return static + */ + public function withProtocolVersion($version) + { + if ($this->getProtocolVersion() === $version) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setProtocolVersion($version); + return $newInstance; + } + + /** + * Returns default HTTP protocol version to be used in case it is not explicitly set. + * @return string HTTP protocol version. + */ + protected function defaultProtocolVersion() + { + if (!empty($_SERVER['SERVER_PROTOCOL'])) { + return str_replace('HTTP/', '', $_SERVER['SERVER_PROTOCOL']); + } + return '1.0'; + } + + /** + * Returns the header collection. + * The header collection contains the currently registered HTTP headers. + * @return HeaderCollection the header collection + */ + public function getHeaderCollection() + { + if ($this->_headerCollection === null) { + $headerCollection = new HeaderCollection(); + $headerCollection->fromArray($this->defaultHeaders()); + $this->_headerCollection = $headerCollection; + } + return $this->_headerCollection; + } + + /** + * Returns default message's headers, which should be present once [[headerCollection]] is instantiated. + * @return string[][] an associative array of the message's headers. + */ + protected function defaultHeaders() + { + return []; + } + + /** + * Sets up message's headers at batch, removing any previously existing ones. + * @param string[][] $headers an associative array of the message's headers. + */ + public function setHeaders($headers) + { + $headerCollection = $this->getHeaderCollection(); + $headerCollection->removeAll(); + $headerCollection->fromArray($headers); + } + + /** + * Sets up a particular message's header, removing any its previously existing value. + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + */ + public function setHeader($name, $value) + { + $this->getHeaderCollection()->set($name, $value); + } + + /** + * Appends the given value to the specified header. + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + */ + public function addHeader($name, $value) + { + $this->getHeaderCollection()->add($name, $value); + } + + /** + * Retrieves all message header values. + * + * The keys represent the header name as it will be sent over the wire, and + * each value is an array of strings associated with the header. + * + * // Represent the headers as a string + * foreach ($message->getHeaders() as $name => $values) { + * echo $name . ": " . implode(", ", $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->getHeaders() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * While header names are not case-sensitive, getHeaders() will preserve the + * exact case in which headers were originally specified. + * + * @return string[][] Returns an associative array of the message's headers. Each + * key MUST be a header name, and each value MUST be an array of strings + * for that header. + */ + public function getHeaders() + { + return $this->getHeaderCollection()->toArray(); + } + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function hasHeader($name) + { + return $this->getHeaderCollection()->has($name); + } + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method will return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function getHeader($name) + { + return $this->getHeaderCollection()->get($name, [], false); + } + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function getHeaderLine($name) + { + return implode(',', $this->getHeader($name)); + } + + /** + * Return an instance with the provided value replacing the specified header. + * This method retains the immutability of the message and returns an instance that has the + * new and/or updated header and value. + * @param string $name Case-insensitive header field name. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withHeader($name, $value) + { + $newInstance = clone $this; + $newInstance->setHeader($name, $value); + return $newInstance; + } + + /** + * Return an instance with the specified header appended with the given value. + * + * Existing values for the specified header will be maintained. The new + * value(s) will be appended to the existing list. If the header did not + * exist previously, it will be added. + * + * This method retains the immutability of the message and returns an instance that has the + * new header and/or value. + * + * @param string $name Case-insensitive header field name to add. + * @param string|string[] $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function withAddedHeader($name, $value) + { + $newInstance = clone $this; + $newInstance->addHeader($name, $value); + return $newInstance; + } + + /** + * Return an instance without the specified header. + * Header resolution performed without case-sensitivity. + * This method retains the immutability of the message and returns an instance that removes + * the named header. + * @param string $name Case-insensitive header field name to remove. + * @return static + */ + public function withoutHeader($name) + { + $newInstance = clone $this; + $newInstance->getHeaderCollection()->remove($name); + return $newInstance; + } + + /** + * Gets the body of the message. + * @return StreamInterface Returns the body as a stream. + */ + public function getBody() + { + if (!$this->_body instanceof StreamInterface) { + if ($this->_body === null) { + $body = $this->defaultBody(); + } elseif ($this->_body instanceof \Closure) { + $body = call_user_func($this->_body, $this); + } else { + $body = $this->_body; + } + + $this->_body = Instance::ensure($body, StreamInterface::class); + } + return $this->_body; + } + + /** + * Specifies message body. + * @param StreamInterface|\Closure|array $body stream instance or its DI compatible configuration. + */ + public function setBody($body) + { + $this->_body = $body; + } + + /** + * Return an instance with the specified message body. + * This method retains the immutability of the message and returns an instance that has the + * new body stream. + * @param StreamInterface $body Body. + * @return static + * @throws \InvalidArgumentException When the body is not valid. + */ + public function withBody(StreamInterface $body) + { + if ($this->getBody() === $body) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setBody($body); + return $newInstance; + } + + /** + * Returns default message body to be used in case it is not explicitly set. + * @return StreamInterface default body instance. + */ + protected function defaultBody() + { + return new MemoryStream(); + } + + /** + * This method is called after the object is created by cloning an existing one. + */ + public function __clone() + { + $this->cloneHttpMessageInternals(); + } + + /** + * Ensures any internal object-type fields related to `MessageTrait` are cloned from their origins. + * In case actual trait owner implementing method [[__clone()]], it must invoke this method within it. + */ + private function cloneHttpMessageInternals() + { + if (is_object($this->_headerCollection)) { + $this->_headerCollection = clone $this->_headerCollection; + } + if (is_object($this->_body)) { + $this->_body = clone $this->_body; + } + } +} \ No newline at end of file diff --git a/framework/http/ResourceStream.php b/framework/http/ResourceStream.php new file mode 100644 index 0000000..6804a58 --- /dev/null +++ b/framework/http/ResourceStream.php @@ -0,0 +1,241 @@ + tmpfile(), + * ]); + * + * $stream->write('some content...'); + * $stream->close(); + * ``` + * + * Usage of this class make sense in case you already have an opened PHP stream from elsewhere and wish to wrap it into `StreamInterface`. + * + * > Note: closing this stream will close the resource associated with it, so it becomes invalid for usage elsewhere. + * + * @author Paul Klimov + * @since 2.1.0 + */ +class ResourceStream extends BaseObject implements StreamInterface +{ + /** + * @var resource stream resource. + */ + public $resource; + + /** + * @var array a resource metadata. + */ + private $_metadata; + + + /** + * Destructor. + * Closes the stream resource when destroyed. + */ + public function __destruct() + { + $this->close(); + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + $this->seek(0); + return $this->getContents(); + } catch (\Exception $e) { + ErrorHandler::convertExceptionToError($e); + return ''; + } + } + + /** + * {@inheritdoc} + */ + public function close() + { + if ($this->resource !== null && is_resource($this->resource)) { + fclose($this->resource); + $this->_metadata = null; + } + } + + /** + * {@inheritdoc} + */ + public function detach() + { + if ($this->resource === null) { + return null; + } + $result = $this->resource; + $this->resource = null; + $this->_metadata = null; + return $result; + } + + /** + * {@inheritdoc} + */ + public function getSize() + { + $uri = $this->getMetadata('uri'); + if (!empty($uri)) { + // clear the stat cache in case stream has a URI + clearstatcache(true, $uri); + } + + $stats = fstat($this->resource); + if (isset($stats['size'])) { + return $stats['size']; + } + return null; + } + + /** + * {@inheritdoc} + */ + public function tell() + { + $result = ftell($this->resource); + if ($result === false) { + throw new \RuntimeException('Unable to determine stream position'); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function eof() + { + return feof($this->resource); + } + + /** + * {@inheritdoc} + */ + public function isSeekable() + { + return (bool)$this->getMetadata('seekable'); + } + + /** + * {@inheritdoc} + */ + public function seek($offset, $whence = SEEK_SET) + { + if (fseek($this->resource, $offset, $whence) === -1) { + throw new \RuntimeException("Unable to seek to stream position '{$offset}' with whence '{$whence}'"); + } + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + $this->seek(0); + } + + /** + * {@inheritdoc} + */ + public function isWritable() + { + $mode = $this->getMetadata('mode'); + foreach (['w', 'c', 'a', 'x', 'r+'] as $key) { + if (strpos($mode, $key) !== false) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function write($string) + { + $result = fwrite($this->resource, $string); + if ($result === false) { + throw new \RuntimeException('Unable to write to stream'); + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function isReadable() + { + $mode = $this->getMetadata('mode'); + foreach (['r', 'w+', 'a+', 'c+', 'x+'] as $key) { + if (strpos($mode, $key) !== false) { + return true; + } + } + return false; + } + + /** + * {@inheritdoc} + */ + public function read($length) + { + $string = fread($this->resource, $length); + if ($string === false) { + throw new \RuntimeException('Unable to read from stream'); + } + return $string; + } + + /** + * {@inheritdoc} + */ + public function getContents() + { + $contents = stream_get_contents($this->resource); + if ($contents === false) { + throw new \RuntimeException('Unable to read stream contents'); + } + return $contents; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key = null) + { + if ($this->_metadata === null) { + $this->_metadata = stream_get_meta_data($this->resource); + } + + if ($key === null) { + return $this->_metadata; + } + + return isset($this->_metadata[$key]) ? $this->_metadata[$key] : null; + } +} \ No newline at end of file diff --git a/framework/http/UploadedFile.php b/framework/http/UploadedFile.php new file mode 100644 index 0000000..9b9cc93 --- /dev/null +++ b/framework/http/UploadedFile.php @@ -0,0 +1,388 @@ + + * @author Paul Klimov + * @since 2.0 + */ +class UploadedFile extends BaseObject implements UploadedFileInterface +{ + /** + * @var string the path of the uploaded file on the server. + * Note, this is a temporary file which will be automatically deleted by PHP + * after the current request is processed. + */ + public $tempFilename; + + /** + * @var string the original name of the file being uploaded + */ + private $_clientFilename; + /** + * @var string the MIME-type of the uploaded file (such as "image/gif"). + * Since this MIME type is not checked on the server-side, do not take this value for granted. + * Instead, use [[\yii\helpers\FileHelper::getMimeType()]] to determine the exact MIME type. + */ + private $_clientMediaType; + /** + * @var int the actual size of the uploaded file in bytes + */ + private $_size; + /** + * @var int an error code describing the status of this file uploading. + * @see http://www.php.net/manual/en/features.file-upload.errors.php + */ + private $_error; + /** + * @var StreamInterface stream for this file. + * @since 2.1.0 + */ + private $_stream; + + private static $_files; + + + /** + * String output. + * This is PHP magic method that returns string representation of an object. + * The implementation here returns the uploaded file's name. + * @return string the string representation of the object + */ + public function __toString() + { + return $this->clientFilename; + } + + /** + * Returns an uploaded file for the given model attribute. + * The file should be uploaded using [[\yii\widgets\ActiveField::fileInput()]]. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes. + * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified model attribute. + * @see getInstanceByName() + */ + public static function getInstance($model, $attribute) + { + $name = Html::getInputName($model, $attribute); + return static::getInstanceByName($name); + } + + /** + * Returns all uploaded files for the given model attribute. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes + * for tabular file uploading, e.g. '[1]file'. + * @return UploadedFile[] array of UploadedFile objects. + * Empty array is returned if no available file was found for the given attribute. + */ + public static function getInstances($model, $attribute) + { + $name = Html::getInputName($model, $attribute); + return static::getInstancesByName($name); + } + + /** + * Returns an uploaded file according to the given file input name. + * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). + * @param string $name the name of the file input field. + * @return null|UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified name. + */ + public static function getInstanceByName($name) + { + $files = self::loadFiles(); + return isset($files[$name]) ? new static($files[$name]) : null; + } + + /** + * Returns an array of uploaded files corresponding to the specified file input name. + * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', + * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. + * @param string $name the name of the array of files + * @return UploadedFile[] the array of UploadedFile objects. Empty array is returned + * if no adequate upload was found. Please note that this array will contain + * all files from all sub-arrays regardless how deeply nested they are. + */ + public static function getInstancesByName($name) + { + $files = self::loadFiles(); + if (isset($files[$name])) { + return [new static($files[$name])]; + } + $results = []; + foreach ($files as $key => $file) { + if (strpos($key, "{$name}[") === 0) { + $results[] = new static($file); + } + } + return $results; + } + + /** + * Cleans up the loaded UploadedFile instances. + * This method is mainly used by test scripts to set up a fixture. + */ + public static function reset() + { + self::$_files = null; + } + + /** + * Saves the uploaded file. + * Note that this method uses php's move_uploaded_file() method. If the target file `$file` + * already exists, it will be overwritten. + * @param string $file the file path used to save the uploaded file + * @param bool $deleteTempFile whether to delete the temporary file after saving. + * If true, you will not be able to save the uploaded file again in the current request. + * @return bool true whether the file is saved successfully + * @see error + */ + public function saveAs($file, $deleteTempFile = true) + { + if ($this->error == UPLOAD_ERR_OK) { + if ($deleteTempFile) { + $this->moveTo($file); + return true; + } elseif (is_uploaded_file($this->tempFilename)) { + return copy($this->tempFilename, $file); + } + } + return false; + } + + /** + * @return string original file base name + */ + public function getBaseName() + { + // https://github.com/yiisoft/yii2/issues/11012 + $pathInfo = pathinfo('_' . $this->getClientFilename(), PATHINFO_FILENAME); + return mb_substr($pathInfo, 1, mb_strlen($pathInfo, '8bit'), '8bit'); + } + + /** + * @return string file extension + */ + public function getExtension() + { + return strtolower(pathinfo($this->getClientFilename(), PATHINFO_EXTENSION)); + } + + /** + * @return bool whether there is an error with the uploaded file. + * Check [[error]] for detailed error code information. + */ + public function getHasError() + { + return $this->error != UPLOAD_ERR_OK; + } + + /** + * Creates UploadedFile instances from $_FILE. + * @return array the UploadedFile instances + */ + private static function loadFiles() + { + if (self::$_files === null) { + self::$_files = []; + if (isset($_FILES) && is_array($_FILES)) { + foreach ($_FILES as $class => $info) { + self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + } + } + } + return self::$_files; + } + + /** + * Creates UploadedFile instances from $_FILE recursively. + * @param string $key key for identifying uploaded file: class name and sub-array indexes + * @param mixed $names file names provided by PHP + * @param mixed $tempNames temporary file names provided by PHP + * @param mixed $types file types provided by PHP + * @param mixed $sizes file sizes provided by PHP + * @param mixed $errors uploading issues provided by PHP + */ + private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) + { + if (is_array($names)) { + foreach ($names as $i => $name) { + self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); + } + } elseif ((int) $errors !== UPLOAD_ERR_NO_FILE) { + self::$_files[$key] = [ + 'clientFilename' => $names, + 'tempFilename' => $tempNames, + 'clientMediaType' => $types, + 'size' => $sizes, + 'error' => $errors, + ]; + } + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getStream() + { + if (!$this->_stream instanceof StreamInterface) { + if ($this->_stream === null) { + if ($this->getError() !== UPLOAD_ERR_OK) { + throw new \RuntimeException('Unable to create file stream due to upload error: ' . $this->getError()); + } + $stream = [ + 'class' => FileStream::class, + 'filename' => $this->tempFilename, + 'mode' => 'r', + ]; + } elseif ($this->_stream instanceof \Closure) { + $stream = call_user_func($this->_stream, $this); + } else { + $stream = $this->_stream; + } + + $this->_stream = Instance::ensure($stream, StreamInterface::class); + } + return $this->_stream; + } + + /** + * @param StreamInterface|\Closure|array $stream stream instance or its DI compatible configuration. + * @since 2.1.0 + */ + public function setStream($stream) + { + $this->_stream = $stream; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function moveTo($targetPath) + { + if ($this->error !== UPLOAD_ERR_OK) { + throw new \RuntimeException('Unable to move file due to upload error: ' . $this->error); + } + if (!move_uploaded_file($this->tempFilename, $targetPath)) { + throw new \RuntimeException('Unable to move uploaded file.'); + } + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getSize() + { + return $this->_size; + } + + /** + * @param int $size the actual size of the uploaded file in bytes. + * @throws InvalidArgumentException on invalid size given. + * @since 2.1.0 + */ + public function setSize($size) + { + if (!is_int($size)) { + throw new InvalidArgumentException('"' . get_class($this) . '::$size" must be an integer.'); + } + $this->_size = $size; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getError() + { + return $this->_error; + } + + /** + * @param int $error upload error code. + * @throws InvalidArgumentException on invalid error given. + * @since 2.1.0 + */ + public function setError($error) + { + if (!is_int($error)) { + throw new InvalidArgumentException('"' . get_class($this) . '::$error" must be an integer.'); + } + $this->_error = $error; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getClientFilename() + { + return $this->_clientFilename; + } + + /** + * @param string $clientFilename the original name of the file being uploaded. + * @since 2.1.0 + */ + public function setClientFilename($clientFilename) + { + $this->_clientFilename = $clientFilename; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getClientMediaType() + { + return $this->_clientMediaType; + } + + /** + * @param string $clientMediaType the MIME-type of the uploaded file (such as "image/gif"). + * @since 2.1.0 + */ + public function setClientMediaType($clientMediaType) + { + $this->_clientMediaType = $clientMediaType; + } +} diff --git a/framework/http/Uri.php b/framework/http/Uri.php new file mode 100644 index 0000000..8f0faad --- /dev/null +++ b/framework/http/Uri.php @@ -0,0 +1,548 @@ + 'http', + * 'user' => 'username', + * 'password' => 'password', + * 'host' => 'example.com', + * 'port' => 9090, + * 'path' => '/content/path', + * 'query' => 'foo=some', + * 'fragment' => 'anchor', + * ]); + * ``` + * + * Create from string example: + * + * ```php + * $uri = new Uri(['string' => 'http://example.com?foo=some']); + * ``` + * + * Create using PSR-7 syntax: + * + * ```php + * $uri = (new Uri()) + * ->withScheme('http') + * ->withUserInfo('username', 'password') + * ->withHost('example.com') + * ->withPort(9090) + * ->withPath('/content/path') + * ->withQuery('foo=some') + * ->withFragment('anchor'); + * ``` + * + * @property string $scheme the scheme component of the URI. + * @property string $user + * @property string $password + * @property string $host the hostname to be used. + * @property int|null $port port number. + * @property string $path the path component of the URI + * @property string|array $query the query string or array of query parameters. + * @property string $fragment URI fragment. + * @property string $authority the authority component of the URI. This property is read-only. + * @property string $userInfo the user information component of the URI. This property is read-only. + * + * @author Paul Klimov + * @since 2.1.0 + */ +class Uri extends BaseObject implements UriInterface +{ + /** + * @var string URI complete string. + */ + private $_string; + /** + * @var array URI components. + */ + private $_components; + /** + * @var array scheme default ports in format: `[scheme => port]` + */ + private static $defaultPorts = [ + 'http' => 80, + 'https' => 443, + 'ftp' => 21, + 'gopher' => 70, + 'nntp' => 119, + 'news' => 119, + 'telnet' => 23, + 'tn3270' => 23, + 'imap' => 143, + 'pop' => 110, + 'ldap' => 389, + ]; + + + /** + * @return string URI string representation. + */ + public function getString() + { + if ($this->_string !== null) { + return $this->_string; + } + if ($this->_components === null) { + return ''; + } + return $this->composeUri($this->_components); + } + + /** + * @param string $string URI full string. + */ + public function setString($string) + { + $this->_string = $string; + $this->_components = null; + } + + /** + * {@inheritdoc} + */ + public function getScheme() + { + return $this->getComponent('scheme'); + } + + /** + * Sets up the scheme component of the URI. + * @param string $scheme the scheme. + */ + public function setScheme($scheme) + { + $this->setComponent('scheme', $scheme); + } + + /** + * {@inheritdoc} + */ + public function withScheme($scheme) + { + if ($this->getScheme() === $scheme) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setScheme($scheme); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getAuthority() + { + return $this->composeAuthority($this->getComponents()); + } + + /** + * {@inheritdoc} + */ + public function getUserInfo() + { + return $this->composeUserInfo($this->getComponents()); + } + + /** + * {@inheritdoc} + */ + public function getHost() + { + return $this->getComponent('host', ''); + } + + /** + * Specifies hostname. + * @param string $host the hostname to be used. + */ + public function setHost($host) + { + $this->setComponent('host', $host); + } + + /** + * {@inheritdoc} + */ + public function withHost($host) + { + if ($this->getHost() === $host) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setHost($host); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getPort() + { + return $this->getComponent('port'); + } + + /** + * Specifies port. + * @param int|null $port The port to be used; a `null` value removes the port information. + */ + public function setPort($port) + { + if ($port !== null) { + if (!is_int($port)) { + throw new InvalidArgumentException('URI port must be an integer.'); + } + } + $this->setComponent('port', $port); + } + + /** + * {@inheritdoc} + */ + public function withPort($port) + { + if ($this->getPort() === $port) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setPort($port); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getPath() + { + return $this->getComponent('path', ''); + } + + /** + * Specifies path component of the URI + * @param string $path the path to be used. + */ + public function setPath($path) + { + $this->setComponent('path', $path); + } + + /** + * {@inheritdoc} + */ + public function withPath($path) + { + if ($this->getPath() === $path) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setPath($path); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getQuery() + { + return $this->getComponent('query', ''); + } + + /** + * Specifies query string. + * @param string|array|object $query the query string or array of query parameters. + */ + public function setQuery($query) + { + if (is_array($query) || is_object($query)) { + $query = http_build_query($query); + } + $this->setComponent('query', $query); + } + + /** + * {@inheritdoc} + */ + public function withQuery($query) + { + if ($this->getQuery() === $query) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setQuery($query); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getFragment() + { + return $this->getComponent('fragment', ''); + } + + /** + * Specifies URI fragment. + * @param string $fragment the fragment to be used. + */ + public function setFragment($fragment) + { + $this->setComponent('fragment', $fragment); + } + + /** + * {@inheritdoc} + */ + public function withFragment($fragment) + { + if ($this->getFragment() === $fragment) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setFragment($fragment); + return $newInstance; + } + + /** + * @return string the user name to use for authority. + */ + public function getUser() + { + return $this->getComponent('user', ''); + } + + /** + * @param string $user the user name to use for authority. + */ + public function setUser($user) + { + $this->setComponent('user', $user); + } + + /** + * @return string password associated with [[user]]. + */ + public function getPassword() + { + return $this->getComponent('pass', ''); + } + + /** + * @param string $password password associated with [[user]]. + */ + public function setPassword($password) + { + $this->setComponent('pass', $password); + } + + /** + * {@inheritdoc} + */ + public function withUserInfo($user, $password = null) + { + $userInfo = $user; + if ($password != '') { + $userInfo .= ':' . $password; + } + + if ($userInfo === $this->composeUserInfo($this->getComponents())) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setUser($user); + $newInstance->setPassword($password); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function __toString() + { + // __toString cannot throw exception + // use trigger_error to bypass this limitation + try { + return $this->getString(); + } catch (\Exception $e) { + ErrorHandler::convertExceptionToError($e); + return ''; + } + } + + /** + * Sets up particular URI component. + * @param string $name URI component name. + * @param mixed $value URI component value. + */ + protected function setComponent($name, $value) + { + if ($this->_string !== null) { + $this->_components = $this->parseUri($this->_string); + } + $this->_components[$name] = $value; + $this->_string = null; + } + + /** + * @param string $name URI component name. + * @param mixed $default default value, which should be returned in case component is not exist. + * @return mixed URI component value. + */ + protected function getComponent($name, $default = null) + { + $components = $this->getComponents(); + if (isset($components[$name])) { + return $components[$name]; + } + return $default; + } + + /** + * Returns URI components for this instance as an associative array. + * @return array URI components in format: `[name => value]` + */ + protected function getComponents() + { + if ($this->_components === null) { + if ($this->_string === null) { + return []; + } + $this->_components = $this->parseUri($this->_string); + } + return $this->_components; + } + + /** + * Parses a URI and returns an associative array containing any of the various components of the URI + * that are present. + * @param string $uri the URI string to parse. + * @return array URI components. + */ + protected function parseUri($uri) + { + $components = parse_url($uri); + if ($components === false) { + throw new InvalidArgumentException("URI string '{$uri}' is not a valid URI."); + } + return $components; + } + + /** + * Composes URI string from given components. + * @param array $components URI components. + * @return string URI full string. + */ + protected function composeUri(array $components) + { + $uri = ''; + + $scheme = empty($components['scheme']) ? '' : $components['scheme']; + if ($scheme !== '') { + $uri .= $components['scheme'] . ':'; + } + + $authority = $this->composeAuthority($components); + + if ($authority !== '' || $scheme === 'file') { + // authority separator is added even when the authority is missing/empty for the "file" scheme + // while `file:///myfile` and `file:/myfile` are equivalent according to RFC 3986, `file:///` is more common + // PHP functions and Chrome, for example, use this format + $uri .= '//' . $authority; + } + + if (!empty($components['path'])) { + $uri .= $components['path']; + } + + if (!empty($components['query'])) { + $uri .= '?' . $components['query']; + } + + if (!empty($components['fragment'])) { + $uri .= '#' . $components['fragment']; + } + + return $uri; + } + + /** + * @param array $components URI components. + * @return string user info string. + */ + protected function composeUserInfo(array $components) + { + $userInfo = ''; + if (!empty($components['user'])) { + $userInfo .= $components['user']; + } + if (!empty($components['pass'])) { + $userInfo .= ':' . $components['pass']; + } + return $userInfo; + } + + /** + * @param array $components URI components. + * @return string authority string. + */ + protected function composeAuthority(array $components) + { + $authority = ''; + + $scheme = empty($components['scheme']) ? '' : $components['scheme']; + + if (empty($components['host'])) { + if (in_array($scheme, ['http', 'https'], true)) { + $authority = 'localhost'; + } + } else { + $authority = $components['host']; + } + if (!empty($components['port']) && !$this->isDefaultPort($scheme, $components['port'])) { + $authority .= ':' . $components['port']; + } + + $userInfo = $this->composeUserInfo($components); + if ($userInfo !== '') { + $authority = $userInfo . '@' . $authority; + } + + return $authority; + } + + /** + * Checks whether specified port is default one for the specified scheme. + * @param string $scheme scheme. + * @param int $port port number. + * @return bool whether specified port is default for specified scheme + */ + protected function isDefaultPort($scheme, $port) + { + if (!isset(self::$defaultPorts[$scheme])) { + return false; + } + return self::$defaultPorts[$scheme] == $port; + } +} \ No newline at end of file diff --git a/framework/rest/Serializer.php b/framework/rest/Serializer.php index 24e1304..2d2bbfa 100644 --- a/framework/rest/Serializer.php +++ b/framework/rest/Serializer.php @@ -237,12 +237,11 @@ class Serializer extends Component $links[] = "<$url>; rel=$rel"; } - $this->response->getHeaders() - ->set($this->totalCountHeader, $pagination->totalCount) - ->set($this->pageCountHeader, $pagination->getPageCount()) - ->set($this->currentPageHeader, $pagination->getPage() + 1) - ->set($this->perPageHeader, $pagination->pageSize) - ->set('Link', implode(', ', $links)); + $this->response->setHeader($this->totalCountHeader, $pagination->totalCount); + $this->response->setHeader($this->pageCountHeader, $pagination->getPageCount()); + $this->response->setHeader($this->currentPageHeader, $pagination->getPage() + 1); + $this->response->setHeader($this->perPageHeader, $pagination->pageSize); + $this->response->setHeader('Link', implode(', ', $links)); } /** diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index b679bdf..15ed365 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -12,7 +12,7 @@ use yii\helpers\FileHelper; use yii\helpers\Html; use yii\helpers\Json; use yii\web\JsExpression; -use yii\web\UploadedFile; +use yii\http\UploadedFile; /** * FileValidator verifies if an attribute is receiving a valid uploaded file. @@ -224,7 +224,7 @@ class FileValidator extends Validator */ protected function validateValue($value) { - if (!$value instanceof UploadedFile || $value->error == UPLOAD_ERR_NO_FILE) { + if (!$value instanceof UploadedFile || $value->getError() == UPLOAD_ERR_NO_FILE) { return [$this->uploadRequired, []]; } @@ -234,7 +234,7 @@ class FileValidator extends Validator return [ $this->tooBig, [ - 'file' => $value->name, + 'file' => $value->getClientFilename(), 'limit' => $this->getSizeLimit(), 'formattedLimit' => Yii::$app->formatter->asShortSize($this->getSizeLimit()), ], @@ -243,35 +243,35 @@ class FileValidator extends Validator return [ $this->tooSmall, [ - 'file' => $value->name, + 'file' => $value->getClientFilename(), 'limit' => $this->minSize, 'formattedLimit' => Yii::$app->formatter->asShortSize($this->minSize), ], ]; } elseif (!empty($this->extensions) && !$this->validateExtension($value)) { - return [$this->wrongExtension, ['file' => $value->name, 'extensions' => implode(', ', $this->extensions)]]; + return [$this->wrongExtension, ['file' => $value->getClientFilename(), 'extensions' => implode(', ', $this->extensions)]]; } elseif (!empty($this->mimeTypes) && !$this->validateMimeType($value)) { - return [$this->wrongMimeType, ['file' => $value->name, 'mimeTypes' => implode(', ', $this->mimeTypes)]]; + return [$this->wrongMimeType, ['file' => $value->getClientFilename(), 'mimeTypes' => implode(', ', $this->mimeTypes)]]; } return null; case UPLOAD_ERR_INI_SIZE: case UPLOAD_ERR_FORM_SIZE: return [$this->tooBig, [ - 'file' => $value->name, + 'file' => $value->getClientFilename(), 'limit' => $this->getSizeLimit(), 'formattedLimit' => Yii::$app->formatter->asShortSize($this->getSizeLimit()), ]]; case UPLOAD_ERR_PARTIAL: - Yii::warning('File was only partially uploaded: ' . $value->name, __METHOD__); + Yii::warning('File was only partially uploaded: ' . $value->getClientFilename(), __METHOD__); break; case UPLOAD_ERR_NO_TMP_DIR: - Yii::warning('Missing the temporary folder to store the uploaded file: ' . $value->name, __METHOD__); + Yii::warning('Missing the temporary folder to store the uploaded file: ' . $value->getClientFilename(), __METHOD__); break; case UPLOAD_ERR_CANT_WRITE: - Yii::warning('Failed to write the uploaded file to disk: ' . $value->name, __METHOD__); + Yii::warning('Failed to write the uploaded file to disk: ' . $value->getClientFilename(), __METHOD__); break; case UPLOAD_ERR_EXTENSION: - Yii::warning('File upload was stopped by some PHP extension: ' . $value->name, __METHOD__); + Yii::warning('File upload was stopped by some PHP extension: ' . $value->getClientFilename(), __METHOD__); break; default: break; @@ -353,7 +353,7 @@ class FileValidator extends Validator $extension = mb_strtolower($file->extension, 'UTF-8'); if ($this->checkExtensionByMimeType) { - $mimeType = FileHelper::getMimeType($file->tempName, null, false); + $mimeType = FileHelper::getMimeType($file->tempFilename, null, false); if ($mimeType === null) { return false; } @@ -476,7 +476,7 @@ class FileValidator extends Validator */ protected function validateMimeType($file) { - $fileMimeType = FileHelper::getMimeType($file->tempName); + $fileMimeType = FileHelper::getMimeType($file->tempFilename); foreach ($this->mimeTypes as $mimeType) { if ($mimeType === $fileMimeType) { diff --git a/framework/validators/ImageValidator.php b/framework/validators/ImageValidator.php index 5fbe844..cf4e34b 100644 --- a/framework/validators/ImageValidator.php +++ b/framework/validators/ImageValidator.php @@ -8,7 +8,7 @@ namespace yii\validators; use Yii; -use yii\web\UploadedFile; +use yii\http\UploadedFile; /** * ImageValidator verifies if an attribute is receiving a valid image. @@ -130,30 +130,30 @@ class ImageValidator extends FileValidator */ protected function validateImage($image) { - if (false === ($imageInfo = getimagesize($image->tempName))) { + if (false === ($imageInfo = getimagesize($image->tempFilename))) { return [$this->notImage, ['file' => $image->name]]; } [$width, $height] = $imageInfo; if ($width == 0 || $height == 0) { - return [$this->notImage, ['file' => $image->name]]; + return [$this->notImage, ['file' => $image->getClientFilename()]]; } if ($this->minWidth !== null && $width < $this->minWidth) { - return [$this->underWidth, ['file' => $image->name, 'limit' => $this->minWidth]]; + return [$this->underWidth, ['file' => $image->getClientFilename(), 'limit' => $this->minWidth]]; } if ($this->minHeight !== null && $height < $this->minHeight) { - return [$this->underHeight, ['file' => $image->name, 'limit' => $this->minHeight]]; + return [$this->underHeight, ['file' => $image->getClientFilename(), 'limit' => $this->minHeight]]; } if ($this->maxWidth !== null && $width > $this->maxWidth) { - return [$this->overWidth, ['file' => $image->name, 'limit' => $this->maxWidth]]; + return [$this->overWidth, ['file' => $image->getClientFilename(), 'limit' => $this->maxWidth]]; } if ($this->maxHeight !== null && $height > $this->maxHeight) { - return [$this->overHeight, ['file' => $image->name, 'limit' => $this->maxHeight]]; + return [$this->overHeight, ['file' => $image->getClientFilename(), 'limit' => $this->maxHeight]]; } return null; diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php deleted file mode 100644 index 931a1ed..0000000 --- a/framework/web/Cookie.php +++ /dev/null @@ -1,68 +0,0 @@ - - * @since 2.0 - */ -class Cookie extends \yii\base\BaseObject -{ - /** - * @var string name of the cookie - */ - public $name; - /** - * @var string value of the cookie - */ - public $value = ''; - /** - * @var string domain of the cookie - */ - public $domain = ''; - /** - * @var int the timestamp at which the cookie expires. This is the server timestamp. - * Defaults to 0, meaning "until the browser is closed". - */ - public $expire = 0; - /** - * @var string the path on the server in which the cookie will be available on. The default is '/'. - */ - public $path = '/'; - /** - * @var bool whether cookie should be sent via secure connection - */ - public $secure = false; - /** - * @var bool whether the cookie should be accessible only through the HTTP protocol. - * By setting this property to true, the cookie will not be accessible by scripting languages, - * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. - */ - public $httpOnly = true; - - - /** - * Magic method to turn a cookie object into a string without having to explicitly access [[value]]. - * - * ```php - * if (isset($request->cookies['name'])) { - * $value = (string) $request->cookies['name']; - * } - * ``` - * - * @return string The value of the cookie. If the value property is null, an empty string will be returned. - */ - public function __toString() - { - return (string) $this->value; - } -} diff --git a/framework/web/CookieCollection.php b/framework/web/CookieCollection.php deleted file mode 100644 index b0e7dff..0000000 --- a/framework/web/CookieCollection.php +++ /dev/null @@ -1,243 +0,0 @@ - - * @since 2.0 - */ -class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var bool whether this collection is read only. - */ - public $readOnly = false; - - /** - * @var Cookie[] the cookies in this collection (indexed by the cookie names) - */ - private $_cookies; - - - /** - * Constructor. - * @param array $cookies the cookies that this collection initially contains. This should be - * an array of name-value pairs. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($cookies = [], $config = []) - { - $this->_cookies = $cookies; - parent::__construct($config); - } - - /** - * Returns an iterator for traversing the cookies in the collection. - * This method is required by the SPL interface [[\IteratorAggregate]]. - * It will be implicitly called when you use `foreach` to traverse the collection. - * @return ArrayIterator an iterator for traversing the cookies in the collection. - */ - public function getIterator() - { - return new ArrayIterator($this->_cookies); - } - - /** - * Returns the number of cookies in the collection. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($collection)`. - * @return int the number of cookies in the collection. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of cookies in the collection. - * @return int the number of cookies in the collection. - */ - public function getCount() - { - return count($this->_cookies); - } - - /** - * Returns the cookie with the specified name. - * @param string $name the cookie name - * @return Cookie the cookie with the specified name. Null if the named cookie does not exist. - * @see getValue() - */ - public function get($name) - { - return isset($this->_cookies[$name]) ? $this->_cookies[$name] : null; - } - - /** - * Returns the value of the named cookie. - * @param string $name the cookie name - * @param mixed $defaultValue the value that should be returned when the named cookie does not exist. - * @return mixed the value of the named cookie. - * @see get() - */ - public function getValue($name, $defaultValue = null) - { - return isset($this->_cookies[$name]) ? $this->_cookies[$name]->value : $defaultValue; - } - - /** - * Returns whether there is a cookie with the specified name. - * Note that if a cookie is marked for deletion from browser, this method will return false. - * @param string $name the cookie name - * @return bool whether the named cookie exists - * @see remove() - */ - public function has($name) - { - return isset($this->_cookies[$name]) && $this->_cookies[$name]->value !== '' - && ($this->_cookies[$name]->expire === null || $this->_cookies[$name]->expire >= time()); - } - - /** - * Adds a cookie to the collection. - * If there is already a cookie with the same name in the collection, it will be removed first. - * @param Cookie $cookie the cookie to be added - * @throws InvalidCallException if the cookie collection is read only - */ - public function add($cookie) - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - $this->_cookies[$cookie->name] = $cookie; - } - - /** - * Removes a cookie. - * If `$removeFromBrowser` is true, the cookie will be removed from the browser. - * In this case, a cookie with outdated expiry will be added to the collection. - * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. - * @param bool $removeFromBrowser whether to remove the cookie from browser - * @throws InvalidCallException if the cookie collection is read only - */ - public function remove($cookie, $removeFromBrowser = true) - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - if ($cookie instanceof Cookie) { - $cookie->expire = 1; - $cookie->value = ''; - } else { - $cookie = new Cookie([ - 'name' => $cookie, - 'expire' => 1, - ]); - } - if ($removeFromBrowser) { - $this->_cookies[$cookie->name] = $cookie; - } else { - unset($this->_cookies[$cookie->name]); - } - } - - /** - * Removes all cookies. - * @throws InvalidCallException if the cookie collection is read only - */ - public function removeAll() - { - if ($this->readOnly) { - throw new InvalidCallException('The cookie collection is read only.'); - } - $this->_cookies = []; - } - - /** - * Returns the collection as a PHP array. - * @return array the array representation of the collection. - * The array keys are cookie names, and the array values are the corresponding cookie objects. - */ - public function toArray() - { - return $this->_cookies; - } - - /** - * Populates the cookie collection from an array. - * @param array $array the cookies to populate from - * @since 2.0.3 - */ - public function fromArray(array $array) - { - $this->_cookies = $array; - } - - /** - * Returns whether there is a cookie with the specified name. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `isset($collection[$name])`. - * @param string $name the cookie name - * @return bool whether the named cookie exists - */ - public function offsetExists($name) - { - return $this->has($name); - } - - /** - * Returns the cookie with the specified name. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `$cookie = $collection[$name];`. - * This is equivalent to [[get()]]. - * @param string $name the cookie name - * @return Cookie the cookie with the specified name, null if the named cookie does not exist. - */ - public function offsetGet($name) - { - return $this->get($name); - } - - /** - * Adds the cookie to the collection. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `$collection[$name] = $cookie;`. - * This is equivalent to [[add()]]. - * @param string $name the cookie name - * @param Cookie $cookie the cookie to be added - */ - public function offsetSet($name, $cookie) - { - $this->add($cookie); - } - - /** - * Removes the named cookie. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `unset($collection[$name])`. - * This is equivalent to [[remove()]]. - * @param string $name the cookie name - */ - public function offsetUnset($name) - { - $this->remove($name); - } -} diff --git a/framework/web/ErrorHandler.php b/framework/web/ErrorHandler.php index 0f0400d..1a103a1 100644 --- a/framework/web/ErrorHandler.php +++ b/framework/web/ErrorHandler.php @@ -82,9 +82,9 @@ class ErrorHandler extends \yii\base\ErrorHandler // reset parameters of response to avoid interference with partially created response data // in case the error occurred while sending the response. $response->isSent = false; - $response->stream = null; + $response->bodyRange = null; $response->data = null; - $response->content = null; + $response->setBody(null); } else { $response = new Response(); } diff --git a/framework/web/HeaderCollection.php b/framework/web/HeaderCollection.php deleted file mode 100644 index a92cdbf..0000000 --- a/framework/web/HeaderCollection.php +++ /dev/null @@ -1,236 +0,0 @@ - - * @since 2.0 - */ -class HeaderCollection extends BaseObject implements \IteratorAggregate, \ArrayAccess, \Countable -{ - /** - * @var array the headers in this collection (indexed by the header names) - */ - private $_headers = []; - - - /** - * Returns an iterator for traversing the headers in the collection. - * This method is required by the SPL interface [[\IteratorAggregate]]. - * It will be implicitly called when you use `foreach` to traverse the collection. - * @return ArrayIterator an iterator for traversing the headers in the collection. - */ - public function getIterator() - { - return new ArrayIterator($this->_headers); - } - - /** - * Returns the number of headers in the collection. - * This method is required by the SPL `Countable` interface. - * It will be implicitly called when you use `count($collection)`. - * @return int the number of headers in the collection. - */ - public function count() - { - return $this->getCount(); - } - - /** - * Returns the number of headers in the collection. - * @return int the number of headers in the collection. - */ - public function getCount() - { - return count($this->_headers); - } - - /** - * Returns the named header(s). - * @param string $name the name of the header to return - * @param mixed $default the value to return in case the named header does not exist - * @param bool $first whether to only return the first header of the specified name. - * If false, all headers of the specified name will be returned. - * @return string|array the named header(s). If `$first` is true, a string will be returned; - * If `$first` is false, an array will be returned. - */ - public function get($name, $default = null, $first = true) - { - $name = strtolower($name); - if (isset($this->_headers[$name])) { - return $first ? reset($this->_headers[$name]) : $this->_headers[$name]; - } - - return $default; - } - - /** - * Adds a new header. - * If there is already a header with the same name, it will be replaced. - * @param string $name the name of the header - * @param string $value the value of the header - * @return $this the collection object itself - */ - public function set($name, $value = '') - { - $name = strtolower($name); - $this->_headers[$name] = (array) $value; - - return $this; - } - - /** - * Adds a new header. - * If there is already a header with the same name, the new one will - * be appended to it instead of replacing it. - * @param string $name the name of the header - * @param string $value the value of the header - * @return $this the collection object itself - */ - public function add($name, $value) - { - $name = strtolower($name); - $this->_headers[$name][] = $value; - - return $this; - } - - /** - * Sets a new header only if it does not exist yet. - * If there is already a header with the same name, the new one will be ignored. - * @param string $name the name of the header - * @param string $value the value of the header - * @return $this the collection object itself - */ - public function setDefault($name, $value) - { - $name = strtolower($name); - if (empty($this->_headers[$name])) { - $this->_headers[$name][] = $value; - } - - return $this; - } - - /** - * Returns a value indicating whether the named header exists. - * @param string $name the name of the header - * @return bool whether the named header exists - */ - public function has($name) - { - $name = strtolower($name); - - return isset($this->_headers[$name]); - } - - /** - * Removes a header. - * @param string $name the name of the header to be removed. - * @return array the value of the removed header. Null is returned if the header does not exist. - */ - public function remove($name) - { - $name = strtolower($name); - if (isset($this->_headers[$name])) { - $value = $this->_headers[$name]; - unset($this->_headers[$name]); - return $value; - } - - return null; - } - - /** - * Removes all headers. - */ - public function removeAll() - { - $this->_headers = []; - } - - /** - * Returns the collection as a PHP array. - * @return array the array representation of the collection. - * The array keys are header names, and the array values are the corresponding header values. - */ - public function toArray() - { - return $this->_headers; - } - - /** - * Populates the header collection from an array. - * @param array $array the headers to populate from - * @since 2.0.3 - */ - public function fromArray(array $array) - { - $this->_headers = $array; - } - - /** - * Returns whether there is a header with the specified name. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `isset($collection[$name])`. - * @param string $name the header name - * @return bool whether the named header exists - */ - public function offsetExists($name) - { - return $this->has($name); - } - - /** - * Returns the header with the specified name. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `$header = $collection[$name];`. - * This is equivalent to [[get()]]. - * @param string $name the header name - * @return string the header value with the specified name, null if the named header does not exist. - */ - public function offsetGet($name) - { - return $this->get($name); - } - - /** - * Adds the header to the collection. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `$collection[$name] = $header;`. - * This is equivalent to [[add()]]. - * @param string $name the header name - * @param string $value the header value to be added - */ - public function offsetSet($name, $value) - { - $this->set($name, $value); - } - - /** - * Removes the named header. - * This method is required by the SPL interface [[\ArrayAccess]]. - * It is implicitly called when you use something like `unset($collection[$name])`. - * This is equivalent to [[remove()]]. - * @param string $name the header name - */ - public function offsetUnset($name) - { - $this->remove($name); - } -} diff --git a/framework/web/HtmlResponseFormatter.php b/framework/web/HtmlResponseFormatter.php index 2b5ee81..0e1c50a 100644 --- a/framework/web/HtmlResponseFormatter.php +++ b/framework/web/HtmlResponseFormatter.php @@ -34,7 +34,7 @@ class HtmlResponseFormatter extends Component implements ResponseFormatterInterf if (stripos($this->contentType, 'charset') === false) { $this->contentType .= '; charset=' . $response->charset; } - $response->getHeaders()->set('Content-Type', $this->contentType); + $response->setHeader('Content-Type', $this->contentType); if ($response->data !== null) { $response->content = $response->data; } diff --git a/framework/web/JsonResponseFormatter.php b/framework/web/JsonResponseFormatter.php index 481ffde..26d9ec6 100644 --- a/framework/web/JsonResponseFormatter.php +++ b/framework/web/JsonResponseFormatter.php @@ -80,7 +80,7 @@ class JsonResponseFormatter extends Component implements ResponseFormatterInterf */ protected function formatJson($response) { - $response->getHeaders()->set('Content-Type', 'application/json; charset=UTF-8'); + $response->setHeader('Content-Type', 'application/json; charset=UTF-8'); if ($response->data !== null) { $options = $this->encodeOptions; if ($this->prettyPrint) { @@ -96,7 +96,7 @@ class JsonResponseFormatter extends Component implements ResponseFormatterInterf */ protected function formatJsonp($response) { - $response->getHeaders()->set('Content-Type', 'application/javascript; charset=UTF-8'); + $response->setHeader('Content-Type', 'application/javascript; charset=UTF-8'); if (is_array($response->data) && isset($response->data['data'], $response->data['callback'])) { $response->content = sprintf('%s(%s);', $response->data['callback'], Json::htmlEncode($response->data['data'])); } elseif ($response->data !== null) { diff --git a/framework/web/MultipartFormDataParser.php b/framework/web/MultipartFormDataParser.php index 8baf810..0ca70ab 100644 --- a/framework/web/MultipartFormDataParser.php +++ b/framework/web/MultipartFormDataParser.php @@ -42,7 +42,7 @@ use yii\helpers\StringHelper; * Usage example: * * ```php - * use yii\web\UploadedFile; + * use yii\http\UploadedFile; * * $restRequestData = Yii::$app->request->getBodyParams(); * $uploadedFile = UploadedFile::getInstancesByName('photo'); diff --git a/framework/web/Request.php b/framework/web/Request.php index 2f4ae7f..0ac250f 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -7,8 +7,17 @@ namespace yii\web; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\StreamInterface; +use Psr\Http\Message\UriInterface; use Yii; use yii\base\InvalidConfigException; +use yii\di\Instance; +use yii\http\Cookie; +use yii\http\CookieCollection; +use yii\http\FileStream; +use yii\http\MessageTrait; +use yii\http\Uri; /** * The web Request class represents an HTTP request @@ -41,7 +50,6 @@ use yii\base\InvalidConfigException; * @property string $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned * if no such header is sent. This property is read-only. * @property array $eTags The entity tags. This property is read-only. - * @property HeaderCollection $headers The header collection. This property is read-only. * @property string|null $hostInfo Schema and hostname part (with port number if needed) of the request URL * (e.g. `http://www.yiiframework.com`), null if can't be obtained from `$_SERVER` and wasn't set. See * [[getHostInfo()]] for security related notes on this property. @@ -60,7 +68,9 @@ use yii\base\InvalidConfigException; * @property bool $isSecureConnection If the request is sent via secure channel (https). This property is * read-only. * @property string $method Request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. The value returned is - * turned into upper case. This property is read-only. + * turned into upper case. + * @property UriInterface $uri the URI instance. + * @property mixed $requestTarget the message's request target. * @property string $pathInfo Part of the request URL that is after the entry script and before the question * mark. Note, the returned path info is already URL-decoded. * @property int $port Port number for insecure requests. @@ -84,8 +94,10 @@ use yii\base\InvalidConfigException; * @author Qiang Xue * @since 2.0 */ -class Request extends \yii\base\Request +class Request extends \yii\base\Request implements RequestInterface { + use MessageTrait; + /** * The name of the HTTP header for sending CSRF token. */ @@ -170,9 +182,17 @@ class Request extends \yii\base\Request */ private $_cookies; /** - * @var HeaderCollection Collection of request headers. + * @var string the HTTP method of the request. + */ + private $_method; + /** + * @var UriInterface the URI instance associated with request. + */ + private $_uri; + /** + * @var mixed the message's request target. */ - private $_headers; + private $_requestTarget; /** @@ -197,56 +217,160 @@ class Request extends \yii\base\Request } /** - * Returns the header collection. - * The header collection contains incoming HTTP headers. - * @return HeaderCollection the header collection + * Returns default message's headers, which should be present once [[headerCollection]] is instantiated. + * @return string[][] an associative array of the message's headers. */ - public function getHeaders() + protected function defaultHeaders() { - if ($this->_headers === null) { - $this->_headers = new HeaderCollection(); - if (function_exists('getallheaders')) { - $headers = getallheaders(); - } elseif (function_exists('http_get_request_headers')) { - $headers = http_get_request_headers(); - } else { - foreach ($_SERVER as $name => $value) { - if (strncmp($name, 'HTTP_', 5) === 0) { - $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); - $this->_headers->add($name, $value); - } + if (function_exists('getallheaders')) { + $headers = getallheaders(); + } elseif (function_exists('http_get_request_headers')) { + $headers = http_get_request_headers(); + } else { + $headers = []; + foreach ($_SERVER as $name => $value) { + if (strncmp($name, 'HTTP_', 5) === 0) { + $name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5))))); + $headers[$name] = $value; } - - return $this->_headers; - } - foreach ($headers as $name => $value) { - $this->_headers->add($name, $value); } } - return $this->_headers; + foreach ($headers as $name => $value) { + $headers[strtolower($name)] = (array)$value; + } + + return $headers; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getRequestTarget() + { + if ($this->_requestTarget === null) { + $this->_requestTarget = $this->getUri()->__toString(); + } + return $this->_requestTarget; } /** - * Returns the method of the current request (e.g. GET, POST, HEAD, PUT, PATCH, DELETE). - * @return string request method, such as GET, POST, HEAD, PUT, PATCH, DELETE. - * The value returned is turned into upper case. + * Specifies the message's request target + * @param mixed $requestTarget the message's request target. + * @since 2.1.0 + */ + public function setRequestTarget($requestTarget) + { + $this->_requestTarget = $requestTarget; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withRequestTarget($requestTarget) + { + if ($this->getRequestTarget() === $requestTarget) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setRequestTarget($requestTarget); + return $newInstance; + } + + /** + * {@inheritdoc} */ public function getMethod() { - if (isset($_POST[$this->methodParam])) { - return strtoupper($_POST[$this->methodParam]); + if ($this->_method === null) { + if (isset($_POST[$this->methodParam])) { + $this->_method = $_POST[$this->methodParam]; + } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { + $this->_method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']; + } elseif (isset($_SERVER['REQUEST_METHOD'])) { + $this->_method = $_SERVER['REQUEST_METHOD']; + } else { + $this->_method = 'GET'; + } } + return $this->_method; + } + + /** + * Specifies request HTTP method. + * @param string $method case-sensitive HTTP method. + * @since 2.1.0 + */ + public function setMethod($method) + { + $this->_method = $method; + } - if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) { - return strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']); + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withMethod($method) + { + if ($this->getMethod() === $method) { + return $this; } - if (isset($_SERVER['REQUEST_METHOD'])) { - return strtoupper($_SERVER['REQUEST_METHOD']); + $newInstance = clone $this; + $newInstance->setMethod($method); + return $newInstance; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function getUri() + { + if (!$this->_uri instanceof UriInterface) { + if ($this->_uri === null) { + $uri = new Uri(['string' => $this->getAbsoluteUrl()]); + } elseif ($this->_uri instanceof \Closure) { + $uri = call_user_func($this->_uri, $this); + } else { + $uri = $this->_uri; + } + + $this->_uri = Instance::ensure($uri, UriInterface::class); + } + return $this->_uri; + } + + /** + * Specifies the URI instance. + * @param UriInterface|\Closure|array $uri URI instance or its DI compatible configuration. + * @since 2.1.0 + */ + public function setUri($uri) + { + $this->_uri = $uri; + } + + /** + * {@inheritdoc} + * @since 2.1.0 + */ + public function withUri(UriInterface $uri, $preserveHost = false) + { + if ($this->getUri() === $uri) { + return $this; } - return 'GET'; + $newInstance = clone $this; + + $newInstance->setUri($uri); + if (!$preserveHost) { + return $newInstance->withHeader('host', $uri->getHost()); + } + return $newInstance; } /** @@ -344,6 +468,18 @@ class Request extends \yii\base\Request (stripos($_SERVER['HTTP_USER_AGENT'], 'Shockwave') !== false || stripos($_SERVER['HTTP_USER_AGENT'], 'Flash') !== false); } + /** + * Returns default message body to be used in case it is not explicitly set. + * @return StreamInterface default body instance. + */ + protected function defaultBody() + { + return new FileStream([ + 'filename' => 'php://input', + 'mode' => 'r', + ]); + } + private $_rawBody; /** @@ -353,7 +489,7 @@ class Request extends \yii\base\Request public function getRawBody() { if ($this->_rawBody === null) { - $this->_rawBody = file_get_contents('php://input'); + $this->_rawBody = $this->getBody()->__toString(); } return $this->_rawBody; @@ -925,7 +1061,7 @@ class Request extends \yii\base\Request */ public function getOrigin() { - return $this->getHeaders()->get('origin'); + return $this->getHeaderLine('origin'); } /** @@ -1400,7 +1536,7 @@ class Request extends \yii\base\Request */ public function getCsrfTokenFromHeader() { - return $this->headers->get(static::CSRF_HEADER); + return $this->getHeaderLine(static::CSRF_HEADER); } /** @@ -1440,6 +1576,7 @@ class Request extends \yii\base\Request return true; } + $trueToken = $this->getCsrfToken(); if ($clientSuppliedToken !== null) { @@ -1467,4 +1604,18 @@ class Request extends \yii\base\Request return $security->unmaskToken($clientSuppliedToken) === $security->unmaskToken($trueToken); } + + /** + * {@inheritdoc} + */ + public function __clone() + { + parent::__clone(); + + $this->cloneHttpMessageInternals(); + + if (is_object($this->_cookies)) { + $this->_cookies = clone $this->_cookies; + } + } } diff --git a/framework/web/Response.php b/framework/web/Response.php index 59056ab..945621f 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -7,14 +7,19 @@ namespace yii\web; +use Psr\Http\Message\ResponseInterface; use Yii; use yii\base\InvalidArgumentException; use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; use yii\helpers\FileHelper; use yii\helpers\Inflector; use yii\helpers\StringHelper; use yii\helpers\Url; +use yii\http\CookieCollection; +use yii\http\HeaderCollection; +use yii\http\MemoryStream; +use yii\http\MessageTrait; +use yii\http\ResourceStream; /** * The web Response class represents an HTTP response @@ -40,7 +45,6 @@ use yii\helpers\Url; * * @property CookieCollection $cookies The cookie collection. This property is read-only. * @property string $downloadHeaders The attachment file name. This property is write-only. - * @property HeaderCollection $headers The header collection. This property is read-only. * @property bool $isClientError Whether this response indicates a client error. This property is read-only. * @property bool $isEmpty Whether this response is empty. This property is read-only. * @property bool $isForbidden Whether this response indicates the current request is forbidden. This property @@ -55,13 +59,16 @@ use yii\helpers\Url; * @property bool $isSuccessful Whether this response is successful. This property is read-only. * @property int $statusCode The HTTP status code to send with the response. * @property \Exception|\Error $statusCodeByException The exception object. This property is write-only. + * @property string $content body content string. * * @author Qiang Xue * @author Carsten Brandt * @since 2.0 */ -class Response extends \yii\base\Response +class Response extends \yii\base\Response implements ResponseInterface { + use MessageTrait; + /** * @event ResponseEvent an event that is triggered at the beginning of [[send()]]. */ @@ -129,17 +136,10 @@ class Response extends \yii\base\Response */ public $data; /** - * @var string the response content. When [[data]] is not null, it will be converted into [[content]] - * according to [[format]] when the response is being sent out. - * @see data + * @var array the stream range to be applied on [[send()]]. This should be an array of the begin position and the end position. + * Note that when this property is set, the [[data]] property will be ignored by [[send()]]. */ - public $content; - /** - * @var resource|array the stream to be sent. This can be a stream handle or an array of stream handle, - * the begin position and the end position. Note that when this property is set, the [[data]] and [[content]] - * properties will be ignored by [[send()]]. - */ - public $stream; + public $bodyRange; /** * @var string the charset of the text response. If not set, it will use * the value of [[Application::charset]]. @@ -149,12 +149,7 @@ class Response extends \yii\base\Response * @var string the HTTP status description that comes together with the status code. * @see httpStatuses */ - public $statusText = 'OK'; - /** - * @var string the version of the HTTP protocol to use. If not set, it will be determined via `$_SERVER['SERVER_PROTOCOL']`, - * or '1.1' if that is not available. - */ - public $version; + public $reasonPhrase = 'OK'; /** * @var bool whether the response has been sent. If this is true, calling [[send()]] will do nothing. */ @@ -236,10 +231,6 @@ class Response extends \yii\base\Response * @var int the HTTP status code to send with the response. */ private $_statusCode = 200; - /** - * @var HeaderCollection - */ - private $_headers; /** @@ -247,13 +238,6 @@ class Response extends \yii\base\Response */ public function init() { - if ($this->version === null) { - if (isset($_SERVER['SERVER_PROTOCOL']) && $_SERVER['SERVER_PROTOCOL'] === 'HTTP/1.0') { - $this->version = '1.0'; - } else { - $this->version = '1.1'; - } - } if ($this->charset === null) { $this->charset = Yii::$app->charset; } @@ -261,7 +245,7 @@ class Response extends \yii\base\Response } /** - * @return int the HTTP status code to send with the response. + * {@inheritdoc} */ public function getStatusCode() { @@ -271,29 +255,51 @@ class Response extends \yii\base\Response /** * Sets the response status code. * This method will set the corresponding status text if `$text` is null. - * @param int $value the status code - * @param string $text the status text. If not set, it will be set automatically based on the status code. + * @param int $code the status code + * @param string $reasonPhrase the status text. If not set, it will be set automatically based on the status code. * @throws InvalidArgumentException if the status code is invalid. * @return $this the response object itself */ - public function setStatusCode($value, $text = null) + public function setStatusCode($code, $reasonPhrase = null) { - if ($value === null) { - $value = 200; + if ($code === null) { + $code = 200; } - $this->_statusCode = (int) $value; + $this->_statusCode = (int) $code; if ($this->getIsInvalid()) { - throw new InvalidArgumentException("The HTTP status code is invalid: $value"); + throw new InvalidArgumentException("The HTTP status code is invalid: $code"); } - if ($text === null) { - $this->statusText = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : ''; + if (empty($reasonPhrase)) { + $this->reasonPhrase = isset(static::$httpStatuses[$this->_statusCode]) ? static::$httpStatuses[$this->_statusCode] : ''; } else { - $this->statusText = $text; + $this->reasonPhrase = $reasonPhrase; } return $this; } /** + * {@inheritdoc} + */ + public function withStatus($code, $reasonPhrase = '') + { + if ($this->getStatusCode() === $code && $this->reasonPhrase === $reasonPhrase) { + return $this; + } + + $newInstance = clone $this; + $newInstance->setStatusCode($code, $reasonPhrase); + return $newInstance; + } + + /** + * {@inheritdoc} + */ + public function getReasonPhrase() + { + return $this->reasonPhrase; + } + + /** * Sets the response status code based on the exception. * @param \Exception|\Error $e the exception object. * @throws InvalidArgumentException if the status code is invalid. @@ -311,16 +317,23 @@ class Response extends \yii\base\Response } /** - * Returns the header collection. - * The header collection contains the currently registered HTTP headers. - * @return HeaderCollection the header collection + * @return string body content string. + * @since 2.1.0 */ - public function getHeaders() + public function getContent() { - if ($this->_headers === null) { - $this->_headers = new HeaderCollection(); - } - return $this->_headers; + return $this->getBody()->__toString(); + } + + /** + * @param string $content body content string. + * @since 2.1.0 + */ + public function setContent($content) + { + $body = new MemoryStream(); + $body->write($content); + $this->setBody($body); } /** @@ -345,14 +358,14 @@ class Response extends \yii\base\Response */ public function clear() { - $this->_headers = null; + $this->_headerCollection = null; $this->_cookies = null; $this->_statusCode = 200; - $this->statusText = 'OK'; + $this->reasonPhrase = 'OK'; $this->data = null; - $this->stream = null; - $this->content = null; + $this->bodyRange = null; $this->isSent = false; + $this->setBody(null); } /** @@ -363,7 +376,7 @@ class Response extends \yii\base\Response if (headers_sent()) { return; } - if ($this->_headers) { + if ($this->_headerCollection) { $headers = $this->getHeaders(); foreach ($headers as $name => $values) { $name = str_replace(' ', '-', ucwords(str_replace('-', ' ', $name))); @@ -376,7 +389,8 @@ class Response extends \yii\base\Response } } $statusCode = $this->getStatusCode(); - header("HTTP/{$this->version} {$statusCode} {$this->statusText}"); + $protocolVersion = $this->getProtocolVersion(); + header("HTTP/{$protocolVersion} {$statusCode} {$this->reasonPhrase}"); $this->sendCookies(); } @@ -409,32 +423,40 @@ class Response extends \yii\base\Response */ protected function sendContent() { - if ($this->stream === null) { - echo $this->content; - - return; + $body = $this->getBody(); + if (!$body->isReadable()) { + throw new \RuntimeException('Unable to send content: body stream is not readable.'); } set_time_limit(0); // Reset time limit for big files $chunkSize = 8 * 1024 * 1024; // 8MB per chunk - if (is_array($this->stream)) { - [$handle, $begin, $end] = $this->stream; - fseek($handle, $begin); - while (!feof($handle) && ($pos = ftell($handle)) <= $end) { + if (is_array($this->bodyRange)) { + [$begin, $end] = $this->bodyRange; + + if (!$body->isSeekable()) { + throw new \RuntimeException('Unable to send content in range: body stream is not seekable.'); + } + + $body->seek($begin); + while (!$body->eof() && ($pos = $body->tell()) <= $end) { if ($pos + $chunkSize > $end) { $chunkSize = $end - $pos + 1; } - echo fread($handle, $chunkSize); + echo $body->read($chunkSize); flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. } - fclose($handle); + $body->close(); } else { - while (!feof($this->stream)) { - echo fread($this->stream, $chunkSize); - flush(); + if ($body->isSeekable()) { + $body->seek(0); + } + while (!$body->eof()) { + echo $body->read($chunkSize); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. } - fclose($this->stream); + $body->close(); + return; } } @@ -507,31 +529,30 @@ class Response extends \yii\base\Response */ public function sendContentAsFile($content, $attachmentName, $options = []) { - $headers = $this->getHeaders(); - $contentLength = StringHelper::byteLength($content); $range = $this->getHttpRange($contentLength); if ($range === false) { - $headers->set('Content-Range', "bytes */$contentLength"); + $this->setHeader('Content-Range', "bytes */$contentLength"); throw new RangeNotSatisfiableHttpException(); } [$begin, $end] = $range; + $body = new MemoryStream(); if ($begin != 0 || $end != $contentLength - 1) { $this->setStatusCode(206); - $headers->set('Content-Range', "bytes $begin-$end/$contentLength"); - $this->content = StringHelper::byteSubstr($content, $begin, $end - $begin + 1); + $this->setHeader('Content-Range', "bytes $begin-$end/$contentLength"); + $body->write(StringHelper::byteSubstr($content, $begin, $end - $begin + 1)); } else { $this->setStatusCode(200); - $this->content = $content; + $body->write($content); } $mimeType = isset($options['mimeType']) ? $options['mimeType'] : 'application/octet-stream'; $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1); $this->format = self::FORMAT_RAW; - + $this->setBody($body); return $this; } @@ -558,7 +579,6 @@ class Response extends \yii\base\Response */ public function sendStreamAsFile($handle, $attachmentName, $options = []) { - $headers = $this->getHeaders(); if (isset($options['fileSize'])) { $fileSize = $options['fileSize']; } else { @@ -568,14 +588,14 @@ class Response extends \yii\base\Response $range = $this->getHttpRange($fileSize); if ($range === false) { - $headers->set('Content-Range', "bytes */$fileSize"); + $this->setHeader('Content-Range', "bytes */$fileSize"); throw new RangeNotSatisfiableHttpException(); } [$begin, $end] = $range; if ($begin != 0 || $end != $fileSize - 1) { $this->setStatusCode(206); - $headers->set('Content-Range', "bytes $begin-$end/$fileSize"); + $this->setHeader('Content-Range', "bytes $begin-$end/$fileSize"); } else { $this->setStatusCode(200); } @@ -584,8 +604,12 @@ class Response extends \yii\base\Response $this->setDownloadHeaders($attachmentName, $mimeType, !empty($options['inline']), $end - $begin + 1); $this->format = self::FORMAT_RAW; - $this->stream = [$handle, $begin, $end]; + $this->bodyRange = [$begin, $end]; + $body = new ResourceStream(); + $body->resource = $handle; + + $this->setBody($body); return $this; } @@ -600,21 +624,28 @@ class Response extends \yii\base\Response */ public function setDownloadHeaders($attachmentName, $mimeType = null, $inline = false, $contentLength = null) { - $headers = $this->getHeaders(); - $disposition = $inline ? 'inline' : 'attachment'; - $headers->setDefault('Pragma', 'public') - ->setDefault('Accept-Ranges', 'bytes') - ->setDefault('Expires', '0') - ->setDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') - ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); + + $headers = [ + 'Pragma' => 'public', + 'Accept-Ranges' => 'bytes', + 'Expires' => '0', + 'Cache-Control' => 'must-revalidate, post-check=0, pre-check=0', + 'Content-Disposition' => $this->getDispositionHeaderValue($disposition, $attachmentName), + ]; if ($mimeType !== null) { - $headers->setDefault('Content-Type', $mimeType); + $headers['Content-Type'] = $mimeType; } if ($contentLength !== null) { - $headers->setDefault('Content-Length', $contentLength); + $headers['Content-Length'] = $contentLength; + } + + foreach ($headers as $name => $value) { + if (!$this->hasHeader($name)) { + $this->setHeader($name, $value); + } } return $this; @@ -728,10 +759,18 @@ class Response extends \yii\base\Response } $disposition = empty($options['inline']) ? 'attachment' : 'inline'; - $this->getHeaders() - ->setDefault($xHeader, $filePath) - ->setDefault('Content-Type', $mimeType) - ->setDefault('Content-Disposition', $this->getDispositionHeaderValue($disposition, $attachmentName)); + + $headers = [ + $xHeader => $filePath, + 'Content-Type' => $mimeType, + 'Content-Disposition' => $this->getDispositionHeaderValue($disposition, $attachmentName), + ]; + + foreach ($headers as $name => $value) { + if (!$this->hasHeader($name)) { + $this->setHeader($name, $value); + } + } $this->format = self::FORMAT_RAW; @@ -843,20 +882,20 @@ class Response extends \yii\base\Response if ($checkAjax) { if (Yii::$app->getRequest()->getIsAjax()) { - if (Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null && $statusCode === 302) { + if (Yii::$app->getRequest()->hasHeader('X-Ie-Redirect-Compatibility') && $statusCode === 302) { // Ajax 302 redirect in IE does not work. Change status code to 200. See https://github.com/yiisoft/yii2/issues/9670 $statusCode = 200; } if (Yii::$app->getRequest()->getIsPjax()) { - $this->getHeaders()->set('X-Pjax-Url', $url); + $this->setHeader('X-Pjax-Url', $url); } else { - $this->getHeaders()->set('X-Redirect', $url); + $this->setHeader('X-Redirect', $url); } } else { - $this->getHeaders()->set('Location', $url); + $this->setHeader('Location', $url); } } else { - $this->getHeaders()->set('Location', $url); + $this->setHeader('Location', $url); } $this->setStatusCode($statusCode); @@ -1022,7 +1061,7 @@ class Response extends \yii\base\Response */ protected function prepare() { - if ($this->stream !== null) { + if ($this->bodyRange !== null) { return; } @@ -1033,26 +1072,44 @@ class Response extends \yii\base\Response } if ($formatter instanceof ResponseFormatterInterface) { $formatter->format($this); - } else { - throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); - } - } elseif ($this->format === self::FORMAT_RAW) { - if ($this->data !== null) { - $this->content = $this->data; + return; } - } else { + throw new InvalidConfigException("The '{$this->format}' response formatter is invalid. It must implement the ResponseFormatterInterface."); + } elseif ($this->format !== self::FORMAT_RAW) { throw new InvalidConfigException("Unsupported response format: {$this->format}"); } - if (is_array($this->content)) { - throw new InvalidArgumentException('Response content must not be an array.'); - } elseif (is_object($this->content)) { - if (method_exists($this->content, '__toString')) { - $this->content = $this->content->__toString(); + if ($this->data !== null) { + if (is_array($this->data)) { + throw new InvalidArgumentException('Response raw data must not be an array.'); + } elseif (is_object($this->data)) { + if (method_exists($this->data, '__toString')) { + $content = $this->data->__toString(); + } else { + throw new InvalidArgumentException('Response raw data must be a string or an object implementing ' + . ' __toString().'); + } } else { - throw new InvalidArgumentException('Response content must be a string or an object implementing ' - . ' __toString().'); + $content = $this->data; } + + $body = new MemoryStream(); + $body->write($content); + $this->setBody($body); + } + } + + /** + * {@inheritdoc} + */ + public function __clone() + { + parent::__clone(); + + $this->cloneHttpMessageInternals(); + + if (is_object($this->_cookies)) { + $this->_cookies = clone $this->_cookies; } } } diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php deleted file mode 100644 index c34a0e6..0000000 --- a/framework/web/UploadedFile.php +++ /dev/null @@ -1,240 +0,0 @@ - - * @since 2.0 - */ -class UploadedFile extends BaseObject -{ - /** - * @var string the original name of the file being uploaded - */ - public $name; - /** - * @var string the path of the uploaded file on the server. - * Note, this is a temporary file which will be automatically deleted by PHP - * after the current request is processed. - */ - public $tempName; - /** - * @var string the MIME-type of the uploaded file (such as "image/gif"). - * Since this MIME type is not checked on the server-side, do not take this value for granted. - * Instead, use [[\yii\helpers\FileHelper::getMimeType()]] to determine the exact MIME type. - */ - public $type; - /** - * @var int the actual size of the uploaded file in bytes - */ - public $size; - /** - * @var int an error code describing the status of this file uploading. - * @see http://www.php.net/manual/en/features.file-upload.errors.php - */ - public $error; - - private static $_files; - - - /** - * String output. - * This is PHP magic method that returns string representation of an object. - * The implementation here returns the uploaded file's name. - * @return string the string representation of the object - */ - public function __toString() - { - return $this->name; - } - - /** - * Returns an uploaded file for the given model attribute. - * The file should be uploaded using [[\yii\widgets\ActiveField::fileInput()]]. - * @param \yii\base\Model $model the data model - * @param string $attribute the attribute name. The attribute name may contain array indexes. - * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. - * @return UploadedFile the instance of the uploaded file. - * Null is returned if no file is uploaded for the specified model attribute. - * @see getInstanceByName() - */ - public static function getInstance($model, $attribute) - { - $name = Html::getInputName($model, $attribute); - return static::getInstanceByName($name); - } - - /** - * Returns all uploaded files for the given model attribute. - * @param \yii\base\Model $model the data model - * @param string $attribute the attribute name. The attribute name may contain array indexes - * for tabular file uploading, e.g. '[1]file'. - * @return UploadedFile[] array of UploadedFile objects. - * Empty array is returned if no available file was found for the given attribute. - */ - public static function getInstances($model, $attribute) - { - $name = Html::getInputName($model, $attribute); - return static::getInstancesByName($name); - } - - /** - * Returns an uploaded file according to the given file input name. - * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). - * @param string $name the name of the file input field. - * @return null|UploadedFile the instance of the uploaded file. - * Null is returned if no file is uploaded for the specified name. - */ - public static function getInstanceByName($name) - { - $files = self::loadFiles(); - return isset($files[$name]) ? new static($files[$name]) : null; - } - - /** - * Returns an array of uploaded files corresponding to the specified file input name. - * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', - * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. - * @param string $name the name of the array of files - * @return UploadedFile[] the array of UploadedFile objects. Empty array is returned - * if no adequate upload was found. Please note that this array will contain - * all files from all sub-arrays regardless how deeply nested they are. - */ - public static function getInstancesByName($name) - { - $files = self::loadFiles(); - if (isset($files[$name])) { - return [new static($files[$name])]; - } - $results = []; - foreach ($files as $key => $file) { - if (strpos($key, "{$name}[") === 0) { - $results[] = new static($file); - } - } - return $results; - } - - /** - * Cleans up the loaded UploadedFile instances. - * This method is mainly used by test scripts to set up a fixture. - */ - public static function reset() - { - self::$_files = null; - } - - /** - * Saves the uploaded file. - * Note that this method uses php's move_uploaded_file() method. If the target file `$file` - * already exists, it will be overwritten. - * @param string $file the file path used to save the uploaded file - * @param bool $deleteTempFile whether to delete the temporary file after saving. - * If true, you will not be able to save the uploaded file again in the current request. - * @return bool true whether the file is saved successfully - * @see error - */ - public function saveAs($file, $deleteTempFile = true) - { - if ($this->error == UPLOAD_ERR_OK) { - if ($deleteTempFile) { - return move_uploaded_file($this->tempName, $file); - } elseif (is_uploaded_file($this->tempName)) { - return copy($this->tempName, $file); - } - } - return false; - } - - /** - * @return string original file base name - */ - public function getBaseName() - { - // https://github.com/yiisoft/yii2/issues/11012 - $pathInfo = pathinfo('_' . $this->name, PATHINFO_FILENAME); - return mb_substr($pathInfo, 1, mb_strlen($pathInfo, '8bit'), '8bit'); - } - - /** - * @return string file extension - */ - public function getExtension() - { - return strtolower(pathinfo($this->name, PATHINFO_EXTENSION)); - } - - /** - * @return bool whether there is an error with the uploaded file. - * Check [[error]] for detailed error code information. - */ - public function getHasError() - { - return $this->error != UPLOAD_ERR_OK; - } - - /** - * Creates UploadedFile instances from $_FILE. - * @return array the UploadedFile instances - */ - private static function loadFiles() - { - if (self::$_files === null) { - self::$_files = []; - if (isset($_FILES) && is_array($_FILES)) { - foreach ($_FILES as $class => $info) { - self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); - } - } - } - return self::$_files; - } - - /** - * Creates UploadedFile instances from $_FILE recursively. - * @param string $key key for identifying uploaded file: class name and sub-array indexes - * @param mixed $names file names provided by PHP - * @param mixed $tempNames temporary file names provided by PHP - * @param mixed $types file types provided by PHP - * @param mixed $sizes file sizes provided by PHP - * @param mixed $errors uploading issues provided by PHP - */ - private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) - { - if (is_array($names)) { - foreach ($names as $i => $name) { - self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); - } - } elseif ((int) $errors !== UPLOAD_ERR_NO_FILE) { - self::$_files[$key] = [ - 'name' => $names, - 'tempName' => $tempNames, - 'type' => $types, - 'size' => $sizes, - 'error' => $errors, - ]; - } - } -} diff --git a/framework/web/User.php b/framework/web/User.php index 8177c18..994c50a 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Component; use yii\base\InvalidConfigException; use yii\base\InvalidValueException; +use yii\http\Cookie; use yii\rbac\CheckAccessInterface; /** diff --git a/framework/web/XmlResponseFormatter.php b/framework/web/XmlResponseFormatter.php index 326e649..a31f94e 100644 --- a/framework/web/XmlResponseFormatter.php +++ b/framework/web/XmlResponseFormatter.php @@ -68,7 +68,7 @@ class XmlResponseFormatter extends Component implements ResponseFormatterInterfa if (stripos($this->contentType, 'charset') === false) { $this->contentType .= '; charset=' . $charset; } - $response->getHeaders()->set('Content-Type', $this->contentType); + $response->setHeader('Content-Type', $this->contentType); if ($response->data !== null) { $dom = new DOMDocument($this->version, $charset); if (!empty($this->rootTag)) { diff --git a/framework/widgets/Pjax.php b/framework/widgets/Pjax.php index 4d85673..51f5adf 100644 --- a/framework/widgets/Pjax.php +++ b/framework/widgets/Pjax.php @@ -182,9 +182,8 @@ class Pjax extends Widget */ protected function requiresPjax() { - $headers = Yii::$app->getRequest()->getHeaders(); - - return $headers->get('X-Pjax') && explode(' ', $headers->get('X-Pjax-Container'))[0] === '#' . $this->options['id']; + $request = Yii::$app->getRequest(); + return $request->hasHeader('X-Pjax') && explode(' ', $request->getHeader('X-Pjax-Container')[0])[0] === '#' . $this->options['id']; } /** diff --git a/tests/framework/captcha/CaptchaActionTest.php b/tests/framework/captcha/CaptchaActionTest.php index 4c9bb2a..d060e1f 100644 --- a/tests/framework/captcha/CaptchaActionTest.php +++ b/tests/framework/captcha/CaptchaActionTest.php @@ -53,12 +53,11 @@ class CaptchaActionTest extends TestCase /* @var $response Response */ $response = Yii::$app->response; $this->assertEquals(Response::FORMAT_RAW, $response->format); - $headerCollection = $response->getHeaders(); - $this->assertEquals($driver->getImageMimeType(), $headerCollection->get('Content-type')); - $this->assertEquals('binary', $headerCollection->get('Content-Transfer-Encoding')); - $this->assertEquals('public', $headerCollection->get('Pragma')); - $this->assertEquals('0', $headerCollection->get('Expires')); - $this->assertEquals('must-revalidate, post-check=0, pre-check=0', $headerCollection->get('Cache-Control')); + $this->assertEquals([$driver->getImageMimeType()], $response->getHeader('Content-type')); + $this->assertEquals(['binary'], $response->getHeader('Content-Transfer-Encoding')); + $this->assertEquals(['public'], $response->getHeader('Pragma')); + $this->assertEquals(['0'], $response->getHeader('Expires')); + $this->assertEquals(['must-revalidate, post-check=0, pre-check=0'], $response->getHeader('Cache-Control')); } public function testRunRefresh() diff --git a/tests/framework/filters/HttpCacheTest.php b/tests/framework/filters/HttpCacheTest.php index 399eb8f..a398760 100644 --- a/tests/framework/filters/HttpCacheTest.php +++ b/tests/framework/filters/HttpCacheTest.php @@ -41,8 +41,7 @@ class HttpCacheTest extends \yiiunit\TestCase }; $httpCache->beforeAction(null); $response = Yii::$app->getResponse(); - $this->assertFalse($response->getHeaders()->offsetExists('Pragma')); - $this->assertNotSame($response->getHeaders()->get('Pragma'), ''); + $this->assertFalse($response->hasHeader('Pragma')); } /** @@ -88,7 +87,7 @@ class HttpCacheTest extends \yiiunit\TestCase }; $httpCache->beforeAction(null); $response = Yii::$app->getResponse(); - $this->assertFalse($response->getHeaders()->offsetExists('ETag')); + $this->assertFalse($response->hasHeader('ETag')); $httpCache->etagSeed = function ($action, $params) { return ''; @@ -96,9 +95,9 @@ class HttpCacheTest extends \yiiunit\TestCase $httpCache->beforeAction(null); $response = Yii::$app->getResponse(); - $this->assertTrue($response->getHeaders()->offsetExists('ETag')); + $this->assertTrue($response->hasHeader('ETag')); - $etag = $response->getHeaders()->get('ETag'); + $etag = $response->getHeaderLine('ETag'); $this->assertStringStartsWith('"', $etag); $this->assertStringEndsWith('"', $etag); @@ -107,9 +106,9 @@ class HttpCacheTest extends \yiiunit\TestCase $httpCache->beforeAction(null); $response = Yii::$app->getResponse(); - $this->assertTrue($response->getHeaders()->offsetExists('ETag')); + $this->assertTrue($response->hasHeader('ETag')); - $etag = $response->getHeaders()->get('ETag'); + $etag = $response->getHeaderLine('ETag'); $this->assertStringStartsWith('W/"', $etag); $this->assertStringEndsWith('"', $etag); } diff --git a/tests/framework/filters/PageCacheTest.php b/tests/framework/filters/PageCacheTest.php index e5ad433..3c53b6d 100644 --- a/tests/framework/filters/PageCacheTest.php +++ b/tests/framework/filters/PageCacheTest.php @@ -16,7 +16,7 @@ use yii\filters\PageCache; use yii\helpers\ArrayHelper; use yii\helpers\Json; use yii\web\Controller; -use yii\web\Cookie; +use yii\http\Cookie; use yii\web\View; use yiiunit\framework\caching\CacheTestCase; use yiiunit\TestCase; @@ -176,7 +176,7 @@ class PageCacheTest extends TestCase if (isset($testCase['headers'])) { foreach (array_keys($testCase['headers']) as $name) { $value = Yii::$app->security->generateRandomString(); - Yii::$app->response->headers->add($name, $value); + Yii::$app->response->addHeader($name, $value); $headers[$name] = $value; } } @@ -191,9 +191,9 @@ class PageCacheTest extends TestCase // Metadata $metadata = [ 'format' => Yii::$app->response->format, - 'version' => Yii::$app->response->version, - 'statusCode' => Yii::$app->response->statusCode, - 'statusText' => Yii::$app->response->statusText, + 'protocolVersion' => Yii::$app->response->getProtocolVersion(), + 'statusCode' => Yii::$app->response->getStatusCode(), + 'reasonPhrase' => Yii::$app->response->getReasonPhrase(), ]; if ($testCase['cacheable']) { $this->assertNotEmpty($this->getInaccessibleProperty($filter->cache->handler, '_cache'), $testCase['name']); @@ -219,9 +219,9 @@ class PageCacheTest extends TestCase $this->assertSame($dynamic, $json['dynamic'], $testCase['name']); // Metadata $this->assertSame($metadata['format'], Yii::$app->response->format, $testCase['name']); - $this->assertSame($metadata['version'], Yii::$app->response->version, $testCase['name']); - $this->assertSame($metadata['statusCode'], Yii::$app->response->statusCode, $testCase['name']); - $this->assertSame($metadata['statusText'], Yii::$app->response->statusText, $testCase['name']); + $this->assertSame($metadata['protocolVersion'], Yii::$app->response->getProtocolVersion(), $testCase['name']); + $this->assertSame($metadata['statusCode'], Yii::$app->response->getStatusCode(), $testCase['name']); + $this->assertSame($metadata['reasonPhrase'], Yii::$app->response->getReasonPhrase(), $testCase['name']); // Cookies if (isset($testCase['cookies'])) { foreach ($testCase['cookies'] as $name => $expected) { @@ -234,9 +234,9 @@ class PageCacheTest extends TestCase // Headers if (isset($testCase['headers'])) { foreach ($testCase['headers'] as $name => $expected) { - $this->assertSame($expected, Yii::$app->response->headers->has($name), $testCase['name']); + $this->assertSame($expected, Yii::$app->response->hasHeader($name), $testCase['name']); if ($expected) { - $this->assertSame($headers[$name], Yii::$app->response->headers->get($name), $testCase['name']); + $this->assertSame($headers[$name], Yii::$app->response->getHeaderLine($name), $testCase['name']); } } } diff --git a/tests/framework/filters/auth/AuthTest.php b/tests/framework/filters/auth/AuthTest.php index 546a689..4044e71 100644 --- a/tests/framework/filters/auth/AuthTest.php +++ b/tests/framework/filters/auth/AuthTest.php @@ -142,7 +142,7 @@ class AuthTest extends \yiiunit\TestCase */ public function testHttpBearerAuth($token, $login) { - Yii::$app->request->headers->set('Authorization', "Bearer $token"); + Yii::$app->request->addHeader('Authorization', "Bearer $token"); $filter = ['class' => HttpBearerAuth::class]; $this->authOnly($token, $login, $filter, 'bearer-auth'); $this->authOptional($token, $login, $filter, 'bearer-auth'); diff --git a/tests/framework/http/FileStreamTest.php b/tests/framework/http/FileStreamTest.php new file mode 100644 index 0000000..d2251fa --- /dev/null +++ b/tests/framework/http/FileStreamTest.php @@ -0,0 +1,250 @@ +testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'file-stream-test-' . getmypid(); + FileHelper::createDirectory($this->testFilePath); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + FileHelper::removeDirectory($this->testFilePath); + parent::tearDown(); + } + + public function testRead() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'read.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isSeekable()); + $this->assertFalse($stream->isWritable()); + + $this->assertSame('01234', $stream->read(5)); + $this->assertFalse($stream->eof()); + + $this->assertSame('56789', $stream->read(6)); + $this->assertTrue($stream->eof()); + } + + /** + * @depends testRead + */ + public function testSeek() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'seek.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $stream->seek(5); + $this->assertSame('56789', $stream->read(5)); + + $stream->seek(0); + $this->assertSame('01234', $stream->read(5)); + } + + /** + * @depends testSeek + */ + public function testGetContents() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-content.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $this->assertSame('0123456789', $stream->getContents()); + + $stream->seek(5); + $this->assertSame('56789', $stream->getContents()); + } + + /** + * @depends testGetContents + */ + public function testToString() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'to-string.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $this->assertSame('0123456789', (string)$stream); + + $stream->seek(5); + $this->assertSame('0123456789', (string)$stream); + } + + /** + * @depends testRead + */ + public function testWrite() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'write.txt'; + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'w+'; + + $this->assertTrue($stream->isWritable()); + + $stream->write('01234'); + $stream->write('56789'); + + $stream->close(); + + $this->assertSame('0123456789', file_get_contents($filename)); + } + + /** + * @depends testRead + */ + public function testGetSize() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-size.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $this->assertSame(10, $stream->getSize()); + + file_put_contents($filename, ''); + $this->assertSame(0, $stream->getSize()); + } + + /** + * @depends testRead + */ + public function testGetMetadata() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-meta-data.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new FileStream(); + $stream->filename = $filename; + $stream->mode = 'r'; + + $metadata = $stream->getMetadata(); + + $this->assertSame('r', $metadata['mode']); + $this->assertSame('plainfile', $metadata['wrapper_type']); + + $this->assertSame('r', $stream->getMetadata('mode')); + } + + /** + * @return array test data. + */ + public function dataProviderFileMode() + { + return [ + ['r', true, false], + ['r+', true, true], + ['w', false, true], + ['w+', true, true], + ['rw', true, true], + ['x', false, true], + ['x+', true, true], + ['c', false, true], + ['c+', true, true], + ['a', false, true], + ['a+', true, true], + ['wb', false, true], + ['rb', true, false], + ['w+b', true, true], + ['r+b', true, true], + ['rt', true, false], + ['w+t', true, true], + ['r+t', true, true], + ['x+t', true, true], + ['c+t', true, true], + ]; + } + + /** + * @depends testGetMetadata + * @dataProvider dataProviderFileMode + * + * @param string $mode + * @param bool $isReadable + * @param bool $isWritable + */ + public function testIsReadable($mode, $isReadable, $isWritable) + { + /* @var $stream FileStream|\PHPUnit_Framework_MockObject_MockObject */ + $stream = $this->getMockBuilder(FileStream::class) + ->setMethods(['getMetadata']) + ->getMock(); + + $stream->expects($this->any()) + ->method('getMetadata') + ->with('mode') + ->willReturn($mode); + + $this->assertSame($isReadable, $stream->isReadable()); + } + + /** + * @depends testGetMetadata + * @dataProvider dataProviderFileMode + * + * @param string $mode + * @param bool $isReadable + * @param bool $isWritable + */ + public function testIsWritable($mode, $isReadable, $isWritable) + { + /* @var $stream FileStream|\PHPUnit_Framework_MockObject_MockObject */ + $stream = $this->getMockBuilder(FileStream::class) + ->setMethods(['getMetadata']) + ->getMock(); + + $stream->expects($this->any()) + ->method('getMetadata') + ->with('mode') + ->willReturn($mode); + + $this->assertSame($isWritable, $stream->isWritable()); + } +} \ No newline at end of file diff --git a/tests/framework/http/MemoryStreamTest.php b/tests/framework/http/MemoryStreamTest.php new file mode 100644 index 0000000..8a90b07 --- /dev/null +++ b/tests/framework/http/MemoryStreamTest.php @@ -0,0 +1,133 @@ +assertTrue($stream->isWritable()); + + $this->assertSame(5, $stream->write('01234')); + $this->assertSame(5, $stream->write('56789')); + + $this->assertSame('0123456789', (string)$stream); + } + + /** + * @depends testWrite + */ + public function testRead() + { + $stream = new MemoryStream(); + $stream->write('0123456789'); + + $this->assertTrue($stream->isReadable()); + + $stream->rewind(); + + $this->assertSame('01234', $stream->read(5)); + $this->assertFalse($stream->eof()); + $this->assertSame('56789', $stream->read(6)); + $this->assertTrue($stream->eof()); + } + + /** + * @depends testRead + */ + public function testSeek() + { + $stream = new MemoryStream(); + $stream->write('0123456789'); + $stream->rewind(); + + $this->assertTrue($stream->isSeekable()); + + $stream->seek(5); + $this->assertSame('56789', $stream->read(5)); + + $stream->seek(0); + $this->assertSame('01234', $stream->read(5)); + } + + /** + * @depends testSeek + */ + public function testGetContents() + { + $stream = new MemoryStream(); + $stream->write('0123456789'); + $stream->rewind(); + + $this->assertSame('0123456789', $stream->getContents()); + + $stream->seek(5); + $this->assertSame('56789', $stream->getContents()); + } + + /** + * @depends testGetContents + */ + public function testToString() + { + $stream = new MemoryStream(); + $stream->write('0123456789'); + $stream->rewind(); + + $this->assertSame('0123456789', (string)$stream); + + $stream->seek(5); + $this->assertSame('0123456789', (string)$stream); + } + + /** + * @depends testRead + */ + public function testGetSize() + { + $stream = new MemoryStream(); + + $this->assertSame(0, $stream->getSize()); + + $stream->write('0123456789'); + + $this->assertSame(10, $stream->getSize()); + } + + /** + * @depends testRead + */ + public function testGetMetadata() + { + $stream = new MemoryStream(); + + $metadata = $stream->getMetadata(); + + $this->assertSame('rw', $metadata['mode']); + $this->assertSame('rw', $stream->getMetadata('mode')); + } + + /** + * @depends testSeek + */ + public function testRewrite() + { + $stream = new MemoryStream(); + $stream->write('0123456789'); + + $stream->seek(5); + $this->assertSame(4, $stream->write('0000')); + + $this->assertSame('0123400009', (string)$stream); + } +} \ No newline at end of file diff --git a/tests/framework/http/MessageTraitTest.php b/tests/framework/http/MessageTraitTest.php new file mode 100644 index 0000000..640cb44 --- /dev/null +++ b/tests/framework/http/MessageTraitTest.php @@ -0,0 +1,134 @@ +setProtocolVersion('2.0'); + $this->assertSame('2.0', $message->getProtocolVersion()); + + $newMessage = $message->withProtocolVersion('2.1'); + $this->assertNotSame($newMessage, $message); + $this->assertSame('2.1', $newMessage->getProtocolVersion()); + } + + /** + * @depends testSetupProtocolVersion + */ + public function testDefaultProtocolVersion() + { + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.2'; + $message = new TestMessage(); + $this->assertSame('1.2', $message->getProtocolVersion()); + + unset($_SERVER['SERVER_PROTOCOL']); + $message = new TestMessage(); + $this->assertSame('1.0', $message->getProtocolVersion()); + } + + public function testSetupBody() + { + $message = new TestMessage(); + + $message->setBody([ + 'class' => FileStream::class + ]); + $this->assertTrue($message->getBody() instanceof FileStream); + + $body = new MemoryStream(); + $newMessage = $message->withBody($body); + $this->assertNotSame($newMessage, $message); + $this->assertSame($body, $newMessage->getBody()); + } + + /** + * @depends testSetupBody + */ + public function testDefaultBody() + { + $message = new TestMessage(); + $this->assertTrue($message->getBody() instanceof MemoryStream); + } + + public function testSetupHeaders() + { + $message = new TestMessage(); + + $this->assertFalse($message->hasHeader('some')); + $headerMessage = $message->withHeader('some', 'foo'); + $this->assertNotSame($headerMessage, $message); + $this->assertTrue($headerMessage->hasHeader('some')); + $this->assertEquals(['some' => ['foo']], $headerMessage->getHeaders()); + + $headerAddedMessage = $headerMessage->withAddedHeader('some', 'another'); + $this->assertNotSame($headerMessage, $headerAddedMessage); + $this->assertEquals(['some' => ['foo', 'another']], $headerAddedMessage->getHeaders()); + $this->assertEquals(['foo', 'another'], $headerAddedMessage->getHeader('some')); + $this->assertEquals('foo,another', $headerAddedMessage->getHeaderLine('some')); + + $overrideMessage = $headerAddedMessage->withHeader('some', 'override'); + $this->assertNotSame($headerAddedMessage, $overrideMessage); + $this->assertEquals(['some' => ['override']], $overrideMessage->getHeaders()); + + $clearMessage = $headerMessage->withoutHeader('some'); + $this->assertNotSame($headerMessage, $clearMessage); + $this->assertFalse($clearMessage->hasHeader('some')); + $this->assertEquals([], $clearMessage->getHeader('some')); + $this->assertEquals('', $clearMessage->getHeaderLine('some')); + + $message->setHeaders([ + 'some' => ['line1', 'line2'] + ]); + $this->assertEquals(['some' => ['line1', 'line2']], $message->getHeaders()); + $message->setHeaders([ + 'another' => ['one'] + ]); + $this->assertEquals(['another' => ['one']], $message->getHeaders()); + } + + /** + * @depends testSetupProtocolVersion + * @depends testSetupBody + * @depends testSetupHeaders + */ + public function testCreateFromConfig() + { + $message = new TestMessage([ + 'protocolVersion' => '2.1', + 'headers' => [ + 'header' => [ + 'line1', + 'line2', + ], + ], + 'body' => [ + 'class' => FileStream::class + ], + ]); + + $this->assertSame('2.1', $message->getProtocolVersion()); + $this->assertEquals(['header' => ['line1', 'line2']], $message->getHeaders()); + $this->assertTrue($message->getBody() instanceof FileStream); + } +} + +class TestMessage extends BaseObject implements MessageInterface +{ + use MessageTrait; +} \ No newline at end of file diff --git a/tests/framework/http/ResourceStreamTest.php b/tests/framework/http/ResourceStreamTest.php new file mode 100644 index 0000000..779f243 --- /dev/null +++ b/tests/framework/http/ResourceStreamTest.php @@ -0,0 +1,243 @@ +testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . 'resource-stream-test-' . getmypid(); + FileHelper::createDirectory($this->testFilePath); + } + + /** + * {@inheritdoc} + */ + protected function tearDown() + { + FileHelper::removeDirectory($this->testFilePath); + parent::tearDown(); + } + + public function testRead() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'read.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $this->assertTrue($stream->isReadable()); + $this->assertTrue($stream->isSeekable()); + $this->assertFalse($stream->isWritable()); + + $this->assertSame('01234', $stream->read(5)); + $this->assertFalse($stream->eof()); + + $this->assertSame('56789', $stream->read(6)); + $this->assertTrue($stream->eof()); + } + + /** + * @depends testRead + */ + public function testSeek() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'seek.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $stream->seek(5); + $this->assertSame('56789', $stream->read(5)); + + $stream->seek(0); + $this->assertSame('01234', $stream->read(5)); + } + + /** + * @depends testSeek + */ + public function testGetContents() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-content.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $this->assertSame('0123456789', $stream->getContents()); + + $stream->seek(5); + $this->assertSame('56789', $stream->getContents()); + } + + /** + * @depends testGetContents + */ + public function testToString() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'to-string.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $this->assertSame('0123456789', (string)$stream); + + $stream->seek(5); + $this->assertSame('0123456789', (string)$stream); + } + + /** + * @depends testRead + */ + public function testWrite() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'write.txt'; + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'w+'); + + $this->assertTrue($stream->isWritable()); + + $stream->write('01234'); + $stream->write('56789'); + + $stream->close(); + + $this->assertSame('0123456789', file_get_contents($filename)); + } + + /** + * @depends testRead + */ + public function testGetSize() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-size.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $this->assertSame(10, $stream->getSize()); + + file_put_contents($filename, ''); + $this->assertSame(0, $stream->getSize()); + } + + /** + * @depends testRead + */ + public function testGetMetadata() + { + $filename = $this->testFilePath . DIRECTORY_SEPARATOR . 'get-meta-data.txt'; + file_put_contents($filename, '0123456789'); + + $stream = new ResourceStream(); + $stream->resource = fopen($filename, 'r'); + + $metadata = $stream->getMetadata(); + + $this->assertSame('r', $metadata['mode']); + $this->assertSame('plainfile', $metadata['wrapper_type']); + + $this->assertSame('r', $stream->getMetadata('mode')); + } + + /** + * @return array test data. + */ + public function dataProviderFileMode() + { + return [ + ['r', true, false], + ['r+', true, true], + ['w', false, true], + ['w+', true, true], + ['rw', true, true], + ['x', false, true], + ['x+', true, true], + ['c', false, true], + ['c+', true, true], + ['a', false, true], + ['a+', true, true], + ['wb', false, true], + ['rb', true, false], + ['w+b', true, true], + ['r+b', true, true], + ['rt', true, false], + ['w+t', true, true], + ['r+t', true, true], + ['x+t', true, true], + ['c+t', true, true], + ]; + } + + /** + * @depends testGetMetadata + * @dataProvider dataProviderFileMode + * + * @param string $mode + * @param bool $isReadable + * @param bool $isWritable + */ + public function testIsReadable($mode, $isReadable, $isWritable) + { + /* @var $stream ResourceStream|\PHPUnit_Framework_MockObject_MockObject */ + $stream = $this->getMockBuilder(ResourceStream::class) + ->setMethods(['getMetadata']) + ->getMock(); + + $stream->expects($this->any()) + ->method('getMetadata') + ->with('mode') + ->willReturn($mode); + + $this->assertSame($isReadable, $stream->isReadable()); + } + + /** + * @depends testGetMetadata + * @dataProvider dataProviderFileMode + * + * @param string $mode + * @param bool $isReadable + * @param bool $isWritable + */ + public function testIsWritable($mode, $isReadable, $isWritable) + { + /* @var $stream ResourceStream|\PHPUnit_Framework_MockObject_MockObject */ + $stream = $this->getMockBuilder(ResourceStream::class) + ->setMethods(['getMetadata']) + ->getMock(); + + $stream->expects($this->any()) + ->method('getMetadata') + ->with('mode') + ->willReturn($mode); + + $this->assertSame($isWritable, $stream->isWritable()); + } +} \ No newline at end of file diff --git a/tests/framework/http/UploadedFileTest.php b/tests/framework/http/UploadedFileTest.php new file mode 100644 index 0000000..417d7cb --- /dev/null +++ b/tests/framework/http/UploadedFileTest.php @@ -0,0 +1,113 @@ +mockApplication(); + $this->generateFakeFiles(); + } + + private function generateFakeFileData() + { + return [ + 'name' => md5(mt_rand()), + 'tmp_name' => md5(mt_rand()), + 'type' => 'image/jpeg', + 'size' => mt_rand(1000, 10000), + 'error' => 0, + ]; + } + + private function generateFakeFiles() + { + $_FILES['ModelStub[prod_image]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); + + $_FILES['ModelStub[vendor_image]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); + $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); + } + + // Tests : + + public function testGetInstance() + { + $productImage = UploadedFile::getInstance(new ModelStub(), 'prod_image'); + $vendorImage = VendorImage::getInstance(new ModelStub(), 'vendor_image'); + + $this->assertInstanceOf(UploadedFile::class, $productImage); + $this->assertInstanceOf(VendorImage::class, $vendorImage); + } + + public function testGetInstances() + { + $productImages = UploadedFile::getInstances(new ModelStub(), 'prod_images'); + $vendorImages = VendorImage::getInstances(new ModelStub(), 'vendor_images'); + + foreach ($productImages as $productImage) { + $this->assertInstanceOf(UploadedFile::class, $productImage); + } + + foreach ($vendorImages as $vendorImage) { + $this->assertInstanceOf(VendorImage::class, $vendorImage); + } + } + + public function testSetupStream() + { + $uploadedFile = new UploadedFile(); + + $stream = new MemoryStream(); + $uploadedFile->setStream($stream); + $this->assertSame($stream, $uploadedFile->getStream()); + + $uploadedFile->setStream(['class' => MemoryStream::class]); + $this->assertNotSame($stream, $uploadedFile->getStream()); + $this->assertTrue($uploadedFile->getStream() instanceof MemoryStream); + + $uploadedFile->setStream(function () { + return new FileStream(['filename' => 'test.txt']); + }); + $this->assertTrue($uploadedFile->getStream() instanceof FileStream); + $this->assertSame('test.txt', $uploadedFile->getStream()->filename); + } + + /** + * @depends testSetupStream + */ + public function testDefaultStream() + { + $uploadedFile = new UploadedFile(); + $uploadedFile->setError(UPLOAD_ERR_OK); + $uploadedFile->tempFilename = tempnam(Yii::getAlias('@yiiunit/runtime'), 'tmp-'); + file_put_contents($uploadedFile->tempFilename, '0123456789'); + + $stream = $uploadedFile->getStream(); + $this->assertTrue($stream instanceof StreamInterface); + $this->assertSame('0123456789', $stream->__toString()); + } +} diff --git a/tests/framework/http/UriTest.php b/tests/framework/http/UriTest.php new file mode 100644 index 0000000..3bd1246 --- /dev/null +++ b/tests/framework/http/UriTest.php @@ -0,0 +1,203 @@ +setString('http://example.com?foo=some'); + $this->assertEquals('http://example.com?foo=some', $uri->getString()); + } + + /** + * @depends testSetupString + */ + public function testParseString() + { + $uri = new Uri(); + + $uri->setString('http://username:password@example.com:9090/content/path?foo=some#anchor'); + + $this->assertSame('http', $uri->getScheme()); + $this->assertSame('username:password', $uri->getUserInfo()); + $this->assertSame('example.com', $uri->getHost()); + $this->assertSame(9090, $uri->getPort()); + $this->assertSame('/content/path', $uri->getPath()); + $this->assertSame('foo=some', $uri->getQuery()); + $this->assertSame('anchor', $uri->getFragment()); + } + + /** + * @depends testSetupString + */ + public function testConstructFromString() + { + $uri = new Uri(['string' => 'http://example.com?foo=some']); + $this->assertSame('http://example.com?foo=some', $uri->getString()); + } + + public function testConstructFromComponents() + { + $uri = new Uri([ + 'scheme' => 'http', + 'user' => 'username', + 'password' => 'password', + 'host' => 'example.com', + 'port' => 9090, + 'path' => '/content/path', + 'query' => 'foo=some', + 'fragment' => 'anchor', + ]); + $this->assertSame('http://username:password@example.com:9090/content/path?foo=some#anchor', $uri->getString()); + } + + /** + * @depends testConstructFromComponents + */ + public function testToString() + { + $uri = new Uri([ + 'scheme' => 'http', + 'host' => 'example.com', + 'path' => '/content/path', + 'query' => 'foo=some', + ]); + $this->assertSame('http://example.com/content/path?foo=some', (string)$uri); + } + + /** + * @depends testParseString + */ + public function testGetUserInfo() + { + $uri = new Uri(); + + $uri->setString('http://username:password@example.com/content/path?foo=some'); + + $this->assertSame('username:password', $uri->getUserInfo()); + } + + /** + * @depends testParseString + */ + public function testGetAuthority() + { + $uri = new Uri(); + + $uri->setString('http://username:password@example.com/content/path?foo=some'); + + $this->assertSame('username:password@example.com', $uri->getAuthority()); + } + + /** + * @depends testConstructFromComponents + */ + public function testOmitDefaultPort() + { + $uri = new Uri([ + 'scheme' => 'http', + 'host' => 'example.com', + 'port' => 80, + 'path' => '/content/path', + 'query' => 'foo=some', + ]); + $this->assertSame('http://example.com/content/path?foo=some', $uri->getString()); + } + + /** + * @depends testConstructFromComponents + */ + public function testSetupQueryByArray() + { + $uri = new Uri([ + 'scheme' => 'http', + 'host' => 'example.com', + 'path' => '/content/path', + 'query' => [ + 'param1' => 'value1', + 'param2' => 'value2', + ], + ]); + $this->assertSame('http://example.com/content/path?param1=value1¶m2=value2', $uri->getString()); + } + + /** + * @depends testToString + */ + public function testPsrSyntax() + { + $uri = (new Uri()) + ->withScheme('http') + ->withUserInfo('username', 'password') + ->withHost('example.com') + ->withPort(9090) + ->withPath('/content/path') + ->withQuery('foo=some') + ->withFragment('anchor'); + + $this->assertSame('http://username:password@example.com:9090/content/path?foo=some#anchor', $uri->getString()); + } + + /** + * @depends testConstructFromString + * @depends testPsrSyntax + */ + public function testModify() + { + $uri = new Uri(['string' => 'http://example.com?foo=some']); + + $uri->setHost('another.com'); + $uri->setPort(9090); + + $this->assertSame('http://another.com:9090?foo=some', $uri->getString()); + } + + /** + * @depends testPsrSyntax + */ + public function testImmutability() + { + $uri = new Uri([ + 'scheme' => 'http', + 'user' => 'username', + 'password' => 'password', + 'host' => 'example.com', + 'port' => 9090, + 'path' => '/content/path', + 'query' => 'foo=some', + 'fragment' => 'anchor', + ]); + + $this->assertSame($uri, $uri->withScheme('http')); + $this->assertNotSame($uri, $uri->withScheme('https')); + + $this->assertSame($uri, $uri->withHost('example.com')); + $this->assertNotSame($uri, $uri->withHost('another.com')); + + $this->assertSame($uri, $uri->withPort(9090)); + $this->assertNotSame($uri, $uri->withPort(33)); + + $this->assertSame($uri, $uri->withPath('/content/path')); + $this->assertNotSame($uri, $uri->withPath('/another/path')); + + $this->assertSame($uri, $uri->withQuery('foo=some')); + $this->assertNotSame($uri, $uri->withQuery('foo=another')); + + $this->assertSame($uri, $uri->withFragment('anchor')); + $this->assertNotSame($uri, $uri->withFragment('another')); + + $this->assertSame($uri, $uri->withUserInfo('username', 'password')); + $this->assertNotSame($uri, $uri->withUserInfo('username', 'another')); + } +} \ No newline at end of file diff --git a/tests/framework/rest/UrlRuleTest.php b/tests/framework/rest/UrlRuleTest.php index 6dc2987..a80cd97 100644 --- a/tests/framework/rest/UrlRuleTest.php +++ b/tests/framework/rest/UrlRuleTest.php @@ -47,7 +47,8 @@ class UrlRuleTest extends TestCase foreach ($tests as $j => $test) { [$request->pathInfo, $route] = $test; $params = $test[2] ?? []; - $_POST['_METHOD'] = $test[3] ?? 'GET'; + $request->setMethod($test[3] ?? 'GET'); + $result = $rule->parseRequest($manager, $request); if ($route === false) { $this->assertFalse($result, "Test#$i-$j: $name"); diff --git a/tests/framework/validators/FileValidatorTest.php b/tests/framework/validators/FileValidatorTest.php index cee61ba..8afcf03 100644 --- a/tests/framework/validators/FileValidatorTest.php +++ b/tests/framework/validators/FileValidatorTest.php @@ -10,7 +10,7 @@ namespace yiiunit\framework\validators; use Yii; use yii\helpers\FileHelper; use yii\validators\FileValidator; -use yii\web\UploadedFile; +use yii\http\UploadedFile; use yiiunit\data\validators\models\FakedValidationModel; use yiiunit\TestCase; @@ -124,7 +124,7 @@ class FileValidatorTest extends TestCase 'attr_files' => $this->createTestFiles( [ [ - 'name' => 'test_up_1.txt', + 'clientFilename' => 'test_up_1.txt', 'size' => 1024, ], [ @@ -155,17 +155,17 @@ class FileValidatorTest extends TestCase 'attr_images' => $this->createTestFiles( [ [ - 'name' => 'image.png', + 'clientFilename' => 'image.png', 'size' => 1024, - 'type' => 'image/png', + 'clientMediaType' => 'image/png', ], [ - 'name' => 'image.png', + 'clientFilename' => 'image.png', 'size' => 1024, - 'type' => 'image/png', + 'clientMediaType' => 'image/png', ], [ - 'name' => 'text.txt', + 'clientFilename' => 'text.txt', 'size' => 1024, ], ] @@ -182,14 +182,14 @@ class FileValidatorTest extends TestCase 'attr_images' => $this->createTestFiles( [ [ - 'name' => 'image.png', + 'clientFilename' => 'image.png', 'size' => 1024, - 'type' => 'image/png', + 'clientMediaType' => 'image/png', ], [ - 'name' => 'image.png', + 'clientFilename' => 'image.png', 'size' => 1024, - 'type' => 'image/png', + 'clientMediaType' => 'image/png', ], ] ), @@ -203,7 +203,7 @@ class FileValidatorTest extends TestCase 'attr_image' => $this->createTestFiles( [ [ - 'name' => 'text.txt', + 'clientFilename' => 'text.txt', 'size' => 1024, ], ] @@ -235,7 +235,7 @@ class FileValidatorTest extends TestCase $files[] = ['no instance of UploadedFile']; continue; } - $name = isset($param['name']) ? $param['name'] : $rndString(); + $name = isset($param['clientFilename']) ? $param['clientFilename'] : $rndString(); $tempName = \Yii::getAlias('@yiiunit/runtime/validators/file/tmp/') . $name; if (is_readable($tempName)) { $size = filesize($tempName); @@ -245,23 +245,23 @@ class FileValidatorTest extends TestCase $this->sizeToBytes(ini_get('upload_max_filesize')) ); } - $type = isset($param['type']) ? $param['type'] : 'text/plain'; + $type = isset($param['clientMediaType']) ? $param['clientMediaType'] : 'text/plain'; $error = isset($param['error']) ? $param['error'] : UPLOAD_ERR_OK; if (count($params) == 1) { $error = empty($param) ? UPLOAD_ERR_NO_FILE : $error; return new UploadedFile([ - 'name' => $name, - 'tempName' => $tempName, - 'type' => $type, + 'clientFilename' => $name, + 'tempFilename' => $tempName, + 'clientMediaType' => $type, 'size' => $size, 'error' => $error, ]); } $files[] = new UploadedFile([ - 'name' => $name, - 'tempName' => $tempName, - 'type' => $type, + 'clientFilename' => $name, + 'tempFilename' => $tempName, + 'clientMediaType' => $type, 'size' => $size, 'error' => $error, ]); @@ -279,9 +279,9 @@ class FileValidatorTest extends TestCase $filePath = \Yii::getAlias('@yiiunit/framework/validators/data/mimeType/') . $fileName; return new UploadedFile([ - 'name' => $fileName, - 'tempName' => $filePath, - 'type' => FileHelper::getMimeType($filePath), + 'clientFilename' => $fileName, + 'tempFilename' => $filePath, + 'clientMediaType' => FileHelper::getMimeType($filePath), 'size' => filesize($filePath), 'error' => UPLOAD_ERR_OK, ]); @@ -341,8 +341,8 @@ class FileValidatorTest extends TestCase ]); $m = FakedValidationModel::createWithAttributes( [ - 'attr_jpg' => $this->createTestFiles([['name' => 'one.jpeg']]), - 'attr_exe' => $this->createTestFiles([['name' => 'bad.exe']]), + 'attr_jpg' => $this->createTestFiles([['clientFilename' => 'one.jpeg']]), + 'attr_exe' => $this->createTestFiles([['clientFilename' => 'bad.exe']]), ] ); $val->validateAttribute($m, 'attr_jpg'); @@ -357,7 +357,7 @@ class FileValidatorTest extends TestCase $baseName = '飛兒樂團光茫'; /** @var UploadedFile $file */ $file = $this->createTestFiles([ - ['name' => $baseName . '.txt'], + ['clientFilename' => $baseName . '.txt'], ]); $this->assertEquals($baseName, $file->getBaseName()); } @@ -414,7 +414,7 @@ class FileValidatorTest extends TestCase return FakedValidationModel::createWithAttributes( [ 'attr_files' => $this->createTestFiles([ - ['name' => 'abc.jpg', 'size' => 1024, 'type' => 'image/jpeg'], + ['clientFilename' => 'abc.jpg', 'size' => 1024, 'clientMediaType' => 'image/jpeg'], ]), 'attr_files_empty' => $this->createTestFiles([[]]), 'attr_err_ini' => $this->createTestFiles([['error' => UPLOAD_ERR_INI_SIZE]]), diff --git a/tests/framework/web/ControllerTest.php b/tests/framework/web/ControllerTest.php index 41c2127..b638049 100644 --- a/tests/framework/web/ControllerTest.php +++ b/tests/framework/web/ControllerTest.php @@ -17,6 +17,11 @@ use yiiunit\TestCase; */ class ControllerTest extends TestCase { + /** + * @var FakeController + */ + protected $controller; + public function testBindActionParams() { $aksi1 = new InlineAction('aksi1', $this->controller, 'actionAksi1'); @@ -61,18 +66,18 @@ class ControllerTest extends TestCase public function testRedirect() { $_SERVER['REQUEST_URI'] = 'http://test-domain.com/'; - $this->assertEquals($this->controller->redirect('')->headers->get('location'), '/'); - $this->assertEquals($this->controller->redirect('http://some-external-domain.com')->headers->get('location'), 'http://some-external-domain.com'); - $this->assertEquals($this->controller->redirect('/')->headers->get('location'), '/'); - $this->assertEquals($this->controller->redirect('/something-relative')->headers->get('location'), '/something-relative'); - $this->assertEquals($this->controller->redirect(['/'])->headers->get('location'), '/index.php?r='); - $this->assertEquals($this->controller->redirect(['view'])->headers->get('location'), '/index.php?r=fake%2Fview'); - $this->assertEquals($this->controller->redirect(['/controller'])->headers->get('location'), '/index.php?r=controller'); - $this->assertEquals($this->controller->redirect(['/controller/index'])->headers->get('location'), '/index.php?r=controller%2Findex'); - $this->assertEquals($this->controller->redirect(['//controller/index'])->headers->get('location'), '/index.php?r=controller%2Findex'); - $this->assertEquals($this->controller->redirect(['//controller/index', 'id' => 3])->headers->get('location'), '/index.php?r=controller%2Findex&id=3'); - $this->assertEquals($this->controller->redirect(['//controller/index', 'id_1' => 3, 'id_2' => 4])->headers->get('location'), '/index.php?r=controller%2Findex&id_1=3&id_2=4'); - $this->assertEquals($this->controller->redirect(['//controller/index', 'slug' => 'äöüß!"§$%&/()'])->headers->get('location'), '/index.php?r=controller%2Findex&slug=%C3%A4%C3%B6%C3%BC%C3%9F%21%22%C2%A7%24%25%26%2F%28%29'); + $this->assertEquals($this->controller->redirect('')->getHeader('location'), ['/']); + $this->assertEquals($this->controller->redirect('http://some-external-domain.com')->getHeader('location'), ['http://some-external-domain.com']); + $this->assertEquals($this->controller->redirect('/')->getHeader('location'), ['/']); + $this->assertEquals($this->controller->redirect('/something-relative')->getHeader('location'), ['/something-relative']); + $this->assertEquals($this->controller->redirect(['/'])->getHeader('location'), ['/index.php?r=']); + $this->assertEquals($this->controller->redirect(['view'])->getHeader('location'), ['/index.php?r=fake%2Fview']); + $this->assertEquals($this->controller->redirect(['/controller'])->getHeader('location'), ['/index.php?r=controller']); + $this->assertEquals($this->controller->redirect(['/controller/index'])->getHeader('location'), ['/index.php?r=controller%2Findex']); + $this->assertEquals($this->controller->redirect(['//controller/index'])->getHeader('location'), ['/index.php?r=controller%2Findex']); + $this->assertEquals($this->controller->redirect(['//controller/index', 'id' => 3])->getHeader('location'), ['/index.php?r=controller%2Findex&id=3']); + $this->assertEquals($this->controller->redirect(['//controller/index', 'id_1' => 3, 'id_2' => 4])->getHeader('location'), ['/index.php?r=controller%2Findex&id_1=3&id_2=4']); + $this->assertEquals($this->controller->redirect(['//controller/index', 'slug' => 'äöüß!"§$%&/()'])->getHeader('location'), ['/index.php?r=controller%2Findex&slug=%C3%A4%C3%B6%C3%BC%C3%9F%21%22%C2%A7%24%25%26%2F%28%29']); } protected function setUp() diff --git a/tests/framework/web/FormatterTest.php b/tests/framework/web/FormatterTest.php index abaa04b..9839e8e 100644 --- a/tests/framework/web/FormatterTest.php +++ b/tests/framework/web/FormatterTest.php @@ -40,7 +40,7 @@ abstract class FormatterTest extends \yiiunit\TestCase { $this->response->data = null; $this->formatter->format($this->response); - $this->assertNull($this->response->content); + $this->assertSame('', $this->response->content); } /** diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index b618f9f..3a6f22f 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -102,7 +102,7 @@ class RequestTest extends TestCase // accept any value on GET request foreach (['GET', 'HEAD', 'OPTIONS'] as $method) { - $_POST[$request->methodParam] = $method; + $request->setMethod($method); $this->assertTrue($request->validateCsrfToken($token)); $this->assertTrue($request->validateCsrfToken($token . 'a')); $this->assertTrue($request->validateCsrfToken([])); @@ -113,7 +113,7 @@ class RequestTest extends TestCase // only accept valid token on POST foreach (['POST', 'PUT', 'DELETE'] as $method) { - $_POST[$request->methodParam] = $method; + $request->setMethod($method); $this->assertTrue($request->validateCsrfToken($token)); $this->assertFalse($request->validateCsrfToken($token . 'a')); $this->assertFalse($request->validateCsrfToken([])); @@ -137,13 +137,13 @@ class RequestTest extends TestCase // accept no value on GET request foreach (['GET', 'HEAD', 'OPTIONS'] as $method) { - $_POST[$request->methodParam] = $method; + $request->setMethod($method); $this->assertTrue($request->validateCsrfToken()); } // only accept valid token on POST foreach (['POST', 'PUT', 'DELETE'] as $method) { - $_POST[$request->methodParam] = $method; + $request->setMethod($method); $request->setBodyParams([]); $this->assertFalse($request->validateCsrfToken()); $request->setBodyParams([$request->csrfParam => $token]); @@ -171,12 +171,11 @@ class RequestTest extends TestCase // only accept valid token on POST foreach (['POST', 'PUT', 'DELETE'] as $method) { - $_POST[$request->methodParam] = $method; + $request->setMethod($method); $request->setBodyParams([]); - $request->headers->remove(Request::CSRF_HEADER); - $this->assertFalse($request->validateCsrfToken()); - $request->headers->add(Request::CSRF_HEADER, $token); - $this->assertTrue($request->validateCsrfToken()); + + $this->assertFalse($request->withoutHeader(Request::CSRF_HEADER)->validateCsrfToken()); + $this->assertTrue($request->withAddedHeader(Request::CSRF_HEADER, $token)->validateCsrfToken()); } } diff --git a/tests/framework/web/ResponseTest.php b/tests/framework/web/ResponseTest.php index a6137f4..cd627bf 100644 --- a/tests/framework/web/ResponseTest.php +++ b/tests/framework/web/ResponseTest.php @@ -55,11 +55,10 @@ class ResponseTest extends \yiiunit\TestCase $this->assertEquals($expectedContent, $content); $this->assertEquals(206, $this->response->statusCode); - $headers = $this->response->headers; - $this->assertEquals('bytes', $headers->get('Accept-Ranges')); - $this->assertEquals('bytes ' . $expectedHeader . '/' . StringHelper::byteLength($fullContent), $headers->get('Content-Range')); - $this->assertEquals('text/plain', $headers->get('Content-Type')); - $this->assertEquals("$length", $headers->get('Content-Length')); + $this->assertEquals(['bytes'], $this->response->getHeader('Accept-Ranges')); + $this->assertEquals(['bytes ' . $expectedHeader . '/' . StringHelper::byteLength($fullContent)], $this->response->getHeader('Content-Range')); + $this->assertEquals(['text/plain'], $this->response->getHeader('Content-Type')); + $this->assertEquals(["$length"], $this->response->getHeader('Content-Length')); } public function wrongRanges() @@ -104,27 +103,26 @@ class ResponseTest extends \yiiunit\TestCase static::assertEquals('test', $content); static::assertEquals(200, $this->response->statusCode); - $headers = $this->response->headers; - static::assertEquals('application/octet-stream', $headers->get('Content-Type')); - static::assertEquals('attachment; filename="test.txt"', $headers->get('Content-Disposition')); - static::assertEquals(4, $headers->get('Content-Length')); + static::assertEquals(['application/octet-stream'], $this->response->getHeader('Content-Type')); + static::assertEquals(['attachment; filename="test.txt"'], $this->response->getHeader('Content-Disposition')); + static::assertEquals([4], $this->response->getHeader('Content-Length')); } public function testRedirect() { $_SERVER['REQUEST_URI'] = 'http://test-domain.com/'; - $this->assertEquals($this->response->redirect('')->headers->get('location'), '/'); - $this->assertEquals($this->response->redirect('http://some-external-domain.com')->headers->get('location'), 'http://some-external-domain.com'); - $this->assertEquals($this->response->redirect('/')->headers->get('location'), '/'); - $this->assertEquals($this->response->redirect('/something-relative')->headers->get('location'), '/something-relative'); - $this->assertEquals($this->response->redirect(['/'])->headers->get('location'), '/index.php?r='); - $this->assertEquals($this->response->redirect(['view'])->headers->get('location'), '/index.php?r=view'); - $this->assertEquals($this->response->redirect(['/controller'])->headers->get('location'), '/index.php?r=controller'); - $this->assertEquals($this->response->redirect(['/controller/index'])->headers->get('location'), '/index.php?r=controller%2Findex'); - $this->assertEquals($this->response->redirect(['//controller/index'])->headers->get('location'), '/index.php?r=controller%2Findex'); - $this->assertEquals($this->response->redirect(['//controller/index', 'id' => 3])->headers->get('location'), '/index.php?r=controller%2Findex&id=3'); - $this->assertEquals($this->response->redirect(['//controller/index', 'id_1' => 3, 'id_2' => 4])->headers->get('location'), '/index.php?r=controller%2Findex&id_1=3&id_2=4'); - $this->assertEquals($this->response->redirect(['//controller/index', 'slug' => 'äöüß!"§$%&/()'])->headers->get('location'), '/index.php?r=controller%2Findex&slug=%C3%A4%C3%B6%C3%BC%C3%9F%21%22%C2%A7%24%25%26%2F%28%29'); + $this->assertEquals($this->response->redirect('')->getHeader('location'), ['/']); + $this->assertEquals($this->response->redirect('http://some-external-domain.com')->getHeader('location'), ['http://some-external-domain.com']); + $this->assertEquals($this->response->redirect('/')->getHeader('location'), ['/']); + $this->assertEquals($this->response->redirect('/something-relative')->getHeader('location'), ['/something-relative']); + $this->assertEquals($this->response->redirect(['/'])->getHeader('location'), ['/index.php?r=']); + $this->assertEquals($this->response->redirect(['view'])->getHeader('location'), ['/index.php?r=view']); + $this->assertEquals($this->response->redirect(['/controller'])->getHeader('location'), ['/index.php?r=controller']); + $this->assertEquals($this->response->redirect(['/controller/index'])->getHeader('location'), ['/index.php?r=controller%2Findex']); + $this->assertEquals($this->response->redirect(['//controller/index'])->getHeader('location'), ['/index.php?r=controller%2Findex']); + $this->assertEquals($this->response->redirect(['//controller/index', 'id' => 3])->getHeader('location'), ['/index.php?r=controller%2Findex&id=3']); + $this->assertEquals($this->response->redirect(['//controller/index', 'id_1' => 3, 'id_2' => 4])->getHeader('location'), ['/index.php?r=controller%2Findex&id_1=3&id_2=4']); + $this->assertEquals($this->response->redirect(['//controller/index', 'slug' => 'äöüß!"§$%&/()'])->getHeader('location'), ['/index.php?r=controller%2Findex&slug=%C3%A4%C3%B6%C3%BC%C3%9F%21%22%C2%A7%24%25%26%2F%28%29']); } /** diff --git a/tests/framework/web/UploadedFileTest.php b/tests/framework/web/UploadedFileTest.php deleted file mode 100644 index 5f0a466..0000000 --- a/tests/framework/web/UploadedFileTest.php +++ /dev/null @@ -1,75 +0,0 @@ -mockApplication(); - $this->generateFakeFiles(); - } - - private function generateFakeFileData() - { - return [ - 'name' => md5(mt_rand()), - 'tmp_name' => md5(mt_rand()), - 'type' => 'image/jpeg', - 'size' => mt_rand(1000, 10000), - 'error' => 0, - ]; - } - - private function generateFakeFiles() - { - $_FILES['ModelStub[prod_image]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[prod_images][]'] = $this->generateFakeFileData(); - - $_FILES['ModelStub[vendor_image]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); - $_FILES['ModelStub[vendor_images][]'] = $this->generateFakeFileData(); - } - - // Tests : - - public function testGetInstance() - { - $productImage = UploadedFile::getInstance(new ModelStub(), 'prod_image'); - $vendorImage = VendorImage::getInstance(new ModelStub(), 'vendor_image'); - - $this->assertInstanceOf(UploadedFile::class, $productImage); - $this->assertInstanceOf(VendorImage::class, $vendorImage); - } - - public function testGetInstances() - { - $productImages = UploadedFile::getInstances(new ModelStub(), 'prod_images'); - $vendorImages = VendorImage::getInstances(new ModelStub(), 'vendor_images'); - - foreach ($productImages as $productImage) { - $this->assertInstanceOf(UploadedFile::class, $productImage); - } - - foreach ($vendorImages as $vendorImage) { - $this->assertInstanceOf(VendorImage::class, $vendorImage); - } - } -} diff --git a/tests/framework/web/UrlManagerParseUrlTest.php b/tests/framework/web/UrlManagerParseUrlTest.php index 98d9df2..a3a2293 100644 --- a/tests/framework/web/UrlManagerParseUrlTest.php +++ b/tests/framework/web/UrlManagerParseUrlTest.php @@ -308,22 +308,22 @@ class UrlManagerParseUrlTest extends TestCase ], ]); // matching pathinfo GET request - $_SERVER['REQUEST_METHOD'] = 'GET'; + $request->setMethod('GET'); $request->pathInfo = 'post/123/this+is+sample'; $result = $manager->parseRequest($request); $this->assertEquals(['post/view', ['id' => '123', 'title' => 'this+is+sample']], $result); // matching pathinfo PUT/POST request - $_SERVER['REQUEST_METHOD'] = 'PUT'; + $request->setMethod('PUT'); $request->pathInfo = 'post/123/this+is+sample'; $result = $manager->parseRequest($request); $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); - $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->setMethod('POST'); $request->pathInfo = 'post/123/this+is+sample'; $result = $manager->parseRequest($request); $this->assertEquals(['post/create', ['id' => '123', 'title' => 'this+is+sample']], $result); // no wrong matching - $_SERVER['REQUEST_METHOD'] = 'POST'; + $request->setMethod('POST'); $request->pathInfo = 'POST/GET'; $result = $manager->parseRequest($request); $this->assertEquals(['post/get', []], $result); @@ -339,7 +339,5 @@ class UrlManagerParseUrlTest extends TestCase ], \yii\web\Application::class); $this->assertEquals('/app/post/delete?id=123', $manager->createUrl(['post/delete', 'id' => 123])); $this->destroyApplication(); - - unset($_SERVER['REQUEST_METHOD']); } } diff --git a/tests/framework/web/UserTest.php b/tests/framework/web/UserTest.php index 612d825..1d0737b 100644 --- a/tests/framework/web/UserTest.php +++ b/tests/framework/web/UserTest.php @@ -22,8 +22,8 @@ use Yii; use yii\base\Component; use yii\base\NotSupportedException; use yii\rbac\PhpManager; -use yii\web\Cookie; -use yii\web\CookieCollection; +use yii\http\Cookie; +use yii\http\CookieCollection; use yii\web\ForbiddenHttpException; use yii\web\IdentityInterface; use yiiunit\TestCase; @@ -159,6 +159,7 @@ class UserTest extends TestCase ]); Yii::$app->user->setReturnUrl(null); } + public function testLoginRequired() { $appConfig = [ @@ -185,7 +186,6 @@ class UserTest extends TestCase $this->assertEquals('normal', $user->getReturnUrl()); $this->assertTrue(Yii::$app->response->getIsRedirection()); - $this->reset(); Yii::$app->request->setUrl('ajax'); $_SERVER['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'; diff --git a/tests/framework/web/stubs/VendorImage.php b/tests/framework/web/stubs/VendorImage.php index 386d65e..6728af2 100644 --- a/tests/framework/web/stubs/VendorImage.php +++ b/tests/framework/web/stubs/VendorImage.php @@ -7,7 +7,7 @@ namespace yiiunit\framework\web\stubs; -use yii\web\UploadedFile; +use yii\http\UploadedFile; class VendorImage extends UploadedFile {