Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
220 / 220 |
|
100.00% |
57 / 57 |
CRAP | |
100.00% |
1 / 1 |
Response | |
100.00% |
220 / 220 |
|
100.00% |
57 / 57 |
99 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
getRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBody | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
setBody | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
prependBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
appendBody | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCookie | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCookies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeCookie | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeCookies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
appendHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeHeader | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeHeaders | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setCsp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setCspReportOnly | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCsp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCspReportOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasCsp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
hasCspReportOnly | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeCsp | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
removeCspReportOnly | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getStatus | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setStatus | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
setStatusCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getStatusCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isStatusCode | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setStatusReason | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getStatusReason | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSent | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
redirect | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
7 | |||
send | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
sendAll | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
3 | |||
sendBody | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
sendCookies | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
sendHeaders | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
7 | |||
negotiateCsp | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
negotiateContentType | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
5 | |||
negotiateEtag | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
setJson | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
setCache | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
setNoCache | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getCacheSeconds | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setAutoEtag | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isAutoEtag | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setContentLanguage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setContentEncoding | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setContentLength | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setDate | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setEtag | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
setExpires | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setLastModified | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
setNotModified | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
setDebugCollector | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 |
1 | <?php declare(strict_types=1); |
2 | /* |
3 | * This file is part of Aplus Framework HTTP Library. |
4 | * |
5 | * (c) Natan Felles <natanfelles@gmail.com> |
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 Framework\HTTP\Debug\HTTPCollector; |
16 | use InvalidArgumentException; |
17 | use JetBrains\PhpStorm\Pure; |
18 | use JsonException; |
19 | use LogicException; |
20 | |
21 | /** |
22 | * Class Response. |
23 | * |
24 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#HTTP_Responses |
25 | * |
26 | * @package http |
27 | */ |
28 | class Response extends Message implements ResponseInterface |
29 | { |
30 | use ResponseDownload; |
31 | |
32 | protected int $cacheSeconds = 0; |
33 | protected bool $isSent = false; |
34 | protected Request $request; |
35 | /** |
36 | * HTTP Response Status Code. |
37 | */ |
38 | protected int $statusCode = Status::OK; |
39 | /** |
40 | * HTTP Response Status Reason. |
41 | */ |
42 | protected string $statusReason = 'OK'; |
43 | protected ?string $sendedBody = null; |
44 | protected bool $inToString = false; |
45 | protected bool $autoEtag = false; |
46 | protected string $autoEtagHashAlgo = 'xxh3'; |
47 | protected HTTPCollector $debugCollector; |
48 | protected CSP $csp; |
49 | protected CSP $cspReportOnly; |
50 | |
51 | /** |
52 | * Response constructor. |
53 | * |
54 | * @param Request $request |
55 | */ |
56 | public function __construct(Request $request) |
57 | { |
58 | $this->request = $request; |
59 | $this->setProtocol($this->request->getProtocol()); |
60 | } |
61 | |
62 | public function __toString() : string |
63 | { |
64 | if ($this->getHeader(Header::DATE) === null) { |
65 | $this->setDate(new DateTime()); |
66 | } |
67 | if ($this->getHeader(Header::CONTENT_TYPE) === null) { |
68 | $this->setContentType('text/html'); |
69 | } |
70 | if ($this->hasDownload()) { |
71 | $this->inToString = true; |
72 | $this->sendDownload(); |
73 | $this->inToString = false; |
74 | } |
75 | return parent::__toString(); |
76 | } |
77 | |
78 | /** |
79 | * @return Request |
80 | */ |
81 | #[Pure] |
82 | public function getRequest() : Request |
83 | { |
84 | return $this->request; |
85 | } |
86 | |
87 | /** |
88 | * Get the Response body. |
89 | * |
90 | * @return string |
91 | */ |
92 | public function getBody() : string |
93 | { |
94 | if ($this->sendedBody !== null) { |
95 | return $this->sendedBody; |
96 | } |
97 | $buffer = ''; |
98 | if (\ob_get_length()) { |
99 | $buffer = \ob_get_contents(); |
100 | \ob_clean(); |
101 | } |
102 | return $this->body = parent::getBody() . $buffer; |
103 | } |
104 | |
105 | /** |
106 | * Set the Response body. |
107 | * |
108 | * @param string $body |
109 | * |
110 | * @return static |
111 | */ |
112 | public function setBody(string $body) : static |
113 | { |
114 | if (\ob_get_length()) { |
115 | \ob_clean(); |
116 | } |
117 | return parent::setBody($body); |
118 | } |
119 | |
120 | /** |
121 | * Prepend a string to the body. |
122 | * |
123 | * @param string $content |
124 | * |
125 | * @return static |
126 | */ |
127 | public function prependBody(string $content) : static |
128 | { |
129 | return parent::setBody($content . $this->getBody()); |
130 | } |
131 | |
132 | /** |
133 | * Append a string to the body. |
134 | * |
135 | * @param string $content |
136 | * |
137 | * @return static |
138 | */ |
139 | public function appendBody(string $content) : static |
140 | { |
141 | return parent::setBody($this->getBody() . $content); |
142 | } |
143 | |
144 | public function setCookie(Cookie $cookie) : static |
145 | { |
146 | return parent::setCookie($cookie); |
147 | } |
148 | |
149 | public function setCookies(array $cookies) : static |
150 | { |
151 | return parent::setCookies($cookies); |
152 | } |
153 | |
154 | public function removeCookie(string $name) : static |
155 | { |
156 | return parent::removeCookie($name); |
157 | } |
158 | |
159 | public function removeCookies(array $names) : static |
160 | { |
161 | return parent::removeCookies($names); |
162 | } |
163 | |
164 | public function setHeader(string $name, string $value) : static |
165 | { |
166 | return parent::setHeader($name, $value); |
167 | } |
168 | |
169 | public function setHeaders(array $headers) : static |
170 | { |
171 | return parent::setHeaders($headers); |
172 | } |
173 | |
174 | public function appendHeader(string $name, string $value) : static |
175 | { |
176 | return parent::appendHeader($name, $value); |
177 | } |
178 | |
179 | public function removeHeader(string $name) : static |
180 | { |
181 | return parent::removeHeader($name); |
182 | } |
183 | |
184 | public function removeHeaders() : static |
185 | { |
186 | return parent::removeHeaders(); |
187 | } |
188 | |
189 | /** |
190 | * Set the Content-Security-Policy instance. |
191 | * |
192 | * @param CSP $csp |
193 | * |
194 | * @return static |
195 | */ |
196 | public function setCsp(CSP $csp) : static |
197 | { |
198 | $this->csp = $csp; |
199 | return $this; |
200 | } |
201 | |
202 | /** |
203 | * Set the Content-Security-Policy-Report-Only instance. |
204 | * |
205 | * @param CSP $csp |
206 | * |
207 | * @return static |
208 | */ |
209 | public function setCspReportOnly(CSP $csp) : static |
210 | { |
211 | $this->cspReportOnly = $csp; |
212 | return $this; |
213 | } |
214 | |
215 | /** |
216 | * Get the Content-Security-Policy instance or null. |
217 | * |
218 | * @return CSP|null |
219 | */ |
220 | public function getCsp() : CSP | null |
221 | { |
222 | return $this->csp ?? null; |
223 | } |
224 | |
225 | /** |
226 | * Get the Content-Security-Policy-Report-Only instance or null. |
227 | * |
228 | * @return CSP|null |
229 | */ |
230 | public function getCspReportOnly() : CSP | null |
231 | { |
232 | return $this->cspReportOnly ?? null; |
233 | } |
234 | |
235 | /** |
236 | * Tells if the Content-Security-Policy instance has been set. |
237 | * |
238 | * @return bool |
239 | */ |
240 | public function hasCsp() : bool |
241 | { |
242 | return isset($this->csp); |
243 | } |
244 | |
245 | /** |
246 | * Tells if the Content-Security-Policy-Report-Only instance has been set. |
247 | * |
248 | * @return bool |
249 | */ |
250 | public function hasCspReportOnly() : bool |
251 | { |
252 | return isset($this->cspReportOnly); |
253 | } |
254 | |
255 | /** |
256 | * Remove the Content-Security-Policy instance. |
257 | * |
258 | * @return static |
259 | */ |
260 | public function removeCsp() : static |
261 | { |
262 | unset($this->csp); |
263 | return $this; |
264 | } |
265 | |
266 | /** |
267 | * Remove the Content-Security-Policy-Report-Only instance. |
268 | * |
269 | * @return static |
270 | */ |
271 | public function removeCspReportOnly() : static |
272 | { |
273 | unset($this->cspReportOnly); |
274 | return $this; |
275 | } |
276 | |
277 | /** |
278 | * Get the status line without the protocol part. |
279 | * |
280 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Messages#status_line |
281 | * |
282 | * @return string |
283 | */ |
284 | #[Pure] |
285 | public function getStatus() : string |
286 | { |
287 | return "{$this->statusCode} {$this->statusReason}"; |
288 | } |
289 | |
290 | /** |
291 | * Set the status part in the start-line. |
292 | * |
293 | * @param int $code |
294 | * @param string|null $reason |
295 | * |
296 | * @throws InvalidArgumentException if status code is invalid |
297 | * @throws LogicException is status code is unknown and a reason is not set |
298 | * |
299 | * @return static |
300 | */ |
301 | public function setStatus(int $code, string $reason = null) : static |
302 | { |
303 | $this->setStatusCode($code); |
304 | $reason ?: $reason = Status::getReason($code); |
305 | $this->setStatusReason($reason); |
306 | return $this; |
307 | } |
308 | |
309 | /** |
310 | * Set the status code. |
311 | * |
312 | * @param int $code |
313 | * |
314 | * @throws InvalidArgumentException if status code is invalid |
315 | * |
316 | * @return static |
317 | */ |
318 | public function setStatusCode(int $code) : static |
319 | { |
320 | return parent::setStatusCode($code); |
321 | } |
322 | |
323 | /** |
324 | * Get the status code. |
325 | * |
326 | * @return int |
327 | */ |
328 | #[Pure] |
329 | public function getStatusCode() : int |
330 | { |
331 | return parent::getStatusCode(); |
332 | } |
333 | |
334 | /** |
335 | * @param int $code |
336 | * |
337 | * @throws InvalidArgumentException if status code is invalid |
338 | * |
339 | * @return bool |
340 | */ |
341 | public function isStatusCode(int $code) : bool |
342 | { |
343 | return parent::isStatusCode($code); |
344 | } |
345 | |
346 | /** |
347 | * Set a custom status reason. |
348 | * |
349 | * @param string $reason |
350 | * |
351 | * @return static |
352 | */ |
353 | public function setStatusReason(string $reason) : static |
354 | { |
355 | $this->statusReason = $reason; |
356 | return $this; |
357 | } |
358 | |
359 | /** |
360 | * Get the status reason. |
361 | * |
362 | * @return string |
363 | */ |
364 | #[Pure] |
365 | public function getStatusReason() : string |
366 | { |
367 | return $this->statusReason; |
368 | } |
369 | |
370 | /** |
371 | * Say if the response was sent. |
372 | * |
373 | * @return bool |
374 | */ |
375 | #[Pure] |
376 | public function isSent() : bool |
377 | { |
378 | return $this->isSent; |
379 | } |
380 | |
381 | /** |
382 | * Sets the HTTP Redirect Response with data accessible in the next HTTP Request. |
383 | * |
384 | * @param string $location Location Header value |
385 | * @param array|mixed[] $data Session data available on next Request |
386 | * @param int|null $code HTTP Redirect status code. Leave null to determine |
387 | * based on the current HTTP method. |
388 | * |
389 | * @see http://en.wikipedia.org/wiki/Post/Redirect/Get |
390 | * @see Request::getRedirectData() |
391 | * |
392 | * @throws InvalidArgumentException for invalid Redirection code |
393 | * @throws LogicException if PHP Session is not active to set redirect data |
394 | * |
395 | * @return static |
396 | */ |
397 | public function redirect(string $location, array $data = [], int $code = null) : static |
398 | { |
399 | if ($code === null) { |
400 | $code = $this->request->getMethod() === Method::GET |
401 | ? Status::TEMPORARY_REDIRECT |
402 | : Status::SEE_OTHER; |
403 | } elseif ($code < 300 || $code > 399) { |
404 | throw new InvalidArgumentException("Invalid Redirection code: {$code}"); |
405 | } |
406 | $this->setStatus($code); |
407 | $this->setHeader(ResponseHeader::LOCATION, $location); |
408 | if ($data) { |
409 | if (\session_status() !== \PHP_SESSION_ACTIVE) { |
410 | throw new LogicException('Session must be active to set redirect data'); |
411 | } |
412 | $_SESSION['$']['redirect_data'] = $data; |
413 | } |
414 | return $this; |
415 | } |
416 | |
417 | /** |
418 | * Send the Response headers, cookies and body to the output. |
419 | * |
420 | * @throws LogicException if Response is already sent |
421 | */ |
422 | public function send() : void |
423 | { |
424 | if (isset($this->debugCollector)) { |
425 | $start = \microtime(true); |
426 | $this->sendAll(); |
427 | $end = \microtime(true); |
428 | $this->debugCollector->addData([ |
429 | 'start' => $start, |
430 | 'end' => $end, |
431 | 'message' => 'response', |
432 | 'type' => 'send', |
433 | ]); |
434 | return; |
435 | } |
436 | $this->sendAll(); |
437 | } |
438 | |
439 | protected function sendAll() : void |
440 | { |
441 | if ($this->isSent) { |
442 | throw new LogicException('Response is already sent'); |
443 | } |
444 | $this->sendHeaders(); |
445 | $this->sendCookies(); |
446 | $this->hasDownload() ? $this->sendDownload() : $this->sendBody(); |
447 | $this->isSent = true; |
448 | } |
449 | |
450 | protected function sendBody() : void |
451 | { |
452 | echo $this->sendedBody = $this->getBody(); |
453 | $this->body = ''; |
454 | } |
455 | |
456 | protected function sendCookies() : void |
457 | { |
458 | foreach ($this->cookies as $cookie) { |
459 | $cookie->send(); |
460 | } |
461 | } |
462 | |
463 | /** |
464 | * Send the HTTP headers to the output. |
465 | * |
466 | * @throws LogicException if headers are already sent |
467 | */ |
468 | protected function sendHeaders() : void |
469 | { |
470 | if (\headers_sent()) { |
471 | throw new LogicException('Headers are already sent'); |
472 | } |
473 | if ($this->getHeader(Header::DATE) === null) { |
474 | $this->setDate(new DateTime()); |
475 | } |
476 | if ($this->getHeader(Header::CONTENT_TYPE) === null) { |
477 | $this->negotiateContentType(); |
478 | } |
479 | if ($this->isAutoEtag() && ! $this->hasDownload()) { |
480 | $this->negotiateEtag(); |
481 | } |
482 | $this->negotiateCsp(); |
483 | \header($this->getStartLine()); |
484 | foreach ($this->getHeaderLines() as $line) { |
485 | \header($line); |
486 | } |
487 | } |
488 | |
489 | /** |
490 | * Set the Content-Security-Policy and Content-Security-Policy-Report-Only |
491 | * headers if the CSP classes are set and the response has not downloads. |
492 | * |
493 | * @return void |
494 | */ |
495 | protected function negotiateCsp() : void |
496 | { |
497 | $csp = $this->getCsp(); |
498 | if ($csp && ! $this->hasDownload()) { |
499 | $this->setHeader( |
500 | ResponseHeader::CONTENT_SECURITY_POLICY, |
501 | $csp->render() |
502 | ); |
503 | } |
504 | $csp = $this->getCspReportOnly(); |
505 | if ($csp && ! $this->hasDownload()) { |
506 | $this->setHeader( |
507 | ResponseHeader::CONTENT_SECURITY_POLICY_REPORT_ONLY, |
508 | $csp->render() |
509 | ); |
510 | } |
511 | } |
512 | |
513 | /** |
514 | * Negotiates the Content-Type header, setting the MIME type "text/html" if |
515 | * the response body is not empty. |
516 | * |
517 | * If the response body is empty, it checks servers to set the header to an |
518 | * empty value, which causes the server to remove the Content-Type header, |
519 | * and it will not appear to the client from the request. |
520 | * |
521 | * The header will also not be set on the PHP Development Server when the |
522 | * body is empty. |
523 | * |
524 | * This prevents the Content-Type from appearing without it being needed in, |
525 | * for example, REST API responses. |
526 | * |
527 | * @see https://stackoverflow.com/a/21029402/6027968 |
528 | */ |
529 | protected function negotiateContentType() : void |
530 | { |
531 | if ($this->getBody() !== '') { |
532 | $this->setContentType('text/html'); |
533 | return; |
534 | } |
535 | $software = (string) $this->getRequest()->getServer('SERVER_SOFTWARE'); |
536 | $software = \strtolower($software); |
537 | // These servers remove headers if they are set to an empty value: |
538 | $servers = [ |
539 | 'apache', |
540 | 'lighttpd', |
541 | 'nginx', |
542 | ]; |
543 | foreach ($servers as $server) { |
544 | if (\str_contains($software, $server)) { |
545 | $this->setHeader(Header::CONTENT_TYPE, ''); |
546 | return; |
547 | } |
548 | } |
549 | // Prevent PHP Development Server from setting the default Content-Type: |
550 | if (\str_contains($software, 'php')) { |
551 | \ini_set('default_mimetype', ''); |
552 | } |
553 | } |
554 | |
555 | /** |
556 | * Set the ETag header, based on the Response body, and start the |
557 | * negotiation. |
558 | * |
559 | * - Empty the body and set a status 304 (Not Modified) if the Request |
560 | * If-None-Match header has the same value of the generated ETag, on GET and |
561 | * HEAD requests. |
562 | * |
563 | * - Empty the body and set a status 412 (Precondition Failed) if the Request |
564 | * If-Match header is set and has not the same value of the generated ETag, |
565 | * on non-GET or non-HEAD requests. |
566 | * |
567 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag |
568 | */ |
569 | protected function negotiateEtag() : void |
570 | { |
571 | // Content-Length is required by Firefox, |
572 | // otherwise it does not send the If-None-Match header |
573 | $this->setContentLength(\strlen($this->getBody())); |
574 | $etag = \hash($this->autoEtagHashAlgo, $this->getBody()); |
575 | $this->setEtag($etag); |
576 | $etag = '"' . $etag . '"'; |
577 | // Cache of unchanged resources: |
578 | $ifNoneMatch = $this->getRequest()->getHeader(RequestHeader::IF_NONE_MATCH); |
579 | if ($ifNoneMatch !== null |
580 | && ($ifNoneMatch === $etag || $ifNoneMatch === 'W/' . $etag) |
581 | && \in_array( |
582 | $this->getRequest()->getMethod(), |
583 | [Method::GET, Method::HEAD], |
584 | true |
585 | ) |
586 | ) { |
587 | $this->setNotModified(); |
588 | $this->setBody(''); |
589 | return; |
590 | } |
591 | // Avoid mid-air collisions: |
592 | $ifMatch = $this->getRequest()->getHeader(RequestHeader::IF_MATCH); |
593 | if ($ifMatch !== null && $ifMatch !== $etag) { |
594 | $this->setBody(''); |
595 | $this->setStatus(Status::PRECONDITION_FAILED); |
596 | } |
597 | } |
598 | |
599 | /** |
600 | * Set Response body and Content-Type as JSON. |
601 | * |
602 | * @param mixed $data The data being encoded. Can be any type except |
603 | * a resource. |
604 | * @param int|null $flags <p> |
605 | * Bitmask consisting of |
606 | * {@see \JSON_HEX_QUOT}<br/> |
607 | * {@see \JSON_HEX_TAG}<br/> |
608 | * {@see \JSON_HEX_AMP}<br/> |
609 | * {@see \JSON_HEX_APOS}<br/> |
610 | * {@see \JSON_NUMERIC_CHECK}<br/> |
611 | * {@see \JSON_PRETTY_PRINT}<br/> |
612 | * {@see \JSON_UNESCAPED_SLASHES}<br/> |
613 | * {@see \JSON_FORCE_OBJECT}<br/> |
614 | * {@see \JSON_UNESCAPED_UNICODE}<br/> |
615 | * {@see \JSON_THROW_ON_ERROR}<br/> |
616 | * The behaviour of these constants is described on the JSON constants page. |
617 | * </p> |
618 | * <p>Default is <b>JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE</b> |
619 | * when null. Set 0 to do not use none.</p> |
620 | * @param int<1,max> $depth Set the maximum depth. Must be greater than zero. |
621 | * |
622 | * @see https://www.php.net/manual/en/function.json-encode.php |
623 | * @see https://www.php.net/manual/en/json.constants.php |
624 | * |
625 | * @throws JsonException if json_encode() fails |
626 | * |
627 | * @return static |
628 | */ |
629 | public function setJson(mixed $data, int $flags = null, int $depth = 512) : static |
630 | { |
631 | if ($flags === null) { |
632 | $flags = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE; |
633 | } |
634 | $data = \json_encode($data, $flags | \JSON_THROW_ON_ERROR, $depth); |
635 | $this->setContentType('application/json'); |
636 | $this->setBody($data); |
637 | return $this; |
638 | } |
639 | |
640 | /** |
641 | * Set the Cache-Control header. |
642 | * |
643 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control |
644 | * @see https://stackoverflow.com/a/3492459/6027968 |
645 | * |
646 | * @param int $seconds |
647 | * @param bool $public |
648 | * |
649 | * @return static |
650 | */ |
651 | public function setCache(int $seconds, bool $public = false) : static |
652 | { |
653 | $date = new DateTime(); |
654 | $date->modify('+' . $seconds . ' seconds'); |
655 | $this->setExpires($date); |
656 | $this->setHeader( |
657 | Header::CACHE_CONTROL, |
658 | ($public ? 'public' : 'private') . ', max-age=' . $seconds |
659 | ); |
660 | $this->cacheSeconds = $seconds; |
661 | return $this; |
662 | } |
663 | |
664 | /** |
665 | * Clear the browser cache. |
666 | * |
667 | * @return static |
668 | */ |
669 | public function setNoCache() : static |
670 | { |
671 | $this->setHeader( |
672 | Header::CACHE_CONTROL, |
673 | 'no-cache, no-store, max-age=0' |
674 | ); |
675 | $this->cacheSeconds = 0; |
676 | return $this; |
677 | } |
678 | |
679 | /** |
680 | * Get the number of seconds the cache is active. |
681 | * |
682 | * @return int |
683 | */ |
684 | #[Pure] |
685 | public function getCacheSeconds() : int |
686 | { |
687 | return $this->cacheSeconds; |
688 | } |
689 | |
690 | /** |
691 | * Enable or disable the capability of auto-add the ETag header and |
692 | * negotiate the response with it. |
693 | * |
694 | * @param bool $active |
695 | * @param string|null $hashAlgo |
696 | * |
697 | * @see Response::negotiateEtag() |
698 | * |
699 | * @return static |
700 | */ |
701 | public function setAutoEtag(bool $active = true, string $hashAlgo = null) : static |
702 | { |
703 | $this->autoEtag = $active; |
704 | if ($hashAlgo !== null) { |
705 | $this->autoEtagHashAlgo = $hashAlgo; |
706 | } |
707 | return $this; |
708 | } |
709 | |
710 | /** |
711 | * @return bool |
712 | */ |
713 | public function isAutoEtag() : bool |
714 | { |
715 | return $this->autoEtag; |
716 | } |
717 | |
718 | /** |
719 | * Set the Content-Type header. |
720 | * |
721 | * @param string $mime |
722 | * @param string $charset |
723 | * |
724 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type |
725 | * |
726 | * @return static |
727 | */ |
728 | public function setContentType(string $mime, string $charset = 'UTF-8') : static |
729 | { |
730 | $this->setHeader( |
731 | Header::CONTENT_TYPE, |
732 | $mime . ($charset ? '; charset=' . $charset : '') |
733 | ); |
734 | return $this; |
735 | } |
736 | |
737 | /** |
738 | * Set the Content-Language header. |
739 | * |
740 | * @param string $language |
741 | * |
742 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Language |
743 | * |
744 | * @return static |
745 | */ |
746 | public function setContentLanguage(string $language) : static |
747 | { |
748 | $this->setHeader(Header::CONTENT_LANGUAGE, $language); |
749 | return $this; |
750 | } |
751 | |
752 | /** |
753 | * Set the Content-Encoding header. |
754 | * |
755 | * @param string $encoding |
756 | * |
757 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding |
758 | * |
759 | * @return static |
760 | */ |
761 | public function setContentEncoding(string $encoding) : static |
762 | { |
763 | $this->setHeader(Header::CONTENT_ENCODING, $encoding); |
764 | return $this; |
765 | } |
766 | |
767 | /** |
768 | * Set the Content-Length header. |
769 | * |
770 | * @param int $length |
771 | * |
772 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Length |
773 | * |
774 | * @return static |
775 | */ |
776 | public function setContentLength(int $length) : static |
777 | { |
778 | $this->setHeader(Header::CONTENT_LENGTH, (string) $length); |
779 | return $this; |
780 | } |
781 | |
782 | /** |
783 | * Set the Date header. |
784 | * |
785 | * @param DateTime $datetime |
786 | * |
787 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date |
788 | * |
789 | * @return static |
790 | */ |
791 | public function setDate(DateTime $datetime) : static |
792 | { |
793 | $date = clone $datetime; |
794 | $date->setTimezone(new DateTimeZone('UTC')); |
795 | $this->setHeader( |
796 | Header::DATE, |
797 | $date->format(DateTimeInterface::RFC7231) |
798 | ); |
799 | return $this; |
800 | } |
801 | |
802 | /** |
803 | * Set the ETag header. |
804 | * |
805 | * @param string $etag |
806 | * @param bool $strong |
807 | * |
808 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag |
809 | * |
810 | * @return static |
811 | */ |
812 | public function setEtag(string $etag, bool $strong = true) : static |
813 | { |
814 | $etag = '"' . $etag . '"'; |
815 | if ($strong === false) { |
816 | $etag = 'W/' . $etag; |
817 | } |
818 | $this->setHeader(ResponseHeader::ETAG, $etag); |
819 | return $this; |
820 | } |
821 | |
822 | /** |
823 | * Set the Expires header. |
824 | * |
825 | * @param DateTime $datetime |
826 | * |
827 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expires |
828 | * |
829 | * @return static |
830 | */ |
831 | public function setExpires(DateTime $datetime) : static |
832 | { |
833 | $date = clone $datetime; |
834 | $date->setTimezone(new DateTimeZone('UTC')); |
835 | $this->setHeader( |
836 | ResponseHeader::EXPIRES, |
837 | $date->format(DateTimeInterface::RFC7231) |
838 | ); |
839 | return $this; |
840 | } |
841 | |
842 | /** |
843 | * Set the Last-Modified header. |
844 | * |
845 | * @param DateTime $datetime |
846 | * |
847 | * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified |
848 | * |
849 | * @return static |
850 | */ |
851 | public function setLastModified(DateTime $datetime) : static |
852 | { |
853 | $date = clone $datetime; |
854 | $date->setTimezone(new DateTimeZone('UTC')); |
855 | $this->setHeader( |
856 | ResponseHeader::LAST_MODIFIED, |
857 | $date->format(DateTimeInterface::RFC7231) |
858 | ); |
859 | return $this; |
860 | } |
861 | |
862 | /** |
863 | * Set the status line as "Not Modified". |
864 | * |
865 | * @return static |
866 | */ |
867 | public function setNotModified() : static |
868 | { |
869 | $this->setStatus(Status::NOT_MODIFIED); |
870 | return $this; |
871 | } |
872 | |
873 | public function setDebugCollector(HTTPCollector $debugCollector) : static |
874 | { |
875 | $this->debugCollector = $debugCollector; |
876 | $this->debugCollector->setResponse($this); |
877 | return $this; |
878 | } |
879 | } |