['*' => (float) HTTP_Header::DEFAULT_QUALITY]]; // Parse the accept header qualities $accepts = HTTP_Header::accept_quality($accepts); $parsed_accept = []; // This method of iteration uses less resource $keys = array_keys($accepts); foreach ($keys as $key) { // Extract the parts $parts = explode('/', $key, 2); // Invalid content type- bail if ( ! isset($parts[1])) continue; // Set the parsed output $parsed_accept[$parts[0]][$parts[1]] = $accepts[$key]; } return $parsed_accept; } /** * Parses the `Accept-Charset:` HTTP header and returns an array containing * the charset and associated quality. * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.2 * @param string $charset charset string to parse * @return array * @since 3.2.0 */ public static function parse_charset_header($charset = NULL) { if ($charset === NULL) { return ['*' => (float) HTTP_Header::DEFAULT_QUALITY]; } return HTTP_Header::accept_quality(explode(',', (string) $charset)); } /** * Parses the `Accept-Encoding:` HTTP header and returns an array containing * the charsets and associated quality. * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3 * @param string $encoding charset string to parse * @return array * @since 3.2.0 */ public static function parse_encoding_header($encoding = NULL) { // Accept everything if ($encoding === NULL) { return ['*' => (float) HTTP_Header::DEFAULT_QUALITY]; } elseif ($encoding === '') { return ['identity' => (float) HTTP_Header::DEFAULT_QUALITY]; } else { return HTTP_Header::accept_quality(explode(',', (string) $encoding)); } } /** * Parses the `Accept-Language:` HTTP header and returns an array containing * the languages and associated quality. * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4 * @param string $language charset string to parse * @return array * @since 3.2.0 */ public static function parse_language_header($language = NULL) { if ($language === NULL) { return ['*' => ['*' => (float) HTTP_Header::DEFAULT_QUALITY]]; } $language = HTTP_Header::accept_quality(explode(',', (string) $language)); $parsed_language = []; $keys = array_keys($language); foreach ($keys as $key) { // Extract the parts $parts = explode('-', $key, 2); // Invalid content type- bail if ( ! isset($parts[1])) { $parsed_language[$parts[0]]['*'] = $language[$key]; } else { // Set the parsed output $parsed_language[$parts[0]][$parts[1]] = $language[$key]; } } return $parsed_language; } /** * Generates a Cache-Control HTTP header based on the supplied array. * * // Set the cache control headers you want to use * $cache_control = array( * 'max-age' => 3600, * 'must-revalidate', * 'public' * ); * * // Create the cache control header, creates : * // cache-control: max-age=3600, must-revalidate, public * $response->headers('Cache-Control', HTTP_Header::create_cache_control($cache_control); * * @link http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13 * @param array $cache_control Cache-Control to render to string * @return string */ public static function create_cache_control(array $cache_control) { $parts = []; foreach ($cache_control as $key => $value) { $parts[] = (is_int($key)) ? $value : ($key.'='.$value); } return implode(', ', $parts); } /** * Parses the Cache-Control header and returning an array representation of the Cache-Control * header. * * // Create the cache control header * $response->headers('cache-control', 'max-age=3600, must-revalidate, public'); * * // Parse the cache control header * if ($cache_control = HTTP_Header::parse_cache_control($response->headers('cache-control'))) * { * // Cache-Control header was found * $maxage = $cache_control['max-age']; * } * * @param array $cache_control Array of headers * @return mixed */ public static function parse_cache_control($cache_control) { $directives = explode(',', strtolower($cache_control)); if ($directives === FALSE) return FALSE; $output = []; foreach ($directives as $directive) { if (strpos($directive, '=') !== FALSE) { list($key, $value) = explode('=', trim($directive), 2); $output[$key] = ctype_digit($value) ? (int) $value : $value; } else { $output[] = trim($directive); } } return $output; } /** * @var array Accept: (content) types */ protected $_accept_content; /** * @var array Accept-Charset: parsed header */ protected $_accept_charset; /** * @var array Accept-Encoding: parsed header */ protected $_accept_encoding; /** * @var array Accept-Language: parsed header */ protected $_accept_language; /** * @var array Accept-Language: language list of parsed header */ protected $_accept_language_list; /** * Constructor method for [KO7_HTTP_Header]. Uses the standard constructor * of the parent `ArrayObject` class. * * $header_object = new HTTP_Header(array('x-powered-by' => 'KO7 3.1.x', 'expires' => '...')); * * @param mixed $input Input array * @param int $flags Flags * @param string $iterator_class The iterator class to use */ public function __construct(array $input = [], $flags = 0, $iterator_class = 'ArrayIterator') { /** * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html * * HTTP header declarations should be treated as case-insensitive */ $input = array_change_key_case( (array) $input, CASE_LOWER); parent::__construct($input, $flags, $iterator_class); } /** * Returns the header object as a string, including * the terminating new line * * // Return the header as a string * echo (string) $request->headers(); * * @return string */ public function __toString() { $header = ''; foreach ($this as $key => $value) { // Put the keys back the Case-Convention expected $key = Text::ucfirst($key); if (is_array($value)) { $header .= $key.': '.(implode(', ', $value))."\r\n"; } else { $header .= $key.': '.$value."\r\n"; } } return $header."\r\n"; } /** * Overloads `ArrayObject::offsetSet()` to enable handling of header * with multiple instances of the same directive. If the `$replace` flag * is `FALSE`, the header will be appended rather than replacing the * original setting. * * @param mixed $index index to set `$newval` to * @param mixed $newval new value to set * @param boolean $replace replace existing value * @return void * @since 3.2.0 */ public function offsetSet(mixed $index, mixed $newval, bool $replace = TRUE): void { // Ensure the index is lowercase $index = strtolower($index); if ($replace OR ! $this->offsetExists($index)) { parent::offsetSet($index, $newval); } $current_value = $this->offsetGet($index); if (is_array($current_value)) { $current_value[] = $newval; } else { $current_value = [$current_value, $newval]; } parent::offsetSet($index, $current_value); } /** * Overloads the `ArrayObject::offsetExists()` method to ensure keys * are lowercase. * * @param string $index * @return boolean * @since 3.2.0 */ public function offsetExists(mixed $index): bool { return parent::offsetExists(strtolower($index)); } /** * Overloads the `ArrayObject::offsetUnset()` method to ensure keys * are lowercase. * * @param string $index * @return void * @since 3.2.0 */ public function offsetUnset(mixed $index): void { parent::offsetUnset(strtolower($index)); } /** * Overload the `ArrayObject::offsetGet()` method to ensure that all * keys passed to it are formatted correctly for this object. * * @param string $index index to retrieve * @return mixed * @since 3.2.0 */ public function offsetGet(mixed $index): mixed { return parent::offsetGet(strtolower($index)); } /** * Overloads the `ArrayObject::exchangeArray()` method to ensure that * all keys are changed to lowercase. * * @param mixed $input * @return array * @since 3.2.0 */ public function exchangeArray(array|object $input): array { /** * @link http://www.w3.org/Protocols/rfc2616/rfc2616.html * * HTTP header declarations should be treated as case-insensitive */ $input = array_change_key_case( (array) $input, CASE_LOWER); return parent::exchangeArray($input); } /** * Parses a HTTP Message header line and applies it to this HTTP_Header * * $header = $response->headers(); * $header->parse_header_string(NULL, 'content-type: application/json'); * * @param resource $resource the resource (required by Curl API) * @param string $header_line the line from the header to parse * @return int * @since 3.2.0 */ public function parse_header_string($resource, $header_line) { if (preg_match_all('/(\w[^\s:]*):[ ]*([^\r\n]*(?:\r\n[ \t][^\r\n]*)*)/', $header_line, $matches)) { foreach ($matches[0] as $key => $value) { $this->offsetSet($matches[1][$key], $matches[2][$key], FALSE); } } return strlen($header_line); } /** * Returns the accept quality of a submitted mime type based on the * request `Accept:` header. If the `$explicit` argument is `TRUE`, * only precise matches will be returned, excluding all wildcard (`*`) * directives. * * // Accept: application/xml; application/json; q=.5; text/html; q=.2, text/* * // Accept quality for application/json * * // $quality = 0.5 * $quality = $request->headers()->accepts_at_quality('application/json'); * * // $quality_explicit = FALSE * $quality_explicit = $request->headers()->accepts_at_quality('text/plain', TRUE); * * @param string $type * @param boolean $explicit explicit check, excludes `*` * @return mixed * @since 3.2.0 */ public function accepts_at_quality($type, $explicit = FALSE) { // Parse Accept header if required if ($this->_accept_content === NULL) { if ($this->offsetExists('Accept')) { $accept = $this->offsetGet('Accept'); } else { $accept = '*/*'; } $this->_accept_content = HTTP_Header::parse_accept_header($accept); } // If not a real mime, try and find it in config if (strpos($type, '/') === FALSE) { $mime = KO7::$config->load('mimes.'.$type); if ($mime === NULL) return FALSE; $quality = FALSE; foreach ($mime as $_type) { $quality_check = $this->accepts_at_quality($_type, $explicit); $quality = ($quality_check > $quality) ? $quality_check : $quality; } return $quality; } $parts = explode('/', $type, 2); if (isset($this->_accept_content[$parts[0]][$parts[1]])) { return $this->_accept_content[$parts[0]][$parts[1]]; } elseif ($explicit === TRUE) { return FALSE; } else { if (isset($this->_accept_content[$parts[0]]['*'])) { return $this->_accept_content[$parts[0]]['*']; } elseif (isset($this->_accept_content['*']['*'])) { return $this->_accept_content['*']['*']; } else { return FALSE; } } } /** * Returns the preferred response content type based on the accept header * quality settings. If items have the same quality value, the first item * found in the array supplied as `$types` will be returned. * * // Get the preferred acceptable content type * // Accept: text/html, application/json; q=.8, text/* * $result = $header->preferred_accept(array( * 'text/html' * 'text/rtf', * 'application/json' * )); // $result = 'application/json' * * $result = $header->preferred_accept(array( * 'text/rtf', * 'application/xml' * ), TRUE); // $result = FALSE (none matched explicitly) * * * @param array $types the content types to examine * @param boolean $explicit only allow explicit references, no wildcards * @return string name of the preferred content type * @since 3.2.0 */ public function preferred_accept(array $types, $explicit = FALSE) { $preferred = FALSE; $ceiling = 0; foreach ($types as $type) { $quality = $this->accepts_at_quality($type, $explicit); if ($quality > $ceiling) { $preferred = $type; $ceiling = $quality; } } return $preferred; } /** * Returns the quality of the supplied `$charset` argument. This method * will automatically parse the `Accept-Charset` header if present and * return the associated resolved quality value. * * // Accept-Charset: utf-8, utf-16; q=.8, iso-8859-1; q=.5 * $quality = $header->accepts_charset_at_quality('utf-8'); * // $quality = (float) 1 * * @param string $charset charset to examine * @return float the quality of the charset * @since 3.2.0 */ public function accepts_charset_at_quality($charset) { if ($this->_accept_charset === NULL) { if ($this->offsetExists('Accept-Charset')) { $charset_header = strtolower($this->offsetGet('Accept-Charset')); $this->_accept_charset = HTTP_Header::parse_charset_header($charset_header); } else { $this->_accept_charset = HTTP_Header::parse_charset_header(NULL); } } $charset = strtolower($charset); if (isset($this->_accept_charset[$charset])) { return $this->_accept_charset[$charset]; } elseif (isset($this->_accept_charset['*'])) { return $this->_accept_charset['*']; } elseif ($charset === 'iso-8859-1') { return (float) 1; } return (float) 0; } /** * Returns the preferred charset from the supplied array `$charsets` based * on the `Accept-Charset` header directive. * * // Accept-Charset: utf-8, utf-16; q=.8, iso-8859-1; q=.5 * $charset = $header->preferred_charset(array( * 'utf-10', 'ascii', 'utf-16', 'utf-8' * )); // $charset = 'utf-8' * * @param array $charsets charsets to test * @return mixed preferred charset or `FALSE` * @since 3.2.0 */ public function preferred_charset(array $charsets) { $preferred = FALSE; $ceiling = 0; foreach ($charsets as $charset) { $quality = $this->accepts_charset_at_quality($charset); if ($quality > $ceiling) { $preferred = $charset; $ceiling = $quality; } } return $preferred; } /** * Returns the quality of the `$encoding` type passed to it. Encoding * is usually compression such as `gzip`, but could be some other * message encoding algorithm. This method allows explicit checks to be * done ignoring wildcards. * * // Accept-Encoding: compress, gzip, *; q=.5 * $encoding = $header->accepts_encoding_at_quality('gzip'); * // $encoding = (float) 1.0s * * @param string $encoding encoding type to interrogate * @param boolean $explicit explicit check, ignoring wildcards and `identity` * @return float * @since 3.2.0 */ public function accepts_encoding_at_quality($encoding, $explicit = FALSE) { if ($this->_accept_encoding === NULL) { if ($this->offsetExists('Accept-Encoding')) { $encoding_header = $this->offsetGet('Accept-Encoding'); } else { $encoding_header = NULL; } $this->_accept_encoding = HTTP_Header::parse_encoding_header($encoding_header); } // Normalize the encoding $encoding = strtolower($encoding); if (isset($this->_accept_encoding[$encoding])) { return $this->_accept_encoding[$encoding]; } if ($explicit === FALSE) { if (isset($this->_accept_encoding['*'])) { return $this->_accept_encoding['*']; } elseif ($encoding === 'identity') { return (float) HTTP_Header::DEFAULT_QUALITY; } } return (float) 0; } /** * Returns the preferred message encoding type based on quality, and can * optionally ignore wildcard references. If two or more encodings have the * same quality, the first listed in `$encodings` will be returned. * * // Accept-Encoding: compress, gzip, *; q.5 * $encoding = $header->preferred_encoding(array( * 'gzip', 'bzip', 'blowfish' * )); * // $encoding = 'gzip'; * * @param array $encodings encodings to test against * @param boolean $explicit explicit check, if `TRUE` wildcards are excluded * @return mixed * @since 3.2.0 */ public function preferred_encoding(array $encodings, $explicit = FALSE) { $ceiling = 0; $preferred = FALSE; foreach ($encodings as $encoding) { $quality = $this->accepts_encoding_at_quality($encoding, $explicit); if ($quality > $ceiling) { $ceiling = $quality; $preferred = $encoding; } } return $preferred; } /** * Returns the quality of `$language` supplied, optionally ignoring * wildcards if `$explicit` is set to a non-`FALSE` value. If the quality * is not found, `0.0` is returned. * * // Accept-Language: en-us, en-gb; q=.7, en; q=.5 * $lang = $header->accepts_language_at_quality('en-gb'); * // $lang = (float) 0.7 * * $lang2 = $header->accepts_language_at_quality('en-au'); * // $lang2 = (float) 0.5 * * $lang3 = $header->accepts_language_at_quality('en-au', TRUE); * // $lang3 = (float) 0.0 * * @param string $language language to interrogate * @param boolean $explicit explicit interrogation, `TRUE` ignores wildcards * @return float * @since 3.2.0 */ public function accepts_language_at_quality($language, $explicit = FALSE) { if ($this->_accept_language === NULL) { if ($this->offsetExists('Accept-Language')) { $language_header = strtolower($this->offsetGet('Accept-Language')); } else { $language_header = NULL; } $this->_accept_language = HTTP_Header::parse_language_header($language_header); } // Normalize the language $language_parts = explode('-', strtolower($language), 2); if (isset($this->_accept_language[$language_parts[0]])) { if (isset($language_parts[1])) { if (isset($this->_accept_language[$language_parts[0]][$language_parts[1]])) { return $this->_accept_language[$language_parts[0]][$language_parts[1]]; } elseif ($explicit === FALSE AND isset($this->_accept_language[$language_parts[0]]['*'])) { return $this->_accept_language[$language_parts[0]]['*']; } } elseif (isset($this->_accept_language[$language_parts[0]]['*'])) { return $this->_accept_language[$language_parts[0]]['*']; } } if ($explicit === FALSE AND isset($this->_accept_language['*'])) { return $this->_accept_language['*']; } return (float) 0; } /** * Parses the `Accept-Language:` HTTP header and returns an array containing * the language names. * * @param string $language charset string to parse * @return array * @since 3.3.8 */ protected static function _parse_language_header_as_list($language = NULL) { $languages = []; $language = explode(',', strtolower($language)); foreach ($language as $lang) { $matches = []; if (preg_match('/([\w-]+)\s*(;.*q.*)?/', $lang, $matches)) { $languages[] = $matches[1]; } } return $languages; } /** * Returns the reordered list of supplied `$languages` using the order * from the `Accept-Language:` HTTP header. * * @param array $languages languages to order * @param boolean $explicit * @return array * @since 3.3.8 */ protected function _order_languages_as_received(array $languages, $explicit = FALSE) { if ($this->_accept_language_list === NULL) { if ($this->offsetExists('Accept-Language')) { $language_header = strtolower($this->offsetGet('Accept-Language')); } else { $language_header = NULL; } $this->_accept_language_list = HTTP_Header::_parse_language_header_as_list($language_header); } $new_order = []; foreach ($this->_accept_language_list as $accept_language) { foreach ($languages as $key => $language) { if (($explicit AND $accept_language == $language) OR ( ! $explicit AND substr($accept_language, 0, 2) == substr($language, 0, 2))) { $new_order[] = $language; unset($languages[$key]); } } } foreach ($languages as $language) { $new_order[] = $language; } return $new_order; } /** * Returns the preferred language from the supplied array `$languages` based * on the `Accept-Language` header directive. * * // Accept-Language: en-us, en-gb; q=.7, en; q=.5 * $lang = $header->preferred_language(array( * 'en-gb', 'en-au', 'fr', 'es' * )); // $lang = 'en-gb' * * @param array $languages * @param boolean $explicit * @return mixed * @since 3.2.0 */ public function preferred_language(array $languages, $explicit = FALSE) { $ceiling = 0; $preferred = FALSE; $languages = $this->_order_languages_as_received($languages, $explicit); foreach ($languages as $language) { $quality = $this->accepts_language_at_quality($language, $explicit); if ($quality > $ceiling) { $ceiling = $quality; $preferred = $language; } } return $preferred; } /** * Sends headers to the php processor, or supplied `$callback` argument. * This method formats the headers correctly for output, re-instating their * capitalization for transmission. * * [!!] if you supply a custom header handler via `$callback`, it is * recommended that `$response` is returned * * @param HTTP_Response $response header to send * @param boolean $replace replace existing value * @param callback $callback optional callback to replace PHP header function * @return mixed * @since 3.2.0 */ public function send_headers(HTTP_Response $response = NULL, $replace = FALSE, $callback = NULL) { $protocol = $response->protocol(); $status = $response->status(); // Create the response header $processed_headers = [$protocol.' '.$status.' '.Response::$messages[$status]]; // Get the headers array $headers = $response->headers()->getArrayCopy(); foreach ($headers as $header => $value) { if (is_array($value)) { $value = implode(', ', $value); } $processed_headers[] = Text::ucfirst($header).': '.$value; } if ( ! isset($headers['content-type'])) { $processed_headers[] = 'Content-Type: '.KO7::$content_type.'; charset='.KO7::$charset; } if (KO7::$expose AND ! isset($headers['x-powered-by'])) { $processed_headers[] = 'X-Powered-By: '.KO7::version(); } // Get the cookies and apply if ($cookies = $response->cookie()) { $processed_headers['Set-Cookie'] = $cookies; } if (is_callable($callback)) { // Use the callback method to set header return call_user_func($callback, $response, $processed_headers, $replace); } else { $this->_send_headers_to_php($processed_headers, $replace); return $response; } } /** * Sends the supplied headers to the PHP output buffer. If cookies * are included in the message they will be handled appropriately. * * @param array $headers headers to send to php * @param boolean $replace replace existing headers * @return self * @since 3.2.0 */ protected function _send_headers_to_php(array $headers, $replace) { // If the headers have been sent, get out if (headers_sent()) return $this; foreach ($headers as $key => $line) { if ($key == 'Set-Cookie' AND is_array($line)) { // Send cookies foreach ($line as $name => $value) { Cookie::set($name, $value['value'], $value['expiration']); } continue; } header($line, $replace); } return $this; } }