'Continue', self::HTTP_SWITCHING_PROTOCOLS => 'Switching Protocols', self::HTTP_NON_AUTHORITATIVE_INFORMATION => 'Non-Authoritative Information', self::HTTP_OK => 'OK', self::HTTP_CREATED => 'Created', self::HTTP_ACCEPT => 'Accepted', self::HTTP_NO_CONTENT => 'No Content', self::HTTP_RESET_CONTENT => 'Reset Content', self::HTTP_PARTIAL_CONTENT => 'Partial Content', self::HTTP_MULTIPLE_CHOICES => 'Multiple Choices', self::HTTP_MOVED_PERMANENTLY => 'Moved Permanently', self::HTTP_MOVED_TEMPORARILY => 'Found', self::HTTP_SEE_OTHER => 'See Other', self::HTTP_NOT_MODIFIED => 'Not Modified', self::HTTP_USE_PROXY => 'Use Proxy', self::HTTP_TEMPORARY_REDIRECT => 'Temporary Redirect', self::HTTP_BAD_REQUEST => 'Bad Request', self::HTTP_UNAUTHORIZED => 'Unauthorized', self::HTTP_PAYMENT_REQUIRED => 'Payment Required', self::HTTP_FORBIDDEN => 'Forbidden', self::HTTP_NOT_FOUND => 'Not Found', self::HTTP_METHOD_NOT_ALLOWED => 'Method Not Allowed', self::HTTP_NOT_ACCEPTABLE => 'Not Acceptable', self::HTTP_PROXY_AUTHENTICATION_REQUIRED => 'Proxy Authentication Required', self::HTTP_REQUEST_TIMEOUT => 'Request Timeout', self::HTTP_CONFILICT => 'Conflict', self::HTTP_GONE => 'Gone', self::HTTP_LENGTH_REQUIRED => 'Length Required', self::HTTP_PRECONDITION_FAILED => 'Precondition Failed', self::HTTP_PAYLOAD_TOO_LARGE => 'Payload Too Large', self::HTTP_URI_TOO_LONG => 'URI Too Long', self::HTTP_UNSUPPORTET_MEDIA_TYPE => 'Unsupported Media Type', self::HTTP_RANGE_NOT_SATISFIABLE => 'Range Not Satisfiable', self::HTTP_EXPECTATION_FAILED => 'Expectation Failed', self::HTTP_UPGRADE_REQUIRED => 'Upgrade Required', self::HTTP_INTERNAL_SERVER_ERROR => 'Internal Server Error', self::HTTP_NOT_IMPLEMENTED => 'Not Implemented', self::HTTP_BAD_GATEWAY => 'Bad Gateway', self::HTTP_SERVICE_UNAVAILABLE => 'Service Unavailable', self::HTTP_GATEWAY_TIMEOUT => 'Gateway Timeout', self::HTTP_VERSION_NOT_SUPPORTED => 'HTTP Version Not Supported', ]; /** @var array $oneValueHeaders headers which can hold ONE value ONLY */ protected static $oneValueHeaders = ['content-type', 'content-length', 'content-disposition', 'date']; /** @var array $headers */ protected $headers = []; /** @var array $headerNames */ protected $headerNames = []; /** @var string $content Response content */ protected $content; /** @var int $statusCode HTTP status code */ protected $statusCode; /** @var string $statusText HTTP status text */ protected $statusText; /** @var string $protocolVersion HTTP protocol version */ protected $protocolVersion; /** @var CookieCollection $cookies */ protected $cookies; /** * @param string $content * @param int $statusCode * @param array $headers * @param string $protocolVersion * @param null $statusText * @param CookieCollection|array $cookies */ public function __construct( $content = null, $statusCode = self::HTTP_OK, array $headers = [], $protocolVersion = '1.1', $statusText = null, $cookies = [] ) { $this->statusCode = (int)$statusCode; $this->headers = []; foreach ($headers as $name => $value) { $this->setHeader($name, $value); } if (!$this->hasHeader('content-type')) { $this->setHeader('Content-Type', 'text/html; charset=utf-8'); } $this->setContent($content); $this->protocolVersion = $protocolVersion; $this->statusText = $statusText; $this->setCookies($cookies); } /** * Sends the response headers and content to the client. * * @param DateTimeInterface|null $sendTime * * @return void */ public function send(DateTimeInterface $sendTime = null) { header(sprintf('HTTP/%s %s %s', $this->getProtocolVersion(), $this->getStatusCode(), $this->getStatusText())); if (!$this->hasHeader('date')) { if ($sendTime === null) { $time = time(); } else { $time = $sendTime->getTimestamp(); } $this->setHeader('Date', gmdate('D, d M Y H:i:s \G\M\T', $time)); } foreach ($this->headers as $name => $value) { $replace = strtolower($name) === 'content-type'; $value = implode(', ', $value); header(sprintf('%s: %s', $this->headerNames[$name], $value), $replace, $this->getStatusCode()); } foreach ($this->cookies as $key => $cookie) { header($cookie->toHttpHeader(), false); } if ($this->content !== null) { echo $this->content; } } /** * Returns the response body. * * @return string|null */ public function getContent() { return $this->content; } /** * Sets the response body. * * @param string|null $content */ public function setContent($content) { if ($content !== null) { $this->content = (string)$content; $this->setHeader('Content-Length', (string)strlen($this->content)); } else { $this->content = null; unset($this->headers['content-type'], $this->headers['content-length']); } } /** * Returns the HTTP status code. * * @return int HTTP status code */ public function getStatusCode() { return $this->statusCode; } /** * Sets the HTTP status code. * * @param int $statusCode HTTP status code */ public function setStatusCode($statusCode) { if (!array_key_exists($statusCode, self::$statusMessages)) { throw new InvalidArgumentException(sprintf('Status Code %s is not supported.', $statusCode)); } $this->statusCode = (int)$statusCode; } /** * Gets the HTTP status text (=reason). * * @return string HTTP status text */ public function getStatusText() { if ($this->statusText === null) { $this->statusText = self::$statusMessages[$this->statusCode]; } return $this->statusText; } /** * Sets the HTTP status text (=reason). * * @param string $message */ public function setStatusText($message) { $this->statusText = $message; } /** * Returns the HTTP version * * @return string */ public function getProtocolVersion() { return $this->protocolVersion; } /** * Sets header (overwrites existing header). * * @param string $name * @param string|string[] $values */ public function setHeader($name, $values) { if (!is_string($values) && !is_array($values)) { throw new HttpHeaderValueException( 'Invalid header, only string|string[] allowed', 0, null, $values ); } if ($values === '' || $values === []) { throw new HttpHeaderValueException('Empty header not allowed', 0, null, $values); } if (!is_array($values)) { $values = [$values]; } foreach ($values as $value) { $value = $this->sanitizeHeaderValue($value); if (!is_string($value) || $value === '') { throw new HttpHeaderValueException('Invalid header', 0, null, $value); } } $normalized = $this->normalizeHeaderName($name); $this->headers[$normalized] = $values; $this->headerNames[$normalized] = $name; } /** * Sets header (appends existing header). * * @param string $name * @param string $value */ public function addHeader($name, $value) { if (in_array($this->normalizeHeaderName($name), self::$oneValueHeaders, true)) { throw new InvalidArgumentException(sprintf('Cannot append header "%s".', $name)); } if (!is_string($value)) { throw new HttpHeaderValueException('Invalid header', 0, null, $value); } $value = $this->sanitizeHeaderValue($value); if ($value === '') { throw new HttpHeaderValueException('Empty header not allowed', 0, null, $value); } $normalized = $this->normalizeHeaderName($name); $header = $this->getHeader($normalized); if (count($header) === 0) { $this->headerNames[$normalized] = $name; } if (count(array_intersect($header, [$value])) === 0) { $header[] = $value; $this->headers[$normalized] = $header; } } /** * Gets header as array. * * @param string $name * * @return string[]|array empty if not set */ public function getHeader($name) { $normalized = $this->normalizeHeaderName($name); if ($this->hasHeader($normalized)) { return $this->headers[$normalized]; } return []; } /** * Gets header as comma-seperated string. * * @example getHeaderLine(Headername) -> 'value1, value2' * * @param string $name * * @return string|null null if header not set */ public function getHeaderLine($name) { $header = $this->getHeader($name); if (count($header) === 0) { return null; } return implode(', ', $header); } /** * Returns true if the header exists. * * @param string $name * * @return bool */ public function hasHeader($name) { return array_key_exists($this->normalizeHeaderName($name), $this->headers); } /** * Returns all headers * * @return array all headers */ public function getHeaders() { $headers = []; foreach ($this->headers as $name => $value) { $headers[$this->headerNames[$name]] = $value; } return $headers; } /** * Returns all headers with normalized names * * @return array all headers */ public function getHeadersNormalized() { return $this->headers; } /** * Returns the value of the Content-Type header. * * @example getContentType() -> 'text/html; charset=utf-8' * * @return string */ public function getContentType() { return $this->getHeaderLine('content-type'); } /** * Overwrites the Content-Type header. * * @param string $contentType * @param string $charset */ public function setContentType($contentType, $charset = 'utf-8') { $this->setHeader('Content-Type', sprintf('%s; charset=%s', $contentType, $charset)); } /** * Returns value of the Content-Disposition header. * * @example getContentDisposition() -> 'attachment; filename*="file.txt"; filename="file.txt"' * * @return string Content-Disposition header */ public function getContentDisposition() { return $this->getHeaderLine('content-disposition'); } /** * Sets the Content-Disposition HTTP header. * * @param string $disposition values: 'inline'|'attachment' * @param string $clientFileName file name for download on client */ public function setContentDisposition($disposition = self::DISPOSITION_ATTACHMENT, $clientFileName = '') { $disposition = strtolower($disposition); if (!in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE], true)) { throw new InvalidArgumentException(sprintf('Invalid Content-Disposition "%s".', $disposition)); } if ($clientFileName === '') { throw new InvalidArgumentException('Filename required.'); } $encodedName = urlencode(StringUtil::toFilename($clientFileName)); $fallbackName = StringUtil::toAscii($clientFileName); $header = sprintf('%s; filename*="%s"', $disposition, $encodedName); if ($fallbackName !== '') { $header .= sprintf('; filename="%s"', $fallbackName); } $this->setHeader('Content-Disposition', $header); } /** * Removes all non-ASCII characters from a header value * * @param string $value * * @return string */ protected function sanitizeHeaderValue($value) { if (!is_string($value)) { throw new HttpHeaderValueException('Invalid header', 0, null, $value); } return StringUtil::toAscii($value); } /** * @param string $name * @param string $value * @param int $timeToLive 0 = for ever */ public function addSimpleCookie($name, $value, $timeToLive = 0) { $this->addCookie(new Cookie($name, $value, $timeToLive)); } /** * @param Cookie $cookie */ public function addCookie(Cookie $cookie) { $this->cookies[] = $cookie; } /** * @param string $cookieName */ public function removeCookie($cookieName) { foreach ($this->cookies as $key => $cookie) { if ($cookieName === $cookie->getName()) { unset($this->cookies[$key]); } } } /** * @return CookieCollection */ public function getCookies() { return $this->cookies; } /** * @param CookieCollection|array $cookies */ protected function setCookies($cookies) { if (is_object($cookies) && get_class($cookies) === CookieCollection::class) { $this->cookies = $cookies; } else { $this->cookies = new CookieCollection($cookies); } } /** * Transforms header name to normalized form. * * @example 'HEADER-NAME' -> 'header-name' * * @param string $name * * @return string */ protected function normalizeHeaderName($name) { if (!preg_match('/^[a-zA-Z0-9\-]+$/', $name)) { throw new InvalidArgumentException(sprintf('Invalid character in header name "%s".', $name)); } return str_replace('_', '-', strtolower($name)); } }