<?php

/**
 * SPIP, Système de publication pour l'internet
 *
 * Copyright © avec tendresse depuis 2001
 * Arnaud Martin, Antoine Pitrou, Philippe Rivière, Emmanuel Saint-James
 *
 * Ce programme est un logiciel libre distribué sous licence GNU/GPL.
 */

/**
 * Fonctions utilitaires du plugin révisions
 *
 * @package SPIP\Revisions\Fonctions
 */
if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}

$GLOBALS['agregation_versions'] = 10;

/** Intervalle de temps (en seconde) separant deux révisions par un même auteur */
if (!defined('_INTERVALLE_REVISIONS')) {
	define('_INTERVALLE_REVISIONS', 600);
}

/**
 * Découper les paragraphes d'un texte en fragments
 *
 * @param string $texte Texte à fragmenter
 * @param array $paras Tableau de fragments déjà là
 * @return string[]      Tableau de fragments (paragraphes)
 */
function separer_paras($texte, $paras = []) {
	if (!$paras) {
		$paras = [];
	}
	while (preg_match("/(\r\n?){2,}|\n{2,}/", $texte, $regs)) {
		$p = strpos($texte, (string) $regs[0]) + strlen($regs[0]);
		$paras[] = substr($texte, 0, $p);
		$texte = substr($texte, $p);
	}
	if ($texte) {
		$paras[] = $texte;
	}

	return $paras;
}

function replace_fragment($id_objet, $objet, $version_min, $version_max, $id_fragment, $fragment) {
	$fragment = serialize($fragment);
	$compress = 0;

	/* On ne compresse plus les fragments car probleme de portabilite de base, corruptions de donnees
	en backup SQLite ou meme en mysqldump */
	// pour le portage en PG il faut l'equivalente au mysql_escape_string
	// et deporter son appel dans les fonctions d'abstraction.
	/*
	if (function_exists('gzcompress')
	AND $GLOBALS['connexions'][0]['type'] == 'mysql') {
		$s = gzcompress($fragment);
		if (strlen($s) < strlen($fragment)) {
			# spip_log("gain gz: ".intval(100 - 100 * strlen($s) / strlen($fragment)),'revisions');
			$compress = 1;
			$fragment = $s;
		}
	}
	*/

	// Attention a echapper $fragment, binaire potentiellement gz
	return [
		'id_objet' => intval($id_objet),
		'objet' => $objet,
		'id_fragment' => intval($id_fragment),
		'version_min' => intval($version_min),
		'version_max' => intval($version_max),
		'compress' => $compress,
		'fragment' => $fragment,
	];
}

function envoi_replace_fragments($replaces) {
	$desc = $GLOBALS['tables_auxiliaires']['spip_versions_fragments'];
	foreach ($replaces as $r) {
		sql_replace('spip_versions_fragments', $r, $desc);
	}
}

