Skip to content

Commit

Permalink
Merge branch 'Rupert-RR-2131-add-accept-header-parsing'
Browse files Browse the repository at this point in the history
* Rupert-RR-2131-add-accept-header-parsing:
  php doc adjustments after #2132
  Yii code style correction
  Removed unnecessary spaces.
  Transferred data for unit tests from the test functions into data providers.
  Undo accidental permissions change on bootstrap.php
  Added unit test file for CHttpRequest for the methods parseAcceptHeader() and compareAcceptTypes(). Modified the regexp in parseAcceptHeader() to accept wildcards in the path. Modified the description of compareAcceptTypes() to better reflect the comparison result (higher preference returns lower value, so that most preferred is first in the array).
  parseAcceptHeader() function description tidy up.
  Typo corrections and code tidy up.
  Altered parseAcceptHeader() to use only one regexp. Thanks to Ka on StackExchange for this expression.
  Typo corrections
  Separated out parse and compare functions. Reduced regular expression count to 2 from 3. Modified MIME type array map structure.
  Added #135 back in changelog, which got lost somehow..
  Moved position of line to follow numerical order.
  Enh #2131: Added Accept header parsing to CHttpRequest to give an array of accepted types in order of preference
  • Loading branch information
cebe committed Mar 27, 2013
2 parents 5e87516 + 7d91ceb commit 968ff9c
Show file tree
Hide file tree
Showing 3 changed files with 414 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -49,6 +49,7 @@ Version 1.1.14 work in progress
- Enh #2038: CFormatter::formatNtext() method can replace newlines with `<p></p>` not just with `<br />` as it was before (resurtm)
- Enh #2090: Allow passing array of columns to CDbSchema::addPrimaryKey() (paystey)
- Enh #2096: CAPTCHA: non-free Duality.ttf font replaced by open/free SpicyRice.ttf (licensed under SIL OFL v1.1) (resurtm)
- Enh #2131: Added Accept header parsing to CHttpRequest to give an array of accepted types in order of preference (Rupert-RR)
- Enh #2135: MessageCommand can now handles Yii::t() messages with files in subfolders (firsyura)
- Enh #2205: CActiveForm::error() now depends on CHtml::$errorContainerTag (malyshev)
- Enh #2217: Support of the empty option for CHtml::radioButtonList() has been introduced (resurtm)
Expand Down
140 changes: 140 additions & 0 deletions framework/web/CHttpRequest.php
Expand Up @@ -50,6 +50,8 @@
* @property integer $port Port number for insecure requests.
* @property integer $securePort Port number for secure requests.
* @property CCookieCollection|CHttpCookie[] $cookies The cookie collection.
* @property array $preferredAcceptType The user preferred accept type as an array map, e.g. array('type' => 'application', 'subType' => 'xhtml', 'baseType' => 'xml', 'params' => array('q' => 0.9)).
* @property array $preferredAcceptTypes An array of all user accepted types (as array maps like array('type' => 'application', 'subType' => 'xhtml', 'baseType' => 'xml', 'params' => array('q' => 0.9)) ) in order of preference.
* @property string $preferredLanguage The user preferred language.
* @property array $preferredLanguages An array of all user accepted languages in order of preference.
* @property string $csrfToken The random token for CSRF validation.
Expand Down Expand Up @@ -93,6 +95,7 @@ class CHttpRequest extends CApplicationComponent
private $_hostInfo;
private $_baseUrl;
private $_cookies;
private $_preferredAcceptTypes;
private $_preferredLanguages;
private $_csrfToken;
private $_restParams;
Expand Down Expand Up @@ -376,6 +379,7 @@ public function setBaseUrl($value)
/**
* Returns the relative URL of the entry script.
* The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework.
* @throws CException when it is unable to determine the entry script URL.
* @return string the relative URL of the entry script.
*/
public function getScriptUrl()
Expand Down Expand Up @@ -800,6 +804,142 @@ public function redirect($url,$terminate=true,$statusCode=302)
Yii::app()->end();
}

