272 lines
8 KiB
PHP
272 lines
8 KiB
PHP
<?php
|
|
// HttpResponseBuilder.php
|
|
// Created: 2022-02-08
|
|
// Updated: 2023-01-10
|
|
|
|
namespace Index\Http;
|
|
|
|
use DateTimeInterface;
|
|
use Index\DateTime;
|
|
use Index\UrlEncoding;
|
|
use Index\MediaType;
|
|
use Index\Performance\Timings;
|
|
|
|
class HttpResponseBuilder extends HttpMessageBuilder {
|
|
private int $statusCode = -1;
|
|
private ?string $statusText;
|
|
private array $vary = [];
|
|
|
|
public function getStatusCode(): int {
|
|
return $this->statusCode < 0 ? 200 : $this->statusCode;
|
|
}
|
|
|
|
public function setStatusCode(int $statusCode): void {
|
|
$this->statusCode = $statusCode;
|
|
}
|
|
|
|
public function hasStatusCode(): bool {
|
|
return $this->statusCode >= 100;
|
|
}
|
|
|
|
public function getStatusText(): string {
|
|
return $this->statusText ?? self::STATUS[$this->getStatusCode()] ?? 'Unknown Status';
|
|
}
|
|
|
|
public function setStatusText(string $statusText): void {
|
|
$this->statusText = (string)$statusText;
|
|
}
|
|
|
|
public function clearStatusText(): void {
|
|
$this->statusText = null;
|
|
}
|
|
|
|
public function addCookie(
|
|
string $name,
|
|
mixed $value,
|
|
DateTimeInterface|int|null $expires = null,
|
|
string $path = '',
|
|
string $domain = '',
|
|
bool $secure = false,
|
|
bool $httpOnly = false,
|
|
bool $sameSiteStrict = false
|
|
): void {
|
|
$cookie = rawurlencode($name) . '=' . rawurlencode($value)
|
|
. '; SameSite=' . ($sameSiteStrict ? 'Strict' : 'Lax');
|
|
|
|
if($expires !== null) {
|
|
if(!($expires instanceof DateTime)) {
|
|
if(is_int($expires))
|
|
$expires = DateTime::fromUnixTimeSeconds($expires);
|
|
else
|
|
$expires = DateTime::createFromInterface($expires);
|
|
}
|
|
|
|
$cookie .= '; Expires=' . $expires->toCookieString();
|
|
}
|
|
|
|
if(!empty($domain))
|
|
$cookie .= '; Domain=' . $domain;
|
|
|
|
if(!empty($path))
|
|
$cookie .= '; Path=' . $path;
|
|
|
|
if($secure)
|
|
$cookie .= '; Secure';
|
|
|
|
if($httpOnly)
|
|
$cookie .= '; HttpOnly';
|
|
|
|
$this->addHeader('Set-Cookie', $cookie);
|
|
}
|
|
|
|
public function removeCookie(
|
|
string $name,
|
|
string $path = '',
|
|
string $domain = '',
|
|
bool $secure = false,
|
|
bool $httpOnly = false,
|
|
bool $sameSiteStrict = false
|
|
): void {
|
|
$this->addCookie($name, '', -9001, $path, $domain, $secure, $httpOnly, $sameSiteStrict);
|
|
}
|
|
|
|
public function redirect(string $to, bool $permanent = false): void {
|
|
$this->setStatusCode($permanent ? 301 : 302);
|
|
$this->setHeader('Location', $to);
|
|
}
|
|
|
|
public function addVary(string|array $headers): void {
|
|
if(!is_array($headers))
|
|
$headers = [$headers];
|
|
|
|
foreach($headers as $header) {
|
|
$header = $header;
|
|
if(!in_array($header, $this->vary))
|
|
$this->vary[] = $header;
|
|
}
|
|
|
|
$this->setHeader('Vary', implode(', ', $this->vary));
|
|
}
|
|
|
|
public function setPoweredBy(string $poweredBy): void {
|
|
$this->setHeader('X-Powered-By', $poweredBy);
|
|
}
|
|
|
|
public function setEntityTag(string $eTag, bool $weak = false): void {
|
|
$eTag = '"' . $eTag . '"';
|
|
|
|
if($weak)
|
|
$eTag = 'W/' . $eTag;
|
|
|
|
$this->setHeader('ETag', $eTag);
|
|
}
|
|
|
|
public function setServerTiming(Timings $timings): void {
|
|
$laps = $timings->getLaps();
|
|
$timings = [];
|
|
|
|
foreach($laps as $lap) {
|
|
$timing = $lap->getName();
|
|
if($lap->hasComment())
|
|
$timing .= ';desc="' . strtr($lap->getComment(), ['"' => '\\"']) . '"';
|
|
|
|
$timing .= ';dur=' . round($lap->getDurationTime(), 5);
|
|
$timings[] = $timing;
|
|
}
|
|
|
|
$this->setHeader('Server-Timing', implode(', ', $timings));
|
|
}
|
|
|
|
public function hasContentType(): bool {
|
|
return $this->hasHeader('Content-Type');
|
|
}
|
|
|
|
public function setContentType(MediaType|string $mediaType): void {
|
|
$this->setHeader('Content-Type', (string)$mediaType);
|
|
}
|
|
|
|
public function setTypeStream(): void {
|
|
$this->setContentType('application/octet-stream');
|
|
}
|
|
|
|
public function setTypePlain(string $charset = 'us-ascii'): void {
|
|
$this->setContentType('text/plain; charset=' . $charset);
|
|
}
|
|
|
|
public function setTypeHTML(string $charset = 'utf-8'): void {
|
|
$this->setContentType('text/html; charset=' . $charset);
|
|
}
|
|
|
|
public function setTypeJson(string $charset = 'utf-8'): void {
|
|
$this->setContentType('application/json; charset=' . $charset);
|
|
}
|
|
|
|
public function setTypeXML(string $charset = 'utf-8'): void {
|
|
$this->setContentType('application/xml; charset=' . $charset);
|
|
}
|
|
|
|
public function setTypeCSS(string $charset = 'utf-8'): void {
|
|
$this->setContentType('text/css; charset=' . $charset);
|
|
}
|
|
|
|
public function setTypeJS(string $charset = 'utf-8'): void {
|
|
$this->setContentType('application/javascript; charset=' . $charset);
|
|
}
|
|
|
|
public function sendFile(string $absolutePath): void {
|
|
$this->setHeader('X-Sendfile', $absolutePath);
|
|
}
|
|
|
|
public function accelRedirect(string $uri): void {
|
|
$this->setHeader('X-Accel-Redirect', $uri);
|
|
}
|
|
|
|
public function setFileName(string $fileName, bool $attachment = false): void {
|
|
$this->setHeader(
|
|
'Content-Disposition',
|
|
sprintf(
|
|
'%s; filename="%s"',
|
|
($attachment ? 'attachment' : 'inline'),
|
|
$fileName
|
|
)
|
|
);
|
|
}
|
|
|
|
public function clearSiteData(string ...$directives): void {
|
|
$this->setHeader(
|
|
'Clear-Site-Data',
|
|
implode(', ', array_map(fn($directive) => sprintf('"%s"', $directive), $directives))
|
|
);
|
|
}
|
|
|
|
public function setCacheControl(string ...$directives): void {
|
|
$this->setHeader('Cache-Control', implode(', ', $directives));
|
|
}
|
|
|
|
public function toResponse(): HttpResponse {
|
|
return new HttpResponse(
|
|
$this->getHttpVersion(),
|
|
$this->getStatusCode(),
|
|
$this->getStatusText(),
|
|
$this->getHeaders(),
|
|
$this->getContent()
|
|
);
|
|
}
|
|
|
|
private const STATUS = [
|
|
100 => 'Continue',
|
|
101 => 'Switching Protocols',
|
|
103 => 'Early Hints',
|
|
200 => 'OK',
|
|
201 => 'Created',
|
|
202 => 'Accepted',
|
|
203 => 'Non-Authoritative Information',
|
|
204 => 'No Content',
|
|
205 => 'Reset Content',
|
|
206 => 'Partial Content',
|
|
300 => 'Multiple Choices',
|
|
301 => 'Moved Permanently',
|
|
302 => 'Found',
|
|
303 => 'See Other',
|
|
304 => 'Not Modified',
|
|
307 => 'Temporary Redirect',
|
|
308 => 'Permanent Redirect',
|
|
400 => 'Bad Request',
|
|
401 => 'Unauthorized',
|
|
402 => 'Payment Required',
|
|
403 => 'Forbidden',
|
|
404 => 'Not Found',
|
|
405 => 'Method Not Allowed',
|
|
406 => 'Not Acceptable',
|
|
407 => 'Proxy Authentication Required',
|
|
408 => 'Request Timeout',
|
|
409 => 'Conflict',
|
|
410 => 'Gone',
|
|
411 => 'Length Required',
|
|
412 => 'Precondition Required',
|
|
413 => 'Payload Too Large',
|
|
414 => 'URI Too Long',
|
|
415 => 'Unsupported Media Type',
|
|
416 => 'Range Not Satisfiable',
|
|
417 => 'Expectation Failed',
|
|
418 => 'I\'m a teapot',
|
|
422 => 'Unprocessable Entity',
|
|
425 => 'Too Early',
|
|
426 => 'Upgrade Required',
|
|
428 => 'Precondition Required',
|
|
429 => 'Too Many Requests',
|
|
431 => 'Request Header Fields Too Large',
|
|
451 => 'Unavailable For Legal Reasons',
|
|
500 => 'Internal Server Error',
|
|
501 => 'Not Implemented',
|
|
502 => 'Bad Gateway',
|
|
503 => 'Service Unavailable',
|
|
504 => 'Gateway Timeout',
|
|
505 => 'HTTP Version Not Supported',
|
|
506 => 'Variant Also Negotiates',
|
|
507 => 'Insufficient Storage',
|
|
508 => 'Loop Detected',
|
|
510 => 'Not Extended',
|
|
511 => 'Network Authentication Required',
|
|
];
|
|
}
|