├── .github └── workflows │ └── php.yml ├── .gitignore ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── autoload.php ├── composer.json ├── phpcs.xml ├── phpunit.xml ├── src ├── Psr15 │ ├── Middleware.php │ └── RequestHandler.php ├── Psr17 │ ├── RequestFactory.php │ ├── ResponseFactory.php │ ├── ServerRequestFactory.php │ ├── StreamFactory.php │ ├── UploadedFileFactory.php │ ├── UriFactory.php │ └── Utils │ │ └── SuperGlobal.php └── Psr7 │ ├── Message.php │ ├── Request.php │ ├── Response.php │ ├── ServerRequest.php │ ├── Stream.php │ ├── UploadedFile.php │ ├── Uri.php │ └── Utils │ └── UploadedFileHelper.php └── tests ├── Psr15 ├── ApiMiddleware.php ├── FinalHandler.php ├── RequestHandlerTest.php └── StringMiddleware.php ├── Psr17 ├── RequestFactoryTest.php ├── ResponseFactoryTest.php ├── ServerRequestFactoryTest.php ├── StreamFactoryTest.php ├── UploadFileFactoryTest.php ├── UriFactoryTest.php └── Utils │ └── SuperGlobalTest.php ├── Psr7 ├── MessageTest.php ├── RequestTest.php ├── ResponseTest.php ├── ServerRequestTest.php ├── StreamTest.php ├── UploadedFileTest.php ├── UriTest.php └── Utils │ └── UploadedFileHelperTest.php ├── bootstrap.php └── sample ├── shieldon_logo.png └── shieldon_logo_bak.png /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | run: 11 | runs-on: ${{ matrix.operating-system }} 12 | strategy: 13 | matrix: 14 | operating-system: [ubuntu-latest] 15 | php-versions: ['7.3', '7.4', '8.1'] 16 | name: PHP ${{ matrix.php-versions }} 17 | 18 | steps: 19 | - uses: actions/checkout@master 20 | 21 | - name: Setup PHP with Xdebug 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php-versions }} 25 | extensions: mbstring, pdo, pdo_mysql, intl, zip, redis, ctype, json 26 | coverage: xdebug 27 | 28 | - name: Create a folder for testing. 29 | run: sudo mkdir /home/runner/work/psr-http/psr-http/tmp 30 | 31 | - name: Make folder writable. 32 | run: sudo chmod 777 /home/runner/work/psr-http/psr-http/tmp 33 | 34 | - name: Install dependencies 35 | run: composer install --prefer-dist --no-interaction --dev 36 | 37 | - name: Update packages 38 | run: composer self-update 39 | 40 | - name: Run tests. 41 | run: composer test 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.phar 3 | composer.lock 4 | phpunit.phar 5 | phpunit.log 6 | tests/report 7 | .DS_Store 8 | .php_cs.cache 9 | .hg 10 | .vscode 11 | tmp 12 | clover.xml 13 | 1.txt 14 | .phpunit.result.cache -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | filter: 2 | paths: ["src/*"] 3 | excluded_paths: ["vendor/*", "tests/*"] 4 | 5 | checks: 6 | php: 7 | code_rating: true 8 | duplication: true 9 | 10 | tools: 11 | external_code_coverage: false 12 | 13 | build: 14 | environment: 15 | php: 16 | version: 7.4.0 17 | 18 | nodes: 19 | analysis: 20 | tests: 21 | override: 22 | - php-scrutinizer-run 23 | dependencies: 24 | before: 25 | - composer self-update 26 | - composer update --no-interaction --prefer-dist --no-progress --dev 27 | tests: 28 | before: 29 | - 30 | command: composer test 31 | coverage: 32 | file: 'clover.xml' 33 | format: 'clover' -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Terry L. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /autoload.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | /** 14 | * Register to PSR-4 autoloader. 15 | * 16 | * @return void 17 | */ 18 | function psr_http_register() 19 | { 20 | spl_autoload_register('psr_http_autoload', true, false); 21 | } 22 | 23 | /** 24 | * PSR-4 autoloader. 25 | * 26 | * @param string $className 27 | * 28 | * @return void 29 | */ 30 | function psr_http_autoload($className) 31 | { 32 | $prefix = 'Shieldon\\'; 33 | $dir = __DIR__ . '/src'; 34 | 35 | if (0 === strpos($className, $prefix . 'Psr')) { 36 | $parts = explode('\\', substr($className, strlen($prefix))); 37 | $filepath = $dir . '/' . implode('/', $parts) . '.php'; 38 | 39 | if (is_file($filepath)) { 40 | require $filepath; 41 | } 42 | } 43 | } 44 | 45 | psr_http_register(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shieldon/psr-http", 3 | "description": "HTTP implementation for PSR standard.", 4 | "keywords": ["php-http", "psr7", "psr-15", "psr-17"], 5 | "homepage": "https://github.com/terrylinooo/psr-http", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Terry Lin", 10 | "email": "contact@terryl.in", 11 | "homepage": "https://terryl.in", 12 | "role": "Developer" 13 | } 14 | ], 15 | "minimum-stability": "stable", 16 | "require": { 17 | "php": ">=7.1.0", 18 | "psr/http-factory": "^1.0", 19 | "psr/http-message": "^1.1 || ^2.0", 20 | "psr/http-server-handler": "^1.0", 21 | "psr/http-server-middleware": "^1.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^9" 25 | }, 26 | "autoload": { 27 | "psr-4": { 28 | "Shieldon\\Psr7\\": "src/Psr7", 29 | "Shieldon\\Psr15\\": "src/Psr15", 30 | "Shieldon\\Psr17\\": "src/Psr17" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "Shieldon\\Test\\Psr7\\": "tests/Psr7", 36 | "Shieldon\\Test\\Psr15\\": "tests/Psr15", 37 | "Shieldon\\Test\\Psr17\\": "tests/Psr17" 38 | } 39 | }, 40 | "config": { 41 | "process-timeout": 0, 42 | "sort-packages": true 43 | }, 44 | "scripts": { 45 | "test": "php vendor/phpunit/phpunit/phpunit" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | PSR-2 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | *.php 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | src 126 | tests 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | tests/Psr7/ 7 | tests/Psr15/ 8 | tests/Psr17/ 9 | 10 | 11 | 12 | 13 | src/ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Psr15/Middleware.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr15; 14 | 15 | use Psr\Http\Server\MiddlewareInterface; 16 | use Psr\Http\Server\RequestHandlerInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Psr\Http\Message\ResponseInterface; 19 | 20 | /** 21 | * PSR-15 Middleware 22 | */ 23 | abstract class Middleware implements MiddlewareInterface 24 | { 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | abstract function process(ServerRequestInterface $request,RequestHandlerInterface $handler): ResponseInterface; 29 | } 30 | -------------------------------------------------------------------------------- /src/Psr15/RequestHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr15; 14 | 15 | use Psr\Http\Server\RequestHandlerInterface; 16 | use Psr\Http\Server\MiddlewareInterface; 17 | use Psr\Http\Message\ServerRequestInterface; 18 | use Psr\Http\Message\ResponseInterface; 19 | use Shieldon\Psr7\Response; 20 | 21 | /** 22 | * PSR-15 Middleware 23 | */ 24 | class RequestHandler implements RequestHandlerInterface 25 | { 26 | /** 27 | * Middlewares in the queue are ready to run. 28 | * 29 | * @var array 30 | */ 31 | protected $queue = []; 32 | 33 | /** 34 | * After the last middleware has been called, a fallback handler should 35 | * parse the request and give an appropriate response. 36 | * 37 | * @var RequestHandlerInterface|null 38 | */ 39 | protected $fallbackHandler = null; 40 | 41 | /** 42 | * RequestHandler constructor. 43 | * 44 | * @param RequestHandler|null $finalRequestHandler A valid resource. 45 | */ 46 | public function __construct(?RequestHandlerInterface $fallbackHandler = null) 47 | { 48 | if ($fallbackHandler instanceof RequestHandlerInterface) { 49 | $this->fallbackHandler = $fallbackHandler; 50 | } 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function add(MiddlewareInterface $middleware) 57 | { 58 | $this->queue[] = $middleware; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function handle(ServerRequestInterface $request): ResponseInterface 65 | { 66 | if (0 === count($this->queue)) { 67 | return $this->final($request); 68 | } 69 | 70 | $middleware = array_shift($this->queue); 71 | 72 | return $middleware->process($request, $this); 73 | } 74 | 75 | /** 76 | * This is the final, there is no middleware needed to execute, pasre the 77 | * layered request and give a parsed response. 78 | * 79 | * @param ServerRequestInterface $request 80 | * 81 | * @return ResponseInterface 82 | */ 83 | protected function final(ServerRequestInterface $request): ResponseInterface 84 | { 85 | if (!$this->fallbackHandler) { 86 | return new Response(); 87 | } 88 | 89 | return $this->fallbackHandler->handle($request); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Psr17/RequestFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\RequestFactoryInterface; 16 | use Psr\Http\Message\RequestInterface; 17 | use Shieldon\Psr17\UriFactory; 18 | use Shieldon\Psr17\StreamFactory; 19 | use Shieldon\Psr17\Utils\SuperGlobal; 20 | use Shieldon\Psr7\Request; 21 | 22 | use function str_replace; 23 | use function extract; 24 | 25 | /** 26 | * PSR-17 Request Factory 27 | */ 28 | class RequestFactory implements RequestFactoryInterface 29 | { 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function createRequest(string $method, $uri): RequestInterface 34 | { 35 | extract(SuperGlobal::extract()); 36 | 37 | $protocol = $server['SERVER_PROTOCOL'] ?? '1.1'; 38 | $protocol = str_replace('HTTP/', '', $protocol); 39 | 40 | $uriFactory = new UriFactory(); 41 | $streamFactory = new StreamFactory(); 42 | 43 | $uri = $uriFactory->createUri($uri); 44 | $body = $streamFactory->createStream(); 45 | 46 | return new Request( 47 | $method, 48 | $uri, 49 | $body, 50 | $header, // from extract. 51 | $protocol 52 | ); 53 | } 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Non PSR-7 Methods. 58 | |-------------------------------------------------------------------------- 59 | */ 60 | 61 | /** 62 | * Create a new Request. 63 | * 64 | * @return RequestInterface 65 | */ 66 | public static function fromNew(): RequestInterface 67 | { 68 | return new Request(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Psr17/ResponseFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\ResponseFactoryInterface; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Shieldon\Psr17\StreamFactory; 18 | use Shieldon\Psr17\Utils\SuperGlobal; 19 | use Shieldon\Psr7\Response; 20 | 21 | use function str_replace; 22 | use function extract; 23 | 24 | /** 25 | * PSR-17 Response Factory 26 | */ 27 | class ResponseFactory implements ResponseFactoryInterface 28 | { 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function createResponse(int $code = 200, string $reasonPhrase = ''): ResponseInterface 33 | { 34 | extract(SuperGlobal::extract()); 35 | 36 | $protocol = $server['SERVER_PROTOCOL'] ?? '1.1'; 37 | $protocol = str_replace('HTTP/', '', $protocol); 38 | 39 | $streamFactory = new streamFactory(); 40 | 41 | $body = $streamFactory->createStream(); 42 | 43 | return new Response( 44 | $code, 45 | $header, // from extract. 46 | $body, 47 | $protocol, 48 | $reasonPhrase 49 | ); 50 | } 51 | 52 | /* 53 | |-------------------------------------------------------------------------- 54 | | Non PSR-7 Methods. 55 | |-------------------------------------------------------------------------- 56 | */ 57 | 58 | /** 59 | * Create a new Response. 60 | * 61 | * @return ResponseInterface 62 | */ 63 | public static function fromNew(): ResponseInterface 64 | { 65 | return new Response(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Psr17/ServerRequestFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\ServerRequestFactoryInterface; 16 | use Psr\Http\Message\ServerRequestInterface; 17 | use Psr\Http\Message\UriInterface; 18 | use Shieldon\Psr17\StreamFactory; 19 | use Shieldon\Psr17\UriFactory; 20 | use Shieldon\Psr17\Utils\SuperGlobal; 21 | use Shieldon\Psr7\ServerRequest; 22 | 23 | use function str_replace; 24 | use function extract; 25 | 26 | /** 27 | * PSR-17 Server Request Factory 28 | */ 29 | class ServerRequestFactory implements ServerRequestFactoryInterface 30 | { 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function createServerRequest(string $method, $uri, array $serverParams = []): ServerRequestInterface 35 | { 36 | extract(SuperGlobal::extract()); 37 | 38 | if ($serverParams !== []) { 39 | $server = $serverParams; 40 | } 41 | 42 | $protocol = $server['SERVER_PROTOCOL'] ?? '1.1'; 43 | $protocol = str_replace('HTTP/', '', $protocol); 44 | 45 | if (!($uri instanceof UriInterface)) { 46 | $uriFactory = new UriFactory(); 47 | $uri = $uriFactory->createUri($uri); 48 | } 49 | 50 | $streamFactory = new StreamFactory(); 51 | $body = $streamFactory->createStream(); 52 | 53 | return new ServerRequest( 54 | $method, 55 | $uri, 56 | $body, 57 | $header, // from extract. 58 | $protocol, 59 | $server, // from extract. 60 | $cookie, // from extract. 61 | $post, // from extract. 62 | $get, // from extract. 63 | $files // from extract. 64 | ); 65 | } 66 | 67 | /* 68 | |-------------------------------------------------------------------------- 69 | | Non PSR-7 Methods. 70 | |-------------------------------------------------------------------------- 71 | */ 72 | 73 | /** 74 | * Create a ServerRequestInterface instance from global variable. 75 | * 76 | * @return ServerRequestInterface 77 | */ 78 | public static function fromGlobal(): ServerRequestInterface 79 | { 80 | extract(SuperGlobal::extract()); 81 | 82 | // HTTP method. 83 | $method = $server['REQUEST_METHOD'] ?? 'GET'; 84 | 85 | // HTTP protocal version. 86 | $protocol = $server['SERVER_PROTOCOL'] ?? '1.1'; 87 | $protocol = str_replace('HTTP/', '', $protocol); 88 | 89 | $uri = UriFactory::fromGlobal(); 90 | 91 | $streamFactory = new StreamFactory(); 92 | $body = $streamFactory->createStream(); 93 | 94 | return new ServerRequest( 95 | $method, 96 | $uri, 97 | $body, 98 | $header, // from extract. 99 | $protocol, 100 | $server, // from extract. 101 | $cookie, // from extract. 102 | $post, // from extract. 103 | $get, // from extract. 104 | $files // from extract. 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Psr17/StreamFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\StreamInterface; 16 | use Psr\Http\Message\StreamFactoryInterface; 17 | use Shieldon\Psr7\Stream; 18 | use InvalidArgumentException; 19 | use RuntimeException; 20 | 21 | use function fopen; 22 | use function fwrite; 23 | use function is_resource; 24 | use function preg_match; 25 | use function rewind; 26 | 27 | /** 28 | * PSR-17 Stream Factory 29 | */ 30 | class StreamFactory implements StreamFactoryInterface 31 | { 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public function createStream(string $content = ''): StreamInterface 36 | { 37 | $resource = @fopen('php://temp', 'r+'); 38 | 39 | self::assertResource($resource); 40 | 41 | fwrite($resource, $content); 42 | rewind($resource); 43 | 44 | return $this->createStreamFromResource($resource); 45 | } 46 | 47 | /** 48 | * {@inheritdoc} 49 | */ 50 | public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface 51 | { 52 | if ($mode === '' || !preg_match('/^[rwaxce]{1}[bt]{0,1}[+]{0,1}+$/', $mode)) { 53 | throw new InvalidArgumentException( 54 | sprintf( 55 | 'Invalid file opening mode "%s"', 56 | $mode 57 | ) 58 | ); 59 | } 60 | 61 | $resource = @fopen($filename, $mode); 62 | 63 | if (!is_resource($resource)) { 64 | throw new RuntimeException( 65 | sprintf( 66 | 'Unable to open file at "%s"', 67 | $filename 68 | ) 69 | ); 70 | } 71 | 72 | return new Stream($resource); 73 | } 74 | 75 | /** 76 | * {@inheritdoc} 77 | */ 78 | public function createStreamFromResource($resource): StreamInterface 79 | { 80 | if (!is_resource($resource)) { 81 | $resource = @fopen('php://temp', 'r+'); 82 | } 83 | 84 | self::assertResource($resource); 85 | 86 | return new Stream($resource); 87 | } 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Non PSR-7 Methods. 92 | |-------------------------------------------------------------------------- 93 | */ 94 | 95 | /** 96 | * Create a new Stream instance. 97 | * 98 | * @return StreamInterface 99 | */ 100 | public static function fromNew(): StreamInterface 101 | { 102 | $resource = @fopen('php://temp', 'r+'); 103 | self::assertResource($resource); 104 | 105 | return new Stream($resource); 106 | } 107 | 108 | /** 109 | * Throw an exception if input is not a valid PHP resource. 110 | * 111 | * @param mixed $resource 112 | * 113 | * @return void 114 | */ 115 | protected static function assertResource($resource) 116 | { 117 | if (!is_resource($resource)) { 118 | throw new RuntimeException( 119 | 'Unable to open "php://temp" resource.' 120 | ); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Psr17/UploadedFileFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\StreamInterface; 16 | use Psr\Http\Message\UploadedFileFactoryInterface; 17 | use Psr\Http\Message\UploadedFileInterface; 18 | use Shieldon\Psr7\UploadedFile; 19 | use Shieldon\Psr7\Utils\UploadedFileHelper; 20 | use InvalidArgumentException; 21 | 22 | /** 23 | * PSR-17 Uploaded File Factory 24 | */ 25 | class UploadedFileFactory implements UploadedFileFactoryInterface 26 | { 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public function createUploadedFile( 31 | StreamInterface $stream, 32 | int $size = null, 33 | int $error = \UPLOAD_ERR_OK, 34 | string $clientFilename = null, 35 | string $clientMediaType = null 36 | ): UploadedFileInterface 37 | { 38 | if (!$stream->isReadable()) { 39 | throw new InvalidArgumentException( 40 | 'File is not readable.' 41 | ); 42 | } 43 | 44 | return new UploadedFile( 45 | $stream, 46 | $clientFilename, 47 | $clientMediaType, 48 | $size, 49 | $error 50 | ); 51 | } 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | Non PSR-7 Methods. 56 | |-------------------------------------------------------------------------- 57 | */ 58 | 59 | /** 60 | * Create an array with UriInterface structure. 61 | * 62 | * @return array 63 | */ 64 | public static function fromGlobal(): array 65 | { 66 | $filesParams = $_FILES ?? []; 67 | $uploadedFiles = []; 68 | 69 | if (!empty($filesParams)) { 70 | $uploadedFiles = UploadedFileHelper::uploadedFileSpecsConvert( 71 | UploadedFileHelper::uploadedFileParse($filesParams) 72 | ); 73 | } 74 | 75 | return $uploadedFiles; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Psr17/UriFactory.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17; 14 | 15 | use Psr\Http\Message\UriFactoryInterface; 16 | use Psr\Http\Message\UriInterface; 17 | use Shieldon\Psr7\Uri; 18 | 19 | /** 20 | * PSR-17 Uri Factory 21 | */ 22 | class UriFactory implements UriFactoryInterface 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | public function createUri(string $uri = '') : UriInterface 28 | { 29 | return new Uri($uri); 30 | } 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Non PSR-7 Methods. 35 | |-------------------------------------------------------------------------- 36 | */ 37 | 38 | /** 39 | * Create a UriInterface instance from global variable. 40 | * 41 | * @return UriInterface 42 | */ 43 | public static function fromGlobal(): UriInterface 44 | { 45 | $server = $_SERVER ?? []; 46 | 47 | $uri = ''; 48 | $user = ''; 49 | $host = ''; 50 | $pass = ''; 51 | $path = ''; 52 | $port = ''; 53 | $query = ''; 54 | $scheme = ''; 55 | 56 | $uriComponents = [ 57 | 'user' => 'PHP_AUTH_USER', 58 | 'host' => 'HTTP_HOST', 59 | 'pass' => 'PHP_AUTH_PW', 60 | 'path' => 'REQUEST_URI', 61 | 'port' => 'SERVER_PORT', 62 | 'query' => 'QUERY_STRING', 63 | 'scheme' => 'REQUEST_SCHEME', 64 | ]; 65 | 66 | foreach ($uriComponents as $key => $value) { 67 | ${$key} = $server[$value] ?? ''; 68 | } 69 | 70 | $userInfo = $user; 71 | 72 | if ($pass) { 73 | $userInfo .= ':' . $pass; 74 | } 75 | 76 | $authority = ''; 77 | 78 | if ($userInfo) { 79 | $authority .= $userInfo . '@'; 80 | } 81 | 82 | $authority .= $host; 83 | 84 | if ($port) { 85 | $authority .= ':' . $port; 86 | } 87 | 88 | if ($scheme) { 89 | $uri .= $scheme . ':'; 90 | } 91 | 92 | if ($authority) { 93 | $uri .= '//' . $authority; 94 | } 95 | 96 | $uri .= '/' . ltrim($path, '/'); 97 | 98 | if ($query) { 99 | $uri .= '?' . $query; 100 | } 101 | 102 | return new Uri($uri); 103 | } 104 | 105 | /** 106 | * Create a new URI. 107 | * 108 | * @return UriInterface 109 | */ 110 | public static function fromNew(): UriInterface 111 | { 112 | return new Uri(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Psr17/Utils/SuperGlobal.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr17\Utils; 14 | 15 | use function microtime; 16 | use function php_sapi_name; 17 | use function str_replace; 18 | use function strtolower; 19 | use function substr; 20 | use function time; 21 | 22 | /** 23 | * Data Helper 24 | */ 25 | class SuperGlobal 26 | { 27 | /** 28 | * Extract data from global variables. 29 | * 30 | * @return array 31 | */ 32 | public static function extract(): array 33 | { 34 | if (php_sapi_name() === 'cli') { 35 | self::mockCliEnvironment(); 36 | } 37 | 38 | // Here we add the HTTP prefix by ourselves... 39 | $headerParamsWithoutHttpPrefix = [ 40 | 'CONTENT_TYPE', 41 | 'CONTENT_LENGTH', 42 | ]; 43 | 44 | foreach ($headerParamsWithoutHttpPrefix as $value) { 45 | if (isset($_SERVER[$value])) { 46 | $_SERVER['HTTP_' . $value] = $_SERVER[$value]; 47 | } 48 | } 49 | 50 | $headerParams = []; 51 | $serverParams = $_SERVER ?? []; 52 | $cookieParams = $_COOKIE ?? []; 53 | $filesParams = $_FILES ?? []; 54 | $postParams = $_POST ?? []; 55 | $getParams = $_GET ?? []; 56 | 57 | foreach ($serverParams as $name => $value) { 58 | if (substr($name, 0, 5) == 'HTTP_') { 59 | $key = strtolower(str_replace('_', '-', substr($name, 5))); 60 | $headerParams[$key] = $value; 61 | } 62 | } 63 | 64 | return [ 65 | 'header' => $headerParams, 66 | 'server' => $serverParams, 67 | 'cookie' => $cookieParams, 68 | 'files' => $filesParams, 69 | 'post' => $postParams, 70 | 'get' => $getParams, 71 | ]; 72 | } 73 | 74 | // @codeCoverageIgnoreStart 75 | 76 | /** 77 | * Mock data for unit testing purpose ONLY. 78 | * 79 | * @param array $server Overwrite the mock data. 80 | * 81 | * @return void 82 | */ 83 | public static function mockCliEnvironment(array $server = []): void 84 | { 85 | $_POST = $_POST ?? []; 86 | $_COOKIE = $_COOKIE ?? []; 87 | $_GET = $_GET ?? []; 88 | $_FILES = $_FILES ?? []; 89 | $_SERVER = $_SERVER ?? []; 90 | 91 | $defaultServerParams = [ 92 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9', 93 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', 94 | 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7', 95 | 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 96 | 'HTTP_HOST' => '127.0.0.1', 97 | 'QUERY_STRING' => '', 98 | 'REMOTE_ADDR' => '127.0.0.1', 99 | 'REQUEST_METHOD' => 'GET', 100 | 'REQUEST_SCHEME' => 'http', 101 | 'REQUEST_TIME' => time(), 102 | 'REQUEST_TIME_FLOAT' => microtime(true), 103 | 'REQUEST_URI' => '', 104 | 'SCRIPT_NAME' => '', 105 | 'SERVER_NAME' => 'localhost', 106 | 'SERVER_PORT' => 80, 107 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 108 | 'CONTENT_TYPE' => 'text/html; charset=UTF-8', 109 | ]; 110 | 111 | if (defined('NO_MOCK_ENV')) { 112 | $defaultServerParams = []; 113 | } 114 | 115 | $_SERVER = array_merge($defaultServerParams, $_SERVER, $server); 116 | } 117 | 118 | // @codeCoverageIgnoreEnd 119 | } 120 | -------------------------------------------------------------------------------- /src/Psr7/Message.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\MessageInterface; 16 | use Psr\Http\Message\StreamInterface; 17 | use Shieldon\Psr7\Stream; 18 | use InvalidArgumentException; 19 | 20 | use function array_map; 21 | use function array_merge; 22 | use function count; 23 | use function fopen; 24 | use function fseek; 25 | use function fwrite; 26 | use function gettype; 27 | use function implode; 28 | use function is_array; 29 | use function is_bool; 30 | use function is_float; 31 | use function is_integer; 32 | use function is_scalar; 33 | use function is_string; 34 | use function preg_match; 35 | use function preg_match_all; 36 | use function sprintf; 37 | use function strtolower; 38 | use function trim; 39 | 40 | use const PREG_SET_ORDER; 41 | 42 | /** 43 | * HTTP messages consist of requests from a client to a server and responses 44 | * from a server to a client. 45 | */ 46 | class Message implements MessageInterface 47 | { 48 | /** 49 | * A HTTP protocol version number. 50 | * 51 | * @var string 52 | */ 53 | protected $protocolVersion = '1.1'; 54 | 55 | /** 56 | * An instance with the specified message body. 57 | * 58 | * @var StreamInterface 59 | */ 60 | protected $body; 61 | 62 | /** 63 | * An array of mapping header information with `string => array[]` format. 64 | * 65 | * @var array 66 | */ 67 | protected $headers = []; 68 | 69 | /** 70 | * A map of header name for lower case and original case. 71 | * In `lower => original` format. 72 | * 73 | * @var array 74 | */ 75 | protected $headerNameMapping = []; 76 | 77 | /** 78 | * Valid HTTP version numbers. 79 | * 80 | * @var array 81 | */ 82 | protected $validProtocolVersions = [ 83 | '1.0', 84 | '1.1', 85 | '2.0', 86 | '3.0', 87 | ]; 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | public function getProtocolVersion(): string 93 | { 94 | return $this->protocolVersion; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | public function withProtocolVersion($version): MessageInterface 101 | { 102 | $clone = clone $this; 103 | $clone->protocolVersion = $version; 104 | 105 | return $clone; 106 | } 107 | 108 | /** 109 | * {@inheritdoc} 110 | */ 111 | public function getHeaders(): array 112 | { 113 | $headers = $this->headers; 114 | 115 | foreach ($this->headerNameMapping as $origin) { 116 | $name = strtolower($origin); 117 | if (isset($headers[$name])) { 118 | $value = $headers[$name]; 119 | unset($headers[$name]); 120 | $headers[$origin] = $value; 121 | } 122 | } 123 | 124 | return $headers; 125 | } 126 | 127 | /** 128 | * {@inheritdoc} 129 | */ 130 | public function hasHeader($name): bool 131 | { 132 | $name = strtolower($name); 133 | 134 | return isset($this->headers[$name]); 135 | } 136 | 137 | /** 138 | * {@inheritdoc} 139 | */ 140 | public function getHeader($name): array 141 | { 142 | $name = strtolower($name); 143 | 144 | if (isset($this->headers[$name])) { 145 | return $this->headers[$name]; 146 | } 147 | 148 | return []; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function getHeaderLine($name): string 155 | { 156 | return implode(', ', $this->getHeader($name)); 157 | } 158 | 159 | /** 160 | * {@inheritdoc} 161 | */ 162 | public function withHeader($name, $value): MessageInterface 163 | { 164 | $origName = $name; 165 | 166 | $name = $this->normalizeHeaderFieldName($name); 167 | $value = $this->normalizeHeaderFieldValue($value); 168 | 169 | $clone = clone $this; 170 | $clone->headers[$name] = $value; 171 | $clone->headerNameMapping[$name] = $origName; 172 | 173 | return $clone; 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function withAddedHeader($name, $value): MessageInterface 180 | { 181 | $origName = $name; 182 | 183 | $name = $this->normalizeHeaderFieldName($name); 184 | $value = $this->normalizeHeaderFieldValue($value); 185 | 186 | $clone = clone $this; 187 | $clone->headerNameMapping[$name] = $origName; 188 | 189 | if (isset($clone->headers[$name])) { 190 | $clone->headers[$name] = array_merge($this->headers[$name], $value); 191 | } else { 192 | $clone->headers[$name] = $value; 193 | } 194 | 195 | return $clone; 196 | } 197 | 198 | /** 199 | * {@inheritdoc} 200 | */ 201 | public function withoutHeader($name): MessageInterface 202 | { 203 | $origName = $name; 204 | $name = strtolower($name); 205 | 206 | $clone = clone $this; 207 | unset($clone->headers[$name]); 208 | unset($clone->headerNameMapping[$name]); 209 | 210 | return $clone; 211 | } 212 | 213 | /** 214 | * {@inheritdoc} 215 | */ 216 | public function getBody(): StreamInterface 217 | { 218 | return $this->body; 219 | } 220 | 221 | /** 222 | * {@inheritdoc} 223 | */ 224 | public function withBody(StreamInterface $body): MessageInterface 225 | { 226 | $clone = clone $this; 227 | $clone->body = $body; 228 | 229 | return $clone; 230 | } 231 | 232 | /* 233 | |-------------------------------------------------------------------------- 234 | | Non PSR-7 Methods. 235 | |-------------------------------------------------------------------------- 236 | */ 237 | 238 | /** 239 | * Set headers to property $headers. 240 | * 241 | * @param array $headers A collection of header information. 242 | * 243 | * @return void 244 | */ 245 | protected function setHeaders(array $headers): void 246 | { 247 | $arr = []; 248 | $origArr = []; 249 | 250 | foreach ($headers as $name => $value) { 251 | $origName = $name; 252 | $name = $this->normalizeHeaderFieldName($name); 253 | $value = $this->normalizeHeaderFieldValue($value); 254 | 255 | $arr[$name] = $value; 256 | $origArr[$name] = $origName; 257 | } 258 | 259 | $this->headers = $arr; 260 | $this->headerNameMapping = $origArr; 261 | } 262 | 263 | /** 264 | * Set the request body. 265 | * 266 | * This method only provides two types of input, string and StreamInterface 267 | * 268 | * String - As a simplest way to initialize a stream resource. 269 | * StreamInterface - If you would like to use stream resource its mode is 270 | * not "r+", you should create a Stream instance by 271 | * yourself. 272 | * 273 | * @param string|StreamInterface $body Request body 274 | * 275 | * @return void 276 | */ 277 | protected function setBody($body): void 278 | { 279 | if ($body instanceof StreamInterface) { 280 | $this->body = $body; 281 | 282 | } elseif (is_string($body)) { 283 | $resource = fopen('php://temp', 'r+'); 284 | 285 | if ($body !== '') { 286 | fwrite($resource, $body); 287 | fseek($resource, 0); 288 | } 289 | 290 | $this->body = new Stream($resource); 291 | } 292 | } 293 | 294 | /** 295 | * Parse raw header text into an associated array. 296 | * 297 | * @param string $message Raw header text. 298 | * 299 | * @return array 300 | */ 301 | public static function parseRawHeader(string $message): array 302 | { 303 | preg_match_all('/^([^:\n]*): ?(.*)$/m', $message, $headers, PREG_SET_ORDER); 304 | 305 | $num = count($headers); 306 | 307 | if ($num > 1) { 308 | $headers = array_merge(...array_map(function($line) { 309 | $name = trim($line[1]); 310 | $field = trim($line[2]); 311 | return [$name => $field]; 312 | }, $headers)); 313 | 314 | return $headers; 315 | 316 | } elseif ($num === 1) { 317 | $name = trim($headers[0][1]); 318 | $field = trim($headers[0][2]); 319 | return [$name => $field]; 320 | } 321 | 322 | return []; 323 | } 324 | 325 | /** 326 | * Normalize the header field name. 327 | * 328 | * @param string $name 329 | * 330 | * @return string 331 | */ 332 | protected function normalizeHeaderFieldName($name): string 333 | { 334 | $this->assertHeaderFieldName($name); 335 | 336 | return trim(strtolower($name)); 337 | } 338 | 339 | /** 340 | * Normalize the header field value. 341 | * 342 | * @param mixed $value 343 | * 344 | * @return mixed 345 | */ 346 | protected function normalizeHeaderFieldValue($value) 347 | { 348 | $this->assertHeaderFieldValue($value); 349 | 350 | $result = false; 351 | 352 | if (is_string($value)) { 353 | $result = [trim($value)]; 354 | 355 | } elseif (is_array($value)) { 356 | foreach ($value as $v) { 357 | if (is_string($v)) { 358 | $value[] = trim($v); 359 | } 360 | } 361 | $result = $value; 362 | 363 | } elseif (is_float($value) || is_integer($value)) { 364 | $result = [(string) $value]; 365 | } 366 | 367 | return $result; 368 | } 369 | 370 | /** 371 | * Throw exception if the header is not compatible with RFC 7230. 372 | * 373 | * @param string $name The header name. 374 | * 375 | * @return void 376 | * 377 | * @throws InvalidArgumentException 378 | */ 379 | protected function assertHeaderFieldName($name): void 380 | { 381 | if (!is_string($name)) { 382 | throw new InvalidArgumentException( 383 | sprintf( 384 | 'Header field name must be a string, but "%s" given.', 385 | gettype($name) 386 | ) 387 | ); 388 | } 389 | // see https://tools.ietf.org/html/rfc7230#section-3.2.6 390 | // alpha => a-zA-Z 391 | // digit => 0-9 392 | // others => !#$%&\'*+-.^_`|~ 393 | 394 | if (!preg_match('/^[a-zA-Z0-9!#$%&\'*+-.^_`|~]+$/', $name)) { 395 | throw new InvalidArgumentException( 396 | sprintf( 397 | '"%s" is not valid header name, it must be an RFC 7230 compatible string.', 398 | $name 399 | ) 400 | ); 401 | } 402 | } 403 | 404 | /** 405 | * Throw exception if the header is not compatible with RFC 7230. 406 | * 407 | * @param array|null $value The header value. 408 | * 409 | * @return void 410 | * 411 | * @throws InvalidArgumentException 412 | */ 413 | protected function assertHeaderFieldValue($value = null): void 414 | { 415 | if (is_scalar($value) && !is_bool($value)) { 416 | $value = [(string) $value]; 417 | } 418 | 419 | if (empty($value)) { 420 | throw new InvalidArgumentException( 421 | 'Empty array is not allowed.' 422 | ); 423 | } 424 | 425 | if (is_array($value)) { 426 | foreach ($value as $item) { 427 | 428 | if ($item === '') { 429 | return; 430 | } 431 | 432 | if (!is_scalar($item) || is_bool($item)) { 433 | throw new InvalidArgumentException( 434 | sprintf( 435 | 'The header values only accept string and number, but "%s" provided.', 436 | gettype($item) 437 | ) 438 | ); 439 | } 440 | 441 | // https://www.rfc-editor.org/rfc/rfc7230.txt (page.25) 442 | // field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] 443 | // field-vchar = VCHAR / obs-text 444 | // obs-text = %x80-FF 445 | // SP = space 446 | // HTAB = horizontal tab 447 | // VCHAR = any visible [USASCII] character. (x21-x7e) 448 | // %x80-FF = character range outside ASCII. 449 | 450 | // I THINK THAT obs-text SHOULD N0T BE USED. 451 | // OR EVEN I CAN PASS CHINESE CHARACTERS, THAT'S WEIRD. 452 | if (!preg_match('/^[ \t\x21-\x7e]+$/', $item)) { 453 | throw new InvalidArgumentException( 454 | sprintf( 455 | '"%s" is not valid header value, it must contains visible ASCII characters only.', 456 | $item 457 | ) 458 | ); 459 | } 460 | } 461 | } else { 462 | throw new InvalidArgumentException( 463 | sprintf( 464 | 'The header field value only accepts string and array, but "%s" provided.', 465 | gettype($value) 466 | ) 467 | ); 468 | } 469 | } 470 | 471 | /** 472 | * Check out whether a protocol version number is supported. 473 | * 474 | * @param string $version HTTP protocol version. 475 | * 476 | * @return void 477 | * 478 | * @throws InvalidArgumentException 479 | */ 480 | protected function assertProtocolVersion(string $version): void 481 | { 482 | if (!in_array($version, $this->validProtocolVersions)) { 483 | throw new InvalidArgumentException( 484 | sprintf( 485 | 'Unsupported HTTP protocol version number. "%s" provided.', 486 | $version 487 | ) 488 | ); 489 | } 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/Psr7/Request.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\RequestInterface; 16 | use Psr\Http\Message\StreamInterface; 17 | use Psr\Http\Message\UriInterface; 18 | use Shieldon\Psr7\Message; 19 | use Shieldon\Psr7\Uri; 20 | use InvalidArgumentException; 21 | 22 | use function in_array; 23 | use function is_string; 24 | use function preg_match; 25 | use function sprintf; 26 | use function strtoupper; 27 | 28 | /* 29 | * Representation of an outgoing, client-side request. 30 | */ 31 | class Request extends Message implements RequestInterface 32 | { 33 | /** 34 | * The HTTP method of the outgoing request. 35 | * 36 | * @var string 37 | */ 38 | protected $method; 39 | 40 | /** 41 | * The target URL of the outgoing request. 42 | * 43 | * @var string 44 | */ 45 | protected $requestTarget; 46 | 47 | /** 48 | * A UriInterface object. 49 | * 50 | * @var UriInterface 51 | */ 52 | protected $uri; 53 | 54 | 55 | /** 56 | * Valid HTTP methods. 57 | * 58 | * @ref http://tools.ietf.org/html/rfc7231 59 | * 60 | * @var array 61 | */ 62 | protected $validMethods = [ 63 | 64 | // The HEAD method asks for a response identical to that of a GET 65 | // request, but without the response body. 66 | 'HEAD', 67 | 68 | // The GET method requests a representation of the specified 69 | // resource. Requests using GET should only retrieve data. 70 | 'GET', 71 | 72 | // The POST method is used to submit an entity to the specified 73 | // resource, often causing a change in state or side effects on the 74 | // server. 75 | 'POST', 76 | 77 | // The PUT method replaces all current representations of the target 78 | // resource with the request payload. 79 | 'PUT', 80 | 81 | // The DELETE method deletes the specified resource. 82 | 'DELETE', 83 | 84 | // The PATCH method is used to apply partial modifications to a 85 | // resource. 86 | 'PATCH', 87 | 88 | // The CONNECT method establishes a tunnel to the server identified 89 | // by the target resource. 90 | 'CONNECT', 91 | 92 | //The OPTIONS method is used to describe the communication options 93 | // for the target resource. 94 | 'OPTIONS', 95 | 96 | // The TRACE method performs a message loop-back test along the 97 | // path to the target resource. 98 | 'TRACE', 99 | ]; 100 | 101 | /** 102 | * Request constructor. 103 | * 104 | * @param string $method Request HTTP method 105 | * @param string|UriInterface $uri Request URI 106 | * @param string|StreamInterface $body Request body - see setBody() 107 | * @param array $headers Request headers 108 | * @param string $version Request protocol version 109 | */ 110 | public function __construct( 111 | string $method = 'GET', 112 | $uri = '' , 113 | $body = '' , 114 | array $headers = [] , 115 | string $version = '1.1' 116 | ) { 117 | $this->method = $method; 118 | 119 | $this->assertMethod($method); 120 | 121 | $this->assertProtocolVersion($version); 122 | $this->protocolVersion = $version; 123 | 124 | if ($uri instanceof UriInterface) { 125 | $this->uri = $uri; 126 | 127 | } elseif (is_string($uri)) { 128 | $this->uri = new Uri($uri); 129 | 130 | } else { 131 | throw new InvalidArgumentException( 132 | sprintf( 133 | 'URI should be a string or an instance of UriInterface, but "%s" provided.', 134 | gettype($uri) 135 | ) 136 | ); 137 | } 138 | 139 | $this->setBody($body); 140 | $this->setHeaders($headers); 141 | } 142 | 143 | /** 144 | * {@inheritdoc} 145 | */ 146 | public function getRequestTarget(): string 147 | { 148 | if ($this->requestTarget) { 149 | return $this->requestTarget; 150 | } 151 | 152 | $path = $this->uri->getPath(); 153 | $query = $this->uri->getQuery(); 154 | 155 | if (empty($path)) { 156 | $path = '/'; 157 | } 158 | 159 | if (!empty($query)) { 160 | $path .= '?' . $query; 161 | } 162 | 163 | return $path; 164 | } 165 | 166 | /** 167 | * {@inheritdoc} 168 | */ 169 | public function withRequestTarget($requestTarget): RequestInterface 170 | { 171 | if (!is_string($requestTarget)) { 172 | throw new InvalidArgumentException( 173 | 'A request target must be a string.' 174 | ); 175 | } 176 | 177 | if (preg_match('/\s/', $requestTarget)) { 178 | throw new InvalidArgumentException( 179 | 'A request target cannot contain any whitespace.' 180 | ); 181 | } 182 | 183 | $clone = clone $this; 184 | $clone->requestTarget = $requestTarget; 185 | 186 | return $clone; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function getMethod(): string 193 | { 194 | return $this->method; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function withMethod($method): RequestInterface 201 | { 202 | $this->assertMethod($method); 203 | 204 | $clone = clone $this; 205 | $clone->method = $method; 206 | 207 | return $clone; 208 | } 209 | 210 | /** 211 | * {@inheritdoc} 212 | */ 213 | public function getUri(): UriInterface 214 | { 215 | return $this->uri; 216 | } 217 | 218 | /** 219 | * {@inheritdoc} 220 | */ 221 | public function withUri(UriInterface $uri, $preserveHost = false): RequestInterface 222 | { 223 | $host = $uri->getHost(); 224 | 225 | $clone = clone $this; 226 | $clone->uri = $uri; 227 | 228 | if ( 229 | // This method MUST update the Host header of the returned request by 230 | // default if the URI contains a host component. 231 | (!$preserveHost && $host !== '') || 232 | 233 | // When `$preserveHost` is set to `true`. 234 | // If the Host header is missing or empty, and the new URI contains 235 | // a host component, this method MUST update the Host header in the returned 236 | // request. 237 | ($preserveHost && !$this->hasHeader('Host') && $host !== '') 238 | ) { 239 | $headers = $this->getHeaders(); 240 | $headers['host'] = $host; 241 | $clone->setHeaders($headers); 242 | } 243 | 244 | return $clone; 245 | } 246 | 247 | /* 248 | |-------------------------------------------------------------------------- 249 | | Non PSR-7 Methods. 250 | |-------------------------------------------------------------------------- 251 | */ 252 | 253 | /** 254 | * Check out whether a method defined in RFC 7231 request methods. 255 | * 256 | * @param string $method Http methods 257 | * 258 | * @return void 259 | * 260 | * @throws InvalidArgumentException 261 | */ 262 | protected function assertMethod($method): void 263 | { 264 | if (!is_string($method)) { 265 | throw new InvalidArgumentException( 266 | sprintf( 267 | 'HTTP method must be a string.', 268 | $method 269 | ) 270 | ); 271 | } 272 | 273 | if (!in_array(strtoupper($this->method), $this->validMethods)) { 274 | throw new InvalidArgumentException( 275 | sprintf( 276 | 'Unsupported HTTP method. It must be compatible with RFC-7231 request method, but "%s" provided.', 277 | $method 278 | ) 279 | ); 280 | } 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/Psr7/Response.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\ResponseInterface; 16 | use Shieldon\Psr7\Message; 17 | use InvalidArgumentException; 18 | 19 | use function gettype; 20 | use function is_integer; 21 | use function is_string; 22 | use function sprintf; 23 | use function str_replace; 24 | use function strpos; 25 | 26 | /* 27 | * Representation of an outgoing, server-side response. 28 | */ 29 | class Response extends Message implements ResponseInterface 30 | { 31 | /** 32 | * HTTP status number. 33 | * 34 | * @var int 35 | */ 36 | protected $status; 37 | 38 | /** 39 | * HTTP status reason phrase. 40 | * 41 | * @var string 42 | */ 43 | protected $reasonPhrase; 44 | 45 | /** 46 | * HTTP status codes. 47 | * 48 | * @see https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 49 | * 50 | * @var array 51 | */ 52 | protected static $statusCode = [ 53 | 54 | // 1xx: Informational 55 | // Request received, continuing process. 56 | 100 => 'Continue', 57 | 101 => 'Switching Protocols', 58 | 102 => 'Processing', 59 | 60 | // 2xx: Success 61 | // The action was successfully received, understood, and accepted. 62 | 200 => 'OK', 63 | 201 => 'Created', 64 | 202 => 'Accepted', 65 | 203 => 'Non-Authoritative Information', 66 | 204 => 'No Content', 67 | 205 => 'Reset Content', 68 | 206 => 'Partial Content', 69 | 207 => 'Multi-status', 70 | 208 => 'Already Reported', 71 | 72 | // 3xx: Redirection 73 | // Further action must be taken in order to complete the request. 74 | 300 => 'Multiple Choices', 75 | 301 => 'Moved Permanently', 76 | 302 => 'Found', 77 | 303 => 'See Other', 78 | 304 => 'Not Modified', 79 | 305 => 'Use Proxy', 80 | 306 => 'Switch Proxy', 81 | 307 => 'Temporary Redirect', 82 | 308 => 'Permanent Redirect', 83 | // => '309-399 Unassigned.' 84 | 85 | // 4xx: Client Error 86 | // The request contains bad syntax or cannot be fulfilled. 87 | 400 => 'Bad Request', 88 | 401 => 'Unauthorized', 89 | 402 => 'Payment Required', 90 | 403 => 'Forbidden', 91 | 404 => 'Not Found', 92 | 405 => 'Method Not Allowed', 93 | 406 => 'Not Acceptable', 94 | 407 => 'Proxy Authentication Required', 95 | 408 => 'Request Time-out', 96 | 409 => 'Conflict', 97 | 410 => 'Gone', 98 | 411 => 'Length Required', 99 | 412 => 'Precondition Failed', 100 | 413 => 'Request Entity Too Large', 101 | 414 => 'Request-URI Too Large', 102 | 415 => 'Unsupported Media Type', 103 | 416 => 'Requested range not satisfiable', 104 | 417 => 'Expectation Failed', 105 | // => '418-412: Unassigned' 106 | 422 => 'Unprocessable Entity', 107 | 423 => 'Locked', 108 | 424 => 'Failed Dependency', 109 | 425 => 'Unordered Collection', 110 | 426 => 'Upgrade Required', 111 | 428 => 'Precondition Required', 112 | 429 => 'Too Many Requests', 113 | 431 => 'Request Header Fields Too Large', 114 | // => '432-450: Unassigned.' 115 | 451 => 'Unavailable For Legal Reasons', 116 | // => '452-499: Unassigned.' 117 | 118 | // 5xx: Server Error 119 | // The server failed to fulfill an apparently valid request. 120 | 500 => 'Internal Server Error', 121 | 501 => 'Not Implemented', 122 | 502 => 'Bad Gateway', 123 | 503 => 'Service Unavailable', 124 | 504 => 'Gateway Time-out', 125 | 505 => 'HTTP Version not supported', 126 | 506 => 'Variant Also Negotiates', 127 | 507 => 'Insufficient Storage', 128 | 508 => 'Loop Detected', 129 | 510 => 'Not Extended', 130 | 511 => 'Network Authentication Required', 131 | // => '512-599 Unassigned.' 132 | ]; 133 | 134 | /** 135 | * Response Constructor. 136 | * 137 | * @param int $status Response HTTP status code. 138 | * @param array $headers Response headers. 139 | * @param StreamInterface|string $body Response body. 140 | * @param string $version Response protocol version. 141 | * @param string $reason Reaspnse HTTP reason phrase. 142 | */ 143 | public function __construct( 144 | int $status = 200 , 145 | array $headers = [] , 146 | $body = '' , 147 | string $version = '1.1', 148 | string $reason = 'OK' 149 | ) { 150 | $this->assertStatus($status); 151 | $this->assertReasonPhrase($reason); 152 | $this->assertProtocolVersion($version); 153 | 154 | $this->setHeaders($headers); 155 | $this->setBody($body); 156 | 157 | $this->status = $status; 158 | $this->protocolVersion = $version; 159 | $this->reasonPhrase = $reason; 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function getStatusCode(): int 166 | { 167 | return $this->status; 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function withStatus($code, $reasonPhrase = ''): ResponseInterface 174 | { 175 | $this->assertStatus($code); 176 | $this->assertReasonPhrase($reasonPhrase); 177 | 178 | if ($reasonPhrase === '' && isset(self::$statusCode[$code])) { 179 | $reasonPhrase = self::$statusCode[$code]; 180 | } 181 | 182 | $clone = clone $this; 183 | $clone->status = $code; 184 | $clone->reasonPhrase = $reasonPhrase; 185 | 186 | return $clone; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function getReasonPhrase(): string 193 | { 194 | return $this->reasonPhrase; 195 | } 196 | 197 | /* 198 | |-------------------------------------------------------------------------- 199 | | Non PSR-7 Methods. 200 | |-------------------------------------------------------------------------- 201 | */ 202 | 203 | /** 204 | * Throw exception when the HTTP status code is not valid. 205 | * 206 | * @param int $code HTTP status code. 207 | * 208 | * @return void 209 | * 210 | * @throws InvalidArgumentException 211 | */ 212 | protected function assertStatus($code) 213 | { 214 | if (!is_integer($code)) { 215 | throw new InvalidArgumentException( 216 | sprintf( 217 | 'Status code should be an integer value, but "%s" provided.', 218 | gettype($code) 219 | ) 220 | ); 221 | } 222 | 223 | if (!($code > 100 && $code < 599)) { 224 | throw new InvalidArgumentException( 225 | sprintf( 226 | 'Status code should be in a range of 100-599, but "%s" provided.', 227 | $code 228 | ) 229 | ); 230 | } 231 | } 232 | 233 | /** 234 | * Throw exception when the HTTP reason phrase is not valid. 235 | * 236 | * @param string $reasonPhrase 237 | * 238 | * @return void 239 | * 240 | * @throws InvalidArgumentException 241 | */ 242 | protected function assertReasonPhrase($reasonPhrase) 243 | { 244 | if ($reasonPhrase === '') { 245 | return; 246 | } 247 | 248 | if (!is_string($reasonPhrase)) { 249 | throw new InvalidArgumentException( 250 | sprintf( 251 | 'Reason phrase must be a string, but "%s" provided.', 252 | gettype($reasonPhrase) 253 | ) 254 | ); 255 | } 256 | 257 | // Special characters, such as "line breaks", "tab" and others... 258 | $escapeCharacters = [ 259 | '\f', '\r', '\n', '\t', '\v', '\0', '[\b]', '\s', '\S', '\w', '\W', '\d', '\D', '\b', '\B', '\cX', '\xhh', '\uhhhh' 260 | ]; 261 | 262 | $filteredPhrase = str_replace($escapeCharacters, '', $reasonPhrase); 263 | 264 | if ($reasonPhrase !== $filteredPhrase) { 265 | foreach ($escapeCharacters as $escape) { 266 | if (strpos($reasonPhrase, $escape) !== false) { 267 | throw new InvalidArgumentException( 268 | sprintf( 269 | 'Reason phrase contains "%s" that is considered as a prohibited character.', 270 | $escape 271 | ) 272 | ); 273 | } 274 | } 275 | } 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/Psr7/ServerRequest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Message\UploadedFileInterface; 17 | use Psr\Http\Message\UriInterface; 18 | use Psr\Http\Message\StreamInterface; 19 | use Shieldon\Psr7\Request; 20 | use Shieldon\Psr7\Utils\UploadedFileHelper; 21 | use InvalidArgumentException; 22 | use function file_get_contents; 23 | use function gettype; 24 | use function is_array; 25 | use function is_null; 26 | use function is_object; 27 | use function json_decode; 28 | use function json_last_error; 29 | use function parse_str; 30 | use function preg_split; 31 | use function sprintf; 32 | use function strtolower; 33 | use function strtoupper; 34 | use const JSON_ERROR_NONE; 35 | 36 | /* 37 | * Representation of an incoming, server-side HTTP request. 38 | */ 39 | class ServerRequest extends Request implements ServerRequestInterface 40 | { 41 | /** 42 | * Typically derived from PHP's $_SERVER superglobal. 43 | * 44 | * @var array 45 | */ 46 | protected $serverParams; 47 | 48 | /** 49 | * Typically derived from PHP's $_COOKIE superglobal. 50 | * 51 | * @var array 52 | */ 53 | protected $cookieParams; 54 | 55 | /** 56 | * Typically derived from PHP's $_POST superglobal. 57 | * 58 | * @var array|object|null 59 | */ 60 | protected $parsedBody; 61 | 62 | /** 63 | * Typically derived from PHP's $_GET superglobal. 64 | * 65 | * @var array 66 | */ 67 | protected $queryParams; 68 | 69 | /** 70 | * Typically derived from PHP's $_FILES superglobal. 71 | * A collection of uploadFileInterface instances. 72 | * 73 | * @var array 74 | */ 75 | protected $uploadedFiles; 76 | 77 | /** 78 | * The request "attributes" may be used to allow injection of any 79 | * parameters derived from the request: e.g., the results of path 80 | * match operations; the results of decrypting cookies; the results of 81 | * deserializing non-form-encoded message bodies; etc. Attributes 82 | * will be application and request specific, and CAN be mutable. 83 | * 84 | * @var array 85 | */ 86 | protected $attributes; 87 | 88 | /** 89 | * ServerRequest constructor. 90 | * 91 | * @param string $method Request HTTP method 92 | * @param string|UriInterface $uri Request URI object URI or URL 93 | * @param string|StreamInterface $body Request body 94 | * @param array $headers Request headers 95 | * @param string $version Request protocol version 96 | * @param array $serverParams Typically $_SERVER superglobal 97 | * @param array $cookieParams Typically $_COOKIE superglobal 98 | * @param array $postParams Typically $_POST superglobal 99 | * @param array $getParams Typically $_GET superglobal 100 | * @param array $filesParams Typically $_FILES superglobal 101 | */ 102 | public function __construct( 103 | string $method = 'GET', 104 | $uri = '' , 105 | $body = '' , 106 | array $headers = [] , 107 | string $version = '1.1', 108 | array $serverParams = [] , 109 | array $cookieParams = [] , 110 | array $postParams = [] , 111 | array $getParams = [] , 112 | array $filesParams = [] 113 | ) { 114 | parent::__construct($method, $uri, $body, $headers, $version); 115 | 116 | $this->serverParams = $serverParams; 117 | $this->cookieParams = $cookieParams; 118 | $this->queryParams = $getParams; 119 | $this->attributes = []; 120 | 121 | $this->determineParsedBody($postParams); 122 | 123 | // This property will be assigned to a parsed array that contains 124 | // the UploadedFile instance(s) as the $filesParams is given. 125 | $this->uploadedFiles = []; 126 | 127 | if (!empty($filesParams)) { 128 | $this->uploadedFiles = UploadedFileHelper::uploadedFileSpecsConvert( 129 | UploadedFileHelper::uploadedFileParse($filesParams) 130 | ); 131 | } 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function getServerParams(): array 138 | { 139 | return $this->serverParams; 140 | } 141 | 142 | /** 143 | * {@inheritdoc} 144 | */ 145 | public function getCookieParams(): array 146 | { 147 | return $this->cookieParams; 148 | } 149 | 150 | /** 151 | * {@inheritdoc} 152 | */ 153 | public function withCookieParams(array $cookies): ServerRequestInterface 154 | { 155 | $clone = clone $this; 156 | $clone->cookieParams = $cookies; 157 | 158 | return $clone; 159 | } 160 | 161 | /** 162 | * {@inheritdoc} 163 | */ 164 | public function getQueryParams(): array 165 | { 166 | return $this->queryParams; 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function withQueryParams(array $query): ServerRequestInterface 173 | { 174 | $clone = clone $this; 175 | $clone->queryParams = $query; 176 | 177 | return $clone; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function getUploadedFiles(): array 184 | { 185 | return $this->uploadedFiles; 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function withUploadedFiles(array $uploadedFiles): ServerRequestInterface 192 | { 193 | $this->assertUploadedFiles($uploadedFiles); 194 | 195 | $clone = clone $this; 196 | $clone->uploadedFiles = $uploadedFiles; 197 | 198 | return $clone; 199 | } 200 | 201 | /** 202 | * {@inheritdoc} 203 | */ 204 | public function getParsedBody() 205 | { 206 | return $this->parsedBody; 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | */ 212 | public function withParsedBody($data): ServerRequestInterface 213 | { 214 | $this->assertParsedBody($data); 215 | 216 | $clone = clone $this; 217 | $clone->parsedBody = $data; 218 | 219 | return $clone; 220 | } 221 | 222 | /** 223 | * {@inheritdoc} 224 | */ 225 | public function getAttributes(): array 226 | { 227 | return $this->attributes; 228 | } 229 | 230 | /** 231 | * {@inheritdoc} 232 | */ 233 | public function getAttribute($name, $default = null) 234 | { 235 | return isset($this->attributes[$name]) ? $this->attributes[$name] : $default; 236 | } 237 | 238 | /** 239 | * {@inheritdoc} 240 | */ 241 | public function withAttribute($name, $value): ServerRequestInterface 242 | { 243 | $clone = clone $this; 244 | $clone->attributes[$name] = $value; 245 | 246 | return $clone; 247 | } 248 | 249 | /** 250 | * {@inheritdoc} 251 | */ 252 | public function withoutAttribute($name): ServerRequestInterface 253 | { 254 | $clone = clone $this; 255 | 256 | if (isset($this->attributes[$name])) { 257 | unset($clone->attributes[$name]); 258 | } 259 | 260 | return $clone; 261 | } 262 | 263 | /* 264 | |-------------------------------------------------------------------------- 265 | | Non-PSR-7 Methods. 266 | |-------------------------------------------------------------------------- 267 | */ 268 | 269 | /** 270 | * Check out whether an array is compatible to PSR-7 file structure. 271 | * 272 | * @param array $values The array to check. 273 | * 274 | * @return void 275 | * 276 | * @throws InvalidArgumentException 277 | */ 278 | protected function assertUploadedFiles(array $values): void 279 | { 280 | foreach ($values as $value) { 281 | if (is_array($value)) { 282 | $this->assertUploadedFiles($value); 283 | } elseif (!($value instanceof UploadedFileInterface)) { 284 | throw new InvalidArgumentException( 285 | 'Invalid PSR-7 array structure for handling UploadedFile.' 286 | ); 287 | } 288 | } 289 | } 290 | 291 | /** 292 | * Throw an exception if an unsupported argument type is provided. 293 | * 294 | * @param string|array|null $data The deserialized body data. This will 295 | * typically be in an array or object. 296 | * 297 | * @return void 298 | * 299 | * @throws InvalidArgumentException 300 | */ 301 | protected function assertParsedBody($data): void 302 | { 303 | if ( 304 | ! is_null($data) && 305 | ! is_array($data) && 306 | ! is_object($data) 307 | ) { 308 | throw new InvalidArgumentException( 309 | sprintf( 310 | 'Only accepts array, object and null, but "%s" provided.', 311 | gettype($data) 312 | ) 313 | ); 314 | } 315 | } 316 | 317 | /** 318 | * Confirm the content type and post values whether fit the requirement. 319 | * 320 | * @param array $postParams 321 | * @return void 322 | */ 323 | protected function determineParsedBody(array $postParams) 324 | { 325 | $headerContentType = $this->getHeaderLine('Content-Type'); 326 | $contentTypeArr = preg_split('/\s*[;,]\s*/', $headerContentType); 327 | $contentType = strtolower($contentTypeArr[0]); 328 | $httpMethod = strtoupper($this->getMethod()); 329 | 330 | // Is it a form submit or not. 331 | $isForm = false; 332 | 333 | if ($httpMethod === 'POST') { 334 | 335 | // If the request Content-Type is either application/x-www-form-urlencoded 336 | // or multipart/form-data, and the request method is POST, this method MUST 337 | // return the contents of $_POST. 338 | $postRequiredContentTypes = [ 339 | '', // For unit testing purpose. 340 | 'application/x-www-form-urlencoded', 341 | 'multipart/form-data', 342 | ]; 343 | 344 | if (in_array($contentType, $postRequiredContentTypes)) { 345 | $this->parsedBody = $postParams ?? null; 346 | $isForm = true; 347 | } 348 | } 349 | 350 | // @codeCoverageIgnoreStart 351 | // Maybe other http methods such as PUT, DELETE, etc... 352 | if ($httpMethod !== 'GET' && !$isForm) { 353 | 354 | // If it a JSON formatted string? 355 | $isJson = false; 356 | 357 | // Receive content from PHP stdin input, if exists. 358 | $rawText = file_get_contents('php://input'); 359 | 360 | if (!empty($rawText)) { 361 | 362 | if ($contentType === 'application/json') { 363 | $jsonParsedBody = json_decode($rawText); 364 | $isJson = (json_last_error() === JSON_ERROR_NONE); 365 | } 366 | 367 | // Condition 1 - It's a JSON, now the body is a JSON object. 368 | if ($isJson) { 369 | $this->parsedBody = $jsonParsedBody ?: null; 370 | } 371 | 372 | // Condition 2 - It's not a JSON, might be a http build query. 373 | if (!$isJson) { 374 | parse_str($rawText, $parsedStr); 375 | $this->parsedBody = $parsedStr ?: null; 376 | } 377 | } 378 | } 379 | 380 | // This part is manually tested by using PostMan. 381 | // @codeCoverageIgnoreEnd 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/Psr7/Stream.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\StreamInterface; 16 | use InvalidArgumentException; 17 | use RuntimeException; 18 | 19 | use function fclose; 20 | use function fread; 21 | use function fseek; 22 | use function fstat; 23 | use function ftell; 24 | use function fwrite; 25 | use function gettype; 26 | use function is_resource; 27 | use function preg_match; 28 | use function sprintf; 29 | use function stream_get_contents; 30 | use function stream_get_meta_data; 31 | 32 | use const SEEK_CUR; 33 | use const SEEK_END; 34 | use const SEEK_SET; 35 | use const PREG_OFFSET_CAPTURE; 36 | 37 | /* 38 | * Describes a data stream. 39 | */ 40 | class Stream implements StreamInterface 41 | { 42 | /** 43 | * @var bool 44 | */ 45 | protected $readable; 46 | 47 | /** 48 | * @var bool 49 | */ 50 | protected $writable; 51 | 52 | /** 53 | * @var bool 54 | */ 55 | protected $seekable; 56 | 57 | /** 58 | * The size of the stream. 59 | * 60 | * @var int|null 61 | */ 62 | protected $size; 63 | 64 | /** 65 | * The keys returned are identical to the keys returned from PHP's 66 | * stream_get_meta_data() function. 67 | * 68 | * @var array 69 | */ 70 | protected $meta; 71 | 72 | /** 73 | * Typically a PHP resource. 74 | * 75 | * @var resource 76 | */ 77 | protected $stream; 78 | 79 | /** 80 | * Stream constructor 81 | * 82 | * @param resource $stream A valid resource. 83 | */ 84 | public function __construct($stream) 85 | { 86 | $this->assertStream($stream); 87 | $this->stream = $stream; 88 | 89 | $meta = $this->getMetadata(); 90 | 91 | $this->readable = false; 92 | $this->writable = false; 93 | 94 | // The mode parameter specifies the type of access you require to 95 | // the stream. @see https://www.php.net/manual/en/function.fopen.php 96 | if (strpos($meta['mode'], '+') !== false) { 97 | $this->readable = true; 98 | $this->writable = true; 99 | } 100 | 101 | if (preg_match('/^[waxc][t|b]{0,1}$/', $meta['mode'], $matches, PREG_OFFSET_CAPTURE)) { 102 | $this->writable = true; 103 | } 104 | 105 | if (strpos($meta['mode'], 'r') !== false) { 106 | $this->readable = true; 107 | } 108 | 109 | $this->seekable = $meta['seekable']; 110 | } 111 | 112 | /** 113 | * {@inheritdoc} 114 | */ 115 | public function isWritable(): bool 116 | { 117 | return $this->writable; 118 | } 119 | 120 | /** 121 | * {@inheritdoc} 122 | */ 123 | public function isReadable(): bool 124 | { 125 | return $this->readable; 126 | } 127 | 128 | /** 129 | * {@inheritdoc} 130 | */ 131 | public function isSeekable(): bool 132 | { 133 | return $this->seekable; 134 | } 135 | 136 | /** 137 | * {@inheritdoc} 138 | */ 139 | public function close(): void 140 | { 141 | if ($this->isStream()) { 142 | fclose($this->stream); 143 | } 144 | 145 | $this->detach(); 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function detach() 152 | { 153 | if (!$this->isStream()) { 154 | return null; 155 | } 156 | 157 | $legacy = $this->stream; 158 | 159 | $this->readable = false; 160 | $this->writable = false; 161 | $this->seekable = false; 162 | $this->size = null; 163 | $this->meta = []; 164 | 165 | unset($this->stream); 166 | 167 | return $legacy; 168 | } 169 | 170 | /** 171 | * {@inheritdoc} 172 | */ 173 | public function getSize(): ?int 174 | { 175 | if (!$this->isStream()) { 176 | return null; 177 | } 178 | 179 | if ($this->size === null) { 180 | $stats = fstat($this->stream); 181 | $this->size = $stats['size'] ?? null; 182 | } 183 | 184 | return $this->size; 185 | } 186 | 187 | /** 188 | * {@inheritdoc} 189 | */ 190 | public function tell(): int 191 | { 192 | $this->assertPropertyStream(); 193 | 194 | $pointer = false; 195 | 196 | if ($this->stream) { 197 | $pointer = ftell($this->stream); 198 | } 199 | 200 | if ($pointer === false) { 201 | 202 | // @codeCoverageIgnoreStart 203 | 204 | throw new RuntimeException( 205 | 'Unable to get the position of the file pointer in stream.' 206 | ); 207 | 208 | // @codeCoverageIgnoreEnd 209 | } 210 | 211 | return $pointer; 212 | } 213 | 214 | /** 215 | * {@inheritdoc} 216 | */ 217 | public function eof(): bool 218 | { 219 | return $this->stream ? feof($this->stream) : true; 220 | } 221 | 222 | /** 223 | * {@inheritdoc} 224 | */ 225 | public function seek($offset, $whence = SEEK_SET): void 226 | { 227 | $this->assertPropertyStream(); 228 | 229 | if (!$this->seekable) { 230 | throw new RuntimeException( 231 | 'Stream is not seekable.' 232 | ); 233 | } 234 | 235 | $offset = (int) $offset; 236 | $whence = (int) $whence; 237 | 238 | $message = [ 239 | SEEK_CUR => 'Set position to current location plus offset.', 240 | SEEK_END => 'Set position to end-of-stream plus offset.', 241 | SEEK_SET => 'Set position equal to offset bytes.', 242 | ]; 243 | 244 | $errorMsg = $message[$whence] ?? 'Unknown error.'; 245 | 246 | if (fseek($this->stream, $offset, $whence) === -1) { 247 | throw new RuntimeException( 248 | sprintf( 249 | '%s. Unable to seek to stream at position %s', 250 | $errorMsg, 251 | $offset 252 | ) 253 | ); 254 | } 255 | } 256 | 257 | /** 258 | * {@inheritdoc} 259 | */ 260 | public function rewind(): void 261 | { 262 | $this->seek(0); 263 | } 264 | 265 | /** 266 | * {@inheritdoc} 267 | */ 268 | public function write($string): int 269 | { 270 | $this->assertPropertyStream(); 271 | 272 | $size = 0; 273 | 274 | if ($this->isWritable()) { 275 | $size = fwrite($this->stream, $string); 276 | } 277 | 278 | if ($size === false) { 279 | 280 | // @codeCoverageIgnoreStart 281 | 282 | throw new RuntimeException( 283 | 'Unable to write to stream.' 284 | ); 285 | 286 | // @codeCoverageIgnoreEnd 287 | } 288 | 289 | // Make sure that `getSize()`will count the correct size again after writing anything. 290 | $this->size = null; 291 | 292 | return $size; 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | */ 298 | public function read($length): string 299 | { 300 | $this->assertPropertyStream(); 301 | 302 | $string = false; 303 | 304 | if ($this->isReadable()) { 305 | $string = fread($this->stream, $length); 306 | } 307 | 308 | if ($string === false) { 309 | 310 | // @codeCoverageIgnoreStart 311 | 312 | throw new RuntimeException( 313 | 'Unable to read from stream.' 314 | ); 315 | 316 | // @codeCoverageIgnoreEnd 317 | } 318 | 319 | return $string; 320 | } 321 | 322 | /** 323 | * {@inheritdoc} 324 | */ 325 | public function getContents(): string 326 | { 327 | $this->assertPropertyStream(); 328 | 329 | $string = false; 330 | 331 | if ($this->isReadable()) { 332 | $string = stream_get_contents($this->stream); 333 | } 334 | 335 | if ($string === false) { 336 | throw new RuntimeException( 337 | 'Unable to read stream contents.' 338 | ); 339 | } 340 | 341 | return $string; 342 | } 343 | 344 | /** 345 | * {@inheritdoc} 346 | */ 347 | public function getMetadata($key = null) 348 | { 349 | if ($this->isStream()) { 350 | $this->meta = stream_get_meta_data($this->stream); 351 | 352 | if (!$key) { 353 | return $this->meta; 354 | } 355 | 356 | if (isset($this->meta[$key])) { 357 | return $this->meta[$key]; 358 | } 359 | } 360 | 361 | return null; 362 | } 363 | 364 | /** 365 | * {@inheritdoc} 366 | */ 367 | public function __toString(): string 368 | { 369 | if ($this->isSeekable()) { 370 | $this->rewind(); 371 | } 372 | 373 | return $this->getContents(); 374 | } 375 | 376 | /* 377 | |-------------------------------------------------------------------------- 378 | | Non PSR-7 Methods. 379 | |-------------------------------------------------------------------------- 380 | */ 381 | 382 | /** 383 | * Throw exception if stream is not a valid PHP resource. 384 | * 385 | * @param resource $stream A valid resource. 386 | * 387 | * @return void 388 | * 389 | * InvalidArgumentException 390 | */ 391 | protected function assertStream($stream): void 392 | { 393 | if (!is_resource($stream)) { 394 | throw new InvalidArgumentException( 395 | sprintf( 396 | 'Stream should be a resource, but "%s" provided.', 397 | gettype($stream) 398 | ) 399 | ); 400 | } 401 | } 402 | 403 | /** 404 | * Throw an exception if the property does not exist. 405 | * 406 | * @return RuntimeException 407 | */ 408 | protected function assertPropertyStream(): void 409 | { 410 | if (!$this->isStream()) { 411 | throw new RuntimeException( 412 | 'Stream does not exist.' 413 | ); 414 | } 415 | } 416 | 417 | /** 418 | * Check if stream exists or not. 419 | * 420 | * @return bool 421 | */ 422 | protected function isStream(): bool 423 | { 424 | return (isset($this->stream) && is_resource($this->stream)); 425 | } 426 | } 427 | -------------------------------------------------------------------------------- /src/Psr7/UploadedFile.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\StreamInterface; 16 | use Psr\Http\Message\UploadedFileInterface; 17 | use Shieldon\Psr7\Stream; 18 | use InvalidArgumentException; 19 | use RuntimeException; 20 | 21 | use function file_exists; 22 | use function file_put_contents; 23 | use function is_string; 24 | use function is_uploaded_file; 25 | use function is_writable; 26 | use function move_uploaded_file; 27 | use function php_sapi_name; 28 | use function rename; 29 | 30 | use const UPLOAD_ERR_CANT_WRITE; 31 | use const UPLOAD_ERR_EXTENSION; 32 | use const UPLOAD_ERR_FORM_SIZE; 33 | use const UPLOAD_ERR_INI_SIZE; 34 | use const UPLOAD_ERR_NO_FILE; 35 | use const UPLOAD_ERR_NO_TMP_DIR; 36 | use const UPLOAD_ERR_OK; 37 | use const UPLOAD_ERR_PARTIAL; 38 | use const LOCK_EX; 39 | 40 | /* 41 | * Describes a data stream. 42 | */ 43 | class UploadedFile implements UploadedFileInterface 44 | { 45 | /** 46 | * The full path of the file provided by client. 47 | * 48 | * @var string|null 49 | */ 50 | protected $file; 51 | 52 | /** 53 | * A stream representing the uploaded file. 54 | * 55 | * @var StreamInterface|null 56 | */ 57 | protected $stream; 58 | 59 | /** 60 | * Is file copy to stream when first time calling getStream(). 61 | * 62 | * @var bool 63 | */ 64 | protected $isFileToStream = false; 65 | 66 | /** 67 | * The file size based on the "size" key in the $_FILES array. 68 | * 69 | * @var int|null 70 | */ 71 | protected $size; 72 | 73 | /** 74 | * The filename based on the "name" key in the $_FILES array. 75 | * 76 | * @var string|null 77 | */ 78 | protected $name; 79 | 80 | /** 81 | * The type of a file. This value is based on the "type" key in the $_FILES array. 82 | * 83 | * @var string|null 84 | */ 85 | protected $type; 86 | 87 | /** 88 | * The error code associated with the uploaded file. 89 | * 90 | * @var int 91 | */ 92 | protected $error; 93 | 94 | /** 95 | * Check if the uploaded file has been moved or not. 96 | * 97 | * @var bool 98 | */ 99 | protected $isMoved = false; 100 | 101 | /** 102 | * The type of interface between web server and PHP. 103 | * This value is typically from `php_sapi_name`, might be changed ony for 104 | * unit testing purpose. 105 | * 106 | * @var string 107 | */ 108 | private $sapi; 109 | 110 | /** 111 | * UploadedFile constructor. 112 | * 113 | * @param string|StreamInterface $source The full path of a file or stream. 114 | * @param string|null $name The file name. 115 | * @param string|null $type The file media type. 116 | * @param int|null $size The file size in bytes. 117 | * @param int $error The status code of the upload. 118 | * @param string|null $sapi Only assign for unit testing purpose. 119 | */ 120 | public function __construct( 121 | $source , 122 | ?string $name = null, 123 | ?string $type = null, 124 | ?int $size = null, 125 | int $error = 0 , 126 | ?string $sapi = null 127 | ) { 128 | 129 | if (is_string($source)) { 130 | $this->file = $source; 131 | 132 | } elseif ($source instanceof StreamInterface) { 133 | $this->stream = $source; 134 | 135 | } else { 136 | throw new InvalidArgumentException( 137 | 'First argument accepts only a string or StreamInterface instance.' 138 | ); 139 | } 140 | 141 | $this->name = $name; 142 | $this->type = $type; 143 | $this->size = $size; 144 | $this->error = $error; 145 | $this->sapi = php_sapi_name(); 146 | 147 | if ($sapi) { 148 | $this->sapi = $sapi; 149 | } 150 | } 151 | 152 | /** 153 | * {@inheritdoc} 154 | */ 155 | public function getStream(): StreamInterface 156 | { 157 | if ($this->isMoved) { 158 | throw new RuntimeException( 159 | 'The stream has been moved.' 160 | ); 161 | } 162 | 163 | if (!$this->isFileToStream && !$this->stream) { 164 | $resource = @fopen($this->file, 'r'); 165 | if (is_resource($resource)) { 166 | $this->stream = new Stream($resource); 167 | } 168 | $this->isFileToStream = true; 169 | } 170 | 171 | if (!$this->stream) { 172 | throw new RuntimeException( 173 | 'No stream is available or can be created.' 174 | ); 175 | } 176 | 177 | return $this->stream; 178 | } 179 | 180 | /** 181 | * {@inheritdoc} 182 | */ 183 | public function moveTo($targetPath): void 184 | { 185 | if ($this->isMoved) { 186 | // Throw exception on the second or subsequent call to the method. 187 | throw new RuntimeException( 188 | 'Uploaded file already moved' 189 | ); 190 | } 191 | 192 | if (!is_writable(dirname($targetPath))) { 193 | // Throw exception if the $targetPath specified is invalid. 194 | throw new RuntimeException( 195 | sprintf( 196 | 'The target path "%s" is not writable.', 197 | $targetPath 198 | ) 199 | ); 200 | } 201 | 202 | // Is a file.. 203 | if (is_string($this->file) && ! empty($this->file)) { 204 | 205 | if ($this->sapi === 'cli') { 206 | 207 | if (!rename($this->file, $targetPath)) { 208 | 209 | // @codeCoverageIgnoreStart 210 | 211 | // Throw exception on any error during the move operation. 212 | throw new RuntimeException( 213 | sprintf( 214 | 'Could not rename the file to the target path "%s".', 215 | $targetPath 216 | ) 217 | ); 218 | 219 | // @codeCoverageIgnoreEnd 220 | } 221 | } else { 222 | 223 | if ( 224 | ! is_uploaded_file($this->file) || 225 | ! move_uploaded_file($this->file, $targetPath) 226 | ) { 227 | // Throw exception on any error during the move operation. 228 | throw new RuntimeException( 229 | sprintf( 230 | 'Could not move the file to the target path "%s".', 231 | $targetPath 232 | ) 233 | ); 234 | } 235 | } 236 | 237 | } elseif ($this->stream instanceof StreamInterface) { 238 | $content = $this->stream->getContents(); 239 | 240 | file_put_contents($targetPath, $content, LOCK_EX); 241 | 242 | // @codeCoverageIgnoreStart 243 | 244 | if (!file_exists($targetPath)) { 245 | // Throw exception on any error during the move operation. 246 | throw new RuntimeException( 247 | sprintf( 248 | 'Could not move the stream to the target path "%s".', 249 | $targetPath 250 | ) 251 | ); 252 | } 253 | 254 | // @codeCoverageIgnoreEnd 255 | 256 | unset($content, $this->stream); 257 | } 258 | 259 | $this->isMoved = true; 260 | } 261 | 262 | /** 263 | * {@inheritdoc} 264 | */ 265 | public function getSize(): ?int 266 | { 267 | return $this->size; 268 | } 269 | 270 | /** 271 | * {@inheritdoc} 272 | */ 273 | public function getError(): int 274 | { 275 | return $this->error; 276 | } 277 | 278 | /** 279 | * {@inheritdoc} 280 | */ 281 | public function getClientFilename(): ?string 282 | { 283 | return $this->name; 284 | } 285 | 286 | /** 287 | * {@inheritdoc} 288 | */ 289 | public function getClientMediaType(): ?string 290 | { 291 | return $this->type; 292 | } 293 | 294 | /* 295 | |-------------------------------------------------------------------------- 296 | | Non-PSR-7 Methods. 297 | |-------------------------------------------------------------------------- 298 | */ 299 | 300 | /** 301 | * Get error message when uploading files. 302 | * 303 | * @return string 304 | */ 305 | public function getErrorMessage(): string 306 | { 307 | $message = [ 308 | UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini', 309 | UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.', 310 | UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.', 311 | UPLOAD_ERR_NO_FILE => 'No file was uploaded.', 312 | UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.', 313 | UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.', 314 | UPLOAD_ERR_EXTENSION => 'File upload stopped by extension.', 315 | UPLOAD_ERR_OK => 'There is no error, the file uploaded with success.', 316 | ]; 317 | 318 | return $message[$this->error] ?? 'Unknown upload error.'; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/Psr7/Uri.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7; 14 | 15 | use Psr\Http\Message\UriInterface; 16 | use InvalidArgumentException; 17 | 18 | use function filter_var; 19 | use function gettype; 20 | use function is_integer; 21 | use function is_null; 22 | use function is_string; 23 | use function ltrim; 24 | use function parse_url; 25 | use function rawurlencode; 26 | use function sprintf; 27 | 28 | /* 29 | * Value object representing a URI. 30 | */ 31 | class Uri implements UriInterface 32 | { 33 | /** 34 | * foo://example.com:8042/over/there?name=ferret#nose 35 | * \_/ \______________/\_________/ \_________/ \__/ 36 | * | | | | | 37 | * scheme authority path query fragment 38 | */ 39 | 40 | /** 41 | * The scheme component of the URI. 42 | * For example, https://terryl.in/ 43 | * In this case, "https" is the scheme. 44 | * 45 | * @var string 46 | */ 47 | protected $scheme; 48 | 49 | /** 50 | * The user component of the URI. 51 | * For example, https://jack:1234@terryl.in 52 | * In this case, "jack" is the user. 53 | * 54 | * @var string 55 | */ 56 | protected $user; 57 | 58 | /** 59 | * The password component of the URI. 60 | * For example, http://jack:1234@terryl.in 61 | * In this case, "1234" is the password. 62 | * 63 | * @var string 64 | */ 65 | protected $pass; 66 | 67 | /** 68 | * The host component of the URI. 69 | * For example, https://terryl.in:443/zh/ 70 | * In this case, "terryl.in" is the host. 71 | * 72 | * @var string 73 | */ 74 | protected $host; 75 | 76 | /** 77 | * The port component of the URI. 78 | * For example, https://terryl.in:443 79 | * In this case, "443" is the port. 80 | * 81 | * @var int|null 82 | */ 83 | protected $port; 84 | 85 | /** 86 | * The path component of the URI. 87 | * For example, https://terryl.in/zh/?paged=2 88 | * In this case, "/zh/" is the path. 89 | * 90 | * @var string 91 | */ 92 | protected $path; 93 | 94 | /** 95 | * The query component of the URI. 96 | * For example, https://terryl.in/zh/?paged=2 97 | * In this case, "paged=2" is the query. 98 | * 99 | * @var string 100 | */ 101 | protected $query; 102 | 103 | /** 104 | * The fragment component of the URI. 105 | * For example, https://terryl.in/#main-container 106 | * In this case, "main-container" is the fragment. 107 | * 108 | * @var string 109 | */ 110 | protected $fragment; 111 | 112 | /** 113 | * Uri constructor. 114 | * 115 | * @param string $uri The URI. 116 | */ 117 | public function __construct($uri = '') 118 | { 119 | $this->assertString($uri, 'uri'); 120 | $this->init((array) parse_url($uri)); 121 | } 122 | 123 | /** 124 | * {@inheritdoc} 125 | */ 126 | public function getScheme(): string 127 | { 128 | return $this->scheme; 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | public function getAuthority(): string 135 | { 136 | $authority = ''; 137 | 138 | if ($this->getUserInfo()) { 139 | $authority .= $this->getUserInfo() . '@'; 140 | } 141 | 142 | $authority .= $this->getHost(); 143 | 144 | if (!is_null($this->getPort())) { 145 | $authority .= ':' . $this->getPort(); 146 | } 147 | 148 | return $authority; 149 | } 150 | 151 | /** 152 | * {@inheritdoc} 153 | */ 154 | public function getUserInfo(): string 155 | { 156 | $userInfo = $this->user; 157 | 158 | if ($this->pass !== '') { 159 | $userInfo .= ':' . $this->pass; 160 | } 161 | 162 | return $userInfo; 163 | } 164 | 165 | /** 166 | * {@inheritdoc} 167 | */ 168 | public function getHost(): string 169 | { 170 | return $this->host; 171 | } 172 | 173 | /** 174 | * {@inheritdoc} 175 | */ 176 | public function getPort(): ?int 177 | { 178 | return $this->port; 179 | } 180 | 181 | /** 182 | * {@inheritdoc} 183 | */ 184 | public function getPath(): string 185 | { 186 | return $this->path; 187 | } 188 | 189 | /** 190 | * {@inheritdoc} 191 | */ 192 | public function getQuery(): string 193 | { 194 | return $this->query; 195 | } 196 | 197 | /** 198 | * {@inheritdoc} 199 | */ 200 | public function getFragment(): string 201 | { 202 | return $this->fragment; 203 | } 204 | 205 | /** 206 | * {@inheritdoc} 207 | */ 208 | public function withScheme($scheme): UriInterface 209 | { 210 | $this->assertScheme($scheme); 211 | 212 | $scheme = $this->filter('scheme', ['scheme' => $scheme]); 213 | 214 | $clone = clone $this; 215 | $clone->scheme = $scheme; 216 | return $clone; 217 | } 218 | 219 | /** 220 | * {@inheritdoc} 221 | */ 222 | public function withUserInfo($user, $pass = null): UriInterface 223 | { 224 | $this->assertString($user, 'user'); 225 | $user = $this->filter('user', ['user' => $user]); 226 | 227 | if ($pass) { 228 | $this->assertString($pass, 'pass'); 229 | $pass = $this->filter('pass', ['pass' => $pass]); 230 | } 231 | 232 | $clone = clone $this; 233 | $clone->user = $user; 234 | $clone->pass = $pass; 235 | 236 | return $clone; 237 | } 238 | 239 | /** 240 | * {@inheritdoc} 241 | */ 242 | public function withHost($host): UriInterface 243 | { 244 | $this->assertHost($host); 245 | 246 | $host = $this->filter('host', ['host' => $host]); 247 | 248 | $clone = clone $this; 249 | $clone->host = $host; 250 | 251 | return $clone; 252 | } 253 | 254 | /** 255 | * {@inheritdoc} 256 | */ 257 | public function withPort($port): UriInterface 258 | { 259 | $this->assertPort($port); 260 | 261 | $port = $this->filter('port', ['port' => $port]); 262 | 263 | $clone = clone $this; 264 | $clone->port = $port; 265 | 266 | return $clone; 267 | } 268 | 269 | /** 270 | * {@inheritdoc} 271 | */ 272 | public function withPath($path): UriInterface 273 | { 274 | $this->assertString($path, 'path'); 275 | 276 | $path = $this->filter('path', ['path' => $path]); 277 | 278 | $clone = clone $this; 279 | $clone->path = '/' . rawurlencode(ltrim($path, '/')); 280 | 281 | return $clone; 282 | } 283 | 284 | /** 285 | * {@inheritdoc} 286 | */ 287 | public function withQuery($query): UriInterface 288 | { 289 | $this->assertString($query, 'query'); 290 | 291 | $query = $this->filter('query', ['query' => $query]); 292 | 293 | // & => %26 294 | // ? => %3F 295 | 296 | $clone = clone $this; 297 | $clone->query = $query; 298 | 299 | return $clone; 300 | } 301 | 302 | /** 303 | * {@inheritdoc} 304 | */ 305 | public function withFragment($fragment): UriInterface 306 | { 307 | $this->assertString($fragment, 'fragment'); 308 | 309 | $fragment = $this->filter('fragment', ['fragment' => $fragment]); 310 | 311 | $clone = clone $this; 312 | $clone->fragment = $fragment; 313 | 314 | return $clone; 315 | } 316 | 317 | /** 318 | * {@inheritdoc} 319 | */ 320 | public function __toString(): string 321 | { 322 | $uri = ''; 323 | 324 | // If a scheme is present, it MUST be suffixed by ":". 325 | if ($this->getScheme() !== '') { 326 | $uri .= $this->getScheme() . ':'; 327 | } 328 | 329 | // If an authority is present, it MUST be prefixed by "//". 330 | if ($this->getAuthority() !== '') { 331 | $uri .= '//' . $this->getAuthority(); 332 | } 333 | 334 | // If the path is rootless and an authority is present, the path MUST 335 | // be prefixed by "/". 336 | $uri .= '/' . ltrim($this->getPath(), '/'); 337 | 338 | // If a query is present, it MUST be prefixed by "?". 339 | if ($this->getQuery() !== '') { 340 | $uri .= '?' . $this->getQuery(); 341 | } 342 | 343 | // If a fragment is present, it MUST be prefixed by "#". 344 | if ($this->getFragment() !== '') { 345 | $uri .= '#' . $this->getFragment(); 346 | } 347 | 348 | return $uri; 349 | } 350 | 351 | /* 352 | |-------------------------------------------------------------------------- 353 | | Non PSR-7 Methods. 354 | |-------------------------------------------------------------------------- 355 | */ 356 | 357 | /** 358 | * Initialize. 359 | * 360 | * @param array $data Parsed URL data. 361 | * 362 | * @return void 363 | */ 364 | protected function init(array $data = []): void 365 | { 366 | $components = [ 367 | 'scheme', 368 | 'user', 369 | 'pass', 370 | 'host', 371 | 'port', 372 | 'path', 373 | 'query', 374 | 'fragment' 375 | ]; 376 | 377 | foreach ($components as $v) { 378 | $this->{$v} = $this->filter($v, $data); 379 | } 380 | } 381 | 382 | /** 383 | * Filter URI components. 384 | * 385 | * Users can provide both encoded and decoded characters. 386 | * Implementations ensure the correct encoding as outlined. 387 | * @see https://tools.ietf.org/html/rfc3986#section-2.2 388 | * 389 | * @param string $key The part of URI. 390 | * @param array $data Data parsed from a given URL. 391 | * 392 | * @return string|int|null 393 | */ 394 | protected function filter(string $key, $data) 395 | { 396 | $notExists = [ 397 | 'scheme' => '', 398 | 'user' => '', 399 | 'pass' => '', 400 | 'host' => '', 401 | 'port' => null, 402 | 'path' => '', 403 | 'query' => '', 404 | 'fragment' => '', 405 | ]; 406 | 407 | if (!isset($data[$key])) { 408 | return $notExists[$key]; 409 | } 410 | 411 | $value = $data[$key]; 412 | 413 | // gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@" 414 | // $genDelims = ':/\?#\[\]@'; 415 | 416 | // sub-delims = "!" / "$" / "&" / "'" / "(" / ")" 417 | // / "*" / "+" / "," / ";" / "=" 418 | $subDelims = '!\$&\'\(\)\*\+,;='; 419 | 420 | // $unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" 421 | $unReserved = 'a-zA-Z0-9\-\._~'; 422 | 423 | // Encoded characters, such as "?" encoded to "%3F". 424 | $encodePattern = '%(?![A-Fa-f0-9]{2})'; 425 | 426 | $regex = ''; 427 | 428 | switch ($key) { 429 | case 'host': 430 | case 'scheme': 431 | return strtolower($value); 432 | break; 433 | 434 | case 'query': 435 | case 'fragment': 436 | $specPattern = '%:@\/\?'; 437 | $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/'; 438 | break; 439 | 440 | case 'path': 441 | $specPattern = '%:@\/'; 442 | $regex = '/(?:[^' . $unReserved . $subDelims . $specPattern . ']+|' . $encodePattern . ')/'; 443 | break; 444 | 445 | case 'user': 446 | case 'pass': 447 | $regex = '/(?:[^%' . $unReserved . $subDelims . ']+|' . $encodePattern . ')/'; 448 | break; 449 | 450 | case 'port': 451 | if ($this->scheme === 'http' && (int) $value !== 80) { 452 | return (int) $value; 453 | } 454 | if ($this->scheme === 'https' && (int) $value !== 443) { 455 | return (int) $value; 456 | } 457 | if ($this->scheme === '') { 458 | return (int) $value; 459 | } 460 | return null; 461 | 462 | // endswitch 463 | } 464 | 465 | if ($regex) { 466 | return preg_replace_callback( 467 | $regex, 468 | function ($match) { 469 | return rawurlencode($match[0]); 470 | }, 471 | $value 472 | ); 473 | } 474 | 475 | // @codeCoverageIgnoreStart 476 | 477 | return $value; 478 | 479 | // @codeCoverageIgnoreEnd 480 | } 481 | 482 | /** 483 | * Throw exception for the invalid scheme. 484 | * 485 | * @param string $scheme The scheme string of a URI. 486 | * 487 | * @return void 488 | * 489 | * @throws InvalidArgumentException 490 | */ 491 | protected function assertScheme($scheme): void 492 | { 493 | $this->assertString($scheme, 'scheme'); 494 | 495 | $validSchemes = [ 496 | 0 => '', 497 | 1 => 'http', 498 | 2 => 'https', 499 | ]; 500 | 501 | if (!in_array($scheme, $validSchemes)) { 502 | throw new InvalidArgumentException( 503 | sprintf( 504 | 'The string "%s" is not a valid scheme.', 505 | $scheme 506 | ) 507 | ); 508 | } 509 | } 510 | 511 | /** 512 | * Throw exception for the invalid value. 513 | * 514 | * @param string $value The value to check. 515 | * @param string $name The name of the value. 516 | * 517 | * @return void 518 | * 519 | * @throws InvalidArgumentException 520 | */ 521 | protected function assertString($value, string $name = 'it'): void 522 | { 523 | if (!is_string($value)) { 524 | throw new InvalidArgumentException( 525 | sprintf( 526 | ucfirst($name) . ' must be a string, but %s provided.', 527 | gettype($value) 528 | ) 529 | ); 530 | } 531 | } 532 | 533 | /** 534 | * Throw exception for the invalid host string. 535 | * 536 | * @param string $host The host string to of a URI. 537 | * 538 | * @return void 539 | * 540 | * @throws InvalidArgumentException 541 | */ 542 | protected function assertHost($host): void 543 | { 544 | $this->assertString($host); 545 | 546 | if (empty($host)) { 547 | // Note: An empty host value is equivalent to removing the host. 548 | // So that if the host is empty, ignore the following check. 549 | return; 550 | } 551 | 552 | if (!filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { 553 | throw new InvalidArgumentException( 554 | sprintf( 555 | '"%s" is not a valid host', 556 | $host 557 | ) 558 | ); 559 | } 560 | } 561 | 562 | /** 563 | * Throw exception for the invalid port. 564 | * 565 | * @param null|int $port The port number to of a URI. 566 | * 567 | * @return void 568 | * 569 | * @throws InvalidArgumentException 570 | */ 571 | protected function assertPort($port): void 572 | { 573 | if ( 574 | !is_null($port) && 575 | !is_integer($port) 576 | ) { 577 | throw new InvalidArgumentException( 578 | sprintf( 579 | 'Port must be an integer or a null value, but %s provided.', 580 | gettype($port) 581 | ) 582 | ); 583 | } 584 | 585 | if (!($port > 0 && $port < 65535)) { 586 | throw new InvalidArgumentException( 587 | sprintf( 588 | 'Port number should be in a range of 0-65535, but %s provided.', 589 | $port 590 | ) 591 | ); 592 | } 593 | } 594 | } 595 | -------------------------------------------------------------------------------- /src/Psr7/Utils/UploadedFileHelper.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Psr7\Utils; 14 | 15 | use Shieldon\Psr7\UploadedFile; 16 | 17 | use function array_merge_recursive; 18 | use function rtrim; 19 | use function is_array; 20 | use function is_string; 21 | use function is_numeric; 22 | 23 | /* 24 | * The helper functions for converting $_FILES to an array of UploadedFile 25 | * instance, only used on ServerRequest class. 26 | * This class is not a part of PSR 7. 27 | */ 28 | class UploadedFileHelper 29 | { 30 | /** 31 | * Create an array for PSR-7 Uploaded File needed. 32 | * 33 | * @param array $files An array generally from $_FILES 34 | * @param bool $isConvert To covert and return $files as an UploadedFile instance. 35 | * 36 | * @return array|UploadedFile 37 | */ 38 | public static function uploadedFileParse(array $files) 39 | { 40 | $specTree = []; 41 | 42 | $specFields = [ 43 | 0 => 'tmp_name', 44 | 1 => 'name', 45 | 2 => 'type', 46 | 3 => 'size', 47 | 4 => 'error', 48 | ]; 49 | 50 | foreach ($files as $fileKey => $fileValue) { 51 | if (!isset($fileValue['tmp_name'])) { 52 | // @codeCoverageIgnoreStart 53 | return []; 54 | // @codeCoverageIgnoreEnd 55 | } 56 | 57 | if (is_string($fileValue['tmp_name']) || is_numeric($fileValue['tmp_name'])) { 58 | $specTree[$fileKey] = $fileValue; 59 | 60 | } elseif (is_array($fileValue['tmp_name'])) { 61 | 62 | $tmp = []; 63 | 64 | // We want to find out how many levels of array it has. 65 | foreach ($specFields as $i => $attr) { 66 | $tmp[$i] = self::uploadedFileNestedFields($fileValue, $attr); 67 | } 68 | 69 | $parsedTree = array_merge_recursive( 70 | $tmp[0], // tmp_name 71 | $tmp[1], // name 72 | $tmp[2], // type 73 | $tmp[3], // size 74 | $tmp[4] // error 75 | ); 76 | 77 | $specTree[$fileKey] = $parsedTree; 78 | unset($tmp, $parsedTree); 79 | } 80 | } 81 | 82 | return self::uploadedFileArrayTrim($specTree); 83 | } 84 | 85 | /** 86 | * Find out how many levels of an array it has. 87 | * 88 | * @param array $files Data structure from $_FILES. 89 | * @param string $attr The attributes of a file. 90 | * 91 | * @return array 92 | */ 93 | public static function uploadedFileNestedFields(array $files, string $attr): array 94 | { 95 | $result = []; 96 | $values = $files; 97 | 98 | if (isset($files[$attr])) { 99 | $values = $files[$attr]; 100 | } 101 | 102 | foreach ($values as $key => $value) { 103 | 104 | /** 105 | * Hereby to add `_` to be a part of the key for letting `array_merge_recursive` 106 | * method can deal with numeric keys as string keys. 107 | * It will be restored in the next step. 108 | * 109 | * @see uploadedFileArrayTrim 110 | */ 111 | if (is_numeric($key)) { 112 | $key .= '_'; 113 | } 114 | 115 | if (is_array($value)) { 116 | $result[$key] = self::uploadedFileNestedFields($value, $attr); 117 | } else { 118 | $result[$key][$attr] = $value; 119 | } 120 | } 121 | 122 | return $result; 123 | } 124 | 125 | /** 126 | * That's because that PHP function `array_merge_recursive` has the different 127 | * results as dealing with string keys and numeric keys. 128 | * In the previous step, we made numeric keys to stringify, so that we want to 129 | * restore them back to numeric ones. 130 | * 131 | * @param array|string $values 132 | * 133 | * @return array|string 134 | */ 135 | public static function uploadedFileArrayTrim($values) 136 | { 137 | $result = []; 138 | 139 | if (is_array($values)) { 140 | 141 | foreach ($values as $key => $value) { 142 | 143 | // Restore the keys back to the original ones. 144 | $key = rtrim($key, '_'); 145 | 146 | if (is_array($value)) { 147 | $result[$key] = self::uploadedFileArrayTrim($value); 148 | } else { 149 | $result[$key] = $value; 150 | } 151 | } 152 | } 153 | 154 | return $result; 155 | } 156 | 157 | /** 158 | * Convert the parse-ready array into PSR-7 specs. 159 | * 160 | * @param string|array $values 161 | * 162 | * @return array 163 | */ 164 | public static function uploadedFileSpecsConvert($values) 165 | { 166 | $result = []; 167 | 168 | if (is_array($values)) { 169 | 170 | foreach ($values as $key => $value) { 171 | 172 | if (is_array($value)) { 173 | 174 | // Continue querying self, until a string is found. 175 | $result[$key] = self::uploadedFileSpecsConvert($value); 176 | 177 | } elseif ($key === 'tmp_name') { 178 | 179 | /** 180 | * Once one of the keys on the same level has been found, 181 | * then we can fetch the others at a time. 182 | * In this case, the `tmp_name` found. 183 | */ 184 | $result = new uploadedFile( 185 | $values['tmp_name'], 186 | $values['name'], 187 | $values['type'], 188 | $values['size'], 189 | $values['error'] 190 | ); 191 | } 192 | } 193 | } 194 | 195 | return $result; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/Psr15/ApiMiddleware.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr15; 12 | 13 | use Psr\Http\Server\RequestHandlerInterface; 14 | use Psr\Http\Message\ServerRequestInterface; 15 | use Psr\Http\Message\ResponseInterface; 16 | use Shieldon\Psr15\Middleware; 17 | use Shieldon\Psr7\Response; 18 | 19 | class ApiMiddleware extends Middleware 20 | { 21 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 22 | { 23 | $contentType = $request->getHeaderLine('Content-Type'); 24 | $key = $request->getHeaderLine('key'); 25 | $secret = $request->getHeaderLine('secret'); 26 | 27 | if ($contentType !== 'application/json') { 28 | return (new Response)->withStatus(406, 'Content type is not accepted.'); 29 | } 30 | 31 | if ($key !== '23492834234') { 32 | return (new Response)->withStatus(401, 'API key is invalid.'); 33 | } 34 | 35 | if ($secret !== '1a163782ee166156294d173fcf8b8e87') { 36 | return (new Response)->withStatus(401, 'API secret is invalid.'); 37 | } 38 | 39 | return $handler->handle($request); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/Psr15/FinalHandler.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | declare(strict_types=1); 12 | 13 | namespace Shieldon\Test\Psr15; 14 | 15 | use Psr\Http\Message\ServerRequestInterface; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Shieldon\Psr15\RequestHandler; 18 | use Shieldon\Psr7\Response; 19 | 20 | /** 21 | * PSR-15 Middleware 22 | */ 23 | class FinalHandler extends RequestHandler 24 | { 25 | public function handle(ServerRequestInterface $request): ResponseInterface 26 | { 27 | $response = new Response(); 28 | 29 | if (!empty($request->getAttribute('string'))) { 30 | $response = $response->withStatus(200, 'OK'); 31 | $response->getBody()->write('e04su3su;6'); 32 | $response->getBody()->rewind(); 33 | } 34 | 35 | return $response; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Psr15/RequestHandlerTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr15; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Shieldon\Psr15\RequestHandler; 15 | use Shieldon\Psr17\ServerRequestFactory; 16 | use Shieldon\Psr17\Utils\SuperGlobal; 17 | use Shieldon\Test\Psr15\ApiMiddleware; 18 | use Shieldon\Test\Psr15\StringMiddleware; 19 | 20 | class RequestHandlerTest extends TestCase 21 | { 22 | public function test_requestHandler() 23 | { 24 | $app = new RequestHandler(); 25 | 26 | $app->add(new ApiMiddleware()); 27 | $app->add(new StringMiddleware()); 28 | 29 | $response = $app->handle(ServerRequestFactory::fromGlobal()); 30 | 31 | $this->assertEquals(406, $response->getStatusCode()); 32 | $this->assertEquals('', $response->getBody()->getContents()); 33 | } 34 | 35 | public function test_requestHandler_Condition_2() 36 | { 37 | $finalHandler = new FinalHandler(); 38 | 39 | $app = new RequestHandler($finalHandler); 40 | 41 | $app->add(new ApiMiddleware()); 42 | $app->add(new StringMiddleware()); 43 | 44 | $request = ServerRequestFactory::fromGlobal(); 45 | 46 | $request = $request->withHeader('Content-Type', 'application/json')-> 47 | withHeader('key', '23492834234')-> 48 | withHeader('secret', '1a163782ee166156294d173fcf8b8e87'); 49 | 50 | 51 | $response = $app->handle($request); 52 | 53 | $this->assertEquals(200, $response->getStatusCode()); 54 | $this->assertEquals('e04su3su;6', $response->getBody()->getContents()); 55 | } 56 | 57 | public function test_requestHandler_Condition_3() 58 | { 59 | // Without a fallback handler... 60 | $app = new RequestHandler(); 61 | 62 | $app->add(new ApiMiddleware()); 63 | $app->add(new StringMiddleware()); 64 | 65 | $request = ServerRequestFactory::fromGlobal(); 66 | 67 | $request = $request->withHeader('Content-Type', 'application/json')-> 68 | withHeader('key', '23492834234')-> 69 | withHeader('secret', '1a163782ee166156294d173fcf8b8e87'); 70 | 71 | 72 | $response = $app->handle($request); 73 | 74 | $this->assertEquals(200, $response->getStatusCode()); 75 | $this->assertEquals('', $response->getBody()->getContents()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Psr15/StringMiddleware.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr15; 12 | 13 | use Psr\Http\Server\RequestHandlerInterface; 14 | use Psr\Http\Message\ServerRequestInterface; 15 | use Psr\Http\Message\ResponseInterface; 16 | use Shieldon\Psr15\Middleware; 17 | 18 | class StringMiddleware extends Middleware 19 | { 20 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface 21 | { 22 | $request = $request->withAttribute('string', 'e04'); 23 | 24 | return $handler->handle($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Psr17/RequestFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\RequestInterface; 15 | use Shieldon\Psr17\RequestFactory; 16 | 17 | class RequestFactoryTest extends TestCase 18 | { 19 | public function test_createRequest() 20 | { 21 | $requestFactory = new RequestFactory(); 22 | $request = $requestFactory->createRequest('POST', 'https://www.google.com'); 23 | 24 | $this->assertTrue(($request instanceof RequestInterface)); 25 | } 26 | 27 | public function test_createNew() 28 | { 29 | $request = RequestFactory::fromNew(); 30 | 31 | $this->assertTrue(($request instanceof RequestInterface)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Psr17/ResponseFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\ResponseInterface; 15 | use Shieldon\Psr17\ResponseFactory; 16 | 17 | class ResponseFactoryTest extends TestCase 18 | { 19 | public function test_createResponse() 20 | { 21 | $responseFactory = new ResponseFactory(); 22 | $response = $responseFactory->createResponse(200, 'OK'); 23 | 24 | $this->assertTrue(($response instanceof ResponseInterface)); 25 | } 26 | 27 | public function test_createNew() 28 | { 29 | $response = ResponseFactory::fromNew(); 30 | 31 | $this->assertTrue(($response instanceof ResponseInterface)); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Psr17/ServerRequestFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\ServerRequestInterface; 15 | use Shieldon\Psr17\ServerRequestFactory; 16 | use Shieldon\Psr17\Utils\SuperGlobal; 17 | 18 | class ServerRequestFactoryTest extends TestCase 19 | { 20 | public function test_createServerRequest() 21 | { 22 | SuperGlobal::mockCliEnvironment(); 23 | 24 | $serverRequestFactory = new ServerRequestFactory(); 25 | $serverRequest = $serverRequestFactory->createServerRequest('GET', '', $_SERVER); 26 | 27 | $this->assertTrue(($serverRequest instanceof ServerRequestInterface)); 28 | } 29 | 30 | public function test_createServerRequestFromGlobal() 31 | { 32 | SuperGlobal::mockCliEnvironment([ 33 | 'PHP_AUTH_USER' => 'terry', 34 | 'PHP_AUTH_PW' => '1234', 35 | 'QUERY_STRING' => 'foo=bar' 36 | ]); 37 | 38 | $serverRequest = ServerRequestFactory::fromGlobal(); 39 | 40 | $this->assertTrue(($serverRequest instanceof ServerRequestInterface)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/Psr17/StreamFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\StreamInterface; 15 | use Shieldon\Psr17\StreamFactory; 16 | use ReflectionObject; 17 | use InvalidArgumentException; 18 | use RuntimeException; 19 | 20 | class StreamFactoryTest extends TestCase 21 | { 22 | public function test_createStream() 23 | { 24 | $streamFactory = new StreamFactory(); 25 | $stream = $streamFactory->createStream('Foo Bar'); 26 | 27 | ob_start(); 28 | echo $stream; 29 | $output = ob_get_contents(); 30 | ob_end_clean(); 31 | 32 | $this->assertSame('Foo Bar', $output); 33 | } 34 | 35 | public function test_createStreamFromFile() 36 | { 37 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 38 | 39 | $streamFactory = new StreamFactory(); 40 | $stream = $streamFactory->createStreamFromFile($sourceFile); 41 | $this->assertTrue(($stream instanceof StreamInterface)); 42 | $this->assertSame($stream->getSize(), 15166); 43 | } 44 | 45 | public function test_createStreamFromResource() 46 | { 47 | $streamFactory = new StreamFactory(); 48 | $stream = $streamFactory->createStreamFromResource('this is string, not resource'); 49 | 50 | $this->assertTrue(($stream instanceof StreamInterface)); 51 | } 52 | 53 | public function test_fromNew() 54 | { 55 | $stream = StreamFactory::fromNew(); 56 | 57 | $this->assertTrue(($stream instanceof StreamInterface)); 58 | } 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Exceptions 63 | |-------------------------------------------------------------------------- 64 | */ 65 | 66 | public function test_Exception_CreateStreamFromFile_InvalidOpeningMethod() 67 | { 68 | $this->expectException(InvalidArgumentException::class); 69 | 70 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 71 | 72 | $streamFactory = new StreamFactory(); 73 | 74 | // Exception: 75 | // => Invalid file opening mode "b" 76 | $stream = $streamFactory->createStreamFromFile($sourceFile, 'b'); 77 | } 78 | 79 | public function test_Exception_CreateStreamFromFile_UnableToOpen() 80 | { 81 | $this->expectException(RuntimeException::class); 82 | 83 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo_not_exists.png'; 84 | 85 | $streamFactory = new StreamFactory(); 86 | 87 | // Exception: 88 | // => Invalid file opening mode "b" 89 | $stream = $streamFactory->createStreamFromFile($sourceFile); 90 | } 91 | 92 | public function test_Exception_assertResource() 93 | { 94 | $this->expectException(RuntimeException::class); 95 | 96 | $streamFactory = new StreamFactory(); 97 | $reflection = new ReflectionObject($streamFactory); 98 | $assertParsedBody = $reflection->getMethod('assertResource'); 99 | $assertParsedBody->setAccessible(true); 100 | 101 | // Exception: 102 | // => Unable to open "php://temp" resource. 103 | $assertParsedBody->invokeArgs($streamFactory, ['test string']); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Psr17/UploadFileFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\UploadedFileInterface; 15 | use Shieldon\Psr7\UploadedFile; 16 | use Shieldon\Psr17\UploadedFileFactory; 17 | use Shieldon\Psr17\StreamFactory; 18 | use ReflectionObject; 19 | use InvalidArgumentException; 20 | 21 | class UploadFileFactoryTest extends TestCase 22 | { 23 | public function test_createUploadedFile() 24 | { 25 | $uploadedFileFactory = new UploadedFileFactory(); 26 | 27 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 28 | $cloneFile = save_testing_file('shieldon_logo_clone_2.png'); 29 | $targetPath = save_testing_file('shieldon_logo_moved_from_file_2.png'); 30 | 31 | // Clone a sample file for testing MoveTo method. 32 | if (!copy($sourceFile, $cloneFile)) { 33 | $this->assertTrue(false); 34 | } 35 | 36 | $streamFactory = new StreamFactory(); 37 | $stream = $streamFactory->createStreamFromFile($cloneFile); 38 | 39 | $uploadedFileFactory = new UploadedFileFactory(); 40 | 41 | $uploadedFile = $uploadedFileFactory->createUploadedFile($stream); 42 | $this->assertTrue(($uploadedFile instanceof UploadedFileInterface)); 43 | 44 | $uploadedFile->moveTo($targetPath); 45 | 46 | if (file_exists($targetPath)) { 47 | $this->assertTrue(true); 48 | } 49 | 50 | unlink($targetPath); 51 | } 52 | 53 | public function test_createFromGlobal() 54 | { 55 | $_FILES = [ 56 | 57 | // 58 | 59 | 'files1' => [ 60 | 'name' => 'example1.jpg', 61 | 'type' => 'image/jpeg', 62 | 'tmp_name' => '/tmp/php200A.tmp', 63 | 'error' => 0, 64 | 'size' => 100000, 65 | ], 66 | 67 | // 68 | // 69 | 70 | 'files2' => [ 71 | 'name' => [ 72 | 'a' => 'example21.jpg', 73 | 'b' => 'example22.jpg', 74 | ], 75 | 'type' => [ 76 | 'a' => 'image/jpeg', 77 | 'b' => 'image/jpeg', 78 | ], 79 | 'tmp_name' => [ 80 | 'a' => '/tmp/php343C.tmp', 81 | 'b' => '/tmp/php343D.tmp', 82 | ], 83 | 'error' => [ 84 | 'a' => 0, 85 | 'b' => 0, 86 | ], 87 | 'size' => [ 88 | 'a' => 125100, 89 | 'b' => 145000, 90 | ], 91 | ], 92 | 93 | // 94 | // 95 | 96 | 'files3' => [ 97 | 'name' => [ 98 | 0 => 'example31.jpg', 99 | 1 => 'example32.jpg', 100 | ], 101 | 'type' => [ 102 | 0 => 'image/jpeg', 103 | 1 => 'image/jpeg', 104 | ], 105 | 'tmp_name' => [ 106 | 0 => '/tmp/php310C.tmp', 107 | 1 => '/tmp/php313D.tmp', 108 | ], 109 | 'error' => [ 110 | 0 => 0, 111 | 1 => 0, 112 | ], 113 | 'size' => [ 114 | 0 => 200000, 115 | 1 => 300000, 116 | ], 117 | ], 118 | 119 | // 120 | 121 | 'files4' => [ 122 | 'name' => [ 123 | 'details' => [ 124 | 'avatar' => 'my-avatar.png', 125 | ], 126 | ], 127 | 'type' => [ 128 | 'details' => [ 129 | 'avatar' => 'image/png', 130 | ], 131 | ], 132 | 'tmp_name' => [ 133 | 'details' => [ 134 | 'avatar' => '/tmp/phpmFLrzD', 135 | ], 136 | ], 137 | 'error' => [ 138 | 'details' => [ 139 | 'avatar' => 0, 140 | ], 141 | ], 142 | 'size' => [ 143 | 'details' => [ 144 | 'avatar' => 90996, 145 | ], 146 | ], 147 | ], 148 | ]; 149 | 150 | $results = UploadedFileFactory::fromGlobal(); 151 | 152 | $expectedFiles = [ 153 | 'files1' => new UploadedFile( 154 | '/tmp/php200A.tmp', 155 | 'example1.jpg', 156 | 'image/jpeg', 157 | 100000, 158 | 0 159 | ), 160 | 'files2' => [ 161 | 'a' => new UploadedFile( 162 | '/tmp/php343C.tmp', 163 | 'example21.jpg', 164 | 'image/jpeg', 165 | 125100, 166 | 0 167 | ), 168 | 'b' => new UploadedFile( 169 | '/tmp/php343D.tmp', 170 | 'example22.jpg', 171 | 'image/jpeg', 172 | 145000, 173 | 0 174 | ), 175 | ], 176 | 'files3' => [ 177 | 0 => new UploadedFile( 178 | '/tmp/php310C.tmp', 179 | 'example31.jpg', 180 | 'image/jpeg', 181 | 200000, 182 | 0 183 | ), 184 | 1 => new UploadedFile( 185 | '/tmp/php313D.tmp', 186 | 'example32.jpg', 187 | 'image/jpeg', 188 | 300000, 189 | 0 190 | ), 191 | ], 192 | 'files4' => [ 193 | 'details' => [ 194 | 'avatar' => new UploadedFile( 195 | '/tmp/phpmFLrzD', 196 | 'my-avatar.png', 197 | 'image/png', 198 | 90996, 199 | 0 200 | ), 201 | ], 202 | ], 203 | ]; 204 | 205 | $this->assertEquals($results, $expectedFiles); 206 | } 207 | 208 | public function testExample() 209 | { 210 | $_FILES = [ 211 | 'foo' => [ 212 | 'name' => 'example1.jpg', 213 | 'type' => 'image/jpeg', 214 | 'tmp_name' => '/tmp/php200A.tmp', 215 | 'error' => 0, 216 | 'size' => 100000, 217 | ], 218 | ]; 219 | 220 | $uploadFileArr = UploadedFileFactory::fromGlobal(); 221 | 222 | $filename = $uploadFileArr['foo']->getClientFilename(); 223 | 224 | $this->assertEquals('example1.jpg', $filename); 225 | } 226 | 227 | /* 228 | |-------------------------------------------------------------------------- 229 | | Exceptions 230 | |-------------------------------------------------------------------------- 231 | */ 232 | 233 | public function test_Exception_FileIsNotReadable() 234 | { 235 | $this->expectException(InvalidArgumentException::class); 236 | 237 | $uploadedFileFactory = new UploadedFileFactory(); 238 | 239 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 240 | 241 | $streamFactory = new StreamFactory(); 242 | $stream = $streamFactory->createStreamFromFile($sourceFile); 243 | 244 | $reflection = new ReflectionObject($stream); 245 | $readable = $reflection->getProperty('readable'); 246 | $readable->setAccessible(true); 247 | $readable->setValue($stream, false); 248 | 249 | $uploadedFileFactory = new UploadedFileFactory(); 250 | 251 | // Exception: 252 | // => File is not readable. 253 | $uploadedFile = $uploadedFileFactory->createUploadedFile($stream); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /tests/Psr17/UriFactoryTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\UriInterface; 15 | use Shieldon\Psr17\UriFactory; 16 | use Shieldon\Psr17\Utils\SuperGlobal; 17 | 18 | class UriFactoryTest extends TestCase 19 | { 20 | public function test_createUri() 21 | { 22 | $uriFactory = new UriFactory; 23 | 24 | $uri = $uriFactory->createUri(); 25 | $this->assertTrue(($uri instanceof UriInterface)); 26 | } 27 | 28 | public function test_fromGlobal() 29 | { 30 | SuperGlobal::mockCliEnvironment([ 31 | 'PHP_AUTH_USER' => 'terry', // user 32 | 'HTTP_HOST' => 'example.org', // host 33 | 'PHP_AUTH_PW' => '1234', // pass 34 | 'REQUEST_URI' => '/test', // path 35 | 'SERVER_PORT' => '8080', // port 36 | 'QUERY_STRING' => 'foo=bar', // query 37 | 'REQUEST_SCHEME' => 'https', // scheme 38 | ]); 39 | 40 | $uri = uriFactory::fromGlobal(); 41 | 42 | $this->assertSame($uri->getScheme(), 'https'); 43 | $this->assertSame($uri->getHost(), 'example.org'); 44 | $this->assertSame($uri->getUserInfo(), 'terry:1234'); // string 45 | $this->assertSame($uri->getPath(), '/test'); // string 46 | $this->assertSame($uri->getPort(), 8080); // int|null 47 | $this->assertSame($uri->getQuery(), 'foo=bar'); // string 48 | $this->assertSame($uri->getFragment(), ''); // string 49 | } 50 | 51 | public function test_fromNew() 52 | { 53 | $uri = uriFactory::fromNew(); 54 | $this->assertTrue(($uri instanceof UriInterface)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/Psr17/Utils/SuperGlobalTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr17\Utils; 12 | 13 | class SuperGlobalTest extends \PHPUnit\Framework\TestCase 14 | { 15 | public function test_Static_extract() 16 | { 17 | $_SERVER = []; 18 | 19 | $data = \Shieldon\Psr17\Utils\SuperGlobal::extract(); 20 | 21 | $array = [ 22 | 'HTTP_ACCEPT' => 'text/html,application/xhtml+xml,application/xml;q=0.9', 23 | 'HTTP_ACCEPT_CHARSET' => 'ISO-8859-1,utf-8;q=0.7,*;q=0.3', 24 | 'HTTP_ACCEPT_LANGUAGE' => 'en-US,en;q=0.9,zh-TW;q=0.8,zh;q=0.7', 25 | 'HTTP_HOST' => '127.0.0.1', 26 | 'HTTP_USER_AGENT' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 27 | 'QUERY_STRING' => '', 28 | 'REMOTE_ADDR' => '127.0.0.1', 29 | 'REQUEST_METHOD' => 'GET', 30 | 'REQUEST_SCHEME' => 'http', 31 | 'REQUEST_URI' => '', 32 | 'SCRIPT_NAME' => '', 33 | 'SERVER_NAME' => 'localhost', 34 | 'SERVER_PORT' => 80, 35 | 'SERVER_PROTOCOL' => 'HTTP/1.1', 36 | 'CONTENT_TYPE' => 'text/html; charset=UTF-8', 37 | 'HTTP_CONTENT_TYPE' => 'text/html; charset=UTF-8', // This is added by line: 46 38 | ]; 39 | 40 | unset($data['server']['REQUEST_TIME']); 41 | unset($data['server']['REQUEST_TIME_FLOAT']); 42 | 43 | $this->assertEquals($data['server'], $array); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tests/Psr7/MessageTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | 15 | use InvalidArgumentException; 16 | use Psr\Http\Message\MessageInterface; 17 | use ReflectionObject; 18 | use Shieldon\Psr7\Message; 19 | use Shieldon\Psr7\Stream; 20 | use stdClass; 21 | 22 | class MessageTest extends TestCase 23 | { 24 | public function test__construct() 25 | { 26 | $message = new Message(); 27 | 28 | $this->assertTrue(($message instanceof MessageInterface)); 29 | } 30 | 31 | public function test_GetPrefixMethods() 32 | { 33 | $message = $this->test_setHeaders(); 34 | 35 | $this->assertSame($message->getProtocolVersion(), '1.1'); 36 | $this->assertEquals($message->getHeader('user-agent'), ['Mozilla/5.0 (Windows NT 10.0; Win64; x64)']); 37 | $this->assertEquals($message->getHeader('header-not-exists'), []); 38 | $this->assertEquals($message->getHeaderLine('user-agent'), 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'); 39 | 40 | // Test - has 41 | $this->assertTrue($message->hasHeader('user-agent')); 42 | } 43 | 44 | public function test_WithPrefixMethods() 45 | { 46 | $message = $this->test_setHeaders(); 47 | $newMessage = $message->withProtocolVersion('2.0')->withHeader('hello-world', 'ok'); 48 | $this->assertSame($newMessage->getProtocolVersion(), '2.0'); 49 | $this->assertEquals($newMessage->getHeader('hello-world'), ['ok']); 50 | 51 | $new2Message = $newMessage 52 | ->withAddedHeader('hello-world', 'not-ok') 53 | ->withAddedHeader('foo-bar', 'okok') 54 | ->withAddedHeader('others', 2) 55 | ->withAddedHeader('others', 6.4); 56 | 57 | $this->assertEquals($new2Message->getHeader('hello-world'), ['ok', 'not-ok']); 58 | $this->assertEquals($new2Message->getHeader('foo-bar'), ['okok']); 59 | $this->assertEquals($new2Message->getHeader('others'), ['2', '6.4']); 60 | 61 | // Test - without 62 | $new3Message = $new2Message->withoutHeader('hello-world'); 63 | $this->assertFalse($new3Message->hasHeader('hello-world')); 64 | } 65 | 66 | public function test_bodyMethods() 67 | { 68 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 69 | $stream = new Stream($resource); 70 | 71 | $message = new Message(); 72 | $newMessage = $message->withBody($stream); 73 | $this->assertEquals($newMessage->getBody(), $stream); 74 | } 75 | 76 | public function test_setHeaders(): MessageInterface 77 | { 78 | $message = new Message(); 79 | 80 | $testArray = [ 81 | 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)', 82 | 'Custom-Value' => '1234', 83 | ]; 84 | 85 | $expectedArray = [ 86 | 'User-Agent' => ['Mozilla/5.0 (Windows NT 10.0; Win64; x64)'], 87 | 'Custom-Value' => ['1234'], 88 | ]; 89 | 90 | $reflection = new ReflectionObject($message); 91 | $setHeaders = $reflection->getMethod('setHeaders'); 92 | $setHeaders->setAccessible(true); 93 | $setHeaders->invokeArgs($message, [$testArray]); 94 | 95 | $this->assertEquals($message->getHeaders(), $expectedArray); 96 | 97 | return $message; 98 | } 99 | 100 | public function test_Static_ParseRawHeader() 101 | { 102 | // Test 1 - General request header. 103 | $rawHeader =<<assertSame($headers['Accept'], '*/*'); 115 | $this->assertSame($headers['Content-Type'], 'application/x-www-form-urlencoded;charset=UTF-8'); 116 | $this->assertSame($headers['Sec-Fetch-Dest'], 'empty'); 117 | $this->assertSame($headers['Sec-Fetch-Mode'], 'cors'); 118 | $this->assertSame($headers['Sec-Fetch-Site'], 'same-site'); 119 | $this->assertSame($headers['User-Agent'], 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'); 120 | $this->assertSame($headers['X-Client-Data'], 'CJC2yQEIorbJAQjBtskBCKmdygEIqLTKARibvsoB'); 121 | 122 | // Test - General response header. 123 | $rawHeader =<<assertSame($headers['access-control-allow-credentials'], 'true'); 149 | $this->assertSame($headers['access-control-allow-methods'], 'OPTIONS'); 150 | $this->assertSame($headers['access-control-allow-origin'], 'https://www.facebook.com'); 151 | $this->assertSame($headers['access-control-expose-headers'], 'X-FB-Debug, X-Loader-Length'); 152 | $this->assertSame($headers['alt-svc'], 'h3-27=":443"; ma=3600'); 153 | $this->assertSame($headers['cache-control'], 'private, no-cache, no-store, must-revalidate'); 154 | $this->assertSame($headers['content-length'], '0'); 155 | $this->assertSame($headers['content-security-policy'], "default-src * data: blob: 'self';script-src *.facebook.com *.fbcdn.net *.facebook.net *.google-analytics.com *.virtualearth.net *.google.com 127.0.0.1:* *.spotilocal.com:* 'unsafe-inline' 'unsafe-eval' blob: data: 'self';style-src data: blob: 'unsafe-inline' *;connect-src *.facebook.com facebook.com *.fbcdn.net *.facebook.net *.spotilocal.com:* wss://*.facebook.com:* https://fb.scanandcleanlocal.com:* attachment.fbsbx.com ws://localhost:* blob: *.cdninstagram.com 'self' chrome-extension://boadgeojelhgndaghljhdicfkmllpafd chrome-extension://dliochdbjfkdbacpmhlcpmleaejidimm;block-all-mixed-content;upgrade-insecure-requests;"); 156 | $this->assertSame($headers['content-type'], 'text/html; charset="utf-8"'); 157 | $this->assertSame($headers['date'], 'Thu, 04 Jun 2020 02:46:12 GMT'); 158 | $this->assertSame($headers['date'], 'Thu, 04 Jun 2020 02:46:12 GMT'); 159 | $this->assertSame($headers['expires'], 'Sat, 01 Jan 2000 00:00:00 GMT'); 160 | $this->assertSame($headers['pragma'], 'no-cache'); 161 | $this->assertSame($headers['status'], '200'); 162 | $this->assertSame($headers['strict-transport-security'], 'max-age=15552000; preload'); 163 | $this->assertSame($headers['vary'], 'Origin'); 164 | $this->assertSame($headers['x-content-type-options'], 'nosniff'); 165 | $this->assertSame($headers['x-fb-debug'], 'XNgyVH3VRKeOuGyxAit2WqKJ+334baHQuKQP0CM2lr/8ToZmwhNFU9N5ctr3LeTgXYWXemfGlJaAl/PASeEL5Q=='); 166 | $this->assertSame($headers['x-frame-options'], 'DENY'); 167 | $this->assertSame($headers['x-xss-protection'], '0'); 168 | 169 | // Test 3 - Just one line. 170 | $rawHeader =<<assertEquals(count($headers), 1); 176 | $this->assertSame($headers['access-control-allow-credentials'], 'true'); 177 | 178 | // Test 4 - Empty. 179 | $rawHeader = ''; 180 | $headers = Message::parseRawHeader($rawHeader); 181 | $this->assertSame($headers, []); 182 | } 183 | 184 | public function test_WithAddedHeaderArrayValueAndKeys() 185 | { 186 | $message = new Message(); 187 | $message = $message->withAddedHeader('content-type', ['foo' => 'text/html']); 188 | $message = $message->withAddedHeader('content-type', ['foo' => 'text/plain', 'bar' => 'application/json']); 189 | 190 | $headerLine = $message->getHeaderLine('content-type'); 191 | $this->assertMatchesRegularExpression('|text/html|', $headerLine); 192 | $this->assertMatchesRegularExpression('|text/plain|', $headerLine); 193 | $this->assertMatchesRegularExpression('|application/json|', $headerLine); 194 | 195 | $message = $message->withAddedHeader('foo', ''); 196 | $headerLine = $message->getHeaderLine('foo'); 197 | $this->assertSame('', $headerLine); 198 | } 199 | 200 | /* 201 | |-------------------------------------------------------------------------- 202 | | Exceptions 203 | |-------------------------------------------------------------------------- 204 | */ 205 | 206 | public function test_Exception_AssertHeaderFieldName() 207 | { 208 | $this->expectException(InvalidArgumentException::class); 209 | 210 | $message = new Message(); 211 | 212 | // Exception: 213 | // => "hello-wo)rld" is not valid header name, it must be an RFC 7230 compatible string. 214 | $newMessage = $message->withHeader('hello-wo)rld', 'ok'); 215 | } 216 | 217 | public function test_Exception_AssertHeaderFieldName_2() 218 | { 219 | $this->expectException(InvalidArgumentException::class); 220 | 221 | $message = new Message(); 222 | 223 | // Exception: 224 | // => "hello-wo)rld" is not valid header name, it must be an RFC 7230 compatible string. 225 | $newMessage = $message->withHeader(['test'], 'ok'); 226 | } 227 | 228 | public function test_Exception_AssertHeaderFieldValue_Booolean() 229 | { 230 | $this->expectException(InvalidArgumentException::class); 231 | 232 | $message = new Message(); 233 | 234 | // Exception: 235 | // => The header field value only accepts string and array, but "boolean" provided. 236 | $newMessage = $message->withHeader('hello-world', false); 237 | } 238 | 239 | public function test_Exception_AssertHeaderFieldValue_Null() 240 | { 241 | $this->expectException(InvalidArgumentException::class); 242 | 243 | $message = new Message(); 244 | 245 | // Exception: 246 | // => The header field value only accepts string and array, but "NULL" provided. 247 | $newMessage = $message->withHeader('hello-world', null); 248 | } 249 | 250 | public function test_Exception_AssertHeaderFieldValue_Object() 251 | { 252 | $this->expectException(InvalidArgumentException::class); 253 | 254 | $message = new Message(); 255 | $mockObject = new stdClass(); 256 | $mockObject->test = 1; 257 | 258 | // Exception: 259 | // => The header field value only accepts string and array, but "object" provided. 260 | $newMessage = $message->withHeader('hello-world', $mockObject); 261 | } 262 | 263 | public function test_Exception_AssertHeaderFieldValue_Array() 264 | { 265 | $this->expectException(InvalidArgumentException::class); 266 | 267 | // An invalid type is inside the array. 268 | $testArr = [ 269 | 'test', 270 | true, 271 | ]; 272 | 273 | $message = new Message(); 274 | 275 | // Exception: 276 | // => The header values only accept string and number, but "boolean" provided. 277 | $newMessage = $message->withHeader('hello-world', $testArr); 278 | } 279 | 280 | public function test_Exception_AssertHeaderFieldValue_InvalidString() 281 | { 282 | $this->expectException(InvalidArgumentException::class); 283 | 284 | $message = new Message(); 285 | 286 | // Exception: 287 | // => "This string contains many invisible spaces." is not valid header 288 | // value, it must contains visible ASCII characters only. 289 | $newMessage = $message->withHeader('hello-world', 'This string contains many invisible spaces.'); 290 | 291 | // $newMessage = $message->withHeader('hello-world', 'This string contains visible space.'); 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /tests/Psr7/RequestTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | 15 | use Psr\Http\Message\RequestInterface; 16 | use Psr\Http\Message\MessageInterface; 17 | use Psr\Http\Message\UriInterface; 18 | use Shieldon\Psr7\Request; 19 | use Shieldon\Psr7\Stream; 20 | use Shieldon\Psr7\Uri; 21 | use InvalidArgumentException; 22 | 23 | class RequestTest extends TestCase 24 | { 25 | public function test__construct() 26 | { 27 | $request = new Request('GET', '', '', [], '1.1'); 28 | 29 | $this->assertTrue(($request instanceof RequestInterface)); 30 | $this->assertTrue(($request instanceof MessageInterface)); 31 | 32 | $uri = new Uri('https://www.example.com'); 33 | $request = new Request('GET', $uri, '', [], '1.1'); 34 | 35 | $this->assertSame($request->getUri()->getHost(), 'www.example.com'); 36 | } 37 | 38 | public function test_GetPrefixMethods() 39 | { 40 | // Test 1 41 | 42 | $request = new Request('POST', 'https://terryl.in/zh/?test=test', '', ['test' => 1234], '1.1'); 43 | 44 | $this->assertSame($request->getRequestTarget(), '/zh/?test=test'); 45 | $this->assertSame($request->getMethod(), 'POST'); 46 | 47 | // Let's double check the Uri instance again. 48 | $this->assertTrue(($request->getUri() instanceof UriInterface)); 49 | $this->assertSame($request->getUri()->getScheme(), 'https'); 50 | $this->assertSame($request->getUri()->getHost(), 'terryl.in'); 51 | $this->assertSame($request->getUri()->getUserInfo(), ''); 52 | $this->assertSame($request->getUri()->getPath(), '/zh/'); 53 | $this->assertSame($request->getUri()->getPort(), null); 54 | $this->assertSame($request->getUri()->getQuery(), 'test=test'); 55 | $this->assertSame($request->getUri()->getFragment(), ''); 56 | 57 | // Test 2 58 | 59 | $request = new Request('GET', 'https://terryl.in', '', [], '1.1'); 60 | 61 | $this->assertSame($request->getRequestTarget(), '/'); 62 | } 63 | 64 | public function test_WithPrefixMethods() 65 | { 66 | $request = new Request('GET', 'https://terryl.in/zh/', '', [], '1.1'); 67 | 68 | $newRequest = $request->withMethod('POST')->withUri(new Uri('https://play.google.com')); 69 | 70 | $this->assertSame($newRequest->getMethod(), 'POST'); 71 | $this->assertSame($newRequest->getRequestTarget(), '/'); 72 | $this->assertSame($newRequest->getUri()->getHost(), 'play.google.com'); 73 | 74 | $new2Request = $newRequest->withRequestTarget('/newTarget/test/?q=1234'); 75 | $this->assertSame($new2Request->getRequestTarget(), '/newTarget/test/?q=1234'); 76 | 77 | $new3Request = $new2Request->withUri(new Uri('https://www.facebook.com'), true); 78 | 79 | // Preserve Host 80 | $this->assertSame($new3Request->getHeaderLine('host'), 'play.google.com'); 81 | $this->assertSame($new3Request->getUri()->getHost(), 'www.facebook.com'); 82 | } 83 | 84 | public function test_setBody() 85 | { 86 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 87 | $stream = new Stream($resource); 88 | 89 | $request = new Request('POST', 'https://terryl.in/zh/', $stream, [], '1.1'); 90 | $this->assertEquals($request->getBody(), $stream); 91 | 92 | $request = new Request('POST', 'https://terryl.in/zh/', 'test stream', [], '1.1'); 93 | $this->assertEquals(sprintf('%s', $request->getBody()->getContents()), 'test stream'); 94 | } 95 | 96 | /* 97 | |-------------------------------------------------------------------------- 98 | | Exceptions 99 | |-------------------------------------------------------------------------- 100 | */ 101 | 102 | public function test_Exception_assertMethod_1() 103 | { 104 | $this->expectException(InvalidArgumentException::class); 105 | $request = new Request('GET', 'https://terryl.in/', '', [], '1.1'); 106 | 107 | 108 | // Exception: 109 | // => HTTP method must be a string. 110 | $newRequest = $request->withMethod(['POST']); 111 | } 112 | 113 | public function test_Exception_assertMethod_2() 114 | { 115 | $this->expectException(InvalidArgumentException::class); 116 | 117 | // Exception: 118 | // => Unsupported HTTP method. It must be compatible with RFC-7231 119 | // request method, but "GETX" provided. 120 | $request = new Request('GETX', 'https://terryl.in/', '', [], '1.1'); 121 | } 122 | 123 | public function test_Exception_assertProtocolVersion() 124 | { 125 | $this->expectException(InvalidArgumentException::class); 126 | 127 | // Exception: 128 | // => Unsupported HTTP protocol version number. "1.5" provided. 129 | $request = new Request('GET', 'https://terryl.in/', '', [], '1.5'); 130 | } 131 | 132 | public function test_Exception_withRequestTarget_ContainSpaceCharacter() 133 | { 134 | $this->expectException(InvalidArgumentException::class); 135 | 136 | $request = new Request('GET', 'https://terryl.in/', '', [], '1.1'); 137 | 138 | // Exception: 139 | // => A request target cannot contain any whitespace. 140 | $newRequest = $request->withRequestTarget('/newTarget/te st/?q=1234'); 141 | } 142 | 143 | public function test_Exception_withRequestTarget_InvalidType() 144 | { 145 | $this->expectException(InvalidArgumentException::class); 146 | 147 | $request = new Request('GET', 'https://terryl.in/', '', [], '1.1'); 148 | 149 | // Exception: 150 | // => A request target must be a string. 151 | $newRequest = $request->withRequestTarget(['foo' => 'bar']); 152 | } 153 | 154 | public function test_Exception_Constructor() 155 | { 156 | $this->expectException(InvalidArgumentException::class); 157 | 158 | // Exception: 159 | // => URI should be a string or an instance of UriInterface, but array provided. 160 | $request = new Request('GET', [], '', [], '1.1'); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /tests/Psr7/ResponseTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | 15 | use Psr\Http\Message\MessageInterface; 16 | use Psr\Http\Message\ResponseInterface; 17 | use Shieldon\Psr7\Response; 18 | use InvalidArgumentException; 19 | 20 | class ResponseTest extends TestCase 21 | { 22 | public function test__construct() 23 | { 24 | $response = new Response(); 25 | 26 | $this->assertTrue(($response instanceof ResponseInterface)); 27 | $this->assertTrue(($response instanceof MessageInterface)); 28 | 29 | $newResponse = $response->withStatus(555, 'Custom reason phrase'); 30 | 31 | $this->assertSame($newResponse->getStatusCode(), 555); 32 | $this->assertSame($newResponse->getReasonPhrase(), 'Custom reason phrase'); 33 | 34 | $new2Response = $newResponse->withStatus(500); 35 | 36 | $this->assertSame($new2Response->getStatusCode(), 500); 37 | $this->assertSame($new2Response->getReasonPhrase(), 'Internal Server Error'); 38 | } 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Exceptions 43 | |-------------------------------------------------------------------------- 44 | */ 45 | 46 | public function test_Exception_AssertStatus_InvalidRange() 47 | { 48 | $this->expectException(InvalidArgumentException::class); 49 | 50 | // Exception: 51 | // => Status code should be in a range of 100-599, but 600 provided. 52 | $response = new Response(600); 53 | } 54 | 55 | public function test_Exception_AssertStatus_InvalidType() 56 | { 57 | $this->expectException(InvalidArgumentException::class); 58 | 59 | $response = new Response(); 60 | 61 | // Exception: 62 | // => Status code should be an integer value, but string provided. 63 | $newResponse = $response->withStatus("500", 'Custom reason phrase'); 64 | } 65 | 66 | public function test_Exception_assertReasonPhrase_InvalidType() 67 | { 68 | $this->expectException(InvalidArgumentException::class); 69 | 70 | $response = new Response(); 71 | 72 | // Exception: 73 | // => Reason phrase must be a string, but integer provided. 74 | $newResponse = $response->withStatus(200, 12345678); 75 | } 76 | 77 | public function test_Exception_assertReasonPhrase_ProhibitedCharacter() 78 | { 79 | $this->expectException(InvalidArgumentException::class); 80 | 81 | $response = new Response(); 82 | 83 | // Exception: 84 | // => Reason phrase contains "\r" that is considered as a prohibited character. 85 | $newResponse = $response->withStatus(200, 'Custom reason phrase\n\rThe next line'); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/Psr7/ServerRequestTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Shieldon\Psr7\ServerRequest; 15 | use Shieldon\Psr7\UploadedFile; 16 | use Shieldon\Psr7\Utils\UploadedFileHelper; 17 | use InvalidArgumentException; 18 | use Psr\Http\Message\ServerRequestInterface; 19 | use Psr\Http\Message\RequestInterface; 20 | use Psr\Http\Message\MessageInterface; 21 | use ReflectionObject; 22 | 23 | class ServerRequestTest extends TestCase 24 | { 25 | public function test__construct() 26 | { 27 | $serverRequest = new ServerRequest('GET', '', '', [], '1.1', [], [], [], [], self::mockFile(1)); 28 | 29 | $this->assertTrue(($serverRequest instanceof RequestInterface)); 30 | $this->assertTrue(($serverRequest instanceof MessageInterface)); 31 | $this->assertTrue(($serverRequest instanceof ServerRequestInterface)); 32 | } 33 | 34 | public function test_Properties() 35 | { 36 | $serverRequest = self::getServerRequest(); 37 | 38 | $properties = [ 39 | 'serverParams', 40 | 'cookieParams', 41 | 'parsedBody', 42 | 'queryParams', 43 | 'uploadedFiles', 44 | 'attributes', 45 | ]; 46 | 47 | $reflection = new ReflectionObject($serverRequest); 48 | 49 | foreach ($properties as $v) { 50 | $tmp = $reflection->getProperty($v); 51 | $tmp->setAccessible(true); 52 | ${$v} = $tmp->getValue($serverRequest); 53 | unset($tmp); 54 | } 55 | 56 | $this->assertSame($serverParams, []); 57 | $this->assertSame($cookieParams, []); 58 | $this->assertSame($parsedBody, null); 59 | $this->assertSame($queryParams, []); 60 | $this->assertSame($uploadedFiles, []); 61 | $this->assertSame($attributes, []); 62 | } 63 | 64 | public function test_GetPrefixMethods() 65 | { 66 | // Test 1 67 | 68 | $serverRequest = self::getServerRequest(); 69 | 70 | $this->assertSame($serverRequest->getServerParams(), []); 71 | $this->assertSame($serverRequest->getCookieParams(), []); 72 | $this->assertSame($serverRequest->getParsedBody(), null); 73 | $this->assertSame($serverRequest->getQueryParams(), []); 74 | $this->assertSame($serverRequest->getUploadedFiles(), []); 75 | $this->assertSame($serverRequest->getAttributes(), []); 76 | 77 | // Test 2 78 | $serverRequest = self::getServerRequest( 79 | 'POST', 80 | ['foo' => 'bar'], 81 | ['foo' => 'bar'], 82 | ['foo' => 'bar'], 83 | ['foo' => 'bar'], 84 | self::mockFile(1) 85 | ); 86 | 87 | $this->assertEquals($serverRequest->getServerParams(), ['foo' => 'bar']); 88 | $this->assertEquals($serverRequest->getCookieParams(), ['foo' => 'bar']); 89 | $this->assertEquals($serverRequest->getParsedBody(), ['foo' => 'bar']); 90 | $this->assertEquals($serverRequest->getQueryParams(), ['foo' => 'bar']); 91 | $this->assertEquals($serverRequest->getAttributes(), []); 92 | 93 | $this->assertEquals($serverRequest->getUploadedFiles(), [ 94 | 'files1' => new UploadedFile( 95 | '/tmp/php200A.tmp', 96 | 'example1.jpg', 97 | 'image/jpeg', 98 | 100000, 99 | 0 100 | ), 101 | ]); 102 | } 103 | 104 | public function test_WithPrefixMethods() 105 | { 106 | $serverRequest = self::getServerRequest(); 107 | 108 | $newUpload = UploadedFileHelper::uploadedFileSpecsConvert( 109 | UploadedFileHelper::uploadedFileParse(self::mockFile(2)) 110 | ); 111 | 112 | $new = $serverRequest->withCookieParams(['foo3' => 'bar3']) 113 | ->withParsedBody(['foo4' => 'bar4', 'foo5' => 'bar5']) 114 | ->withQueryParams(['foo6' => 'bar6', 'foo7' => 'bar7']) 115 | ->withAttribute('foo8', 'bar9') 116 | ->withUploadedFiles($newUpload); 117 | 118 | $this->assertEquals($new->getServerParams(), []); 119 | $this->assertEquals($new->getCookieParams(), ['foo3' => 'bar3']); 120 | $this->assertEquals($new->getParsedBody(), ['foo4' => 'bar4', 'foo5' => 'bar5']); 121 | $this->assertEquals($new->getQueryParams(), ['foo6' => 'bar6', 'foo7' => 'bar7']); 122 | $this->assertEquals($new->getAttribute('foo8'), 'bar9'); 123 | 124 | $this->assertEquals($new->getUploadedFiles(), [ 125 | 'avatar' => new UploadedFile( 126 | '/tmp/phpmFLrzD', 127 | 'my-avatar.png', 128 | 'image/png', 129 | 90996, 130 | 0 131 | ), 132 | ]); 133 | 134 | $new2 = $new->withoutAttribute('foo8'); 135 | 136 | $this->assertEquals($new2->getAttribute('foo8'), null); 137 | } 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | Exceptions 142 | |-------------------------------------------------------------------------- 143 | */ 144 | 145 | public function test_Exception_AssertUploadedFiles() 146 | { 147 | $this->expectException(InvalidArgumentException::class); 148 | 149 | $serverRequest = new ServerRequest('GET', 'https://example.com'); 150 | 151 | $reflection = new ReflectionObject($serverRequest); 152 | $assertUploadedFiles = $reflection->getMethod('assertUploadedFiles'); 153 | $assertUploadedFiles->setAccessible(true); 154 | 155 | // Exception: 156 | // => Invalid PSR-7 array structure for handling UploadedFile. 157 | $assertUploadedFiles->invokeArgs($serverRequest, [ 158 | [ 159 | ['files' => ''], 160 | ], 161 | ]); 162 | } 163 | 164 | public function test_Exception_AsertParsedBody() 165 | { 166 | $this->expectException(InvalidArgumentException::class); 167 | 168 | $serverRequest = new ServerRequest('GET', 'https://example.com'); 169 | 170 | $reflection = new ReflectionObject($serverRequest); 171 | $assertParsedBody = $reflection->getMethod('assertParsedBody'); 172 | $assertParsedBody->setAccessible(true); 173 | 174 | // Exception: 175 | // => Only accepts array, object and null, but string provided. 176 | $assertParsedBody->invokeArgs($serverRequest, ['invalid string body']); 177 | 178 | // Just for code coverage. 179 | $assertParsedBody->invokeArgs($serverRequest, [[]]); 180 | } 181 | 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Methods that help for testing. 185 | |-------------------------------------------------------------------------- 186 | */ 187 | 188 | /** 189 | * Get a ServerRequest instance for testing simply. 190 | * 191 | * @param string $method 192 | * @param array $server 193 | * @param array $cookie 194 | * @param array $post 195 | * @param array $get 196 | * @param array $files 197 | * 198 | * @return ServerRequest 199 | */ 200 | private static function getServerRequest( 201 | $method = 'GET', 202 | $server = [], 203 | $cookie = [], 204 | $post = [], 205 | $get = [], 206 | $files = [] 207 | ) 208 | { 209 | return new ServerRequest( 210 | $method, 211 | '', 212 | '', 213 | [], 214 | '1.1', 215 | $server, 216 | $cookie, 217 | $post, 218 | $get, 219 | $files 220 | ); 221 | } 222 | 223 | /** 224 | * Moke a $_FILES variable for testing simply. 225 | * 226 | * @return array 227 | */ 228 | private static function mockFile($item = 1) 229 | { 230 | if ($item === 1) { 231 | $_FILES['files1'] = [ 232 | 'name' => 'example1.jpg', 233 | 'type' => 'image/jpeg', 234 | 'tmp_name' => '/tmp/php200A.tmp', 235 | 'error' => 0, 236 | 'size' => 100000, 237 | ]; 238 | } 239 | 240 | if ($item === 2) { 241 | $_FILES['avatar'] = [ 242 | 'tmp_name' => '/tmp/phpmFLrzD', 243 | 'name' => 'my-avatar.png', 244 | 'type' => 'image/png', 245 | 'error' => 0, 246 | 'size' => 90996, 247 | ]; 248 | } 249 | 250 | return $_FILES; 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /tests/Psr7/StreamTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\StreamInterface; 15 | use Shieldon\Psr7\Stream; 16 | 17 | use InvalidArgumentException; 18 | use RuntimeException; 19 | use ReflectionObject; 20 | 21 | class StreamTest extends TestCase 22 | { 23 | public function test__construct() 24 | { 25 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 26 | $stream = new Stream($resource); 27 | 28 | $this->assertTrue(($stream instanceof StreamInterface)); 29 | 30 | $this->assertTrue($stream->isWritable()); 31 | $this->assertTrue($stream->isReadable()); 32 | $this->assertTrue($stream->isSeekable()); 33 | 34 | $this->assertTrue( 35 | is_integer($stream->getSize()) 36 | ); 37 | 38 | $this->assertTrue( 39 | is_bool($stream->eof()) 40 | ); 41 | 42 | $this->assertTrue( 43 | is_integer($stream->tell()) 44 | ); 45 | 46 | // close. 47 | $this->assertEquals($resource, $stream->detach()); 48 | 49 | $this->assertTrue( 50 | is_null($stream->getSize()) 51 | ); 52 | 53 | $this->assertTrue( 54 | is_null($stream->detach()) 55 | ); 56 | 57 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'c'); 58 | $stream = new Stream($resource); 59 | $meta = $stream->getMetadata(); 60 | 61 | $this->assertTrue($stream->isWritable()); 62 | $this->assertFalse($stream->isReadable()); 63 | 64 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r'); 65 | $stream = new Stream($resource); 66 | $meta = $stream->getMetadata(); 67 | 68 | $this->assertFalse($stream->isWritable()); 69 | $this->assertTrue($stream->isReadable()); 70 | } 71 | 72 | public function test__toString() 73 | { 74 | $stream = new Stream(fopen('php://temp', 'r+')); 75 | $stream->write('Foo Bar'); 76 | 77 | ob_start(); 78 | echo $stream; 79 | $output = ob_get_contents(); 80 | ob_end_clean(); 81 | 82 | $this->assertSame('Foo Bar', $output); 83 | } 84 | 85 | public function test_getSize() 86 | { 87 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 88 | $stream = new Stream($resource); 89 | $this->assertSame($stream->getSize(), 15166); 90 | 91 | $stream->close(); 92 | } 93 | 94 | public function test_getMetadata() 95 | { 96 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 97 | $stream = new Stream($resource); 98 | 99 | $expectedMeta = [ 100 | 'timed_out' => false, 101 | 'blocked' => true, 102 | 'eof' => false, 103 | 'wrapper_type' => 'plainfile', 104 | 'stream_type' => 'STDIO', 105 | 'mode' => 'r+', 106 | 'unread_bytes' => 0, 107 | 'seekable' => true, 108 | 'uri' => '/home/terrylin/data/psr7/tests/sample/shieldon_logo.png', 109 | ]; 110 | 111 | $meta = $stream->getMetadata(); 112 | 113 | $this->assertEquals($expectedMeta['mode'], $meta['mode']); 114 | $this->assertEquals($stream->getMetadata('mode'), 'r+'); 115 | 116 | $stream->close(); 117 | 118 | $this->assertEquals($stream->getMetadata(), null); 119 | } 120 | 121 | public function test_SeekAndRewind() 122 | { 123 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 124 | $stream = new Stream($resource); 125 | 126 | $stream->seek(10); 127 | $this->assertSame($stream->tell(), 10); 128 | 129 | $stream->rewind(); 130 | $this->assertSame($stream->tell(), 0); 131 | 132 | $stream->close(); 133 | } 134 | 135 | 136 | public function test_ReadAndWrite() 137 | { 138 | $stream = new Stream(fopen('php://temp', 'r+')); 139 | $stream->write('Foo Bar'); 140 | $stream->rewind(); 141 | $this->assertSame($stream->read(2), 'Fo'); 142 | 143 | $stream->close(); 144 | } 145 | 146 | /* 147 | |-------------------------------------------------------------------------- 148 | | Exceptions 149 | |-------------------------------------------------------------------------- 150 | */ 151 | 152 | public function test_Exception_assertStream() 153 | { 154 | $this->expectException(InvalidArgumentException::class); 155 | 156 | // Exception: 157 | // => Stream should be a resource, but string provided. 158 | $stream = new Stream('string'); 159 | } 160 | 161 | public function test_Exception_Tell_StreamDoesNotExist() 162 | { 163 | $this->expectException(RuntimeException::class); 164 | 165 | $stream = new Stream(fopen('php://temp', 'r+')); 166 | 167 | $stream->close(); 168 | 169 | // Exception: 170 | // => Stream does not exist. 171 | $position = $stream->tell(); 172 | } 173 | 174 | public function test_Exception_Seek_StreamDoesNotExist() 175 | { 176 | $this->expectException(RuntimeException::class); 177 | 178 | $stream = new Stream(fopen('php://temp', 'r+')); 179 | 180 | $stream->close(); 181 | 182 | // Exception: 183 | // => Stream does not exist. 184 | $stream->seek(10); 185 | } 186 | 187 | public function test_Exception_Seek_NotSeekable() 188 | { 189 | $this->expectException(RuntimeException::class); 190 | 191 | $stream = new Stream(fopen('php://temp', 'r')); 192 | 193 | $reflection = new ReflectionObject($stream); 194 | $seekable = $reflection->getProperty('seekable'); 195 | $seekable->setAccessible(true); 196 | $seekable->setValue($stream, false); 197 | 198 | // Exception: 199 | // => Stream is not seekable. 200 | $stream->seek(10); 201 | } 202 | 203 | public function test_Exception_Seek_StreamDoesNotSeekable() 204 | { 205 | $this->expectException(RuntimeException::class); 206 | 207 | $stream = new Stream(fopen('php://temp', 'r')); 208 | 209 | // Exception: 210 | // => Set position equal to offset bytes.. Unable to seek to stream at position 10 211 | $stream->seek(10); 212 | } 213 | 214 | public function test_Exception_Write_StreamDoesNotExist() 215 | { 216 | $this->expectException(RuntimeException::class); 217 | 218 | $stream = new Stream(fopen('php://temp', 'r+')); 219 | 220 | $stream->close(); 221 | 222 | // Exception: 223 | // => Stream does not exist. 224 | $stream->write('Foo Bar'); 225 | } 226 | 227 | public function test_Exception_Read_StreamDoesNotExist() 228 | { 229 | $this->expectException(RuntimeException::class); 230 | 231 | $stream = new Stream(fopen('php://temp', 'r+')); 232 | $stream->write('Foo Bar'); 233 | $stream->rewind(); 234 | $stream->close(); 235 | 236 | // Exception: 237 | // => Stream does not exist. 238 | $stream->read(2); 239 | } 240 | 241 | public function test_Exception_getContents_StreamDoesNotExist() 242 | { 243 | $this->expectException(RuntimeException::class); 244 | 245 | $stream = new Stream(fopen('php://temp', 'r+')); 246 | $stream->write('Foo Bar'); 247 | $stream->rewind(); 248 | $stream->close(); 249 | 250 | // Exception: 251 | // => Stream does not exist. 252 | $result = $stream->getContents(); 253 | } 254 | 255 | public function test_Exception_getContents_StreamIsNotReadable() 256 | { 257 | $this->expectException(RuntimeException::class); 258 | 259 | $stream = new Stream(fopen('php://temp', 'r+')); 260 | $stream->write('Foo Bar'); 261 | $stream->rewind(); 262 | 263 | $reflection = new ReflectionObject($stream); 264 | $seekable = $reflection->getProperty('readable'); 265 | $seekable->setAccessible(true); 266 | $seekable->setValue($stream, false); 267 | 268 | // Exception: 269 | // => Unable to read stream contents. 270 | $result = $stream->getContents(); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /tests/Psr7/UploadedFileTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\UploadedFileInterface; 15 | use Shieldon\Psr7\UploadedFile; 16 | use Shieldon\Psr7\Stream; 17 | 18 | use InvalidArgumentException; 19 | use Psr\Http\Message\StreamInterface; 20 | use RuntimeException; 21 | use ReflectionObject; 22 | 23 | class UploadedFileTest extends TestCase 24 | { 25 | public function test__construct() 26 | { 27 | // Test 1 28 | 29 | $uploadedFile = new UploadedFile( 30 | '/tmp/php200A.tmp', // source 31 | 'example1.jpg', // name 32 | 'image/jpeg', // type 33 | 100000, // size 34 | 0 // error 35 | ); 36 | 37 | $this->assertTrue(($uploadedFile instanceof UploadedFileInterface)); 38 | 39 | // Test 2 40 | 41 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 42 | $stream = new Stream($resource); 43 | $uploadedFile = new UploadedFile($stream); 44 | $stream2 = $uploadedFile->getStream(); // Test `getStream()` 45 | 46 | $this->assertEquals($stream, $stream2); 47 | $this->assertEquals($stream, $stream2); 48 | } 49 | 50 | public function test_MoveTo_Sapi_Cli() 51 | { 52 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 53 | $cloneFile = save_testing_file('shieldon_logo_clone.png'); 54 | $targetPath = save_testing_file('shieldon_logo_moved_from_file.png'); 55 | 56 | // Clone a sample file for testing MoveTo method. 57 | if (!copy($sourceFile, $cloneFile)) { 58 | $this->assertTrue(false); 59 | } 60 | 61 | $uploadedFile = new UploadedFile( 62 | $cloneFile, 63 | 'shieldon_logo.png', 64 | 'image/png', 65 | 100000, 66 | 0 67 | ); 68 | 69 | $uploadedFile->moveTo($targetPath); 70 | 71 | if (file_exists($targetPath)) { 72 | $this->assertTrue(true); 73 | } 74 | 75 | unlink($targetPath); 76 | } 77 | 78 | public function test_GetPrefixMethods() 79 | { 80 | $sourceFile = BOOTSTRAP_DIR . '/sample/shieldon_logo.png'; 81 | $cloneFile = save_testing_file('shieldon_logo_clone.png'); 82 | 83 | // Clone a sample file for testing MoveTo method. 84 | if (!copy($sourceFile, $cloneFile)) { 85 | $this->assertTrue(false); 86 | } 87 | 88 | $uploadedFile = new UploadedFile( 89 | $cloneFile, 90 | 'shieldon_logo.png', 91 | 'image/png', 92 | 100000, 93 | 0 94 | ); 95 | 96 | $this->assertSame($uploadedFile->getSize(), 100000); 97 | $this->assertSame($uploadedFile->getError(), 0); 98 | $this->assertSame($uploadedFile->getClientFilename(), 'shieldon_logo.png'); 99 | $this->assertSame($uploadedFile->getClientMediaType(), 'image/png'); 100 | $this->assertSame($uploadedFile->getErrorMessage(), 'There is no error, the file uploaded with success.'); 101 | 102 | $stream = $uploadedFile->getStream(); 103 | 104 | $this->assertTrue(($stream instanceof StreamInterface)); 105 | } 106 | 107 | public function testGetErrorMessage() 108 | { 109 | $uploadedFile = new UploadedFile( 110 | BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 111 | 'shieldon_logo.png', 112 | 'image/png', 113 | 100000, 114 | UPLOAD_ERR_OK 115 | ); 116 | 117 | $this->assertSame($uploadedFile->getErrorMessage(), 'There is no error, the file uploaded with success.'); 118 | 119 | $reflection = new ReflectionObject($uploadedFile); 120 | $error = $reflection->getProperty('error'); 121 | $error->setAccessible(true); 122 | 123 | $error->setValue($uploadedFile, UPLOAD_ERR_INI_SIZE); 124 | $this->assertSame( 125 | $uploadedFile->getErrorMessage(), 126 | 'The uploaded file exceeds the upload_max_filesize directive in php.ini' 127 | ); 128 | 129 | $error->setValue($uploadedFile, UPLOAD_ERR_FORM_SIZE); 130 | $this->assertSame( 131 | $uploadedFile->getErrorMessage(), 132 | 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.' 133 | ); 134 | 135 | $error->setValue($uploadedFile, UPLOAD_ERR_PARTIAL); 136 | $this->assertSame($uploadedFile->getErrorMessage(), 'The uploaded file was only partially uploaded.'); 137 | 138 | $error->setValue($uploadedFile, UPLOAD_ERR_NO_FILE); 139 | $this->assertSame($uploadedFile->getErrorMessage(), 'No file was uploaded.'); 140 | 141 | $error->setValue($uploadedFile, UPLOAD_ERR_NO_TMP_DIR); 142 | $this->assertSame($uploadedFile->getErrorMessage(), 'Missing a temporary folder.'); 143 | 144 | $error->setValue($uploadedFile, UPLOAD_ERR_CANT_WRITE); 145 | $this->assertSame($uploadedFile->getErrorMessage(), 'Failed to write file to disk.'); 146 | 147 | $error->setValue($uploadedFile, UPLOAD_ERR_EXTENSION); 148 | $this->assertSame($uploadedFile->getErrorMessage(), 'File upload stopped by extension.'); 149 | 150 | $error->setValue($uploadedFile, 19890604); 151 | $this->assertSame($uploadedFile->getErrorMessage(), 'Unknown upload error.'); 152 | } 153 | 154 | /* 155 | |-------------------------------------------------------------------------- 156 | | Exceptions 157 | |-------------------------------------------------------------------------- 158 | */ 159 | 160 | public function test_Exception_ArgumentIsInvalidSource() 161 | { 162 | $this->expectException(InvalidArgumentException::class); 163 | 164 | // Exception: 165 | // => First argument accepts only a string or StreamInterface instance. 166 | $uploadedFile = new UploadedFile([]); 167 | } 168 | 169 | public function test_Exception_GetStream_StreamIsNotAvailable() 170 | { 171 | $this->expectException(RuntimeException::class); 172 | 173 | // Test 1: Source is not a stream. 174 | 175 | $uploadedFile = new UploadedFile( 176 | '/tmp/php200A.tmp', // source 177 | 'example1.jpg', // name 178 | 'image/jpeg', // type 179 | 100000, // size 180 | 0 // error 181 | ); 182 | 183 | // Exception: 184 | // => No stream is available or can be created. 185 | $stream = $uploadedFile->getStream(); 186 | } 187 | 188 | public function test_Exception_GetStream_StreamIsMoved() 189 | { 190 | $this->expectException(RuntimeException::class); 191 | 192 | // Test 2: Stream has been moved, so can't find it using getStream(). 193 | 194 | $resource = fopen(BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 'r+'); 195 | $stream = new Stream($resource); 196 | $uploadedFile = new UploadedFile($stream); 197 | 198 | $targetPath = save_testing_file('shieldon_logo_moved_from_stream.png'); 199 | $uploadedFile->moveTo($targetPath); 200 | 201 | if (!file_exists($targetPath)) { 202 | // Remind us there is something wrong on this test. 203 | $this->assertTrue(false); 204 | } 205 | 206 | unlink($targetPath); 207 | 208 | // Exception: 209 | // => The stream has been moved 210 | $stream = $uploadedFile->getStream(); 211 | } 212 | 213 | public function test_Exception_MoveTo_FileIsMoved() 214 | { 215 | $this->expectException(RuntimeException::class); 216 | 217 | $uploadedFile = new UploadedFile( 218 | '/tmp/php200A.tmp', 219 | 'shieldon_logo.png', 220 | 'image/png', 221 | 100000, 222 | 0 223 | ); 224 | 225 | $reflection = new ReflectionObject($uploadedFile); 226 | $isMoved = $reflection->getProperty('isMoved'); 227 | $isMoved->setAccessible(true); 228 | $isMoved->setValue($uploadedFile, true); 229 | 230 | $targetPath = save_testing_file('shieldon_logo_moved_from_stream.png'); 231 | 232 | // Exception: 233 | // => The uploaded file has been moved. 234 | $uploadedFile->moveTo($targetPath); 235 | } 236 | 237 | public function test_Exception_MoveTo_TargetIsNotWritable() 238 | { 239 | $this->expectException(RuntimeException::class); 240 | 241 | $uploadedFile = new UploadedFile( 242 | BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 243 | 'shieldon_logo.png', 244 | 'image/png', 245 | 100000, 246 | 0 247 | ); 248 | 249 | // Exception: 250 | // => The target path "/tmp/folder-not-exists/test.png" is not writable. 251 | $uploadedFile->moveTo(BOOTSTRAP_DIR . '/tmp/folder-not-exists/test.png'); 252 | } 253 | 254 | public function test_Exception_MoveTo_FileNotUploaded() 255 | { 256 | $this->expectException(RuntimeException::class); 257 | 258 | $uploadedFile = new UploadedFile( 259 | BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 260 | 'shieldon_logo.png', 261 | 'image/png', 262 | 100000, 263 | 0, 264 | 'mock-is-uploaded-file-false' 265 | ); 266 | 267 | $targetPath = save_testing_file('shieldon_logo_moved_from_file.png'); 268 | 269 | $uploadedFile->moveTo($targetPath); 270 | } 271 | 272 | public function test_Exception_MoveTo_FileCannotBeMoved() 273 | { 274 | $this->expectException(RuntimeException::class); 275 | 276 | $uploadedFile = new UploadedFile( 277 | BOOTSTRAP_DIR . '/sample/shieldon_logo.png', 278 | 'shieldon_logo.png', 279 | 'image/png', 280 | 100000, 281 | 0, 282 | 'unit-test-2' 283 | ); 284 | 285 | $targetPath = save_testing_file('shieldon_logo_moved_from_file.png'); 286 | 287 | $uploadedFile->moveTo($targetPath); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /tests/Psr7/UriTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Psr\Http\Message\UriInterface; 15 | use Shieldon\Psr7\Uri; 16 | 17 | use InvalidArgumentException; 18 | use ReflectionObject; 19 | 20 | class UriTest extends TestCase 21 | { 22 | public function test__construct() 23 | { 24 | $uri = new Uri('http://jack:1234@example.com/demo/?test=5678&test2=90#section-1'); 25 | 26 | $this->assertTrue(($uri instanceof UriInterface)); 27 | } 28 | 29 | public function test__toString() 30 | { 31 | // Test 1 32 | 33 | $uri = new Uri('http://jack:1234@example.com:8888/demo/?test=5678&test2=90#section-1'); 34 | 35 | ob_start(); 36 | echo $uri; 37 | $output = ob_get_contents(); 38 | ob_end_clean(); 39 | 40 | $this->assertSame('http://jack:1234@example.com:8888/demo/?test=5678&test2=90#section-1', $output); 41 | 42 | // Test 2 43 | 44 | $uri = new Uri('http://example.com:8888/demo/#section-1'); 45 | 46 | ob_start(); 47 | echo $uri; 48 | $output = ob_get_contents(); 49 | ob_end_clean(); 50 | 51 | $this->assertSame('http://example.com:8888/demo/#section-1', $output); 52 | } 53 | 54 | public function test_Properties() 55 | { 56 | $uri = new Uri('http://jack:1234@example.com:8080/demo/?test=5678&test2=90#section-1'); 57 | 58 | $components = [ 59 | 'scheme', 60 | 'user', 61 | 'pass', 62 | 'host', 63 | 'port', 64 | 'path', 65 | 'query', 66 | 'fragment', 67 | ]; 68 | 69 | $reflection = new ReflectionObject($uri); 70 | 71 | foreach ($components as $v) { 72 | $tmp = $reflection->getProperty($v); 73 | $tmp->setAccessible(true); 74 | ${$v} = $tmp->getValue($uri); 75 | unset($tmp); 76 | } 77 | 78 | $this->assertSame($scheme, 'http'); 79 | $this->assertSame($host, 'example.com'); 80 | $this->assertSame($user, 'jack'); 81 | $this->assertSame($pass, '1234'); 82 | $this->assertSame($path, '/demo/'); 83 | $this->assertSame($port, 8080); 84 | $this->assertSame($query, 'test=5678&test2=90'); 85 | $this->assertSame($fragment, 'section-1'); 86 | } 87 | 88 | public function test_GetPrefixMethods() 89 | { 90 | // Test 1 91 | 92 | $uri = new Uri('http://jack:1234@example.com:8080/demo/?test=5678&test2=90#section-1'); 93 | 94 | $this->assertSame($uri->getScheme(), 'http'); 95 | $this->assertSame($uri->getHost(), 'example.com'); 96 | $this->assertSame($uri->getUserInfo(), 'jack:1234'); 97 | $this->assertSame($uri->getPath(), '/demo/'); 98 | $this->assertSame($uri->getPort(), 8080); 99 | $this->assertSame($uri->getQuery(), 'test=5678&test2=90'); 100 | $this->assertSame($uri->getFragment(), 'section-1'); 101 | 102 | // Test 2 103 | 104 | $uri = new Uri('https://www.example.com'); 105 | 106 | $this->assertSame($uri->getScheme(), 'https'); 107 | $this->assertSame($uri->getHost(), 'www.example.com'); 108 | $this->assertSame($uri->getUserInfo(), ''); // string 109 | $this->assertSame($uri->getPath(), ''); // string 110 | $this->assertSame($uri->getPort(), null); // int|null 111 | $this->assertSame($uri->getQuery(), ''); // string 112 | $this->assertSame($uri->getFragment(), ''); // string 113 | } 114 | 115 | public function test_WithPrefixMethods() 116 | { 117 | $uri = new Uri('https://www.example.com'); 118 | 119 | // Test 1 120 | 121 | $newUri = $uri->withScheme('http') 122 | ->withHost('example.com') 123 | ->withPort(8080) 124 | ->withUserInfo('jack', '4321') 125 | ->withPath('/en') 126 | ->withQuery('test=123') 127 | ->withFragment('1234'); 128 | 129 | $this->assertSame($newUri->getScheme(), 'http'); 130 | $this->assertSame($newUri->getHost(), 'example.com'); 131 | $this->assertSame($newUri->getUserInfo(), 'jack:4321'); 132 | $this->assertSame($newUri->getPath(), '/en'); 133 | $this->assertSame($newUri->getPort(), 8080); 134 | $this->assertSame($newUri->getQuery(), 'test=123'); 135 | $this->assertSame($newUri->getFragment(), '1234'); 136 | 137 | unset($newUri); 138 | 139 | // Test 2 140 | 141 | $newUri = $uri->withScheme('http') 142 | ->withHost('freedom.com') 143 | ->withPort(80) 144 | ->withUserInfo('people') 145 | ->withPath('/天安門') 146 | ->withQuery('chineseChars=六四') 147 | ->withFragment('19890604'); 148 | 149 | $this->assertSame($newUri->getScheme(), 'http'); 150 | $this->assertSame($newUri->getHost(), 'freedom.com'); 151 | $this->assertSame($newUri->getUserInfo(), 'people:'); 152 | $this->assertSame($newUri->getPath(), '/%25E5%25A4%25A9%25E5%25AE%2589%25E9%2596%2580'); 153 | $this->assertSame($newUri->getPort(), null); 154 | $this->assertSame($newUri->getQuery(), 'chineseChars=%E5%85%AD%E5%9B%9B'); 155 | $this->assertSame($newUri->getFragment(), '19890604'); 156 | } 157 | 158 | public function test_filterPort() 159 | { 160 | $uri = new Uri('http://example.com:80'); 161 | $this->assertSame($uri->getPort(), null); 162 | 163 | $uri = new Uri('//example.com:80'); 164 | $this->assertSame($uri->getPort(), 80); 165 | } 166 | 167 | /* 168 | |-------------------------------------------------------------------------- 169 | | Exceptions 170 | |-------------------------------------------------------------------------- 171 | */ 172 | 173 | public function test_Exception_AssertScheme() 174 | { 175 | $this->expectException(InvalidArgumentException::class); 176 | 177 | $uri = new Uri(); 178 | 179 | $reflection = new ReflectionObject($uri); 180 | $assertScheme = $reflection->getMethod('assertScheme'); 181 | $assertScheme->setAccessible(true); 182 | 183 | // Exception: 184 | // => The string "telnet" is not a valid scheme. 185 | $assertScheme->invokeArgs($uri, ['telnet']); 186 | } 187 | 188 | public function test_Exception_AssertString() 189 | { 190 | $this->expectException(InvalidArgumentException::class); 191 | 192 | $uri = new Uri(); 193 | 194 | $reflection = new ReflectionObject($uri); 195 | $assertString = $reflection->getMethod('assertString'); 196 | $assertString->setAccessible(true); 197 | 198 | // Exception: 199 | // => It must be a string, but integer provided. 200 | $assertString->invokeArgs($uri, [1234]); 201 | } 202 | 203 | public function test_Exception_AssertHost() 204 | { 205 | $this->expectException(InvalidArgumentException::class); 206 | 207 | $uri = new Uri(); 208 | 209 | $reflection = new ReflectionObject($uri); 210 | $assertHost = $reflection->getMethod('assertHost'); 211 | $assertHost->setAccessible(true); 212 | 213 | // Exception: 214 | // => "example_test.com" is not a valid host 215 | $assertHost->invokeArgs($uri, ['example_test.com']); 216 | } 217 | 218 | public function test_AssertHost_ReturnVoid() 219 | { 220 | $uri = new Uri(); 221 | 222 | $reflection = new ReflectionObject($uri); 223 | $assertHost = $reflection->getMethod('assertHost'); 224 | $assertHost->setAccessible(true); 225 | $result = $assertHost->invokeArgs($uri, ['']); 226 | 227 | $this->assertSame($result, null); 228 | } 229 | 230 | public function test_Exception_AssertPort_InvalidVariableType() 231 | { 232 | $this->expectException(InvalidArgumentException::class); 233 | 234 | $uri = new Uri(); 235 | 236 | $reflection = new ReflectionObject($uri); 237 | $assertPort = $reflection->getMethod('assertPort'); 238 | $assertPort->setAccessible(true); 239 | 240 | // Exception: 241 | // => Port must be an integer or a null value, but string provided. 242 | $assertPort->invokeArgs($uri, ['8080']); 243 | } 244 | 245 | public function test_Exception_AssertPort_InvalidRangeNumer() 246 | { 247 | $this->expectException(InvalidArgumentException::class); 248 | 249 | $uri = new Uri(); 250 | 251 | $reflection = new ReflectionObject($uri); 252 | $assertPort = $reflection->getMethod('assertPort'); 253 | $assertPort->setAccessible(true); 254 | 255 | // Exception: 256 | // => Port number should be in a range of 0-65535, but 70000 provided. 257 | $assertPort->invokeArgs($uri, [70000]); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /tests/Psr7/Utils/UploadedFileHelperTest.php: -------------------------------------------------------------------------------- 1 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | 11 | namespace Shieldon\Test\Psr7; 12 | 13 | use PHPUnit\Framework\TestCase; 14 | use Shieldon\Psr7\UploadedFile; 15 | use Shieldon\Psr7\Utils\UploadedFileHelper; 16 | 17 | class UploadedFileHelperTest extends TestCase 18 | { 19 | public function test_ParseUploadedFiles() 20 | { 21 | $files = [ 22 | 23 | // 24 | 25 | 'files1' => [ 26 | 'name' => 'example1.jpg', 27 | 'type' => 'image/jpeg', 28 | 'tmp_name' => '/tmp/php200A.tmp', 29 | 'error' => 0, 30 | 'size' => 100000, 31 | ], 32 | 33 | // 34 | // 35 | 36 | 'files2' => [ 37 | 'name' => [ 38 | 'a' => 'example21.jpg', 39 | 'b' => 'example22.jpg', 40 | ], 41 | 'type' => [ 42 | 'a' => 'image/jpeg', 43 | 'b' => 'image/jpeg', 44 | ], 45 | 'tmp_name' => [ 46 | 'a' => '/tmp/php343C.tmp', 47 | 'b' => '/tmp/php343D.tmp', 48 | ], 49 | 'error' => [ 50 | 'a' => 0, 51 | 'b' => 0, 52 | ], 53 | 'size' => [ 54 | 'a' => 125100, 55 | 'b' => 145000, 56 | ], 57 | ], 58 | 59 | // 60 | // 61 | 62 | 'files3' => [ 63 | 'name' => [ 64 | 0 => 'example31.jpg', 65 | 1 => 'example32.jpg', 66 | ], 67 | 'type' => [ 68 | 0 => 'image/jpeg', 69 | 1 => 'image/jpeg', 70 | ], 71 | 'tmp_name' => [ 72 | 0 => '/tmp/php310C.tmp', 73 | 1 => '/tmp/php313D.tmp', 74 | ], 75 | 'error' => [ 76 | 0 => 0, 77 | 1 => 0, 78 | ], 79 | 'size' => [ 80 | 0 => 200000, 81 | 1 => 300000, 82 | ], 83 | ], 84 | 85 | // 86 | 87 | 'files4' => [ 88 | 'name' => [ 89 | 'details' => [ 90 | 'avatar' => 'my-avatar.png', 91 | ], 92 | ], 93 | 'type' => [ 94 | 'details' => [ 95 | 'avatar' => 'image/png', 96 | ], 97 | ], 98 | 'tmp_name' => [ 99 | 'details' => [ 100 | 'avatar' => '/tmp/phpmFLrzD', 101 | ], 102 | ], 103 | 'error' => [ 104 | 'details' => [ 105 | 'avatar' => 0, 106 | ], 107 | ], 108 | 'size' => [ 109 | 'details' => [ 110 | 'avatar' => 90996, 111 | ], 112 | ], 113 | ], 114 | ]; 115 | 116 | $expectedFiles = [ 117 | 'files1' => [ 118 | 'name' => 'example1.jpg', 119 | 'type' => 'image/jpeg', 120 | 'tmp_name' => '/tmp/php200A.tmp', 121 | 'error' => 0, 122 | 'size' => 100000, 123 | ], 124 | 'files2' => [ 125 | 'a' => [ 126 | 'tmp_name' => '/tmp/php343C.tmp', 127 | 'name' => 'example21.jpg', 128 | 'type' => 'image/jpeg', 129 | 'error' => 0, 130 | 'size' => 125100, 131 | ], 132 | 'b' => [ 133 | 'tmp_name' => '/tmp/php343D.tmp', 134 | 'name' => 'example22.jpg', 135 | 'type' => 'image/jpeg', 136 | 'error' => 0, 137 | 'size' => 145000, 138 | ], 139 | ], 140 | 'files3' => [ 141 | 0 => [ 142 | 'tmp_name' => '/tmp/php310C.tmp', 143 | 'name' => 'example31.jpg', 144 | 'type' => 'image/jpeg', 145 | 'error' => 0, 146 | 'size' => 200000, 147 | ], 148 | 1 => [ 149 | 'tmp_name' => '/tmp/php313D.tmp', 150 | 'name' => 'example32.jpg', 151 | 'type' => 'image/jpeg', 152 | 'error' => 0, 153 | 'size' => 300000, 154 | ], 155 | ], 156 | 'files4' => [ 157 | 'details' => [ 158 | 'avatar' => [ 159 | 'tmp_name' => '/tmp/phpmFLrzD', 160 | 'name' => 'my-avatar.png', 161 | 'type' => 'image/png', 162 | 'error' => 0, 163 | 'size' => 90996, 164 | ], 165 | ], 166 | ], 167 | ]; 168 | 169 | $results = UploadedFileHelper::uploadedFileParse($files); 170 | 171 | $this->assertEquals($results, $expectedFiles); 172 | } 173 | public function test_UploadedFileSpecsConvert() 174 | { 175 | $formattedFiles = [ 176 | 'files1' => [ 177 | 'name' => 'example1.jpg', 178 | 'type' => 'image/jpeg', 179 | 'tmp_name' => '/tmp/php200A.tmp', 180 | 'error' => 0, 181 | 'size' => 100000, 182 | ], 183 | 'files2' => [ 184 | 'a' => [ 185 | 'tmp_name' => '/tmp/php343C.tmp', 186 | 'name' => 'example21.jpg', 187 | 'type' => 'image/jpeg', 188 | 'error' => 0, 189 | 'size' => 125100, 190 | ], 191 | 'b' => [ 192 | 'tmp_name' => '/tmp/php343D.tmp', 193 | 'name' => 'example22.jpg', 194 | 'type' => 'image/jpeg', 195 | 'error' => 0, 196 | 'size' => 145000, 197 | ], 198 | ], 199 | 'files3' => [ 200 | 0 => [ 201 | 'tmp_name' => '/tmp/php310C.tmp', 202 | 'name' => 'example31.jpg', 203 | 'type' => 'image/jpeg', 204 | 'error' => 0, 205 | 'size' => 200000, 206 | ], 207 | 1 => [ 208 | 'tmp_name' => '/tmp/php313D.tmp', 209 | 'name' => 'example32.jpg', 210 | 'type' => 'image/jpeg', 211 | 'error' => 0, 212 | 'size' => 300000, 213 | ], 214 | ], 215 | 'files4' => [ 216 | 'details' => [ 217 | 'avatar' => [ 218 | 'tmp_name' => '/tmp/phpmFLrzD', 219 | 'name' => 'my-avatar.png', 220 | 'type' => 'image/png', 221 | 'error' => 0, 222 | 'size' => 90996, 223 | ], 224 | ], 225 | ], 226 | ]; 227 | 228 | $expectedFiles = [ 229 | 'files1' => new UploadedFile( 230 | '/tmp/php200A.tmp', 231 | 'example1.jpg', 232 | 'image/jpeg', 233 | 100000, 234 | 0 235 | ), 236 | 'files2' => [ 237 | 'a' => new UploadedFile( 238 | '/tmp/php343C.tmp', 239 | 'example21.jpg', 240 | 'image/jpeg', 241 | 125100, 242 | 0 243 | ), 244 | 'b' => new UploadedFile( 245 | '/tmp/php343D.tmp', 246 | 'example22.jpg', 247 | 'image/jpeg', 248 | 145000, 249 | 0 250 | ), 251 | ], 252 | 'files3' => [ 253 | 0 => new UploadedFile( 254 | '/tmp/php310C.tmp', 255 | 'example31.jpg', 256 | 'image/jpeg', 257 | 200000, 258 | 0 259 | ), 260 | 1 => new UploadedFile( 261 | '/tmp/php313D.tmp', 262 | 'example32.jpg', 263 | 'image/jpeg', 264 | 300000, 265 | 0 266 | ), 267 | ], 268 | 'files4' => [ 269 | 'details' => [ 270 | 'avatar' => new UploadedFile( 271 | '/tmp/phpmFLrzD', 272 | 'my-avatar.png', 273 | 'image/png', 274 | 90996, 275 | 0 276 | ), 277 | ], 278 | ], 279 | ]; 280 | 281 | $results = UploadedFileHelper::uploadedFileSpecsConvert($formattedFiles); 282 | 283 | $this->assertEquals($results, $expectedFiles); 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |