<?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);
}
}