<?php /** * Slim Framework (https://slimframework.com) * * @license https://github.com/slimphp/Slim-Psr7/blob/master/LICENSE.md (MIT License) */ declare(strict_types=1); namespace Slim\Psr7; use InvalidArgumentException; use Slim\Psr7\Interfaces\HeadersInterface; use function base64_encode; use function function_exists; use function getallheaders; use function is_array; use function is_numeric; use function is_string; use function preg_match; use function strpos; use function strtolower; use function strtr; use function substr; use function trim; class Headers implements HeadersInterface { /** * @var array */ protected $globals; /** * @var Header[] */ protected $headers; /** * @param array $headers * @param array $globals */ final public function __construct(array $headers = [], ?array $globals = null) { $this->globals = $globals ?? $_SERVER; $this->setHeaders($headers); } /** * {@inheritdoc} */ public function addHeader($name, $value): HeadersInterface { [$values, $originalName, $normalizedName] = $this->prepareHeader($name, $value); if (isset($this->headers[$normalizedName])) { $header = $this->headers[$normalizedName]; $header->addValues($values); } else { $this->headers[$normalizedName] = new Header($originalName, $normalizedName, $values); } return $this; } /** * {@inheritdoc} */ public function removeHeader(string $name): HeadersInterface { $name = $this->normalizeHeaderName($name); unset($this->headers[$name]); return $this; } /** * {@inheritdoc} */ public function getHeader(string $name, $default = []): array { $name = $this->normalizeHeaderName($name); if (isset($this->headers[$name])) { $header = $this->headers[$name]; return $header->getValues(); } if (empty($default)) { return $default; } $this->validateHeader($name, $default); return $this->trimHeaderValue($default); } /** * {@inheritdoc} */ public function setHeader($name, $value): HeadersInterface { [$values, $originalName, $normalizedName] = $this->prepareHeader($name, $value); // Ensure we preserve original case if the header already exists in the stack if (isset($this->headers[$normalizedName])) { $existingHeader = $this->headers[$normalizedName]; $originalName = $existingHeader->getOriginalName(); } $this->headers[$normalizedName] = new Header($originalName, $normalizedName, $values); return $this; } /** * {@inheritdoc} */ public function setHeaders(array $headers): HeadersInterface { $this->headers = []; foreach ($this->parseAuthorizationHeader($headers) as $name => $value) { $this->addHeader($name, $value); } return $this; } /** * {@inheritdoc} */ public function hasHeader(string $name): bool { $name = $this->normalizeHeaderName($name); return isset($this->headers[$name]); } /** * {@inheritdoc} */ public function getHeaders(bool $originalCase = false): array { $headers = []; foreach ($this->headers as $header) { $name = $originalCase ? $header->getOriginalName() : $header->getNormalizedName(); $headers[$name] = $header->getValues(); } return $headers; } /** * @param string $name * @param bool $preserveCase * @return string */ protected function normalizeHeaderName(string $name, bool $preserveCase = false): string { $name = strtr($name, '_', '-'); if (!$preserveCase) { $name = strtolower($name); } if (strpos(strtolower($name), 'http-') === 0) { $name = substr($name, 5); } return $name; } /** * Parse incoming headers and determine Authorization header from original headers * * @param array $headers * @return array */ protected function parseAuthorizationHeader(array $headers): array { $hasAuthorizationHeader = false; foreach ($headers as $name => $value) { if (strtolower($name) === 'authorization') { $hasAuthorizationHeader = true; break; } } if (!$hasAuthorizationHeader) { if (isset($this->globals['REDIRECT_HTTP_AUTHORIZATION'])) { $headers['Authorization'] = $this->globals['REDIRECT_HTTP_AUTHORIZATION']; } elseif (isset($this->globals['PHP_AUTH_USER'])) { $pw = isset($this->globals['PHP_AUTH_PW']) ? $this->globals['PHP_AUTH_PW'] : ''; $headers['Authorization'] = 'Basic ' . base64_encode($this->globals['PHP_AUTH_USER'] . ':' . $pw); } elseif (isset($this->globals['PHP_AUTH_DIGEST'])) { $headers['Authorization'] = $this->globals['PHP_AUTH_DIGEST']; } } return $headers; } /** * @param array|string $value * * @return array */ protected function trimHeaderValue($value): array { $items = is_array($value) ? $value : [$value]; $result = []; foreach ($items as $item) { $result[] = trim((string) $item, " \t"); } return $result; } /** * @param string $name * @param array|string $value * * @throws InvalidArgumentException * * @return array */ protected function prepareHeader($name, $value): array { $this->validateHeader($name, $value); $values = $this->trimHeaderValue($value); $originalName = $this->normalizeHeaderName($name, true); $normalizedName = $this->normalizeHeaderName($name); return [$values, $originalName, $normalizedName]; } /** * Make sure the header complies with RFC 7230. * * Header names must be a non-empty string consisting of token characters. * * Header values must be strings consisting of visible characters with all optional * leading and trailing whitespace stripped. This method will always strip such * optional whitespace. Note that the method does not allow folding whitespace within * the values as this was deprecated for almost all instances by the RFC. * * header-field = field-name ":" OWS field-value OWS * field-name = 1*( "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / "^" * / "_" / "`" / "|" / "~" / %x30-39 / ( %x41-5A / %x61-7A ) ) * OWS = *( SP / HTAB ) * field-value = *( ( %x21-7E / %x80-FF ) [ 1*( SP / HTAB ) ( %x21-7E / %x80-FF ) ] ) * * @see https://tools.ietf.org/html/rfc7230#section-3.2.4 * * @param string $name * @param array|string $value * * @throws InvalidArgumentException; */ protected function validateHeader($name, $value): void { $this->validateHeaderName($name); $this->validateHeaderValue($value); } /** * @param mixed $name * * @throws InvalidArgumentException */ protected function validateHeaderName($name): void { if (!is_string($name) || preg_match("@^[!#$%&'*+.^_`|~0-9A-Za-z-]+$@", $name) !== 1) { throw new InvalidArgumentException('Header name must be an RFC 7230 compatible string.'); } } /** * @param mixed $value * * @throws InvalidArgumentException */ protected function validateHeaderValue($value): void { $items = is_array($value) ? $value : [$value]; if (empty($items)) { throw new InvalidArgumentException( 'Header values must be a string or an array of strings, empty array given.' ); } $pattern = "@^[ \t\x21-\x7E\x80-\xFF]*$@"; foreach ($items as $item) { $hasInvalidType = !is_numeric($item) && !is_string($item); $rejected = $hasInvalidType || preg_match($pattern, (string) $item) !== 1; if ($rejected) { throw new InvalidArgumentException( 'Header values must be RFC 7230 compatible strings.' ); } } } /** * @return static */ public static function createFromGlobals() { $headers = null; if (function_exists('getallheaders')) { $headers = getallheaders(); } if (!is_array($headers)) { $headers = []; } return new static($headers); } }