Par Alexandre Alapetite le 2007-11-19 ; mise à jour 2015-01-06

Redirection HTTP gérée par une page d’erreur 404 personnalisée

La gestion des changements d’adresse des pages d’un site Web est quelque chose d’important dans la vie d’un site.

Pour cela, il existe par exemple un système de redirection HTTP sous Apache, très utilisé, mais celui-ci présente plusieurs inconvénients comme le fait de ne pas être portable sous d’autres serveurs Web comme IIS, d’alourdir rapidement un site, et d’être difficilement testable ou déplaçable sur un serveur avec une adresse ou une racine différente (par exemple un serveur de développement).

Dans le cas du serveur Microsoft IIS, la gestion des redirections peut être fastidieuse, et ne pas être forcément disponible dans le cas d’un serveur mutualisé.

En réponse à cela, je propose sur cette page un système de gestion des redirections basé sur les pages d’erreur 404 personnalisées. C’est-à-dire que le système de redirection n’est activé que lorsqu’une ressource n’a pas été trouvée, ce qui supporte une montée en charge supérieure. Le système est compatible avec les serveurs Web Apache, Microsoft IIS, et d’autres. Une implémentation est déjà disponible en PHP, et en ASP.NET. Enfin, le portage dans un autre langage est simple (JSP est prévu).

Télécharger le module pour PHP

Télécharger le module pour ASP.NET

English

Sommaire

Quitter

Concept

Lorsqu’une page n’est pas trouvée, plutôt que d’afficher tout de suite une page d’erreur 404, un script va chercher dans une liste de règles si une redirection est disponible. Si une telle règle est disponible, une redirection HTTP sera envoyée au client ; sinon, une page d’erreur 404 sera affichée.

[Diagramme]

Avantages

Légèreté et montée en charge :
Comme les règles de redirection ne sont lues que lorsqu’une ressource est introuvable, et que ces règles sont stockées dans une arborescence permettant de réduire le nombre de règles à lire, le système est léger et rapide pour le serveur, et accepte une bonne montée en charge (nombre de requêtes, et nombre de règles).
Portabilité et tests :
Le système est compatible avec plusieurs types de serveurs différents (Apache, IIS, …).
De plus, comme des règles avec des adresses non-absolues peuvent être utilisées, cela permet de tester complètement les redirections sur un serveur de développement avant de les déployer sur le serveur de production.
En particulier, cela permet de garder les mêmes règles, même si un site est déplacé de la racine d’un serveur vers un sous-dossier comme http://example.net/~monSiteWeb/, ou vice versa.
Compatible avec les environnements mutualisés :
La plupart des hébergements mutualisés (qu’ils soient sous Apache ou IIS) proposent une personnalisation des pages d’erreurs 404, et c’est la seule chose nécessaire pour faire marcher ce système.
Multi-langage :
Le système de redirection proposé est déjà disponible pour PHP et ASP.NET. Il est de plus facilement implémentable dans d’autres langages comme ASP classique, JSP ou autres CGI.
Sommaire

Structure

Arborescence

En admettant que vous placiez le script de redirection dans un sous-dossier /erreurs/ de votre site Web (recommandé), celui-ci va par défaut chercher les règles de redirection dans /erreurs/404//404.txt, c’est à dire dans les fichiers 404.txt situés dans le dossier /erreurs/404/ ou ses sous-répertoires, comme l’illustre l’arborescence suivante :

Les noms des dossiers et sous-dossiers doivent correspondre à des dossiers du site actuel ou à des anciens dossiers à rediriger.

La profondeur et la largeur de l’arborescence ne sont pas limitées.

Faire des sous-répertoires n’est pas obligatoire et toutes les règles de redirection peuvent être dans /erreurs/404/404.txt. Mais cette possibilité d’arborescence permet d’éviter d’avoir un seul gros fichier 404.txt, permet de mieux structurer des règles ensemble, et permet d’augmenter les performances en évitant d’avoir trop de règles à lire.
Un sous-répertoire est typiquement créé lorsqu’il y a plusieurs règles de redirection le concernant.

Dans le nom des dossiers, les caractères spéciaux non-acceptables dans une URL (en gros tout ce qui n’est pas a-z_.0-9-) doivent être %-encodés. En particulier, une espace sera encodée %20 (voir exemple ci-dessus : dossier%20avec%20espaces/).

Attention : le script de redirection est configuré par défaut pour être dans un sous-dossier (niveau 1) du site Web, tel /erreurs/ comme dans l’exemple ci-dessus. Ceci est configurable dans la constante distanceToRoot en début de script.


Priorités

Lorsqu’un sous-dossier existe (ce qui n’est pas obligatoire), toutes les règles de redirection le concernant doivent se trouver dans son fichier 404.txt.
Au final, pour une requête de redirection donnée, un seul fichier 404.txt sera lu : celui le plus précis possible dans l’arborescence de /erreurs/404//.


Exemple

La syntaxe est expliquée juste après. Considérez déjà la règle de redirection suivante :

404.txt

permanent	/ancien-dossier/sous-dossier/fichier\.html	/nouveau-dossier/fichier.html

Cette règle pourra être placée dans le fichier /erreurs/404/404.txt, ou bien /erreurs/404/ancien-dossier/404.txt, ou encore /erreurs/404/ancien-dossier/sous-dossier/404.txt. Mais si le dossier /erreurs/404/ancien-dossier/ existe, la règle ne sera pas lue si elle se trouve dans le dossier parent /erreurs/404/404.txt.


