Skip to content

Instantly share code, notes, and snippets.

@Freeaqingme
Created February 3, 2012 18:35
Show Gist options
  • Save Freeaqingme/1731637 to your computer and use it in GitHub Desktop.
Save Freeaqingme/1731637 to your computer and use it in GitHub Desktop.
BSD'ed AcceptHandler Plugin by Freeaqingme, Enrise
<?php
/**
* Determne the media type and format to serve based on the accept headers
*
* How this class works:
* It first determines which mediatypes that are part of the accept header can be
* possibly used based on their formats. In a later stage one can pick
* a specific mediatype using the method match(). This method is meant to select
* the most specific entry that still matches the requested media type.
*
* @author Dolf Schimmel - www.enrise.com
* @license 3clause BSD
*
*/
class Project_Controller_Action_Helper_AcceptHandler
extends Zend_Controller_Action_Helper_Abstract
{
public $debug = false;
const RETURN_FLAT = 1;
const RETURN_NESTED = 2;
const RETURN_AS_ARRAY = 4;
const RETURN_AS_OBJECT = 8;
protected $_formatsAllowed = array();
protected $_formatDefault;
protected $_viableTypes;
protected $_priorities;
public function setAllowedFormats(array $formats = array())
{
$this->_formatsAllowed = $formats;
}
public function getAllowedFormats()
{
return $this->_formatsAllowed;
}
/**
* Return the possible matching types
* @param intger $returnMethod Default RETURN_FLAT | RETURN_AS_OBJECT
*/
public function getViableTypes($returnMethod = 5, $headerString = null)
{
if (!$this->_viableTypes) {
if (!$headerString) {
$headerString = $this->getRequest()->getHeader('Accept');
}
$this->_viableTypes = $this->_determineViableTypes($headerString);
}
if ($returnMethod & self::RETURN_FLAT) {
return $this->_getFlattenedViableTypeTree($returnMethod);
}
if ($returnMethod & self::RETURN_AS_ARRAY && $returnMethod & self::RETURN_NESTED) {
return $this->_viableTypes;
}
$out = array();
foreach($this->_viableTypes as $prio => $branch) {
foreach($branch as $type) {
$out[$prio][] = (object) $type;
}
}
return $out;
}
public function clearViableTypes()
{
$this->_viableTypes = null;
return $this;
}
protected function _determineViableTypes($headerString)
{
if (!$headerString) {
$this->_viableTypes = array();
return;
}
$mediaTypes = explode(',', $headerString);
$out = array();
foreach($mediaTypes as $mediaTypeString) {
$mediaType = $this->mediaTypeToArray($mediaTypeString);
foreach($this->getAllowedFormats() as $typeAllowed => $subTypeAllowed) {
if (($mediaType['format'] == $subTypeAllowed &&
is_numeric($typeAllowed))
||
(!is_numeric($typeAllowed) &&
$mediaType['format'] == $subTypeAllowed &&
$mediaType['type'] == $typeAllowed)
||
($mediaType['type'] == (string)$typeAllowed && $mediaType['format'] == '*')
||
($mediaType['type'] == '*' && $mediaType['format'] == '*')
) {
$out[$mediaType['priority']][] = $mediaType;
break;
}
}
}
foreach($out as $prio => $value) {
$out[$prio] = $this->_sortTypeBranch($value);
}
krsort($out);
return $out;
}
protected function _sortTypeBranch(array $branch) {
$sort = function($a, $b) // If A has higher prio than B, return -1.
{
// Asterisks
$values = array('type', 'subtype','format');
foreach($values as $value) {
if($a[$value] == '*' && $b[$value] == '*') {
return 0;
} elseif($a[$value] == '*') {
return 1;
} elseif($b[$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($branch, $sort);
return $branch;
}
public function match($match, $setHeaders = true, $headerString = null)
{
if (! ($result = $this->_match($match, $headerString)) ) {
return false;
}
if($setHeaders) {
if ($result['format'] == $result['subtypeRaw']) {
$contentType = $result['type'] . '/'
. $result['subtype'];
} else {
$contentType = $result['type'] . '/'
. $result['subtype'] . '+'
. $result['format'];
}
$response = $this->getResponse();
$response->setHeader('Vary', 'Accept')
->setHeader('Content-Type', $contentType)
->setOutputFormat($result['format']);
$this->getRequest()->setParam('format', $result['format']);
}
return $result;
}
protected function _match($collectionMatch, $headerString)
{
$viableTypes = $this->getViableTypes(self::RETURN_NESTED|self::RETURN_AS_ARRAY, $headerString);
$matches = array();
foreach ((array)$collectionMatch as $match) {
if (is_array($match)) {
$matches[] = & $match;
} else {
$matches[] = $this->mediaTypeToArray($match);
}
}
foreach($viableTypes as $prio => $typeGroup)
{
foreach($typeGroup as $type) {
foreach($matches as $match) {
if($type['type'] == '*') {
if ($match['format'] == '*' && $match['format'] == '*') {
$allowedFormats = $this->getAllowedFormats();
if(array_key_exists($match['type'], $allowedFormats)) {
$match['format'] = $allowedFormats[$match['type']];
} else {
$match['format'] = $this->getDefaultFormat(true);
}
}
if ($res = $this->_matchParams($match, $type)) {
return $res;
}
}
if ($match['type'] == $type['type'])
{
if ((($match['subtype'] == $type['subtype'] ||
$match['subtype'] == '*') &&
($match['format'] == $type['format'] ||
$match['format'] == '*')))
{
if ($res = $this->_matchParams($type, $match)) {
return $res;
}
}
}
}
}
}
return false;
}
protected function _matchParams(array $match1, array $match2)
{
foreach($match2['params'] as $key => $value) {
if (isset($match1['params'][$key])) {
if (strpos($value, '-')) {
$values = explode('-', $value, 2);
if($values[0] > $match1['params'][$key] ||
$values[1] < $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;
}
public function mediaTypeToArray($mediaType)
{
$raw = $mediaType;
if ($pos = strpos($mediaType, '/')) {
$type = trim(substr($mediaType, 0, $pos));
} else {
$type = trim(substr($mediaType, 0));
}
$params = array();
if(($pos = strpos($mediaType,';'))) {
$paramsStrings = explode(';', substr($mediaType, $pos+1));
foreach($paramsStrings as $value) { // fetch q=0.2 to array
$explode = explode('=', $value, 2);
$params[trim($explode[0])] = trim($explode[1]);
}
}
if ($pos = strpos($mediaType, ';')) {
$mediaType = trim(substr($mediaType, 0, $pos));
}
if ($pos = strpos($mediaType, '/')) {
$subtypeWhole = $format = $subtype = trim(substr($mediaType, strpos($mediaType, '/')+1));
} else {
$subtypeWhole = '';
$format = '*';
$subtype = '*';
}
$pos = strpos($subtype, '+');
if (false !== $pos) {
$format = trim(substr($subtype, $pos+1));
$subtype = trim(substr($subtype, 0, $pos));
}
return array(
'typeString' => trim($mediaType),
'type' => $type,
'subtype' => $subtype,
'subtypeRaw' => $subtypeWhole,
'format' => $format,
'priority' => isset($params['q']) ? $params['q'] : 1,
'params' => $params,
'raw' => trim($raw)
);
}
protected function _getFlattenedViableTypeTree($returnMethod)
{
if($returnMethod & self::RETURN_AS_ARRAY) {
$tree = $this->getViableTypes(self::RETURN_NESTED|self::RETURN_AS_ARRAY);
} else {
$tree = $this->getViableTypes(self::RETURN_NESTED|self::RETURN_AS_OBJECT);
}
$flat = array();
foreach ($tree as $branch) {
$flat = array_merge($flat, $branch);
}
return $flat;
}
public function getDefaultFormat($feelingLucky = false) {
if (!$feelingLucky) {
return $this->_formatDefault;
}
if (null == $this->_formatDefault && $formats = $this->getAllowedFormats()) {
return $formats[0];
}
return $this->_formatDefault;
}
public function setDefaultFormat($format)
{
if (!in_array($format, $this->_formatDefault)) {
throw new exception(
'A default format was tried to set that is not an allowed format'
);
}
$this->_formatDefault = (string) $format;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment