Newer
Older
<?php
/***************************************************************************\
* SPIP, Systeme de publication pour l'internet *
* *
* Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
* *
* Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
* Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
\***************************************************************************/
if (!defined("_ECRIRE_INC_VERSION")) return;
include_spip('inc/sax');
define('_REGEXP_DOCTYPE',
'/^\s*(<[?][^>]*>\s*)?<!DOCTYPE\s+(\w+)\s+(\w+)\s*([^>]*)>/');
define('_REGEXP_ID', '/^[A-Za-z_][\w_:.-]*$/');
// Document Type Compilation
class DTC {
var $macros = array();
var $elements = array();
var $peres = array();
var $attributs = array();
var $entites = array();
var $regles = array();
function analyser_doctype($data)
{
if (!preg_match(_REGEXP_DOCTYPE, $data, $r))
return array();
list(,,$topelement, $avail,$suite) = $r;
if (!preg_match('/^"([^"]*)"\s*(.*)$/', $suite, $r))
if (!preg_match("/^'([^']*)'\s*(.*)$/", $suite, $r))
list(,$rotlvl, $suite) = $r;
if (!$suite) {
$grammaire = $rotlvl;
$rotlvl = '';
} else {
if (!preg_match('/^"([^"]*)"\s*$/', $suite, $r))
if (!preg_match("/^'([^']*)'\s*$/", $suite, $r))
return array();
$grammaire = $r[1];
}
return array($topelement, $avail, $grammaire, $rotlvl);
}
// http://doc.spip.org/@charger_dtd
function charger_dtd($data)
{
$r = analyser_doctype($data);
if (!$r) return array();
list ($topelement, $avail, $grammaire, $rotlvl) = $r;
spip_log("Racine $topelement dans $grammaire ($rotlvl)");
$dtc = new DTC;
analyser_dtd($grammaire, $avail, $dtc);
return $dtc;
}
function analyser_dtd($grammaire, $avail, &$dtc)
{
esj
a validé
static $trace = array(); // pour debug
if ($avail == 'SYSTEM')
$file = $grammaire;
else
$file = sous_repertoire(_DIR_DTD);
$file .= preg_replace('/[^\w.]/','_', $grammaire);
if (@is_readable($file)) {
lire_fichier($file, $dtd);
} else {
if ($avail == 'PUBLIC') {
include_spip('inc/distant');
if ($dtd = recuperer_page($grammaire))
ecrire_fichier($file, $dtd);
}
}
if (!$dtd) {
spip_log("DTD $grammaire inaccessible");
return array();
// ejecter les commentaires, surtout quand ils contiennent du code.
// Option /s car sur plusieurs lignes parfois
$dtd = preg_replace('/<!--.*?-->/s','',$dtd);
if (preg_match_all('/<!ENTITY\s+(%?)\s*([\w;.-]+)\s+(PUBLIC|SYSTEM)?\s*"([^"]*)"\s*("([^"]*)")?\s*>/', $dtd, $r, PREG_SET_ORDER)) {
foreach($r as $m) {
list($t, $term, $nom, $type, $val, $q, $alt) = $m;
esj
a validé
if ($type AND $alt) {
// valeur par defaut de $alt obscure. A etudier.
if (strpos($alt, '/') === false)
$alt = preg_replace(',/[^/]+$,', '/', $grammaire)
esj
a validé
. ($alt ? $alt : "loose.dtd") ;
// en cas d'inclusion, l'espace de nom est le meme
analyser_dtd($alt, $type, $dtc);
}
elseif (!$term) {
$dtc->entites[$nom] = $val;
}
else {
$dtc->macros[$nom] = expanserEntite($val, $dtc->macros) ;
// memoriser la regle de production de l'element
// et dresser le tableau de ses fils potentiels
// pour traquer tres vite les balises filles illegitimes
if (preg_match_all('/<!ELEMENT\s+(\S+)\s+([^>]*)>/', $dtd, $r, PREG_SET_ORDER)) {
foreach($r as $m) {
list(,$nom, $val) = $m;
$nom = expanserEntite($nom, $dtc->macros);
$val = expanserEntite($val, $dtc->macros);
$dtc->regles[$nom]= $val;
$val = array_values(preg_split('/\W+/', $val,-1, PREG_SPLIT_NO_EMPTY));
$dtc->elements[$nom]= $val;
foreach ($val as $k) {
if (($k != 'EMPTY') AND ($k != 'ANY') AND ($k[0] != '#') AND ((!isset($dtc->peres[$k])) OR !in_array($nom, $dtc->peres[$k])))
$dtc->peres[$k][]= $nom;
}
}
// tri pour presenter les suggestions de corrections
foreach ($dtc->peres as $k => $v) {
asort($v);
$dtc->peres[$k] = $v;
}
if (preg_match_all('/<!ATTLIST\s+(\S+)\s+([^>]*)>/', $dtd, $r, PREG_SET_ORDER)) {
foreach($r as $m) {
list(,$nom, $val) = $m;
$nom = expanserEntite($nom, $dtc->macros);
if (preg_match_all("/\s*(\S+)\s+(([(][^)]*[)])|(\S+))\s+(\S+)(\s*'[^']*')?/", $val, $r2, PREG_SET_ORDER)) {
foreach($r2 as $m2) {
$v = preg_match('/^\w+$/', $m2[2]) ? $m2[2]
: ('/^' . preg_replace('/\s+/', '', $m2[2]) . '$/');
$m21 = expanserEntite($m2[1], $dtc->macros);
$m25 = expanserEntite($m2[5], $dtc->macros);
esj
a validé
$trace[$v] = 1;
$att[$m21] = array($v, $m25);
}
esj
a validé
spip_log("DTD $avail $grammaire ". strlen($dtd) . ' octets ' . count($dtc->macros) . ' macros, ' . count($dtc->elements) . ' elements, ' . count($trace) . " types diffrents d'attributs " . count($dtc->entites) . " entites");
{
if (preg_match_all('/%([.\w]+);/', $val, $r, PREG_SET_ORDER)) {
foreach($r as $m)
// il peut valoir ""
if (isset($macros[$m[1]]))
$val = str_replace($m[0], $macros[$m[1]], $val);
return trim(preg_replace('/\s+/', ' ', $val));
}
function validerElement($phraseur, $name, $attrs)
global $phraseur_xml;
if (!isset($phraseur_xml->dtc->elements[$name]))
$phraseur_xml->err[]= " <b>$name</b>"
. _L(' balise inconnue ')
// controler les filles illegitimes, mais pas le droit d'ainesse
// (i.e. l'ordre de succession indique par une virgule dans la regle:
// le cas est rare, du coup interet/temps-de-calcul tend vers 0)
// Pour XHTML 1.0 il n'y a que <html>, <head> et <table>.
// Mais a-t-on jamais mis head apres body et caption apres tr ?
// Quant a Head, c'est juste pour avoir 1 seul title et 1 seul base.
else {
$depth = $phraseur_xml->depth;
$ouvrant = $phraseur_xml->ouvrant;
if (isset($ouvrant[$depth])) {
if (preg_match('/^\s*(\w+)/', $ouvrant[$depth], $r)) {
$pere = $r[1];
if (isset($phraseur_xml->dtc->elements[$pere]))
if (!@in_array($name, $phraseur_xml->dtc->elements[$pere])) {
$bons_peres = @join ('</b>, <b>', $phraseur_xml->dtc->peres[$name]);
$phraseur_xml->err[]= " <b>$name</b>"
. _L(" n'est pas un fils de ")
. '<b>'
. $pere
. '</b>'
. (!$bons_peres ? ''
: (_L( '<p style="font-size: 80%"> mais de <b>') . $bons_peres . '</b></p>'))
}
}
if (isset($phraseur_xml->dtc->attributs[$name])) {
foreach ($phraseur_xml->dtc->attributs[$name] as $n => $v)
{ if (($v[1] == '#REQUIRED') AND (!isset($attrs[$n])))
$phraseur_xml->err[]= " <b>$n</b>"
. ' : '
. _L(" attribut obligatoire mais absent dans ")
. "<b>$name</b>"
function validerAttribut($phraseur, $name, $val, $bal)
global $phraseur_xml;
// Si la balise est inconnue, eviter d'insister
if (!isset($phraseur_xml->dtc->attributs[$bal]))
return ;
if (!isset($a[$name])) {
$bons = join(', ',array_keys($a));
if ($bons)
$bons = " title=' " .
_L('attributs connus: ') .
$bons .
"'";
$bons .= " style='font-weight: bold'";
$phraseur_xml->err[]= " <b>$name</b>"
. _L(' attribut inconnu de ')
. "<a$bons>$bal</a>"
. _L(" (survoler pour voir les corrects)")
} else{
$type = $a[$name][0];
if (!preg_match('/^\w+$/', $type))
valider_motif($phraseur, $name, $val, $bal, $type);
else if (function_exists($f = 'validerAttribut_' . $type))
$f($phraseur, $name, $val, $bal);
}
}
function validerAttribut_ID($phraseur, $name, $val, $bal)
{
global $phraseur_xml;
if (isset($phraseur_xml->ids[$val])) {
list($l,$c) = $phraseur_xml->ids[$val];
$phraseur_xml->err[]= " <p><b>$val</b>"
. _L(" valeur de l'attribut ")
. "<b>$name</b>"
. _L(' de ')
. "<b>$bal</b>"
. _L(" vu auparavant ")
. "(L$l,C$c)"
. coordonnees_erreur($phraseur);
} else {
valider_motif($phraseur, $name, $val, $bal, _REGEXP_ID);
$phraseur_xml->ids[$val] = array(xml_get_current_line_number($phraseur), xml_get_current_column_number($phraseur));
}
function validerAttribut_IDREF($phraseur, $name, $val, $bal)
{
global $phraseur_xml;
$phraseur_xml->idrefs[] = array($val, xml_get_current_line_number($phraseur), xml_get_current_column_number($phraseur));
}
function validerAttribut_IDREFS($phraseur, $name, $val, $bal)
{
global $phraseur_xml;
$phraseur_xml->idrefss[] = array($val, xml_get_current_line_number($phraseur), xml_get_current_column_number($phraseur));
}
function valider_motif($phraseur, $name, $val, $bal, $motif)
{
global $phraseur_xml;
if (!preg_match($motif, $val)) {
$phraseur_xml->err[]= " <p><b>$val</b>"
. _L(" valeur de l'attribut ")
. "<b>$name</b>"
. _L(' de ')
. "<b>$bal</b>"
. _L(" n'est pas conforme au motif</p><p>")
. "<b>" . $motif . "</b></p>"
}
}
function valider_idref(&$own, $nom, $ligne, $col)
if (!isset($own->ids[$nom]))
$own->err[]= " <p><b>$nom</b>"
. _L(" ID inconnu ")
. $ligne
. " "
. $col;
}
function inc_valider_passe2_dist(&$own)
{
if (!$own->err) {
foreach ($own->idrefs as $idref) {
list($nom, $ligne, $col) = $idref;
valider_idref($own, $nom, $ligne, $col);
}
foreach ($own->idrefss as $idref) {
list($noms, $ligne, $col) = $idref;
foreach(preg_split('/\s+/', $noms) as $nom)
valider_idref($own, $nom, $ligne, $col);
}
}
}
class ValidateurXML {
function debutElement($phraseur, $name, $attrs)
{
global $phraseur_xml;
validerElement($phraseur, $name, $attrs);
xml_debutElement($phraseur, $name, $attrs);
$depth = &$phraseur_xml->depth;
$phraseur_xml->debuts[$depth] = strlen($phraseur_xml->res);
foreach ($attrs as $k => $v) {
validerAttribut($phraseur, $k, $v, $name);
}
}
function finElement($phraseur, $name)
{
global $phraseur_xml;
$depth = &$phraseur_xml->depth;
$contenu = &$phraseur_xml->contenu;
$ouvrant = &$phraseur_xml->ouvrant;
$ouv = $ouvrant[$depth];
if ($ouv[0] != ' ')
$ouvrant[$depth] = ' ' . $ouv;
else $ouv= "";
$n = strlen($phraseur_xml->res) + strlen(trim($contenu[$depth]));
$k = $phraseur_xml->debuts[$depth];
$regle = $phraseur_xml->dtc->regles[$name];
$vide = ($regle == 'EMPTY');
// controler que les balises devant etre vides le sont (ok),
// idem pour les nons vides (approximatif, car on ignore "|")
// Pour Xhtml 1.0, cette approximation suffit
if ($vide) {
if ($n <> $k)
$phraseur_xml->err[]= " <p><b>$name</b>"
. _L(' balise non vide')
. coordonnees_erreur($phraseur);
} elseif ($n == $k) {
if (strpos($regle, '+')) $ok = false;
else {
if (!preg_match('/^\(.*(.)$/', $regle,$r))
$ok = true; // DATA: ok.
elseif ($r[1] != ')') $ok = true; // i.e. ? ou *: ok
elseif (preg_match('/[^?*)][)]*,/', $regle)) $ok= false;
else $ok= true;
}
if( !$ok) {
$phraseur_xml->err[]= " <p>\n<b>$name</b>"
. _L(' balise vide')
. coordonnees_erreur($phraseur);
}
}
xml_finElement($phraseur, $name, $vide);
}
function textElement($phraseur, $data)
{ xml_textElement($phraseur, $data);}
function PiElement($phraseur, $target, $data)
{ xml_PiElement($phraseur, $target, $data);}
function defautElement($phraseur, $data)
{
global $phraseur_xml;
if (!preg_match('/^<!--/', $data)
AND (preg_match_all('/&([^;]*)?/', $data, $r, PREG_SET_ORDER)))
foreach ($r as $m) {
list($t,$e) = $m;
if (!isset($phraseur_xml->dtc->entites[$e]))
$phraseur_xml->err[]= " <b>$e</b>"
. _L(' entite inconnue ')
. coordonnees_erreur($phraseur);
}
xml_defautElement($phraseur, $data);
}
function phraserTout($phraseur, $data)
{
$this->dtc = charger_dtd($data);
// bug de SAX qui ne dit pas si une Entite est dans un attribut ou non
// ==> eliminer toutes les entites
$data = unicode2charset(html2unicode($data, true));
xml_parsestring($phraseur, $data);
$valider_passe2 = charger_fonction('valider_passe2', 'inc');
$valider_passe2($this);
return !$this->err ? $this->res : join('<br />', $this->err) . '<br />';
}
var $depth = "";
var $res = "";
var $contenu = array();
var $ouvrant = array();
var $reperes = array();
var $dtc = NULL;
var $err = array();
var $ids = array();
var $idrefs = array();
var $idrefss = array();
var $debuts = array();
}
function inc_valider_xml_dist($page, $apply=false)
{
$sax = charger_fonction('sax', 'inc');
return $sax($page, $apply, $GLOBALS['phraseur_xml'] = new ValidateurXML());
}