Personnalisation du dossier contenant les règles de redirection

Notez que par défaut, et contrairement aux fichiers .htaccess de Apache, les fichiers 404.txt ne sont pas mélangés au reste du site (ils sont dans /erreurs/404/), et cela pour éviter d’avoir à conserver d’anciens dossiers (ou d’avoir un gros fichier à la racine). Néanmoins, vous pouvez personnaliser l’endroit où les fichiers 404.txt sont cherchés grâce à la variable path404 en début de script.

Sommaire

Syntaxe

La syntaxe d’une règle de redirection est la suivante (une règle par ligne, et les trois parties de la règle sont séparées par des tabulations ou des espaces) :

404.txt

<type de redirection>	<ancienne adresse en expression régulière>	<nouvelle adresse>

Différences avec Apache

La syntaxe utilisée pour les règles de redirection est proche de celle utilisée par l’instruction RedirectMatch de Apache ; voici les différences majeures :

  1. Les fichiers ou répertoires existants ne sont pas redirigés, car les règles de redirection ne sont lues que lorsqu’une ressource n’a pas été trouvée.
  2. Toutes les règles sont du style RedirectMatch (c’est-à-dire à base d’expressions régulières), et non simplement Redirect.
  3. Les règles doivent être écrites de manière extensive, c’est-à-dire qu’elles doivent décrire du début à la fin le nom du document à rediriger.
  4. Les adresses redirigées ne sont pas sensibles à la casse (par défaut).
  5. Si un site Web n’est pas à la racine de son serveur Web, comme http://example.net/~monSiteWeb/, les redirections peuvent être notées de manière plus pratique par rapport à la racine du site Web plutôt que la racine du serveur.
  6. L’ancienne adresse à rediriger peut être donnée relativement, en fonction de l’adresse du fichier 404.txt (comme par exemple abréger /ancien-dossier/fichier\.html en fichier\.html si l’instruction se trouve dans /erreurs/404/ancien-dossier/404.txt).
  7. Les adresses de redirection (nouvelles) peuvent être relatives à la racine du site (/nouveau-dossier/fichier.html), ou même relatives à l’ancien emplacement (nouveau-fichier.html), au lieu d’être une adresse complète comme Apache le demande, de la forme http://example.net/nouveau-dossier/fichier.html.

Types de redirection

Les différents types de redirection pris en charge sont :

permanent
Pour une redirection permanente (HTTP/1.1 301 Moved Permanently)
temp ou found
Pour une redirection temporaire (HTTP/1.1 302 Found)
seeother
(Rare) Pour une redirection temporaire où le document (en particulier si dynamique) a changé de type ou de fonctionnement (HTTP/1.1 303 See Other)
temporary
(Rare) Pour une redirection temporaire où le document (en particulier si dynamique) a changé de type ou de fonctionnement (HTTP/1.1 307 Temporary Redirect)
gone
Pour indiquer qu’une ressource a été supprimée et n’a pas de redirection (HTTP/1.1 410 Gone)
Dans ce cas, il n’y a pas de troisième paramètre indiquant la nouvelle adresse.

Syntaxe des anciennes adresses à rediriger

Les anciennes adresses sont écrites sous formes d’expressions régulières. Si vous n’êtes pas familier avec cette notation, mettez simplement l’ancienne adresse en prenant soin de traiter certains caractères spéciaux comme suit :

Les anciennes adresses sont entendues depuis la racine du site Web ; c’est-à-dire qu’elles ne contiennent pas http:// ou le nom du serveur.
Par exemple, pour rediriger http://example.net/ancien-dossier/, l’ancienne adresse à écrire sera /ancien-dossier/ seulement.

En particulier, si un site Web n’est pas à la racine de son serveur Web, comme http://example.net/~monSiteWeb/, les redirections sont entendues par rapport à la racine du site Web et non depuis la racine du serveur (ceci est configurable dans la constante distanceToRoot en début de script).
Par exemple, pour rediriger http://example.net/~monSiteWeb/ancien-dossier/, l’ancienne adresse à écrire sera /ancien-dossier/ seulement.

L’ancienne adresse peut optionnellement être abrégée en fonction de l’adresse du fichier 404.txt (comme par exemple abréger /ancien-dossier/fichier\.html en fichier\.html si l’instruction se trouve dans /erreurs/404/ancien-dossier/404.txt). Cependant, la syntaxe complète relative à la racine est recommandée pour sa flexibilité et sa robustesse.


Syntaxe des nouvelles adresses

Les nouvelles adresses ne sont pas des expressions régulières ; il ne faut donc pas utiliser d’anti-slash.

On peut utiliser $1, $2 etc. pour référencer une capture () provenant de l’expression régulière de l’ancienne adresse.

