diff --git a/src/Header/Accept.php b/src/Header/Accept.php index d68246f378..ebd3a05a5b 100755 --- a/src/Header/Accept.php +++ b/src/Header/Accept.php @@ -2,15 +2,26 @@ namespace Zend\Http\Header; +use Zend\Stdlib\PriorityQueue; + /** - * @todo Implement q and level lookups + * @todo Implement level lookups * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 */ class Accept implements HeaderDescription { protected $values = array(); + protected $prioritizedValues = array(); + protected $priorityQueue; + protected $mediaTypes = array(); + /** + * Factory method: parse Accept header string + * + * @param string $headerLine + * @return Accept + */ public static function fromString($headerLine) { $acceptHeader = new static(); @@ -23,77 +34,197 @@ public static function fromString($headerLine) } // process multiple accept values - // @todo q and level processing here to be retrieved by getters in accept object later + // @todo level processing $acceptHeader->values = explode(',', $values); + foreach ($acceptHeader->values as $index => $value) { - $acceptHeader->values[$index] = explode(';', $value); + $value = trim($value); + $acceptHeader->values[$index] = $value; + + $payload = array( + 'media_type' => strtolower($value), + 'priority' => 1, + ); + if (strstr($value, ';')) { + list($type, $priority) = explode(';', $value, 2); + $payload['media_type'] = strtolower(trim($type)); + + // parse priority + $priority = explode(';', trim($priority)); + + $finalPriority = 1; + foreach ($priority as $p) { + list($type, $value) = explode('=', trim($p), 2); + if ($type != 'q') { + // Not going to worry about "level" for now + continue; + } + $finalPriority = $value; + } + $payload['priority'] = $finalPriority; + } + + if (!isset($acceptHeader->mediaTypes[$payload['media_type']])) { + $acceptHeader->mediaTypes[$payload['media_type']] = true; + } + + $acceptHeader->prioritizedValues[] = $payload; } return $acceptHeader; } + /** + * Get field name + * + * @return string + */ public function getFieldName() { return 'Accept'; } + /** + * Get field value + * + * @return string + */ public function getFieldValue() { $strings = array(); foreach ($this->values as $value) { - $strings[] = implode('; ', $value); + $strings[] = implode('; ', (array) $value); } return implode(',', $strings); } + /** + * Cast to string + * + * @return string + */ public function toString() { return 'Accept: ' . $this->getFieldValue(); } -// -// /** -// * Get the quality factor of the value (q=) -// * -// * @param string $value -// * @return float -// */ -// public function getQualityFactor($value) -// { -// if ($this->hasValue($value)) { -// if (!empty($this->arrayValue)) { -// if (isset($this->arrayValue[$value])) { -// foreach ($this->arrayValue[$value] as $val) { -// if (preg_match('/q=(\d\.?\d?)/',$val,$matches)) { -// return $matches[1]; -// } -// } -// } -// return 1; -// } -// } -// return false; -// } -// -// /** -// * Get the level of a value (level=) -// * -// * @param string $value -// * @return integer -// */ -// public function getLevel($value) -// { -// if ($this->hasValue($value)) { -// if (isset($this->arrayValue[$value])) { -// foreach ($this->arrayValue[$value] as $val) { -// if (preg_match('/level=(\d+)/',$val,$matches)) { -// return $matches[1]; -// } -// } -// } -// } -// return false; -// } -// + /** + * Add a media type, with the given priority + * + * @param string $type + * @param int|float $priority + * @param int $level Unused currently + * @return Accept + */ + public function addMediaType($type, $priority = 1, $level = null) + { + if (!preg_match('#^([a-zA-Z+-]+|\*)/(\*|[a-zA-Z0-9+-]+)$#', $type)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a valid media type; received "%s"', + __METHOD__, + (string) $type + )); + } + + if (!is_int($priority) && !is_float($priority) && !is_numeric($priority)) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a numeric priority; received %s', + __METHOD__, + (string) $priority + )); + } + + if ($priority > 1 || $priority < 0) { + throw new Exception\InvalidArgumentException(sprintf( + '%s expects a priority between 0 and 1; received %01.1f', + __METHOD__, + (float) $priority + )); + } + + $this->mediaTypes[$type] = true; + + $this->prioritizedValues[] = array( + 'media_type' => $type, + 'priority' => $priority, + ); + + $value = $type; + if ($priority < 1) { + $value .= sprintf(';q=%01.1f', $priority); + } + $this->values[] = $value; + + return $this; + } + + /** + * Does the header have the requested media type? + * + * @param string $type + * @return bool + */ + public function hasMediaType($type) + { + $type = strtolower($type); + + // Exact match + if (isset($this->mediaTypes[$type])) { + return true; + } + + // No "/" -- not a media type + if (false === strstr($type, '/')) { + return false; + } + + // Parent type wildcard matching + $parent = substr($type, 0, strpos($type, '/')); + if (isset($this->mediaTypes[$parent . '/*'])) { + return true; + } + + // Wildcard matching + if (isset($this->mediaTypes['*/*'])) { + return true; + } + + // No match + return false; + } + + /** + * Get a prioritized list of media types + * + * @return PriorityQueue + */ + public function getPrioritized() + { + if (!$this->priorityQueue) { + $this->createPriorityQueue(); + } + + return $this->priorityQueue; + } + + /** + * Create the priority queue + * + * @return void + */ + protected function createPriorityQueue() + { + $queue = new PriorityQueue(); + foreach ($this->prioritizedValues as $data) { + // Do not include priority 0 in list + if ($data['priority'] == 0) { + continue; + } + // Hack to ensure priorities are correct; was not treating + // fractional values correctly + $queue->insert($data['media_type'], (float) $data['priority'] * 10); + } + $this->priorityQueue = $queue; + } } diff --git a/test/Header/AcceptTest.php b/test/Header/AcceptTest.php index 9a5af68ca6..c9db4c8345 100644 --- a/test/Header/AcceptTest.php +++ b/test/Header/AcceptTest.php @@ -2,7 +2,8 @@ namespace ZendTest\Http\Header; -use Zend\Http\Header\Accept; +use Zend\Http\Header\GenericHeader, + Zend\Http\Header\Accept; class AcceptTest extends \PHPUnit_Framework_TestCase { @@ -22,173 +23,46 @@ public function testAcceptGetFieldNameReturnsHeaderName() public function testAcceptGetFieldValueReturnsProperValue() { - $this->markTestIncomplete('Accept needs to be completed'); - - $acceptHeader = new Accept(); + $acceptHeader = Accept::fromString('Accept: xxx'); $this->assertEquals('xxx', $acceptHeader->getFieldValue()); } public function testAcceptToStringReturnsHeaderFormattedString() { - $this->markTestIncomplete('Accept needs to be completed'); - $acceptHeader = new Accept(); + $acceptHeader->addMediaType('text/html', 0.8) + ->addMediaType('application/json', 1) + ->addMediaType('application/atom+xml', 0.9); // @todo set some values, then test output - $this->assertEmpty('Accept: xxx', $acceptHeader->toString()); + $this->assertEquals('Accept: text/html;q=0.8,application/json,application/atom+xml;q=0.9', $acceptHeader->toString()); } - /** Implmentation specific tests here */ + /** Implementation specific tests here */ -// /** -// * Test construct with type -// */ -// public function testConstructWithType() -// { -// $header= new Header('Accept'); -// $this->assertEquals($header->getType(), 'Accept'); -// } -// -// /** -// * Test construct with type and value -// */ -// public function testConstructWithTypeValue() -// { -// $header= new Header('Accept', 'text/html'); -// $this->assertEquals($header->getType(), 'Accept'); -// $this->assertEquals($header->getValue(), 'text/html'); -// } -// -// /** -// * Test construct with a header encoded in a raw string -// */ -// public function testConstructWithRawString() -// { -// $header= new Header('Accept: text/html'); -// $this->assertEquals($header->getType(), 'Accept'); -// $this->assertEquals($header->getValue(), 'text/html'); -// } -// -// /** -// * Test construct with Accept-Charset type and multiple values -// */ -// public function testConstructAcceptMultipleValue() -// { -// $header= new Header('Accept-Charset: iso-8859-1, utf-8'); -// $this->assertEquals($header->getValue(), 'iso-8859-1, utf-8'); -// } -// -// /** -// * Test normalize header type -// */ -// public function testNormalizeHeaderType() -// { -// $header= new Header('accept'); -// $this->assertEquals($header->getType(), 'Accept'); -// $header->setType('Accept charset'); -// $this->assertEquals($header->getType(), 'Accept-Charset'); -// } -// -// /** -// * Test load header from a raw string -// */ -// public function testLoadFromString() -// { -// $header= new Header('Accept'); -// $this->assertTrue($header->fromString('Accept: text/html')); -// $this->assertEquals($header->getType(), 'Accept'); -// $this->assertEquals($header->getValue(), 'text/html'); -// } -// -// /** -// * Test to string -// */ -// public function testToString() -// { -// $header= new Header('Accept', 'text/html'); -// $this->assertEquals((string) $header,"Accept: text/html\r\n"); -// } -// -// /** -// * Test load header from an invalid raw string -// */ -// public function testLoadFromInvalidString() -// { -// $header= new Header('Accept'); -// $this->setExpectedException( -// 'Zend\Http\Exception\InvalidArgumentException', -// 'The header specified is not valid' -// ); -// $header->fromString('text/html'); -// } -// -// /** -// * Test set type -// */ -// public function testSetType() -// { -// $header= new Header('Accept'); -// $header->setType('Accept-Encoding'); -// $this->assertEquals($header->getType(), 'Accept-Encoding'); -// } -// -// /** -// * Test set value -// */ -// public function testSetValue() -// { -// $header= new Header('Accept'); -// $header->setValue('text/html'); -// $this->assertEquals($header->getValue(), 'text/html'); -// } -// -// /** -// * Test has value -// */ -// public function testHasValue() -// { -// $header= new Header('Accept: text/html'); -// $this->assertTrue($header->hasValue('text/html')); -// $this->assertEquals($header->getValue(), 'text/html'); -// } -// -// /** -// * Test has value with multiple values -// */ -// public function testHasValueWithMultiple() -// { -// $header= new Header('Accept: text/html, text/plain'); -// $this->assertTrue($header->hasValue('text/html')); -// $this->assertTrue($header->hasValue('text/plain')); -// $this->assertEquals($header->getValue(), 'text/html, text/plain'); -// } -// -// /** -// * Test quality factor value -// */ -// public function testQualityFactor() -// { -// $header= new Header('Accept-Charset: iso-8859-1, utf-8;q=0.5, *;q=0.5'); -// $this->assertTrue($header->hasValue('utf-8')); -// $this->assertEquals($header->getQualityFactor('utf-8'), '0.5'); -// $this->assertTrue($header->hasValue('iso-8859-1')); -// $this->assertEquals($header->getQualityFactor('iso-8859-1'), '1'); // by default -// $this->assertTrue($header->hasValue('*')); -// $this->assertEquals($header->getQualityFactor('*'), '0.5'); -// } -// -// /** -// * Test level value -// */ -// public function testLevel() -// { -// $header= new Header('Accept-Charset: iso-8859-1;level=1, utf-8;q=0.5;level=2, *;q=0.5'); -// $this->assertTrue($header->hasValue('utf-8')); -// $this->assertEquals($header->getLevel('utf-8'), '2'); -// $this->assertTrue($header->hasValue('iso-8859-1')); -// $this->assertEquals($header->getLevel('iso-8859-1'), '1'); // by default -// $this->assertTrue($header->hasValue('*')); -// $this->assertEquals($header->getLevel('*'),false); -// } + public function testCanParseCommaSeparatedValues() + { + $header = Accept::fromString('Accept: text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c'); + $this->assertTrue($header->hasMediaType('text/plain')); + $this->assertTrue($header->hasMediaType('text/html')); + $this->assertTrue($header->hasMediaType('text/x-dvi')); + $this->assertTrue($header->hasMediaType('text/x-c')); + } + + public function testPrioritizesValuesBasedOnQParameter() + { + $header = Accept::fromString('Accept: text/plain; q=0.5, text/html, text/xml; q=0, text/x-dvi; q=0.8, text/x-c'); + $expected = array( + 'text/html', + 'text/x-c', + 'text/x-dvi', + 'text/plain', + ); + $test = array(); + foreach($header->getPrioritized() as $type) { + $test[] = $type; + } + $this->assertEquals($expected, $test); + } }