Utiliser un système de routage dans les URL sans se prendre la tête avec un framework lourd et contraignant

Quand j'ai développé Bloginus pour avoir un CMS léger et sans base de données suite à une Nième vague d'attaques réussies de blogs sous WordPress non patchés, j'avais en tête la volonté de faire simple, léger et sécurisé. En moins de 1 Mo, j'ai un équivalent "light" de WordPress et propose deux thèmes par défaut. Le tout étant paramétrable par un système d'extensions gérant une partie visible, une partie administration et une partie configuration. J'ai fait simple pour les utilisateurs et simple pour les développeurs.

Les bases de ce développement sont les mêmes que j'utilise depuis des années sur mes outils personnels et quelques sites et backoffices clients : un système de route qui permet d'adresser la bonne extension qui va ensuite gérer les paramètres reçus soit dans l'URL, soit en $_GET et $_POST comme d'habitude.

Cette solution est basée sur l'URL Rewriting activée dans un .htaccess sous Apache. Bien entendu vous pouvez l'adapter pour Nginx ou un autre serveur web.

RewriteEngine on
RewriteRule robots.txt robots.txt [L]
RewriteRule ascreen.jpg ascreen.jpg [L]
RewriteRule favicon.ico favicon.ico [L]
RewriteRule favicon.png favicon.png [L]
RewriteRule apple-touch-icon.png apple-touch-icon.png [L]
RewriteRule (.*) index.php [L]

Je commence par lancer le moteur de réécriture d'URL fourni avec Apache puis je gère en dur quelques fichiers : 

  • robots.txt utilisé en référencement pour autoriser ou refuser l'accès des crawlers des moteurs de recherche
  • ascreen.jpg utiisé par certains sites pour afficher une vignette de la page d'accueil du site. C'est normalement une image en 120x90 pixels.
  • favicon.ico utilisé par les navigateurs pour afficher une "icone du site" dans la barre d'adresse à côté de l'URL du site et également dans les bookmarks
  • apple-touch-icon.png utilisée par iOS en guise d'icone lorsqu'on désire mettre un raccourci vers le site sur le bureau d'un iPhone, un iPod Touch ou un iPad.

La dernière ligne permet de rediriger tous les autres accès vers le fichier index.php du site. Ca veut dire que c'est ce fichier PHP qui permettra d'afficher tous les éléments du site, que ce soit des feuilles de style, des scripts, des images, des pages HTML ou n'importe quoi d'autre. On peut ainsi simuler une arborescence virtuelle qui ne correspondrait pas à l'arborescence réelle.