Les adresses de redirection peuvent être absolues (http://example.net/nouveau-dossier/fichier.html), ou aussi relatives à la racine du site (/nouveau-dossier/fichier.html) [recommandé], ou relatives à l’ancien emplacement (nouveau-fichier.html)

Sommaire

Exemples

/erreurs/404/404.txt

#Redirige un fichier précis
permanent	/ancien-dossier/sous-dossier/fichier\.html	/nouveau-dossier/fichier.html
#Même exemple, avec un nom de répertoire et de fichier contenant une espace (codée %20)
permanent	/ancien%20dossier/mon%20fichier\.html	/nouveau%20dossier/mon%20fichier.html

#Redirige un site complet
permanent	/(.*)	http://nouveau-site.fr/$1
#Redirige uniquement la racine du site
permanent	/	/dossier/

#Redirige un répertoire et tous ses sous-dossiers et fichiers
permanent	/ancien-dossier/sous-dossier2/(.*)	/nouveau-dossier2/$1
#Même effet, en permettant aussi la redirection lorsque le / final est omis
permanent	/ancien-dossier/sous-dossier2(.*)	/nouveau-dossier2$1
#Redirige un répertoire mais pas ses sous-dossiers ni fichiers
permanent	/ancien-dossier/sous-dossier2/	/nouveau-dossier2/
#Redirige un répertoire et tous ses fichiers mais pas ses sous-dossiers
permanent	/ancien-dossier/sous-dossier2/([^/\\]*)	/nouveau-dossier2/$1

#Redirige en masse des fichiers correspondant à un motif donné,
#et réutilise une partie de leur nom (avec $n) pour former la nouvelle adresse
permanent	/images/image([0-9]+)\.(gif|jpg)	/images/image$1.png
#Même effet, syntaxe de destination allégée
permanent	/images/image([0-9]+)\.(gif|jpg)	image$1.png

#Redirection temporaire
#(l’adresse d’origine doit continuer à être celle utilisée et référencée)
temp	/raccourci	/longue-adresse/plus-compliquee/

#Indique qu’une ressource n’est plus disponible
gone	/dossier/ancien\.pdf
Sommaire

Installation

Options

Un certain nombre d’options sont paramétrables sous forme de constantes en début de script :

distanceToRoot = 1
Nombre de niveaux depuis la racine du site Web jusqu’à ce script. Utile pour la gestion automatique des sites qui sont dans un sous-dossier du serveur Web, tel http://example.net/~monSiteWeb/
Mettre à -1 pour désactiver cette fonctionnalité
Par exemple, mettre à 0 si ce script se trouve dans http://example.com/ ou http://example.net/~monSiteWeb/
Par exemple, mettre à 1 si ce script se trouve dans http://example.com/erreurs/ ou http://example.net/~monSiteWeb/erreurs/
Par exemple, mettre à 2 si ce script se trouve dans http://example.com/dossier/erreurs/ ou http://example.net/~monSiteWeb/dossier/erreurs/
path404 = "./404/"
Racine du répertoire contenant les redirections (fichiers 404.txt)
customRedirect = "/"
Pour spécifier une redirection HTML optionnelle à suivre après l’affichage du message d’erreur
La valeur par défaut "/" redirige à la racine du site Web (qui peut être différente de la racine du serveur Web).
customRedirectTimeOut = 5
Délais en secondes avant de suivre la redirection optionnelle ci-dessus
defaultNewServer = ""
Change le serveur pour les redirections utilisant des adresses relatives. Par exemple : "http://example.net:80"
allowASPmode = true
Permet aux anciennes adresses d’être passées en paramètre (pour ASP.NET ou pour recevoir des erreurs 404 d’un autre serveur)

Serveur Apache

Sous Apache, l’ajout de cette page d’erreur personnalisée se fait via la directive ErrorDocument, qui peut en particulier être utilisée dans le fichier de configuration général ./conf/httpd.conf dans le répertoire d’installation de Apache, ou dans un fichier /.htaccess à la racine du site Web concerné, comme illustré ci-dessous :

/.htaccess

ErrorDocument 404 /erreurs/redirection-404.php

Logs Apache

Les redirections apparaissent dans les logs d’accès Apache, (codes 301, 302, 410…), et dans les logs d’erreur :

access.log

127.0.0.1 - - [05/Jan/2008:18:50:39 +0100] "GET /ancien-dossier/ HTTP/1.1" 301 651 "-" "Mozilla"
127.0.0.1 - - [05/Jan/2008:18:50:40 +0100] "GET /nouveau-dossier/ HTTP/1.1" 200 8397 "-" "Mozilla"
error.log

[Sat Jan 05 18:50:39 2008] [error] [client 127.0.0.1] File does not exist: E:/www/ancien-dossier

Déléguer la redirection à un autre serveur

(Facultatif et rare): Dans le cas où vous ne voulez ou ne pouvez pas traiter les redirections sur un serveur donné, la méthode suivante permet de déléguer cette tâche à un autre serveur, après avoir activé cette possibilité dans la variable allowASPmode, au début du script de redirection.

/.htaccess

<IfModule mod_rewrite.c>
	RewriteEngine On
	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d
	RewriteRule ^(.*)$ http://example.net/erreurs/redirection-404.php?aspxerrorpath=$1 [R=301,L]
</IfModule>
Sommaire

Serveur Microsoft IIS

Dans IIS, l’ajout de cette page d’erreur personnalisée se fait depuis sa console d’administration (accessible entre autres depuis les outils d’administration, “Gestion de l’ordinateur”, ou “Gestionnaire des services Internet”).

La configuration se fait ensuite dans les propriétés du site Web, dans l’onglet “Messages d’erreur personnalisés” (IIS 5 & 6) ou à partir de l’icone “Pages d’erreurs” (IIS 7). Après avoir sélectionné la ligne du code d’erreur 404, cliquer sur le bouton ou le lien “Modifier”, puis entrer une adresse de type URL indiquant le chemin du script redirection-404 à partir de la racine du site Web comme par exemple /erreurs/redirection-404.aspx ou /erreurs/redirection-404.php.

[IIS erreur 404 personnalisées]

Dans IIS 7 (voir capture d’écran ci-dessus), le lien “Modifier les paramètres de fonction…” (à droite) permet entre autres de spécifier si l’on souhaite utiliser cette page d’erreur personnalisée y compris lorsqu’on se connecte en local (localhost), ce qui est désactivé par défaut.


Configuration IIS manuelle

Dans IIS 6 et plus récent, la configuration en mode graphique ci-dessus peut aussi s’effectuer manuellement, avec la directive httpErrors comme suit :

/Web.config

<configuration>
	<system.webServer>
		<httpErrors errorMode="Custom"><!-- Pour les fichiers non-managés -->
			<remove statusCode="404" subStatusCode="-1" />
			<error statusCode="404" path="/erreurs/redirection-404.aspx" responseMode="ExecuteURL" />
		</httpErrors>
	</system.webServer>
	…
</configuration>

Notez que pour avoir le droit de mettre cette configuration au niveau local dans un Web.config à la racine de votre site, plutôt que dans le applicationHost.config global, il faut que la ligne suivante soit activée dans applicationHost.config :

%windir%\System32\inetsrv\config\applicationHost.config

	<section name="httpErrors" overrideModeDefault="Allow" />

Configuration propre aux fichiers ASP.NET

La gestion des redirections pour les fichier .aspx nécessite une configuration supplémentaire.

ASP.NET sous IIS 6 et plus vieux

Jusqu’à IIS 6, il y avait la possibilité de vérifier l’existence des fichiers .aspx avant de les envoyer au moteur ASP.NET, dans [Propriétés / Répertoire de base / Configuration de l’application / Mappages / .aspx / Modifier] (capture d’écran de IIS 5) :

[IIS vérifier fichier]
ASP.NET sous IIS 7.5 et plus récent

Si IIS 7.5+ est utilisé, ou si l’option ci-dessus ne peut pas être employée, alors une directive additionnelle customErrors doit être utilisée pour les fichiers ASP.NET comme .aspx. De plus, le script de redirection doit avoir la variable allowASPmode activée au début du script de redirection.

/Web.config

<configuration>
	…
	<system.web>
		<customErrors mode="On" redirectMode="ResponseRewrite">
			<error statusCode="404" redirect="/erreurs/redirection-404.aspx" />
		</customErrors>
	</system.web>
</configuration>

Notez que l’attribut redirectMode avec la valeur ResponseRewrite supprime la problématique redirection 302 intermédiaire par défaut, mais il n’est disponible que depuis ASP.NET 3.5 SP1 et IIS 7.5 (Windows 7).

Configuration universelle pour ASP.NET avec un module

Si vous ne pouvez pas utiliser les méthodes ci-dessus pour assurer la redirection des fichiers .aspx alors il reste la possibilité d’écrire un gestionnaire d’erreur avec un module personnalisé, qui a l’avantage de marcher avec IIS 6 et 7.0+ :

/App_Code/Redirection404Helper.cs

using System;
using System.Web;

/// <summary>Module personnalisé assistant pour les erreurs 404. 2008-10-20</summary>
/// <see cref="https://alexandre.alapetite.fr/doc-alex/redirection-404/"/>
public class Redirection404Helper : IHttpModule
{
	public void Dispose() {}

	public void Init(HttpApplication context)
	{
		context.Error += new EventHandler(On404Error);
	}

	void On404Error(object sender, EventArgs e)
	{
		HttpApplication httpApplication = sender as HttpApplication;
		if (httpApplication == null) return;
		HttpContext httpContext = httpApplication.Context;
		HttpException httpException = httpContext.Error as HttpException;
		if (httpException == null) return;
		if (httpException.GetHttpCode() == 404)
			httpContext.Server.Transfer(httpContext.Request.ApplicationPath +
				@"/erreurs/Redirection-404.aspx?aspxerrorpath=" + httpContext.Request.RawUrl);
	}
}

Placez le fichier C# ci-dessus dans le répertoire /App_Code/ de votre site Web (en prenant soin de vérifier l’adresse du script de redirection à la dernière ligne du module), puis référencez-le comme suit (peut aussi être configuré graphiquement via l’icône “Modules”, puis lien “Ajouter un module managé…”) :

/Web.config

<configuration>
	<system.webServer>
		<httpErrors errorMode="Custom"><!-- Pour les fichiers non-managés -->
			<remove statusCode="404" subStatusCode="-1" />
			<error statusCode="404" path="/erreurs/redirection-404.aspx" responseMode="ExecuteURL" />
		</httpErrors>
		<!-- Pour pouvoir avoir les sections IIS 6 et 7+ en même temps -->
		<validation validateIntegratedModeConfiguration="false" />
		<modules><!-- Pour IIS 7+ -->
			<add name="Redirection404Helper" type="Redirection404Helper" preCondition="managedHandler" />
		</modules>
	</system.webServer>
	<system.web>
		<httpModules><!-- Pour IIS 6 -->
			<add name="Redirection404Helper" type="Redirection404Helper" />
		</httpModules>
	</system.web>
</configuration>
Sommaire

Serveur Apache Tomcat

En construction…

./conf/web.xml

<error-page>
	<error-code>404</error-code>
	<location>/erreurs/redirection-404.jsp</location>
</error-page>
Sommaire

Scripts de redirection HTTP pour page d’erreur 404 personnalisée

Implémentation en PHP

redirection-404.php

<?php
/*
 Script PHP pour gérer les redirections HTTP. À utiliser en tant que page d’erreur 404 personnalisée.
 https://alexandre.alapetite.fr/doc-alex/redirection-404/
*/

//--<Constantes>--

//Nombre de niveaux depuis la racine du site Web jusqu’à ce script, pour la gestion automatique des sites qui sont dans un sous-dossier du serveur Web.
//Mettre à -1 pour désactiver cette fonctionnalité
//Par exemple, mettre à 1 si ce script se trouve dans http://example.com/erreurs/ ou http://example.net/~monSiteWeb/erreurs/
$distanceToRoot=1;

//Racine du répertoire contenant les redirections (fichiers 404.txt)
$path404=(empty($_SERVER['SCRIPT_FILENAME']) ? '404/' : dirname($_SERVER['SCRIPT_FILENAME'])).'/404/';

$customRedirect='/';	//Pour spécifier une redirection HTML optionnelle à suivre après l’affichage du message d’erreur
$customRedirectTimeOut=5;	//Délais en secondes avant de suivre la redirection optionnelle ci-dessus

$defaultNewServer='';	//Change le serveur pour les redirections utilisant des adresses relatives. Par exemple : 'http://example.net:80'

$allowASPmode=true;	//Permet aux anciennes adresses d’être passées en paramètre (pour ASP.NET ou pour recevoir des erreurs 404 d’un autre serveur

//--</Constantes>--

//Ancienne adresse
$oldUrl='';
if (!empty($_SERVER['REQUEST_URI'])) $oldUrl=substr($_SERVER['REQUEST_URI'],0,1024);	//Apache, IIS6
elseif (!empty($_SERVER['QUERY_STRING'])) $oldUrl=substr($_SERVER['QUERY_STRING'],0,1024);	//IIS5
else $oldUrl='/_unknown_';
if (($sc=strpos($oldUrl,';'))!==false) $oldUrl=trim(substr($oldUrl,++$sc));	//IIS
$oldUrlParsed=strpos($oldUrl,'://')===false ? @parse_url($oldUrl) : '';
if (empty($oldUrlParsed))
{
 $oldUrl='/_unknown_';
 $oldUrlParsed=parse_url($oldUrl);
}
if ($allowASPmode&&(!empty($oldUrlParsed['query'])))	//Spécial ASP.NET
{
 parse_str($oldUrlParsed['query'],$oldquery);
 if (!empty($oldquery['aspxerrorpath'])) $oldUrlParsed=parse_url($oldquery['aspxerrorpath']);
}
$oldPath=$oldUrlParsed['path'];
$siteRoot='';	//Pour le cas où le site Web n’est pas à la racine du serveur Web, comme http://example.net/~monSiteWeb/
if (($distanceToRoot>=0)&&(!empty($_SERVER['SCRIPT_NAME'])))
{
 $map404=$_SERVER['SCRIPT_NAME'];
 if (substr($map404,-1)!=='/') $map404=dirname($map404);
 $map404=trim($map404,'/\\');
 $dirs=explode('/',$map404);
 $nbSubLevels=count($dirs)-$distanceToRoot;
 for ($i=0;$i<$nbSubLevels;$i++) $siteRoot.='/'.$dirs[$i];
 if (!empty($siteRoot))
 {
  if (strcasecmp(substr($oldPath,0,strlen($siteRoot)),$siteRoot)===0) $oldPath=substr($oldPath,strlen($siteRoot));
  if ((!empty($customRedirect))&&($customRedirect[0]==='/')) $customRedirect=$siteRoot.$customRedirect;
 }
}

//Cherche le meilleur fichier 404.txt
$absolute='/';
$dirs=explode('/',$oldPath);	//Pas de urldecode(), donc les caractères spéciaux doivent être %-encodés : ./404/Bonjour%20Monde/404.txt
foreach ($dirs as $dir)
 if (strlen($dir)>0)
 {
  if (($dir[0]!=='.')&&is_dir($path404.$dir))
  {
   $path404.=$dir.'/';
   $absolute.=$dir.'/';
  }
  else break;
 }
$path404.='404.txt';

//Cherche dans le fichier 404.txt la première correspondance pour $oldPath
$newPath='';
$httpStatus=302;
$found=false;
if (is_file($path404)&&($handle=@fopen($path404,'r')))
{
 while (!feof($handle))
 {
  $line=trim(fgets($handle,4096));
  if ((strlen($line)<3)||($line[0]=='#')) continue;	//Commentaire ou invalide
  $map=preg_split('"\s+"',$line,4);
  if (count($map)<2) continue;	//invalide
  $mapOld=$map[1];
  if ($mapOld[0]!='/') $mapOld=$absolute.$mapOld;
  if (@preg_match('"^'.$mapOld.'$"iD',$oldPath)&&
   ((($status=$map[0])==='gone')||
    ((count($map)>2)&&
     (strlen($newPath=@preg_replace('"^'.$mapOld.'$"iD',$map[2],$oldPath))>0))))
  {
   switch ($status)
   {
    case 'permanent': $httpStatus=301; break;
    case 'found':
    case 'temp': $httpStatus=302; break;
    case 'seeother': $httpStatus=303; break;
    case 'temporary': $httpStatus=307; break;
    case 'gone': $httpStatus=410; break;
    default: continue 2;
   }
   $found=true;
   if ($httpStatus!==410)
   {
    if (!empty($siteRoot)) $newPath=$siteRoot.$newPath;
    if (!preg_match('"^(?:(?:[a-z]{3,6}:)|(?:\.\./))"i',$newPath))	//Pas de protocole URI, et pas de ../ au début
    {//Quand c’est possible et que ce n’est pas déjà le cas, transforme en une URL absolue
     if (empty($defaultNewServer)&&isset($_SERVER['HTTP_HOST']))
     {
      $defaultNewServer=(empty($_SERVER['HTTPS'])?'http':'https').'://'.$_SERVER['HTTP_HOST'];
      if (!empty($_SERVER['SERVER_PORT']))
      {
       if (empty($_SERVER['HTTPS']))
       {
        if ($_SERVER['SERVER_PORT']!='80') $defaultNewServer.=':'.$_SERVER['SERVER_PORT'];
       }
       elseif ($_SERVER['SERVER_PORT']!='443') $defaultNewServer.=':'.$_SERVER['SERVER_PORT'];
      }
      if (empty($newPath)||($newPath[0]!=='/'))	//adresse relative
       $newPath=rtrim(substr($oldPath,-1)==='/' ? $oldPath : dirname($oldPath),'/\\').'/'.$newPath;
     }
     $newPath=$defaultNewServer.$newPath;
    }
   }
   break;
  }
 }
 fclose($handle);
}

if ($found)	//Redirection si une nouvelle adresse a été trouvée
{
 if ($httpStatus===410)
 {
  header('HTTP/1.1 410 Gone');
  header('Status: 410 Gone');
  echo '<!DOCTYPE html>'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta charset="UTF-8" />'."\n",
   empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
   '<title>410 Gone</title>'."\n",
   '<meta name="robots" content="noindex,follow" />'."\n",
   '</head>'."\n",
   '<body>'."\n",
   '<h1>Gone</h1>'."\n",
   '<p>The requested resource <kbd>'.$oldPath.'</kbd> is no longer available on this server and there is no forwarding address. ',
   'Please remove all references to this resource.</p>'."\n",
   '</body>'."\n",
   '</html>'."\n";
 }
 else
 {
  if (isset($oldUrlParsed['query'])) $newPath.='?'.$oldUrlParsed['query'];
  $status=array(301=>'Moved Permanently',302=>'Found',303=>'See Other',307=>'Temporary Redirect');
  header('Location: '.$newPath);
  header('HTTP/1.1 '.$httpStatus.' '.$status[$httpStatus]);
  header('Status: '.$httpStatus.' '.$status[$httpStatus]);
  echo '<!DOCTYPE html>'."\n",
   '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
   '<head>'."\n",
   '<meta charset="UTF-8" />'."\n",
   '<meta http-equiv="Refresh" content="0; url='.$newPath.'" />'."\n",
   '<title>'.$httpStatus.' '.$status[$httpStatus].'</title>'."\n",
   '<meta name="robots" content="noindex,follow" />'."\n",
   '</head>'."\n",
   '<body>'."\n",
   '<h1>'.$status[$httpStatus].'</h1>'."\n",
   '<p>The document has moved <a href="'.$newPath.'">here</a>.</p>'."\n",
   '</body>'."\n",
   '</html>'."\n";
 }
}
else	//Message d’erreur 404
{
 header('HTTP/1.1 404 Not Found');
 header('Status: 404 Not Found');
 echo '<!DOCTYPE html>'."\n",
  '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-GB" lang="en-GB">'."\n",
  '<head>'."\n",
  '<meta charset="UTF-8" />'."\n",
  empty($customRedirect) ? '' : '<meta http-equiv="Refresh" content="'.$customRedirectTimeOut.'; url='.$customRedirect.'" />'."\n",
  '<title>404 Not Found</title>'."\n",
  '<meta name="robots" content="noindex,follow" />'."\n",
  '</head>'."\n",
  '<body>'."\n",
  '<h1>Not Found</h1>'."\n",
  '<p>The requested <abbr title="Uniform Resource Locator">URL</abbr> <kbd>'.$oldPath.'</kbd> was not found on this server.</p>'."\n",
  '</body>'."\n",
  '</html>'."\n";
}
?>

Historique

1.10 2020-08-09
Correction d’un bogue avec continue dans PHP7.3+
1.9 2015-01-06
Correction d’un bogue pour Gone
1.8 2011-09-05
Meilleur support pour HTTPS et les ports spéciaux
HTML5
Suppression d’un avertissement avec certaines adresses invalides
1.7 2010-01-31
Correction d’un bogue pour Gone
1.6 2009-06-22
Gestion des erreurs de formats de l’adresse demandée
1.5 2008-10-19
Non-sensible à la casse pour certaines détections
1.4 2008-10-04
Ajout de la redirection des fichiers ASP.NET
1.3 2008-09-29
Ajout de la gestion automatique des sites qui ne sont pas à la racine de leur serveur
1.1 2008-01-27
Première distribution publique
1.0 2007-11-19
Première version interne
Sommaire

Implémentation en ASP.NET

Redirection-404.aspx

<%--
 Script ASP.NET pour gérer les redirections HTTP. À utiliser en tant que page d’erreur 404 personnalisée.
 https://alexandre.alapetite.fr/doc-alex/redirection-404/
--%>
<%@ Page Language="C#" %>
<%@ Import Namespace="System.IO" %>
<script runat="server">
 protected void Page_Load(object sender, EventArgs e)
 {
  #region --Constantes--
  
 	//Nombre de niveaux depuis la racine du site Web jusqu’à ce script, pour la gestion automatique des sites qui sont dans un sous-dossier du serveur Web.
  //Mettre à -1 pour désactiver cette fonctionnalité
  //Par exemple, mettre à 1 si ce script se trouve dans http://example.com/erreurs/ ou http://example.net/~monSiteWeb/erreurs/
  int distanceToRoot = 1;
  
 	//Racine du répertoire contenant les redirections (fichiers 404.txt)
  string path = Path.GetDirectoryName(Server.MapPath(Request.CurrentExecutionFilePath)) + @"/404/";
  
  string customRedirect = @"/";	//Pour spécifier une redirection HTML optionnelle à suivre après l’affichage du message d’erreur
  int customRedirectTimeOut = 5;	//Délais en secondes avant de suivre la redirection optionnelle ci-dessus
  
  string defaultNewServer = @"";	//Change le serveur pour les redirections utilisant des adresses relatives. Par exemple : 'http://example.net:80'
  
  bool allowASPmode = true;	//Permet aux anciennes adresses d’être passées en paramètre (pour ASP.NET ou pour recevoir des erreurs 404 d’un autre serveur
  
  #endregion

  #region Ancienne adresse
  string oldUrl = "http://localhost" + Request.RawUrl;
  int sc = oldUrl.IndexOf("404;");
  if (sc >= 0) oldUrl = oldUrl.Substring(sc + 4).Trim();	//IIS
  Uri oldUrlParsed = null;
  try
  {
   oldUrlParsed = new Uri(oldUrl);
   if (allowASPmode && (!String.IsNullOrEmpty(oldUrlParsed.Query)))	//Spécial ASP.NET
   {
    NameValueCollection oldquery = HttpUtility.ParseQueryString(oldUrlParsed.Query);
    if (!String.IsNullOrEmpty(oldquery["aspxerrorpath"])) oldUrlParsed = new Uri("http://localhost" + oldquery["aspxerrorpath"]);
   }
  }
  catch
  {
   oldUrlParsed = new Uri(@"http://localhost/_unknown_");
  }
  string oldPath = oldUrlParsed.AbsolutePath;
  string siteRoot = "";	//Pour le cas où le site Web n’est pas à la racine du serveur Web, comme http://example.net/~monSiteWeb/
  string[] dirs;
  if (distanceToRoot >= 0)
  {
   string map404 = Request.CurrentExecutionFilePath;
   if (map404[map404.Length - 1] != '/') map404 = Path.GetDirectoryName(map404);
   map404 = map404.Trim(new char[] { '/', '\\' }).Replace('\\', '/');
   dirs = map404.Split('/');
   int nbSubLevels = dirs.Length - distanceToRoot;
   for (int i = 0; i < nbSubLevels; i++) siteRoot += '/' + dirs[i];
   if (!String.IsNullOrEmpty(siteRoot))
   {
    if (oldPath.StartsWith(siteRoot, StringComparison.InvariantCultureIgnoreCase)) oldPath = oldPath.Substring(siteRoot.Length);
    if ((!String.IsNullOrEmpty(customRedirect)) && (customRedirect[0] == '/')) customRedirect = siteRoot + customRedirect;
   }
  }
  #endregion

  #region Cherche le meilleur fichier 404.txt
  string absolute = "/";
  dirs = oldPath.Split('/');	//Pas de Server.UrlDecode(), donc les caractères spéciaux doivent être %-encodés : ./404/Bonjour%20Monde/404.txt
  foreach (string dir in dirs)
   if (dir.Length > 0)
   {
    if ((dir[0] != '.') && Directory.Exists(path + dir))
    {
     path += dir + '/';
     absolute += dir + '/';
    }
    else break;
   }
  path += "404.txt";
  #endregion

  #region Cherche dans le fichier 404.txt la première correspondance pour oldPath
  string newPath = "";
  int httpStatus = 302;
  bool found = false;
  FileInfo fileInfo = new FileInfo(path);
  StreamReader streamReader = null;
  try
  {
   if (fileInfo.Exists && ((streamReader = fileInfo.OpenText()) != null))
   {
    string line;
    while ((line = streamReader.ReadLine()) != null)
    {
     if (line.Length > 4096) line = line.Substring(0, 4096);
     line = line.Trim();
     if ((line.Length < 3) || (line[0] == '#')) continue;	//Commentaire ou invalide
     string[] map = Regex.Split(line, @"\s+");
     if (map.Length < 2) continue;	//invalide
     string status = map[0];
     string mapOld = map[1];
     if (mapOld[0] != '/') mapOld = absolute + mapOld;
     if (Regex.Match(oldPath, "^" + mapOld + '$', RegexOptions.IgnoreCase | RegexOptions.CultureInvariant).Success &&
      ((status == "gone") ||
       ((map.Length > 2) && ((newPath = Regex.Replace(oldPath, "^" + mapOld + '$', map[2], RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)).Length > 0))
      ))
     {
      switch (status)
      {
       case "permanent": httpStatus=301; break;
       case "found":
       case "temp": httpStatus=302; break;
       case "seeother": httpStatus=303; break;
       case "temporary": httpStatus=307; break;
       case "gone": httpStatus=410; break;
       default: found = false; break;
      }
      found = true;
      if (httpStatus != 410)
      {
       if (!String.IsNullOrEmpty(siteRoot)) newPath = siteRoot + newPath;
       if (!Regex.Match(newPath, @"^(?:(?:[a-z]{3,6}:)|(?:\.\./))", RegexOptions.IgnoreCase).Success)	//Pas de protocole URI, et pas de ../ au début
       {//Quand c’est possible et que ce n’est pas déjà le cas, transforme en une URL absolue
        if (defaultNewServer.Length < 8)
        {
         defaultNewServer = "http://" + Request.ServerVariables["HTTP_HOST"];
         if (String.IsNullOrEmpty(newPath) || (newPath[0] != '/'))	//adresse relative
          newPath = (oldPath[oldPath.Length - 1] == '/' ? oldPath : Path.GetDirectoryName(oldPath).Replace('\\', '/')).TrimEnd(new char[] { '/', '\\' }) + '/' + newPath;
        }
        newPath = defaultNewServer + newPath;
       }
      }
      break;
     }
    }
   }
  }
  catch (Exception ex)
  {
   Trace.Write("Redirection-404", "Error while reading a 404.txt file", ex);
  }
  finally
  {
   if (streamReader != null) streamReader.Close();
   fileInfo = null;
  }
  #endregion

  #region Response
  if (found)	//Redirection si une nouvelle adresse a été trouvée
  {
   if (httpStatus == 410)
   {
    Response.Status = "410 Gone";
    Response.Write(@"<!DOCTYPE html>
<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en-GB"" lang=""en-GB"">
<head>
<meta charset=""UTF-8"" />
");
    if (customRedirect.Length > 0)
     Response.Write(string.Format("<meta http-equiv=\"Refresh\" content=\"{0}; url={1}\" />\n", customRedirectTimeOut, customRedirect));
    Response.Write(@"<title>410 Gone</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>Gone</h1>
<p>The requested resource <kbd>" + oldPath + @"</kbd> is no longer available on this server and there is no forwarding address.
Please remove all references to this resource.</p>
</body>
</html>
");
   }
   else
   {
    if (!String.IsNullOrEmpty(oldUrlParsed.Query)) newPath += oldUrlParsed.Query;
    Response.RedirectLocation = newPath;
    switch (httpStatus)
    {
     case 301: Response.Status = "301 Moved Permanently"; break;
     case 302: Response.Status = "302 Found"; break;
     case 303: Response.Status = "303 See Other"; break;
     case 307: Response.Status = "307 Temporary Redirect"; break;
     default: Response.Status = "302 Found"; break;
    }
    Response.Write(@"<!DOCTYPE html>
<head>
<meta charset=""UTF-8"" />
<meta http-equiv=""Refresh"" content=""0; url=" + newPath + @""" />
<title>" + Response.Status + @"</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>" + Response.StatusDescription + @"</h1>
<p>The document has moved <a href=""" + newPath + @""">here</a>.</p>
</body>
</html>
");
   }
  }
  else	//Message d’erreur 404
  {
   Response.Status = "404 Not Found";
   Response.Write(@"<!DOCTYPE html>
<html xmlns=""http://www.w3.org/1999/xhtml"" xml:lang=""en-GB"" lang=""en-GB"">
<head>
<meta charset=""UTF-8"" />
");
   if (customRedirect.Length > 0)
    Response.Write(string.Format("<meta http-equiv=\"Refresh\" content=\"{0}; url={1}\" />\n", customRedirectTimeOut, customRedirect));
   Response.Write(@"<title>404 Not Found</title>
<meta name=""robots"" content=""noindex,follow"" />
</head>
<body>
<h1>Not Found</h1>
<p>The requested <abbr title=""Uniform Resource Locator"">URL</abbr> <kbd>" + oldPath + @"</kbd> was not found on this server.</p>
</body>
</html>
");
  }
  #endregion
 }
</script>

Historique

1.9 2015-01-06
Correction d’un bogue pour Gone
1.8 2011-09-05
Meilleur support pour HTTPS et les ports spéciaux
HTML5
1.7 2010-01-31
Correction d’un bogue pour Gone
1.5 2008-10-19
Possibilité d’être utilisé en réécriture d’adresse
Non-sensible à la casse pour certaines détections
1.4 2008-10-04
Ajout de la redirection des fichiers ASP.NET
1.3 2008-09-29
Première distribution publique
Sommaire

Licence

Ce contenu est protégé par une licence Creative Commons Paternité - Partage des Conditions Initiales à l’Identique 2.0 France “BY-SA (FR)” [Creative Commons License]

Si vous utilisez et aimez ce logiciel (surtout pour un usage professionnel), merci de considérer faire un don.


Commentaires

Si vous souhaitez une réponse ou rapporter un problème avec ce script, merci de me contacter par courriel.

object : Voir les commentaires

https://alexandre.alapetite.fr

Retour