diff options
Diffstat (limited to 'src/ext/HTTP_Accept.class.php')
-rw-r--r-- | src/ext/HTTP_Accept.class.php | 659 |
1 files changed, 659 insertions, 0 deletions
diff --git a/src/ext/HTTP_Accept.class.php b/src/ext/HTTP_Accept.class.php new file mode 100644 index 0000000..5efaa5d --- /dev/null +++ b/src/ext/HTTP_Accept.class.php @@ -0,0 +1,659 @@ +<?php + +/** + * HTTP_Accept class for dealing with the HTTP 'Accept' header + * + * PHP versions 4 and 5 + * + * LICENSE: This source file is subject to the MIT License. + * The full text of this license is available at the following URL: + * http://www.opensource.org/licenses/mit-license.php + * + * @category HTTP + * @package HTTP_Accept + * @author Kevin Locke <kwl7@cornell.edu> + * @copyright 2007 Kevin Locke + * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @version SVN: $Id: HTTP_Accept.php 22 2007-10-06 18:46:45Z kevin $ + * @link http://pear.php.net/package/HTTP_Accept + */ + +/** + * HTTP_Accept class for dealing with the HTTP 'Accept' header + * + * This class is intended to be used to parse the HTTP Accept header into + * usable information and provide a simple API for dealing with that + * information. + * + * The parsing of this class is designed to follow RFC 2616 to the letter, + * any deviations from that standard are bugs and should be reported to the + * maintainer. + * + * Often the class will be used very simply as + * <code> + * <?php + * $accept = new HTTP_Accept($_SERVER['HTTP_ACCEPT']); + * if ($accept->getQuality("image/png") > $accept->getQuality("image/jpeg")) + * // Send PNG image + * else + * // Send JPEG image + * ?> + * </code> + * + * However, for browsers which do not accurately describe their preferences, + * it may be necessary to check if a MIME Type is explicitly listed in their + * Accept header, in addition to being preferred to another type + * + * <code> + * <?php + * if ($accept->isMatchExact("application/xhtml+xml")) + * // Client specifically asked for this type at some quality level + * ?> + * </code> + * + * + * @category HTTP + * @package HTTP_Accept + * @access public + * @link http://pear.php.net/package/HTTP_Accept + */ +class HTTP_Accept +{ + /** + * Array of types and their associated parameters, extensions, and quality + * factors, as represented in the Accept: header. + * Indexed by [type][subtype][index], + * and contains 'PARAMS', 'QUALITY', and 'EXTENSIONS' keys for the + * parameter set, quality factor, and extensions set respectively. + * Note: Since type, subtype, and parameters are case-insensitive + * (RFC 2045 5.1) they are stored as lower-case. + * + * @var array + * @access private + */ + var $acceptedtypes = array(); + + /** + * Regular expression to match a token, as defined in RFC 2045 + * + * @var string + * @access private + */ + var $_matchtoken = '(?:[^[:cntrl:]()<>@,;:\\\\"\/\[\]?={} \t]+)'; + + /** + * Regular expression to match a quoted string, as defined in RFC 2045 + * + * @var string + * @access private + */ + var $_matchqstring = '(?:"[^\\\\"]*(?:\\\\.[^\\\\"]*)*")'; + + /** + * Constructs a new HTTP_Accept object + * + * Initializes the HTTP_Accept class with a given accept string + * or creates a new (empty) HTTP_Accept object if no string is given + * + * Note: The behavior is a little strange here to accomodate + * missing headers (to be interpreted as accept all) as well as + * new empty objects which should accept nothing. This means that + * HTTP_Accept("") will be different than HTTP_Accept() + * + * @access public + * @return object HTTP_Accept + * @param string $acceptstring The value of an Accept: header + * Will often be $_SERVER['HTTP_ACCEPT'] + * Note: If get_magic_quotes_gpc is on, + * run stripslashes() on the string first + */ + function HTTP_Accept() + { + if (func_num_args() == 0) { + // User wishes to create empty HTTP_Accept object + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 0, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + $acceptstring = trim(func_get_arg(0)); + if (empty($acceptstring)) { + // Accept header empty or not sent, interpret as "*/*" + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 1, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + $matches = preg_match_all( + '/\s*('.$this->_matchtoken.')\/' . // typegroup/ + '('.$this->_matchtoken.')' . // subtype + '((?:\s*;\s*'.$this->_matchtoken.'\s*' . // parameter + '(?:=\s*' . // optional =value + '(?:'.$this->_matchqstring.'|'.$this->_matchtoken.'))?)*)/', // value + $acceptstring, $acceptedtypes, + PREG_SET_ORDER); + + if ($matches == 0) { + // Malformed Accept header + $this->acceptedtypes = array( + '*' => array( + '*' => array ( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 1, + 'EXTENSIONS' => array() + ) + ) + ) + ); + return; + } + + foreach ($acceptedtypes as $accepted) { + $typefamily = strtolower($accepted[1]); + $subtype = strtolower($accepted[2]); + + // */subtype is invalid according to grammar in section 14.1 + // so we ignore it + if ($typefamily == '*' && $subtype != '*') + continue; + + // Parse all arguments of the form "key=value" + $matches = preg_match_all('/;\s*('.$this->_matchtoken.')\s*' . + '(?:=\s*' . + '('.$this->_matchqstring.'|'. + $this->_matchtoken.'))?/', + $accepted[3], $args, + PREG_SET_ORDER); + + $params = array(); + $quality = -1; + $extensions = array(); + foreach ($args as $arg) { + array_shift($arg); + if (!empty($arg[1])) { + // Strip quotes (Note: Can't use trim() in case "text\"") + $len = strlen($arg[1]); + if ($arg[1][0] == '"' && $arg[1][$len-1] == '"' + && $len > 1) { + $arg[1] = substr($arg[1], 1, $len-2); + $arg[1] = stripslashes($arg[1]); + } + } else if (!isset($arg[1])) { + $arg[1] = null; + } + + // Everything before q=# is a parameter, after is an extension + if ($quality >= 0) { + $extensions[$arg[0]] = $arg[1]; + } else if ($arg[0] == 'q') { + $quality = (float)$arg[1]; + + if ($quality < 0) + $quality = 0; + else if ($quality > 1) + $quality = 1; + } else { + $arg[0] = strtolower($arg[0]); + // Values required for parameters, + // assume empty-string for missing values + if (isset($arg[1])) + $params[$arg[0]] = $arg[1]; + else + $params[$arg[0]] = ""; + } + } + + if ($quality < 0) + $quality = 1; + else if ($quality == 0) + continue; + + if (!isset($this->acceptedtypes[$typefamily])) + $this->acceptedtypes[$typefamily] = array(); + if (!isset($this->acceptedtypes[$typefamily][$subtype])) + $this->acceptedtypes[$typefamily][$subtype] = array(); + + $this->acceptedtypes[$typefamily][$subtype][] = + array('PARAMS' => $params, + 'QUALITY' => $quality, + 'EXTENSIONS' => $extensions); + } + + if (!isset($this->acceptedtypes['*'])) + $this->acceptedtypes['*'] = array(); + if (!isset($this->acceptedtypes['*']['*'])) + $this->acceptedtypes['*']['*'] = array( + 0 => array( + 'PARAMS' => array(), + 'QUALITY' => 0, + 'EXTENSIONS' => array() + ) + ); + } + + /** + * Gets the accepted quality factor for a given MIME Type + * + * Note: If there are multiple best matches + * (e.g. "text/html;level=4;charset=utf-8" matching both "text/html;level=4" + * and "text/html;charset=utf-8"), it returns the lowest quality factor as + * a conservative estimate. Further, if the ambiguity is between parameters + * and extensions (e.g. "text/html;level=4;q=1;ext=foo" matching both + * "text/html;level=4" and "text/html;q=1;ext=foo") the parameters take + * precidence. + * + * Usage Note: If the quality factor for all supported media types is 0, + * RFC 2616 specifies that applications SHOULD send an HTTP 406 (not + * acceptable) response. + * + * @access public + * @return double the quality value for the given MIME Type + * Quality values are in the range [0,1] where 0 means + * "not accepted" and 1 is "perfect quality". + * @param string $mimetype The MIME Type to query ("text/html") + * @param array $params Parameters of Type to query ([level => 4]) + * @param array $extensions Extension parameters to query + */ + function getQuality($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + if (empty($this->acceptedtypes[$supertype])) { + if ($supertype == '*') + return 0; + else + return $this->getQuality("*/*", $params, $extensions); + } + + if (empty($this->acceptedtypes[$supertype][$subtype])) { + if ($subtype == '*') + return $this->getQuality("*/*", $params, $extensions); + else + return $this->getQuality("$supertype/*", $params, $extensions); + } + + $params = array_change_key_case($params, CASE_LOWER); + + $matches = $this->_findBestMatchIndices($supertype, $subtype, + $params, $extensions); + + if (count($matches) == 0) { + if ($subtype != '*') + return $this->getQuality("$supertype/*", $params, $extensions); + else if ($supertype != '*') + return $this->getQuality("*/*", $params, $extensions); + else + return 0; + } + + $minquality = 1; + foreach ($matches as $match) + if ($this->acceptedtypes[$supertype][$subtype][$match]['QUALITY'] < $minquality) + $minquality = $this->acceptedtypes[$supertype][$subtype][$match]['QUALITY']; + + return $minquality; + } + + /** + * Determines if there is an exact match for the specified MIME Type + * + * @access public + * @return boolean true if there is an exact match to the given + * values, false otherwise. + * @param string $mimetype The MIME Type to query (e.g. "text/html") + * @param array $params Parameters of Type to query (e.g. [level => 4]) + * @param array $extensions Extension parameters to query + */ + function isMatchExact($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + return $this->_findExactMatchIndex($supertype, $subtype, + $params, $extensions) >= 0; + } + + /** + * Gets a list of all MIME Types explicitly accepted, sorted by quality + * + * @access public + * @return array list of MIME Types explicitly accepted, sorted + * in decreasing order of quality factor + */ + function getTypes() + { + $qvalues = array(); + $types = array(); + foreach ($this->acceptedtypes as $typefamily => $subtypes) { + if ($typefamily == '*') + continue; + + foreach ($subtypes as $subtype => $variants) { + if ($subtype == '*') + continue; + + $maxquality = 0; + foreach ($variants as $variant) + if ($variant['QUALITY'] > $maxquality) + $maxquality = $variant['QUALITY']; + + if ($maxquality > 0) { + $qvalues[] = $maxquality; + $types[] = $typefamily.'/'.$subtype; + } + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $types, SORT_DESC, SORT_STRING); + + return $types; + } + + /** + * Gets the parameter sets for a given mime type, sorted by quality. + * Only parameter sets where the extensions set is empty will be returned. + * + * @access public + * @return array list of sets of name=>value parameter pairs + * in decreasing order of quality factor + * @param string $mimetype The MIME Type to query ("text/html") + */ + function getParameterSets($mimetype) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if (!isset($this->acceptedtypes[$supertype]) + || !isset($this->acceptedtypes[$supertype][$subtype])) + return array(); + + $qvalues = array(); + $paramsets = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $acceptedtype) { + if (count($acceptedtype['EXTENSIONS']) == 0) { + $qvalues[] = $acceptedtype['QUALITY']; + $paramsets[] = $acceptedtype['PARAMS']; + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $paramsets, SORT_DESC, SORT_STRING); + + return $paramsets; + } + + /** + * Gets the extension sets for a given mime type, sorted by quality. + * Only extension sets where the parameter set is empty will be returned. + * + * @access public + * @return array list of sets of name=>value extension pairs + * in decreasing order of quality factor + * @param string $mimetype The MIME Type to query ("text/html") + */ + function getExtensionSets($mimetype) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if (!isset($this->acceptedtypes[$supertype]) + || !isset($this->acceptedtypes[$supertype][$subtype])) + return array(); + + $qvalues = array(); + $extensionsets = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $acceptedtype) { + if (count($acceptedtype['PARAMS']) == 0) { + $qvalues[] = $acceptedtype['QUALITY']; + $extensionsets[] = $acceptedtype['EXTENSIONS']; + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $extensionsets, SORT_DESC, SORT_STRING); + + return $extensionsets; + } + + /** + * Adds a type to the set of accepted types + * + * @access public + * @param string $mimetype The MIME Type to add (e.g. "text/html") + * @param double $quality The quality value for the given MIME Type + * Quality values are in the range [0,1] where + * 0 means "not accepted" and 1 is + * "perfect quality". + * @param array $params Parameters of the type to add (e.g. [level => 4]) + * @param array $extensions Extension parameters of the type to add + */ + function addType($mimetype, $quality = 1, + $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + $index = $this->_findExactMatchIndex($supertype, $subtype, $params, $extensions); + + if ($index >= 0) { + $this->acceptedtypes[$supertype][$subtype][$index]['QUALITY'] = $quality; + } else { + if (!isset($this->acceptedtypes[$supertype])) + $this->acceptedtypes[$supertype] = array(); + if (!isset($this->acceptedtypes[$supertype][$subtype])) + $this->acceptedtypes[$supertype][$subtype] = array(); + + $this->acceptedtypes[$supertype][$subtype][] = + array('PARAMS' => $params, + 'QUALITY' => $quality, + 'EXTENSIONS' => $extensions); + } + } + + /** + * Removes a type from the set of accepted types + * + * @access public + * @param string $mimetype The MIME Type to remove (e.g. "text/html") + * @param array $params Parameters of the type to remove (e.g. [level => 4]) + * @param array $extensions Extension parameters of the type to remove + */ + function removeType($mimetype, $params = array(), $extensions = array()) + { + $type = explode("/", $mimetype); + $supertype = strtolower($type[0]); + $subtype = strtolower($type[1]); + + if ($params == null) + $params = array(); + if ($extensions == null) + $extensions = array(); + + $index = $this->_findExactMatchIndex($supertype, $subtype, $params, $extensions); + + if ($index >= 0) { + $this->acceptedtypes[$supertype][$subtype] = + array_merge(array_slice($this->acceptedtypes[$supertype][$subtype], + 0, $index), + array_slice($this->acceptedtypes[$supertype][$subtype], + $index+1)); + } + } + + /** + * Gets a string representation suitable for use in an HTTP Accept header + * + * @access public + * @return string a string representation of this object + */ + function __toString() + { + $accepted = array(); + $qvalues = array(); + foreach ($this->acceptedtypes as $supertype => $subtypes) { + foreach ($subtypes as $subtype => $entries) { + foreach ($entries as $entry) { + $accepted[] = array('TYPE' => "$supertype/$subtype", + 'QUALITY' => $entry['QUALITY'], + 'PARAMS' => $entry['PARAMS'], + 'EXTENSIONS' => $entry['EXTENSIONS']); + $qvalues[] = $entry['QUALITY']; + } + } + } + + array_multisort($qvalues, SORT_DESC, SORT_NUMERIC, + $accepted); + + $str = ""; + foreach ($accepted as $accept) { + // Skip the catchall value if it is 0, since this is implied + if ($accept['TYPE'] == '*/*' && + $accept['QUALITY'] == 0 && + count($accept['PARAMS']) == 0 && + count($accept['EXTENSIONS'] == 0)) + continue; + + $str = $str.$accept['TYPE'].';'; + + foreach ($accept['PARAMS'] as $param => $value) { + if (preg_match('/^'.$this->_matchtoken.'$/', $value)) + $str = $str.$param.'='.$value.';'; + else + $str = $str.$param.'="'.addcslashes($value,'"\\').'";'; + } + + if ($accept['QUALITY'] < 1 || !empty($accept['EXTENSIONS'])) + $str = $str.'q='.$accept['QUALITY'].';'; + + foreach ($accept['EXTENSIONS'] as $extension => $value) { + if (preg_match('/^'.$this->_matchtoken.'$/', $value)) + $str = $str.$extension.'='.$value.';'; + else + $str = $str.$extension.'="'.addcslashes($value,'"\\').'";'; + } + + $str[strlen($str)-1] = ','; + } + + return rtrim($str, ','); + } + + /** + * Finds the index of an exact match for the specified MIME Type + * + * @access private + * @return int the index of an exact match if found, + * -1 otherwise + * @param string $supertype The general MIME Type to find (e.g. "text") + * @param string $subtype The MIME subtype to find (e.g. "html") + * @param array $params Parameters of Type to find ([level => 4]) + * @param array $extensions Extension parameters to find + */ + function _findExactMatchIndex($supertype, $subtype, $params, $extensions) + { + if (empty($this->acceptedtypes[$supertype]) + || empty($this->acceptedtypes[$supertype][$subtype])) + return -1; + + $params = array_change_key_case($params, CASE_LOWER); + + $parammatches = array(); + foreach ($this->acceptedtypes[$supertype][$subtype] as $index => $typematch) + if ($typematch['PARAMS'] == $params + && $typematch['EXTENSIONS'] == $extensions) + return $index; + + return -1; + } + + /** + * Finds the indices of the best matches for the specified MIME Type + * + * A "match" in this context is an exact type match and no extraneous + * matches for parameters or extensions (so the best match for + * "text/html;level=4" may be "text/html" but not the other way around). + * + * "Best" is interpreted as the entries that match the most + * parameters and extensions (the sum of the number of matches) + * + * @access private + * @return array an array of the indices of the best matches + * (empty if no matches) + * @param string $supertype The general MIME Type to find (e.g. "text") + * @param string $subtype The MIME subtype to find (e.g. "html") + * @param array $params Parameters of Type to find ([level => 4]) + * @param array $extensions Extension parameters to find + */ + function _findBestMatchIndices($supertype, $subtype, $params, $extensions) + { + $bestmatches = array(); + $bestlength = 0; + + if (empty($this->acceptedtypes[$supertype]) + || empty($this->acceptedtypes[$supertype][$subtype])) + return $bestmatches; + + foreach ($this->acceptedtypes[$supertype][$subtype] as $index => $typematch) { + if (count(array_diff_assoc($typematch['PARAMS'], $params)) == 0 + && count(array_diff_assoc($typematch['EXTENSIONS'], + $extensions)) == 0) { + $length = count($typematch['PARAMS']) + + count($typematch['EXTENSIONS']); + + if ($length > $bestlength) { + $bestmatches = array($index); + $bestlength = $length; + } else if ($length == $bestlength) { + $bestmatches[] = $index; + } + } + } + + return $bestmatches; + } +} + +// vim: set ts=4 sts=4 sw=4 et: +?> |