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