├── src ├── RequestInterface.php ├── ResponseInterface.php ├── MessageInterface.php ├── Protocol.php ├── Method.php ├── AntiCSRF.php ├── RequestHeader.php ├── ResponseHeader.php ├── Cookie.php ├── Header.php ├── ResponseDownload.php ├── Debug │ └── HTTPCollector.php ├── URL.php ├── Message.php ├── UserAgent.php ├── CSP.php └── Response.php ├── LICENSE ├── README.md ├── composer.json └── .phpstorm.meta.php /src/RequestInterface.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 | namespace Framework\HTTP; 11 | 12 | /** 13 | * Interface RequestInterface. 14 | * 15 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#http_requests 16 | * 17 | * @package http 18 | */ 19 | interface RequestInterface extends MessageInterface 20 | { 21 | public function getMethod() : string; 22 | 23 | public function isMethod(string $method) : bool; 24 | 25 | public function getUrl() : URL; 26 | } 27 | -------------------------------------------------------------------------------- /src/ResponseInterface.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 | namespace Framework\HTTP; 11 | 12 | /** 13 | * Interface ResponseInterface. 14 | * 15 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#http_responses 16 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status 17 | * @see https://datatracker.ietf.org/doc/html/rfc7231#section-6 18 | * 19 | * @package http 20 | */ 21 | interface ResponseInterface extends MessageInterface 22 | { 23 | public function getStatusCode() : int; 24 | 25 | public function isStatusCode(int $code) : bool; 26 | 27 | public function getStatusReason() : string; 28 | 29 | public function getStatus() : string; 30 | } 31 | -------------------------------------------------------------------------------- /src/MessageInterface.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 | namespace Framework\HTTP; 11 | 12 | use Stringable; 13 | 14 | /** 15 | * Interface MessageInterface. 16 | * 17 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP 18 | * 19 | * @package http 20 | */ 21 | interface MessageInterface extends Stringable 22 | { 23 | public function getProtocol() : string; 24 | 25 | public function getStartLine() : string; 26 | 27 | public function getHeader(string $name) : ?string; 28 | 29 | public function hasHeader(string $name, string $value = null) : bool; 30 | 31 | /** 32 | * @return array 33 | */ 34 | public function getHeaders() : array; 35 | 36 | public function getBody() : string; 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Natan Felles 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Aplus Framework HTTP Library 2 | 3 | # Aplus Framework HTTP Library 4 | 5 | - [Home](https://aplus-framework.com/packages/http) 6 | - [User Guide](https://docs.aplus-framework.com/guides/libraries/http/index.html) 7 | - [API Documentation](https://docs.aplus-framework.com/packages/http.html) 8 | 9 | [![tests](https://github.com/aplus-framework/http/actions/workflows/tests.yml/badge.svg)](https://github.com/aplus-framework/http/actions/workflows/tests.yml) 10 | [![pipeline](https://gitlab.com/aplus-framework/libraries/http/badges/master/pipeline.svg)](https://gitlab.com/aplus-framework/libraries/http/-/pipelines?scope=branches) 11 | [![coverage](https://gitlab.com/aplus-framework/libraries/http/badges/master/coverage.svg?job=test:php)](https://aplus-framework.gitlab.io/libraries/http/coverage/) 12 | [![packagist](https://img.shields.io/packagist/v/aplus/http)](https://packagist.org/packages/aplus/http) 13 | [![open-source](https://img.shields.io/badge/open--source-sponsor-magenta)](https://aplus-framework.com/sponsor) 14 | -------------------------------------------------------------------------------- /src/Protocol.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 | namespace Framework\HTTP; 11 | 12 | use InvalidArgumentException; 13 | 14 | /** 15 | * Class Protocol. 16 | * 17 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview 18 | * 19 | * @package http 20 | */ 21 | class Protocol 22 | { 23 | /** 24 | * @see https://en.wikipedia.org/wiki/HTTP/1.0 25 | * 26 | * @var string 27 | */ 28 | public const HTTP_1_0 = 'HTTP/1.0'; 29 | /** 30 | * @see https://en.wikipedia.org/wiki/HTTP/1.1 31 | * 32 | * @var string 33 | */ 34 | public const HTTP_1_1 = 'HTTP/1.1'; 35 | /** 36 | * @see https://en.wikipedia.org/wiki/HTTP/2.0 37 | * 38 | * @var string 39 | */ 40 | public const HTTP_2_0 = 'HTTP/2.0'; 41 | /** 42 | * @see https://en.wikipedia.org/wiki/HTTP/2 43 | * 44 | * @var string 45 | */ 46 | public const HTTP_2 = 'HTTP/2'; 47 | /** 48 | * @see https://en.wikipedia.org/wiki/HTTP/3 49 | * 50 | * @var string 51 | */ 52 | public const HTTP_3 = 'HTTP/3'; 53 | /** 54 | * @var array 55 | */ 56 | protected static array $protocols = [ 57 | 'HTTP/1.0', 58 | 'HTTP/1.1', 59 | 'HTTP/2.0', 60 | 'HTTP/2', 61 | 'HTTP/3', 62 | ]; 63 | 64 | /** 65 | * @param string $protocol 66 | * 67 | * @throws InvalidArgumentException for invalid protocol 68 | * 69 | * @return string 70 | */ 71 | public static function validate(string $protocol) : string 72 | { 73 | $valid = \strtoupper($protocol); 74 | if (\in_array($valid, static::$protocols, true)) { 75 | return $valid; 76 | } 77 | throw new InvalidArgumentException('Invalid protocol: ' . $protocol); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aplus/http", 3 | "description": "Aplus Framework HTTP Library", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "http", 8 | "anti-csrf", 9 | "csrf", 10 | "message", 11 | "request", 12 | "response", 13 | "url", 14 | "cookie", 15 | "user-agent", 16 | "download", 17 | "rest" 18 | ], 19 | "authors": [ 20 | { 21 | "name": "Natan Felles", 22 | "email": "natanfelles@gmail.com", 23 | "homepage": "https://natanfelles.github.io" 24 | } 25 | ], 26 | "homepage": "https://aplus-framework.com/packages/http", 27 | "support": { 28 | "email": "support@aplus-framework.com", 29 | "issues": "https://gitlab.com/aplus-framework/libraries/http/issues", 30 | "forum": "https://aplus-framework.com/forum", 31 | "source": "https://gitlab.com/aplus-framework/libraries/http", 32 | "docs": "https://docs.aplus-framework.com/guides/libraries/http/" 33 | }, 34 | "funding": [ 35 | { 36 | "type": "Aplus Sponsor", 37 | "url": "https://aplus-framework.com/sponsor" 38 | } 39 | ], 40 | "require": { 41 | "php": ">=8.1", 42 | "ext-fileinfo": "*", 43 | "ext-json": "*", 44 | "aplus/debug": "^3.0", 45 | "aplus/helpers": "^3.0" 46 | }, 47 | "require-dev": { 48 | "ext-xdebug": "*", 49 | "aplus/coding-standard": "^1.14", 50 | "ergebnis/composer-normalize": "^2.25", 51 | "jetbrains/phpstorm-attributes": "^1.0", 52 | "phpmd/phpmd": "^2.13", 53 | "phpstan/phpstan": "^1.9", 54 | "phpunit/phpunit": "^9.5" 55 | }, 56 | "minimum-stability": "dev", 57 | "prefer-stable": true, 58 | "autoload": { 59 | "psr-4": { 60 | "Framework\\HTTP\\": "src/" 61 | } 62 | }, 63 | "autoload-dev": { 64 | "psr-4": { 65 | "Tests\\HTTP\\": "tests/" 66 | } 67 | }, 68 | "config": { 69 | "allow-plugins": { 70 | "ergebnis/composer-normalize": true 71 | }, 72 | "optimize-autoloader": true, 73 | "preferred-install": "dist", 74 | "sort-packages": true 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Method.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 | namespace Framework\HTTP; 11 | 12 | use InvalidArgumentException; 13 | 14 | /** 15 | * Class Method. 16 | * 17 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods 18 | * 19 | * @package http 20 | */ 21 | class Method 22 | { 23 | /** 24 | * The HTTP CONNECT method starts two-way communications with the requested 25 | * resource. It can be used to open a tunnel. 26 | * 27 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT 28 | * 29 | * @var string 30 | */ 31 | public const CONNECT = 'CONNECT'; 32 | /** 33 | * The HTTP DELETE request method deletes the specified resource. 34 | * 35 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE 36 | * 37 | * @var string 38 | */ 39 | public const DELETE = 'DELETE'; 40 | /** 41 | * The HTTP GET method requests a representation of the specified resource. 42 | * Requests using GET should only be used to request data (they shouldn't 43 | * include data). 44 | * 45 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET 46 | * 47 | * @var string 48 | */ 49 | public const GET = 'GET'; 50 | /** 51 | * The HTTP HEAD method requests the headers that would be returned if the 52 | * HEAD request's URL was instead requested with the HTTP GET method. 53 | * 54 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD 55 | * 56 | * @var string 57 | */ 58 | public const HEAD = 'HEAD'; 59 | /** 60 | * The HTTP OPTIONS method requests permitted communication options for a 61 | * given URL or server. A client can specify a URL with this method, or an 62 | * asterisk (*) to refer to the entire server. 63 | * 64 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS 65 | * 66 | * @var string 67 | */ 68 | public const OPTIONS = 'OPTIONS'; 69 | /** 70 | * The HTTP PATCH request method applies partial modifications to a resource. 71 | * 72 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH 73 | * 74 | * @var string 75 | */ 76 | public const PATCH = 'PATCH'; 77 | /** 78 | * The HTTP POST method sends data to the server. The type of the body of 79 | * the request is indicated by the Content-Type header. 80 | * 81 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST 82 | * @see Header::CONTENT_TYPE 83 | * 84 | * @var string 85 | */ 86 | public const POST = 'POST'; 87 | /** 88 | * The HTTP PUT request method creates a new resource or replaces a 89 | * representation of the target resource with the request payload. 90 | * 91 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT 92 | * 93 | * @var string 94 | */ 95 | public const PUT = 'PUT'; 96 | /** 97 | * The HTTP TRACE method performs a message loop-back test along the path to 98 | * the target resource, providing a useful debugging mechanism. 99 | * 100 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE 101 | * 102 | * @var string 103 | */ 104 | public const TRACE = 'TRACE'; 105 | /** 106 | * @var array 107 | */ 108 | protected static array $methods = [ 109 | 'CONNECT', 110 | 'DELETE', 111 | 'GET', 112 | 'HEAD', 113 | 'OPTIONS', 114 | 'PATCH', 115 | 'POST', 116 | 'PUT', 117 | 'TRACE', 118 | ]; 119 | 120 | /** 121 | * @param string $method 122 | * 123 | * @throws InvalidArgumentException for invalid method 124 | * 125 | * @return string 126 | */ 127 | public static function validate(string $method) : string 128 | { 129 | $valid = \strtoupper($method); 130 | if (\in_array($valid, static::$methods, true)) { 131 | return $valid; 132 | } 133 | throw new InvalidArgumentException('Invalid request method: ' . $method); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/AntiCSRF.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 | namespace Framework\HTTP; 11 | 12 | use JetBrains\PhpStorm\Pure; 13 | use LogicException; 14 | 15 | /** 16 | * Class AntiCSRF. 17 | * 18 | * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern 19 | * @see https://stackoverflow.com/q/6287903/6027968 20 | * @see https://portswigger.net/web-security/csrf 21 | * @see https://www.netsparker.com/blog/web-security/protecting-website-using-anti-csrf-token/ 22 | * 23 | * @package http 24 | */ 25 | class AntiCSRF 26 | { 27 | protected string $tokenName = 'csrf_token'; 28 | protected Request $request; 29 | protected bool $verified = false; 30 | protected bool $enabled = true; 31 | 32 | /** 33 | * AntiCSRF constructor. 34 | * 35 | * @param Request $request 36 | */ 37 | public function __construct(Request $request) 38 | { 39 | if (\session_status() !== \PHP_SESSION_ACTIVE) { 40 | throw new LogicException('Session must be active to use AntiCSRF class'); 41 | } 42 | $this->request = $request; 43 | if ($this->getToken() === null) { 44 | $this->setToken(); 45 | } 46 | } 47 | 48 | /** 49 | * Gets the anti-csrf token name. 50 | * 51 | * @return string 52 | */ 53 | #[Pure] 54 | public function getTokenName() : string 55 | { 56 | return $this->tokenName; 57 | } 58 | 59 | /** 60 | * Sets the anti-csrf token name. 61 | * 62 | * @param string $tokenName 63 | * 64 | * @return static 65 | */ 66 | public function setTokenName(string $tokenName) : static 67 | { 68 | $this->tokenName = \htmlspecialchars($tokenName, \ENT_QUOTES | \ENT_HTML5); 69 | return $this; 70 | } 71 | 72 | /** 73 | * Gets the anti-csrf token from the session. 74 | * 75 | * @return string|null 76 | */ 77 | #[Pure] 78 | public function getToken() : ?string 79 | { 80 | return $_SESSION['$']['csrf_token'] ?? null; 81 | } 82 | 83 | /** 84 | * Sets the anti-csrf token into the session. 85 | * 86 | * @param string|null $token A custom anti-csrf token or null to generate one 87 | * 88 | * @return static 89 | */ 90 | public function setToken(string $token = null) : static 91 | { 92 | $_SESSION['$']['csrf_token'] = $token ?? \base64_encode(\random_bytes(8)); 93 | return $this; 94 | } 95 | 96 | /** 97 | * Gets the user token from the request input form. 98 | * 99 | * @return string|null 100 | */ 101 | public function getUserToken() : ?string 102 | { 103 | return $this->request->getParsedBody($this->getTokenName()); 104 | } 105 | 106 | /** 107 | * Verifies the request input token, if the verification is enabled. 108 | * The verification always succeed on HTTP GET, HEAD and OPTIONS methods. 109 | * If verification is successful with other HTTP methods, a new token is 110 | * generated. 111 | * 112 | * @return bool 113 | */ 114 | public function verify() : bool 115 | { 116 | if ($this->isEnabled() === false) { 117 | return true; 118 | } 119 | if ($this->isSafeMethod()) { 120 | return true; 121 | } 122 | if ($this->getUserToken() === null) { 123 | return false; 124 | } 125 | if ( ! $this->validate($this->getUserToken())) { 126 | return false; 127 | } 128 | if ( ! $this->isVerified()) { 129 | $this->setToken(); 130 | $this->setVerified(); 131 | } 132 | return true; 133 | } 134 | 135 | /** 136 | * Safe HTTP Request methods are: GET, HEAD and OPTIONS. 137 | * 138 | * @return bool 139 | */ 140 | #[Pure] 141 | public function isSafeMethod() : bool 142 | { 143 | return \in_array($this->request->getMethod(), [ 144 | Method::GET, 145 | Method::HEAD, 146 | Method::OPTIONS, 147 | ], true); 148 | } 149 | 150 | /** 151 | * Validates if a user token is equals the session token. 152 | * 153 | * This method can be used to validate tokens not received through forms. 154 | * For example: Through a request header, JSON, etc. 155 | * 156 | * @param string $userToken 157 | * 158 | * @return bool 159 | */ 160 | public function validate(string $userToken) : bool 161 | { 162 | return \hash_equals($_SESSION['$']['csrf_token'], $userToken); 163 | } 164 | 165 | #[Pure] 166 | protected function isVerified() : bool 167 | { 168 | return $this->verified; 169 | } 170 | 171 | /** 172 | * @param bool $status 173 | * 174 | * @return static 175 | */ 176 | protected function setVerified(bool $status = true) : static 177 | { 178 | $this->verified = $status; 179 | return $this; 180 | } 181 | 182 | /** 183 | * Gets the HTML form hidden input if the verification is enabled. 184 | * 185 | * @return string 186 | */ 187 | #[Pure] 188 | public function input() : string 189 | { 190 | if ($this->isEnabled() === false) { 191 | return ''; 192 | } 193 | return ''; 196 | } 197 | 198 | /** 199 | * Tells if the verification is enabled. 200 | * 201 | * @see AntiCSRF::verify() 202 | * 203 | * @return bool 204 | */ 205 | #[Pure] 206 | public function isEnabled() : bool 207 | { 208 | return $this->enabled; 209 | } 210 | 211 | /** 212 | * Enables the Anti CSRF verification. 213 | * 214 | * @see AntiCSRF::verify() 215 | * 216 | * @return static 217 | */ 218 | public function enable() : static 219 | { 220 | $this->enabled = true; 221 | return $this; 222 | } 223 | 224 | /** 225 | * Disables the Anti CSRF verification. 226 | * 227 | * @see AntiCSRF::verify() 228 | * 229 | * @return static 230 | */ 231 | public function disable() : static 232 | { 233 | $this->enabled = false; 234 | return $this; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/RequestHeader.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 | namespace Framework\HTTP; 11 | 12 | /** 13 | * Class RequestHeader. 14 | * 15 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Request_header 16 | * 17 | * @package http 18 | */ 19 | class RequestHeader extends Header 20 | { 21 | /** 22 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept 23 | * 24 | * @var string 25 | */ 26 | public const ACCEPT = 'Accept'; 27 | /** 28 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset 29 | * 30 | * @var string 31 | */ 32 | public const ACCEPT_CHARSET = 'Accept-Charset'; 33 | /** 34 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding 35 | * 36 | * @var string 37 | */ 38 | public const ACCEPT_ENCODING = 'Accept-Encoding'; 39 | /** 40 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language 41 | * 42 | * @var string 43 | */ 44 | public const ACCEPT_LANGUAGE = 'Accept-Language'; 45 | /** 46 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers 47 | * 48 | * @var string 49 | */ 50 | public const ACCESS_CONTROL_REQUEST_HEADERS = 'Access-Control-Request-Headers'; 51 | /** 52 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method 53 | * 54 | * @var string 55 | */ 56 | public const ACCESS_CONTROL_REQUEST_METHOD = 'Access-Control-Request-Method'; 57 | /** 58 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization 59 | * 60 | * @var string 61 | */ 62 | public const AUTHORIZATION = 'Authorization'; 63 | /** 64 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie 65 | * 66 | * @var string 67 | */ 68 | public const COOKIE = 'Cookie'; 69 | /** 70 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT 71 | * 72 | * @var string 73 | */ 74 | public const DNT = 'DNT'; 75 | /** 76 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect 77 | * 78 | * @var string 79 | */ 80 | public const EXPECT = 'Expect'; 81 | /** 82 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded 83 | * 84 | * @var string 85 | */ 86 | public const FORWARDED = 'Forwarded'; 87 | /** 88 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/From 89 | * 90 | * @var string 91 | */ 92 | public const FROM = 'From'; 93 | /** 94 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host 95 | * 96 | * @var string 97 | */ 98 | public const HOST = 'Host'; 99 | /** 100 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match 101 | * 102 | * @var string 103 | */ 104 | public const IF_MATCH = 'If-Match'; 105 | /** 106 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since 107 | * 108 | * @var string 109 | */ 110 | public const IF_MODIFIED_SINCE = 'If-Modified-Since'; 111 | /** 112 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match 113 | * 114 | * @var string 115 | */ 116 | public const IF_NONE_MATCH = 'If-None-Match'; 117 | /** 118 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range 119 | * 120 | * @var string 121 | */ 122 | public const IF_RANGE = 'If-Range'; 123 | /** 124 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since 125 | * 126 | * @var string 127 | */ 128 | public const IF_UNMODIFIED_SINCE = 'If-Unmodified-Since'; 129 | /** 130 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin 131 | * 132 | * @var string 133 | */ 134 | public const ORIGIN = 'Origin'; 135 | /** 136 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization 137 | * 138 | * @var string 139 | */ 140 | public const PROXY_AUTHORIZATION = 'Proxy-Authorization'; 141 | /** 142 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range 143 | * 144 | * @var string 145 | */ 146 | public const RANGE = 'Range'; 147 | /** 148 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer 149 | * 150 | * @var string 151 | */ 152 | public const REFERER = 'Referer'; 153 | /** 154 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest 155 | * 156 | * @var string 157 | */ 158 | public const SEC_FETCH_DEST = 'Sec-Fetch-Dest'; 159 | /** 160 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode 161 | * 162 | * @var string 163 | */ 164 | public const SEC_FETCH_MODE = 'Sec-Fetch-Mode'; 165 | /** 166 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site 167 | * 168 | * @var string 169 | */ 170 | public const SEC_FETCH_SITE = 'Sec-Fetch-Site'; 171 | /** 172 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-User 173 | * 174 | * @var string 175 | */ 176 | public const SEC_FETCH_USER = 'Sec-Fetch-User'; 177 | /** 178 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE 179 | * 180 | * @var string 181 | */ 182 | public const TE = 'TE'; 183 | /** 184 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade-Insecure-Requests 185 | * 186 | * @var string 187 | */ 188 | public const UPGRADE_INSECURE_REQUESTS = 'Upgrade-Insecure-Requests'; 189 | /** 190 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent 191 | * 192 | * @var string 193 | */ 194 | public const USER_AGENT = 'User-Agent'; 195 | /** 196 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For 197 | * 198 | * @var string 199 | */ 200 | public const X_FORWARDED_FOR = 'X-Forwarded-For'; 201 | /** 202 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host 203 | * 204 | * @var string 205 | */ 206 | public const X_FORWARDED_HOST = 'X-Forwarded-Host'; 207 | /** 208 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto 209 | * 210 | * @var string 211 | */ 212 | public const X_FORWARDED_PROTO = 'X-Forwarded-Proto'; 213 | /** 214 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Real-IP 215 | * 216 | * @var string 217 | */ 218 | public const X_REAL_IP = 'X-Real-IP'; 219 | /** 220 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Requested-With 221 | * 222 | * @var string 223 | */ 224 | public const X_REQUESTED_WITH = 'X-Requested-With'; 225 | 226 | /** 227 | * @param array $input 228 | * 229 | * @return array 230 | */ 231 | public static function parseInput(array $input) : array 232 | { 233 | $headers = []; 234 | foreach ($input as $name => $value) { 235 | if (\str_starts_with($name, 'HTTP_')) { 236 | $name = \strtr(\substr($name, 5), ['_' => '-']); 237 | $name = static::getName($name); 238 | $headers[$name] = (string) $value; 239 | } 240 | } 241 | return $headers; 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/ResponseHeader.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 | namespace Framework\HTTP; 11 | 12 | /** 13 | * Class ResponseHeader. 14 | * 15 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Response_header 16 | * 17 | * @package http 18 | */ 19 | class ResponseHeader extends Header 20 | { 21 | /** 22 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges 23 | * 24 | * @var string 25 | */ 26 | public const ACCEPT_RANGES = 'Accept-Ranges'; 27 | /** 28 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 29 | * 30 | * @var string 31 | */ 32 | public const ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials'; 33 | /** 34 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers 35 | * 36 | * @var string 37 | */ 38 | public const ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers'; 39 | /** 40 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods 41 | * 42 | * @var string 43 | */ 44 | public const ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods'; 45 | /** 46 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 47 | * 48 | * @var string 49 | */ 50 | public const ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin'; 51 | /** 52 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 53 | * 54 | * @var string 55 | */ 56 | public const ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers'; 57 | /** 58 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 59 | * 60 | * @var string 61 | */ 62 | public const ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age'; 63 | /** 64 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age 65 | * 66 | * @var string 67 | */ 68 | public const AGE = 'Age'; 69 | /** 70 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow 71 | * 72 | * @var string 73 | */ 74 | public const ALLOW = 'Allow'; 75 | /** 76 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data 77 | * 78 | * @var string 79 | */ 80 | public const CLEAR_SITE_DATA = 'Clear-Site-Data'; 81 | /** 82 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 83 | * 84 | * @var string 85 | */ 86 | public const CONTENT_SECURITY_POLICY = 'Content-Security-Policy'; 87 | /** 88 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only 89 | * 90 | * @var string 91 | */ 92 | public const CONTENT_SECURITY_POLICY_REPORT_ONLY = 'Content-Security-Policy-Report-Only'; 93 | /** 94 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag 95 | * 96 | * @var string 97 | */ 98 | public const ETAG = 'ETag'; 99 | /** 100 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT 101 | * 102 | * @var string 103 | */ 104 | public const EXPECT_CT = 'Expect-CT'; 105 | /** 106 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires 107 | * 108 | * @var string 109 | */ 110 | public const EXPIRES = 'Expires'; 111 | /** 112 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy 113 | * 114 | * @var string 115 | */ 116 | public const FEATURE_POLICY = 'Feature-Policy'; 117 | /** 118 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified 119 | * 120 | * @var string 121 | */ 122 | public const LAST_MODIFIED = 'Last-Modified'; 123 | /** 124 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location 125 | * 126 | * @var string 127 | */ 128 | public const LOCATION = 'Location'; 129 | /** 130 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate 131 | * 132 | * @var string 133 | */ 134 | public const PROXY_AUTHENTICATE = 'Proxy-Authenticate'; 135 | /** 136 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Public-Key-Pins 137 | * 138 | * @var string 139 | */ 140 | public const PUBLIC_KEY_PINS = 'Public-Key-Pins'; 141 | /** 142 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Public-Key-Pins-Report-Only 143 | * 144 | * @var string 145 | */ 146 | public const PUBLIC_KEY_PINS_REPORT_ONLY = 'Public-Key-Pins-Report-Only'; 147 | /** 148 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy 149 | * 150 | * @var string 151 | */ 152 | public const REFERRER_POLICY = 'Referrer-Policy'; 153 | /** 154 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After 155 | * 156 | * @var string 157 | */ 158 | public const RETRY_AFTER = 'Retry-After'; 159 | /** 160 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server 161 | * 162 | * @var string 163 | */ 164 | public const SERVER = 'Server'; 165 | /** 166 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie 167 | * 168 | * @var string 169 | */ 170 | public const SET_COOKIE = 'Set-Cookie'; 171 | /** 172 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap 173 | * 174 | * @var string 175 | */ 176 | public const SOURCEMAP = 'SourceMap'; 177 | /** 178 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 179 | * 180 | * @var string 181 | */ 182 | public const STRICT_TRANSPORT_SECURITY = 'Strict-Transport-Security'; 183 | /** 184 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin 185 | * 186 | * @var string 187 | */ 188 | public const TIMING_ALLOW_ORIGIN = 'Timing-Allow-Origin'; 189 | /** 190 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Tk 191 | * 192 | * @var string 193 | */ 194 | public const TK = 'Tk'; 195 | /** 196 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary 197 | * 198 | * @var string 199 | */ 200 | public const VARY = 'Vary'; 201 | /** 202 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate 203 | * 204 | * @var string 205 | */ 206 | public const WWW_AUTHENTICATE = 'WWW-Authenticate'; 207 | /** 208 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 209 | * 210 | * @var string 211 | */ 212 | public const X_CONTENT_TYPE_OPTIONS = 'X-Content-Type-Options'; 213 | /** 214 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 215 | * 216 | * @var string 217 | */ 218 | public const X_DNS_PREFETCH_CONTROL = 'X-DNS-Prefetch-Control'; 219 | /** 220 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 221 | * 222 | * @var string 223 | */ 224 | public const X_FRAME_OPTIONS = 'X-Frame-Options'; 225 | /** 226 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection 227 | * 228 | * @var string 229 | */ 230 | public const X_XSS_PROTECTION = 'X-XSS-Protection'; 231 | // ------------------------------------------------------------------------- 232 | // Custom 233 | // ------------------------------------------------------------------------- 234 | /** 235 | * @var string 236 | */ 237 | public const X_POWERED_BY = 'X-Powered-By'; 238 | } 239 | -------------------------------------------------------------------------------- /src/Cookie.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 | namespace Framework\HTTP; 11 | 12 | use DateTime; 13 | use DateTimeInterface; 14 | use DateTimeZone; 15 | use Exception; 16 | use InvalidArgumentException; 17 | use JetBrains\PhpStorm\Deprecated; 18 | use JetBrains\PhpStorm\Pure; 19 | 20 | /** 21 | * Class Cookie. 22 | * 23 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie 24 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies 25 | * @see https://datatracker.ietf.org/doc/html/rfc6265 26 | * @see https://www.php.net/manual/en/function.setcookie.php 27 | * 28 | * @package http 29 | */ 30 | class Cookie implements \Stringable 31 | { 32 | protected ?string $domain = null; 33 | protected ?DateTime $expires = null; 34 | protected bool $httpOnly = false; 35 | protected string $name; 36 | protected ?string $path = null; 37 | protected ?string $sameSite = null; 38 | protected bool $secure = false; 39 | protected string $value; 40 | 41 | /** 42 | * Cookie constructor. 43 | * 44 | * @param string $name 45 | * @param string $value 46 | */ 47 | public function __construct(string $name, string $value) 48 | { 49 | $this->setName($name); 50 | $this->setValue($value); 51 | } 52 | 53 | public function __toString() : string 54 | { 55 | return $this->toString(); 56 | } 57 | 58 | /** 59 | * @return string 60 | * 61 | * @deprecated Use {@see Cookie::toString()} 62 | * 63 | * @codeCoverageIgnore 64 | */ 65 | #[Deprecated( 66 | reason: 'since HTTP Library version 5.3, use toString() instead', 67 | replacement: '%class%->toString()' 68 | )] 69 | public function getAsString() : string 70 | { 71 | \trigger_error( 72 | 'Method ' . __METHOD__ . ' is deprecated', 73 | \E_USER_DEPRECATED 74 | ); 75 | return $this->toString(); 76 | } 77 | 78 | /** 79 | * @since 5.3 80 | * 81 | * @return string 82 | */ 83 | public function toString() : string 84 | { 85 | $string = $this->getName() . '=' . $this->getValue(); 86 | $part = $this->getExpires(); 87 | if ($part !== null) { 88 | $string .= '; expires=' . $this->expires->format(DateTimeInterface::RFC7231); 89 | $string .= '; Max-Age=' . $this->expires->diff(new DateTime('-1 second'))->s; 90 | } 91 | $part = $this->getPath(); 92 | if ($part !== null) { 93 | $string .= '; path=' . $part; 94 | } 95 | $part = $this->getDomain(); 96 | if ($part !== null) { 97 | $string .= '; domain=' . $part; 98 | } 99 | $part = $this->isSecure(); 100 | if ($part) { 101 | $string .= '; secure'; 102 | } 103 | $part = $this->isHttpOnly(); 104 | if ($part) { 105 | $string .= '; HttpOnly'; 106 | } 107 | $part = $this->getSameSite(); 108 | if ($part !== null) { 109 | $string .= '; SameSite=' . $part; 110 | } 111 | return $string; 112 | } 113 | 114 | /** 115 | * @return string|null 116 | */ 117 | #[Pure] 118 | public function getDomain() : ?string 119 | { 120 | return $this->domain; 121 | } 122 | 123 | /** 124 | * @param string|null $domain 125 | * 126 | * @return static 127 | */ 128 | public function setDomain(?string $domain) : static 129 | { 130 | $this->domain = $domain; 131 | return $this; 132 | } 133 | 134 | /** 135 | * @return DateTime|null 136 | */ 137 | #[Pure] 138 | public function getExpires() : ?DateTime 139 | { 140 | return $this->expires; 141 | } 142 | 143 | /** 144 | * @param DateTime|int|string|null $expires 145 | * 146 | * @throws Exception if can not create from format 147 | * 148 | * @return static 149 | */ 150 | public function setExpires(DateTime | int | string | null $expires) : static 151 | { 152 | if ($expires instanceof DateTime) { 153 | $expires = clone $expires; 154 | $expires->setTimezone(new DateTimeZone('UTC')); 155 | } elseif (\is_numeric($expires)) { 156 | $expires = DateTime::createFromFormat('U', (string) $expires, new DateTimeZone('UTC')); 157 | } elseif ($expires !== null) { 158 | $expires = new DateTime($expires, new DateTimeZone('UTC')); 159 | } 160 | $this->expires = $expires; // @phpstan-ignore-line 161 | return $this; 162 | } 163 | 164 | /** 165 | * @return bool 166 | */ 167 | public function isExpired() : bool 168 | { 169 | return $this->getExpires() && \time() > $this->getExpires()->getTimestamp(); 170 | } 171 | 172 | /** 173 | * @return string 174 | */ 175 | #[Pure] 176 | public function getName() : string 177 | { 178 | return $this->name; 179 | } 180 | 181 | /** 182 | * @param string $name 183 | * 184 | * @return static 185 | */ 186 | protected function setName(string $name) : static 187 | { 188 | $this->name = $name; 189 | return $this; 190 | } 191 | 192 | /** 193 | * @return string|null 194 | */ 195 | #[Pure] 196 | public function getPath() : ?string 197 | { 198 | return $this->path; 199 | } 200 | 201 | /** 202 | * @param string|null $path 203 | * 204 | * @return static 205 | */ 206 | public function setPath(?string $path) : static 207 | { 208 | $this->path = $path; 209 | return $this; 210 | } 211 | 212 | /** 213 | * @return string|null 214 | */ 215 | #[Pure] 216 | public function getSameSite() : ?string 217 | { 218 | return $this->sameSite; 219 | } 220 | 221 | /** 222 | * @param string|null $sameSite Strict, Lax, Unset or None 223 | * 224 | * @throws InvalidArgumentException for invalid $sameSite value 225 | * 226 | * @return static 227 | */ 228 | public function setSameSite(?string $sameSite) : static 229 | { 230 | if ($sameSite !== null) { 231 | $sameSite = \ucfirst(\strtolower($sameSite)); 232 | if ( ! \in_array($sameSite, ['Strict', 'Lax', 'Unset', 'None'])) { 233 | throw new InvalidArgumentException('SameSite must be Strict, Lax, Unset or None'); 234 | } 235 | } 236 | $this->sameSite = $sameSite; 237 | return $this; 238 | } 239 | 240 | /** 241 | * @return string 242 | */ 243 | #[Pure] 244 | public function getValue() : string 245 | { 246 | return $this->value; 247 | } 248 | 249 | /** 250 | * @param string $value 251 | * 252 | * @return static 253 | */ 254 | public function setValue(string $value) : static 255 | { 256 | $this->value = $value; 257 | return $this; 258 | } 259 | 260 | /** 261 | * @param bool $httpOnly 262 | * 263 | * @return static 264 | */ 265 | public function setHttpOnly(bool $httpOnly = true) : static 266 | { 267 | $this->httpOnly = $httpOnly; 268 | return $this; 269 | } 270 | 271 | /** 272 | * @return bool 273 | */ 274 | #[Pure] 275 | public function isHttpOnly() : bool 276 | { 277 | return $this->httpOnly; 278 | } 279 | 280 | /** 281 | * @param bool $secure 282 | * 283 | * @return static 284 | */ 285 | public function setSecure(bool $secure = true) : static 286 | { 287 | $this->secure = $secure; 288 | return $this; 289 | } 290 | 291 | /** 292 | * @return bool 293 | */ 294 | #[Pure] 295 | public function isSecure() : bool 296 | { 297 | return $this->secure; 298 | } 299 | 300 | /** 301 | * @return bool 302 | */ 303 | public function send() : bool 304 | { 305 | $options = []; 306 | $value = $this->getExpires(); 307 | if ($value) { 308 | $options['expires'] = $value->getTimestamp(); 309 | } 310 | $value = $this->getPath(); 311 | if ($value !== null) { 312 | $options['path'] = $value; 313 | } 314 | $value = $this->getDomain(); 315 | if ($value !== null) { 316 | $options['domain'] = $value; 317 | } 318 | $options['secure'] = $this->isSecure(); 319 | $options['httponly'] = $this->isHttpOnly(); 320 | $value = $this->getSameSite(); 321 | if ($value !== null) { 322 | $options['samesite'] = $value; 323 | } 324 | // @phpstan-ignore-next-line 325 | return \setcookie($this->getName(), $this->getValue(), $options); 326 | } 327 | 328 | /** 329 | * Parses a Set-Cookie Header line and creates a new Cookie object. 330 | * 331 | * @param string $line 332 | * 333 | * @throws Exception if setExpires fail 334 | * 335 | * @return Cookie|null 336 | */ 337 | public static function parse(string $line) : ?Cookie 338 | { 339 | $parts = \array_map('\trim', \explode(';', $line, 20)); 340 | $cookie = null; 341 | foreach ($parts as $key => $part) { 342 | [$arg, $val] = static::makeArgumentValue($part); 343 | if ($key === 0) { 344 | if (isset($arg, $val)) { 345 | $cookie = new Cookie($arg, $val); 346 | continue; 347 | } 348 | break; 349 | } 350 | if ($arg === null) { 351 | continue; 352 | } 353 | switch (\strtolower($arg)) { 354 | case 'expires': 355 | $cookie->setExpires($val); 356 | break; 357 | case 'domain': 358 | $cookie->setDomain($val); 359 | break; 360 | case 'path': 361 | $cookie->setPath($val); 362 | break; 363 | case 'httponly': 364 | $cookie->setHttpOnly(); 365 | break; 366 | case 'secure': 367 | $cookie->setSecure(); 368 | break; 369 | case 'samesite': 370 | $cookie->setSameSite($val); 371 | break; 372 | } 373 | } 374 | return $cookie; 375 | } 376 | 377 | /** 378 | * Create Cookie objects from a Cookie Header line. 379 | * 380 | * @param string $line 381 | * 382 | * @return array 383 | */ 384 | public static function create(string $line) : array 385 | { 386 | $items = \array_map('\trim', \explode(';', $line, 3000)); 387 | $cookies = []; 388 | foreach ($items as $item) { 389 | [$name, $value] = static::makeArgumentValue($item); 390 | if (isset($name, $value)) { 391 | $cookies[$name] = new Cookie($name, $value); 392 | } 393 | } 394 | return $cookies; 395 | } 396 | 397 | /** 398 | * @param string $part 399 | * 400 | * @return array 401 | */ 402 | protected static function makeArgumentValue(string $part) : array 403 | { 404 | $part = \array_pad(\explode('=', $part, 2), 2, null); 405 | if ($part[0] !== null) { 406 | $part[0] = static::trimmedOrNull($part[0]); 407 | } 408 | if ($part[1] !== null) { 409 | $part[1] = static::trimmedOrNull($part[1]); 410 | } 411 | return $part; 412 | } 413 | 414 | protected static function trimmedOrNull(string $value) : ?string 415 | { 416 | $value = \trim($value); 417 | if ($value === '') { 418 | $value = null; 419 | } 420 | return $value; 421 | } 422 | } 423 | -------------------------------------------------------------------------------- /src/Header.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 | namespace Framework\HTTP; 11 | 12 | /** 13 | * Class Header. 14 | * 15 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 16 | * 17 | * @package http 18 | */ 19 | class Header 20 | { 21 | // ------------------------------------------------------------------------- 22 | // General headers (Request and Response) 23 | // ------------------------------------------------------------------------- 24 | /** 25 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control 26 | * 27 | * @var string 28 | */ 29 | public const CACHE_CONTROL = 'Cache-Control'; 30 | /** 31 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection 32 | * 33 | * @var string 34 | */ 35 | public const CONNECTION = 'Connection'; 36 | /** 37 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition 38 | * 39 | * @var string 40 | */ 41 | public const CONTENT_DISPOSITION = 'Content-Disposition'; 42 | /** 43 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date 44 | * 45 | * @var string 46 | */ 47 | public const DATE = 'Date'; 48 | /** 49 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive 50 | * 51 | * @var string 52 | */ 53 | public const KEEP_ALIVE = 'Keep-Alive'; 54 | /** 55 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma 56 | * 57 | * @var string 58 | */ 59 | public const PRAGMA = 'Pragma'; 60 | /** 61 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via 62 | * 63 | * @var string 64 | */ 65 | public const VIA = 'Via'; 66 | /** 67 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning 68 | * 69 | * @var string 70 | */ 71 | public const WARNING = 'Warning'; 72 | // ------------------------------------------------------------------------- 73 | // Representation headers (Request and Response) 74 | // ------------------------------------------------------------------------- 75 | /** 76 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding 77 | * 78 | * @var string 79 | */ 80 | public const CONTENT_ENCODING = 'Content-Encoding'; 81 | /** 82 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language 83 | * 84 | * @var string 85 | */ 86 | public const CONTENT_LANGUAGE = 'Content-Language'; 87 | /** 88 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Location 89 | * 90 | * @var string 91 | */ 92 | public const CONTENT_LOCATION = 'Content-Location'; 93 | /** 94 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type 95 | * 96 | * @var string 97 | */ 98 | public const CONTENT_TYPE = 'Content-Type'; 99 | // ------------------------------------------------------------------------- 100 | // Payload headers (Request and Response) 101 | // ------------------------------------------------------------------------- 102 | /** 103 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length 104 | * 105 | * @var string 106 | */ 107 | public const CONTENT_LENGTH = 'Content-Length'; 108 | /** 109 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range 110 | * 111 | * @var string 112 | */ 113 | public const CONTENT_RANGE = 'Content-Range'; 114 | /** 115 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link 116 | * 117 | * @var string 118 | */ 119 | public const LINK = 'Link'; 120 | /** 121 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer 122 | * 123 | * @var string 124 | */ 125 | public const TRAILER = 'Trailer'; 126 | /** 127 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding 128 | * 129 | * @var string 130 | */ 131 | public const TRANSFER_ENCODING = 'Transfer-Encoding'; 132 | /** 133 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade 134 | * 135 | * @var string 136 | */ 137 | public const UPGRADE = 'Upgrade'; 138 | // ------------------------------------------------------------------------- 139 | // Custom 140 | // ------------------------------------------------------------------------- 141 | /** 142 | * @see https://riptutorial.com/http-headers/topic/10581/x-request-id 143 | * 144 | * @var string 145 | */ 146 | public const X_REQUEST_ID = 'X-Request-ID'; 147 | /** 148 | * Header names. 149 | * 150 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 151 | * 152 | * @var array 153 | */ 154 | protected static array $headers = [ 155 | // --------------------------------------------------------------------- 156 | // General headers (Request and Response) 157 | // --------------------------------------------------------------------- 158 | 'cache-control' => 'Cache-Control', 159 | 'connection' => 'Connection', 160 | 'content-disposition' => 'Content-Disposition', 161 | 'date' => 'Date', 162 | 'keep-alive' => 'Keep-Alive', 163 | 'link' => 'Link', 164 | 'pragma' => 'Pragma', 165 | 'via' => 'Via', 166 | 'warning' => 'Warning', 167 | // --------------------------------------------------------------------- 168 | // Representation headers (Request and Response) 169 | // --------------------------------------------------------------------- 170 | 'content-encoding' => 'Content-Encoding', 171 | 'content-language' => 'Content-Language', 172 | 'content-location' => 'Content-Location', 173 | 'content-type' => 'Content-Type', 174 | // --------------------------------------------------------------------- 175 | // Payload headers (Request and Response) 176 | // --------------------------------------------------------------------- 177 | 'content-length' => 'Content-Length', 178 | 'content-range' => 'Content-Range', 179 | 'trailer' => 'Trailer', 180 | 'transfer-encoding' => 'Transfer-Encoding', 181 | // --------------------------------------------------------------------- 182 | // Request headers 183 | // --------------------------------------------------------------------- 184 | 'accept' => 'Accept', 185 | 'accept-charset' => 'Accept-Charset', 186 | 'accept-encoding' => 'Accept-Encoding', 187 | 'accept-language' => 'Accept-Language', 188 | 'access-control-request-headers' => 'Access-Control-Request-Headers', 189 | 'access-control-request-method' => 'Access-Control-Request-Method', 190 | 'authorization' => 'Authorization', 191 | 'cookie' => 'Cookie', 192 | 'dnt' => 'DNT', 193 | 'expect' => 'Expect', 194 | 'forwarded' => 'Forwarded', 195 | 'from' => 'From', 196 | 'host' => 'Host', 197 | 'if-match' => 'If-Match', 198 | 'if-modified-since' => 'If-Modified-Since', 199 | 'if-none-match' => 'If-None-Match', 200 | 'if-range' => 'If-Range', 201 | 'if-unmodified-since' => 'If-Unmodified-Since', 202 | 'origin' => 'Origin', 203 | 'proxy-authorization' => 'Proxy-Authorization', 204 | 'range' => 'Range', 205 | 'referer' => 'Referer', 206 | 'sec-fetch-dest' => 'Sec-Fetch-Dest', 207 | 'sec-fetch-mode' => 'Sec-Fetch-Mode', 208 | 'sec-fetch-site' => 'Sec-Fetch-Site', 209 | 'sec-fetch-user' => 'Sec-Fetch-User', 210 | 'te' => 'TE', 211 | 'upgrade-insecure-requests' => 'Upgrade-Insecure-Requests', 212 | 'user-agent' => 'User-Agent', 213 | 'x-forwarded-for' => 'X-Forwarded-For', 214 | 'x-forwarded-host' => 'X-Forwarded-Host', 215 | 'x-forwarded-proto' => 'X-Forwarded-Proto', 216 | 'x-real-ip' => 'X-Real-IP', 217 | 'x-requested-with' => 'X-Requested-With', 218 | // --------------------------------------------------------------------- 219 | // Response headers 220 | // --------------------------------------------------------------------- 221 | 'accept-ranges' => 'Accept-Ranges', 222 | 'access-control-allow-credentials' => 'Access-Control-Allow-Credentials', 223 | 'access-control-allow-headers' => 'Access-Control-Allow-Headers', 224 | 'access-control-allow-methods' => 'Access-Control-Allow-Methods', 225 | 'access-control-allow-origin' => 'Access-Control-Allow-Origin', 226 | 'access-control-expose-headers' => 'Access-Control-Expose-Headers', 227 | 'access-control-max-age' => 'Access-Control-Max-Age', 228 | 'age' => 'Age', 229 | 'allow' => 'Allow', 230 | 'clear-site-data' => 'Clear-Site-Data', 231 | 'content-security-policy' => 'Content-Security-Policy', 232 | 'content-security-policy-report-only' => 'Content-Security-Policy-Report-Only', 233 | 'etag' => 'ETag', 234 | 'expect-ct' => 'Expect-CT', 235 | 'expires' => 'Expires', 236 | 'feature-policy' => 'Feature-Policy', 237 | 'last-modified' => 'Last-Modified', 238 | 'location' => 'Location', 239 | 'proxy-authenticate' => 'Proxy-Authenticate', 240 | 'public-key-pins' => 'Public-Key-Pins', 241 | 'public-key-pins-report-only' => 'Public-Key-Pins-Report-Only', 242 | 'referrer-policy' => 'Referrer-Policy', 243 | 'retry-after' => 'Retry-After', 244 | 'server' => 'Server', 245 | 'set-cookie' => 'Set-Cookie', 246 | 'sourcemap' => 'SourceMap', 247 | 'strict-transport-security' => 'Strict-Transport-Security', 248 | 'timing-allow-origin' => 'Timing-Allow-Origin', 249 | 'tk' => 'Tk', 250 | 'vary' => 'Vary', 251 | 'www-authenticate' => 'WWW-Authenticate', 252 | 'x-content-type-options' => 'X-Content-Type-Options', 253 | 'x-dns-prefetch-control' => 'X-DNS-Prefetch-Control', 254 | 'x-frame-options' => 'X-Frame-Options', 255 | 'x-xss-protection' => 'X-XSS-Protection', 256 | // --------------------------------------------------------------------- 257 | // Custom (Response) 258 | // --------------------------------------------------------------------- 259 | 'x-request-id' => 'X-Request-ID', 260 | 'x-powered-by' => 'X-Powered-By', 261 | // --------------------------------------------------------------------- 262 | // WebSocket 263 | // --------------------------------------------------------------------- 264 | 'sec-websocket-extensions' => 'Sec-WebSocket-Extensions', 265 | 'sec-websocket-key' => 'Sec-WebSocket-Key', 266 | 'sec-websocket-protocol' => 'Sec-WebSocket-Protocol', 267 | 'sec-websocket-version' => 'Sec-WebSocket-Version', 268 | ]; 269 | 270 | public static function getName(string $name) : string 271 | { 272 | return static::$headers[\strtolower($name)] ?? $name; 273 | } 274 | 275 | public static function setName(string $name) : void 276 | { 277 | static::$headers[\strtolower($name)] = $name; 278 | } 279 | 280 | /** 281 | * @return array 282 | */ 283 | public static function getMultilines() : array 284 | { 285 | return [ 286 | 'date', 287 | 'expires', 288 | 'if-modified-since', 289 | 'if-range', 290 | 'if-unmodified-since', 291 | 'last-modified', 292 | 'proxy-authenticate', 293 | 'retry-after', 294 | 'set-cookie', 295 | 'www-authenticate', 296 | ]; 297 | } 298 | 299 | public static function isMultiline(string $name) : bool 300 | { 301 | return \in_array(\strtolower($name), static::getMultilines(), true); 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /src/ResponseDownload.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 | namespace Framework\HTTP; 11 | 12 | use InvalidArgumentException; 13 | use JetBrains\PhpStorm\Pure; 14 | use RuntimeException; 15 | 16 | /** 17 | * Trait ResponseDownload. 18 | * 19 | * @see https://datatracker.ietf.org/doc/html/rfc7233 20 | * 21 | * @property Request $request 22 | * 23 | * @package http 24 | */ 25 | trait ResponseDownload 26 | { 27 | private string $filepath; 28 | private int $filesize; 29 | private bool $acceptRanges = true; 30 | /** 31 | * @var array>|false 32 | */ 33 | private array | false $byteRanges = []; 34 | private string $sendType = 'normal'; 35 | private string $boundary; 36 | /** 37 | * @var resource 38 | */ 39 | private $handle; 40 | private int $delay = 0; 41 | private int $readLength = 1024; 42 | 43 | /** 44 | * Sets a file to download/stream. 45 | * 46 | * @param string $filepath 47 | * @param bool $inline Set Content-Disposition header as "inline". Browsers 48 | * load the file in the window. Set true to allow video or audio streams 49 | * @param bool $acceptRanges Set Accept-Ranges header to "bytes". Allow 50 | * partial downloads, media players to move the time position forward and 51 | * back and download managers to continue/download multi-parts 52 | * @param int $delay Delay between flushs in microseconds 53 | * @param int $readLength Bytes read by flush 54 | * @param string|null $filename A custom filename 55 | * 56 | * @throws InvalidArgumentException If invalid file path 57 | * @throws RuntimeException If can not get the file size or modification time 58 | * 59 | * @return static 60 | */ 61 | public function setDownload( 62 | string $filepath, 63 | bool $inline = false, 64 | bool $acceptRanges = true, 65 | int $delay = 0, 66 | int $readLength = 1024, 67 | ?string $filename = null 68 | ) : static { 69 | $realpath = \realpath($filepath); 70 | if ($realpath === false || ! \is_file($realpath)) { 71 | throw new InvalidArgumentException('Invalid file path: ' . $filepath); 72 | } 73 | $this->filepath = $realpath; 74 | $this->delay = $delay; 75 | $this->readLength = $readLength; 76 | $filesize = @\filesize($this->filepath); 77 | if ($filesize === false) { 78 | throw new RuntimeException( 79 | "Could not get the file size of '{$this->filepath}'" 80 | ); 81 | } 82 | $this->filesize = $filesize; 83 | $filemtime = \filemtime($this->filepath); 84 | if ($filemtime === false) { 85 | throw new RuntimeException( 86 | "Could not get the file modification time of '{$this->filepath}'" 87 | ); 88 | } 89 | $this->setHeader(ResponseHeader::LAST_MODIFIED, \gmdate(\DATE_RFC7231, $filemtime)); 90 | $filename ??= \basename($filepath); 91 | $filename = \htmlspecialchars($filename, \ENT_QUOTES | \ENT_HTML5); 92 | $filename = \strtr($filename, ['/' => '_', '\\' => '_']); 93 | $this->setHeader( 94 | Header::CONTENT_DISPOSITION, 95 | $inline ? 'inline' : \sprintf('attachment; filename="%s"', $filename) 96 | ); 97 | $this->setAcceptRanges($acceptRanges); 98 | if ($acceptRanges) { 99 | $rangeLine = $this->request->getHeader(RequestHeader::RANGE); 100 | if ($rangeLine) { 101 | $this->prepareRange($rangeLine); 102 | return $this; 103 | } 104 | } 105 | $this->setHeader(Header::CONTENT_LENGTH, (string) $this->filesize); 106 | $this->setHeader( 107 | Header::CONTENT_TYPE, 108 | \mime_content_type($this->filepath) ?: 'application/octet-stream' 109 | ); 110 | $this->sendType = 'normal'; 111 | return $this; 112 | } 113 | 114 | private function prepareRange(string $rangeLine) : void 115 | { 116 | $this->byteRanges = $this->parseByteRange($rangeLine); 117 | if ($this->byteRanges === false) { 118 | // https://datatracker.ietf.org/doc/html/rfc7233#section-4.2 119 | $this->setStatus(Status::RANGE_NOT_SATISFIABLE); 120 | $this->setHeader(Header::CONTENT_RANGE, '*/' . $this->filesize); 121 | return; 122 | } 123 | $this->setStatus(Status::PARTIAL_CONTENT); 124 | if (\count($this->byteRanges) === 1) { 125 | $this->setSinglePart(...$this->byteRanges[0]); 126 | return; 127 | } 128 | $this->setMultiPart(...$this->byteRanges); 129 | } 130 | 131 | private function setAcceptRanges(bool $acceptRanges) : void 132 | { 133 | $this->acceptRanges = $acceptRanges; 134 | $this->setHeader( 135 | ResponseHeader::ACCEPT_RANGES, 136 | $acceptRanges ? 'bytes' : 'none' 137 | ); 138 | } 139 | 140 | /** 141 | * Parse the HTTP Range Header line. 142 | * 143 | * Returns arrays of two indexes, representing first-byte-pos and last-byte-pos. 144 | * If return false, the Byte Ranges are invalid, so the Response must return 145 | * a 416 (Range Not Satisfiable) status. 146 | * 147 | * @see https://datatracker.ietf.org/doc/html/rfc7233#section-2.1 148 | * @see https://datatracker.ietf.org/doc/html/rfc7233#section-4.4 149 | * 150 | * @param string $line 151 | * 152 | * @return array>|false 153 | * 154 | * @phpstan-ignore-next-line 155 | */ 156 | #[Pure] 157 | private function parseByteRange(string $line) : array | false 158 | { 159 | if ( ! \str_starts_with($line, 'bytes=')) { 160 | return false; 161 | } 162 | $line = \substr($line, 6); 163 | $ranges = \explode(',', $line, 100); 164 | foreach ($ranges as &$range) { 165 | $range = \array_pad(\explode('-', $range, 2), 2, null); 166 | if ($range[0] === null || $range[1] === null) { 167 | return false; 168 | } 169 | if ($range[0] === '') { 170 | $range[1] = $this->validBytePos($range[1]); 171 | if ($range[1] === false) { 172 | return false; 173 | } 174 | $range[0] = $this->filesize - $range[1]; 175 | $range[1] = $this->filesize - 1; 176 | continue; 177 | } 178 | $range[0] = $this->validBytePos($range[0]); 179 | if ($range[0] === false) { 180 | return false; 181 | } 182 | if ($range[1] === '') { 183 | $range[1] = $this->filesize - 1; 184 | continue; 185 | } 186 | $range[1] = $this->validBytePos($range[1]); 187 | if ($range[1] === false) { 188 | return false; 189 | } 190 | } 191 | // @phpstan-ignore-next-line 192 | return $ranges; 193 | } 194 | 195 | /** 196 | * @param string $pos 197 | * 198 | * @return false|int 199 | */ 200 | #[Pure] 201 | private function validBytePos(string $pos) : false | int 202 | { 203 | if ( ! \is_numeric($pos) || $pos < \PHP_INT_MIN || $pos > \PHP_INT_MAX) { 204 | return false; 205 | } 206 | if ($pos < 0 || $pos >= $this->filesize) { 207 | return false; 208 | } 209 | return (int) $pos; 210 | } 211 | 212 | private function setSinglePart(int $firstByte, int $lastByte) : void 213 | { 214 | $this->sendType = 'single'; 215 | $this->setHeader( 216 | Header::CONTENT_LENGTH, 217 | (string) ($lastByte - $firstByte + 1) 218 | ); 219 | $this->setHeader( 220 | Header::CONTENT_TYPE, 221 | \mime_content_type($this->filepath) ?: 'application/octet-stream' 222 | ); 223 | $this->setHeader( 224 | Header::CONTENT_RANGE, 225 | \sprintf('bytes %d-%d/%d', $firstByte, $lastByte, $this->filesize) 226 | ); 227 | } 228 | 229 | private function sendSinglePart() : void 230 | { 231 | // @phpstan-ignore-next-line 232 | $this->readBuffer($this->byteRanges[0][0], $this->byteRanges[0][1]); 233 | //$this->readFile(); 234 | } 235 | 236 | /** 237 | * @param array ...$byteRanges 238 | */ 239 | private function setMultiPart(array ...$byteRanges) : void 240 | { 241 | $this->sendType = 'multi'; 242 | $this->boundary = \md5($this->filepath); 243 | $length = 0; 244 | $topLength = \strlen($this->getMultiPartTopLine()); 245 | foreach ($byteRanges as $range) { 246 | $length += $topLength; 247 | $length += \strlen($this->getContentRangeLine($range[0], $range[1])); 248 | $length += $range[1] - $range[0] + 1; 249 | } 250 | $length += \strlen($this->getBoundaryLine()); 251 | $this->setHeader(Header::CONTENT_LENGTH, (string) $length); 252 | $this->setHeader( 253 | Header::CONTENT_TYPE, 254 | "multipart/x-byteranges; boundary={$this->boundary}" 255 | ); 256 | } 257 | 258 | private function sendMultiPart() : void 259 | { 260 | $topLine = $this->getMultiPartTopLine(); 261 | foreach ((array) $this->byteRanges as $range) { 262 | echo $topLine; 263 | echo $this->getContentRangeLine($range[0], $range[1]); // @phpstan-ignore-line 264 | $this->readBuffer($range[0], $range[1]); // @phpstan-ignore-line 265 | } 266 | echo $this->getBoundaryLine(); 267 | if ($this->inToString) { 268 | $this->appendBody(''); 269 | } 270 | } 271 | 272 | #[Pure] 273 | private function getBoundaryLine() : string 274 | { 275 | return "\r\n--{$this->boundary}--\r\n"; 276 | } 277 | 278 | #[Pure] 279 | private function getMultiPartTopLine() : string 280 | { 281 | return $this->getBoundaryLine() 282 | . "Content-Type: application/octet-stream\r\n"; 283 | } 284 | 285 | #[Pure] 286 | private function getContentRangeLine(int $fistByte, int $lastByte) : string 287 | { 288 | return \sprintf( 289 | "Content-Range: bytes %d-%d/%d\r\n\r\n", 290 | $fistByte, 291 | $lastByte, 292 | $this->filesize 293 | ); 294 | } 295 | 296 | private function readBuffer(int $firstByte, int $lastByte) : void 297 | { 298 | \fseek($this->handle, $firstByte); 299 | $bytesLeft = $lastByte - $firstByte + 1; 300 | while ($bytesLeft > 0 && ! \feof($this->handle)) { 301 | $bytesRead = $bytesLeft > $this->readLength ? $this->readLength : $bytesLeft; 302 | $bytesLeft -= $bytesRead; 303 | $this->flush($bytesRead); 304 | if (\connection_status() !== \CONNECTION_NORMAL) { 305 | break; 306 | } 307 | } 308 | } 309 | 310 | private function flush(int $length) : void 311 | { 312 | echo \fread($this->handle, $length); // @phpstan-ignore-line 313 | if ($this->inToString) { 314 | $this->appendBody(''); 315 | return; 316 | } 317 | \ob_flush(); 318 | \flush(); 319 | if ($this->delay) { 320 | \usleep($this->delay); 321 | } 322 | } 323 | 324 | private function readFile() : void 325 | { 326 | while ( ! \feof($this->handle)) { 327 | $this->flush($this->readLength); 328 | if (\connection_status() !== \CONNECTION_NORMAL) { 329 | break; 330 | } 331 | } 332 | } 333 | 334 | /** 335 | * Tell if Response has a downloadable file. 336 | * 337 | * @return bool 338 | */ 339 | #[Pure] 340 | public function hasDownload() : bool 341 | { 342 | return isset($this->filepath); 343 | } 344 | 345 | protected function sendDownload() : void 346 | { 347 | $handle = \fopen($this->filepath, 'rb'); 348 | if ($handle === false) { 349 | throw new RuntimeException( 350 | "Could not open a resource for file '{$this->filepath}'" 351 | ); 352 | } 353 | $this->handle = $handle; 354 | switch ($this->sendType) { 355 | case 'multi': 356 | $this->sendMultiPart(); 357 | break; 358 | case 'single': 359 | $this->sendSinglePart(); 360 | break; 361 | default: 362 | $this->readFile(); 363 | } 364 | \fclose($this->handle); 365 | } 366 | } 367 | -------------------------------------------------------------------------------- /src/Debug/HTTPCollector.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 | namespace Framework\HTTP\Debug; 11 | 12 | use Framework\Debug\Collector; 13 | use Framework\Debug\Debugger; 14 | use Framework\Helpers\ArraySimple; 15 | use Framework\HTTP\Request; 16 | use Framework\HTTP\Response; 17 | 18 | /** 19 | * Class HTTPCollector. 20 | * 21 | * @package http 22 | */ 23 | class HTTPCollector extends Collector 24 | { 25 | protected Request $request; 26 | protected Response $response; 27 | 28 | public function setRequest(Request $request) : static 29 | { 30 | $this->request = $request; 31 | return $this; 32 | } 33 | 34 | public function setResponse(Response $response, bool $replaceRequest = true) : static 35 | { 36 | $this->response = $response; 37 | if ($replaceRequest) { 38 | $this->setRequest($response->getRequest()); 39 | } 40 | return $this; 41 | } 42 | 43 | public function getActivities() : array 44 | { 45 | $activities = []; 46 | foreach ($this->getData() as $data) { 47 | if (isset($data['message'], $data['type']) && 48 | $data['message'] === 'response' && 49 | $data['type'] === 'send' 50 | ) { 51 | $activities[] = [ 52 | 'collector' => $this->getName(), 53 | 'class' => static::class, 54 | 'description' => 'Send response', 55 | 'start' => $data['start'], 56 | 'end' => $data['end'], 57 | ]; 58 | } 59 | } 60 | return $activities; 61 | } 62 | 63 | public function getContents() : string 64 | { 65 | \ob_start(); ?> 66 |

Request

67 | renderRequest() ?> 68 |

Response

69 | renderResponse() ?> 70 | request)) { 77 | return '

A Request instance has not been set on this collector.

'; 78 | } 79 | \ob_start(); ?> 80 |

IP: request->getIp() ?>

81 |

Protocol: request->getProtocol() ?>

82 |

Method: request->getMethod() ?>

83 |

URL: request->getUrl() ?>

84 |

Server: request->getServer('SERVER_SOFTWARE') ?>

85 |

Hostname:

86 | renderRequestUserAgent() ?> 87 | renderHeadersTable($this->request->getHeaderLines()); 89 | echo $this->renderRequestBody(); 90 | echo $this->renderRequestForm(); 91 | echo $this->renderRequestFiles(); 92 | return \ob_get_clean(); // @phpstan-ignore-line 93 | } 94 | 95 | protected function renderRequestUserAgent() : string 96 | { 97 | $userAgent = $this->request->getUserAgent(); 98 | if ($userAgent === null) { 99 | return ''; 100 | } 101 | \ob_start(); ?> 102 |

User-Agent

103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 120 | 121 | 122 | 123 | 124 |
TypeNameVersionPlatformIs Mobile
getType()) ?>getName()) ?>isBrowser() 118 | ? \htmlentities((string) $userAgent->getBrowserVersion()) 119 | : '' ?>getPlatform()) ?>isMobile() ? 'Yes' : 'No' ?>
125 | request->hasFiles() 132 | ? \http_build_query($this->request->getPost()) 133 | : $this->request->getBody(); 134 | if ($body === '') { 135 | return ''; 136 | } 137 | \ob_start(); ?> 138 |

