Managing Content-Negotiation with PHP

Back home and almost caught up on email…

One of the many things on my to-do list for Grand Rapids WiFi is to switch from the current system of having separate URLs for the RDF representations of resources to something based on content negotiation.

In order to do that using PHP I needed a couple of functions to extract the relevant headers. A quick web search didn’t turn up any appropriate libraries, so I threw together a couple of functions that should cover everything. For those not familiar with content-negotiation it works by having a user agent (web browser, newsreader, etc.) add an “Accept:” header specifying what types of content it can manage and which it prefers. For example, Firefox might send something like:

Accept: text/xml, application/xml, application/xhtml+xml, text/html;q=0.9,
text/plain;q=0.8, image/png, image/jpeg, image/gif;q=0.2, */*;q=0.1

The ‘q’ value is a measure of preference, and where none is specified we can assume a value of 1.0. So this browser prefers XML/XHTML over plain HTML, and HTML over plain text, but will accept anything (*/*).

So without more ado, the code:

/*
 * To be used with array_map. For a given content-type string
 * from an Accept: header this returns an array with the
 * content-type name and the preference level associated with
 * it.
 *
 * @author	James Stewart <james@jystewart.net>
 * @version	0.1
 * @license	http://creativecommons.org/licenses/by-sa/2.0/ Creative Commons Attribution ShareALike 2.0
 * @param	string	$type
 * @return	array	types with associated weighting
 */
function getValue($type)
{
	if (strstr($type, ";q=")) {
		$values = explode(";q=", $type);
	} else {
		$values = array($type, 1.0);
	}
	return $values;
}

/*
 * Given an HTTP Accept: header and an array of options, this function
 * returns an array of the overlap, ordered by the level of preference
 * sorted in order of descending preference.
 *
 * @author	James Stewart <james@jystewart.net>
 * @version	0.1
 * @license	http://creativecommons.org/licenses/by-sa/2.0/ Creative Commons Attribution ShareALike 2.0
 * @param	string	$header
 * @param	array	$options
 */
function parseAccept($header, $options)
{
	/* Explode types into an array */
	$types = explode(",", $header);
	$types = array_map("getValue", $types);

	/* Select only the requested options and sort by preference */
	$accepted = array();
	$accepted_type = array();
	$accepted_val = array();
	foreach ($types as $type) {
		if (in_array($type[0], $options)) {
			array_push($accepted, $type);
			array_push($accepted_type, $type[0]);
			array_push($accepted_val, $type[1]);
		}
	}
	array_multisort($accepted_val, SORT_DESC, $accepted_type, SORT_ASC, $accepted);
	return $accepted;
}

Update (03/09/07): Removed a space in the explode call. See comments for more details.

Tags: , ,

5 comments

  1. It appears that the code doesnt work. Amy I doing something wrong?

    $options = array(“text/html”);
    $prefs = parseAccept($_SERVER[“HTTP_ACCEPT”], $options);
    var_dump($prefs);

  2. Have you checked that the header is definitely not empty?

    Is the array returning any content?

    If the array is empty, it may be that the browser is simply sending a header of */* meaning it will accept anything in any order. I tend to use it with:

    $options = array(‘application/xhtml+xml’, ‘text/html’, ‘xml/rdf+xml’, ‘application/rdf+xml’)
    $prefs = parseAccept($_SERVER[‘HTTP_ACCEPT’], $options);

    switch ($prefs[0][0])
    {
    case ‘application/xhtml+xml’:
    // xhtml specific handling
    break;
    case ‘xml/rdf+xml’:
    case ‘application/rdf+xml’:
    // rdf specific handling
    break;
    default:
    // anything else
    break;
    }

  3. The Array was returning empty (I’m using FireFox 1.5.0.1 and PHP 5.1.2).

    I’ll have another look again tonight and see if I can identify the problem.

  4. Yup, its working fine now (I left the options array unpopulated) Duh!

  5. Hi,

    Just came back to your site (after a year!) and tried your code again, and noticed a small bug that casued the same symptoms discussed in the previous comments. The bug is this line:

    /* Explode types into an array */
    $types = explode(“, “, $header);

    There is a space after the comma in the delimiter. It should be:

    $types = explode(“,”, $header);

    That fixes the problem and the code runs perfectly!

    Cheers!