On 5 Jul 2009, at 01:52, Pierre-Antoine Champin wrote:

I guess a PHP version would not even require that .htaccess, but sorry, I'm not fluent in PHP ;)


The situation with PHP should be much the same, though I suppose web hosts might be more likely to set index.php in the DirectoryIndex as a default.

Anyway, I've done a quick port of your code to PHP. (I stripped out your connection negotiation code and replaced it with my own, as I figured out it would be faster to paste in the ConNeg class I'm familiar with rather than do line-by-line porting of the Python to PHP.) Here it is, same license - LGPL 3.

We should start a repository somewhere of useful code for serving linked data.

--
Toby A Inkster
<mailto:m...@tobyinkster.co.uk>
<http://tobyinkster.co.uk>

<?php

#    EasyPub: easy publication of RDF vocabulary
#    Copyright (C) 2009 Toby Inkster <m...@tobyinkster.co.uk>
#    Authors: Pierre-Antoine Champin <pcham...@liris.cnrs.fr>
#             Toby Inkster <m...@tobyinkster.co.uk>
#
#    EasyPub is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Lesser General Public License as published
#    by the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    EasyPub is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Lesser General Public License for more details.
#
#    You should have received a copy of the GNU Lesser General Public License
#    along with EasyPub.  If not, see <http://www.gnu.org/licenses/>.

/*
This is a drop-in PHP script for publishing RDF vocabulary.

Quick start
===========

Assuming you want to publish the vocabulary http://example.com/mydir/myvoc, the
reciepe with the most chances to work is the following:

1. Make `myvoc` a directory at a place where your HTTP server will serve it at
   the desired URI.
2. Copy the script in this directory as 'index.php'.
3. In the same directory, put two files named 'index.html' and 'index.rdf'

At this point, it may work (if you are lucky), or may have to tell your HTTP
server that the directory index (i.e. the file to serve for the bare directory)
is index.php.

In apache, this is done by creating (if not present) a `.htaccess` file in the
`myvoc` diractory, and adding the following line::
    DirectoryIndex index.php

Fortunately, this option is allowed to end-users by most webmasters.

More generaly
=============

The script will redirect, according to the Accept HTTP header, to a file with
the same name but a different extension. The file may have no extension at all,
so the following layout would work as well::

  mydir/myvoc (the script)
  mydir/myvoc.html
  mydir/myvoc.rdf

However, the tricky part is to convince the HTTP server to consider `myvoc` (an
extension-less file) as a PHP script (a thing in which I didn't succeed for the
moment...). The interesting feature of such a config is that it would support
"slash-based" vocabulary. For example, http://example.com/mydir/myvoc/MyTerm
would still redirect to the html or rdf file. This would not work with the reciep.
This would not work with the `index.php` recipe.

The script is can be configured to serve different files or support other mime
types by altering the `MAPPING` constant below.
*/

# the list below maps mime-types to redirection URL; %s is to be replaced by
# the script name (without its extension); note that the order may be
# significant (when matching */*)
$MAPPING = array(
	"text/html" => "%s.html",
	"application/rdf+xml" => "%s.rdf",
	## uncomment the following if applicable
	# "application/xhtml+xml" => "%s.html",
	# "application/turtle" => "%s.ttl",
	# "text/n3" => "%s.n3",
);

$HTML_REDIRECT = <<<CHUNK
<html>
<head><title>Non-Information Resource</title></head>
<body>
<h1>Non-Information Resource</h1>
You should be redirected to <a href="%1\$s">%1\$s</a>.
</body>
</html>
CHUNK
;

$HTML_NOT_ACCEPTABLE = <<<CHUNK
<html>
<head><title>No acceptable representation</title></head>
<body>
<h1>No acceptable representation</h1>
This server has no representation of the required resource that is acceptable
by your web agent. Available representations are:<ul>
%s
</ul>
</body>
</html>
CHUNK
;

$HTML_REPRESENTATION = '<li><a href="%1$s">%1$s</a> (%2$s)</li>'."\n";

