<?php

/**
 * Fichier permettant de phraser les XML des fichiers paquet.xml et plugin.xml
 * ainsi que des fichiers décrivant le contenu d'un dépot de paquets.
 *
 * @plugin SVP pour SPIP
 * @license GPL
 * @package SPIP\SVP\Plugins
 */

if (!defined('_ECRIRE_INC_VERSION')) {
	return;
}

include_spip('inc/xml');
include_spip('inc/config');

if (!defined('_SVP_MODE_RUNTIME')) {
	/**
	 * Mode d'utilisation de SVP runtime ou pas :
	 * - En mode runtime (true), on ne charge que les plugins compatibles avec la version courante
	 * - En mode non runtime (false) on charge tous les plugins : cas du site Plugins SPIP
	 * Runtime est le mode par defaut
	 *
	 * @var bool
	 */
	define('_SVP_MODE_RUNTIME', (lire_config('svp/mode_runtime', 'oui') == 'oui' ? true : false));
}

// Type parseur XML à appliquer pour récupérer les infos du plugin
/** @var string Phraseur à utiliser pour un XML de plugin.xml */
define('_SVP_DTD_PLUGIN', 'plugin');
/** @var string Phraseur à utiliser pour un XML de paquet.xml */
define('_SVP_DTD_PAQUET', 'paquet');

// Regexp de recherche des balises principales de archives.xml
define('_SVP_REGEXP_BALISE_DEPOT', '#<depot[^>]*>(.*)</depot>#Uims');
define('_SVP_REGEXP_BALISE_ARCHIVES', '#<archives[^>]*>(.*)</archives>#Uims');
define('_SVP_REGEXP_BALISE_ARCHIVE', '#<archive[^s][^>]*>.*</archive>#Uims');
define('_SVP_REGEXP_BALISE_ZIP', '#<zip[^>]*>(.*)</zip>#Uims');
define('_SVP_REGEXP_BALISE_TRADUCTIONS', '#<traductions[^>]*>(.*)</traductions>#Uims');
define('_SVP_REGEXP_BALISE_PLUGIN', '#<plugin[^>]*>(.*)</plugin>#Uims');
define('_SVP_REGEXP_BALISE_PAQUET', '#<paquet[^>]*>(.*)</paquet>#Uims');
define('_SVP_REGEXP_BALISE_MULTIS', '#<multis[^>]*>(.*)</multis>#Uims');

/** Liste des balises techniques autorisées dans la balise <spip> */
$GLOBALS['balises_techniques'] = [
	'menu',
	'chemin',
	'lib',
	'necessite',
	'onglet',
	'procure',
	'pipeline',
	'utilise',
	'options',
	'fonctions',
	'install',
];
# define('_BALISES_TECHNIQUES', serialize($balises_techniques));

/** Liste des balises autorisant une traduction */
$GLOBALS['balises_multis'] = [
	'nom',
	'slogan',
	'description',
];
# define('_BALISES_MULTIS', serialize($balises_multis));

/**
 * Phrase un fichier décrivant un dépot, dont le chemin local est donné
 *
 * Le fichier est au format XML et contient deux balises principales :
 * - <depot>...</depot> : informations de description du depot (facultatif)
 * - <archives>...</archives> : liste des informations sur chaque archive (obligatoire)
 *
 * La fonction met en cache le résultat du phrasage de chaque archive et ne
 * rephrase que les archives ayant changées.
 *
 * @uses  svp_aplatir_balises()
 * @uses  svp_phraser_archives()
 * @param string $fichier_xml
 *     Chemin local du fichier XML de description du dépot
 * @return array|bool
 *     false si erreur,
 *     Tableau de 2 index sinon :
 *     - depot : description du dépot
 *     - paquets :
 */