Body Contents

139 |
142 | request->isPost() && ! $this->request->isForm()) { 149 | return ''; 150 | } 151 | \ob_start(); ?> 152 |

Form

153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | request->getParsedBody()) as $field => $value): ?> 162 | 163 | 164 | 167 | 168 | 169 | 170 |
FieldValue
165 |
166 |
171 | request->hasFiles()) { 178 | return ''; 179 | } 180 | \ob_start(); ?> 181 |

Uploaded Files

182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | request->getFiles()) as $field => $file): ?> 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 |
FieldNameFull PathTypeClient TypeExtensionSizeDestinationError
getName()) ?>getFullPath()) ?>getType()) ?>getClientType()) ?>getExtension()) ?>getSize()) ?>getDestination() ?>getError() ?>getErrorMessage()) ?>
213 | response)) { 220 | return '

A Response instance has not been set on this collector.

'; 221 | } 222 | \ob_start(); ?> 223 |

Protocol: response->getProtocol()) ?>

224 |

Status: response->getStatus()) ?>

225 |

Sent: response->isSent() ? 'Yes' : 'No' ?>

226 | response->isSent()): 228 | $info = []; 229 | foreach ($this->getData() as $data) { 230 | if ( 231 | isset($data['message'], $data['type']) 232 | && $data['message'] === 'response' 233 | && $data['type'] === 'send' 234 | ) { 235 | $info = $data; 236 | break; 237 | } 238 | } ?> 239 |