/**
* Parses an HTTP Accept header, returning an array map with all parts of each entry.
* Each array entry consists of a map with the type, subType, baseType and params, an array map of key-value parameters,
* obligatorily including a `q` value (i.e. preference ranking) as a double.
* For example, an Accept header value of <code>'application/xhtml+xml;q=0.9;level=1'</code> would give an array entry of
* <pre>
* array(
* 'type' => 'application',
* 'subType' => 'xhtml',
* 'baseType' => 'xml',
* 'params' => array(
* 'q' => 0.9,
* 'level' => '1',
* ),
* )
* </pre>
*
* <b>Please note:</b>
* To avoid great complexity, there are no steps taken to ensure that quoted strings are treated properly.
* If the header text includes quoted strings containing space or the , or ; characters then the results may not be correct!
*
* See also {@link http://tools.ietf.org/html/rfc2616#section-14.1} for details on Accept header.
* @param string $header the accept header value to parse
* @return array the user accepted MIME types.
*/
public static function parseAcceptHeader($header)
{
$matches=array();
$accepts=array();
// get individual entries with their type, subtype, basetype and params
preg_match_all('/(?:\G\s?,\s?|^)(\w+|\*)\/(\w+|\*)(?:\+(\w+))?|(?<!^)\G(?:\s?;\s?(\w+)=([\w\.]+))/',$header,$matches);
// the regexp should (in theory) always return an array of 6 arrays
if(count($matches)===6)
{
$i=0;
$itemLen=count($matches[1]);
while($i<$itemLen)
{
// fill out a content type
$accept=array(
'type'=>$matches[1][$i],
'subType'=>$matches[2][$i],
'baseType'=>null,
'params'=>array(),
);
// fill in the base type if it exists
if($matches[3][$i]!==null && $matches[3][$i]!=='')
$accept['baseType']=$matches[3][$i];
// continue looping while there is no new content type, to fill in all accompanying params
for($i++;$i<$itemLen;$i++)
{
// if the next content type is null, then the item is a param for the current content type
if($matches[1][$i]===null || $matches[1][$i]==='')
{
// if this is the quality param, convert it to a double
if($matches[4][$i]==='q')
{
// sanity check on q value
$q=(double)$matches[5][$i];
if($q>1)
$q=(double)1;
elseif($q<0)
$q=(double)0;
$accept['params'][$matches[4][$i]]=$q;
}
else
$accept['params'][$matches[4][$i]]=$matches[5][$i];
}
else
break;
}
// q defaults to 1 if not explicitly given
if(!isset($accept['params']['q']))
$accept['params']['q']=(double)1;
$accepts[] = $accept;
}
}
return $accepts;
}

/**
* Compare function for determining the preference of accepted MIME type array maps
* See {@link parseAcceptHeader()} for the format of $a and $b
* @param array $a user accepted MIME type as an array map
* @param array $b user accepted MIME type as an array map
* @return integer -1, 0 or 1 if $a has respectively greater preference, equal preference or less preference than $b (higher preference comes first).
*/
public static function compareAcceptTypes($a,$b)
{
// check for equal quality first
if($a['params']['q']===$b['params']['q'])
if(!($a['type']==='*' xor $b['type']==='*'))
if (!($a['subType']==='*' xor $b['subType']==='*'))
// finally, higher number of parameters counts as greater precedence
if(count($a['params'])===count($b['params']))
return 0;
else
return count($a['params'])<count($b['params']) ? 1 : -1;
// more specific takes precedence - whichever one doesn't have a * subType
else
return $a['subType']==='*' ? 1 : -1;
// more specific takes precedence - whichever one doesn't have a * type
else
return $a['type']==='*' ? 1 : -1;
else
return ($a['params']['q']<$b['params']['q']) ? 1 : -1;
}

/**
* Returns an array of user accepted MIME types in order of preference.
* Each array entry consists of a map with the type, subType, baseType and params, an array map of key-value parameters.
* See {@link parseAcceptHeader()} for a description of the array map.
* @return array the user accepted MIME types, as array maps, in the order of preference.
*/
public function getPreferredAcceptTypes()
{
if($this->_preferredAcceptTypes===null)
{
$accepts=self::parseAcceptHeader($this->getAcceptTypes());
usort($accepts,array(get_class($this),'compareAcceptTypes'));
$this->_preferredAcceptTypes=$accepts;
}
return $this->_preferredAcceptTypes;
}

/**
* Returns the user preferred accept MIME type.
* The MIME type is returned as an array map (see {@link parseAcceptHeader()}).
* @return array the user preferred accept MIME type or false if the user does not have any.
*/
public function getPreferredAcceptType()
{
$preferredAcceptTypes=$this->getPreferredAcceptTypes();
return empty($preferredAcceptTypes) ? false : $preferredAcceptTypes[0];
}

/**
* Returns an array of user accepted languages in order of preference.
* The returned language IDs will NOT be canonicalized using {@link CLocale::getCanonicalID}.
Expand Down

0 comments on commit 968ff9c

Please sign in to comment.