main($MAPPING, $HTML_REDIRECT, $HTML_NOT_ACCEPTABLE, $HTML_REPRESENTATION);

function main ($map, $h_redir, $h_unaccept, $h_rep)
{
	# Convert list of available MIME types into a string suitable for ConNeg class.
	$offers = array();
	foreach ($map as $mime => $file)
		$offers[] = $mime;
	$offers = implode(',' , $offers);
	
	$chosen = ConNeg::negotiate($offers);
	
	if (empty($chosen) || empty($map[$chosen]))
	{
		$representations = '';
		foreach ($map as $mime => $file)
			$representations .= sprintf($h_rep, $file, $mime);
		$msg = sprintf($h_unaccept, $representations);
		header("HTTP/1.1 406 Not Acceptable");
		header("Content-Type: text/html; charset=us-ascii");
		header("Content-Length: " . strlen($msg));
		print $msg;
		exit;
	}
	
	else
	{
		$filename = sprintf($map[$chosen], basename($_SERVER['SCRIPT_NAME'], '.php'));

		$goto = sprintf('%s://%s%s/%s',
			(empty($_SERVER['HTTPS']) ? 'http' : 'https'),          # protocol
			$_SERVER['SERVER_NAME']                                 # authority
				.($_SERVER['SERVER_PORT']==80?'':(':'.($_SERVER['SERVER_PORT']))),
			dirname($_SERVER['SCRIPT_NAME']),                       # directory
			$filename                                               # file
			);
		$msg = sprintf($h_redir, $goto);
		header("HTTP/1.1 303 See Other");
		header("Location: $goto");
		header("Content-Type: text/html; charset=us-ascii");
		header("Content-Length: " . strlen($msg));
		print $msg;
		exit;
	}
}

class ConNeg
{
	public static function negotiate ($offers, $accept=null, $death=false)
	{
		if (!isset($accept))
		{
			$accept = $_SERVER['HTTP_ACCEPT'];
			header('Vary: Accept');
		}
		
		$a_parsed = self::parse_accept($accept);
		$o_parsed = self::parse_accept($offers);
		
		$best = self::choose_offer($o_parsed, $a_parsed);
		
		if (!isset($best))
		{
			if (!$death)
				return self::offer_serialise($o_parsed[0]);
			
			header('HTTP/1.1 406 Not Acceptable');
			header('Content-Type: text/plain; charset=utf-8');
			print "Acceptable types would have been:\n\n";
			foreach ($o_parsed as $o)
				print self::offer_serialise($o) . "\n";
			exit;
		}
		
		return self::offer_serialise($best);
	}

	# logically a constant, but a function is easier to comment.
	public static function STANDARD_OFFERS()
	{
		return 'application/xhtml+xml; charset=utf-8; x-serialisation=html, '	# XHTML+RDFa
			.'text/html; charset=utf-8; q=0.9; x-serialisation=html, '				#     Equivalent to above (for now)
			.'application/rdf+xml; x-serialisation=xml, '								# RDF/XML
			.'text/rdf; charset=utf-8; x-serialisation=xml, '							#     Alias for above
			.'application/xml; q=0.9; x-serialisation=xml, '							#     Equivalent to above (for now)
			.'text/xml; charset=utf-8; q=0.9; x-serialisation=xml, '					#     Equivalent to above (for now)
			.'application/rss+xml; x-serialisation=rss, '								# RSS 1.0 compatible RDF/XML
			.'application/turtle; x-serialisation=turtle, '								# Turtle
			.'application/x-turtle; x-serialisation=turtle, '							#     Alias for above
			.'text/turtle; charset=utf-8; x-serialisation=turtle, '					#     Alias for above
			.'text/n3; charset=utf-8; q=0.9; x-serialisation=turtle, '				#     Equivalent to above (for now)
			.'text/rdf+n3; charset=utf-8; q=0.9; x-serialisation=turtle, '			#         Alias for above
			.'application/json; x-serialisation=json, '									# JSON
			.'application/x-json; x-serialisation=json, '								#     Alias for above
			.'application/ecmascript; x-serialisation=js, '								# Javascript
			.'application/javascript; x-serialisation=js, '								#     Alias for above
			.'text/ecmascript; charset=utf-8; x-serialisation=js, '					#     Alias for above
			.'text/javascript; charset=utf-8; x-serialisation=js, '					#     Alias for above
			.'text/plain; charset=utf-8; x-serialisation=ntriples, '					# N-Triples
			.'application/turtle; level=nt; x-serialisation=ntriples, '				#     Alias for above
			.'application/x-turtle; level=nt; x-serialisation=ntriples, '			#     Alias for above
			.'text/turtle; charset=utf-8; level=nt; x-serialisation=ntriples'		#     Alias for above			
			; 
	}
	