function svp_phraser_depot($fichier_xml) {

	$xml = null;
	$cache_md5 = null;
	// le fichier xml fournit sous forme de fichier
	lire_fichier($fichier_xml, $xml);

	// Initialisation du tableau des informations
	// -- Si aucun bloc depot n'est trouve le titre et le type prennent une valeur par defaut
	$infos = [
		'depot' => [
			'titre' => _T('svp:titre_nouveau_depot'),
			'type' => 'manuel',
		],
		'paquets' => [],
	];

	// Extraction et phrasage du bloc depot si il existe
	// -- Si le bloc <depot> n'est pas renseigne on ne considere pas cela comme une erreur
	$balises_depot = ['titre', 'descriptif', 'type', 'url_serveur', 'url_brouteur', 'url_archives', 'url_commits'];
	if (preg_match(_SVP_REGEXP_BALISE_DEPOT, $xml, $matches)) {
		if (is_array($arbre_depot = spip_xml_parse($matches[1]))) {
			$infos['depot'] = svp_aplatir_balises($balises_depot, $arbre_depot, 'nonvide', $infos['depot']);
		}
	}

	// Extraction et phrasage du bloc des archives si il existe
	// -- Le bloc <archives> peut etre une chaine de grande taille et provoquer une erreur
	// sur une recherche de regexp. On ne teste donc pas l'existence de cette balise
	// -- Si aucun bloc <archive> c'est aussi une erreur
	if (!preg_match_all(_SVP_REGEXP_BALISE_ARCHIVE, $xml, $matches)) {
		return false;
	}
	// plus besoin de ce (potentiellement) gros truc en mémoire
	unset($xml);

	// lire le cache des md5 pour ne parser que ce qui a change
	$fichier_xml_md5 = $fichier_xml . '.md5.txt';
	lire_fichier($fichier_xml_md5, $cache_md5);
	if (
		!$cache_md5
		or !$cache_md5 = unserialize($cache_md5)
	) {
		$cache_md5 = [];
	}

	$matches = $matches[0];
	$infos['paquets'] = svp_phraser_archives($matches, $cache_md5);
	ecrire_fichier($fichier_xml_md5, serialize($cache_md5));

	// -- Si aucun paquet extrait c'est aussi une erreur
	if (empty($infos['paquets'])) {
		return false;
	}

	return $infos;
}

/**
 * Phrase la liste des balises <archive>
 *
 * Chaque bloc XML est constitue de 3 sous-blocs principaux :
 * - <zip> : contient les balises d'information sur le zip (obligatoire)
 * - <traductions> : contient la compilation des informations de traduction (facultatif)
 * - <plugin> ou <paquet> suivant la DTD : le contenu du fichier plugin.xml ou paquet.xml (facultatif)
 *
 * @uses  svp_phraser_zip()
 * @uses  svp_phraser_traductions()
 * @uses  svp_phraser_plugin()
 * @uses  plugin_version_compatible()
 * @param array $archives
 *     Tableau de la liste des archives trouvées dans la description d'un dépot
 * @param array $md5_cache
 *     Tableau des descriptions d'archives déjà connues : on supprime
 *     à la fin celles qui ne font plus parties du dépot.
 * @return array
 *     Tableau décrivant chaque archive, avec en index l'url de l'archive.
 *     Tableau (url => Tableau de description de l'archive)
 */
function svp_phraser_archives(&$archives, &$md5_cache = []) {
	include_spip('inc/plugin');
	$seen = [];

	$paquets = [];
	$version_spip = $GLOBALS['spip_version_branche'] . '.' . $GLOBALS['spip_version_code'];
	$version_spip_dev_compat = (defined('_DEV_VERSION_SPIP_COMPAT')
		? _DEV_VERSION_SPIP_COMPAT
		: $GLOBALS['spip_version_branche']);

	// On verifie qu'il existe au moins une archive
	if (!$archives) {
		return $paquets;
	}

	// On phrase chacune des archives
	// Seul le bloc <zip> est obligatoire
	while (!empty($archives)) {
		$_archive = array_shift($archives);
		// quand version spip ou mode runtime changent,
		// il faut mettre le xml a jour pour voir les plugins compatibles ou non
		$md5 = md5($_archive . ":$version_spip:$version_spip_dev_compat:" . _SVP_MODE_RUNTIME);
		if (isset($md5_cache[$md5])) {
			if (is_array($p = $md5_cache[$md5])) {
				// ce paquet est connu et parsé
				$paquets[$p['file']] = $p;
			}
			$seen[] = $md5;
		} elseif (preg_match(_SVP_REGEXP_BALISE_ZIP, $_archive, $matches)) {
			// Extraction de la balise <zip>
			$zip = svp_phraser_zip($matches[1]);

			if ($zip) {
				// Extraction de la balise traductions
				$traductions = [];
				if (preg_match(_SVP_REGEXP_BALISE_TRADUCTIONS, $_archive, $matches)) {
					$traductions = svp_phraser_traductions($matches[1]);
				}

				// La balise <archive> peut posseder un attribut qui precise la DTD utilisee pour les plugins (plugin ou paquet)
				// Sinon, c'est la DTD plugin qui est utilisee
				[$tag, $attributs] = spip_xml_decompose_tag($_archive);
				// -- On stocke la DTD d'extraction des infos du plugin
				$dtd = (isset($attributs['dtd']) and $attributs['dtd']) ? $attributs['dtd'] : _SVP_DTD_PLUGIN;

				// Extraction *des balises* plugin ou *de la balise* paquet suivant la DTD et la version SPIP
				// -- DTD : si on utilise plugin.xml on extrait la balise <plugin> sinon la balise <paquet>
				$xml = svp_phraser_plugin($dtd, $_archive);

				// Si on est en mode runtime, on est seulement interesse par les plugins compatibles avec
				// la version courant de SPIP. On ne stocke donc pas les autres plugins.
				// Si on est pas en mode runtime on prend tout !
				if (
					!_SVP_MODE_RUNTIME
					|| (
						_SVP_MODE_RUNTIME
						&& isset($xml['compatibilite'])
						&& plugin_version_compatible($xml['compatibilite'], $version_spip, 'spip')
					)
				) {
					$paquets[$zip['file']] = $zip;
					$paquets[$zip['file']]['traductions'] = $traductions;
					$paquets[$zip['file']]['dtd'] = $dtd;
					$paquets[$zip['file']]['plugin'] = $xml;
					$paquets[$zip['file']]['md5'] = $md5;
					$md5_cache[$md5] = $paquets[$zip['file']];
					$seen[] = $md5;
				} else {
					$md5_cache[$md5] = $zip['file'];
					$seen[] = $md5;
				}
			}
		}
	}

	// supprimer du cache les zip qui ne sont pas dans le nouveau $archives
	$oldies = array_diff(array_keys($md5_cache), $seen);
	foreach ($oldies as $old_md5) {
		unset($md5_cache[$old_md5]);
	}

	return $paquets;
}