240 | Time Sending: seconds 241 |

242 | renderHeadersTable($this->response->getHeaderLines()); 245 | echo $this->renderResponseCookies(); 246 | echo $this->renderResponseBody(); 247 | return \ob_get_clean(); // @phpstan-ignore-line 248 | } 249 | 250 | protected function renderResponseCookies() : string 251 | { 252 | if ( ! $this->response->getCookies()) { 253 | return ''; 254 | } 255 | \ob_start(); ?> 256 |

Cookies

257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | response->getCookies() as $cookie): ?> 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 |
NameValueExpiresPathDomainIs SecureIs HTTP OnlySameSite
getName()) ?>getValue()) ?>getExpires()?->format('D, d-M-Y H:i:s \G\M\T') ?>getPath()) ?>getDomain()) ?>isSecure() ? 'Yes' : 'No' ?>isHttpOnly() ? 'Yes' : 'No' ?>getSameSite()) ?>
285 | 292 |

Body Contents

293 | response->isSent()) { 295 | echo '

Response has not been sent.

'; 296 | return \ob_get_clean(); // @phpstan-ignore-line 297 | } 298 | if ($this->response->hasDownload()) { 299 | echo '

Body has downloadable content.

'; 300 | return \ob_get_clean(); // @phpstan-ignore-line 301 | } 302 | $body = $this->response->getBody(); 303 | if ($body === '') { 304 | echo '

Body is empty.

'; 305 | return \ob_get_clean(); // @phpstan-ignore-line 306 | } ?> 307 |
310 | $headerLines 316 | * 317 | * @return string 318 | */ 319 | protected function renderHeadersTable(array $headerLines) : string 320 | { 321 | \ob_start(); ?> 322 |

Headers

323 | No headers.

'; 326 | return \ob_get_clean(); // @phpstan-ignore-line 327 | } ?> 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 338 | 339 | 340 | 341 | 342 | 343 | 344 |
NameValue
345 | 6 | * 7 | * For the full copyright and license information, please view the LICENSE 8 | * file that was distributed with this source code. 9 | */ 10 | namespace Framework\HTTP; 11 | 12 | use InvalidArgumentException; 13 | use JetBrains\PhpStorm\ArrayShape; 14 | use JetBrains\PhpStorm\Deprecated; 15 | use JetBrains\PhpStorm\Pure; 16 | use RuntimeException; 17 | 18 | /** 19 | * Class URL. 20 | * 21 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#urls 22 | * @see https://developer.mozilla.org/en-US/docs/Web/API/URL 23 | * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3 24 | * 25 | * @package http 26 | */ 27 | class URL implements \JsonSerializable, \Stringable 28 | { 29 | /** 30 | * The #fragment (id). 31 | */ 32 | protected ?string $fragment = null; 33 | protected ?string $hostname = null; 34 | protected ?string $pass = null; 35 | /** 36 | * The /paths/of/url. 37 | * 38 | * @var array 39 | */ 40 | protected array $pathSegments = []; 41 | protected ?int $port = null; 42 | /** 43 | * The ?queries. 44 | * 45 | * @var array 46 | */ 47 | protected array $queryData = []; 48 | protected ?string $scheme = null; 49 | protected ?string $user = null; 50 | 51 | /** 52 | * URL constructor. 53 | * 54 | * @param string $url 55 | */ 56 | public function __construct(string $url) 57 | { 58 | $this->setUrl($url); 59 | } 60 | 61 | /** 62 | * @return string 63 | */ 64 | #[Pure] 65 | public function __toString() : string 66 | { 67 | return $this->toString(); 68 | } 69 | 70 | /** 71 | * @param string $query 72 | * @param int|string|null $value 73 | * 74 | * @return static 75 | */ 76 | public function addQuery(string $query, $value = null) : static 77 | { 78 | $this->queryData[$query] = $value; 79 | return $this; 80 | } 81 | 82 | /** 83 | * @param array $queries 84 | * 85 | * @return static 86 | */ 87 | public function addQueries(array $queries) : static 88 | { 89 | foreach ($queries as $name => $value) { 90 | $this->addQuery($name, $value); 91 | } 92 | return $this; 93 | } 94 | 95 | /** 96 | * @param array $allowed 97 | * 98 | * @return array 99 | */ 100 | #[Pure] 101 | protected function filterQuery(array $allowed) : array 102 | { 103 | return $this->queryData ? 104 | \array_intersect_key($this->queryData, \array_flip($allowed)) 105 | : []; 106 | } 107 | 108 | #[Pure] 109 | public function getBaseUrl(string $path = '/') : string 110 | { 111 | if ($path && $path !== '/') { 112 | $path = '/' . \ltrim($path, '/'); 113 | } 114 | return $this->getOrigin() . $path; 115 | } 116 | 117 | /** 118 | * @return string|null 119 | */ 120 | public function getFragment() : ?string 121 | { 122 | return $this->fragment; 123 | } 124 | 125 | /** 126 | * @return string|null 127 | */ 128 | #[Pure] 129 | public function getHost() : ?string 130 | { 131 | return $this->hostname === null ? null : $this->hostname . $this->getPortPart(); 132 | } 133 | 134 | #[Pure] 135 | public function getHostname() : ?string 136 | { 137 | return $this->hostname; 138 | } 139 | 140 | #[Pure] 141 | public function getOrigin() : string 142 | { 143 | return $this->getScheme() . '://' . $this->getHost(); 144 | } 145 | 146 | /** 147 | * @return array 148 | */ 149 | #[ArrayShape([ 150 | 'scheme' => 'string', 151 | 'user' => 'null|string', 152 | 'pass' => 'null|string', 153 | 'hostname' => 'string', 154 | 'port' => 'int|null', 155 | 'path' => 'string[]', 156 | 'query' => 'mixed[]', 157 | 'fragment' => 'null|string', 158 | ])] 159 | #[Pure] 160 | public function getParsedUrl() : array 161 | { 162 | return [ 163 | 'scheme' => $this->getScheme(), 164 | 'user' => $this->getUser(), 165 | 'pass' => $this->getPass(), 166 | 'hostname' => $this->getHostname(), 167 | 'port' => $this->getPort(), 168 | 'path' => $this->getPathSegments(), 169 | 'query' => $this->getQueryData(), 170 | 'fragment' => $this->getFragment(), 171 | ]; 172 | } 173 | 174 | /** 175 | * @return string|null 176 | */ 177 | #[Pure] 178 | public function getPass() : ?string 179 | { 180 | return $this->pass; 181 | } 182 | 183 | #[Pure] 184 | public function getPath() : string 185 | { 186 | return '/' . \implode('/', $this->pathSegments); 187 | } 188 | 189 | /** 190 | * @return array 191 | */ 192 | #[Pure] 193 | public function getPathSegments() : array 194 | { 195 | return $this->pathSegments; 196 | } 197 | 198 | #[Pure] 199 | public function getPathSegment(int $index) : ?string 200 | { 201 | return $this->pathSegments[$index] ?? null; 202 | } 203 | 204 | /** 205 | * @return int|null 206 | */ 207 | #[Pure] 208 | public function getPort() : ?int 209 | { 210 | return $this->port; 211 | } 212 | 213 | #[Pure] 214 | protected function getPortPart() : string 215 | { 216 | $part = $this->getPort(); 217 | if ( ! \in_array($part, [ 218 | null, 219 | 80, 220 | 443, 221 | ], true)) { 222 | return ':' . $part; 223 | } 224 | return ''; 225 | } 226 | 227 | /** 228 | * Get the "Query" part of the URL. 229 | * 230 | * @param array $allowedKeys Allowed query keys 231 | * 232 | * @return string|null 233 | */ 234 | #[Pure] 235 | public function getQuery(array $allowedKeys = []) : ?string 236 | { 237 | $query = $this->getQueryData($allowedKeys); 238 | return $query ? \http_build_query($query) : null; 239 | } 240 | 241 | /** 242 | * @param array $allowedKeys 243 | * 244 | * @return array 245 | */ 246 | #[Pure] 247 | public function getQueryData(array $allowedKeys = []) : array 248 | { 249 | return $allowedKeys ? $this->filterQuery($allowedKeys) : $this->queryData; 250 | } 251 | 252 | /** 253 | * @return string|null 254 | */ 255 | #[Pure] 256 | public function getScheme() : ?string 257 | { 258 | return $this->scheme; 259 | } 260 | 261 | /** 262 | * @return string 263 | * 264 | * @deprecated Use {@see URL::toString()} 265 | * 266 | * @codeCoverageIgnore 267 | */ 268 | #[Deprecated( 269 | reason: 'since HTTP Library version 5.3, use toString() instead', 270 | replacement: '%class%->toString()' 271 | )] 272 | public function getAsString() : string 273 | { 274 | \trigger_error( 275 | 'Method ' . __METHOD__ . ' is deprecated', 276 | \E_USER_DEPRECATED 277 | ); 278 | return $this->toString(); 279 | } 280 | 281 | /** 282 | * @since 5.3 283 | * 284 | * @return string 285 | */ 286 | #[Pure] 287 | public function toString() : string 288 | { 289 | $url = $this->getScheme() . '://'; 290 | $part = $this->getUser(); 291 | if ($part !== null) { 292 | $url .= $part; 293 | $part = $this->getPass(); 294 | if ($part !== null) { 295 | $url .= ':' . $part; 296 | } 297 | $url .= '@'; 298 | } 299 | $url .= $this->getHost(); 300 | $url .= $this->getPath(); 301 | $part = $this->getQuery(); 302 | if ($part !== null) { 303 | $url .= '?' . $part; 304 | } 305 | $part = $this->getFragment(); 306 | if ($part !== null) { 307 | $url .= '#' . $part; 308 | } 309 | return $url; 310 | } 311 | 312 | /** 313 | * @return string|null 314 | */ 315 | #[Pure] 316 | public function getUser() : ?string 317 | { 318 | return $this->user; 319 | } 320 | 321 | /** 322 | * @param string $key 323 | * 324 | * @return static 325 | */ 326 | public function removeQueryData(string $key) : static 327 | { 328 | unset($this->queryData[$key]); 329 | return $this; 330 | } 331 | 332 | /** 333 | * @param string $fragment 334 | * 335 | * @return static 336 | */ 337 | public function setFragment(string $fragment) : static 338 | { 339 | $this->fragment = \ltrim($fragment, '#'); 340 | return $this; 341 | } 342 | 343 | /** 344 | * @param string $hostname 345 | * 346 | * @throws InvalidArgumentException for invalid URL Hostname 347 | * 348 | * @return static 349 | */ 350 | public function setHostname(string $hostname) : static 351 | { 352 | $filtered = \filter_var($hostname, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME); 353 | if ( ! $filtered) { 354 | throw new InvalidArgumentException("Invalid URL Hostname: {$hostname}"); 355 | } 356 | $this->hostname = $filtered; 357 | return $this; 358 | } 359 | 360 | /** 361 | * @param string $pass 362 | * 363 | * @return static 364 | */ 365 | public function setPass(string $pass) : static 366 | { 367 | $this->pass = $pass; 368 | return $this; 369 | } 370 | 371 | /** 372 | * @param string $segments 373 | * 374 | * @return static 375 | */ 376 | public function setPath(string $segments) : static 377 | { 378 | return $this->setPathSegments(\explode('/', \ltrim($segments, '/'))); 379 | } 380 | 381 | /** 382 | * @param array $segments 383 | * 384 | * @return static 385 | */ 386 | public function setPathSegments(array $segments) : static 387 | { 388 | $this->pathSegments = $segments; 389 | return $this; 390 | } 391 | 392 | /** 393 | * @param int $port 394 | * 395 | * @throws InvalidArgumentException for invalid URL Port 396 | * 397 | * @return static 398 | */ 399 | public function setPort(int $port) : static 400 | { 401 | if ($port < 1 || $port > 65535) { 402 | throw new InvalidArgumentException("Invalid URL Port: {$port}"); 403 | } 404 | $this->port = $port; 405 | return $this; 406 | } 407 | 408 | /** 409 | * @param string $data 410 | * @param array $only 411 | * 412 | * @return static 413 | */ 414 | public function setQuery(string $data, array $only = []) : static 415 | { 416 | \parse_str(\ltrim($data, '?'), $data); 417 | return $this->setQueryData($data, $only); 418 | } 419 | 420 | /** 421 | * @param array $data 422 | * @param array $only 423 | * 424 | * @return static 425 | */ 426 | public function setQueryData(array $data, array $only = []) : static 427 | { 428 | if ($only) { 429 | $data = \array_intersect_key($data, \array_flip($only)); 430 | } 431 | $this->queryData = $data; 432 | return $this; 433 | } 434 | 435 | /** 436 | * @param string $scheme 437 | * 438 | * @return static 439 | */ 440 | public function setScheme(string $scheme) : static 441 | { 442 | $this->scheme = $scheme; 443 | return $this; 444 | } 445 | 446 | /** 447 | * @param string $url 448 | * 449 | * @throws InvalidArgumentException for invalid URL 450 | * 451 | * @return static 452 | */ 453 | protected function setUrl(string $url) : static 454 | { 455 | $filteredUrl = \filter_var($url, \FILTER_VALIDATE_URL); 456 | if ( ! $filteredUrl) { 457 | throw new InvalidArgumentException("Invalid URL: {$url}"); 458 | } 459 | $url = \parse_url($filteredUrl); 460 | if ($url === false) { 461 | throw new RuntimeException("URL could not be parsed: {$filteredUrl}"); 462 | } 463 | $this->setScheme($url['scheme']); // @phpstan-ignore-line 464 | if (isset($url['user'])) { 465 | $this->setUser($url['user']); 466 | } 467 | if (isset($url['pass'])) { 468 | $this->setPass($url['pass']); 469 | } 470 | $this->setHostname($url['host']); // @phpstan-ignore-line 471 | if (isset($url['port'])) { 472 | $this->setPort($url['port']); 473 | } 474 | if (isset($url['path'])) { 475 | $this->setPath($url['path']); 476 | } 477 | if (isset($url['query'])) { 478 | $this->setQuery($url['query']); 479 | } 480 | if (isset($url['fragment'])) { 481 | $this->setFragment($url['fragment']); 482 | } 483 | return $this; 484 | } 485 | 486 | /** 487 | * @param string $user 488 | * 489 | * @return static 490 | */ 491 | public function setUser(string $user) : static 492 | { 493 | $this->user = $user; 494 | return $this; 495 | } 496 | 497 | #[Pure] 498 | public function jsonSerialize() : string 499 | { 500 | return $this->toString(); 501 | } 502 | } 503 | -------------------------------------------------------------------------------- /src/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 | namespace Framework\HTTP; 11 | 12 | use BadMethodCallException; 13 | use InvalidArgumentException; 14 | use JetBrains\PhpStorm\Pure; 15 | 16 | /** 17 | * Class Message. 18 | * 19 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages 20 | * @see https://datatracker.ietf.org/doc/html/rfc7231 21 | * 22 | * @package http 23 | */ 24 | abstract class Message implements MessageInterface 25 | { 26 | /** 27 | * HTTP Message Protocol. 28 | */ 29 | protected string $protocol = Protocol::HTTP_1_1; 30 | /** 31 | * HTTP Request URL. 32 | */ 33 | protected URL $url; 34 | /** 35 | * HTTP Request Method. 36 | */ 37 | protected string $method; 38 | /** 39 | * HTTP Response Status Code. 40 | */ 41 | protected int $statusCode; 42 | /** 43 | * HTTP Message Body. 44 | */ 45 | protected string $body; 46 | /** 47 | * HTTP Message Cookies. 48 | * 49 | * @var array 50 | */ 51 | protected array $cookies = []; 52 | /** 53 | * HTTP Message Headers. 54 | * 55 | * @var array 56 | */ 57 | protected array $headers = []; 58 | 59 | public function __toString() : string 60 | { 61 | $eol = "\r\n"; 62 | $message = $this->getStartLine() . $eol; 63 | foreach ($this->getHeaderLines() as $headerLine) { 64 | $message .= $headerLine . $eol; 65 | } 66 | $message .= $eol; 67 | $message .= $this->getBody(); 68 | return $message; 69 | } 70 | 71 | /** 72 | * Get the Message Start-Line. 73 | * 74 | * @throws BadMethodCallException if $this is not an instance of 75 | * RequestInterface or ResponseInterface 76 | * 77 | * @return string 78 | */ 79 | public function getStartLine() : string 80 | { 81 | if ($this instanceof RequestInterface) { 82 | $query = $this->getUrl()->getQuery(); 83 | $query = ($query !== null && $query !== '') ? '?' . $query : ''; 84 | return $this->getMethod() 85 | . ' ' . $this->getUrl()->getPath() . $query 86 | . ' ' . $this->getProtocol(); 87 | } 88 | if ($this instanceof ResponseInterface) { 89 | return $this->getProtocol() 90 | . ' ' . $this->getStatus(); 91 | } 92 | throw new BadMethodCallException( 93 | static::class . ' is not an instance of ' . RequestInterface::class 94 | . ' or ' . ResponseInterface::class 95 | ); 96 | } 97 | 98 | #[Pure] 99 | public function hasHeader(string $name, string $value = null) : bool 100 | { 101 | return $value === null 102 | ? $this->getHeader($name) !== null 103 | : $this->getHeader($name) === $value; 104 | } 105 | 106 | #[Pure] 107 | public function getHeader(string $name) : ?string 108 | { 109 | return $this->headers[\strtolower($name)] ?? null; 110 | } 111 | 112 | /** 113 | * @return array 114 | */ 115 | #[Pure] 116 | public function getHeaders() : array 117 | { 118 | return $this->headers; 119 | } 120 | 121 | #[Pure] 122 | public function getHeaderLine(string $name) : ?string 123 | { 124 | $value = $this->getHeader($name); 125 | if ($value === null) { 126 | return null; 127 | } 128 | $name = Header::getName($name); 129 | return $name . ': ' . $value; 130 | } 131 | 132 | /** 133 | * @return array 134 | */ 135 | #[Pure] 136 | public function getHeaderLines() : array 137 | { 138 | $lines = []; 139 | foreach ($this->getHeaders() as $name => $value) { 140 | $name = Header::getName($name); 141 | if (\str_contains($value, "\n")) { 142 | foreach (\explode("\n", $value) as $val) { 143 | $lines[] = $name . ': ' . $val; 144 | } 145 | continue; 146 | } 147 | $lines[] = $name . ': ' . $value; 148 | } 149 | return $lines; 150 | } 151 | 152 | /** 153 | * Set a Message header. 154 | * 155 | * @param string $name 156 | * @param string $value 157 | * 158 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 159 | * 160 | * @return static 161 | */ 162 | protected function setHeader(string $name, string $value) : static 163 | { 164 | $this->headers[\strtolower($name)] = $value; 165 | return $this; 166 | } 167 | 168 | /** 169 | * Set a list of headers. 170 | * 171 | * @param array $headers 172 | * 173 | * @return static 174 | */ 175 | protected function setHeaders(array $headers) : static 176 | { 177 | foreach ($headers as $name => $value) { 178 | $this->setHeader((string) $name, (string) $value); 179 | } 180 | return $this; 181 | } 182 | 183 | /** 184 | * Append a Message header. 185 | * 186 | * Used to set repeated header field names. 187 | * 188 | * @param string $name 189 | * @param string $value 190 | * 191 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers 192 | * 193 | * @return static 194 | */ 195 | protected function appendHeader(string $name, string $value) : static 196 | { 197 | $current = $this->getHeader($name); 198 | if ($current !== null) { 199 | $separator = $this->getHeaderValueSeparator($name); 200 | $value = $current . $separator . $value; 201 | } 202 | $this->setHeader($name, $value); 203 | return $this; 204 | } 205 | 206 | /** 207 | * @param string $headerName 208 | * 209 | * @see https://stackoverflow.com/a/38406581/6027968 210 | * 211 | * @return string 212 | */ 213 | private function getHeaderValueSeparator(string $headerName) : string 214 | { 215 | if (Header::isMultiline($headerName)) { 216 | return "\n"; 217 | } 218 | return ', '; 219 | } 220 | 221 | /** 222 | * Remove a header by name. 223 | * 224 | * @param string $name 225 | * 226 | * @return static 227 | */ 228 | protected function removeHeader(string $name) : static 229 | { 230 | unset($this->headers[\strtolower($name)]); 231 | return $this; 232 | } 233 | 234 | /** 235 | * Remove many headers by a list of headers. 236 | * 237 | * @return static 238 | */ 239 | protected function removeHeaders() : static 240 | { 241 | $this->headers = []; 242 | return $this; 243 | } 244 | 245 | /** 246 | * Say if the Message has a Cookie. 247 | * 248 | * @param string $name Cookie name 249 | * 250 | * @return bool 251 | */ 252 | #[Pure] 253 | public function hasCookie(string $name) : bool 254 | { 255 | return (bool) $this->getCookie($name); 256 | } 257 | 258 | /** 259 | * Get a Cookie by name. 260 | * 261 | * @param string $name 262 | * 263 | * @return Cookie|null 264 | */ 265 | #[Pure] 266 | public function getCookie(string $name) : ?Cookie 267 | { 268 | return $this->cookies[$name] ?? null; 269 | } 270 | 271 | /** 272 | * Get all Cookies. 273 | * 274 | * @return array 275 | */ 276 | #[Pure] 277 | public function getCookies() : array 278 | { 279 | return $this->cookies; 280 | } 281 | 282 | /** 283 | * Set a new Cookie. 284 | * 285 | * @param Cookie $cookie 286 | * 287 | * @return static 288 | */ 289 | protected function setCookie(Cookie $cookie) : static 290 | { 291 | $this->cookies[$cookie->getName()] = $cookie; 292 | return $this; 293 | } 294 | 295 | /** 296 | * Set a list of Cookies. 297 | * 298 | * @param array $cookies 299 | * 300 | * @return static 301 | */ 302 | protected function setCookies(array $cookies) : static 303 | { 304 | foreach ($cookies as $cookie) { 305 | $this->setCookie($cookie); 306 | } 307 | return $this; 308 | } 309 | 310 | /** 311 | * Remove a Cookie by name. 312 | * 313 | * @param string $name 314 | * 315 | * @return static 316 | */ 317 | protected function removeCookie(string $name) : static 318 | { 319 | unset($this->cookies[$name]); 320 | return $this; 321 | } 322 | 323 | /** 324 | * Remove many Cookies by names. 325 | * 326 | * @param array $names 327 | * 328 | * @return static 329 | */ 330 | protected function removeCookies(array $names) : static 331 | { 332 | foreach ($names as $name) { 333 | $this->removeCookie($name); 334 | } 335 | return $this; 336 | } 337 | 338 | /** 339 | * Get the Message body. 340 | * 341 | * @return string 342 | */ 343 | #[Pure] 344 | public function getBody() : string 345 | { 346 | return $this->body ?? ''; 347 | } 348 | 349 | /** 350 | * Set the Message body. 351 | * 352 | * @param string $body 353 | * 354 | * @return static 355 | */ 356 | protected function setBody(string $body) : static 357 | { 358 | $this->body = $body; 359 | return $this; 360 | } 361 | 362 | /** 363 | * Get the HTTP protocol. 364 | * 365 | * @return string 366 | */ 367 | #[Pure] 368 | public function getProtocol() : string 369 | { 370 | return $this->protocol; 371 | } 372 | 373 | /** 374 | * Set the HTTP protocol. 375 | * 376 | * @param string $protocol HTTP/1.1, HTTP/2, etc 377 | * 378 | * @return static 379 | */ 380 | protected function setProtocol(string $protocol) : static 381 | { 382 | $this->protocol = Protocol::validate($protocol); 383 | return $this; 384 | } 385 | 386 | /** 387 | * Gets the HTTP Request Method. 388 | * 389 | * @return string $method One of: CONNECT, DELETE, GET, HEAD, OPTIONS, 390 | * PATCH, POST, PUT, or TRACE 391 | */ 392 | #[Pure] 393 | protected function getMethod() : string 394 | { 395 | return $this->method; 396 | } 397 | 398 | /** 399 | * @param string $method 400 | * 401 | * @throws InvalidArgumentException for invalid method 402 | * 403 | * @return bool 404 | */ 405 | protected function isMethod(string $method) : bool 406 | { 407 | return $this->getMethod() === Method::validate($method); 408 | } 409 | 410 | /** 411 | * Set the request method. 412 | * 413 | * @param string $method One of: CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH, 414 | * POST, PUT, or TRACE 415 | * 416 | * @throws InvalidArgumentException for invalid method 417 | * 418 | * @return static 419 | */ 420 | protected function setMethod(string $method) : static 421 | { 422 | $this->method = Method::validate($method); 423 | return $this; 424 | } 425 | 426 | protected function setStatusCode(int $code) : static 427 | { 428 | $this->statusCode = Status::validate($code); 429 | return $this; 430 | } 431 | 432 | /** 433 | * Get the status code. 434 | * 435 | * @return int 436 | */ 437 | #[Pure] 438 | protected function getStatusCode() : int 439 | { 440 | return $this->statusCode; 441 | } 442 | 443 | protected function isStatusCode(int $code) : bool 444 | { 445 | return $this->getStatusCode() === Status::validate($code); 446 | } 447 | 448 | /** 449 | * Gets the requested URL. 450 | * 451 | * @return URL 452 | */ 453 | #[Pure] 454 | protected function getUrl() : URL 455 | { 456 | return $this->url; 457 | } 458 | 459 | /** 460 | * Set the Message URL. 461 | * 462 | * @param string|URL $url 463 | * 464 | * @return static 465 | */ 466 | protected function setUrl(string | URL $url) : static 467 | { 468 | if ( ! $url instanceof URL) { 469 | $url = new URL($url); 470 | } 471 | $this->url = $url; 472 | return $this; 473 | } 474 | 475 | #[Pure] 476 | protected function parseContentType() : ?string 477 | { 478 | $contentType = $this->getHeader('Content-Type'); 479 | if ($contentType === null) { 480 | return null; 481 | } 482 | $contentType = \explode(';', $contentType, 2)[0]; 483 | return \trim($contentType); 484 | } 485 | 486 | /** 487 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Quality_values 488 | * @see https://stackoverflow.com/a/33748742/6027968 489 | * 490 | * @param string|null $string 491 | * 492 | * @return array 493 | */ 494 | public static function parseQualityValues(?string $string) : array 495 | { 496 | if (empty($string)) { 497 | return []; 498 | } 499 | $quality = \array_reduce( 500 | \explode(',', $string, 20), 501 | static function ($qualifier, $part) { 502 | [$value, $priority] = \array_merge(\explode(';q=', $part), [1]); 503 | $qualifier[\trim((string) $value)] = (float) $priority; 504 | return $qualifier; 505 | }, 506 | [] 507 | ); 508 | \arsort($quality); 509 | return $quality; 510 | } 511 | } 512 | -------------------------------------------------------------------------------- /src/UserAgent.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 | namespace Framework\HTTP; 11 | 12 | use JetBrains\PhpStorm\Deprecated; 13 | use JetBrains\PhpStorm\Pure; 14 | 15 | /** 16 | * Class UserAgent. 17 | * 18 | * @package http 19 | */ 20 | class UserAgent implements \JsonSerializable, \Stringable 21 | { 22 | protected ?string $agent = null; 23 | protected ?string $browser = null; 24 | protected ?string $browserVersion = null; 25 | protected ?string $mobile = null; 26 | protected ?string $platform = null; 27 | protected ?string $robot = null; 28 | protected bool $isBrowser = false; 29 | protected bool $isMobile = false; 30 | protected bool $isRobot = false; 31 | /** 32 | * @var array> 33 | */ 34 | protected static array $config = [ 35 | 'platforms' => [ 36 | 'windows nt 10.0' => 'Windows 10', 37 | 'windows nt 6.3' => 'Windows 8.1', 38 | 'windows nt 6.2' => 'Windows 8', 39 | 'windows nt 6.1' => 'Windows 7', 40 | 'windows nt 6.0' => 'Windows Vista', 41 | 'windows nt 5.2' => 'Windows 2003', 42 | 'windows nt 5.1' => 'Windows XP', 43 | 'windows nt 5.0' => 'Windows 2000', 44 | 'windows nt 4.0' => 'Windows NT 4.0', 45 | 'winnt4.0' => 'Windows NT 4.0', 46 | 'winnt 4.0' => 'Windows NT', 47 | 'winnt' => 'Windows NT', 48 | 'windows 98' => 'Windows 98', 49 | 'win98' => 'Windows 98', 50 | 'windows 95' => 'Windows 95', 51 | 'win95' => 'Windows 95', 52 | 'windows phone' => 'Windows Phone', 53 | 'windows' => 'Unknown Windows OS', 54 | 'android' => 'Android', 55 | 'blackberry' => 'BlackBerry', 56 | 'iphone' => 'iOS', 57 | 'ipad' => 'iOS', 58 | 'ipod' => 'iOS', 59 | 'os x' => 'Mac OS X', 60 | 'ppc mac' => 'Power PC Mac', 61 | 'freebsd' => 'FreeBSD', 62 | 'ppc' => 'Macintosh', 63 | 'ubuntu' => 'Ubuntu', 64 | 'debian' => 'Debian', 65 | 'fedora' => 'Fedora', 66 | 'linux' => 'Linux', 67 | 'sunos' => 'Sun Solaris', 68 | 'beos' => 'BeOS', 69 | 'apachebench' => 'ApacheBench', 70 | 'aix' => 'AIX', 71 | 'irix' => 'Irix', 72 | 'osf' => 'DEC OSF', 73 | 'hp-ux' => 'HP-UX', 74 | 'netbsd' => 'NetBSD', 75 | 'bsdi' => 'BSDi', 76 | 'openbsd' => 'OpenBSD', 77 | 'gnu' => 'GNU/Linux', 78 | 'unix' => 'Unknown Unix OS', 79 | 'symbian' => 'Symbian OS', 80 | ], 81 | // The order of this array should NOT be changed. Many browsers return 82 | // multiple browser types so we want to identify the sub-type first. 83 | 'browsers' => [ 84 | 'curl' => 'Curl', 85 | 'PostmanRuntime' => 'Postman', 86 | 'OPR' => 'Opera', 87 | 'Flock' => 'Flock', 88 | 'Edge' => 'Spartan', 89 | 'Edg' => 'Edge', 90 | 'EdgA' => 'Edge', 91 | 'Chrome' => 'Chrome', 92 | // Opera 10+ always reports Opera/9.80 and appends 93 | // Version/ to the user agent string 94 | 'Opera.*?Version' => 'Opera', 95 | 'Opera' => 'Opera', 96 | 'MSIE' => 'Internet Explorer', 97 | 'Internet Explorer' => 'Internet Explorer', 98 | 'Trident.* rv' => 'Internet Explorer', 99 | 'Shiira' => 'Shiira', 100 | 'Firefox' => 'Firefox', 101 | 'Chimera' => 'Chimera', 102 | 'Phoenix' => 'Phoenix', 103 | 'Firebird' => 'Firebird', 104 | 'Camino' => 'Camino', 105 | 'Netscape' => 'Netscape', 106 | 'OmniWeb' => 'OmniWeb', 107 | 'Safari' => 'Safari', 108 | 'Mozilla' => 'Mozilla', 109 | 'Konqueror' => 'Konqueror', 110 | 'icab' => 'iCab', 111 | 'Lynx' => 'Lynx', 112 | 'Links' => 'Links', 113 | 'hotjava' => 'HotJava', 114 | 'amaya' => 'Amaya', 115 | 'IBrowse' => 'IBrowse', 116 | 'Maxthon' => 'Maxthon', 117 | 'Ubuntu' => 'Ubuntu Web Browser', 118 | 'Vivaldi' => 'Vivaldi', 119 | ], 120 | 'mobiles' => [ 121 | 'mobileexplorer' => 'Mobile Explorer', 122 | 'palmsource' => 'Palm', 123 | 'palmscape' => 'Palmscape', 124 | // Phones and Manufacturers 125 | 'motorola' => 'Motorola', 126 | 'nokia' => 'Nokia', 127 | 'palm' => 'Palm', 128 | 'iphone' => 'Apple iPhone', 129 | 'ipad' => 'iPad', 130 | 'ipod' => 'Apple iPod Touch', 131 | 'sony' => 'Sony Ericsson', 132 | 'ericsson' => 'Sony Ericsson', 133 | 'blackberry' => 'BlackBerry', 134 | 'cocoon' => 'O2 Cocoon', 135 | 'blazer' => 'Treo', 136 | 'lg' => 'LG', 137 | 'amoi' => 'Amoi', 138 | 'xda' => 'XDA', 139 | 'mda' => 'MDA', 140 | 'vario' => 'Vario', 141 | 'htc' => 'HTC', 142 | 'samsung' => 'Samsung', 143 | 'sharp' => 'Sharp', 144 | 'sie-' => 'Siemens', 145 | 'alcatel' => 'Alcatel', 146 | 'benq' => 'BenQ', 147 | 'ipaq' => 'HP iPaq', 148 | 'mot-' => 'Motorola', 149 | 'playstation portable' => 'PlayStation Portable', 150 | 'playstation 3' => 'PlayStation 3', 151 | 'playstation vita' => 'PlayStation Vita', 152 | 'hiptop' => 'Danger Hiptop', 153 | 'nec-' => 'NEC', 154 | 'panasonic' => 'Panasonic', 155 | 'philips' => 'Philips', 156 | 'sagem' => 'Sagem', 157 | 'sanyo' => 'Sanyo', 158 | 'spv' => 'SPV', 159 | 'zte' => 'ZTE', 160 | 'sendo' => 'Sendo', 161 | 'nintendo dsi' => 'Nintendo DSi', 162 | 'nintendo ds' => 'Nintendo DS', 163 | 'nintendo 3ds' => 'Nintendo 3DS', 164 | 'wii' => 'Nintendo Wii', 165 | 'open web' => 'Open Web', 166 | 'openweb' => 'OpenWeb', 167 | // Operating Systems 168 | 'android' => 'Android', 169 | 'symbian' => 'Symbian', 170 | 'SymbianOS' => 'SymbianOS', 171 | 'elaine' => 'Palm', 172 | 'series60' => 'Symbian S60', 173 | 'windows ce' => 'Windows CE', 174 | // Browsers 175 | 'obigo' => 'Obigo', 176 | 'netfront' => 'Netfront Browser', 177 | 'openwave' => 'Openwave Browser', 178 | 'mobilexplorer' => 'Mobile Explorer', 179 | 'operamini' => 'Opera Mini', 180 | 'opera mini' => 'Opera Mini', 181 | 'opera mobi' => 'Opera Mobile', 182 | 'fennec' => 'Firefox Mobile', 183 | // Other 184 | 'digital paths' => 'Digital Paths', 185 | 'avantgo' => 'AvantGo', 186 | 'xiino' => 'Xiino', 187 | 'novarra' => 'Novarra Transcoder', 188 | 'vodafone' => 'Vodafone', 189 | 'docomo' => 'NTT DoCoMo', 190 | 'o2' => 'O2', 191 | // Fallback 192 | 'mobile' => 'Generic Mobile', 193 | 'wireless' => 'Generic Mobile', 194 | 'j2me' => 'Generic Mobile', 195 | 'midp' => 'Generic Mobile', 196 | 'cldc' => 'Generic Mobile', 197 | 'up.link' => 'Generic Mobile', 198 | 'up.browser' => 'Generic Mobile', 199 | 'smartphone' => 'Generic Mobile', 200 | 'cellphone' => 'Generic Mobile', 201 | ], 202 | 'robots' => [ 203 | 'googlebot' => 'Googlebot', 204 | 'msnbot' => 'MSNBot', 205 | 'baiduspider' => 'Baiduspider', 206 | 'bingbot' => 'Bing', 207 | 'slurp' => 'Inktomi Slurp', 208 | 'yahoo' => 'Yahoo', 209 | 'ask jeeves' => 'Ask Jeeves', 210 | 'fastcrawler' => 'FastCrawler', 211 | 'infoseek' => 'InfoSeek Robot 1.0', 212 | 'lycos' => 'Lycos', 213 | 'yandex' => 'YandexBot', 214 | 'mediapartners-google' => 'MediaPartners Google', 215 | 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler', 216 | 'adsbot-google' => 'AdsBot Google', 217 | 'feedfetcher-google' => 'Feedfetcher Google', 218 | 'curious george' => 'Curious George', 219 | 'ia_archiver' => 'Alexa Crawler', 220 | 'MJ12bot' => 'Majestic-12', 221 | 'Uptimebot' => 'Uptimebot', 222 | ], 223 | ]; 224 | 225 | /** 226 | * UserAgent constructor. 227 | * 228 | * @param string $userAgent User-Agent string 229 | */ 230 | public function __construct(string $userAgent) 231 | { 232 | $this->parse($userAgent); 233 | } 234 | 235 | #[Pure] 236 | public function __toString() : string 237 | { 238 | return $this->toString(); 239 | } 240 | 241 | /** 242 | * @param string $string 243 | * 244 | * @return static 245 | */ 246 | protected function parse(string $string) : static 247 | { 248 | $this->isBrowser = false; 249 | $this->isRobot = false; 250 | $this->isMobile = false; 251 | $this->browser = null; 252 | $this->browserVersion = null; 253 | $this->mobile = null; 254 | $this->robot = null; 255 | $this->agent = $string; 256 | $this->compileData(); 257 | return $this; 258 | } 259 | 260 | protected function compileData() : void 261 | { 262 | $this->setPlatform(); 263 | foreach (['setRobot', 'setBrowser', 'setMobile'] as $function) { 264 | if ($this->{$function}()) { 265 | break; 266 | } 267 | } 268 | } 269 | 270 | protected function setPlatform() : bool 271 | { 272 | foreach (static::$config['platforms'] as $key => $val) { 273 | if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) { 274 | $this->platform = $val; 275 | return true; 276 | } 277 | } 278 | return false; 279 | } 280 | 281 | protected function setBrowser() : bool 282 | { 283 | foreach (static::$config['browsers'] as $key => $val) { 284 | if (\preg_match( 285 | '#' . \preg_quote($key, '#') . '.*?([0-9\.]+)#i', 286 | $this->agent, 287 | $match 288 | )) { 289 | $this->isBrowser = true; 290 | $this->browserVersion = $match[1]; 291 | $this->browser = $val; 292 | $this->setMobile(); 293 | return true; 294 | } 295 | } 296 | return false; 297 | } 298 | 299 | protected function setMobile() : bool 300 | { 301 | foreach (static::$config['mobiles'] as $key => $val) { 302 | if (\stripos($this->agent, $key) !== false) { 303 | $this->isMobile = true; 304 | $this->mobile = $val; 305 | return true; 306 | } 307 | } 308 | return false; 309 | } 310 | 311 | protected function setRobot() : bool 312 | { 313 | foreach (static::$config['robots'] as $key => $val) { 314 | if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) { 315 | $this->isRobot = true; 316 | $this->robot = $val; 317 | $this->setMobile(); 318 | return true; 319 | } 320 | } 321 | return false; 322 | } 323 | 324 | /** 325 | * @return string 326 | * 327 | * @deprecated Use {@see UserAgent::toString()} 328 | * 329 | * @codeCoverageIgnore 330 | */ 331 | #[Deprecated( 332 | reason: 'since HTTP Library version 5.3, use toString() instead', 333 | replacement: '%class%->toString()' 334 | )] 335 | public function getAsString() : string 336 | { 337 | \trigger_error( 338 | 'Method ' . __METHOD__ . ' is deprecated', 339 | \E_USER_DEPRECATED 340 | ); 341 | return $this->toString(); 342 | } 343 | 344 | /** 345 | * Get the User-Agent as string. 346 | * 347 | * @since 5.3 348 | * 349 | * @return string 350 | */ 351 | #[Pure] 352 | public function toString() : string 353 | { 354 | return $this->agent; 355 | } 356 | 357 | /** 358 | * Gets the Browser name. 359 | * 360 | * @return string|null 361 | */ 362 | #[Pure] 363 | public function getBrowser() : ?string 364 | { 365 | return $this->browser; 366 | } 367 | 368 | /** 369 | * Gets the Browser Version. 370 | * 371 | * @return string|null 372 | */ 373 | #[Pure] 374 | public function getBrowserVersion() : ?string 375 | { 376 | return $this->browserVersion; 377 | } 378 | 379 | /** 380 | * Gets the Mobile device. 381 | * 382 | * @return string|null 383 | */ 384 | #[Pure] 385 | public function getMobile() : ?string 386 | { 387 | return $this->mobile; 388 | } 389 | 390 | /** 391 | * Gets the OS Platform. 392 | * 393 | * @return string|null 394 | */ 395 | #[Pure] 396 | public function getPlatform() : ?string 397 | { 398 | return $this->platform; 399 | } 400 | 401 | /** 402 | * Gets the Robot name. 403 | * 404 | * @return string|null 405 | */ 406 | #[Pure] 407 | public function getRobot() : ?string 408 | { 409 | return $this->robot; 410 | } 411 | 412 | /** 413 | * Is Browser. 414 | * 415 | * @param string|null $key 416 | * 417 | * @return bool 418 | */ 419 | #[Pure] 420 | public function isBrowser(string $key = null) : bool 421 | { 422 | if ($key === null || $this->isBrowser === false) { 423 | return $this->isBrowser; 424 | } 425 | $config = static::$config['browsers'] ?? []; 426 | return isset($config[$key]) 427 | && $this->browser === $config[$key]; 428 | } 429 | 430 | /** 431 | * Is Mobile. 432 | * 433 | * @param string|null $key 434 | * 435 | * @return bool 436 | */ 437 | #[Pure] 438 | public function isMobile(string $key = null) : bool 439 | { 440 | if ($key === null || $this->isMobile === false) { 441 | return $this->isMobile; 442 | } 443 | $config = static::$config['mobiles'] ?? []; 444 | return isset($config[$key]) 445 | && $this->mobile === $config[$key]; 446 | } 447 | 448 | /** 449 | * Is Robot. 450 | * 451 | * @param string|null $key 452 | * 453 | * @return bool 454 | */ 455 | #[Pure] 456 | public function isRobot(string $key = null) : bool 457 | { 458 | if ($key === null || $this->isRobot === false) { 459 | return $this->isRobot; 460 | } 461 | $config = static::$config['robots'] ?? []; 462 | return isset($config[$key]) 463 | && $this->robot === $config[$key]; 464 | } 465 | 466 | #[Pure] 467 | public function getType() : string 468 | { 469 | if ($this->isBrowser()) { 470 | return 'Browser'; 471 | } 472 | if ($this->isRobot()) { 473 | return 'Robot'; 474 | } 475 | return 'Unknown'; 476 | } 477 | 478 | #[Pure] 479 | public function getName() : string 480 | { 481 | if ($this->isBrowser()) { 482 | return $this->getBrowser(); 483 | } 484 | if ($this->isRobot()) { 485 | return $this->getRobot(); 486 | } 487 | return 'Unknown'; 488 | } 489 | 490 | #[Pure] 491 | public function jsonSerialize() : string 492 | { 493 | return $this->toString(); 494 | } 495 | } 496 | -------------------------------------------------------------------------------- /src/CSP.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 | namespace Framework\HTTP; 11 | 12 | use InvalidArgumentException; 13 | use LogicException; 14 | 15 | /** 16 | * Class CSP. 17 | * 18 | * @see https://content-security-policy.com/ 19 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP 20 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy 21 | * 22 | * @package http 23 | */ 24 | class CSP implements \Stringable 25 | { 26 | /** 27 | * Restricts the URLs which can be used in a document's `` element. 28 | * 29 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/base-uri 30 | * 31 | * @var string 32 | */ 33 | public const baseUri = 'base-uri'; 34 | /** 35 | * Defines the valid sources for web workers and nested browsing contexts 36 | * loaded using elements such as `` and `