	public static function serialisation ($fmt)
	{
		if (preg_match('/x-serialisation=([a-z]+)/i', $fmt, $matches))
		{
			return $matches[1];
		}

		return 'xml';
	}

	private static function offer_serialise ($o)
	{
		$rv = array($o['_type']);
		foreach ($o as $k => $v)
		{
			if ($k!='_type' && $k!='_position' && $k!='q')
				$rv[] = sprintf("%s=%s", $k, $v);
		}
		return implode('; ', $rv);
	}

	private static function parse_accept ($header)
	{
		$rv = array();
		$bits = preg_split('/\s*,\s*/', $header);
		for ($i=0; isset($bits[$i]); $i++)
		{
			$bit = trim($bits[$i]);
			
			$pieces = preg_split('/\s*;\s*/', $bit);
			$type   = strtolower(trim(array_shift($pieces)));
			$entry  = array('_position' => $i+1);
			
			foreach ($pieces as $piece)
			{
				list ($key, $val) = preg_split('/\s*\=\s*/', trim($piece));
				if (!isset($entry[ strtolower(trim($key)) ]))
					$entry[ strtolower(trim($key)) ] = trim($val);
			}
			
			if (!isset($entry['q']) || $entry['q'] > 1.0)
				$entry['q'] = 1.0;
			
			$entry['_type'] = $type;
			$rv[] = $entry;
		}
		
		return $rv;
	}

	private static function choose_offer ($offers, $requests)
	{
		$offer_scores = array();
		
		foreach ($offers as $offer)
		{
			foreach ($requests as $request)
			{
				if (self::match_offer($offer, $request))
				{
					$score = $offer;
					$score['q'] *= $request['q'];
					$score['_position'] *= $request['_position'];
					
					$offer_scores[] = $score;
				}
			}
		}
		
		usort($offer_scores, array(__CLASS__, 'score_sort'));
		
		return $offer_scores[0];
	}

	private static function score_sort ($a, $b)
	{
		if ((float)$a['q'] < (float)$b['q'])
			return 1;
		if ((float)$a['q'] > (float)$b['q'])
			return -1;
			
		if ($a['_position'] < $b['_position'])
			return -1;
		if ($a['_position'] > $b['_position'])
			return 1;
		
		return 0;
	}

	private static function match_offer ($o, $r)
	{
		# Content-Type - look for a mismatch
		list ($omaj, $omin) = explode('/', $o['_type']);
		list ($rmaj, $rmin) = explode('/', $r['_type']);
		if (!($omaj==$rmaj || $omaj=='*' || $rmaj=='*'))
			return false;
		if (!($omin==$rmin || $omin=='*' || $rmin=='*'))
			return false;
		
		# Content-Type Parameters - look for a mismatch
		foreach ($r as $rparam=>$rvalue)
		{
			if ($rparam != 'q' && $rparam != '_type' && $rparam != '_position')
			{
				if ($o[$rparam] != $rvalue)
					return false;
				print "OK\n";
			}
		}
		
		# No mismatches.
		return true;
	}
}




Reply via email to