/**
 * Phrase le contenu du XML décrivant une archive suivant une DTD
 * de plugin.xml ou de paquet.xml donnée
 *
 * La fonction peut-être appelée via archives.xml ou via un xml de plugin.
 * Elle phrase la balise <multi> dans le cas d'une DTD paquet qui contient
 * les traductions du nom, slogan et description
 *
 * @uses svp_aplatir_balises()
 *
 * @global $balises_multis
 * @static array $informer
 *
 * @param string $dtd
 *     Nom du type de dtd : plugin ou paquet (pour phraser un plugin.xml ou un paquet.xml)
 * @param string $contenu
 *     Contenu XML à phraser
 * @return array
 *     Description du plugin
 */
function svp_phraser_plugin($dtd, $contenu) {
	global $balises_multis;
	static $informer = [];

	$plugin = [];

	// On initialise les informations du plugin avec le contenu du plugin.xml ou paquet.xml
	$regexp = ($dtd == 'plugin') ? _SVP_REGEXP_BALISE_PLUGIN : _SVP_REGEXP_BALISE_PAQUET;
	if ($nb_balises = preg_match_all($regexp, $contenu, $matches)) {
		$plugins = [];
		// Pour chacune des occurences de la balise on extrait les infos
		foreach ($matches[0] as $_balise_plugin) {
			// Extraction des informations du plugin suivant le standard SPIP
			if (!isset($informer[$dtd])) {
				$informer[$dtd] = charger_fonction('infos_' . $dtd, 'plugins');
			}
			$plugins[] = $informer[$dtd]($_balise_plugin);
		}

		// On appelle systematiquement une fonction de mise a jour de la structure de donnees du plugin :
		// -- Si DTD plugin et que le nombre de balises plugin > 1 ou si DTD paquet avec une presence de balise spip
		//    alors on fusionne donc les informations recoltees
		// -- sinon on arrange la structure pour deplacer le contenu des balises dites techniques dans un sous tableau
		//    d'index 0 par similitude avec la structure fusionnee
		$fusionner = charger_fonction('fusion_' . $dtd, 'plugins');
		if ($dtd == 'plugin') {
			$plugin = $fusionner($plugins);
		} else {
			$plugin = $fusionner($plugins[0]);
		}

		// Durant la période où les XML contiennent encore l'attribut ou la balise categorie il faut la traiter
		// lors du phrasage mais la supprimer après la fusion afin d'éviter une erreur SQL lors de l'insertion.
		if (isset($plugin['categorie'])) {
			unset($plugin['categorie']);
		}

		// Pour la DTD paquet, les traductions du nom, slogan et description sont compilees dans une balise
		// du fichier archives.xml. Il faut donc completer les informations precedentes avec cette balise
		if (($dtd == _SVP_DTD_PAQUET) and (preg_match(_SVP_REGEXP_BALISE_MULTIS, $contenu, $matches))) {
			$multis = [];
			if (is_array($arbre = spip_xml_parse($matches[1]))) {
				$multis = svp_aplatir_balises($balises_multis, $arbre);
			}
			// Le nom peut etre traduit ou pas, il faut donc le tester
			if ($multis['nom']) {
				$plugin['nom'] = $multis['nom'];
			}
			// Slogan et description sont forcement des items de langue
			$plugin['slogan'] = $multis['slogan'];
			$plugin['description'] = $multis['description'];
		}
	}

	return $plugin;
}