function envoi_delete_fragments($id_objet, $objet, $deletes) {
	if (is_countable($deletes) ? count($deletes) : 0) {
		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)
//
function ajouter_fragments($id_objet, $objet, $id_version, $fragments) {
	global $agregation_versions;

	$replaces = [];
	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 = [$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)
//
function supprimer_fragments($id_objet, $objet, $version_debut, $version_fin) {
	$deb_version_min = [];
	$deb_version_max = [];
	global $agregation_versions;

	$replaces = [];
	$deletes = [];

	// 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 = [];
	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 = ((is_countable($deb_fragment[$id_fragment]) ? count($deb_fragment[$id_fragment]) : 0) + (is_countable($fragment) ? count($fragment) : 0) <= $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 = [];

	if ($id_version == 0) {
		return [];
	}

	$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([$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 = [$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
//
function apparier_paras($src, $dest, $flou = true) {
	$src_dest = [];
	$dest_src = [];

	$t1 = $t2 = [];

	$md1 = $md2 = [];
	$gz_min1 = $gz_min2 = [];
	$gz_trans1 = $gz_trans2 = [];
	$l1 = $l2 = [];

	// 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 [$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 [];
	}

	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 [];
	} 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 = []) {

	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 .= $fragments[$id] ?? "[$msg$id]";
			}
			$res[$nom] = $t;
		}
	}

	return $res;
}

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 = [];

	// Attention a une edition anonyme (type wiki): id_auteur n'est pas
	// definie, on enregistre alors le numero IP
	$str_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', [
		'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
	[$ms, $sec] = explode(' ', microtime());
	// SQL ne ramene que 4 chiffres significatifs apres la virgule pour 0.0+titre_version
	$date = ($sec . substr($ms, 1, 4)) - 20;
	$datediff = ($sec - mktime(0, 0, 0, 9, 1, 2007)) * 1_000_000 + substr($ms, 2, strlen($ms) - 4);

	$valeurs = [
		'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 = [];
	foreach ($champs as $nom => $texte) {
		$codes[$nom] = [];
		$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
		[, $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 = [$nom => 0] + $paras_champ;

		for ($i = 0; $i < $n; $i++) {
			while ($i >= $paras_champ[$nom]) {
				$nom = key($champs);
				next($champs);
			}
			// Lier au fragment existant si possible, sinon creer un nouveau fragment
			$id_fragment = $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',
			[
				'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', [
			'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 <span> et <div> parfois mal places
function propre_diff($texte) {

	$span_diff = [];
	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);
		}
	}

	// [ ...<span diff> -> lien ]
	// < tag <span diff> >
	$texte = preg_replace(
		',<([^>]*?@@@SPIP_DIFF[0-9]+@@@),',
		'&lt;\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&#91;\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', '&lt;\1>', $texte);

	// Dans les <cadre> c'est un peu plus complique
	if (preg_match_all(',<textarea (.*)</textarea>,Uims', $texte, $area, PREG_SET_ORDER)) {
		foreach ($area as $reg) {
			$remplace = preg_replace(',@@@SPIP_DIFF[0-9]+@@@,', '**', $reg[0]);
			if ($remplace != $reg[0]) {
				$texte = str_replace($reg[0], $remplace, $texte);
			}
		}
	}

	// replacer les valeurs des <span> et <div> diff-
	if ($regs && is_array($regs)) {
		foreach ($regs as $c => $reg) {
			$bal = (!$reg[1]) ? $reg[0] : "</$reg[2]>";
			$texte = str_replace('@@@SPIP_DIFF' . $c . '@@@', $bal, $texte);
			$GLOBALS['les_notes'] = str_replace('@@@SPIP_DIFF' . $c . '@@@', $bal, $GLOBALS['les_notes']);
		}
		// quand le dernier tag est ouvrant le refermer ...
		$reg = end($regs);
		if (!$reg[1] and $reg[2]) {
			$texte .= "</$reg[2]>";
		}
	}

	// et interdire_scripts !
	$texte = interdire_scripts($texte);

	return $texte;
}

/**
 * Liste les champs versionnés d'une table objet.
 *
 * @param string $table
 *     Nom complet de sa table sql. Exemple 'spip_articles'
 * @return array
 *     Liste des champs versionnés
 */
function liste_champs_versionnes($table) {
	include_spip('inc/config');
	$liste_objets_versionnes = lire_config('objets_versions/', []);
	$liste_objets_versionnes = (is_array($liste_objets_versionnes) ? $liste_objets_versionnes : []);

	if (!in_array($table, $liste_objets_versionnes)) {
		return [];
	}

	include_spip('base/objets');
	if (
		$infos = lister_tables_objets_sql($table)
		and isset($infos['champs_versionnes'])
	) {
		return $infos['champs_versionnes'];
	}

	return [];
}

/**
 * Lorsqu'un champ versionée est une jointure, récuperer tous les liens
 * et les mettre sous forme de liste énumérée
 *
 * @param string $objet
 * @param string $id_objet
 * @param string $jointure
 * @return string
 */
function recuperer_valeur_champ_jointure($objet, $id_objet, $jointure) {
	$objet_joint = objet_type($jointure);
	include_spip('action/editer_liens');
	$v = [];
	if (objet_associable($objet_joint)) {
		$liens = objet_trouver_liens([$objet_joint => '*'], [$objet => $id_objet]);
		foreach ($liens as $l) {
			$v[] = $l[$objet_joint];
		}
	} elseif (objet_associable($objet)) {
		$liens = objet_trouver_liens([$objet => $id_objet], [$objet_joint => '*']);
		foreach ($liens as $l) {
			$v[] = $l[$objet];
		}
	}
	sort($v);

	return implode(',', $v);
}

/**
 * Créer la première révision d'un objet si nécessaire
 *
 * À faire notamment si on vient d'activer l'extension et qu'on fait une modif
 * sur un objet qui était déjà en base, mais non versionné
 *
 * La fonction renvoie le numéro de la dernière version de l'objet,
 * et 0 si pas de version pour cet objet
 *
 * @param string $table
 * @param string $objet
 * @param int $id_objet
 * @param array $champs
 * @param int $id_auteur
 * @return int
 */
function verifier_premiere_revision($table, $objet, $id_objet, $champs = null, $id_auteur = 0) {

	$id_table_objet = id_table_objet($objet);
	if (!$champs) {
		$champs = liste_champs_versionnes($table);
	}
	if (!$champs) {
		return false;
	}

	if (
		!$id_version = sql_getfetsel(
			'id_version',
			'spip_versions',
			'id_objet=' . intval($id_objet) . ' AND objet=' . sql_quote($objet),
			'',
			'id_version DESC',
			'0,1'
		)
	) {
		// recuperer toutes les valeurs actuelles des champs
		// pour l'objet
		$originaux = sql_fetsel('*', $table, "$id_table_objet=" . intval($id_objet));
		$premiere = false;
		$champs_originaux = [];

		foreach ($champs as $v) {
			if (isset($originaux[$v])) {
				$champs_originaux[$v] = $originaux[$v];
			} elseif (strncmp($v, 'jointure_', 9) == 0) {
				$champs_originaux[$v] = recuperer_valeur_champ_jointure($objet, $id_objet, substr($v, 9));
			}
			if (isset($champs_originaux[$v]) and isset($originaux[$v]) and strlen($originaux[$v])) {
				$premiere = true;
			}
		}

		// Si un champ est non vide,
		// il faut creer une premiere revision
		if ($premiere) {
			$trouver_table = charger_fonction('trouver_table', 'base');
			$desc = $trouver_table($table);

			// "trouver" une date raisonnable pour la version initiale

			$date_modif = '';
			foreach (['date_modif', 'maj'] as $d) {
				if (!$date_modif and isset($originaux[$d]) and $t = strtotime($originaux[$d])) {
					$date_modif = date('Y-m-d H:i:s', $t - 20);
				}
			}
			if (
				!$date_modif
				and isset($desc['date'])
				and isset($originaux[$desc['date']])
			) {
				$date_modif = $originaux[$desc['date']];
			} elseif (!$date_modif) {
				$date_modif = date('Y-m-d H:i:s', time() - 20);
			}

			if (
				$id_version = ajouter_version(
					$id_objet,
					$objet,
					$champs_originaux,
					_T('revisions:version_initiale'),
					$id_auteur
				)
			) {
				sql_updateq(
					'spip_versions',
					['date' => $date_modif],
					'id_objet=' . intval($id_objet) . ' AND objet=' . sql_quote($objet) . " AND id_version=$id_version"
				);
			}
		}
	}

	return $id_version;
}
