[SPIP] ~v3.0.20-->v3.0.25
[lhc/web/clavette_www.git] / www / ecrire / xml / analyser_dtd.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2016 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
8 * *
9 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
10 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
11 \***************************************************************************/
12
13 if (!defined('_ECRIRE_INC_VERSION')) return;
14
15 include_spip('xml/interfaces');
16
17 // http://doc.spip.org/@charger_dtd
18 function charger_dtd($grammaire, $avail, $rotlvl)
19 {
20 static $dtd = array(); # cache bien utile pour le validateur en boucle
21
22 if (isset($dtd[$grammaire]))
23 return $dtd[$grammaire];
24
25 if ($avail == 'SYSTEM') $grammaire = find_in_path($grammaire);
26
27 $file = _DIR_CACHE_XML . preg_replace('/[^\w.]/','_', $rotlvl) . '.gz';
28
29 if (lire_fichier($file, $r)) {
30 if (!$grammaire) return array();
31 if (($avail == 'SYSTEM') AND filemtime($file) < filemtime($grammaire))
32 $r = false;
33 }
34
35 if ($r) {
36 $dtc = unserialize($r);
37 } else {
38 spip_timer('dtd');
39 $dtc = new DTC;
40 // L'analyseur retourne un booleen de reussite et modifie $dtc.
41 // Retourner vide en cas d'echec
42 if (!analyser_dtd($grammaire, $avail, $dtc))
43 $dtc = array();
44 else {
45 // tri final pour presenter les suggestions de corrections
46 foreach ($dtc->peres as $k => $v) {
47 asort($v);
48 $dtc->peres[$k] = $v;
49 }
50
51 spip_log("Analyser DTD $avail $grammaire (" . spip_timer('dtd') . ") " . count($dtc->macros) . ' macros, ' . count($dtc->elements) . ' elements, ' . count($dtc->attributs) . " listes d'attributs, " . count($dtc->entites) . " entites");
52 # $r = $dtc->regles; ksort($r);foreach($r as $l => $v) {$t=array_keys($dtc->attributs[$l]);echo "<b>$l</b> '$v' ", count($t), " attributs: ", join (', ',$t);$t=$dtc->peres[$l];echo "<br />",count($t), " peres: ", @join (', ',$t), "<br />\n";}exit;
53 ecrire_fichier($file, serialize($dtc), true);
54 }
55
56 }
57 $dtd[$grammaire] = $dtc;
58 return $dtc;
59 }
60
61 // Compiler une regle de production en une Regexp qu'on appliquera sur la
62 // suite des noms de balises separes par des espaces. Du coup:
63 // supprimer #PCDATA etc, ca ne sert pas pour le controle des balises;
64 // supprimer les virgules (les sequences sont implicites dans une Regexp)
65 // conserver | + * ? ( ) qui ont la meme signification en DTD et en Regexp;
66 // faire suivre chaque nom d'un espace (et supprimer les autres) ...
67 // et parentheser le tout pour que | + * ? s'applique dessus.
68
69 // http://doc.spip.org/@compilerRegle
70 function compilerRegle($val)
71 {
72 $x = str_replace('()','',
73 preg_replace('/\s*,\s*/','',
74 preg_replace('/(\w+)\s*/','(?:\1 )',
75 preg_replace('/\s*\)/',')',
76 preg_replace('/\s*([(+*|?])\s*/','\1',
77 preg_replace('/\s*#\w+\s*[,|]?\s*/','', $val))))));
78 return $x;
79 }
80
81
82 // http://doc.spip.org/@analyser_dtd
83 function analyser_dtd($loc, $avail, &$dtc)
84 {
85 // creer le repertoire de cache si ce n'est fait
86 // (utile aussi pour le resultat de la compil)
87 $file = sous_repertoire(_DIR_CACHE_XML);
88 // si DTD locale, ignorer ce repertoire pour le moment
89 if ($avail == 'SYSTEM'){
90 $file = $loc;
91 if (_DIR_RACINE AND strncmp($file,_DIR_RACINE,strlen(_DIR_RACINE))==0)
92 $file = substr($file,strlen(_DIR_RACINE));
93 $file = find_in_path($file);
94 }
95 else {
96 $file .= preg_replace('/[^\w.]/','_', $loc);
97 }
98
99 $dtd = '';
100 if (@is_readable($file)) {
101 lire_fichier($file, $dtd);
102 } else {
103 if ($avail == 'PUBLIC') {
104 include_spip('inc/distant');
105 if ($dtd = trim(recuperer_page($loc)))
106 ecrire_fichier($file, $dtd, true);
107 }
108 }
109
110 $dtd = ltrim($dtd);
111 if (!$dtd) {
112 spip_log("DTD '$loc' ($file) inaccessible");
113 return false;
114 } else spip_log("analyse de la DTD $loc ");
115
116 while ($dtd) {
117 if ($dtd[0] != '<')
118 $r = analyser_dtd_lexeme($dtd, $dtc, $loc);
119 elseif ($dtd[1] != '!')
120 $r = analyser_dtd_pi($dtd, $dtc, $loc);
121 elseif ($dtd[2] == '[')
122 $r = analyser_dtd_data($dtd, $dtc, $loc);
123 else {
124 switch ($dtd[3]) {
125 case '%' : $r = analyser_dtd_data($dtd, $dtc, $loc); break;
126 case 'T' : $r = analyser_dtd_attlist($dtd, $dtc, $loc);break;
127 case 'L' : $r = analyser_dtd_element($dtd, $dtc, $loc);break;
128 case 'N' : $r = analyser_dtd_entity($dtd, $dtc, $loc);break;
129 case 'O' : $r = analyser_dtd_notation($dtd, $dtc, $loc);break;
130 case '-' : $r = analyser_dtd_comment($dtd, $dtc, $loc); break;
131 default: $r = -1;
132 }
133 }
134 if (!is_string($r)) {
135 spip_log("erreur $r dans la DTD " . substr($dtd,0,80) . ".....");
136 return false;
137 }
138 $dtd = $r;
139 }
140 return true;
141 }
142
143 // http://doc.spip.org/@analyser_dtd_comment
144 function analyser_dtd_comment($dtd, &$dtc, $grammaire){
145 // ejecter les commentaires, surtout quand ils contiennent du code.
146 // Option /s car sur plusieurs lignes parfois
147
148 if (!preg_match('/^<!--.*?-->\s*(.*)$/s',$dtd, $m))
149 return -6;
150 return $m[1];
151 }
152
153 // http://doc.spip.org/@analyser_dtd_pi
154 function analyser_dtd_pi($dtd, &$dtc, $grammaire){
155 if (!preg_match('/^<\?.*?>\s*(.*)$/s', $dtd, $m))
156 return -10;
157 return $m[1];
158 }
159
160 // http://doc.spip.org/@analyser_dtd_lexeme
161 function analyser_dtd_lexeme($dtd, &$dtc, $grammaire){
162
163 if (!preg_match(_REGEXP_ENTITY_DEF,$dtd, $m))
164 return -9;
165
166 list(,$s) = $m;
167 $n = $dtc->macros[$s];
168
169 if (is_array($n)) {
170 // en cas d'inclusion, l'espace de nom est le meme
171 // mais gaffe aux DTD dont l'URL est relative a l'engloblante
172 if (($n[0] == 'PUBLIC')
173 AND !preg_match("%^http://%", $n[1])) {
174 $n[1] = substr($grammaire,0, strrpos($grammaire,'/')+1) . $n[1];
175 }
176 analyser_dtd($n[1], $n[0], $dtc);
177 }
178
179 return ltrim(substr($dtd,strlen($m[0])));
180 }
181
182 // il faudrait gerer plus proprement les niveaux d'inclusion:
183 // ca ne depasse pas 3 ici.
184
185 // http://doc.spip.org/@analyser_dtd_data
186 function analyser_dtd_data($dtd, &$dtc, $grammaire){
187
188 if (!preg_match(_REGEXP_INCLUDE_USE,$dtd,$m))
189 return -11;
190 if (!preg_match('/^((\s*<!(\[\s*%\s*[^;]*;\s*\[([^]<]*<[^>]*>)*[^]<]*\]\]>)|([^]>]*>))*[^]<]*)\]\]>\s*/s',$m[2], $r))
191 return -12;
192
193 if ($dtc->macros[$m[1]] == 'INCLUDE')
194 $retour = $r[1] . substr($m[2], strlen($r[0]));
195 else $retour = substr($m[2], strlen($r[0]));
196
197 return $retour;
198 }
199
200 // http://doc.spip.org/@analyser_dtd_notation
201 function analyser_dtd_notation($dtd, &$dtc, $grammaire){
202 if (!preg_match('/^<!NOTATION.*?>\s*(.*)$/s',$dtd, $m))
203 return -8;
204 spip_log("analyser_dtd_notation a ecrire");
205 return $m[1];
206 }
207
208 // http://doc.spip.org/@analyser_dtd_entity
209 function analyser_dtd_entity($dtd, &$dtc, $grammaire)
210 {
211 if (!preg_match(_REGEXP_ENTITY_DECL, $dtd, $m))
212 return -2;
213
214 list($t, $term, $nom, $type, $k1,$k2,$k3,$k4,$k5,$k6, $c, $q, $alt, $dtd) = $m;
215
216 if (isset($dtc->macros[$nom]) AND $dtc->macros[$nom])
217 return $dtd;
218 if (isset($dtc->entites[$nom]))
219 spip_log("redefinition de l'entite $nom");
220 if ($k6) return $k6 . $dtd; // cas du synonyme complet
221 $val = expanserEntite(($k2 ? $k3 : ($k4 ? $k5 : $k6)), $dtc->macros);
222
223 // cas particulier double evaluation: 'PUBLIC "..." "...."'
224 if (preg_match('/(PUBLIC|SYSTEM)\s+"([^"]*)"\s*("([^"]*)")?\s*$/s',$val,$r)) {
225 list($t, $type, $val, $q, $alt) = $r;
226 }
227
228 if (!$term)
229 $dtc->entites[$nom] = $val;
230 elseif (!$type)
231 $dtc->macros[$nom] = $val;
232 else {
233 if (($type == 'SYSTEM') AND !$alt) $alt = $val;
234 if (!$alt)
235 $dtc->macros[$nom] = $val;
236 else {
237 if (($type == 'PUBLIC')
238 AND (strpos($alt, '/') === false))
239 $alt = preg_replace(',/[^/]+$,', '/', $grammaire)
240 . $alt ;
241 $dtc->macros[$nom] = array($type, $alt);
242 }
243 }
244
245 return $dtd;
246 }
247
248 // Dresser le tableau des filles potentielles de l'element
249 // pour traquer tres vite les illegitimes.
250 // Si la regle a au moins une sequence (i.e. une virgule)
251 // ou n'est pas une itération (i.e. se termine par * ou +)
252 // en faire une RegExp qu'on appliquera aux balises rencontrees.
253 // Sinon, conserver seulement le type de l'iteration car la traque
254 // aura fait l'essentiel du controle sans memorisation des balises.
255 // Fin du controle en finElement
256
257 // http://doc.spip.org/@analyser_dtd_element
258 function analyser_dtd_element($dtd, &$dtc, $grammaire)
259 {
260 if (!preg_match('/^<!ELEMENT\s+([^>\s]+)([^>]*)>\s*(.*)$/s', $dtd, $m))
261 return -3;
262
263 list(,$nom, $contenu, $dtd) = $m;
264 $nom = expanserEntite($nom, $dtc->macros);
265
266 if (isset($dtc->elements[$nom])) {
267 spip_log("redefinition de l'element $nom dans la DTD");
268 return -4;
269 }
270 $filles = array();
271 $contenu = expanserEntite($contenu, $dtc->macros);
272 $val = $contenu ? compilerRegle($contenu) : '(?:EMPTY )';
273 if ($val == '(?:EMPTY )')
274 $dtc->regles[$nom] = 'EMPTY';
275 elseif ($val == '(?:ANY )')
276 $dtc->regles[$nom] = 'ANY';
277 else {
278 $last = substr($val,-1);
279 if (preg_match('/ \w/', $val)
280 OR strpos('*+', $last) === false)
281 $dtc->regles[$nom] = "/^$val$/";
282 else
283 $dtc->regles[$nom] = $last;
284 $filles = array_values(preg_split('/\W+/', $val,-1, PREG_SPLIT_NO_EMPTY));
285
286 foreach ($filles as $k) {
287 if (!isset($dtc->peres[$k]))
288 $dtc->peres[$k] = array();
289 if (!in_array($nom, $dtc->peres[$k]))
290 $dtc->peres[$k][]= $nom;
291 }
292 }
293 $dtc->pcdata[$nom]= (strpos($contenu, '#PCDATA')===false);
294 $dtc->elements[$nom]= $filles;
295 return $dtd;
296 }
297
298
299 // http://doc.spip.org/@analyser_dtd_attlist
300 function analyser_dtd_attlist($dtd, &$dtc, $grammaire)
301 {
302 if (!preg_match('/^<!ATTLIST\s+(\S+)\s+([^>]*)>\s*(.*)/s', $dtd, $m))
303 return -5;
304
305 list(,$nom, $val, $dtd) = $m;
306 $nom = expanserEntite($nom, $dtc->macros);
307 $val = expanserEntite($val, $dtc->macros);
308 if (!isset($dtc->attributs[$nom]))
309 $dtc->attributs[$nom] = array();
310
311 if (preg_match_all("/\s*(\S+)\s+(([(][^)]*[)])|(\S+))\s+([^\s']*)(\s*'[^']*')?/", $val, $r2, PREG_SET_ORDER)) {
312 foreach($r2 as $m2) {
313 $v = preg_match('/^\w+$/', $m2[2]) ? $m2[2]
314 : ('/^' . preg_replace('/\s+/', '', $m2[2]) . '$/');
315 $m21 = expanserEntite($m2[1], $dtc->macros);
316 $m25 = expanserEntite($m2[5], $dtc->macros);
317 $dtc->attributs[$nom][$m21] = array($v, $m25);
318 }
319 }
320
321 return $dtd;
322 }
323
324
325 // Remplace dans la chaine $val les sous-chaines de forme "%NOM;"
326 // par leur definition dans le tableau $macros
327 // Si le premier argument n'est pas une chaine,
328 // retourne les statistiques (pour debug de DTD, inutilise en mode normal)
329
330 // http://doc.spip.org/@expanserEntite
331 function expanserEntite($val, $macros=array())
332 {
333 static $vu = array();
334 if (!is_string($val)) return $vu;
335
336 if (preg_match_all(_REGEXP_ENTITY_USE, $val, $r, PREG_SET_ORDER)){
337 foreach($r as $m) {
338 $ent = $m[1];
339 // il peut valoir ""
340 if (!isset($macros[$ent]))
341 spip_log("Entite $ent inconnu");
342 else {
343 @$vu[$ent]++;
344 $val = str_replace($m[0], $macros[$ent], $val);
345 }
346 }
347 }
348
349 return trim(preg_replace('/\s+/', ' ', $val));
350 }
351 ?>