├── src
├── Debug
│ ├── HTTPCollection.php
│ ├── icons
│ │ └── http.svg
│ └── HTTPCollector.php
├── RequestInterface.php
├── ResponseInterface.php
├── MessageInterface.php
├── Protocol.php
├── Method.php
├── RequestHeader.php
├── ResponseHeader.php
├── AntiCSRF.php
├── Cookie.php
├── HeaderTrait.php
├── ResponseDownload.php
├── URL.php
├── Message.php
├── UserAgent.php
├── CSP.php
└── Response.php
├── README.md
├── LICENSE
├── composer.json
└── .phpstorm.meta.php
/src/Debug/HTTPCollection.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\Collection;
13 |
14 | /**
15 | * Class HTTPCollection.
16 | *
17 | * @package http
18 | */
19 | class HTTPCollection extends Collection
20 | {
21 | protected string $iconPath = __DIR__ . '/icons/http.svg';
22 | }
23 |
--------------------------------------------------------------------------------
/src/Debug/icons/http.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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://coveralls.io/github/aplus-framework/http?branch=master)
11 | [](https://packagist.org/packages/aplus/http)
12 | [](https://aplus-framework.com/sponsor)
13 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | public const string HTTP_1_0 = 'HTTP/1.0';
27 | /**
28 | * @see https://en.wikipedia.org/wiki/HTTP/1.1
29 | */
30 | public const string HTTP_1_1 = 'HTTP/1.1';
31 | /**
32 | * @see https://en.wikipedia.org/wiki/HTTP/2.0
33 | */
34 | public const string HTTP_2_0 = 'HTTP/2.0';
35 | /**
36 | * @see https://en.wikipedia.org/wiki/HTTP/2
37 | */
38 | public const string HTTP_2 = 'HTTP/2';
39 | /**
40 | * @see https://en.wikipedia.org/wiki/HTTP/3
41 | */
42 | public const string HTTP_3 = 'HTTP/3';
43 | /**
44 | * @var array
45 | */
46 | protected static array $protocols = [
47 | 'HTTP/1.0',
48 | 'HTTP/1.1',
49 | 'HTTP/2.0',
50 | 'HTTP/2',
51 | 'HTTP/3',
52 | ];
53 |
54 | /**
55 | * @param string $protocol
56 | *
57 | * @throws InvalidArgumentException for invalid protocol
58 | *
59 | * @return string
60 | */
61 | public static function validate(string $protocol) : string
62 | {
63 | $valid = \strtoupper($protocol);
64 | if (\in_array($valid, static::$protocols, true)) {
65 | return $valid;
66 | }
67 | throw new InvalidArgumentException('Invalid protocol: ' . $protocol);
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/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://github.com/aplus-framework/http/issues",
30 | "forum": "https://aplus-framework.com/forum",
31 | "source": "https://github.com/aplus-framework/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.3",
42 | "ext-fileinfo": "*",
43 | "ext-json": "*",
44 | "aplus/debug": "^4.3",
45 | "aplus/helpers": "^4.0"
46 | },
47 | "require-dev": {
48 | "ext-xdebug": "*",
49 | "aplus/coding-standard": "^2.8",
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": "^10.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 | public const string CONNECT = 'CONNECT';
30 | /**
31 | * The HTTP DELETE request method deletes the specified resource.
32 | *
33 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE
34 | */
35 | public const string DELETE = 'DELETE';
36 | /**
37 | * The HTTP GET method requests a representation of the specified resource.
38 | * Requests using GET should only be used to request data (they shouldn't
39 | * include data).
40 | *
41 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET
42 | */
43 | public const string GET = 'GET';
44 | /**
45 | * The HTTP HEAD method requests the headers that would be returned if the
46 | * HEAD request's URL was instead requested with the HTTP GET method.
47 | *
48 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD
49 | */
50 | public const string HEAD = 'HEAD';
51 | /**
52 | * The HTTP OPTIONS method requests permitted communication options for a
53 | * given URL or server. A client can specify a URL with this method, or an
54 | * asterisk (*) to refer to the entire server.
55 | *
56 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS
57 | */
58 | public const string OPTIONS = 'OPTIONS';
59 | /**
60 | * The HTTP PATCH request method applies partial modifications to a resource.
61 | *
62 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH
63 | */
64 | public const string PATCH = 'PATCH';
65 | /**
66 | * The HTTP POST method sends data to the server. The type of the body of
67 | * the request is indicated by the Content-Type header.
68 | *
69 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
70 | * @see Header::CONTENT_TYPE
71 | */
72 | public const string POST = 'POST';
73 | /**
74 | * The HTTP PUT request method creates a new resource or replaces a
75 | * representation of the target resource with the request payload.
76 | *
77 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT
78 | */
79 | public const string PUT = 'PUT';
80 | /**
81 | * The HTTP TRACE method performs a message loop-back test along the path to
82 | * the target resource, providing a useful debugging mechanism.
83 | *
84 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE
85 | */
86 | public const string TRACE = 'TRACE';
87 | /**
88 | * @var array
89 | */
90 | protected static array $methods = [
91 | 'CONNECT',
92 | 'DELETE',
93 | 'GET',
94 | 'HEAD',
95 | 'OPTIONS',
96 | 'PATCH',
97 | 'POST',
98 | 'PUT',
99 | 'TRACE',
100 | ];
101 |
102 | /**
103 | * @param string $method
104 | *
105 | * @throws InvalidArgumentException for invalid method
106 | *
107 | * @return string
108 | */
109 | public static function validate(string $method) : string
110 | {
111 | $valid = \strtoupper($method);
112 | if (\in_array($valid, static::$methods, true)) {
113 | return $valid;
114 | }
115 | throw new InvalidArgumentException('Invalid request method: ' . $method);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/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
20 | {
21 | use HeaderTrait;
22 | /**
23 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept
24 | */
25 | public const string ACCEPT = 'Accept';
26 | /**
27 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Charset
28 | */
29 | public const string ACCEPT_CHARSET = 'Accept-Charset';
30 | /**
31 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
32 | */
33 | public const string ACCEPT_ENCODING = 'Accept-Encoding';
34 | /**
35 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language
36 | */
37 | public const string ACCEPT_LANGUAGE = 'Accept-Language';
38 | /**
39 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers
40 | */
41 | public const string ACCESS_CONTROL_REQUEST_HEADERS = 'Access-Control-Request-Headers';
42 | /**
43 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Method
44 | */
45 | public const string ACCESS_CONTROL_REQUEST_METHOD = 'Access-Control-Request-Method';
46 | /**
47 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
48 | */
49 | public const string AUTHORIZATION = 'Authorization';
50 | /**
51 | * @see https://datatracker.ietf.org/doc/html/rfc8586
52 | */
53 | public const string CDN_LOOP = 'CDN-Loop';
54 | /**
55 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cookie
56 | */
57 | public const string COOKIE = 'Cookie';
58 | /**
59 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT
60 | */
61 | public const string DNT = 'DNT';
62 | /**
63 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect
64 | */
65 | public const string EXPECT = 'Expect';
66 | /**
67 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Forwarded
68 | */
69 | public const string FORWARDED = 'Forwarded';
70 | /**
71 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/From
72 | */
73 | public const string FROM = 'From';
74 | /**
75 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Host
76 | */
77 | public const string HOST = 'Host';
78 | /**
79 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Match
80 | */
81 | public const string IF_MATCH = 'If-Match';
82 | /**
83 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since
84 | */
85 | public const string IF_MODIFIED_SINCE = 'If-Modified-Since';
86 | /**
87 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-None-Match
88 | */
89 | public const string IF_NONE_MATCH = 'If-None-Match';
90 | /**
91 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Range
92 | */
93 | public const string IF_RANGE = 'If-Range';
94 | /**
95 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Unmodified-Since
96 | */
97 | public const string IF_UNMODIFIED_SINCE = 'If-Unmodified-Since';
98 | /**
99 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin
100 | */
101 | public const string ORIGIN = 'Origin';
102 | /**
103 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Priority
104 | */
105 | public const string PRIORITY = 'Priority';
106 | /**
107 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authorization
108 | */
109 | public const string PROXY_AUTHORIZATION = 'Proxy-Authorization';
110 | /**
111 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
112 | */
113 | public const string RANGE = 'Range';
114 | /**
115 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
116 | */
117 | public const string REFERER = 'Referer';
118 | /**
119 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Dest
120 | */
121 | public const string SEC_FETCH_DEST = 'Sec-Fetch-Dest';
122 | /**
123 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Mode
124 | */
125 | public const string SEC_FETCH_MODE = 'Sec-Fetch-Mode';
126 | /**
127 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-Site
128 | */
129 | public const string SEC_FETCH_SITE = 'Sec-Fetch-Site';
130 | /**
131 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Fetch-User
132 | */
133 | public const string SEC_FETCH_USER = 'Sec-Fetch-User';
134 | /**
135 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/TE
136 | */
137 | public const string TE = 'TE';
138 | /**
139 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade-Insecure-Requests
140 | */
141 | public const string UPGRADE_INSECURE_REQUESTS = 'Upgrade-Insecure-Requests';
142 | /**
143 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent
144 | */
145 | public const string USER_AGENT = 'User-Agent';
146 | /**
147 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For
148 | */
149 | public const string X_FORWARDED_FOR = 'X-Forwarded-For';
150 | /**
151 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Host
152 | */
153 | public const string X_FORWARDED_HOST = 'X-Forwarded-Host';
154 | /**
155 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto
156 | */
157 | public const string X_FORWARDED_PROTO = 'X-Forwarded-Proto';
158 | /**
159 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Real-IP
160 | */
161 | public const string X_REAL_IP = 'X-Real-IP';
162 | /**
163 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Requested-With
164 | */
165 | public const string X_REQUESTED_WITH = 'X-Requested-With';
166 |
167 | /**
168 | * @param array $input
169 | *
170 | * @return array
171 | */
172 | public static function parseInput(array $input) : array
173 | {
174 | $headers = [];
175 | foreach ($input as $name => $value) {
176 | if (\str_starts_with($name, 'HTTP_')) {
177 | $name = \strtr(\substr($name, 5), ['_' => '-']);
178 | $name = static::getName($name);
179 | $headers[$name] = (string) $value;
180 | }
181 | }
182 | return $headers;
183 | }
184 | }
185 |
--------------------------------------------------------------------------------
/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
20 | {
21 | use HeaderTrait;
22 |
23 | /**
24 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Ranges
25 | */
26 | public const string ACCEPT_RANGES = 'Accept-Ranges';
27 | /**
28 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials
29 | */
30 | public const string ACCESS_CONTROL_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
31 | /**
32 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers
33 | */
34 | public const string ACCESS_CONTROL_ALLOW_HEADERS = 'Access-Control-Allow-Headers';
35 | /**
36 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods
37 | */
38 | public const string ACCESS_CONTROL_ALLOW_METHODS = 'Access-Control-Allow-Methods';
39 | /**
40 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin
41 | */
42 | public const string ACCESS_CONTROL_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
43 | /**
44 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers
45 | */
46 | public const string ACCESS_CONTROL_EXPOSE_HEADERS = 'Access-Control-Expose-Headers';
47 | /**
48 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age
49 | */
50 | public const string ACCESS_CONTROL_MAX_AGE = 'Access-Control-Max-Age';
51 | /**
52 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Age
53 | */
54 | public const string AGE = 'Age';
55 | /**
56 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Allow
57 | */
58 | public const string ALLOW = 'Allow';
59 | /**
60 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
61 | */
62 | public const string CLEAR_SITE_DATA = 'Clear-Site-Data';
63 | /**
64 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
65 | */
66 | public const string CONTENT_SECURITY_POLICY = 'Content-Security-Policy';
67 | /**
68 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only
69 | */
70 | public const string CONTENT_SECURITY_POLICY_REPORT_ONLY = 'Content-Security-Policy-Report-Only';
71 | /**
72 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag
73 | */
74 | public const string ETAG = 'ETag';
75 | /**
76 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
77 | */
78 | public const string EXPECT_CT = 'Expect-CT';
79 | /**
80 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires
81 | */
82 | public const string EXPIRES = 'Expires';
83 | /**
84 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Feature-Policy
85 | */
86 | public const string FEATURE_POLICY = 'Feature-Policy';
87 | /**
88 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified
89 | */
90 | public const string LAST_MODIFIED = 'Last-Modified';
91 | /**
92 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location
93 | */
94 | public const string LOCATION = 'Location';
95 | /**
96 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Proxy-Authenticate
97 | */
98 | public const string PROXY_AUTHENTICATE = 'Proxy-Authenticate';
99 | /**
100 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Public-Key-Pins
101 | */
102 | public const string PUBLIC_KEY_PINS = 'Public-Key-Pins';
103 | /**
104 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Public-Key-Pins-Report-Only
105 | */
106 | public const string PUBLIC_KEY_PINS_REPORT_ONLY = 'Public-Key-Pins-Report-Only';
107 | /**
108 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
109 | */
110 | public const string REFERRER_POLICY = 'Referrer-Policy';
111 | /**
112 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
113 | */
114 | public const string RETRY_AFTER = 'Retry-After';
115 | /**
116 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server
117 | */
118 | public const string SERVER = 'Server';
119 | /**
120 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
121 | */
122 | public const string SET_COOKIE = 'Set-Cookie';
123 | /**
124 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/SourceMap
125 | */
126 | public const string SOURCEMAP = 'SourceMap';
127 | /**
128 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
129 | */
130 | public const string STRICT_TRANSPORT_SECURITY = 'Strict-Transport-Security';
131 | /**
132 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Timing-Allow-Origin
133 | */
134 | public const string TIMING_ALLOW_ORIGIN = 'Timing-Allow-Origin';
135 | /**
136 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Tk
137 | */
138 | public const string TK = 'Tk';
139 | /**
140 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
141 | */
142 | public const string VARY = 'Vary';
143 | /**
144 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
145 | */
146 | public const string WWW_AUTHENTICATE = 'WWW-Authenticate';
147 | /**
148 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
149 | */
150 | public const string X_CONTENT_TYPE_OPTIONS = 'X-Content-Type-Options';
151 | /**
152 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
153 | */
154 | public const string X_DNS_PREFETCH_CONTROL = 'X-DNS-Prefetch-Control';
155 | /**
156 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
157 | */
158 | public const string X_FRAME_OPTIONS = 'X-Frame-Options';
159 | /**
160 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Robots-Tag
161 | * @see https://developers.google.com/search/docs/crawling-indexing/robots-meta-tag
162 | */
163 | public const string X_ROBOTS_TAG = 'X-Robots-Tag';
164 | /**
165 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
166 | */
167 | public const string X_XSS_PROTECTION = 'X-XSS-Protection';
168 | // -------------------------------------------------------------------------
169 | // Custom
170 | // -------------------------------------------------------------------------
171 | /**
172 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Powered-By
173 | */
174 | public const string X_POWERED_BY = 'X-Powered-By';
175 | }
176 |
--------------------------------------------------------------------------------
/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 InvalidArgumentException;
13 | use JetBrains\PhpStorm\Pure;
14 | use LogicException;
15 |
16 | /**
17 | * Class AntiCSRF.
18 | *
19 | * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#synchronizer-token-pattern
20 | * @see https://stackoverflow.com/q/6287903/6027968
21 | * @see https://portswigger.net/web-security/csrf
22 | * @see https://www.netsparker.com/blog/web-security/protecting-website-using-anti-csrf-token/
23 | *
24 | * @package http
25 | */
26 | class AntiCSRF
27 | {
28 | protected string $tokenName = 'csrf_token';
29 | protected Request $request;
30 | protected bool $verified = false;
31 | protected bool $enabled = true;
32 | protected int $tokenBytesLength = 8;
33 | protected string $generateTokenFunction = 'base64_encode';
34 | /**
35 | * @var array
36 | */
37 | protected array $generateTokenFunctions = [
38 | 'base64_encode',
39 | 'bin2hex',
40 | 'md5',
41 | ];
42 |
43 | /**
44 | * AntiCSRF constructor.
45 | *
46 | * @param Request $request
47 | * @param int|null $tokenBytesLength
48 | * @param string|null $generateTokenFunction
49 | */
50 | public function __construct(
51 | Request $request,
52 | ?int $tokenBytesLength = null,
53 | ?string $generateTokenFunction = null,
54 | ) {
55 | if (\session_status() !== \PHP_SESSION_ACTIVE) {
56 | throw new LogicException('Session must be active to use AntiCSRF class');
57 | }
58 | $this->request = $request;
59 | if ($tokenBytesLength !== null) {
60 | $this->setTokenBytesLength($tokenBytesLength);
61 | }
62 | if ($generateTokenFunction !== null) {
63 | $this->setGenerateTokenFunction($generateTokenFunction);
64 | }
65 | if ($this->getToken() === null) {
66 | $this->setToken();
67 | }
68 | }
69 |
70 | public function setTokenBytesLength(int $length) : static
71 | {
72 | if ($length < 3) {
73 | throw new InvalidArgumentException(
74 | 'AntiCSRF token bytes length must be greater than 2, ' . $length . ' given'
75 | );
76 | }
77 | $this->tokenBytesLength = $length;
78 | return $this;
79 | }
80 |
81 | #[Pure]
82 | public function getTokenBytesLength() : int
83 | {
84 | return $this->tokenBytesLength;
85 | }
86 |
87 | /**
88 | * Gets the anti-csrf token name.
89 | *
90 | * @return string
91 | */
92 | #[Pure]
93 | public function getTokenName() : string
94 | {
95 | return $this->tokenName;
96 | }
97 |
98 | /**
99 | * Sets the anti-csrf token name.
100 | *
101 | * @param string $tokenName
102 | *
103 | * @return static
104 | */
105 | public function setTokenName(string $tokenName) : static
106 | {
107 | $this->tokenName = \htmlspecialchars($tokenName, \ENT_QUOTES | \ENT_HTML5);
108 | return $this;
109 | }
110 |
111 | /**
112 | * Gets the anti-csrf token from the session.
113 | *
114 | * @return string|null
115 | */
116 | #[Pure]
117 | public function getToken() : ?string
118 | {
119 | return $_SESSION['$']['csrf_token'] ?? null;
120 | }
121 |
122 | /**
123 | * Sets the anti-csrf token into the session.
124 | *
125 | * @param string|null $token A custom anti-csrf token or null to generate one
126 | *
127 | * @return static
128 | */
129 | public function setToken(?string $token = null) : static
130 | {
131 | $_SESSION['$']['csrf_token'] = $token ?? $this->generateToken();
132 | return $this;
133 | }
134 |
135 | public function setGenerateTokenFunction(string $function) : static
136 | {
137 | if (!\in_array($function, $this->generateTokenFunctions, true)) {
138 | throw new InvalidArgumentException(
139 | 'Invalid generate token function name: ' . $function
140 | );
141 | }
142 | $this->generateTokenFunction = $function;
143 | return $this;
144 | }
145 |
146 | #[Pure]
147 | public function getGenerateTokenFunction() : string
148 | {
149 | return $this->generateTokenFunction;
150 | }
151 |
152 | public function generateToken() : string
153 | {
154 | $bytes = \random_bytes($this->getTokenBytesLength()); // @phpstan-ignore-line
155 | return $this->getGenerateTokenFunction()($bytes); // @phpstan-ignore-line
156 | }
157 |
158 | /**
159 | * Gets the user token from the request input form.
160 | *
161 | * @return string|null
162 | */
163 | public function getUserToken() : ?string
164 | {
165 | $token = $this->request->getParsedBody($this->getTokenName());
166 | return \is_string($token) ? $token : null;
167 | }
168 |
169 | /**
170 | * Verifies the request input token, if the verification is enabled.
171 | * The verification always succeed on HTTP GET, HEAD and OPTIONS methods.
172 | * If verification is successful with other HTTP methods, a new token is
173 | * generated.
174 | *
175 | * @return bool
176 | */
177 | public function verify() : bool
178 | {
179 | if ($this->isEnabled() === false) {
180 | return true;
181 | }
182 | if ($this->isSafeMethod()) {
183 | return true;
184 | }
185 | if ($this->getUserToken() === null) {
186 | return false;
187 | }
188 | if (!$this->validate($this->getUserToken())) {
189 | return false;
190 | }
191 | if (!$this->isVerified()) {
192 | $this->setToken();
193 | $this->setVerified();
194 | }
195 | return true;
196 | }
197 |
198 | /**
199 | * Safe HTTP Request methods are: GET, HEAD and OPTIONS.
200 | *
201 | * @return bool
202 | */
203 | #[Pure]
204 | public function isSafeMethod() : bool
205 | {
206 | return \in_array($this->request->getMethod(), [
207 | Method::GET,
208 | Method::HEAD,
209 | Method::OPTIONS,
210 | ], true);
211 | }
212 |
213 | /**
214 | * Validates if a user token is equals the session token.
215 | *
216 | * This method can be used to validate tokens not received through forms.
217 | * For example: Through a request header, JSON, etc.
218 | *
219 | * @param string $userToken
220 | *
221 | * @return bool
222 | */
223 | public function validate(string $userToken) : bool
224 | {
225 | return \hash_equals($_SESSION['$']['csrf_token'], $userToken);
226 | }
227 |
228 | #[Pure]
229 | protected function isVerified() : bool
230 | {
231 | return $this->verified;
232 | }
233 |
234 | /**
235 | * @param bool $status
236 | *
237 | * @return static
238 | */
239 | protected function setVerified(bool $status = true) : static
240 | {
241 | $this->verified = $status;
242 | return $this;
243 | }
244 |
245 | /**
246 | * Gets the HTML form hidden input if the verification is enabled.
247 | *
248 | * @return string
249 | */
250 | #[Pure]
251 | public function input() : string
252 | {
253 | if ($this->isEnabled() === false) {
254 | return '';
255 | }
256 | return ' ';
259 | }
260 |
261 | /**
262 | * Tells if the verification is enabled.
263 | *
264 | * @see AntiCSRF::verify()
265 | *
266 | * @return bool
267 | */
268 | #[Pure]
269 | public function isEnabled() : bool
270 | {
271 | return $this->enabled;
272 | }
273 |
274 | /**
275 | * Enables the Anti CSRF verification.
276 | *
277 | * @see AntiCSRF::verify()
278 | *
279 | * @return static
280 | */
281 | public function enable() : static
282 | {
283 | $this->enabled = true;
284 | return $this;
285 | }
286 |
287 | /**
288 | * Disables the Anti CSRF verification.
289 | *
290 | * @see AntiCSRF::verify()
291 | *
292 | * @return static
293 | */
294 | public function disable() : static
295 | {
296 | $this->enabled = false;
297 | return $this;
298 | }
299 | }
300 |
--------------------------------------------------------------------------------
/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\Pure;
18 |
19 | /**
20 | * Class Cookie.
21 | *
22 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
23 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies
24 | * @see https://datatracker.ietf.org/doc/html/rfc6265
25 | * @see https://www.php.net/manual/en/function.setcookie.php
26 | *
27 | * @package http
28 | */
29 | class Cookie implements \Stringable
30 | {
31 | protected ?string $domain = null;
32 | protected ?DateTime $expires = null;
33 | protected bool $httpOnly = false;
34 | protected string $name;
35 | protected ?string $path = null;
36 | protected ?string $sameSite = null;
37 | protected bool $secure = false;
38 | protected string $value;
39 |
40 | /**
41 | * Cookie constructor.
42 | *
43 | * @param string $name
44 | * @param string $value
45 | */
46 | public function __construct(string $name, string $value)
47 | {
48 | $this->setName($name);
49 | $this->setValue($value);
50 | }
51 |
52 | public function __toString() : string
53 | {
54 | return $this->toString();
55 | }
56 |
57 | /**
58 | * @since 5.3
59 | *
60 | * @return string
61 | */
62 | public function toString() : string
63 | {
64 | $string = $this->getName() . '=' . $this->getValue();
65 | $part = $this->getExpires();
66 | if ($part !== null) {
67 | $string .= '; expires=' . $this->expires->format(DateTimeInterface::RFC7231);
68 | $string .= '; Max-Age=' . $this->expires->diff(new DateTime('-1 second'))->s;
69 | }
70 | $part = $this->getPath();
71 | if ($part !== null) {
72 | $string .= '; path=' . $part;
73 | }
74 | $part = $this->getDomain();
75 | if ($part !== null) {
76 | $string .= '; domain=' . $part;
77 | }
78 | $part = $this->isSecure();
79 | if ($part) {
80 | $string .= '; secure';
81 | }
82 | $part = $this->isHttpOnly();
83 | if ($part) {
84 | $string .= '; HttpOnly';
85 | }
86 | $part = $this->getSameSite();
87 | if ($part !== null) {
88 | $string .= '; SameSite=' . $part;
89 | }
90 | return $string;
91 | }
92 |
93 | /**
94 | * @return string|null
95 | */
96 | #[Pure]
97 | public function getDomain() : ?string
98 | {
99 | return $this->domain;
100 | }
101 |
102 | /**
103 | * @param string|null $domain
104 | *
105 | * @return static
106 | */
107 | public function setDomain(?string $domain) : static
108 | {
109 | $this->domain = $domain;
110 | return $this;
111 | }
112 |
113 | /**
114 | * @return DateTime|null
115 | */
116 | #[Pure]
117 | public function getExpires() : ?DateTime
118 | {
119 | return $this->expires;
120 | }
121 |
122 | /**
123 | * @param DateTime|int|string|null $expires
124 | *
125 | * @throws Exception if can not create from format
126 | *
127 | * @return static
128 | */
129 | public function setExpires(DateTime | int | string | null $expires) : static
130 | {
131 | if ($expires instanceof DateTime) {
132 | $expires = clone $expires;
133 | $expires->setTimezone(new DateTimeZone('UTC'));
134 | } elseif (\is_numeric($expires)) {
135 | $expires = DateTime::createFromFormat('U', (string) $expires, new DateTimeZone('UTC'));
136 | } elseif ($expires !== null) {
137 | $expires = new DateTime($expires, new DateTimeZone('UTC'));
138 | }
139 | $this->expires = $expires; // @phpstan-ignore-line
140 | return $this;
141 | }
142 |
143 | /**
144 | * @return bool
145 | */
146 | public function isExpired() : bool
147 | {
148 | return $this->getExpires() && \time() > $this->getExpires()->getTimestamp();
149 | }
150 |
151 | /**
152 | * @return string
153 | */
154 | #[Pure]
155 | public function getName() : string
156 | {
157 | return $this->name;
158 | }
159 |
160 | /**
161 | * @param string $name
162 | *
163 | * @return static
164 | */
165 | protected function setName(string $name) : static
166 | {
167 | $this->name = $name;
168 | return $this;
169 | }
170 |
171 | /**
172 | * @return string|null
173 | */
174 | #[Pure]
175 | public function getPath() : ?string
176 | {
177 | return $this->path;
178 | }
179 |
180 | /**
181 | * @param string|null $path
182 | *
183 | * @return static
184 | */
185 | public function setPath(?string $path) : static
186 | {
187 | $this->path = $path;
188 | return $this;
189 | }
190 |
191 | /**
192 | * @return string|null
193 | */
194 | #[Pure]
195 | public function getSameSite() : ?string
196 | {
197 | return $this->sameSite;
198 | }
199 |
200 | /**
201 | * @param string|null $sameSite Strict, Lax, Unset or None
202 | *
203 | * @throws InvalidArgumentException for invalid $sameSite value
204 | *
205 | * @return static
206 | */
207 | public function setSameSite(?string $sameSite) : static
208 | {
209 | if ($sameSite !== null) {
210 | $sameSite = \ucfirst(\strtolower($sameSite));
211 | if (!\in_array($sameSite, ['Strict', 'Lax', 'Unset', 'None'])) {
212 | throw new InvalidArgumentException('SameSite must be Strict, Lax, Unset or None');
213 | }
214 | }
215 | $this->sameSite = $sameSite;
216 | return $this;
217 | }
218 |
219 | /**
220 | * @return string
221 | */
222 | #[Pure]
223 | public function getValue() : string
224 | {
225 | return $this->value;
226 | }
227 |
228 | /**
229 | * @param string $value
230 | *
231 | * @return static
232 | */
233 | public function setValue(string $value) : static
234 | {
235 | $this->value = $value;
236 | return $this;
237 | }
238 |
239 | /**
240 | * @param bool $httpOnly
241 | *
242 | * @return static
243 | */
244 | public function setHttpOnly(bool $httpOnly = true) : static
245 | {
246 | $this->httpOnly = $httpOnly;
247 | return $this;
248 | }
249 |
250 | /**
251 | * @return bool
252 | */
253 | #[Pure]
254 | public function isHttpOnly() : bool
255 | {
256 | return $this->httpOnly;
257 | }
258 |
259 | /**
260 | * @param bool $secure
261 | *
262 | * @return static
263 | */
264 | public function setSecure(bool $secure = true) : static
265 | {
266 | $this->secure = $secure;
267 | return $this;
268 | }
269 |
270 | /**
271 | * @return bool
272 | */
273 | #[Pure]
274 | public function isSecure() : bool
275 | {
276 | return $this->secure;
277 | }
278 |
279 | /**
280 | * @return bool
281 | */
282 | public function send() : bool
283 | {
284 | $options = [];
285 | $value = $this->getExpires();
286 | if ($value) {
287 | $options['expires'] = $value->getTimestamp();
288 | }
289 | $value = $this->getPath();
290 | if ($value !== null) {
291 | $options['path'] = $value;
292 | }
293 | $value = $this->getDomain();
294 | if ($value !== null) {
295 | $options['domain'] = $value;
296 | }
297 | $options['secure'] = $this->isSecure();
298 | $options['httponly'] = $this->isHttpOnly();
299 | $value = $this->getSameSite();
300 | if ($value !== null) {
301 | $options['samesite'] = $value;
302 | }
303 | // @phpstan-ignore-next-line
304 | return \setcookie($this->getName(), $this->getValue(), $options);
305 | }
306 |
307 | /**
308 | * Parses a Set-Cookie Header line and creates a new Cookie object.
309 | *
310 | * @param string $line
311 | *
312 | * @throws Exception if setExpires fail
313 | *
314 | * @return Cookie|null
315 | */
316 | public static function parse(string $line) : ?Cookie
317 | {
318 | $parts = \array_map('\trim', \explode(';', $line, 20));
319 | $cookie = null;
320 | foreach ($parts as $key => $part) {
321 | [$arg, $val] = static::makeArgumentValue($part);
322 | if ($key === 0) {
323 | if (isset($arg, $val)) {
324 | $cookie = new Cookie($arg, $val);
325 | continue;
326 | }
327 | break;
328 | }
329 | if ($arg === null) {
330 | continue;
331 | }
332 | switch (\strtolower($arg)) {
333 | case 'expires':
334 | $cookie->setExpires($val);
335 | break;
336 | case 'domain':
337 | $cookie->setDomain($val);
338 | break;
339 | case 'path':
340 | $cookie->setPath($val);
341 | break;
342 | case 'httponly':
343 | $cookie->setHttpOnly();
344 | break;
345 | case 'secure':
346 | $cookie->setSecure();
347 | break;
348 | case 'samesite':
349 | $cookie->setSameSite($val);
350 | break;
351 | }
352 | }
353 | return $cookie;
354 | }
355 |
356 | /**
357 | * Create Cookie objects from a Cookie Header line.
358 | *
359 | * @param string $line
360 | *
361 | * @return array
362 | */
363 | public static function create(string $line) : array
364 | {
365 | $items = \array_map('\trim', \explode(';', $line, 3000));
366 | $cookies = [];
367 | foreach ($items as $item) {
368 | [$name, $value] = static::makeArgumentValue($item);
369 | if (isset($name, $value)) {
370 | $cookies[$name] = new Cookie($name, $value);
371 | }
372 | }
373 | return $cookies;
374 | }
375 |
376 | /**
377 | * @param string $part
378 | *
379 | * @return array
380 | */
381 | protected static function makeArgumentValue(string $part) : array
382 | {
383 | $part = \array_pad(\explode('=', $part, 2), 2, null);
384 | if ($part[0] !== null) {
385 | $part[0] = static::trimmedOrNull($part[0]);
386 | }
387 | if ($part[1] !== null) {
388 | $part[1] = static::trimmedOrNull($part[1]);
389 | }
390 | return $part;
391 | }
392 |
393 | protected static function trimmedOrNull(string $value) : ?string
394 | {
395 | $value = \trim($value);
396 | if ($value === '') {
397 | $value = null;
398 | }
399 | return $value;
400 | }
401 | }
402 |
--------------------------------------------------------------------------------
/src/HeaderTrait.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 | * Trait HeaderTrait.
14 | *
15 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
16 | *
17 | * @package http
18 | */
19 | trait HeaderTrait
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 | public const string CACHE_CONTROL = 'Cache-Control';
28 | /**
29 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection
30 | */
31 | public const string CONNECTION = 'Connection';
32 | /**
33 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
34 | */
35 | public const string CONTENT_DISPOSITION = 'Content-Disposition';
36 | /**
37 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
38 | */
39 | public const string DATE = 'Date';
40 | /**
41 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive
42 | */
43 | public const string KEEP_ALIVE = 'Keep-Alive';
44 | /**
45 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pragma
46 | */
47 | public const string PRAGMA = 'Pragma';
48 | /**
49 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Via
50 | */
51 | public const string VIA = 'Via';
52 | /**
53 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Warning
54 | */
55 | public const string WARNING = 'Warning';
56 | // -------------------------------------------------------------------------
57 | // Representation headers (Request and Response)
58 | // -------------------------------------------------------------------------
59 | /**
60 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
61 | */
62 | public const string CONTENT_ENCODING = 'Content-Encoding';
63 | /**
64 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language
65 | */
66 | public const string CONTENT_LANGUAGE = 'Content-Language';
67 | /**
68 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Location
69 | */
70 | public const string CONTENT_LOCATION = 'Content-Location';
71 | /**
72 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
73 | */
74 | public const string CONTENT_TYPE = 'Content-Type';
75 | // -------------------------------------------------------------------------
76 | // Payload headers (Request and Response)
77 | // -------------------------------------------------------------------------
78 | /**
79 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length
80 | */
81 | public const string CONTENT_LENGTH = 'Content-Length';
82 | /**
83 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Range
84 | */
85 | public const string CONTENT_RANGE = 'Content-Range';
86 | /**
87 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
88 | */
89 | public const string LINK = 'Link';
90 | /**
91 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer
92 | */
93 | public const string TRAILER = 'Trailer';
94 | /**
95 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding
96 | */
97 | public const string TRANSFER_ENCODING = 'Transfer-Encoding';
98 | /**
99 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Upgrade
100 | */
101 | public const string UPGRADE = 'Upgrade';
102 | // -------------------------------------------------------------------------
103 | // Custom
104 | // -------------------------------------------------------------------------
105 | /**
106 | * @see https://riptutorial.com/http-headers/topic/10581/x-request-id
107 | */
108 | public const string X_REQUEST_ID = 'X-Request-ID';
109 | /**
110 | * Header names.
111 | *
112 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
113 | *
114 | * @var array
115 | */
116 | protected static array $headers = [
117 | // ---------------------------------------------------------------------
118 | // General headers (Request and Response)
119 | // ---------------------------------------------------------------------
120 | 'cache-control' => 'Cache-Control',
121 | 'connection' => 'Connection',
122 | 'content-disposition' => 'Content-Disposition',
123 | 'date' => 'Date',
124 | 'keep-alive' => 'Keep-Alive',
125 | 'link' => 'Link',
126 | 'pragma' => 'Pragma',
127 | 'via' => 'Via',
128 | 'warning' => 'Warning',
129 | // ---------------------------------------------------------------------
130 | // Representation headers (Request and Response)
131 | // ---------------------------------------------------------------------
132 | 'content-encoding' => 'Content-Encoding',
133 | 'content-language' => 'Content-Language',
134 | 'content-location' => 'Content-Location',
135 | 'content-type' => 'Content-Type',
136 | // ---------------------------------------------------------------------
137 | // Payload headers (Request and Response)
138 | // ---------------------------------------------------------------------
139 | 'content-length' => 'Content-Length',
140 | 'content-range' => 'Content-Range',
141 | 'trailer' => 'Trailer',
142 | 'transfer-encoding' => 'Transfer-Encoding',
143 | // ---------------------------------------------------------------------
144 | // Request headers
145 | // ---------------------------------------------------------------------
146 | 'accept' => 'Accept',
147 | 'accept-charset' => 'Accept-Charset',
148 | 'accept-encoding' => 'Accept-Encoding',
149 | 'accept-language' => 'Accept-Language',
150 | 'access-control-request-headers' => 'Access-Control-Request-Headers',
151 | 'access-control-request-method' => 'Access-Control-Request-Method',
152 | 'authorization' => 'Authorization',
153 | 'cdn-loop' => 'CDN-Loop',
154 | 'cookie' => 'Cookie',
155 | 'dnt' => 'DNT',
156 | 'expect' => 'Expect',
157 | 'forwarded' => 'Forwarded',
158 | 'from' => 'From',
159 | 'host' => 'Host',
160 | 'if-match' => 'If-Match',
161 | 'if-modified-since' => 'If-Modified-Since',
162 | 'if-none-match' => 'If-None-Match',
163 | 'if-range' => 'If-Range',
164 | 'if-unmodified-since' => 'If-Unmodified-Since',
165 | 'origin' => 'Origin',
166 | 'priority' => 'Priority',
167 | 'proxy-authorization' => 'Proxy-Authorization',
168 | 'range' => 'Range',
169 | 'referer' => 'Referer',
170 | 'sec-fetch-dest' => 'Sec-Fetch-Dest',
171 | 'sec-fetch-mode' => 'Sec-Fetch-Mode',
172 | 'sec-fetch-site' => 'Sec-Fetch-Site',
173 | 'sec-fetch-user' => 'Sec-Fetch-User',
174 | 'te' => 'TE',
175 | 'upgrade-insecure-requests' => 'Upgrade-Insecure-Requests',
176 | 'user-agent' => 'User-Agent',
177 | 'x-forwarded-for' => 'X-Forwarded-For',
178 | 'x-forwarded-host' => 'X-Forwarded-Host',
179 | 'x-forwarded-proto' => 'X-Forwarded-Proto',
180 | 'x-real-ip' => 'X-Real-IP',
181 | 'x-requested-with' => 'X-Requested-With',
182 | // ---------------------------------------------------------------------
183 | // Response headers
184 | // ---------------------------------------------------------------------
185 | 'accept-ranges' => 'Accept-Ranges',
186 | 'access-control-allow-credentials' => 'Access-Control-Allow-Credentials',
187 | 'access-control-allow-headers' => 'Access-Control-Allow-Headers',
188 | 'access-control-allow-methods' => 'Access-Control-Allow-Methods',
189 | 'access-control-allow-origin' => 'Access-Control-Allow-Origin',
190 | 'access-control-expose-headers' => 'Access-Control-Expose-Headers',
191 | 'access-control-max-age' => 'Access-Control-Max-Age',
192 | 'age' => 'Age',
193 | 'allow' => 'Allow',
194 | 'clear-site-data' => 'Clear-Site-Data',
195 | 'content-security-policy' => 'Content-Security-Policy',
196 | 'content-security-policy-report-only' => 'Content-Security-Policy-Report-Only',
197 | 'etag' => 'ETag',
198 | 'expect-ct' => 'Expect-CT',
199 | 'expires' => 'Expires',
200 | 'feature-policy' => 'Feature-Policy',
201 | 'last-modified' => 'Last-Modified',
202 | 'location' => 'Location',
203 | 'proxy-authenticate' => 'Proxy-Authenticate',
204 | 'public-key-pins' => 'Public-Key-Pins',
205 | 'public-key-pins-report-only' => 'Public-Key-Pins-Report-Only',
206 | 'referrer-policy' => 'Referrer-Policy',
207 | 'retry-after' => 'Retry-After',
208 | 'server' => 'Server',
209 | 'set-cookie' => 'Set-Cookie',
210 | 'sourcemap' => 'SourceMap',
211 | 'strict-transport-security' => 'Strict-Transport-Security',
212 | 'timing-allow-origin' => 'Timing-Allow-Origin',
213 | 'tk' => 'Tk',
214 | 'vary' => 'Vary',
215 | 'www-authenticate' => 'WWW-Authenticate',
216 | 'x-content-type-options' => 'X-Content-Type-Options',
217 | 'x-dns-prefetch-control' => 'X-DNS-Prefetch-Control',
218 | 'x-frame-options' => 'X-Frame-Options',
219 | 'x-robots-tag' => 'X-Robots-Tag',
220 | 'x-xss-protection' => 'X-XSS-Protection',
221 | // ---------------------------------------------------------------------
222 | // Custom (Response)
223 | // ---------------------------------------------------------------------
224 | 'x-request-id' => 'X-Request-ID',
225 | 'x-powered-by' => 'X-Powered-By',
226 | // ---------------------------------------------------------------------
227 | // WebSocket
228 | // ---------------------------------------------------------------------
229 | 'sec-websocket-extensions' => 'Sec-WebSocket-Extensions',
230 | 'sec-websocket-key' => 'Sec-WebSocket-Key',
231 | 'sec-websocket-protocol' => 'Sec-WebSocket-Protocol',
232 | 'sec-websocket-version' => 'Sec-WebSocket-Version',
233 | ];
234 |
235 | public static function getName(string $name) : string
236 | {
237 | return static::$headers[\strtolower($name)] ?? $name;
238 | }
239 |
240 | public static function setName(string $name) : void
241 | {
242 | static::$headers[\strtolower($name)] = $name;
243 | }
244 |
245 | /**
246 | * @return array
247 | */
248 | public static function getMultilines() : array
249 | {
250 | return [
251 | 'cdn-loop',
252 | 'date',
253 | 'expires',
254 | 'if-modified-since',
255 | 'if-range',
256 | 'if-unmodified-since',
257 | 'last-modified',
258 | 'proxy-authenticate',
259 | 'retry-after',
260 | 'set-cookie',
261 | 'x-robots-tag',
262 | 'www-authenticate',
263 | ];
264 | }
265 |
266 | public static function isMultiline(string $name) : bool
267 | {
268 | return \in_array(\strtolower($name), static::getMultilines(), true);
269 | }
270 | }
271 |
--------------------------------------------------------------------------------
/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 | ResponseHeader::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(ResponseHeader::CONTENT_LENGTH, (string) $this->filesize);
106 | $this->setHeader(
107 | ResponseHeader::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(ResponseHeader::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 | ResponseHeader::CONTENT_LENGTH,
217 | (string) ($lastByte - $firstByte + 1)
218 | );
219 | $this->setHeader(
220 | ResponseHeader::CONTENT_TYPE,
221 | \mime_content_type($this->filepath) ?: 'application/octet-stream'
222 | );
223 | $this->setHeader(
224 | ResponseHeader::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(ResponseHeader::CONTENT_LENGTH, (string) $length);
252 | $this->setHeader(
253 | ResponseHeader::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/URL.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\ArrayShape;
14 | use JetBrains\PhpStorm\Pure;
15 | use RuntimeException;
16 |
17 | /**
18 | * Class URL.
19 | *
20 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Identifying_resources_on_the_Web#urls
21 | * @see https://developer.mozilla.org/en-US/docs/Web/API/URL
22 | * @see https://datatracker.ietf.org/doc/html/rfc3986#section-3
23 | *
24 | * @package http
25 | */
26 | class URL implements \JsonSerializable, \Stringable
27 | {
28 | /**
29 | * The #fragment (id).
30 | */
31 | protected ?string $fragment = null;
32 | protected ?string $hostname = null;
33 | protected ?string $pass = null;
34 | /**
35 | * The /paths/of/url.
36 | *
37 | * @var array
38 | */
39 | protected array $pathSegments = [];
40 | protected ?int $port = null;
41 | /**
42 | * The ?queries.
43 | *
44 | * @var array
45 | */
46 | protected array $queryData = [];
47 | protected ?string $scheme = null;
48 | protected ?string $user = null;
49 |
50 | /**
51 | * URL constructor.
52 | *
53 | * @param string $url An absolute URL
54 | */
55 | public function __construct(string $url)
56 | {
57 | $this->setUrl($url);
58 | }
59 |
60 | /**
61 | * @return string
62 | */
63 | public function __toString() : string
64 | {
65 | return $this->toString();
66 | }
67 |
68 | /**
69 | * @param string $query
70 | * @param int|string|null $value
71 | *
72 | * @return static
73 | */
74 | public function addQuery(string $query, int | string | null $value = null) : static
75 | {
76 | $this->queryData[$query] = $value;
77 | return $this;
78 | }
79 |
80 | /**
81 | * @param array $queries
82 | *
83 | * @return static
84 | */
85 | public function addQueries(array $queries) : static
86 | {
87 | foreach ($queries as $name => $value) {
88 | $this->addQuery($name, $value);
89 | }
90 | return $this;
91 | }
92 |
93 | /**
94 | * @param array $allowed
95 | *
96 | * @return array
97 | */
98 | #[Pure]
99 | protected function filterQuery(array $allowed) : array
100 | {
101 | return $this->queryData ?
102 | \array_intersect_key($this->queryData, \array_flip($allowed))
103 | : [];
104 | }
105 |
106 | #[Pure]
107 | public function getBaseUrl(string $path = '/') : string
108 | {
109 | if ($path && $path !== '/') {
110 | $path = '/' . \ltrim($path, '/');
111 | }
112 | return $this->getOrigin() . $path;
113 | }
114 |
115 | /**
116 | * @return string|null
117 | */
118 | public function getFragment() : ?string
119 | {
120 | return $this->fragment;
121 | }
122 |
123 | /**
124 | * @return string|null
125 | */
126 | #[Pure]
127 | public function getHost() : ?string
128 | {
129 | return $this->hostname === null ? null : $this->hostname . $this->getPortPart();
130 | }
131 |
132 | #[Pure]
133 | public function getHostname() : ?string
134 | {
135 | return $this->hostname;
136 | }
137 |
138 | #[Pure]
139 | public function getOrigin() : string
140 | {
141 | return $this->getScheme() . '://' . $this->getHost();
142 | }
143 |
144 | /**
145 | * @return array
146 | */
147 | #[ArrayShape([
148 | 'scheme' => 'string',
149 | 'user' => 'null|string',
150 | 'pass' => 'null|string',
151 | 'hostname' => 'string',
152 | 'port' => 'int|null',
153 | 'path' => 'string[]',
154 | 'query' => 'mixed[]',
155 | 'fragment' => 'null|string',
156 | ])]
157 | #[Pure]
158 | public function getParsedUrl() : array
159 | {
160 | return [
161 | 'scheme' => $this->getScheme(),
162 | 'user' => $this->getUser(),
163 | 'pass' => $this->getPass(),
164 | 'hostname' => $this->getHostname(),
165 | 'port' => $this->getPort(),
166 | 'path' => $this->getPathSegments(),
167 | 'query' => $this->getQueryData(),
168 | 'fragment' => $this->getFragment(),
169 | ];
170 | }
171 |
172 | /**
173 | * @return string|null
174 | */
175 | #[Pure]
176 | public function getPass() : ?string
177 | {
178 | return $this->pass;
179 | }
180 |
181 | #[Pure]
182 | public function getPath() : string
183 | {
184 | return '/' . \implode('/', $this->pathSegments);
185 | }
186 |
187 | /**
188 | * @return array
189 | */
190 | #[Pure]
191 | public function getPathSegments() : array
192 | {
193 | return $this->pathSegments;
194 | }
195 |
196 | #[Pure]
197 | public function getPathSegment(int $index) : ?string
198 | {
199 | return $this->pathSegments[$index] ?? null;
200 | }
201 |
202 | /**
203 | * @return int|null
204 | */
205 | #[Pure]
206 | public function getPort() : ?int
207 | {
208 | return $this->port;
209 | }
210 |
211 | #[Pure]
212 | protected function getPortPart() : string
213 | {
214 | $part = $this->getPort();
215 | if (!\in_array($part, [
216 | null,
217 | 80,
218 | 443,
219 | ], true)) {
220 | return ':' . $part;
221 | }
222 | return '';
223 | }
224 |
225 | /**
226 | * Get the "Query" part of the URL.
227 | *
228 | * @param array $allowedKeys Allowed query keys
229 | *
230 | * @return string|null
231 | */
232 | #[Pure]
233 | public function getQuery(array $allowedKeys = []) : ?string
234 | {
235 | $query = $this->getQueryData($allowedKeys);
236 | return $query ? \http_build_query($query) : null;
237 | }
238 |
239 | /**
240 | * @param array $allowedKeys
241 | *
242 | * @return array
243 | */
244 | #[Pure]
245 | public function getQueryData(array $allowedKeys = []) : array
246 | {
247 | return $allowedKeys ? $this->filterQuery($allowedKeys) : $this->queryData;
248 | }
249 |
250 | /**
251 | * @return string|null
252 | */
253 | #[Pure]
254 | public function getScheme() : ?string
255 | {
256 | return $this->scheme;
257 | }
258 |
259 | /**
260 | * @since 5.3
261 | *
262 | * @return string
263 | */
264 | public function toString() : string
265 | {
266 | return $this->getUrl();
267 | }
268 |
269 | /**
270 | * @since 6.1
271 | *
272 | * @return string
273 | */
274 | public function getUrl() : string
275 | {
276 | $url = $this->getScheme() . '://';
277 | $part = $this->getUser();
278 | if ($part !== null) {
279 | $url .= $part;
280 | $part = $this->getPass();
281 | if ($part !== null) {
282 | $url .= ':' . $part;
283 | }
284 | $url .= '@';
285 | }
286 | $url .= $this->getHost();
287 | $url .= $this->getRelative();
288 | return $url;
289 | }
290 |
291 | /**
292 | * Get the relative URL.
293 | *
294 | * @since 6.1
295 | *
296 | * @return string
297 | */
298 | public function getRelative() : string
299 | {
300 | $relative = $this->getPath();
301 | $part = $this->getQuery();
302 | if ($part !== null) {
303 | $relative .= '?' . $part;
304 | }
305 | $part = $this->getFragment();
306 | if ($part !== null) {
307 | $relative .= '#' . $part;
308 | }
309 | return $relative;
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/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 | Is Secure: = $this->request->isSecure() ? 'Yes' : 'No' ?>
82 | Protocol: = $this->request->getProtocol() ?>
83 | Method: = $this->request->getMethod() ?>
84 | URL: = $this->request->getUrl() ?>
85 | Server: = $this->request->getServer('SERVER_SOFTWARE') ?>
86 | Hostname: = \gethostname() ?>
87 | = $this->renderRequestUserAgent() ?>
88 | renderHeadersTable($this->request->getHeaderLines());
90 | echo $this->renderRequestBody();
91 | echo $this->renderRequestForm();
92 | echo $this->renderRequestFiles();
93 | return \ob_get_clean(); // @phpstan-ignore-line
94 | }
95 |
96 | protected function renderRequestUserAgent() : string
97 | {
98 | $userAgent = $this->request->getUserAgent();
99 | if ($userAgent === null) {
100 | return '';
101 | }
102 | \ob_start(); ?>
103 | User-Agent
104 |
105 |
106 |
107 | Type
108 | Name
109 | Version
110 | Platform
111 | Is Mobile
112 |
113 |
114 |
115 |
116 | = \htmlentities($userAgent->getType()) ?>
117 | = \htmlentities($userAgent->getName()) ?>
118 | = $userAgent->isBrowser()
119 | ? \htmlentities((string) $userAgent->getBrowserVersion())
120 | : '' ?>
121 | = \htmlentities((string) $userAgent->getPlatform()) ?>
122 | = $userAgent->isMobile() ? 'Yes' : 'No' ?>
123 |
124 |
125 |
126 | request->hasFiles()
133 | ? \http_build_query($this->request->getPost())
134 | : $this->request->getBody();
135 | if ($body === '') {
136 | return '';
137 | }
138 | \ob_start(); ?>
139 | Body Contents
140 |
143 | request->isPost() && !$this->request->isFormUrlEncoded()) {
150 | return '';
151 | }
152 | \ob_start(); ?>
153 | Form
154 |
155 |
156 |
157 | Field
158 | Value
159 |
160 |
161 |
162 | request->getParsedBody()) as $field => $value): ?>
163 |
164 | = \htmlentities($field) ?>
165 |
166 | = \htmlentities($value) ?>
167 |
168 |
169 |
170 |
171 |
172 | request->hasFiles()) {
179 | return '';
180 | }
181 | \ob_start(); ?>
182 | Uploaded Files
183 |
184 |
185 |
186 | Field
187 | Name
188 | Full Path
189 | Type
190 | Client Type
191 | Extension
192 | Size
193 | Destination
194 | Error
195 |
196 |
197 |
198 | request->getFiles()) as $field => $file): ?>
199 |
200 | = \htmlentities($field) ?>
201 | = \htmlentities($file->getName()) ?>
202 | = \htmlentities($file->getFullPath()) ?>
203 | = \htmlentities($file->getType()) ?>
204 | = \htmlentities($file->getClientType()) ?>
205 | = \htmlentities($file->getExtension()) ?>
206 | = Debugger::convertSize($file->getSize()) ?>
207 | = $file->getDestination() ?>
208 | = $file->getError() ?>
209 | = \htmlentities($file->getErrorMessage()) ?>
210 |
211 |
212 |
213 |
214 | response)) {
221 | return 'A Response instance has not been set on this collector.
';
222 | }
223 | \ob_start(); ?>
224 | Protocol: = \htmlentities($this->response->getProtocol()) ?>
225 | Status: = \htmlentities($this->response->getStatus()) ?>
226 | Sent: = $this->response->isSent() ? 'Yes' : 'No' ?>
227 | response->isSent()):
229 | $info = [];
230 | foreach ($this->getData() as $data) {
231 | if (
232 | isset($data['message'], $data['type'])
233 | && $data['message'] === 'response'
234 | && $data['type'] === 'send'
235 | ) {
236 | $info = $data;
237 | break;
238 | }
239 | } ?>
240 |
241 | Time Sending: = Debugger::roundSecondsToMilliseconds($info['end'] - $info['start']) ?> ms
242 |
243 | renderHeadersTable($this->response->getHeaderLines());
246 | if ($this->response->isReplacingHeaders()) {
247 | echo '* Note that the Response is replacing headers.
';
248 | }
249 | echo '* Note that some headers can be set outside the Response';
250 | echo ' class, for example by the session or the server.';
251 | echo ' So they don\'t appear here.
';
252 | echo $this->renderResponseCookies();
253 | echo $this->renderResponseBody();
254 | return \ob_get_clean(); // @phpstan-ignore-line
255 | }
256 |
257 | protected function renderResponseCookies() : string
258 | {
259 | if (!$this->response->getCookies()) {
260 | return '';
261 | }
262 | \ob_start(); ?>
263 | Cookies
264 |
265 |
266 |
267 | Name
268 | Value
269 | Expires
270 | Path
271 | Domain
272 | Is Secure
273 | Is HTTP Only
274 | SameSite
275 |
276 |
277 |
278 | response->getCookies() as $cookie): ?>
279 |
280 | = \htmlentities($cookie->getName()) ?>
281 | = \htmlentities($cookie->getValue()) ?>
282 | = $cookie->getExpires()?->format('D, d-M-Y H:i:s \G\M\T') ?>
283 | = \htmlentities((string) $cookie->getPath()) ?>
284 | = \htmlentities((string) $cookie->getDomain()) ?>
285 | = $cookie->isSecure() ? 'Yes' : 'No' ?>
286 | = $cookie->isHttpOnly() ? 'Yes' : 'No' ?>
287 | = \htmlentities((string) $cookie->getSameSite()) ?>
288 |
289 |
290 |
291 |
292 |
299 | Body Contents
300 | response->isSent()) {
302 | echo 'Response has not been sent.
';
303 | return \ob_get_clean(); // @phpstan-ignore-line
304 | }
305 | if ($this->response->hasDownload()) {
306 | echo 'Body has downloadable content.
';
307 | return \ob_get_clean(); // @phpstan-ignore-line
308 | }
309 | $body = $this->response->getBody();
310 | if ($body === '') {
311 | echo 'Body is empty.
';
312 | return \ob_get_clean(); // @phpstan-ignore-line
313 | } ?>
314 |
317 | $headerLines
323 | *
324 | * @return string
325 | */
326 | protected function renderHeadersTable(array $headerLines) : string
327 | {
328 | \ob_start(); ?>
329 | Headers
330 | No headers.
';
333 | return \ob_get_clean(); // @phpstan-ignore-line
334 | } ?>
335 |
336 |
337 |
338 | Name
339 | Value
340 |
341 |
342 |
343 |
345 |
346 | = \htmlentities($name) ?>
347 | = \htmlentities($value) ?>
348 |
349 |
350 |
351 |
352 |
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 | protected int $jsonFlags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE;
59 |
60 | public function __toString() : string
61 | {
62 | $eol = "\r\n";
63 | $message = $this->getStartLine() . $eol;
64 | foreach ($this->getHeaderLines() as $headerLine) {
65 | $message .= $headerLine . $eol;
66 | }
67 | $message .= $eol;
68 | $message .= $this->getBody();
69 | return $message;
70 | }
71 |
72 | /**
73 | * Get the Message Start-Line.
74 | *
75 | * @throws BadMethodCallException if $this is not an instance of
76 | * RequestInterface or ResponseInterface
77 | *
78 | * @return string
79 | */
80 | public function getStartLine() : string
81 | {
82 | if ($this instanceof RequestInterface) {
83 | $query = $this->getUrl()->getQuery();
84 | $query = ($query !== null && $query !== '') ? '?' . $query : '';
85 | return $this->getMethod()
86 | . ' ' . $this->getUrl()->getPath() . $query
87 | . ' ' . $this->getProtocol();
88 | }
89 | if ($this instanceof ResponseInterface) {
90 | return $this->getProtocol()
91 | . ' ' . $this->getStatus();
92 | }
93 | throw new BadMethodCallException(
94 | static::class . ' is not an instance of ' . RequestInterface::class
95 | . ' or ' . ResponseInterface::class
96 | );
97 | }
98 |
99 | #[Pure]
100 | public function hasHeader(string $name, ?string $value = null) : bool
101 | {
102 | return $value === null
103 | ? $this->getHeader($name) !== null
104 | : $this->getHeader($name) === $value;
105 | }
106 |
107 | #[Pure]
108 | public function getHeader(string $name) : ?string
109 | {
110 | return $this->headers[\strtolower($name)] ?? null;
111 | }
112 |
113 | /**
114 | * @return array
115 | */
116 | #[Pure]
117 | public function getHeaders() : array
118 | {
119 | return $this->headers;
120 | }
121 |
122 | #[Pure]
123 | public function getHeaderLine(string $name) : ?string
124 | {
125 | $value = $this->getHeader($name);
126 | if ($value === null) {
127 | return null;
128 | }
129 | $name = ResponseHeader::getName($name);
130 | return $name . ': ' . $value;
131 | }
132 |
133 | /**
134 | * @return array
135 | */
136 | #[Pure]
137 | public function getHeaderLines() : array
138 | {
139 | $lines = [];
140 | foreach ($this->getHeaders() as $name => $value) {
141 | $name = ResponseHeader::getName($name);
142 | if (\str_contains($value, "\n")) {
143 | foreach (\explode("\n", $value) as $val) {
144 | $lines[] = $name . ': ' . $val;
145 | }
146 | continue;
147 | }
148 | $lines[] = $name . ': ' . $value;
149 | }
150 | return $lines;
151 | }
152 |
153 | /**
154 | * Set a Message header.
155 | *
156 | * @param string $name
157 | * @param string $value
158 | *
159 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
160 | *
161 | * @return static
162 | */
163 | protected function setHeader(string $name, string $value) : static
164 | {
165 | $this->headers[\strtolower($name)] = $value;
166 | return $this;
167 | }
168 |
169 | /**
170 | * Set a list of headers.
171 | *
172 | * @param array $headers
173 | *
174 | * @return static
175 | */
176 | protected function setHeaders(array $headers) : static
177 | {
178 | foreach ($headers as $name => $value) {
179 | $this->setHeader((string) $name, (string) $value);
180 | }
181 | return $this;
182 | }
183 |
184 | /**
185 | * Append a Message header.
186 | *
187 | * Used to set repeated header field names.
188 | *
189 | * @param string $name
190 | * @param string $value
191 | *
192 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
193 | *
194 | * @return static
195 | */
196 | protected function appendHeader(string $name, string $value) : static
197 | {
198 | $current = $this->getHeader($name);
199 | if ($current !== null) {
200 | $separator = $this->getHeaderValueSeparator($name);
201 | $value = $current . $separator . $value;
202 | }
203 | $this->setHeader($name, $value);
204 | return $this;
205 | }
206 |
207 | /**
208 | * @param string $headerName
209 | *
210 | * @see https://stackoverflow.com/a/38406581/6027968
211 | *
212 | * @return string
213 | */
214 | private function getHeaderValueSeparator(string $headerName) : string
215 | {
216 | if (ResponseHeader::isMultiline($headerName)) {
217 | return "\n";
218 | }
219 | return ', ';
220 | }
221 |
222 | /**
223 | * Remove a header by name.
224 | *
225 | * @param string $name
226 | *
227 | * @return static
228 | */
229 | protected function removeHeader(string $name) : static
230 | {
231 | unset($this->headers[\strtolower($name)]);
232 | return $this;
233 | }
234 |
235 | /**
236 | * Remove all headers.
237 | *
238 | * @return static
239 | */
240 | protected function removeHeaders() : static
241 | {
242 | $this->headers = [];
243 | return $this;
244 | }
245 |
246 | /**
247 | * Remove headers by names.
248 | *
249 | * @param array $names
250 | *
251 | * @return static
252 | */
253 | protected function removeHeadersByNames(array $names) : static
254 | {
255 | foreach ($names as $name) {
256 | $this->removeHeader($name);
257 | }
258 | return $this;
259 | }
260 |
261 | /**
262 | * Say if the Message has a Cookie.
263 | *
264 | * @param string $name Cookie name
265 | *
266 | * @return bool
267 | */
268 | #[Pure]
269 | public function hasCookie(string $name) : bool
270 | {
271 | return (bool) $this->getCookie($name);
272 | }
273 |
274 | /**
275 | * Get a Cookie by name.
276 | *
277 | * @param string $name
278 | *
279 | * @return Cookie|null
280 | */
281 | #[Pure]
282 | public function getCookie(string $name) : ?Cookie
283 | {
284 | return $this->cookies[$name] ?? null;
285 | }
286 |
287 | /**
288 | * Get all Cookies.
289 | *
290 | * @return array
291 | */
292 | #[Pure]
293 | public function getCookies() : array
294 | {
295 | return $this->cookies;
296 | }
297 |
298 | /**
299 | * Set a new Cookie.
300 | *
301 | * @param Cookie $cookie
302 | *
303 | * @return static
304 | */
305 | protected function setCookie(Cookie $cookie) : static
306 | {
307 | $this->cookies[$cookie->getName()] = $cookie;
308 | return $this;
309 | }
310 |
311 | /**
312 | * Set a list of Cookies.
313 | *
314 | * @param array $cookies
315 | *
316 | * @return static
317 | */
318 | protected function setCookies(array $cookies) : static
319 | {
320 | foreach ($cookies as $cookie) {
321 | $this->setCookie($cookie);
322 | }
323 | return $this;
324 | }
325 |
326 | /**
327 | * Remove a Cookie by name.
328 | *
329 | * @param string $name
330 | *
331 | * @return static
332 | */
333 | protected function removeCookie(string $name) : static
334 | {
335 | unset($this->cookies[$name]);
336 | return $this;
337 | }
338 |
339 | /**
340 | * Remove many Cookies by names.
341 | *
342 | * @param array $names
343 | *
344 | * @return static
345 | */
346 | protected function removeCookies(array $names) : static
347 | {
348 | foreach ($names as $name) {
349 | $this->removeCookie($name);
350 | }
351 | return $this;
352 | }
353 |
354 | /**
355 | * Get the Message body.
356 | *
357 | * @return string
358 | */
359 | #[Pure]
360 | public function getBody() : string
361 | {
362 | return $this->body ?? '';
363 | }
364 |
365 | /**
366 | * Set the Message body.
367 | *
368 | * @param string $body
369 | *
370 | * @return static
371 | */
372 | protected function setBody(string $body) : static
373 | {
374 | $this->body = $body;
375 | return $this;
376 | }
377 |
378 | /**
379 | * Get the HTTP protocol.
380 | *
381 | * @return string
382 | */
383 | #[Pure]
384 | public function getProtocol() : string
385 | {
386 | return $this->protocol;
387 | }
388 |
389 | /**
390 | * Set the HTTP protocol.
391 | *
392 | * @param string $protocol HTTP/1.1, HTTP/2, etc
393 | *
394 | * @return static
395 | */
396 | protected function setProtocol(string $protocol) : static
397 | {
398 | $this->protocol = Protocol::validate($protocol);
399 | return $this;
400 | }
401 |
402 | /**
403 | * Gets the HTTP Request Method.
404 | *
405 | * @return string $method One of: CONNECT, DELETE, GET, HEAD, OPTIONS,
406 | * PATCH, POST, PUT, or TRACE
407 | */
408 | #[Pure]
409 | protected function getMethod() : string
410 | {
411 | return $this->method;
412 | }
413 |
414 | /**
415 | * @param string $method
416 | *
417 | * @throws InvalidArgumentException for invalid method
418 | *
419 | * @return bool
420 | */
421 | protected function isMethod(string $method) : bool
422 | {
423 | return $this->getMethod() === Method::validate($method);
424 | }
425 |
426 | /**
427 | * Set the request method.
428 | *
429 | * @param string $method One of: CONNECT, DELETE, GET, HEAD, OPTIONS, PATCH,
430 | * POST, PUT, or TRACE
431 | *
432 | * @throws InvalidArgumentException for invalid method
433 | *
434 | * @return static
435 | */
436 | protected function setMethod(string $method) : static
437 | {
438 | $this->method = Method::validate($method);
439 | return $this;
440 | }
441 |
442 | protected function setStatusCode(int $code) : static
443 | {
444 | $this->statusCode = Status::validate($code);
445 | return $this;
446 | }
447 |
448 | /**
449 | * Get the status code.
450 | *
451 | * @return int
452 | */
453 | #[Pure]
454 | protected function getStatusCode() : int
455 | {
456 | return $this->statusCode;
457 | }
458 |
459 | protected function isStatusCode(int $code) : bool
460 | {
461 | return $this->getStatusCode() === Status::validate($code);
462 | }
463 |
464 | /**
465 | * Gets the requested URL.
466 | *
467 | * @return URL
468 | */
469 | #[Pure]
470 | protected function getUrl() : URL
471 | {
472 | return $this->url;
473 | }
474 |
475 | /**
476 | * Set the Message URL.
477 | *
478 | * @param URL|string $url
479 | *
480 | * @return static
481 | */
482 | protected function setUrl(URL | string $url) : static
483 | {
484 | if (!$url instanceof URL) {
485 | $url = new URL($url);
486 | }
487 | $this->url = $url;
488 | return $this;
489 | }
490 |
491 | #[Pure]
492 | protected function parseContentType() : ?string
493 | {
494 | $contentType = $this->getHeader('Content-Type');
495 | if ($contentType === null) {
496 | return null;
497 | }
498 | $contentType = \explode(';', $contentType, 2)[0];
499 | return \trim($contentType);
500 | }
501 |
502 | /**
503 | * Set JSON flags.
504 | *
505 | * @since 5.6
506 | *
507 | * @param int $flags
508 | *
509 | * @return static
510 | */
511 | public function setJsonFlags(int $flags) : static
512 | {
513 | $this->jsonFlags = $flags;
514 | return $this;
515 | }
516 |
517 | /**
518 | * Get JSON flags.
519 | *
520 | * @since 5.6
521 | *
522 | * @return int
523 | */
524 | public function getJsonFlags() : int
525 | {
526 | return $this->jsonFlags;
527 | }
528 |
529 | /**
530 | * @see https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
531 | * @see https://stackoverflow.com/a/33748742/6027968
532 | *
533 | * @param string|null $string
534 | *
535 | * @return array
536 | */
537 | public static function parseQualityValues(?string $string) : array
538 | {
539 | if (empty($string)) {
540 | return [];
541 | }
542 | $quality = \array_reduce(
543 | \explode(',', $string, 20),
544 | static function ($qualifier, $part) {
545 | [$value, $priority] = \array_merge(\explode(';q=', $part), [1]);
546 | $qualifier[\trim((string) $value)] = (float) $priority;
547 | return $qualifier;
548 | },
549 | []
550 | );
551 | \arsort($quality);
552 | return $quality;
553 | }
554 | }
555 |
--------------------------------------------------------------------------------
/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\Pure;
13 |
14 | /**
15 | * Class UserAgent.
16 | *
17 | * @package http
18 | */
19 | class UserAgent implements \JsonSerializable, \Stringable
20 | {
21 | protected ?string $agent = null;
22 | protected ?string $browser = null;
23 | protected ?string $browserVersion = null;
24 | protected ?string $mobile = null;
25 | protected ?string $platform = null;
26 | protected ?string $robot = null;
27 | protected bool $isBrowser = false;
28 | protected bool $isMobile = false;
29 | protected bool $isRobot = false;
30 | /**
31 | * @var array>
32 | */
33 | protected static array $config = [
34 | 'platforms' => [
35 | 'windows nt 10.0' => 'Windows 10',
36 | 'windows nt 6.3' => 'Windows 8.1',
37 | 'windows nt 6.2' => 'Windows 8',
38 | 'windows nt 6.1' => 'Windows 7',
39 | 'windows nt 6.0' => 'Windows Vista',
40 | 'windows nt 5.2' => 'Windows 2003',
41 | 'windows nt 5.1' => 'Windows XP',
42 | 'windows nt 5.0' => 'Windows 2000',
43 | 'windows nt 4.0' => 'Windows NT 4.0',
44 | 'winnt4.0' => 'Windows NT 4.0',
45 | 'winnt 4.0' => 'Windows NT',
46 | 'winnt' => 'Windows NT',
47 | 'windows 98' => 'Windows 98',
48 | 'win98' => 'Windows 98',
49 | 'windows 95' => 'Windows 95',
50 | 'win95' => 'Windows 95',
51 | 'windows phone' => 'Windows Phone',
52 | 'windows' => 'Unknown Windows OS',
53 | 'android' => 'Android',
54 | 'blackberry' => 'BlackBerry',
55 | 'iphone' => 'iOS',
56 | 'ipad' => 'iOS',
57 | 'ipod' => 'iOS',
58 | 'os x' => 'Mac OS X',
59 | 'ppc mac' => 'Power PC Mac',
60 | 'freebsd' => 'FreeBSD',
61 | 'ppc' => 'Macintosh',
62 | 'ubuntu' => 'Ubuntu',
63 | 'debian' => 'Debian',
64 | 'fedora' => 'Fedora',
65 | 'linux' => 'Linux',
66 | 'sunos' => 'Sun Solaris',
67 | 'beos' => 'BeOS',
68 | 'apachebench' => 'ApacheBench',
69 | 'aix' => 'AIX',
70 | 'irix' => 'Irix',
71 | 'osf' => 'DEC OSF',
72 | 'hp-ux' => 'HP-UX',
73 | 'netbsd' => 'NetBSD',
74 | 'bsdi' => 'BSDi',
75 | 'openbsd' => 'OpenBSD',
76 | 'gnu' => 'GNU/Linux',
77 | 'unix' => 'Unknown Unix OS',
78 | 'symbian' => 'Symbian OS',
79 | ],
80 | // The order of this array should NOT be changed. Many browsers return
81 | // multiple browser types so we want to identify the sub-type first.
82 | 'browsers' => [
83 | 'curl' => 'Curl',
84 | 'PostmanRuntime' => 'Postman',
85 | 'OPR' => 'Opera',
86 | 'Flock' => 'Flock',
87 | 'Edge' => 'Spartan',
88 | 'Edg' => 'Edge',
89 | 'EdgA' => 'Edge',
90 | 'Chrome' => 'Chrome',
91 | // Opera 10+ always reports Opera/9.80 and appends
92 | // Version/ to the user agent string
93 | 'Opera.*?Version' => 'Opera',
94 | 'Opera' => 'Opera',
95 | 'MSIE' => 'Internet Explorer',
96 | 'Internet Explorer' => 'Internet Explorer',
97 | 'Trident.* rv' => 'Internet Explorer',
98 | 'Shiira' => 'Shiira',
99 | 'Firefox' => 'Firefox',
100 | 'Chimera' => 'Chimera',
101 | 'Phoenix' => 'Phoenix',
102 | 'Firebird' => 'Firebird',
103 | 'Camino' => 'Camino',
104 | 'Netscape' => 'Netscape',
105 | 'OmniWeb' => 'OmniWeb',
106 | 'Safari' => 'Safari',
107 | 'Mozilla' => 'Mozilla',
108 | 'Konqueror' => 'Konqueror',
109 | 'icab' => 'iCab',
110 | 'Lynx' => 'Lynx',
111 | 'Links' => 'Links',
112 | 'hotjava' => 'HotJava',
113 | 'amaya' => 'Amaya',
114 | 'IBrowse' => 'IBrowse',
115 | 'Maxthon' => 'Maxthon',
116 | 'Ubuntu' => 'Ubuntu Web Browser',
117 | 'Vivaldi' => 'Vivaldi',
118 | ],
119 | 'mobiles' => [
120 | 'mobileexplorer' => 'Mobile Explorer',
121 | 'palmsource' => 'Palm',
122 | 'palmscape' => 'Palmscape',
123 | // Phones and Manufacturers
124 | 'motorola' => 'Motorola',
125 | 'nokia' => 'Nokia',
126 | 'palm' => 'Palm',
127 | 'iphone' => 'Apple iPhone',
128 | 'ipad' => 'iPad',
129 | 'ipod' => 'Apple iPod Touch',
130 | 'sony' => 'Sony Ericsson',
131 | 'ericsson' => 'Sony Ericsson',
132 | 'blackberry' => 'BlackBerry',
133 | 'cocoon' => 'O2 Cocoon',
134 | 'blazer' => 'Treo',
135 | 'lg' => 'LG',
136 | 'amoi' => 'Amoi',
137 | 'xda' => 'XDA',
138 | 'mda' => 'MDA',
139 | 'vario' => 'Vario',
140 | 'htc' => 'HTC',
141 | 'samsung' => 'Samsung',
142 | 'sharp' => 'Sharp',
143 | 'sie-' => 'Siemens',
144 | 'alcatel' => 'Alcatel',
145 | 'benq' => 'BenQ',
146 | 'ipaq' => 'HP iPaq',
147 | 'mot-' => 'Motorola',
148 | 'playstation portable' => 'PlayStation Portable',
149 | 'playstation 3' => 'PlayStation 3',
150 | 'playstation vita' => 'PlayStation Vita',
151 | 'hiptop' => 'Danger Hiptop',
152 | 'nec-' => 'NEC',
153 | 'panasonic' => 'Panasonic',
154 | 'philips' => 'Philips',
155 | 'sagem' => 'Sagem',
156 | 'sanyo' => 'Sanyo',
157 | 'spv' => 'SPV',
158 | 'zte' => 'ZTE',
159 | 'sendo' => 'Sendo',
160 | 'nintendo dsi' => 'Nintendo DSi',
161 | 'nintendo ds' => 'Nintendo DS',
162 | 'nintendo 3ds' => 'Nintendo 3DS',
163 | 'wii' => 'Nintendo Wii',
164 | 'open web' => 'Open Web',
165 | 'openweb' => 'OpenWeb',
166 | // Operating Systems
167 | 'android' => 'Android',
168 | 'symbian' => 'Symbian',
169 | 'SymbianOS' => 'SymbianOS',
170 | 'elaine' => 'Palm',
171 | 'series60' => 'Symbian S60',
172 | 'windows ce' => 'Windows CE',
173 | // Browsers
174 | 'obigo' => 'Obigo',
175 | 'netfront' => 'Netfront Browser',
176 | 'openwave' => 'Openwave Browser',
177 | 'mobilexplorer' => 'Mobile Explorer',
178 | 'operamini' => 'Opera Mini',
179 | 'opera mini' => 'Opera Mini',
180 | 'opera mobi' => 'Opera Mobile',
181 | 'fennec' => 'Firefox Mobile',
182 | // Other
183 | 'digital paths' => 'Digital Paths',
184 | 'avantgo' => 'AvantGo',
185 | 'xiino' => 'Xiino',
186 | 'novarra' => 'Novarra Transcoder',
187 | 'vodafone' => 'Vodafone',
188 | 'docomo' => 'NTT DoCoMo',
189 | 'o2' => 'O2',
190 | // Fallback
191 | 'mobile' => 'Generic Mobile',
192 | 'wireless' => 'Generic Mobile',
193 | 'j2me' => 'Generic Mobile',
194 | 'midp' => 'Generic Mobile',
195 | 'cldc' => 'Generic Mobile',
196 | 'up.link' => 'Generic Mobile',
197 | 'up.browser' => 'Generic Mobile',
198 | 'smartphone' => 'Generic Mobile',
199 | 'cellphone' => 'Generic Mobile',
200 | ],
201 | 'robots' => [
202 | 'googlebot' => 'Googlebot',
203 | 'msnbot' => 'MSNBot',
204 | 'baiduspider' => 'Baiduspider',
205 | 'bingbot' => 'Bing',
206 | 'slurp' => 'Inktomi Slurp',
207 | 'yahoo' => 'Yahoo',
208 | 'ask jeeves' => 'Ask Jeeves',
209 | 'fastcrawler' => 'FastCrawler',
210 | 'infoseek' => 'InfoSeek Robot 1.0',
211 | 'lycos' => 'Lycos',
212 | 'yandex' => 'YandexBot',
213 | 'mediapartners-google' => 'MediaPartners Google',
214 | 'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
215 | 'adsbot-google' => 'AdsBot Google',
216 | 'feedfetcher-google' => 'Feedfetcher Google',
217 | 'curious george' => 'Curious George',
218 | 'ia_archiver' => 'Alexa Crawler',
219 | 'MJ12bot' => 'Majestic-12',
220 | 'Uptimebot' => 'Uptimebot',
221 | ],
222 | ];
223 |
224 | /**
225 | * UserAgent constructor.
226 | *
227 | * @param string $userAgent User-Agent string
228 | */
229 | public function __construct(string $userAgent)
230 | {
231 | $this->parse($userAgent);
232 | }
233 |
234 | #[Pure]
235 | public function __toString() : string
236 | {
237 | return $this->toString();
238 | }
239 |
240 | /**
241 | * @param string $string
242 | *
243 | * @return static
244 | */
245 | protected function parse(string $string) : static
246 | {
247 | $this->isBrowser = false;
248 | $this->isRobot = false;
249 | $this->isMobile = false;
250 | $this->browser = null;
251 | $this->browserVersion = null;
252 | $this->mobile = null;
253 | $this->robot = null;
254 | $this->agent = $string;
255 | $this->compileData();
256 | return $this;
257 | }
258 |
259 | protected function compileData() : void
260 | {
261 | $this->setPlatform();
262 | foreach (['setRobot', 'setBrowser', 'setMobile'] as $function) {
263 | if ($this->{$function}()) {
264 | break;
265 | }
266 | }
267 | }
268 |
269 | protected function setPlatform() : bool
270 | {
271 | foreach (static::$config['platforms'] as $key => $val) {
272 | if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) {
273 | $this->platform = $val;
274 | return true;
275 | }
276 | }
277 | return false;
278 | }
279 |
280 | protected function setBrowser() : bool
281 | {
282 | foreach (static::$config['browsers'] as $key => $val) {
283 | if (\preg_match(
284 | '#' . \preg_quote($key, '#') . '.*?([0-9\.]+)#i',
285 | $this->agent,
286 | $match
287 | )) {
288 | $this->isBrowser = true;
289 | $this->browserVersion = $match[1];
290 | $this->browser = $val;
291 | $this->setMobile();
292 | return true;
293 | }
294 | }
295 | return false;
296 | }
297 |
298 | protected function setMobile() : bool
299 | {
300 | foreach (static::$config['mobiles'] as $key => $val) {
301 | if (\stripos($this->agent, $key) !== false) {
302 | $this->isMobile = true;
303 | $this->mobile = $val;
304 | return true;
305 | }
306 | }
307 | return false;
308 | }
309 |
310 | protected function setRobot() : bool
311 | {
312 | foreach (static::$config['robots'] as $key => $val) {
313 | if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) {
314 | $this->isRobot = true;
315 | $this->robot = $val;
316 | $this->setMobile();
317 | return true;
318 | }
319 | }
320 | return false;
321 | }
322 |
323 | /**
324 | * Get the User-Agent as string.
325 | *
326 | * @since 5.3
327 | *
328 | * @return string
329 | */
330 | #[Pure]
331 | public function toString() : string
332 | {
333 | return $this->agent;
334 | }
335 |
336 | /**
337 | * Gets the Browser name.
338 | *
339 | * @return string|null
340 | */
341 | #[Pure]
342 | public function getBrowser() : ?string
343 | {
344 | return $this->browser;
345 | }
346 |
347 | /**
348 | * Gets the Browser Version.
349 | *
350 | * @return string|null
351 | */
352 | #[Pure]
353 | public function getBrowserVersion() : ?string
354 | {
355 | return $this->browserVersion;
356 | }
357 |
358 | /**
359 | * Gets the Mobile device.
360 | *
361 | * @return string|null
362 | */
363 | #[Pure]
364 | public function getMobile() : ?string
365 | {
366 | return $this->mobile;
367 | }
368 |
369 | /**
370 | * Gets the OS Platform.
371 | *
372 | * @return string|null
373 | */
374 | #[Pure]
375 | public function getPlatform() : ?string
376 | {
377 | return $this->platform;
378 | }
379 |
380 | /**
381 | * Gets the Robot name.
382 | *
383 | * @return string|null
384 | */
385 | #[Pure]
386 | public function getRobot() : ?string
387 | {
388 | return $this->robot;
389 | }
390 |
391 | /**
392 | * Is Browser.
393 | *
394 | * @param string|null $key
395 | *
396 | * @return bool
397 | */
398 | #[Pure]
399 | public function isBrowser(?string $key = null) : bool
400 | {
401 | if ($key === null || $this->isBrowser === false) {
402 | return $this->isBrowser;
403 | }
404 | $config = static::$config['browsers'] ?? [];
405 | return isset($config[$key])
406 | && $this->browser === $config[$key];
407 | }
408 |
409 | /**
410 | * Is Mobile.
411 | *
412 | * @param string|null $key
413 | *
414 | * @return bool
415 | */
416 | #[Pure]
417 | public function isMobile(?string $key = null) : bool
418 | {
419 | if ($key === null || $this->isMobile === false) {
420 | return $this->isMobile;
421 | }
422 | $config = static::$config['mobiles'] ?? [];
423 | return isset($config[$key])
424 | && $this->mobile === $config[$key];
425 | }
426 |
427 | /**
428 | * Is Robot.
429 | *
430 | * @param string|null $key
431 | *
432 | * @return bool
433 | */
434 | #[Pure]
435 | public function isRobot(?string $key = null) : bool
436 | {
437 | if ($key === null || $this->isRobot === false) {
438 | return $this->isRobot;
439 | }
440 | $config = static::$config['robots'] ?? [];
441 | return isset($config[$key])
442 | && $this->robot === $config[$key];
443 | }
444 |
445 | #[Pure]
446 | public function getType() : string
447 | {
448 | if ($this->isBrowser()) {
449 | return 'Browser';
450 | }
451 | if ($this->isRobot()) {
452 | return 'Robot';
453 | }
454 | return 'Unknown';
455 | }
456 |
457 | #[Pure]
458 | public function getName() : string
459 | {
460 | if ($this->isBrowser()) {
461 | return $this->getBrowser();
462 | }
463 | if ($this->isRobot()) {
464 | return $this->getRobot();
465 | }
466 | return 'Unknown';
467 | }
468 |
469 | #[Pure]
470 | public function jsonSerialize() : string
471 | {
472 | return $this->toString();
473 | }
474 | }
475 |
--------------------------------------------------------------------------------
/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 | public const string baseUri = 'base-uri';
32 | /**
33 | * Defines the valid sources for web workers and nested browsing contexts
34 | * loaded using elements such as ` ` and `