- <?php
- /**
- * Zend Framework (http://framework.zend.com/)
- *
- * @link http://github.com/zendframework/zf2 for the canonical source repository
- * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com)
- * @license http://framework.zend.com/license/new-bsd New BSD License
- */
-
- namespace Zend\Http\Header;
-
- use stdClass;
-
- /**
- * Abstract Accept Header
- *
- * Naming conventions:
- *
- * Accept: audio/mp3; q=0.2; version=0.5, audio/basic+mp3
- * |------------------------------------------------------| header line
- * |------| field name
- * |-----------------------------------------------| field value
- * |-------------------------------| field value part
- * |------| type
- * |--| subtype
- * |--| format
- * |----| subtype
- * |---| format
- * |-------------------| parameter set
- * |-----------| parameter
- * |-----| parameter key
- * |--| parameter value
- * |---| priority
- *
- *
- * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1
- * @author Dolf Schimmel - Freeaqingme
- */
- abstract class AbstractAccept implements HeaderInterface
- {
-
- /**
- *
- * @var array
- */
- protected $fieldValueParts = array();
-
- protected $regexAddType;
-
- /**
- * Determines if since last mutation the stack was sorted
- *
- * @var bool
- */
- protected $sorted = false;
-
-
- /**
- *
- * @param string $headerLine
- */
- public function parseHeaderLine($headerLine)
- {
- $fieldName = $this->getFieldName();
- $pos = strlen($fieldName) + 2;
- if (strtolower(substr($headerLine, 0, $pos)) == strtolower($fieldName) . ': ') {
- $headerLine = substr($headerLine, $pos);
- }
-
- foreach ($this->getFieldValuePartsFromHeaderLine($headerLine) as $value) {
- $this->addFieldValuePartToQueue($value);
- }
- }
-
- /**
- * Factory method: parse Accept header string
- *
- * @param string $headerLine
- * @return Accept
- */
- public static function fromString($headerLine)
- {
- $obj = new static();
- $obj->parseHeaderLine($headerLine);
- return $obj;
- }
-
- /**
- * Parse the Field Value Parts represented by a header line
- *
- * @param string $headerLine
- * @throws Exception\InvalidArgumentException If header is invalid
- * @return array
- */
- public function getFieldValuePartsFromHeaderLine($headerLine)
- {
- // process multiple accept values, they may be between quotes
- if (!preg_match_all('/(?:[^,"]|"(?:[^\\\"]|\\\.)*")+/', $headerLine, $values)
- || !isset($values[0])
- ) {
- throw new Exception\InvalidArgumentException(
- 'Invalid header line for ' . $this->getFieldName() . ' header string'
- );
- }
-
- $out = array();
- foreach ($values[0] as $value) {
- $value = trim($value);
-
- $out[] = $this->parseFieldValuePart($value);
- }
-
- return $out;
- }
-
- /**
- * Parse the accept params belonging to a media range
- *
- * @param string $fieldValuePart
- * @return stdClass
- */
- protected function parseFieldValuePart($fieldValuePart)
- {
- $raw = $subtypeWhole = $type = $fieldValuePart;
- if ($pos = strpos($fieldValuePart, ';')) {
- $type = substr($fieldValuePart, 0, $pos);
- }
-
- $params = $this->getParametersFromFieldValuePart($fieldValuePart);
-
- if ($pos = strpos($fieldValuePart, ';')) {
- $fieldValuePart = trim(substr($fieldValuePart, 0, $pos));
- }
-
- $format = '*';
- $subtype = '*';
-
- return (object) array(
- 'typeString' => trim($fieldValuePart),
- 'type' => $type,
- 'subtype' => $subtype,
- 'subtypeRaw' => $subtypeWhole,
- 'format' => $format,
- 'priority' => isset($params['q']) ? $params['q'] : 1,
- 'params' => $params,
- 'raw' => trim($raw)
- );
- }
-
- /**
- * Parse the keys contained in the header line
- *
- * @param string $fieldValuePart
- * @return array
- */
- protected function getParametersFromFieldValuePart($fieldValuePart)
- {
- $params = array();
- if ((($pos = strpos($fieldValuePart, ';')) !== false)) {
- preg_match_all('/(?:[^;"]|"(?:[^\\\"]|\\\.)*")+/', $fieldValuePart, $paramsStrings);
-
- if (isset($paramsStrings[0])) {
- array_shift($paramsStrings[0]);
- $paramsStrings = $paramsStrings[0];
- }
-
- foreach ($paramsStrings as $param) {
- $explode = explode('=', $param, 2);
-
- $value = trim($explode[1]);
- if (isset($value[0]) && $value[0] == '"' && substr($value, -1) == '"') {
- $value = substr(substr($value, 1), 0, -1);
- }
-
- $params[trim($explode[0])] = stripslashes($value);
- }
- }
-
- return $params;
- }
-
-
- /**
- * Get field value
- *
- * @param array|null $values
- * @return string
- */
- public function getFieldValue($values = null)
- {
- if (!$values) {
- return $this->getFieldValue($this->fieldValueParts);
- }
-
- $strings = array();
- foreach ($values as $value) {
- $params = $value->params;
- array_walk($params, array($this, 'assembleAcceptParam'));
- $strings[] = implode(';', array($value->typeString) + $params);
- }
-
- return implode(', ', $strings);
- }
-
-
- /**
- * Assemble and escape the field value parameters based on RFC 2616 section 2.1
- *
- * @todo someone should review this thoroughly
- * @param string $value
- * @param string $key
- * @return string
- */
- protected function assembleAcceptParam(&$value, $key)
- {
- $separators = array('(', ')', '<', '>', '@', ',', ';', ':',
- '/', '[', ']', '?', '=', '{', '}', ' ', "\t");
-
- $escaped = preg_replace_callback('/[[:cntrl:]"\\\\]/', // escape cntrl, ", \
- function ($v) {
- return '\\' . $v[0];
- },
- $value
- );
-
- if ($escaped == $value && !array_intersect(str_split($value), $separators)) {
- $value = $key . '=' . $value;
- } else {
- $value = $key . '="' . $escaped . '"';
- }
-
- return $value;
- }
-
- /**
- * Add a type, with the given priority
- *
- * @param string $type
- * @param int|float $priority
- * @param array (optional) $params
- * @throws Exception\InvalidArgumentException
- * @return Accept
- */
- protected function addType($type, $priority = 1, array $params = array())
- {
- if (!preg_match($this->regexAddType, $type)) {
- throw new Exception\InvalidArgumentException(sprintf(
- '%s expects a valid type; received "%s"',
- __METHOD__,
- (string) $type
- ));
- }
-
- if (!is_int($priority) && !is_float($priority) && !is_numeric($priority)
- || $priority > 1 || $priority < 0
- ) {
- throw new Exception\InvalidArgumentException(sprintf(
- '%s expects a numeric priority; received %s',
- __METHOD__,
- (string) $priority
- ));
- }
-
- if ($priority != 1) {
- $params = array('q' => sprintf('%01.1f', $priority)) + $params;
- }
-
- $assembledString = $this->getFieldValue(
- array((object) array('typeString' => $type, 'params' => $params))
- );
-
- $value = $this->parseFieldValuePart($assembledString);
- $this->addFieldValuePartToQueue($value);
- return $this;
- }
-
-
- /**
- * Does the header have the requested type?
- *
- * @param array|string $matchAgainst
- * @return bool
- */
- protected function hasType($matchAgainst)
- {
- return (bool) $this->match($matchAgainst);
- }
-
- /**
- * Match a media string against this header
- *
- * @param array|string $matchAgainst
- * @return Accept\FieldValuePart\AcceptFieldValuePart|bool The matched value or false
- */
- public function match($matchAgainst)
- {
- if (is_string($matchAgainst)) {
- $matchAgainst = $this->getFieldValuePartsFromHeaderLine($matchAgainst);
- }
-
- foreach ($this->getPrioritized() as $left) {
- foreach ($matchAgainst as $right) {
- if ($right->type == '*' || $left->type == '*') {
- if ($this->matchAcceptParams($left, $right)) {
- $left->setMatchedAgainst($right);
-
- return $left;
- }
- }
-
- if ($left->type == $right->type) {
- if ((($left->subtype == $right->subtype ||
- ($right->subtype == '*' || $left->subtype == '*')) &&
- ($left->format == $right->format ||
- $right->format == '*' || $left->format == '*')))
- {
- if ($this->matchAcceptParams($left, $right)) {
- $left->setMatchedAgainst($right);
-
- return $left;
- }
- }
- }
-
-
- }
- }
-
- return false;
- }
-
- /**
- * Return a match where all parameters in argument #1 match those in argument #2
- *
- * @param array $match1
- * @param array $match2
- * @return bool|array
- */
- protected function matchAcceptParams($match1, $match2)
- {
- foreach ($match2->params as $key => $value) {
- if (isset($match1->params[$key])) {
- if (strpos($value, '-')) {
- preg_match(
- '/^(?|([^"-]*)|"([^"]*)")-(?|([^"-]*)|"([^"]*)")\z/',
- $value,
- $pieces
- );
-
- if (count($pieces) == 3 &&
- (version_compare($pieces[1], $match1->params[$key], '<=') xor
- version_compare($pieces[2], $match1->params[$key], '>=')
- )
- ) {
- return false;
- }
- } elseif (strpos($value, '|')) {
- $options = explode('|', $value);
- $good = false;
- foreach ($options as $option) {
- if ($option == $match1->params[$key]) {
- $good = true;
- break;
- }
- }
-
- if (!$good) {
- return false;
- }
- } elseif ($match1->params[$key] != $value) {
- return false;
- }
- }
-
- }
-
- return $match1;
- }
-
-
- /**
- * Add a key/value combination to the internal queue
- *
- * @param stdClass $value
- * @return number
- */
- protected function addFieldValuePartToQueue($value)
- {
- $this->fieldValueParts[] = $value;
- $this->sorted = false;
- }
-
- /**
- * Sort the internal Field Value Parts
- *
- * @See rfc2616 sect 14.1
- * Media ranges can be overridden by more specific media ranges or
- * specific media types. If more than one media range applies to a given
- * type, the most specific reference has precedence. For example,
- *
- * Accept: text/*, text/html, text/html;level=1, * /*
- *
- * have the following precedence:
- *
- * 1) text/html;level=1
- * 2) text/html
- * 3) text/*
- * 4) * /*
- *
- * @return number
- */
- protected function sortFieldValueParts()
- {
- $sort = function ($a, $b) { // If A has higher prio than B, return -1.
- if ($a->priority > $b->priority) {
- return -1;
- } elseif ($a->priority < $b->priority) {
- return 1;
- }
-
- // Asterisks
- $values = array('type', 'subtype', 'format');
- foreach ($values as $value) {
- if ($a->$value == '*' && $b->$value != '*') {
- return 1;
- } elseif ($b->$value == '*' && $a->$value != '*') {
- return -1;
- }
- }
-
- if ($a->type == 'application' && $b->type != 'application') {
- return -1;
- } elseif ($b->type == 'application' && $a->type != 'application') {
- return 1;
- }
-
- //@todo count number of dots in case of type==application in subtype
-
- // So far they're still the same. Longest stringlength may be more specific
- if (strlen($a->raw) == strlen($b->raw)) return 0;
- return (strlen($a->raw) > strlen($b->raw)) ? -1 : 1;
- };
-
- usort($this->fieldValueParts, $sort);
- $this->sorted = true;
- }
-
- /**
- * @return array with all the keys, values and parameters this header represents:
- */
- public function getPrioritized()
- {
- if (!$this->sorted) {
- $this->sortFieldValueParts();
- }
-
- return $this->fieldValueParts;
- }
- }