1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* Lenevor Framework |
5
|
|
|
* |
6
|
|
|
* LICENSE |
7
|
|
|
* |
8
|
|
|
* This source file is subject to the new BSD license that is bundled |
9
|
|
|
* with this package in the file license.md. |
10
|
|
|
* It is also available through the world-wide-web at this URL: |
11
|
|
|
* https://lenevor.com/license |
12
|
|
|
* If you did not receive a copy of the license and are unable to |
13
|
|
|
* obtain it through the world-wide-web, please send an email |
14
|
|
|
* to [email protected] so we can send you a copy immediately. |
15
|
|
|
* |
16
|
|
|
* @package Lenevor |
17
|
|
|
* @subpackage Base |
18
|
|
|
* @link https://lenevor.com |
19
|
|
|
* @copyright Copyright (c) 2019 - 2023 Alexander Campo <[email protected]> |
20
|
|
|
* @license https://opensource.org/licenses/BSD-3-Clause New BSD license or see https://lenevor.com/license or see /license.md |
21
|
|
|
*/ |
22
|
|
|
|
23
|
|
|
namespace Syscodes\Components\Http\Loaders; |
24
|
|
|
|
25
|
|
|
use DateTime; |
26
|
|
|
use Countable; |
27
|
|
|
use Traversable; |
28
|
|
|
use ArrayIterator; |
29
|
|
|
use RuntimeException; |
30
|
|
|
use DateTimeInterface; |
31
|
|
|
use IteratorAggregate; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* Headers class is a container for HTTP headers. |
35
|
|
|
*/ |
36
|
|
|
class Headers implements IteratorAggregate, Countable |
37
|
|
|
{ |
38
|
|
|
protected const STRING_UPPER = '_ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
39
|
|
|
protected const STRING_LOWER = '-abcdefghijklmnopqrstuvwxyz'; |
40
|
|
|
|
41
|
|
|
/** |
42
|
|
|
* An array of HTTP headers. |
43
|
|
|
* |
44
|
|
|
* @var array $herders |
45
|
|
|
*/ |
46
|
|
|
protected $headers = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* Specifies the directives for the caching mechanisms in both |
50
|
|
|
* the requests and the responses. |
51
|
|
|
* |
52
|
|
|
* @var array $cacheControl |
53
|
|
|
*/ |
54
|
|
|
protected $cacheControl = []; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Constructor. The Headers class instance. |
58
|
|
|
* |
59
|
|
|
* @param array $headers |
60
|
|
|
* |
61
|
|
|
* @return void |
62
|
|
|
*/ |
63
|
|
|
public function __construct(array $headers = []) |
64
|
|
|
{ |
65
|
|
|
foreach ($headers as $key => $values) { |
66
|
|
|
$this->set($key, $values); |
67
|
|
|
} |
68
|
|
|
} |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Returns all the headers. |
72
|
|
|
* |
73
|
|
|
* @param string|null $key The name of the headers |
74
|
|
|
* |
75
|
|
|
* @return array |
76
|
|
|
*/ |
77
|
|
|
public function all(string $key = null): array |
78
|
|
|
{ |
79
|
|
|
if (null !== $key) { |
80
|
|
|
return $this->headers[strtr($key, self::STRING_UPPER, self::STRING_LOWER)] ?? []; |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
return $this->headers; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Returns the parameter keys. |
88
|
|
|
* |
89
|
|
|
* @return array An array of parameter keys |
90
|
|
|
*/ |
91
|
|
|
public function keys(): array |
92
|
|
|
{ |
93
|
|
|
return array_keys($this->all()); |
94
|
|
|
} |
95
|
|
|
|
96
|
|
|
/** |
97
|
|
|
* Replaces the current HTTP headers by a new set. |
98
|
|
|
* |
99
|
|
|
* @param array $headers |
100
|
|
|
* |
101
|
|
|
* @return void |
102
|
|
|
*/ |
103
|
|
|
public function replace(array $headers = []): void |
104
|
|
|
{ |
105
|
|
|
$this->headers = []; |
106
|
|
|
$this->add($headers); |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
|
|
* Adds multiple header to the queue. |
111
|
|
|
* |
112
|
|
|
* @param array $headers The header name |
113
|
|
|
* |
114
|
|
|
* @return mixed |
115
|
|
|
*/ |
116
|
|
|
public function add(array $headers = []) |
117
|
|
|
{ |
118
|
|
|
foreach ($headers as $key => $values) { |
119
|
|
|
$this->set($key, $values); |
120
|
|
|
} |
121
|
|
|
} |
122
|
|
|
|
123
|
|
|
/** |
124
|
|
|
* Gets a header value by name. |
125
|
|
|
* |
126
|
|
|
* @param string $key The header name, or null for all headers |
127
|
|
|
* @param string|null $default The default value |
128
|
|
|
* |
129
|
|
|
* @return mixed |
130
|
|
|
*/ |
131
|
|
|
public function get(string $key, string $default = null): ?string |
132
|
|
|
{ |
133
|
|
|
$headers = $this->all($key); |
134
|
|
|
|
135
|
|
|
if ( ! $headers) { |
|
|
|
|
136
|
|
|
return $default; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
if (null === $headers[0]) { |
140
|
|
|
return null; |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
return (string) $headers[0]; |
144
|
|
|
} |
145
|
|
|
|
146
|
|
|
/** |
147
|
|
|
* Sets a header by name. |
148
|
|
|
* |
149
|
|
|
* @param string $key The header name |
150
|
|
|
* @param string|string[]|null $values The value or an array of values |
151
|
|
|
* @param bool $replace If you want to replace the value exists by the header, |
152
|
|
|
* it is not overwritten / overwritten when it is false |
153
|
|
|
* |
154
|
|
|
* @return void |
155
|
|
|
*/ |
156
|
|
|
public function set(string $key, $values, bool $replace = true): void |
157
|
|
|
{ |
158
|
|
|
$key = strtr($key, self::STRING_UPPER, self::STRING_LOWER); |
159
|
|
|
|
160
|
|
|
if (is_array($values)) { |
161
|
|
|
$values = array_values($values); |
162
|
|
|
|
163
|
|
|
if (true === $replace || ! isset($this->headers[$key])) { |
164
|
|
|
$this->headers[$key] = $values; |
165
|
|
|
} else { |
166
|
|
|
$this->headers[$key] = array_merge($this->headers[$key], $values); |
167
|
|
|
} |
168
|
|
|
} else { |
169
|
|
|
if (true === $replace || ! isset($this->headers[$key])) { |
170
|
|
|
$this->headers[$key] = [$values]; |
171
|
|
|
} else { |
172
|
|
|
$this->headers[$key][] = $values; |
173
|
|
|
} |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
if ('cache-control' === $key) { |
177
|
|
|
$this->cacheControl = $this->parseCacheControl(implode(', ', $this->headers[$key])); |
178
|
|
|
} |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
/** |
182
|
|
|
* Returns true if the HTTP header is defined. |
183
|
|
|
* |
184
|
|
|
* @param string $key The HTTP header |
185
|
|
|
* |
186
|
|
|
* @return bool true if the parameter exists, false otherwise |
187
|
|
|
*/ |
188
|
|
|
public function has(string $key): bool |
189
|
|
|
{ |
190
|
|
|
return array_key_exists(strtr($key, self::STRING_UPPER, self::STRING_LOWER), $this->all()); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* Removes a header. |
195
|
|
|
* |
196
|
|
|
* @param string $name The header name |
197
|
|
|
* |
198
|
|
|
* @return mixed |
199
|
|
|
*/ |
200
|
|
|
public function remove(string $key) |
201
|
|
|
{ |
202
|
|
|
$key = strtr($key, self::STRING_UPPER, self::STRING_LOWER); |
203
|
|
|
|
204
|
|
|
unset($this->headers[$key]); |
205
|
|
|
|
206
|
|
|
if ('cache-control' === $key) { |
207
|
|
|
$this->cacheControl = []; |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Returns the HTTP header value converted to a date. |
213
|
|
|
* |
214
|
|
|
* @param string $key |
215
|
|
|
* @param DateTime|null $default |
216
|
|
|
* |
217
|
|
|
* @throws \RuntimeException When the HTTP header is not parseable |
218
|
|
|
*/ |
219
|
|
|
public function getDate(string $key, DateTime $default = null): ?DateTimeInterface |
220
|
|
|
{ |
221
|
|
|
if (null === $value = $this->get($key)) { |
|
|
|
|
222
|
|
|
return $default; |
223
|
|
|
} |
224
|
|
|
|
225
|
|
|
if (false === $date = DateTime::createFromFormat(DATE_RFC2822, $value)) { |
226
|
|
|
throw new RuntimeException(sprintf('The "%s" HTTP header is not parseable (%s).', $key, $value)); |
227
|
|
|
} |
228
|
|
|
|
229
|
|
|
return $date; |
230
|
|
|
} |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* Returns an iterator for headers. |
234
|
|
|
* |
235
|
|
|
* @return \ArrayIterator An \ArrayIterator instance |
236
|
|
|
*/ |
237
|
|
|
public function getIterator(): Traversable |
238
|
|
|
{ |
239
|
|
|
return new ArrayIterator($this->headers); |
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
/** |
243
|
|
|
* Returns the number of headers. |
244
|
|
|
* |
245
|
|
|
* @return int The number of headers |
246
|
|
|
*/ |
247
|
|
|
public function count(): int |
248
|
|
|
{ |
249
|
|
|
return count($this->headers); |
250
|
|
|
} |
251
|
|
|
|
252
|
|
|
/** |
253
|
|
|
* Parses a Cache-Control HTTP header. |
254
|
|
|
* |
255
|
|
|
* @param string $header The value of the Cache-Control HTTP header |
256
|
|
|
* |
257
|
|
|
* @return array An array representing the attribute values |
258
|
|
|
*/ |
259
|
|
|
protected function parseCacheControl($header): array |
260
|
|
|
{ |
261
|
|
|
$cacheControl = []; |
262
|
|
|
|
263
|
|
|
preg_match_all('~([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?~', $header, $matches, PREG_SET_ORDER); |
264
|
|
|
|
265
|
|
|
foreach ($matches as $match) { |
266
|
|
|
$cacheControl[strtolower($match[1])] = isset($match[3]) ? $match[3] : (isset($match[2]) ? $match[2] : true); |
267
|
|
|
} |
268
|
|
|
|
269
|
|
|
return $cacheControl; |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Magic method. |
274
|
|
|
* |
275
|
|
|
* Returns the headers as a string. |
276
|
|
|
* |
277
|
|
|
* @return string The headers |
278
|
|
|
*/ |
279
|
|
|
public function __toString(): string |
280
|
|
|
{ |
281
|
|
|
if ( ! $headers = $this->all()) { |
282
|
|
|
return ''; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
ksort($headers); |
286
|
|
|
|
287
|
|
|
$max = max(array_map('strlen', array_keys($headers))) + 1; |
288
|
|
|
$content = ''; |
289
|
|
|
|
290
|
|
|
foreach ($headers as $name => $values) { |
291
|
|
|
$name = ucwords($name, '-'); |
292
|
|
|
|
293
|
|
|
foreach ($values as $value) { |
294
|
|
|
$content .= sprintf("%-{$max}s %s\r\n", $name.':', $value); |
295
|
|
|
} |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
return $content; |
299
|
|
|
} |
300
|
|
|
} |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.