Le programme index.php va donc être appelé et décortiquer l'URL. Pour ce faire voici son contenu:

	// calculating route
	$elem = getRoute();
	// var_dump($elem);
	if ((! is_array($elem)) || (count($elem)<1) || ("" == $elem[0]) || ("index.html" == $elem[0]) || ("index.htm" == $elem[0]) || ("index.php" == $elem[0]))
	{
		// display home page
	}
	else if (("setup.php" == $elem[0]) && file_exists(dirname(__FILE__)."/setup.php")) {
		require_once(dirname(__FILE__)."/setup.php");
	}
	else if (file_exists(dirname(__FILE__)."/plugin/".$elem[0]."/index.php"))
	{
		require_once(dirname(__FILE__)."/plugin/".$elem[0]."/index.php");
	}
	else
	{
		page404WithError("unknow module : 
".$_SERVER["PHP_SELF"]."
".basename($_SERVER["PHP_SELF"])."
".dirname($_SERVER["PHP_SELF"])."
".$_SERVER["REQUEST_URI"]."
".print_r($elem,true));
	}

On commence par générer un tableau à partir de la décomposition de l'URL. C'est la fonction getRoute() qui s'en charge.

Une fois les morceaux de l'URL récupérés, on en traite les différents accès, soit en dur, soit en appelant l'extension censée le gérer. Les extensions sont stockées dans /plugin/NomExtension/ et accessibles par http(s)://MonDomaine/MonExtension/LeResteDeLURL
Dans ce système, tout est géré sous forme extension. Ce fichier index.php et les fonctions appelées composent le noyau du site.

Lorsque rien n'est spécifié, le script doit traiter l'affichage de la page d'accueil, c'est le seul morceau en dur du programme. Dans cet exemple j'ai juste laissé un commentaire, mais en pratique il y a un affichage de page web, l'appel d'un module ou autre chose censé représenter la page d'accueil du site.

La seconde condition permet d'appeler le programme d'installation ou de configuration du site. Celui-ci se nomme setup.php et n'est appelé que s'il existe sur le serveur.

La troisième condition traite le routage en tant que tel : en fonction du premier élément du tableau, on sait quel module ou extension appeler. Ce que l'on fait s'il existe.

Enfin, si aucun chemin valide n'a été trouvé, on envoie une page d'erreur.

Reste donc à voir les deux fonctions utilisées. Je vais commencer par page404WithError().

	// display 404 "errorTextToDisplay" HTML page to the user and stop the program
	function page404WithError($errorTextToDisplay = "")
	{
		if (strlen(trim($errorTextToDisplay)) < 1) {
			$errorTextToDisplay = "Error 404 - Page not found.";
		}
		header('HTTP/1.0 404 Not Found', true, 404);
		print("<html><head><meta charset=\"utf-8\"><meta name=\"robots\" content=\"nofollow,noindex,noarchive,nosnippet,noodp,noydir\" /></head><body><p>".nl2br($errorTextToDisplay)."</p><p><a href=\"#\" onclick=\"history.go(-1);\">Go back</a></p></body></html>");
		exit;
	}

Le rôle de cette fonction est uniquement de retourner une page d'erreur de type "404 - Page not found". Je l'ai faite sans fioriture, elle n'est censée être affichée qu'aux pirates ou lors d'erreurs de saisie dans une URL. La fonction envoi le code HTTP correspondant pour signaler aux navigateurs, serveurs et moteurs de recherche que la page n'existe pas, puis la partie utilisateur (HTML) en reprenant le message passé en paramètre ou le message classique, en dur, lorsqu'il n'y a pas de paramètre.

Pour finir sur le noyau du système de routage, voici la fonction qui découpe l'URL et la retourne sous forme de tableau.

	// get route for the actual URL (to know what program to use)
	function getRoute() {
		$root_folder = dirname($_SERVER["PHP_SELF"]);
		if ("/" == $root_folder)
		{
			$root_folder = "";
		}
		$rel_folder = substr($_SERVER["REQUEST_URI"],strlen($root_folder)+1); // extract first "/"
		if (false !== ($pos = strpos($rel_folder, "?"))) {
			$rel_folder = substr($rel_folder, 0, $pos);
		}
		$elem = explode("/", $rel_folder);
		if (is_array($elem)) {
			$folders = array();
			reset($elem);
			while (list($key,$value) = each($elem))
			{
				$value = trim($value);
				if ((strlen($value)>0) && ("." != substr($value,0,1))) {
					$folders[] = trim($value);
				}
			}
			return $folders;
		}
		else {
			return array("");
		}
	}

Le découpage se fait par rapport aux slashes présents dans l'URL et s'arrête en fin d'adresse ou lorsqu'on trouve le point d'interrogation qui permet de définir les paramètres passés en GET.

Chaque partie de l'URL située entre deux slashes est considérée comme un élément de la route et stocké dans le tableau sauf s'il y a un point devant afin d'éviter les changements d'arborescence ou les tentatives d'accès à des fichiers dont le nom commencerait par un point (.htaccess ou .htpasswd par exemple).

Maintenant que vous avez la structure et le fonctionnement du routage, je vous propose de voir comment est géré un téléchargement de fichier PDF dans Bloginus. Il en est de même pour les images et autres fichiers pouvant soit être en dur dans le site, téléchargés par les utilisateurs, dans le thème choisi, dans son thème parent ou dans le thème ancêtre...

Pour télécharger un PDF dans cet exemple on utiliserait l'URL http(s)://MonDomaine/PDF/MonfichierATelecharger.pdf

Cela va appeler le fichier index.php de l'extension PDF qui se trouve dans le dossier /plugin/PDF/

Ce programme va vérifier le nombre de paramètres passés dans la route. On s'attend à en avoir 2 : le nom de l'extension, puis le nom du fichier à télécharger. C'est une version simplifiée, on peut bien entendu gérer une arborescence suplémentaire à ce niveau.

Le programme vérifie ensuite l'existence du fichier demandé puis le retourne à l'utilisateur ou émet à son tour une erreur 404.

La vérification se fait dans l'arborescence du thème en cours, du thème par défaut, puis sur celle du site. J'ai laissé ici $elem[0], mais j'aurais pu le remplacer par "PDF" dans les chemins d'accès. C'est juste pour limiter la maintenance lorsque je copie/colle ce script pour gérer un CSS, un JPG/PNG/GIF ou n'importe quel autre type de fichier autorisé sur le site.

En voici le code source :

<?php
	if ((2 == count($elem)) && ("PDF" == $elem[0]))
	{
		$theme = config_getvar("theme","_default");
		if (file_exists(dirname(__FILE__)."/../../theme/".$theme."/".$elem[0]."/".$elem[1]))
		{
			header("content-type: application/pdf");
			readfile(dirname(__FILE__)."/../../theme/".$theme."/".$elem[0]."/".$elem[1]);
			exit;
		}
		else if (file_exists(dirname(__FILE__)."/../../theme/_default/".$elem[0]."/".$elem[1]))
		{
			header("content-type: application/pdf");
			readfile(dirname(__FILE__)."/../../theme/_default/".$elem[0]."/".$elem[1]);
			exit;
		}
		else if (file_exists(dirname(__FILE__)."/../../".$elem[0]."/".$elem[1]))
		{
			header("content-type: application/pdf");
			readfile(dirname(__FILE__)."/../../".$elem[0]."/".$elem[1]);
			exit;
		}
	}
	page404WithError();
?>

Ces quelques lignes de code sont la version simplifiée d'un système de routes à partir d'une URL.

De nombreux framework et CMS le gèrent en plus tordu, avec plus de lignes de code ou plus de fichiers. Si vous n'avez pas besoin d'une usine à gaz qui gère un moteur d'avion pour afficher quelques pages, privilégiez la simplicité. Au moins, avec ces sources, vous n'aurez pas de soucis de maintenance ni de mises à jour régulières pour installer les patchs liés à des bogues ou trous de sécurité sur des fonctionnalités que vous n'utilisez pas !

Bien entendu vous pouvez continuer à utiliser le passage de paramètres en GET ou POST. PHP et Apache les gèrent très bien, même si on virtualise le chemin d'accès aux fichier. Les tableaux $_GET et $_POST restent accessibles partout dans vos programmes web.