La fabrique de caches pour plugins
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

525 lines
24 KiB

<?php
/**
* Ce fichier contient les fonctions de service du plugin Cache.
*
* Chaque fonction, soit aiguille, si elle existe, vers une fonction "homonyme" propre au plugin appelant
* soit déroule sa propre implémentation.
* Ainsi, les plugins externes peuvent utiliser, si elle leur convient, l'implémentation proposée par Cache Factory.
*
* @package SPIP\CACHE\SERVICE
*/
if (!defined('_ECRIRE_INC_VERSION')) {
return;
}
// -----------------------------------------------------------------------
// ---------------------- SERVICES SURCHARGEABLES ------------------------
// -----------------------------------------------------------------------
/**
* Récupère ou charge la configuration des types de cache d'un plugin, la complète et la stocke dans une meta.
*
* Le plugin Cache Factory propose une configuration par défaut des caches.
*
* @uses service_cache_chercher()
* @uses lire_config()
* @uses ecrire_config()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier ou
* un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
*
* @return array<string, mixed> Tableau des configurations complétée des caches d'un plugin venant d'être enregistrée.
*/
function ezcache_cache_configurer(string $plugin) : array {
// Initialisation du tableau des configurations qui sera retourné.
$configurations = [];
// Initialisation du tableau de configuration avec les valeurs par défaut du plugin Cache.
$configuration_defaut = [
'racine' => '_DIR_CACHE', // Emplacement de base du répertoire des caches. Attention c'est la chaine de la constante SPIP
'sous_dossier' => false, // Indicateur d'utilisation d'un sous-dossier
'sous_dossier_auto' => false, // Indique que la valeur par défaut du sous-dossier est le type de cache
'nom_prefixe' => '', // Préfixe du nom du cache : si non vide devient le premier composant obligatoire.
'nom_obligatoire' => [], // Composants obligatoires ordonnés de gauche à droite.
'nom_facultatif' => [], // Composants facultatifs
'separateur' => '', // Caractère de séparation des composants du nom '_' ou '-' ou '' si un seul composant est utilisé
'extension' => '.txt', // Extension du fichier cache (vaut .php si cache sécurisé)
'securisation' => false, // Indicateur de sécurisation du fichier
'serialisation' => true, // Indicateur de sérialisation
'decodage' => false, // Permet d'appliquer une fonction de décodage à la lecture qui dépend de l'extension
'conservation' => 0 // Durée de conservation du cache en secondes. 0 pour permanent
];
// Le plugin utilisateur doit fournir un service propre pour la ou les configurations de ses caches.
// Chaque configuration peut-être partielle, dans ce cas les données manquantes seront complétées
// par celles par défaut.
$configurations_plugin = [];
if ($configurer = service_cache_chercher($plugin, '', 'cache_configurer')) {
$configurations_plugin = $configurer($plugin);
}
// On boucle sur les différentes configuration de cache du plugin (appelée type de cache).
foreach ($configurations_plugin as $_type_cache => $_configuration_type_cache) {
// On merge la configuration du plugin avec celle par défaut pour assure la complétude.
$configuration = array_merge($configuration_defaut, $_configuration_type_cache);
// On rajoute le type de cache dans la configuration de façon à toujours l'avoir à disposition quand on fournit
// le tableau de configuration
$configuration['type_cache'] = $_type_cache;
// On vérifie que la durée de conservation du cache est bien un entier supérieur ou égal à 0.
// La durée est exprimée en secondes.
$configuration['conservation'] = abs((int) ($configuration['conservation']));
// On vérifie en priorité la sécurisation. Si le cache doit être sécurisé :
// - le décodage n'est pas possible
// - l'extension du cache doit absolument être .php. Si ce n'est pas le cas on la force.
if ($configuration['securisation']) {
$configuration['decodage'] = false;
if ($configuration['extension'] != '.php') {
$configuration['extension'] = '.php';
}
}
// On vérifie ensuite la sérialisation. Si le cache est sérialisé :
// - le décodage n'est pas possible.
if ($configuration['serialisation']) {
$configuration['decodage'] = false;
}
// On vérifie en dernier le décodage. Si le cache demande un décodage :
// - sécurisation et sérialisation ne sont pas possibles mais ont été traitées précédemment
// - le cache n'accepte que les extensions : json, xml ou yaml.
if ($configuration['decodage']) {
if ((($configuration['extension'] == 'yaml') or ($configuration['extension'] == 'yml'))
and (!defined('_DIR_PLUGIN_YAML'))) {
$configuration['decodage'] = false;
}
}
// Pour faciliter la construction du chemin des caches on stocke les éléments récurrents composant
// le dossier de base.
// -- Vérification de la localisation de la racine qui ne peut être que dans les trois dossiers SPIP
// prévus.
if (!in_array($configuration['racine'], ['_DIR_CACHE', '_DIR_TMP', '_DIR_ETC', '_DIR_VAR'])) {
$configuration['racine'] = $configuration_defaut['racine'];
}
// -- Sous-dossier spécifique au plugin
$configuration['dossier_plugin'] = ($configuration['racine'] == '_DIR_VAR') ? "cache-{$plugin}/" : "{$plugin}/";
// -- Si le type sert de sous-dossier on force l'indicateur sous-dossier à vrai
if ($configuration['sous_dossier_auto']) {
$configuration['sous_dossier'] = true;
}
// Construction du tableau des composants du nom : dans l'ordre on a toujours les composants obligatoires
// suivis des composants facultatifs.
// -- Traitement du préfixe: si il existe on le positionne comme premier composant obligatoire
if ($configuration['nom_prefixe']) {
$configuration['nom_obligatoire'] = array_merge(['_prefixe_nom'], $configuration['nom_obligatoire']);
}
// -- Si le nom obligatoire est vide (non initialisé et aucun préfixe), on le positionne avec un seul
// composant 'nom'
if (!$configuration['nom_obligatoire']) {
$configuration['nom_obligatoire'] = 'nom';
}
// -- Calcul du nom complet
$configuration['nom'] = array_merge($configuration['nom_obligatoire'], $configuration['nom_facultatif']);
// Vérifier le séparateur
if (count($configuration['nom']) == 1) {
// -- Si le nom ne comporte qu'un seul composant forcer le séparateur à '' pour ne pas interdire d'utiliser les
// caractères '_' ou '-' dans le composant unique.
$configuration['separateur'] = '';
} elseif (
count($configuration['nom']) > 1
and !$configuration['separateur']
) {
// -- Eviter une erreur si le séparateur est vide : on le force à '_'
$configuration['separateur'] = '_';
}
// On range la configuration élaborée dans le tableau des configurations du plugin
$configurations[$_type_cache] = $configuration;
}
// Enregistrement de la configuration du plugin utilisateur dans la meta prévue.
// Si une configuration existe déjà on l'écrase.
include_spip('inc/config');
$meta_cache = lire_config('cache', []);
$meta_cache[$plugin] = $configurations;
ecrire_config('cache', $meta_cache);
return $configurations;
}
/**
* Vérifie l'identifiant relatif du cache et le complète le cas échéant si le plugin utilise un sous-dossier ou
* un préfixe.
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param array<string, mixed> $cache Tableau identifiant le cache pour lequel on veut construire le nom.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utlisateur lue à partir de la meta de stockage.
*
* @return array<string, mixed> L'identifiant relatif du cache vérifié et éventuellement complété ou tableau vide si erreur.
*/
function ezcache_cache_verifier(string $plugin, array $cache, array $configuration) : array {
// Vérifications :
// -- on vérifie la configuration du sous-dossier
$cache_ok = true;
if (
$configuration['sous_dossier']
and !$configuration['sous_dossier_auto']
and empty($cache['sous_dossier'])
) {
$cache_ok = false;
}
// -- on vérifie la présence de tous les composants obligatoires du nom à l'exception du préfixe
if ($cache_ok) {
foreach ($configuration['nom_obligatoire'] as $_composant) {
if (
($_composant !== '_prefixe_nom')
and empty($cache[$_composant])
) {
$cache_ok = false;
break;
}
}
}
// Compléments :
// -- Cache avec sous-dossier: si la configuration impose un sous-dossier auto et que l'identifiant relatif $cache
// ne fournit pas cet index, c'est que le type de cache doit être utilisé comme nom de sous-dossier.
// -> on l'ajoute pour assurer un traitement standard.
if ($cache_ok) {
if (
$configuration['sous_dossier']
and $configuration['sous_dossier_auto']
and empty($cache['sous_dossier'])
) {
$cache['sous_dossier'] = $configuration['type_cache'];
}
// Cache avec préfixe: si il existe on le rajoute dans le tableau $cache pour assurer un
// traitement standard.
if ($configuration['nom_prefixe']) {
$cache['_prefixe_nom'] = $configuration['nom_prefixe'];
}
// On passe la main à un service spécifique si aucune erreur n'a été détectée.
if ($verifier = service_cache_chercher($plugin, $configuration['type_cache'], 'cache_composer')) {
$cache = $verifier($plugin, $cache, $configuration);
}
}
return $cache;
}
/**
* Construit le chemin complet du fichier cache.
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param array<string, mixed> $cache Tableau identifiant le cache pour lequel on veut construire le nom.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utlisateur lue à partir de la meta de stockage.
*
* @return string
*/
function ezcache_cache_composer(string $plugin, array $cache, array $configuration) : string {
// Le plugin utilisateur peut fournir un service propre pour construire le chemin complet du fichier cache.
// Néanmoins, étant donné la généricité du mécanisme offert par le plugin Cache cela devrait être rare.
if ($composer = service_cache_chercher($plugin, $configuration['type_cache'], 'cache_composer')) {
$fichier_cache = $composer($plugin, $cache, $configuration);
} else {
// On utilise le mécanisme de nommage standard du plugin Cache.
// Initialisation du chemin complet du fichier cache
$fichier_cache = '';
// Détermination du répertoire final du fichier cache qui peut-être inclus dans un sous-dossier du dossier
// de base des caches du plugin.
$dir_cache = constant($configuration['racine']) . $configuration['dossier_plugin'];
if ($configuration['sous_dossier']) {
if (!empty($cache['sous_dossier'])) {
// Si le cache nécessite un sous-dossier, appelé sous_dossier dans l'identifiant du cache.
$dir_cache .= rtrim($cache['sous_dossier'], '/') . '/';
} else {
// C'est une erreur, le sous-dossier n'a pas été fourni alors qu'il est requis.
$dir_cache = '';
}
}
// Détermination du nom du cache sans extension.
// Celui-ci est construit à partir des éléments fournis sur le cache et de la configuration
// fournie par le plugin (liste ordonnée de composant).
$nom_cache = '';
if ($dir_cache) {
foreach ($configuration['nom'] as $_composant) {
if (isset($cache[$_composant])) {
if (!$nom_cache) {
// Il y a forcément un composant non vide en premier.
$nom_cache .= $cache[$_composant];
} elseif (
$cache[$_composant]
or in_array($_composant, $configuration['nom_obligatoire'])
) {
// Le composant est à ajouter : non vide ou vide mais obligatoire (cas bizarre!)
$nom_cache .= $configuration['separateur'] . $cache[$_composant];
}
}
}
}
// Si le nom a pu être construit on finalise le chemin complet, sinon on renvoie une chaine vide.
if ($nom_cache) {
// L'extension par défaut est dans la configuration mais peut-être forcée pour un cache donné.
// Par contre, si le cache est sécurisé alors on ne tient pas compte du forçage éventuel car l'extension
// doit toujours être .php et celle-ci a été forcée lors de la configuration des caches du plugin.
$extension = (!empty($cache['extension']) and !$configuration['securisation'])
? $cache['extension']
: $configuration['extension'];
// Le chemin complet
$fichier_cache = "{$dir_cache}{$nom_cache}{$extension}";
}
}
return $fichier_cache;
}
/**
* Décompose le chemin complet du fichier cache en éléments constitutifs. Par défaut, le tableau obtenu coïncide
* avec l’identifiant relatif du cache. La fonction utilise la configuration générale pour connaitre la structure
* du chemin du fichier.
*
* Cache Factory renvoie uniquement les éléments de l'identifiant relatif.
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param string $fichier_cache Le chemin complet du fichier à phraser.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utlisateur lue à partir de la meta de stockage.
*
* @return array<string, mixed> Tableau des composants constitutifs du cache
*/
function ezcache_cache_decomposer(string $plugin, string $fichier_cache, array $configuration) : array {
// Le plugin utilisateur peut fournir un service propre pour construire le chemin complet du fichier cache.
// Néanmoins, étant donné la généricité du mécanisme offert par le plugin Cache cela devrait être rare.
if ($decomposer = service_cache_chercher($plugin, $configuration['type_cache'], 'cache_decomposer')) {
$cache = $decomposer($plugin, $fichier_cache, $configuration);
} else {
// On utilise le mécanisme de nommage standard du plugin Cache. De fait, on considère qu'aucun composant
// n'est facultatif ou du moins qu'un seul composant est facultatif et positionné en dernier.
// Initialisation du tableau cache
$cache = [];
// On supprime le dossier de base pour n'avoir que la partie spécifique du cache.
$dir_cache = constant($configuration['racine']) . $configuration['dossier_plugin'];
$fichier_cache = str_replace($dir_cache, '', $fichier_cache);
// Détermination du nom du cache sans extension et décomposition suivant la configuration du nom.
$nom_cache = basename($fichier_cache, $configuration['extension']);
if (count($configuration['nom']) === 1) {
// Le nom est composé d'un seul composant : on le renvoie directement.
$cache[$configuration['nom'][0]] = $nom_cache;
} else {
// Le nom est composé de plus d'un composant.
// - si un préfixe est configuré est renvoyé comme un autre composant car il a été accolé dans l'index
// 'nom' lors de la configuration.
foreach (explode($configuration['separateur'], $nom_cache) as $_cle => $_composant) {
$cache[$configuration['nom'][$_cle]] = $_composant;
}
}
// Identification d'un sous-dossier si il existe.
if (
$configuration['sous_dossier']
and ($sous_dossier = dirname($fichier_cache))
) {
$cache['sous_dossier'] = $sous_dossier;
}
}
return $cache;
}
/**
* Complète la description d'un cache issue du service `cache_decomposer()`.
*
* Le plugin Cache Factory complète la description canonique avec le nom sans extension, l'extension du fichier,
* la date et la taille du fichier.
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param array<string, mixed> $cache Tableau identifiant le cache pour lequel on veut construire le nom.
* @param string $fichier_cache Fichier cache désigné par son chemin complet.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utilisateur lue à partir de la meta de stockage.
*
* @return array<string, mixed> Description du cache complétée par un ensemble de données propres au plugin.
*/
function ezcache_cache_completer(string $plugin, array $cache, string $fichier_cache, array $configuration) : array {
// Cache Factory complète la description avec le nom sans extension, le type de cache, l'extension du fichier, la date
// et la taille du cache avant de passer la main au plugin utilisateur.
// Toutes ces informations proviennent du fichier lui-même et pas de la configuration (qui peut être écrasée
// dans certains cas).
$infos_cache = pathinfo($fichier_cache);
$cache['nom_cache'] = $infos_cache['basename'];
$cache['extension_cache'] = ".{$infos_cache['extension']}";
$cache['date_cache'] = date('Y-m-d H:i:s', filemtime($fichier_cache));
$cache['taille_cache'] = filesize($fichier_cache);
$cache['type_cache'] = $configuration['type_cache'];
// Le plugin utilisateur peut fournir un service propre pour construire le chemin complet du fichier cache.
// Néanmoins, étant donné la généricité du mécanisme offert par le plugin Cache cela devrait être rare.
if ($completer = service_cache_chercher($plugin, $configuration['type_cache'], 'cache_completer')) {
$cache = $completer($plugin, $cache, $fichier_cache, $configuration);
}
return $cache;
}
/**
* Décode le contenu du fichier cache en fonction de l'extension.
*
* Le plugin Cache Factory utilise des fonctions standard de PHP, SPIP ou du plugin YAML. Un plugin appelant peut
* proposer une fonction spécifique de décodage
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param string $contenu Contenu du fichier cache au format chaine.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utilisateur lue à partir de la meta de stockage.
*
* @return array Contenu du cache décodé si la fonction idoine a été appliqué ou tel que fourni en entrée sinon.
*/
function ezcache_cache_decoder($plugin, $contenu, $configuration) : array {
// Cache Factory décode le contenu du fichier cache en fonction de l'extension (json, yaml, yml ou xml).
$encodage = ltrim($configuration['extension'], '.');
// Le plugin utilisateur peut fournir un service propre pour décoder le contenu du cache.
// Néanmoins, étant donné la généricité du mécanisme offert par le plugin Cache cela devrait être rare.
if ($decoder = service_cache_chercher($plugin, $configuration['type_cache'], "cache_decoder_{$encodage}")) {
$contenu = $decoder($plugin, $contenu);
} else {
// Utilisation des fonctions génériques de Cache Factory
switch ($encodage) {
case 'json':
// On utilise la fonction PHP native
$contenu = json_decode($contenu, true);
break;
case 'yaml':
case 'yml':
// On utilise la fonction du plugin YAML si il est actif (un jour on l'aura dans SPIP...)
if (defined('_DIR_PLUGIN_YAML')) {
include_spip('inc/yaml');
$contenu = yaml_decode($contenu);
}
break;
case 'xml':
// On utilise la fonction historique de SPIP sachant qu'il en existe d'autre. Pour changer il suffit
// d'utiliser une fonction spécifique du plugin appelant.
include_spip('inc/xml');
$contenu = spip_xml_parse($contenu, true);
break;
default:
}
}
return $contenu;
}
/**
* Vérifie la validité du fichier cache.
*
* Le plugin Cache Factory teste toujours la péremption du fichier (durée de conservation issue). Il est toutefois
* possible de rajouter des tests spécifiques en appelant un service propre au plugin.
*
* @uses service_cache_chercher()
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier
* ou un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param string $fichier_cache Le chemin complet du fichier à valider.
* @param array<string, mixed> $cache Tableau identifiant le cache (redondant avec l'argument $fichier_cache mais peut être
* nécessaire pour les éventuels tests complémentaires du plugin utilisateur.
* @param array<string, mixed> $configuration Configuration complète des caches du plugin utlisateur lue à partir de la meta de stockage.
*
* @return bool `true` si le cache est valide, `false` sinon.
*/
function ezcache_cache_valider(string $plugin, string $fichier_cache, array $cache, array $configuration) : bool {
// Initialisation de la sortie
$est_valide = true;
// Vérifier en premier lieu l'existence du fichier.
if (!file_exists($fichier_cache)) {
$est_valide = false;
} else {
// Vérifier la péremption ou pas du fichier.
// -- un délai de conservation est configuré pour les caches du plugin utilisateur mais il possible
// de préciser un délai spécifique à un cache donné (index 'conservation' dans l'id du cache).
// -- si le délai est à 0 cela correspond à un cache dont la durée de vie est infinie.
$conservation = (is_array($cache) and isset($cache['conservation']))
? $cache['conservation']
: $configuration['conservation'];
if (($conservation > 0)
and (!filemtime($fichier_cache) or (time() - filemtime($fichier_cache) > $conservation))) {
$est_valide = false;
}
// Si le fichier a passé la vérification de péremption, il est possible de rajouter des tests spécifiques
// au plugin utilisateur.
if ($valider = service_cache_chercher($plugin, $configuration['type_cache'], 'cache_valider')) {
$est_valide = $valider($plugin, $cache, $configuration);
}
}
return $est_valide;
}
// -----------------------------------------------------------------------
// ----------------- UTILITAIRE PROPRE AU PLUGIN CACHE -------------------
// -----------------------------------------------------------------------
/**
* Cherche une fonction donnée en se basant sur le plugin appelant et le type de cache.
* Si le plugin utilisateur ne fournit pas la fonction demandée la chaîne vide est renvoyée.
*
* @internal
*
* @param string $plugin Identifiant qui permet de distinguer le module appelant qui peut-être un plugin comme le noiZetier ou
* un script. Pour un plugin, le plus pertinent est d'utiliser le préfixe.
* @param string $type_cache Type de cache identifiant la configuration dans la liste des configurations de cache du plugin.
* Si on veut forcer un service plugin uniquement il faut passer la chaine vide.
* @param string $fonction Nom de la fonction de service à chercher.
*
* @return string Nom complet de la fonction si trouvée ou chaine vide sinon.
*/
function service_cache_chercher(string $plugin, string $type_cache, string $fonction) : string {
$fonction_trouvee = '';
// Eviter la réentrance si on demande explicitement le service du plugin Cache Factory.
if ($plugin != 'ezcache') {
include_spip("ezcache/{$plugin}");
// On teste dans l'ordre la fonction la plus précise, celle correspondant au couple (plugin, type de cache) puis
// si elle n'existe pas, celle correspondant au plugin seul.
$fonction_trouvee = "{$plugin}_{$type_cache}_{$fonction}";
if (
!$type_cache
or !function_exists($fonction_trouvee)
) {
$fonction_trouvee = "{$plugin}_{$fonction}";
if (!function_exists($fonction_trouvee)) {
$fonction_trouvee = '';
}
}
}
return $fonction_trouvee;
}