DOMDocument et UTF-8, un problème de charset en php

Posté le dimanche 18 octobre 2009 par Matt

Aujourd’hui, nous allons voir comment manipuler des éléments du DOM avec php.
Nous prendrons pour exemple une fonction très pratique mais pourtant apparamment bien difficile à trouver : commenter ajouter un attribut spécifique à des éléments HTML.
Ceci peut-être utile par exemple pour ajouter un attribut rel= »nofollow » à des liens pour signaler aux moteurs de recherche qu’ils n’ont pas à les suivre, tout en laissant ces liens accessibles pour vos utilisateurs. D’un point de vue référencement, ça peut être bien pratique puisque cela vous évite de voir votre PageRank être dispersé vers tous les liens présents sur vos pages.

Pour arriver à ce but, nous allons rencontrer plusieurs problèmes assez vicieux. Commençons par une fonction aboutie, pour faire gagner du temps au plus vifs d’entre vous :

function addAttribute($context, $tag, $attribute, $value)
{
	$initialEncoding = mb_detect_encoding($context);
	if( $initialEncoding != 'UTF-8' ){
		$context = utf8_encode($context);
	}

	$doc = new DOMDocument("4.01", "utf-8");

	$contentPrefix = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><title>required meta for utf-8 handling!</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>';
	$contentSuffix = '</body></html>';

	$doc->loadHTML($contentPrefix . $context . $contentSuffix);

	$elements = $doc->getElementsByTagName($tag);

	if(!is_array($value)){
		$value = array($value);
	}

	foreach($elements as $element)
	{

		foreach($value as $currentValue)
		{
			$alreadySet = false;

			if($element->hasAttribute($attribute))
			{

				$attributeCurrentValue = $element->getAttribute($attribute);

				$attributeCurrentValues = explode(' ', $attributeCurrentValue);

				foreach( $attributeCurrentValues as $attributeCurrentValue )
				{
					if($attributeCurrentValue == $currentValue){
						$alreadySet = true;
					}
				}
				if(!$alreadySet){
					$element->setAttribute($attribute, implode(' ', $attributeCurrentValues) . ' ' . $currentValue);
				}
			} else {
				$element->setAttribute($attribute, $currentValue);
			}
		}
	}

	$output = mb_substr($doc->saveHTML(), 236, -16);

	if( $initialEncoding != 'UTF-8' ){
		mb_convert_encoding($output, $initialEncoding, 'UTF-8');
	}

	return $output;
}

Explications

$initialEncoding = mb_detect_encoding($context);
	if( $initialEncoding != 'UTF-8' ){
		$context = utf8_encode($context);
	}

On commence par détecter le format d’encodage actuellement utilisé dans le contexte fourni.
On le stocke pour pouvoir retourner la version modifiée dans le même format, et on le converti en UTF-8.
Pourquoi UTF-8 ? Ce format a l’avantage (énorme) de gérer tous les caractères, y compris les caractères accentués ou spéciaux de différents langages.

	$doc = new DOMDocument("4.01", "utf-8");

On crée ensuite un nouvel objet DOM, au constructeur duquel on passe deux paramètres : la version du document que l’on va utiliser (typiquement « 1.0″ pour du XML et « 4.01″ pour du HTML), et le jeu de caractères de ce document (charset).

	$contentPrefix = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"><html><head><title>required meta for utf-8 handling!</title><meta http-equiv="Content-Type" content="text/html; charset=utf-8"></head><body>';
	$contentSuffix = '</body></html>';

	$doc->loadHTML($contentPrefix . $context . $contentSuffix);

On va ensuite ajouter un header au contexte passé. En effet, même si on a défini les valeurs attendues pour le document fourni, celles-ci seront effacées par le document si ces headers ne sont pas fournis. Et je peux vous assurer que lorsqu’on ne sait pas ça, il y a de quoi s’arracher les cheveux !! C’est sans doute le point le plus vicieux de la manipulation.
On peut ensuite charger le contenu dans cet objet DOM.

	if(!is_array($value)){
		$value = array($value);
	}

Cette fonction prévoit de pouvoir ajouter plusieurs valeurs à un attribut donné. Si l’argument passé à la fonction est une chaine de caractères, on la transforme donc en tableau.

Je ne m’attarderai pas sur le reste de la fonction en elle même, qui est assez explicite.
Notez simplement que l’on préserve les valeurs déjà existantes en les stockant dans un tableau, et que l’on vérifie avant d’ajouter la valeur de l’attribut si elle n’existe pas déjà.

	$output = mb_substr($doc->saveHTML(), 236, -16);

On va ensuite sauvegarder le résultat obtenu, et retirer les préfixe et suffixe que l’on avait ajouté grâce à une fonction gérant les caractères multibytes. En effet, un substr classique ne fonctionnerait pas convenablement avec certains caractères, puisque l’on est ici en UTF-8 qui utilise plusieurs octets pour stocker certains d’entre eux.

	if( $initialEncoding != 'UTF-8' ){
		mb_convert_encoding($output, $initialEncoding, 'UTF-8');
	}

	return $output;

Il est temps de remettre le résultat obtenu dans son jeu de caractère d’origine et de retourner le résultat.

Et… Voila!

Laisser un Commentaire

Haut de page
Clicky Web Analytics