├── 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 |
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 | [](https://github.com/aplus-framework/http/actions/workflows/tests.yml)
10 | [](https://gitlab.com/aplus-framework/libraries/http/-/pipelines?scope=branches)
11 | [](https://aplus-framework.gitlab.io/libraries/http/coverage/)
12 | [](https://packagist.org/packages/aplus/http)
13 | [](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 | = $this->renderRequest() ?>
68 | Response
69 | = $this->renderResponse() ?>
70 | request)) {
77 | return 'A Request instance has not been set on this collector.
';
78 | }
79 | \ob_start(); ?>
80 | IP: = $this->request->getIp() ?>
81 | Protocol: = $this->request->getProtocol() ?>
82 | Method: = $this->request->getMethod() ?>
83 | URL: = $this->request->getUrl() ?>
84 | Server: = $this->request->getServer('SERVER_SOFTWARE') ?>
85 | Hostname: = \gethostname() ?>
86 | = $this->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 | | Type |
107 | Name |
108 | Version |
109 | Platform |
110 | Is Mobile |
111 |
112 |
113 |
114 |
115 | | = \htmlentities($userAgent->getType()) ?> |
116 | = \htmlentities($userAgent->getName()) ?> |
117 | = $userAgent->isBrowser()
118 | ? \htmlentities((string) $userAgent->getBrowserVersion())
119 | : '' ?> |
120 | = \htmlentities((string) $userAgent->getPlatform()) ?> |
121 | = $userAgent->isMobile() ? 'Yes' : 'No' ?> |
122 |
123 |
124 |
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 | | Field |
157 | Value |
158 |
159 |
160 |
161 | request->getParsedBody()) as $field => $value): ?>
162 |
163 | | = \htmlentities($field) ?> |
164 |
165 | = \htmlentities($value) ?>
166 | |
167 |
168 |
169 |
170 |
171 | request->hasFiles()) {
178 | return '';
179 | }
180 | \ob_start(); ?>
181 | Uploaded Files
182 |
183 |
184 |
185 | | Field |
186 | Name |
187 | Full Path |
188 | Type |
189 | Client Type |
190 | Extension |
191 | Size |
192 | Destination |
193 | Error |
194 |
195 |
196 |
197 | request->getFiles()) as $field => $file): ?>
198 |
199 | | = \htmlentities($field) ?> |
200 | = \htmlentities($file->getName()) ?> |
201 | = \htmlentities($file->getFullPath()) ?> |
202 | = \htmlentities($file->getType()) ?> |
203 | = \htmlentities($file->getClientType()) ?> |
204 | = \htmlentities($file->getExtension()) ?> |
205 | = Debugger::convertSize($file->getSize()) ?> |
206 | = $file->getDestination() ?> |
207 | = $file->getError() ?> |
208 | = \htmlentities($file->getErrorMessage()) ?> |
209 |
210 |
211 |
212 |
213 | response)) {
220 | return 'A Response instance has not been set on this collector.
';
221 | }
222 | \ob_start(); ?>
223 | Protocol: = \htmlentities($this->response->getProtocol()) ?>
224 | Status: = \htmlentities($this->response->getStatus()) ?>
225 | Sent: = $this->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: = \round($info['end'] - $info['start'], 6) ?> 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 | | Name |
261 | Value |
262 | Expires |
263 | Path |
264 | Domain |
265 | Is Secure |
266 | Is HTTP Only |
267 | SameSite |
268 |
269 |
270 |
271 | response->getCookies() as $cookie): ?>
272 |
273 | | = \htmlentities($cookie->getName()) ?> |
274 | = \htmlentities($cookie->getValue()) ?> |
275 | = $cookie->getExpires()?->format('D, d-M-Y H:i:s \G\M\T') ?> |
276 | = \htmlentities((string) $cookie->getPath()) ?> |
277 | = \htmlentities((string) $cookie->getDomain()) ?> |
278 | = $cookie->isSecure() ? 'Yes' : 'No' ?> |
279 | = $cookie->isHttpOnly() ? 'Yes' : 'No' ?> |
280 | = \htmlentities((string) $cookie->getSameSite()) ?> |
281 |
282 |
283 |
284 |
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 | | Name |
332 | Value |
333 |
334 |
335 |
336 |
338 |
339 | | = \htmlentities($name) ?> |
340 | = \htmlentities($value) ?> |
341 |
342 |
343 |
344 |
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 `