[SPIP][PLUGINS] v3.0-->v3.2
[lhc/web/www.git] / www / plugins-dist / revisions / inc / revisions.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 /**
14 * Fonctions utilitaires du plugin révisions
15 *
16 * @package SPIP\Revisions\Fonctions
17 **/
18 if (!defined("_ECRIRE_INC_VERSION")) {
19 return;
20 }
21
22 $GLOBALS['agregation_versions'] = 10;
23
24 /** Intervalle de temps (en seconde) separant deux révisions par un même auteur */
25 define('_INTERVALLE_REVISIONS', 600);
26
27 /**
28 * Découper les paragraphes d'un texte en fragments
29 *
30 * @param string $texte Texte à fragmenter
31 * @param array $paras Tableau de fragments déjà là
32 * @return string[] Tableau de fragments (paragraphes)
33 **/
34 function separer_paras($texte, $paras = array()) {
35 if (!$paras) {
36 $paras = array();
37 }
38 while (preg_match("/(\r\n?){2,}|\n{2,}/", $texte, $regs)) {
39 $p = strpos($texte, $regs[0]) + strlen($regs[0]);
40 $paras[] = substr($texte, 0, $p);
41 $texte = substr($texte, $p);
42 }
43 if ($texte) {
44 $paras[] = $texte;
45 }
46
47 return $paras;
48 }
49
50 // https://code.spip.net/@replace_fragment
51 function replace_fragment($id_objet, $objet, $version_min, $version_max, $id_fragment, $fragment) {
52 $fragment = serialize($fragment);
53 $compress = 0;
54
55 /* On ne compresse plus les fragments car probleme de portabilite de base, corruptions de donnees
56 en backup SQLite ou meme en mysqldump */
57 // pour le portage en PG il faut l'equivalente au mysql_escape_string
58 // et deporter son appel dans les fonctions d'abstraction.
59 /*
60 if (function_exists('gzcompress')
61 AND $GLOBALS['connexions'][0]['type'] == 'mysql') {
62 $s = gzcompress($fragment);
63 if (strlen($s) < strlen($fragment)) {
64 # spip_log("gain gz: ".intval(100 - 100 * strlen($s) / strlen($fragment)),'revisions');
65 $compress = 1;
66 $fragment = $s;
67 }
68 }
69 */
70
71 // Attention a echapper $fragment, binaire potentiellement gz
72 return array(
73 'id_objet' => intval($id_objet),
74 'objet' => $objet,
75 'id_fragment' => intval($id_fragment),
76 'version_min' => intval($version_min),
77 'version_max' => intval($version_max),
78 'compress' => $compress,
79 'fragment' => $fragment
80 );
81 }
82
83 // https://code.spip.net/@envoi_replace_fragments
84 function envoi_replace_fragments($replaces) {
85 $desc = $GLOBALS['tables_auxiliaires']['spip_versions_fragments'];
86 foreach ($replaces as $r) {
87 sql_replace('spip_versions_fragments', $r, $desc);
88 }
89 }
90
91
92 // https://code.spip.net/@envoi_delete_fragments
93 function envoi_delete_fragments($id_objet, $objet, $deletes) {
94 if (count($deletes)) {
95 sql_delete("spip_versions_fragments",
96 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND ((" . join(") OR (",
97 $deletes) . "))");
98 }
99 }
100
101
102 //
103 // Ajouter les fragments de la derniere version (tableau associatif id_fragment => texte)
104 //
105 // https://code.spip.net/@ajouter_fragments
106 function ajouter_fragments($id_objet, $objet, $id_version, $fragments) {
107 global $agregation_versions;
108
109 $replaces = array();
110 foreach ($fragments as $id_fragment => $texte) {
111 $nouveau = true;
112 // Recuperer la version la plus recente
113 $row = sql_fetsel("compress, fragment, version_min, version_max", "spip_versions_fragments",
114 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_fragment=$id_fragment AND version_min<=$id_version",
115 "", "version_min DESC", "1");
116
117 if ($row) {
118 $fragment = $row['fragment'];
119 $version_min = $row['version_min'];
120 if ($row['compress'] > 0) {
121 $fragment = @gzuncompress($fragment);
122 }
123 $fragment = unserialize($fragment);
124 if (is_array($fragment)) {
125 unset($fragment[$id_version]);
126 // Si le fragment n'est pas trop gros, prolonger celui-ci
127 $nouveau = count($fragment) >= $agregation_versions
128 && strlen($row['fragment']) > 1000;
129 }
130 }
131 if ($nouveau) {
132 $fragment = array($id_version => $texte);
133 $version_min = $id_version;
134 } else {
135 // Ne pas dupliquer les fragments non modifies
136 $modif = true;
137 for ($i = $id_version - 1; $i >= $version_min; $i--) {
138 if (isset($fragment[$i])) {
139 $modif = ($fragment[$i] != $texte);
140 break;
141 }
142 }
143 if ($modif) {
144 $fragment[$id_version] = $texte;
145 }
146 }
147
148 // Preparer l'enregistrement du fragment
149 $replaces[] = replace_fragment($id_objet, $objet, $version_min, $id_version, $id_fragment, $fragment);
150 }
151
152 envoi_replace_fragments($replaces);
153 }
154
155 //
156 // Supprimer tous les fragments d'un objet lies a un intervalle de versions
157 // (essaie d'eviter une trop grande fragmentation)
158 //
159 // https://code.spip.net/@supprimer_fragments
160 function supprimer_fragments($id_objet, $objet, $version_debut, $version_fin) {
161 global $agregation_versions;
162
163 $replaces = array();
164 $deletes = array();
165
166 // D'abord, vider les fragments inutiles
167 sql_delete("spip_versions_fragments",
168 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min>=$version_debut AND version_max<=$version_fin");
169
170 // Fragments chevauchant l'ensemble de l'intervalle, s'ils existent
171 $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments",
172 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min<$version_debut AND version_max>$version_fin");
173
174 while ($row = sql_fetch($result)) {
175 $id_fragment = $row['id_fragment'];
176 $fragment = $row['fragment'];
177 if ($row['compress'] > 0) {
178 $fragment = gzuncompress($fragment);
179 }
180 $fragment = unserialize($fragment);
181 for ($i = $version_fin; $i >= $version_debut; $i--) {
182 if (isset($fragment[$i])) {
183 // Recopier le dernier fragment si implicite
184 if (!isset($fragment[$version_fin + 1])) {
185 $fragment[$version_fin + 1] = $fragment[$i];
186 }
187 unset($fragment[$i]);
188 }
189 }
190
191 $replaces[] = replace_fragment($id_objet, $objet,
192 $row['version_min'], $row['version_max'], $id_fragment, $fragment);
193 }
194
195 // Fragments chevauchant le debut de l'intervalle, s'ils existent
196 $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments",
197 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min<$version_debut AND version_max>=$version_debut AND version_max<=$version_fin");
198
199 $deb_fragment = array();
200 while ($row = sql_fetch($result)) {
201 $id_fragment = $row['id_fragment'];
202 $fragment = $row['fragment'];
203 $version_min = $row['version_min'];
204 $version_max = $row['version_max'];
205 if ($row['compress'] > 0) {
206 $fragment = gzuncompress($fragment);
207 }
208 $fragment = unserialize($fragment);
209 for ($i = $version_debut; $i <= $version_max; $i++) {
210 if (isset($fragment[$i])) {
211 unset($fragment[$i]);
212 }
213 }
214
215 // Stocker temporairement le fragment pour agregation
216 $deb_fragment[$id_fragment] = $fragment;
217 // Ajuster l'intervalle des versions
218 $deb_version_min[$id_fragment] = $version_min;
219 $deb_version_max[$id_fragment] = $version_debut - 1;
220 }
221
222 // Fragments chevauchant la fin de l'intervalle, s'ils existent
223 $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments",
224 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_max>$version_fin AND version_min>=$version_debut AND version_min<=$version_fin");
225
226 while ($row = sql_fetch($result)) {
227 $id_fragment = $row['id_fragment'];
228 $fragment = $row['fragment'];
229 $version_min = $row['version_min'];
230 $version_max = $row['version_max'];
231 if ($row['compress'] > 0) {
232 $fragment = gzuncompress($fragment);
233 }
234 $fragment = unserialize($fragment);
235 for ($i = $version_fin; $i >= $version_min; $i--) {
236 if (isset($fragment[$i])) {
237 // Recopier le dernier fragment si implicite
238 if (!isset($fragment[$version_fin + 1])) {
239 $fragment[$version_fin + 1] = $fragment[$i];
240 }
241 unset($fragment[$i]);
242 }
243 }
244
245 // Virer l'ancien enregistrement (la cle primaire va changer)
246 $deletes[] = "id_fragment=$id_fragment AND version_min=$version_min";
247 // Essayer l'agregation
248 $agreger = false;
249 if (isset($deb_fragment[$id_fragment])) {
250 $agreger = (count($deb_fragment[$id_fragment]) + count($fragment) <= $agregation_versions);
251 if ($agreger) {
252 $fragment = $deb_fragment[$id_fragment] + $fragment;
253 $version_min = $deb_version_min[$id_fragment];
254 } else {
255 $replaces[] = replace_fragment($id_objet, $objet,
256 $deb_version_min[$id_fragment], $deb_version_max[$id_fragment],
257 $id_fragment, $deb_fragment[$id_fragment]);
258 }
259 unset($deb_fragment[$id_fragment]);
260 }
261 if (!$agreger) {
262 // Ajuster l'intervalle des versions
263 $version_min = $version_fin + 1;
264 }
265 $replaces[] = replace_fragment($id_objet, $objet, $version_min, $version_max, $id_fragment, $fragment);
266 }
267
268 // Ajouter fragments restants
269 if (is_array($deb_fragment) && count($deb_fragment) > 0) {
270 foreach ($deb_fragment as $id_fragment => $fragment) {
271 $replaces[] = replace_fragment($id_objet, $objet,
272 $deb_version_min[$id_fragment], $deb_version_max[$id_fragment],
273 $id_fragment, $deb_fragment[$id_fragment]);
274 }
275 }
276
277 envoi_replace_fragments($replaces);
278 envoi_delete_fragments($id_objet, $objet, $deletes);
279 }
280
281
282 /**
283 * Récupérer les fragments d'un objet pour une version demandée
284 *
285 * @param int $id_objet Identifiant de l'objet
286 * @param string $objet Objet
287 * @param int $id_version Identifiant de la version
288 * @return array Couples id_fragment => texte
289 */
290 function recuperer_fragments($id_objet, $objet, $id_version) {
291 $fragments = array();
292
293 if ($id_version == 0) {
294 return array();
295 }
296
297 $result = sql_select(
298 "id_fragment, version_min, version_max, compress, fragment",
299 "spip_versions_fragments",
300 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet)
301 . " AND version_min<=$id_version AND version_max>=$id_version");
302
303 while ($row = sql_fetch($result)) {
304 $id_fragment = $row['id_fragment'];
305 $version_min = $row['version_min'];
306 $fragment = $row['fragment'];
307 // si le fragment est compressé, tenter de le décompresser, sinon écrire une erreur
308 if ($row['compress'] > 0) {
309 $fragment_ = @gzuncompress($fragment);
310 if (strlen($fragment) && $fragment_ === false) {
311 $fragment = serialize(array($row['version_max'] => "[" . _T('forum_titre_erreur') . $id_fragment . "]"));
312 } else {
313 $fragment = $fragment_;
314 }
315 }
316 // tenter dedésérialiser le fragment, sinon écrire une erreur
317 $fragment_ = unserialize($fragment);
318 if (strlen($fragment) && $fragment_ === false) {
319 $fragment = array($row['version_max'] => "[" . _T('forum_titre_erreur') . $id_fragment . "]");
320 } else {
321 $fragment = $fragment_;
322 }
323 // on retrouve le fragment le plus près de notre version
324 for ($i = $id_version; $i >= $version_min; $i--) {
325 if (isset($fragment[$i])) {
326
327 ## hack destine a sauver les archives des sites iso-8859-1
328 ## convertis en utf-8 (les archives ne sont pas converties
329 ## mais ce code va les nettoyer ; pour les autres charsets
330 ## la situation n'est pas meilleure ni pire qu'avant)
331 if ($GLOBALS['meta']['charset'] == 'utf-8'
332 and include_spip('inc/charsets')
333 and !is_utf8($fragment[$i])
334 ) {
335 $fragment[$i] = importer_charset($fragment[$i], 'iso-8859-1');
336 }
337
338 $fragments[$id_fragment] = $fragment[$i];
339 // quitter la boucle dès le premier touvé.
340 break;
341 }
342 }
343 }
344
345 return $fragments;
346 }
347
348
349 //
350 // Apparier des paragraphes deux a deux entre une version originale
351 // et une version modifiee
352 //
353 // https://code.spip.net/@apparier_paras
354 function apparier_paras($src, $dest, $flou = true) {
355 $src_dest = array();
356 $dest_src = array();
357
358 $t1 = $t2 = array();
359
360 $md1 = $md2 = array();
361 $gz_min1 = $gz_min2 = array();
362 $gz_trans1 = $gz_trans2 = array();
363 $l1 = $l2 = array();
364
365 // Nettoyage de la ponctuation pour faciliter l'appariement
366 foreach ($src as $key => $val) {
367 $t1[$key] = strval(preg_replace("/[[:punct:][:space:]]+/", " ", $val));
368 }
369 foreach ($dest as $key => $val) {
370 $t2[$key] = strval(preg_replace("/[[:punct:][:space:]]+/", " ", $val));
371 }
372
373 // Premiere passe : chercher les correspondance exactes
374 foreach ($t1 as $key => $val) {
375 $md1[$key] = md5($val);
376 }
377 foreach ($t2 as $key => $val) {
378 $md2[md5($val)][$key] = $key;
379 }
380 foreach ($md1 as $key1 => $h) {
381 if (isset($md2[$h])) {
382 $key2 = reset($md2[$h]);
383 if (isset($t1[$key1]) and isset($t2[$key2]) and $t1[$key1] == $t2[$key2]) {
384 $src_dest[$key1] = $key2;
385 $dest_src[$key2] = $key1;
386 unset($t1[$key1]);
387 unset($t2[$key2]);
388 unset($md2[$h][$key2]);
389 }
390 }
391 }
392
393 if ($flou) {
394 // Deuxieme passe : recherche de correlation par test de compressibilite
395 foreach ($t1 as $key => $val) {
396 $l1[$key] = strlen(gzcompress($val));
397 }
398 foreach ($t2 as $key => $val) {
399 $l2[$key] = strlen(gzcompress($val));
400 }
401 foreach ($t1 as $key1 => $s1) {
402 foreach ($t2 as $key2 => $s2) {
403 $r = strlen(gzcompress($s1 . $s2));
404 $taux = 1.0 * $r / ($l1[$key1] + $l2[$key2]);
405 if (!isset($gz_min1[$key1]) || !$gz_min1[$key1] || $gz_min1[$key1] > $taux) {
406 $gz_min1[$key1] = $taux;
407 $gz_trans1[$key1] = $key2;
408 }
409 if (!isset($gz_min2[$key2]) || !$gz_min2[$key2] || $gz_min2[$key2] > $taux) {
410 $gz_min2[$key2] = $taux;
411 $gz_trans2[$key2] = $key1;
412 }
413 }
414 }
415
416 // Depouiller les resultats de la deuxieme passe :
417 // ne retenir que les correlations reciproques
418 foreach ($gz_trans1 as $key1 => $key2) {
419 if ($gz_trans2[$key2] == $key1 && $gz_min1[$key1] < 0.9) {
420 $src_dest[$key1] = $key2;
421 $dest_src[$key2] = $key1;
422 }
423 }
424 }
425
426 // Retourner les mappings
427 return array($src_dest, $dest_src);
428 }
429
430 /**
431 * Retrouve les champs d'un objet et leurs contenus à une version donnée
432 *
433 * @uses recuperer_version()
434 * @uses liste_champs_versionnes()
435 * @uses retrouver_champ_version_objet()
436 *
437 * @param int $id_objet Identifiant de l'objet
438 * @param string $objet Objet
439 * @param int $id_version Identifiant de la version
440 * @return array Couples champs => textes
441 **/
442 function recuperer_version_complete($id_objet, $objet, $id_version) {
443 if (!$id_version or !$id_objet or !$objet) {
444 return array();
445 }
446
447 include_spip('inc/suivi_versions');
448
449 // champs modifiés à la version voulue
450 $textes = recuperer_version($id_objet, $objet, $id_version);
451
452 // tous les champs possibles versionnés pour l'objet
453 $champs = liste_champs_versionnes(table_objet_sql($objet));
454
455 foreach ($champs as $champ) {
456 // Remonter dans le temps pour trouver le champ en question pour chaque version
457 retrouver_champ_version_objet($objet, $id_objet, $id_version, $champ, $textes);
458 }
459
460 return $textes;
461 }
462
463 /**
464 * Récupérer les champs d'un objet, pour une version demandée
465 *
466 * @param int $id_objet Identifiant de l'objet
467 * @param string $objet Objet
468 * @param int $id_version Identifiant de la version
469 * @return array Couples champs => textes
470 */
471 function recuperer_version($id_objet, $objet, $id_version) {
472
473 $champs = sql_getfetsel("champs", "spip_versions",
474 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version=" . intval($id_version));
475 if (!$champs or !is_array($champs = unserialize($champs))) {
476 return array();
477 } else {
478 return reconstuire_version($champs,
479 recuperer_fragments($id_objet, $objet, $id_version));
480 }
481 }
482
483 /**
484 * Reconstruire une version donnée
485 *
486 * À partir de la liste des champs et de fragments,
487 * retourne le texte de chaque champ.
488 *
489 * @param array $champs Couples (champ => liste d'id_fragment).
490 * La liste est de la forme "5 32 7 16 8 2"
491 * @param array $fragments Couples (id_fragment => texte)
492 * @param array $res Couples (champ => texte) déjà connus
493 * @return array Couples (champ => texte)
494 */
495 function reconstuire_version($champs, $fragments, $res = array()) {
496
497 static $msg;
498 if (!$msg) {
499 $msg = _T('forum_titre_erreur');
500 }
501
502 foreach ($champs as $nom => $code) {
503 if (!isset($res[$nom])) {
504 $t = '';
505 foreach (array_filter(explode(' ', $code)) as $id) {
506 $t .= isset($fragments[$id])
507 ? $fragments[$id]
508 : "[$msg$id]";
509 }
510 $res[$nom] = $t;
511 }
512 }
513
514 return $res;
515 }
516
517 // https://code.spip.net/@supprimer_versions
518 function supprimer_versions($id_objet, $objet, $version_min, $version_max) {
519 sql_delete("spip_versions",
520 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version>=$version_min AND id_version<=$version_max");
521
522 supprimer_fragments($id_objet, $objet, $version_min, $version_max);
523 }
524
525
526 /**
527 * Ajouter une version à un objet éditorial
528 *
529 * @param int $id_objet
530 * @param string $objet
531 * @param array $champs
532 * @param string $titre_version
533 * Titre donné aux modifications apportées
534 * @param int|null $id_auteur
535 * Auteur apportant les modifications. En absence (session anonyme), utilisera l'IP pour garder une trace.
536 * @return int
537 * id_version : identifiant de la version
538 **/
539 function ajouter_version($id_objet, $objet, $champs, $titre_version = "", $id_auteur = null) {
540 $paras = $paras_old = $paras_champ = $fragments = array();
541
542 // Attention a une edition anonyme (type wiki): id_auteur n'est pas
543 // definie, on enregistre alors le numero IP
544 $str_auteur = intval($id_auteur) ? intval($id_auteur) : $GLOBALS['ip'];
545
546 // si pas de titre dans cette version, la marquer 'non' permanente,
547 // et elle pourra etre fusionnee avec une revision ulterieure dans un delai < _INTERVALLE_REVISIONS
548 // permet de fusionner plusieurs editions consecutives champs par champs avec les crayons
549 $permanent = empty($titre_version) ? 'non' : '';
550
551 // Detruire les tentatives d'archivages non abouties en 1 heure
552 sql_delete('spip_versions', array(
553 "id_objet=" . intval($id_objet),
554 "objet=" . sql_quote($objet),
555 "id_version <= 0",
556 "date < DATE_SUB(" . sql_quote(date('Y-m-d H:i:s')) . ", INTERVAL " . _INTERVALLE_REVISIONS . " SECOND)"
557 )
558 );
559
560 // Signaler qu'on opere en mettant un numero de version negatif
561 // distinctif (pour eviter la violation d'unicite)
562 // et un titre contenant en fait le moment de l'insertion
563 list($ms, $sec) = explode(' ', microtime());
564 $date = $sec . substr($ms, 1,
565 4) - 20; // SQL ne ramene que 4 chiffres significatifs apres la virgule pour 0.0+titre_version
566 $datediff = ($sec - mktime(0, 0, 0, 9, 1, 2007)) * 1000000 + substr($ms, 2, strlen($ms) - 4);
567
568 $valeurs = array(
569 'id_objet' => $id_objet,
570 'objet' => $objet,
571 'id_version' => (0 - $datediff),
572 'date' => date('Y-m-d H:i:s'),
573 'id_auteur' => $str_auteur, // varchar ici!
574 'titre_version' => $date
575 );
576 sql_insertq('spip_versions', $valeurs);
577
578 // Eviter les validations entremelees en s'endormant s'il existe
579 // une version <0 plus recente mais pas plus vieille que 10s
580 // Une <0 encore plus vieille est une operation avortee,
581 // on passe outre (vaut mieux archiver mal que pas du tout).
582 // Pour tester:
583 // 0. mettre le delai a 30
584 // 1. decommenter le premier sleep(15)
585 // 2. enregistrer une modif
586 // 3. recommenter le premier sleep(15), decommenter le second.
587 // 4. enregistrer une autre modif dans les 15 secondes
588 # sleep(15);
589 $delai = $sec - 10;
590 while (sql_countsel('spip_versions',
591 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version < 0 AND 0.0+titre_version < $date AND titre_version<>" . sql_quote($date,
592 '', 'text') . " AND 0.0+titre_version > $delai")) {
593 spip_log("version $objet $id_objet :insertion en cours avant $date ($delai)", 'revisions');
594 sleep(1);
595 $delai++;
596 }
597 # sleep(15); spip_log("sortie $sec $delai");
598 // Determiner le numero du prochain fragment
599 $next = sql_fetsel("id_fragment", "spip_versions_fragments",
600 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet), "", "id_fragment DESC", "1");
601
602 $onlylock = '';
603
604 // Examiner la derniere version
605 $row = sql_fetsel("id_version, champs, id_auteur, date, permanent", "spip_versions",
606 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version > 0", '', "id_version DESC",
607 "1"); // le champ id_auteur est un varchar dans cette table
608
609 if ($row) {
610 $id_version = $row['id_version'];
611 $paras_old = recuperer_fragments($id_objet, $objet, $id_version);
612 $champs_old = $row['champs'];
613 if ($row['id_auteur'] != $str_auteur
614 or $row['permanent'] != 'non'
615 or strtotime($row['date']) < (time() - _INTERVALLE_REVISIONS)
616 ) {
617 spip_log(strtotime($row['date']), 'revisions');
618 spip_log(time(), 'revisions');
619 spip_log(_INTERVALLE_REVISIONS, 'revisions');
620 $id_version++;
621 }
622 // version precedente recente, on va la mettre a jour
623 // avec les nouveaux arrivants si presents
624 else {
625 $champs = reconstuire_version(unserialize($champs_old), $paras_old, $champs);
626 $onlylock = 're';
627 }
628 } else {
629 $id_version = 1;
630 }
631
632 $next = !$next ? 1 : ($next['id_fragment'] + 1);
633
634 // Generer les nouveaux fragments
635 $codes = array();
636 foreach ($champs as $nom => $texte) {
637 $codes[$nom] = array();
638 $paras = separer_paras($texte, $paras);
639 $paras_champ[$nom] = count($paras);
640 }
641
642 // Apparier les fragments de maniere optimale
643 $n = count($paras);
644 if ($n) {
645 // Tables d'appariement dans les deux sens
646 list(, $trans) = apparier_paras($paras_old, $paras);
647 reset($champs);
648 $nom = '';
649
650 // eviter une notice PHP au tout debut de la boucle
651 // on ajoute ''=>0 en debut de tableau.
652 $paras_champ = array($nom => 0) + $paras_champ;
653
654 for ($i = 0; $i < $n; $i++) {
655 while ($i >= $paras_champ[$nom]) {
656 list($nom, ) = each($champs);
657 }
658 // Lier au fragment existant si possible, sinon creer un nouveau fragment
659 $id_fragment = isset($trans[$i]) ? $trans[$i] : $next++;
660 $codes[$nom][] = $id_fragment;
661 $fragments[$id_fragment] = $paras[$i];
662 }
663 }
664 foreach ($champs as $nom => $t) {
665 $codes[$nom] = join(' ', $codes[$nom]);
666 # avec la ligne qui suit, un champ qu'on vide ne s'enregistre pas
667 # if (!strlen($codes[$nom])) unset($codes[$nom]);
668 }
669
670 // Enregistrer les modifications
671 ajouter_fragments($id_objet, $objet, $id_version, $fragments);
672
673 // Si l'insertion ne servait que de verrou,
674 // la detruire apres mise a jour de l'ancienne entree,
675 // sinon la mise a jour efface en fait le verrou.
676
677 if (!$onlylock) {
678 sql_updateq('spip_versions', array(
679 'id_version' => $id_version,
680 'date' => date('Y-m-d H:i:s'),
681 'champs' => serialize($codes),
682 'permanent' => $permanent,
683 'titre_version' => $titre_version
684 ),
685 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version < 0 AND titre_version='$date'");
686 } else {
687 sql_updateq('spip_versions', array(
688 'date' => date('Y-m-d H:i:s'),
689 'champs' => serialize($codes),
690 'permanent' => $permanent,
691 'titre_version' => $titre_version
692 ), "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version=$id_version");
693 sql_delete("spip_versions",
694 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version < 0 AND titre_version ='$date'");
695 }
696 spip_log($onlylock . "memorise la version $id_version de l'objet $objet $id_objet $titre_version", 'revisions');
697
698 return $id_version;
699 }
700
701 // les textes "diff" ne peuvent pas passer dans propre directement,
702 // car ils contiennent des <span> et <div> parfois mal places
703 // https://code.spip.net/@propre_diff
704 function propre_diff($texte) {
705
706 $span_diff = array();
707 if (preg_match_all(',<(/)?(span|div) (class|rem)="diff-[^>]*>,', $texte, $regs, PREG_SET_ORDER)) {
708 $regs = array_slice($regs, 0, 500); #limiter la casse s'il y en a trop
709 foreach ($regs as $c => $reg) {
710 $texte = str_replace($reg[0], '@@@SPIP_DIFF' . $c . '@@@', $texte);
711 }
712 }
713
714 // [ ...<span diff> -> lien ]
715 // < tag <span diff> >
716 $texte = preg_replace(',<([^>]*?@@@SPIP_DIFF[0-9]+@@@),',
717 '&lt;\1', $texte);
718
719 # attention ici astuce seulement deux @@ finals car on doit eviter
720 # deux patterns a suivre, afin de pouvoir prendre [ mais eviter [[
721 $texte = preg_replace(',(^|[^[])[[]([^[\]]*@@@SPIP_DIFF[0-9]+@@),',
722 '\1&#91;\2', $texte);
723
724 // desactiver TeX & toujours-paragrapher
725 $tex = $GLOBALS['traiter_math'];
726 $GLOBALS['traiter_math'] = '';
727 $mem = $GLOBALS['toujours_paragrapher'];
728 $GLOBALS['toujours_paragrapher'] = false;
729
730 $texte = propre($texte);
731
732 // retablir
733 $GLOBALS['traiter_math'] = $tex;
734 $GLOBALS['toujours_paragrapher'] = $mem;
735
736 // un blockquote mal ferme peut gener l'affichage, et title plante safari
737 $texte = preg_replace(',<(/?(blockquote|title)[^>]*)>,i', '&lt;\1>', $texte);
738
739 // Dans les <cadre> c'est un peu plus complique
740 if (preg_match_all(',<textarea (.*)</textarea>,Uims', $texte, $area, PREG_SET_ORDER)) {
741 foreach ($area as $reg) {
742 $remplace = preg_replace(',@@@SPIP_DIFF[0-9]+@@@,', '**', $reg[0]);
743 if ($remplace <> $reg[0]) {
744 $texte = str_replace($reg[0], $remplace, $texte);
745 }
746 }
747 }
748
749 // replacer les valeurs des <span> et <div> diff-
750 if (is_array($regs)) {
751 foreach ($regs as $c => $reg) {
752 $bal = (!$reg[1]) ? $reg[0] : "</$reg[2]>";
753 $texte = str_replace('@@@SPIP_DIFF' . $c . '@@@', $bal, $texte);
754 $GLOBALS['les_notes'] = str_replace('@@@SPIP_DIFF' . $c . '@@@', $bal, $GLOBALS['les_notes']);
755 }
756 }
757
758
759 // quand le dernier tag est ouvrant le refermer ...
760 $reg = end($regs);
761 if (!$reg[1] and $reg[2]) {
762 $texte .= "</$reg[2]>";
763 }
764
765 // et interdire_scripts !
766 $texte = interdire_scripts($texte);
767
768 return $texte;
769 }
770
771
772 /**
773 * Liste les champs versionnés d'une table objet.
774 *
775 * @param string $table
776 * Nom complet de sa table sql. Exemple 'spip_articles'
777 * @return array
778 * Liste des champs versionnés
779 */
780 function liste_champs_versionnes($table) {
781 $liste_objets_versionnees = is_array(unserialize($GLOBALS['meta']['objets_versions'])) ? unserialize($GLOBALS['meta']['objets_versions']) : array();
782
783 if (!in_array($table, $liste_objets_versionnees)) {
784 return array();
785 }
786
787 include_spip('base/objets');
788 if ($infos = lister_tables_objets_sql($table)
789 and isset($infos['champs_versionnes'])
790 ) {
791 return $infos['champs_versionnes'];
792 }
793
794 return array();
795 }
796
797 /**
798 * Lorsqu'un champ versionée est une jointure, récuperer tous les liens
799 * et les mettre sous forme de liste énumérée
800 *
801 * @param string $objet
802 * @param string $id_objet
803 * @param string $jointure
804 * @return string
805 */
806 function recuperer_valeur_champ_jointure($objet, $id_objet, $jointure) {
807 $objet_joint = objet_type($jointure);
808 include_spip('action/editer_liens');
809 $v = array();
810 if (objet_associable($objet_joint)) {
811 $liens = objet_trouver_liens(array($objet_joint => '*'), array($objet => $id_objet));
812 foreach ($liens as $l) {
813 $v[] = $l[$objet_joint];
814 }
815 } elseif (objet_associable($objet)) {
816 $liens = objet_trouver_liens(array($objet => $id_objet), array($objet_joint => '*'));
817 foreach ($liens as $l) {
818 $v[] = $l[$objet];
819 }
820 }
821 sort($v);
822
823 return implode(",", $v);
824 }
825
826 /**
827 * Créer la première révision d'un objet si nécessaire
828 *
829 * À faire notamment si on vient d'activer l'extension et qu'on fait une modif
830 * sur un objet qui était déjà en base, mais non versionné
831 *
832 * La fonction renvoie le numéro de la dernière version de l'objet,
833 * et 0 si pas de version pour cet objet
834 *
835 * @param string $table
836 * @param string $objet
837 * @param int $id_objet
838 * @param array $champs
839 * @param int $id_auteur
840 * @return int
841 */
842 function verifier_premiere_revision($table, $objet, $id_objet, $champs = null, $id_auteur = 0) {
843
844 $id_table_objet = id_table_objet($objet);
845 if (!$champs) {
846 $champs = liste_champs_versionnes($table);
847 }
848 if (!$champs) {
849 return false;
850 }
851
852 if (!$id_version = sql_getfetsel('id_version', 'spip_versions',
853 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet), '', 'id_version DESC', '0,1')
854 ) {
855 // recuperer toutes les valeurs actuelles des champs
856 // pour l'objet
857 $originaux = sql_fetsel("*", $table, "$id_table_objet=" . intval($id_objet));
858 $premiere = false;
859 $champs_originaux = array();
860
861 foreach ($champs as $v) {
862 if (isset($originaux[$v])) {
863 $champs_originaux[$v] = $originaux[$v];
864 } elseif (strncmp($v, 'jointure_', 9) == 0) {
865 $champs_originaux[$v] = recuperer_valeur_champ_jointure($objet, $id_objet, substr($v, 9));
866 }
867 if (isset($champs_originaux[$v]) and isset($originaux[$v]) and strlen($originaux[$v])) {
868 $premiere = true;
869 }
870 }
871
872 // Si un champ est non vide,
873 // il faut creer une premiere revision
874 if ($premiere) {
875 $trouver_table = charger_fonction('trouver_table', 'base');
876 $desc = $trouver_table($table);
877
878 // "trouver" une date raisonnable pour la version initiale
879
880 $date_modif = "";
881 foreach (array('date_modif', 'maj') as $d) {
882 if (!$date_modif and isset($originaux[$d]) and $t = strtotime($originaux[$d])) {
883 $date_modif = date("Y-m-d H:i:s", $t - 20);
884 }
885 }
886 if (!$date_modif
887 and isset($desc['date'])
888 and isset($originaux[$desc['date']])
889 ) {
890 $date_modif = $originaux[$desc['date']];
891 } elseif (!$date_modif) {
892 $date_modif = date("Y-m-d H:i:s", time() - 20);
893 }
894
895 if ($id_version = ajouter_version($id_objet, $objet, $champs_originaux, _T('revisions:version_initiale'),
896 $id_auteur)
897 ) {
898 sql_updateq('spip_versions', array('date' => $date_modif),
899 "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version=$id_version");
900 }
901 }
902 }
903
904 return $id_version;
905 }