/**
 * Phrase le contenu de la balise <zip>
 *
 * Extrait du XML les informations du zip
 *
 * @uses svp_aplatir_balises()
 *
 * @param string $contenu
 *     Description XML de l'archive
 * @return array
 *     Description du zip.
 *     - Index 'file' : nom du zip
 *     - Index 'size' : taille
 *     - Index 'date' : date de création
 *     - Index 'last_commit' : date du dernier commit
 *     - Index 'source' : arborescence relative des sources
 *     - Index 'logo' : nom du logo
 */
function svp_phraser_zip($contenu) {
	static $balises_zip = ['file', 'size', 'date', 'source', 'last_commit', 'logo'];

	$zip = [];
	if (is_array($arbre = spip_xml_parse($contenu))) {
		$zip = svp_aplatir_balises($balises_zip, $arbre);
	}

	return $zip;
}

/**
 * Phrase le contenu d'une balise <traductions> en un tableau plus
 * facilement utilisable
 *
 * @param string $contenu
 *     Contenu XML de la balise <traductions>
 * @return array
 *     Tableau complexe avec pour index les noms des modules de langue et pour
 *     valeur leur description. Chaque description contient dedans 3 index :
 *     - reference : la langue de référence
 *     - gestionnaire : quel logiciel à servi à gérer les traductions
 *     - langues : tableau classé par langue puis par traducteurs, qui indique
 *       l'ensemble des traducteurs pour chacune des langues présentes
 */
function svp_phraser_traductions($contenu) {

	$traductions = [];
	if (is_array($arbre = spip_xml_parse($contenu))) {
		foreach ($arbre as $_tag => $_langues) {
			// On commence par les balises <traduction> et leurs attributs
			[$tag, $attributs_traduction] = spip_xml_decompose_tag($_tag);
			$traductions[$attributs_traduction['module']]['reference'] = $attributs_traduction['reference'];
			$traductions[$attributs_traduction['module']]['gestionnaire'] = $attributs_traduction['gestionnaire'] ?? '';

			// On continue par les balises <langue> qui donnent le code en attribut
			// et les balises <traducteur> qui donnent uniquement le nom en attribut
			if (is_array($_langues[0])) {
				foreach ($_langues[0] as $_tag_trad => $_traducteurs) {
					[$tag, $attributs_langue] = spip_xml_decompose_tag($_tag_trad);
					$traducteurs = [];
					if (is_array($_traducteurs[0])) {
						foreach ($_traducteurs[0] as $_tag_lang => $_vide) {
							[$tag, $attributs_traducteur] = spip_xml_decompose_tag($_tag_lang);
							$traducteurs[] = $attributs_traducteur;
						}
					}
					$traductions[$attributs_traduction['module']]['langues'][$attributs_langue['code']] = $traducteurs;
				}
			}
		}
	}

	return $traductions;
}

/**
 * Aplatit plusieurs clés d'un arbre xml dans un tableau
 *
 * Effectue un trim() de la valeur trouvée dans l'arbre
 *
 * @uses  spip_xml_aplatit()
 *
 * @param array $balises
 *     Liste de noms de balises XML.
 *     Peut aussi être un tableau indiquant un renommage d'une balise
 *     au passage tel que 'x' => 'y' qui cherchera x dans l'arbre XML et
 *     l'applatira dans y.
 * @param array $arbre_xml
 *     Un arbre issu de spip_xml_parse()
 * @param string $mode
 *     Mode d'affectation des valeurs trouvées
 *     - 'vide_et_nonvide' : Affecte une chaine vide si la balise n'est
 *       pas trouvée dans l'arbre et affecte la valeur de la balise sinon.
 *     - 'nonvide' : Si la balise n'est pas trouvée dans l'arbre ou si son
 *       contenu est vide, affecte la valeur du tableau initial concernant
 *       cette balise si elle est connue.
 * @param array $tableau_initial
 *     Tableau initial pouvant contenir des valeurs par défaut à affecter
 *     à chaque balise avec 'x' => 'valeur'
 */
function svp_aplatir_balises($balises, $arbre_xml, $mode = 'vide_et_nonvide', $tableau_initial = []) {
	$tableau_aplati = [];

	if (!$balises) {
		return $tableau_initial;
	}

	foreach ($balises as $_cle => $_valeur) {
		$tag = (is_string($_cle)) ? $_cle : $_valeur;
		$valeur_aplatie = '';
		if (isset($arbre_xml[$tag])) {
			$valeur_aplatie = trim(spip_xml_aplatit($arbre_xml[$tag]));
		}
		if (
			($mode == 'vide_et_nonvide')
			or (($mode == 'nonvide') and $valeur_aplatie)
		) {
			$tableau_aplati[$_valeur] = $valeur_aplatie;
		} else {
			$tableau_aplati[$_valeur] = $tableau_initial[$_valeur] ?? '';
		}
	}

	return $tableau_aplati;
}
