intval($id_objet), 'objet' => $objet, 'id_fragment' => intval($id_fragment), 'version_min' => intval($version_min), 'version_max' => intval($version_max), 'compress' => $compress, 'fragment' => $fragment ); } // https://code.spip.net/@envoi_replace_fragments function envoi_replace_fragments($replaces) { $desc = $GLOBALS['tables_auxiliaires']['spip_versions_fragments']; foreach ($replaces as $r) { sql_replace('spip_versions_fragments', $r, $desc); } } // https://code.spip.net/@envoi_delete_fragments function envoi_delete_fragments($id_objet, $objet, $deletes) { if (count($deletes)) { sql_delete("spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND ((" . join(") OR (", $deletes) . "))"); } } // // Ajouter les fragments de la derniere version (tableau associatif id_fragment => texte) // // https://code.spip.net/@ajouter_fragments function ajouter_fragments($id_objet, $objet, $id_version, $fragments) { global $agregation_versions; $replaces = array(); foreach ($fragments as $id_fragment => $texte) { $nouveau = true; // Recuperer la version la plus recente $row = sql_fetsel("compress, fragment, version_min, version_max", "spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_fragment=$id_fragment AND version_min<=$id_version", "", "version_min DESC", "1"); if ($row) { $fragment = $row['fragment']; $version_min = $row['version_min']; if ($row['compress'] > 0) { $fragment = @gzuncompress($fragment); } $fragment = unserialize($fragment); if (is_array($fragment)) { unset($fragment[$id_version]); // Si le fragment n'est pas trop gros, prolonger celui-ci $nouveau = count($fragment) >= $agregation_versions && strlen($row['fragment']) > 1000; } } if ($nouveau) { $fragment = array($id_version => $texte); $version_min = $id_version; } else { // Ne pas dupliquer les fragments non modifies $modif = true; for ($i = $id_version - 1; $i >= $version_min; $i--) { if (isset($fragment[$i])) { $modif = ($fragment[$i] != $texte); break; } } if ($modif) { $fragment[$id_version] = $texte; } } // Preparer l'enregistrement du fragment $replaces[] = replace_fragment($id_objet, $objet, $version_min, $id_version, $id_fragment, $fragment); } envoi_replace_fragments($replaces); } // // Supprimer tous les fragments d'un objet lies a un intervalle de versions // (essaie d'eviter une trop grande fragmentation) // // https://code.spip.net/@supprimer_fragments function supprimer_fragments($id_objet, $objet, $version_debut, $version_fin) { global $agregation_versions; $replaces = array(); $deletes = array(); // D'abord, vider les fragments inutiles sql_delete("spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min>=$version_debut AND version_max<=$version_fin"); // Fragments chevauchant l'ensemble de l'intervalle, s'ils existent $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min<$version_debut AND version_max>$version_fin"); while ($row = sql_fetch($result)) { $id_fragment = $row['id_fragment']; $fragment = $row['fragment']; if ($row['compress'] > 0) { $fragment = gzuncompress($fragment); } $fragment = unserialize($fragment); for ($i = $version_fin; $i >= $version_debut; $i--) { if (isset($fragment[$i])) { // Recopier le dernier fragment si implicite if (!isset($fragment[$version_fin + 1])) { $fragment[$version_fin + 1] = $fragment[$i]; } unset($fragment[$i]); } } $replaces[] = replace_fragment($id_objet, $objet, $row['version_min'], $row['version_max'], $id_fragment, $fragment); } // Fragments chevauchant le debut de l'intervalle, s'ils existent $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments", "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"); $deb_fragment = array(); while ($row = sql_fetch($result)) { $id_fragment = $row['id_fragment']; $fragment = $row['fragment']; $version_min = $row['version_min']; $version_max = $row['version_max']; if ($row['compress'] > 0) { $fragment = gzuncompress($fragment); } $fragment = unserialize($fragment); for ($i = $version_debut; $i <= $version_max; $i++) { if (isset($fragment[$i])) { unset($fragment[$i]); } } // Stocker temporairement le fragment pour agregation $deb_fragment[$id_fragment] = $fragment; // Ajuster l'intervalle des versions $deb_version_min[$id_fragment] = $version_min; $deb_version_max[$id_fragment] = $version_debut - 1; } // Fragments chevauchant la fin de l'intervalle, s'ils existent $result = sql_select("id_fragment, compress, fragment, version_min, version_max", "spip_versions_fragments", "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"); while ($row = sql_fetch($result)) { $id_fragment = $row['id_fragment']; $fragment = $row['fragment']; $version_min = $row['version_min']; $version_max = $row['version_max']; if ($row['compress'] > 0) { $fragment = gzuncompress($fragment); } $fragment = unserialize($fragment); for ($i = $version_fin; $i >= $version_min; $i--) { if (isset($fragment[$i])) { // Recopier le dernier fragment si implicite if (!isset($fragment[$version_fin + 1])) { $fragment[$version_fin + 1] = $fragment[$i]; } unset($fragment[$i]); } } // Virer l'ancien enregistrement (la cle primaire va changer) $deletes[] = "id_fragment=$id_fragment AND version_min=$version_min"; // Essayer l'agregation $agreger = false; if (isset($deb_fragment[$id_fragment])) { $agreger = (count($deb_fragment[$id_fragment]) + count($fragment) <= $agregation_versions); if ($agreger) { $fragment = $deb_fragment[$id_fragment] + $fragment; $version_min = $deb_version_min[$id_fragment]; } else { $replaces[] = replace_fragment($id_objet, $objet, $deb_version_min[$id_fragment], $deb_version_max[$id_fragment], $id_fragment, $deb_fragment[$id_fragment]); } unset($deb_fragment[$id_fragment]); } if (!$agreger) { // Ajuster l'intervalle des versions $version_min = $version_fin + 1; } $replaces[] = replace_fragment($id_objet, $objet, $version_min, $version_max, $id_fragment, $fragment); } // Ajouter fragments restants if (is_array($deb_fragment) && count($deb_fragment) > 0) { foreach ($deb_fragment as $id_fragment => $fragment) { $replaces[] = replace_fragment($id_objet, $objet, $deb_version_min[$id_fragment], $deb_version_max[$id_fragment], $id_fragment, $deb_fragment[$id_fragment]); } } envoi_replace_fragments($replaces); envoi_delete_fragments($id_objet, $objet, $deletes); } /** * Récupérer les fragments d'un objet pour une version demandée * * @param int $id_objet Identifiant de l'objet * @param string $objet Objet * @param int $id_version Identifiant de la version * @return array Couples id_fragment => texte */ function recuperer_fragments($id_objet, $objet, $id_version) { $fragments = array(); if ($id_version == 0) { return array(); } $result = sql_select( "id_fragment, version_min, version_max, compress, fragment", "spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND version_min<=$id_version AND version_max>=$id_version"); while ($row = sql_fetch($result)) { $id_fragment = $row['id_fragment']; $version_min = $row['version_min']; $fragment = $row['fragment']; // si le fragment est compressé, tenter de le décompresser, sinon écrire une erreur if ($row['compress'] > 0) { $fragment_ = @gzuncompress($fragment); if (strlen($fragment) && $fragment_ === false) { $fragment = serialize(array($row['version_max'] => "[" . _T('forum_titre_erreur') . $id_fragment . "]")); } else { $fragment = $fragment_; } } // tenter dedésérialiser le fragment, sinon écrire une erreur $fragment_ = unserialize($fragment); if (strlen($fragment) && $fragment_ === false) { $fragment = array($row['version_max'] => "[" . _T('forum_titre_erreur') . $id_fragment . "]"); } else { $fragment = $fragment_; } // on retrouve le fragment le plus près de notre version for ($i = $id_version; $i >= $version_min; $i--) { if (isset($fragment[$i])) { ## hack destine a sauver les archives des sites iso-8859-1 ## convertis en utf-8 (les archives ne sont pas converties ## mais ce code va les nettoyer ; pour les autres charsets ## la situation n'est pas meilleure ni pire qu'avant) if ($GLOBALS['meta']['charset'] == 'utf-8' and include_spip('inc/charsets') and !is_utf8($fragment[$i]) ) { $fragment[$i] = importer_charset($fragment[$i], 'iso-8859-1'); } $fragments[$id_fragment] = $fragment[$i]; // quitter la boucle dès le premier touvé. break; } } } return $fragments; } // // Apparier des paragraphes deux a deux entre une version originale // et une version modifiee // // https://code.spip.net/@apparier_paras function apparier_paras($src, $dest, $flou = true) { $src_dest = array(); $dest_src = array(); $t1 = $t2 = array(); $md1 = $md2 = array(); $gz_min1 = $gz_min2 = array(); $gz_trans1 = $gz_trans2 = array(); $l1 = $l2 = array(); // Nettoyage de la ponctuation pour faciliter l'appariement foreach ($src as $key => $val) { $t1[$key] = strval(preg_replace("/[[:punct:][:space:]]+/", " ", $val)); } foreach ($dest as $key => $val) { $t2[$key] = strval(preg_replace("/[[:punct:][:space:]]+/", " ", $val)); } // Premiere passe : chercher les correspondance exactes foreach ($t1 as $key => $val) { $md1[$key] = md5($val); } foreach ($t2 as $key => $val) { $md2[md5($val)][$key] = $key; } foreach ($md1 as $key1 => $h) { if (isset($md2[$h])) { $key2 = reset($md2[$h]); if (isset($t1[$key1]) and isset($t2[$key2]) and $t1[$key1] == $t2[$key2]) { $src_dest[$key1] = $key2; $dest_src[$key2] = $key1; unset($t1[$key1]); unset($t2[$key2]); unset($md2[$h][$key2]); } } } if ($flou) { // Deuxieme passe : recherche de correlation par test de compressibilite foreach ($t1 as $key => $val) { $l1[$key] = strlen(gzcompress($val)); } foreach ($t2 as $key => $val) { $l2[$key] = strlen(gzcompress($val)); } foreach ($t1 as $key1 => $s1) { foreach ($t2 as $key2 => $s2) { $r = strlen(gzcompress($s1 . $s2)); $taux = 1.0 * $r / ($l1[$key1] + $l2[$key2]); if (!isset($gz_min1[$key1]) || !$gz_min1[$key1] || $gz_min1[$key1] > $taux) { $gz_min1[$key1] = $taux; $gz_trans1[$key1] = $key2; } if (!isset($gz_min2[$key2]) || !$gz_min2[$key2] || $gz_min2[$key2] > $taux) { $gz_min2[$key2] = $taux; $gz_trans2[$key2] = $key1; } } } // Depouiller les resultats de la deuxieme passe : // ne retenir que les correlations reciproques foreach ($gz_trans1 as $key1 => $key2) { if ($gz_trans2[$key2] == $key1 && $gz_min1[$key1] < 0.9) { $src_dest[$key1] = $key2; $dest_src[$key2] = $key1; } } } // Retourner les mappings return array($src_dest, $dest_src); } /** * Retrouve les champs d'un objet et leurs contenus à une version donnée * * @uses recuperer_version() * @uses liste_champs_versionnes() * @uses retrouver_champ_version_objet() * * @param int $id_objet Identifiant de l'objet * @param string $objet Objet * @param int $id_version Identifiant de la version * @return array Couples champs => textes **/ function recuperer_version_complete($id_objet, $objet, $id_version) { if (!$id_version or !$id_objet or !$objet) { return array(); } include_spip('inc/suivi_versions'); // champs modifiés à la version voulue $textes = recuperer_version($id_objet, $objet, $id_version); // tous les champs possibles versionnés pour l'objet $champs = liste_champs_versionnes(table_objet_sql($objet)); foreach ($champs as $champ) { // Remonter dans le temps pour trouver le champ en question pour chaque version retrouver_champ_version_objet($objet, $id_objet, $id_version, $champ, $textes); } return $textes; } /** * Récupérer les champs d'un objet, pour une version demandée * * @param int $id_objet Identifiant de l'objet * @param string $objet Objet * @param int $id_version Identifiant de la version * @return array Couples champs => textes */ function recuperer_version($id_objet, $objet, $id_version) { $champs = sql_getfetsel("champs", "spip_versions", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version=" . intval($id_version)); if (!$champs or !is_array($champs = unserialize($champs))) { return array(); } else { return reconstuire_version($champs, recuperer_fragments($id_objet, $objet, $id_version)); } } /** * Reconstruire une version donnée * * À partir de la liste des champs et de fragments, * retourne le texte de chaque champ. * * @param array $champs Couples (champ => liste d'id_fragment). * La liste est de la forme "5 32 7 16 8 2" * @param array $fragments Couples (id_fragment => texte) * @param array $res Couples (champ => texte) déjà connus * @return array Couples (champ => texte) */ function reconstuire_version($champs, $fragments, $res = array()) { static $msg; if (!$msg) { $msg = _T('forum_titre_erreur'); } foreach ($champs as $nom => $code) { if (!isset($res[$nom])) { $t = ''; foreach (array_filter(explode(' ', $code)) as $id) { $t .= isset($fragments[$id]) ? $fragments[$id] : "[$msg$id]"; } $res[$nom] = $t; } } return $res; } // https://code.spip.net/@supprimer_versions function supprimer_versions($id_objet, $objet, $version_min, $version_max) { sql_delete("spip_versions", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version>=$version_min AND id_version<=$version_max"); supprimer_fragments($id_objet, $objet, $version_min, $version_max); } /** * Ajouter une version à un objet éditorial * * @param int $id_objet * @param string $objet * @param array $champs * @param string $titre_version * Titre donné aux modifications apportées * @param int|null $id_auteur * Auteur apportant les modifications. En absence (session anonyme), utilisera l'IP pour garder une trace. * @return int * id_version : identifiant de la version **/ function ajouter_version($id_objet, $objet, $champs, $titre_version = "", $id_auteur = null) { $paras = $paras_old = $paras_champ = $fragments = array(); // Attention a une edition anonyme (type wiki): id_auteur n'est pas // definie, on enregistre alors le numero IP $str_auteur = intval($id_auteur) ? intval($id_auteur) : $GLOBALS['ip']; // si pas de titre dans cette version, la marquer 'non' permanente, // et elle pourra etre fusionnee avec une revision ulterieure dans un delai < _INTERVALLE_REVISIONS // permet de fusionner plusieurs editions consecutives champs par champs avec les crayons $permanent = empty($titre_version) ? 'non' : ''; // Detruire les tentatives d'archivages non abouties en 1 heure sql_delete('spip_versions', array( "id_objet=" . intval($id_objet), "objet=" . sql_quote($objet), "id_version <= 0", "date < DATE_SUB(" . sql_quote(date('Y-m-d H:i:s')) . ", INTERVAL " . _INTERVALLE_REVISIONS . " SECOND)" ) ); // Signaler qu'on opere en mettant un numero de version negatif // distinctif (pour eviter la violation d'unicite) // et un titre contenant en fait le moment de l'insertion list($ms, $sec) = explode(' ', microtime()); $date = $sec . substr($ms, 1, 4) - 20; // SQL ne ramene que 4 chiffres significatifs apres la virgule pour 0.0+titre_version $datediff = ($sec - mktime(0, 0, 0, 9, 1, 2007)) * 1000000 + substr($ms, 2, strlen($ms) - 4); $valeurs = array( 'id_objet' => $id_objet, 'objet' => $objet, 'id_version' => (0 - $datediff), 'date' => date('Y-m-d H:i:s'), 'id_auteur' => $str_auteur, // varchar ici! 'titre_version' => $date ); sql_insertq('spip_versions', $valeurs); // Eviter les validations entremelees en s'endormant s'il existe // une version <0 plus recente mais pas plus vieille que 10s // Une <0 encore plus vieille est une operation avortee, // on passe outre (vaut mieux archiver mal que pas du tout). // Pour tester: // 0. mettre le delai a 30 // 1. decommenter le premier sleep(15) // 2. enregistrer une modif // 3. recommenter le premier sleep(15), decommenter le second. // 4. enregistrer une autre modif dans les 15 secondes # sleep(15); $delai = $sec - 10; while (sql_countsel('spip_versions', "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, '', 'text') . " AND 0.0+titre_version > $delai")) { spip_log("version $objet $id_objet :insertion en cours avant $date ($delai)", 'revisions'); sleep(1); $delai++; } # sleep(15); spip_log("sortie $sec $delai"); // Determiner le numero du prochain fragment $next = sql_fetsel("id_fragment", "spip_versions_fragments", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet), "", "id_fragment DESC", "1"); $onlylock = ''; // Examiner la derniere version $row = sql_fetsel("id_version, champs, id_auteur, date, permanent", "spip_versions", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version > 0", '', "id_version DESC", "1"); // le champ id_auteur est un varchar dans cette table if ($row) { $id_version = $row['id_version']; $paras_old = recuperer_fragments($id_objet, $objet, $id_version); $champs_old = $row['champs']; if ($row['id_auteur'] != $str_auteur or $row['permanent'] != 'non' or strtotime($row['date']) < (time() - _INTERVALLE_REVISIONS) ) { spip_log(strtotime($row['date']), 'revisions'); spip_log(time(), 'revisions'); spip_log(_INTERVALLE_REVISIONS, 'revisions'); $id_version++; } // version precedente recente, on va la mettre a jour // avec les nouveaux arrivants si presents else { $champs = reconstuire_version(unserialize($champs_old), $paras_old, $champs); $onlylock = 're'; } } else { $id_version = 1; } $next = !$next ? 1 : ($next['id_fragment'] + 1); // Generer les nouveaux fragments $codes = array(); foreach ($champs as $nom => $texte) { $codes[$nom] = array(); $paras = separer_paras($texte, $paras); $paras_champ[$nom] = count($paras); } // Apparier les fragments de maniere optimale $n = count($paras); if ($n) { // Tables d'appariement dans les deux sens list(, $trans) = apparier_paras($paras_old, $paras); reset($champs); $nom = ''; // eviter une notice PHP au tout debut de la boucle // on ajoute ''=>0 en debut de tableau. $paras_champ = array($nom => 0) + $paras_champ; for ($i = 0; $i < $n; $i++) { while ($i >= $paras_champ[$nom]) { list($nom, ) = each($champs); } // Lier au fragment existant si possible, sinon creer un nouveau fragment $id_fragment = isset($trans[$i]) ? $trans[$i] : $next++; $codes[$nom][] = $id_fragment; $fragments[$id_fragment] = $paras[$i]; } } foreach ($champs as $nom => $t) { $codes[$nom] = join(' ', $codes[$nom]); # avec la ligne qui suit, un champ qu'on vide ne s'enregistre pas # if (!strlen($codes[$nom])) unset($codes[$nom]); } // Enregistrer les modifications ajouter_fragments($id_objet, $objet, $id_version, $fragments); // Si l'insertion ne servait que de verrou, // la detruire apres mise a jour de l'ancienne entree, // sinon la mise a jour efface en fait le verrou. if (!$onlylock) { sql_updateq('spip_versions', array( 'id_version' => $id_version, 'date' => date('Y-m-d H:i:s'), 'champs' => serialize($codes), 'permanent' => $permanent, 'titre_version' => $titre_version ), "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version < 0 AND titre_version='$date'"); } else { sql_updateq('spip_versions', array( 'date' => date('Y-m-d H:i:s'), 'champs' => serialize($codes), 'permanent' => $permanent, 'titre_version' => $titre_version ), "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version=$id_version"); sql_delete("spip_versions", "id_objet=" . intval($id_objet) . " AND objet=" . sql_quote($objet) . " AND id_version < 0 AND titre_version ='$date'"); } spip_log($onlylock . "memorise la version $id_version de l'objet $objet $id_objet $titre_version", 'revisions'); return $id_version; } // les textes "diff" ne peuvent pas passer dans propre directement, // car ils contiennent des et
parfois mal places // https://code.spip.net/@propre_diff function propre_diff($texte) { $span_diff = array(); if (preg_match_all(',<(/)?(span|div) (class|rem)="diff-[^>]*>,', $texte, $regs, PREG_SET_ORDER)) { $regs = array_slice($regs, 0, 500); #limiter la casse s'il y en a trop foreach ($regs as $c => $reg) { $texte = str_replace($reg[0], '@@@SPIP_DIFF' . $c . '@@@', $texte); } } // [ ... -> lien ] // < tag > $texte = preg_replace(',<([^>]*?@@@SPIP_DIFF[0-9]+@@@),', '<\1', $texte); # attention ici astuce seulement deux @@ finals car on doit eviter # deux patterns a suivre, afin de pouvoir prendre [ mais eviter [[ $texte = preg_replace(',(^|[^[])[[]([^[\]]*@@@SPIP_DIFF[0-9]+@@),', '\1[\2', $texte); // desactiver TeX & toujours-paragrapher $tex = $GLOBALS['traiter_math']; $GLOBALS['traiter_math'] = ''; $mem = $GLOBALS['toujours_paragrapher']; $GLOBALS['toujours_paragrapher'] = false; $texte = propre($texte); // retablir $GLOBALS['traiter_math'] = $tex; $GLOBALS['toujours_paragrapher'] = $mem; // un blockquote mal ferme peut gener l'affichage, et title plante safari $texte = preg_replace(',<(/?(blockquote|title)[^>]*)>,i', '<\1>', $texte); // Dans les c'est un peu plus complique if (preg_match_all(',