PHP Supported Versions as a SPIP Plugin
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.
 
 
 

803 lines
20 KiB

<?php
// SPIP Standard API
/**
* Pipeline insert_head_css.
*
* @see https://programmer.spip.net/insert_head_css,561
*
* @param string $flux
* @return string
*/
function supportedversions_insert_head_css(string $flux): string {
$flux .= '<link rel="stylesheet" type="text/css" media="all" href="' .
find_in_path('css/supported-versions.css') .
'" />' . "\n";
return $flux;
}
/**
* Pipeline header_prive_cs.
*
* @codeCoverageIgnore
*
* @param string $flux
* @return string
*/
function supportedversions_header_prive_css(string $flux): string {
return supportedversions_insert_head_css($flux);
}
/**
* Balise #SUPPORTED_VERSIONS.
*
* Récupère les éléments définis dans la super globale $supportedversions
* Valeurs par défaut founies dans le fichier supportedversions_options.php
*
* @see https://www.spip.net/fr_article4014.html
* @api
*
* @param StdClass $p
* @return StdClass
*/
function balise_SUPPORTED_VERSIONS_dist($p) {
return balise_ENV_dist($p, '$GLOBALS["supportedversions"]');
}
/**
* Supported Versions filters class.
*/
class SupportedVersions
{
/**
* Configuration par défaut.
*
* @var array<mixed>
*/
private static $defaultConfig = [
'calendar' => [
'min_year' => 'P3Y',
'max_year' => 'P5Y',
],
'svg' => [
'margin_left' => 80,
'margin_right' => 50,
'header_height' => 24,
'year_width' => 120,
'branch_height' => 30,
'footer_height' => 24,
],
];
/**
* Configuration du calendrier (SVG, CSS, Dates).
*
* @var array<mixed>|null
*/
protected static $config = null;
/**
* Date courante.
*
* @var DateTime
*/
protected static $now;
/**
* Date de début du calendrier.
*
* @var DateTime
*/
private static $minDate;
/**
* Date de fin du calendrier.
*
* @var DateTime
*/
private static $maxDate;
/**
* Liste des années (format 'Y-m-d') du calendrier
*
* @var array<string>
*/
private static $years;
/**
* Liste des versions issues du fichier $releasesFile.
*
* @var array<mixed>
*/
private static $releases;
/**
* Chemin relatif du fichier JSON des versions à afficher
*
* @var string
*/
protected static $releasesFile;
/**
* @internal Définit et stocke les paramètres de configuration.
*
* - du calendrier,
* - les données des releases
* - et la date courante.
*
* @codeCoverageIgnore
*
* @return void
*/
protected static function init() {
self::$config = $GLOBALS['supportedversions'];
self::$now = new DateTime();
self::$releasesFile = 'data/releases.json';
self::compute();
}
/**
* @internal Calcule certaines données après initialisation.
*
* @return void
*/
protected static function compute() {
// Calendar initialization
$calendar = isset(self::$config['calendar']) ?
self::$config['calendar'] :
self::$defaultConfig['calendar']
;
$nowYear = self::$now->format('Y');
self::$minDate = (new DateTime('January 1 ' . $nowYear))
->sub(new DateInterval($calendar['min_year']));
self::$maxDate = (new DateTime('January 1 ' . $nowYear))
->add(new DateInterval($calendar['max_year']));
self::$years = array_map(
function ($year) {
return $year->format('Y-m-d');
},
iterator_to_array(new DatePeriod(self::$minDate, new DateInterval('P1Y'), self::$maxDate))
);
// Releases initialization
$json = file_get_contents(find_in_path(self::$releasesFile));
self::$releases = $json ? json_decode($json, true) : [];
}
/**
* Filtre sélectionnant les données des branches à afficher.
*
* Par défaut, récupère les branches affichables dans l'intervalle du calendrier
* Sont affichables:
* - Les branches avec une date de release initiale effective ou prévue antérieure à la date de fin de l'intervalle
* - Les branches avec une date de fin postérieure à la date de début de l'intervalle ou non prévue
*
* @param boolean $eol Ajoute les branches 'eol' à la sélection si true
* @return array<mixed> les branches à afficher
*/
public static function branchesToShow($eol = false) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
return array_reduce(
self::$releases,
function ($branches, $release) use ($eol) {
if ($release['initial_release'] !== '') {
$start = new DateTime($release['initial_release']);
$state = self::state($release);
$statesToShow = ['future', 'stable', 'security'];
if ($eol) {
array_push($statesToShow, 'eol');
}
if (in_array($state, $statesToShow)) {
$end = ($release['eol'] !== '') ? new DateTime($release['eol']) : '';
$limit = (new DateTime(self::$minDate->format('Y-m-d')))->sub(new DateInterval('P1D'));
$end = max(($end ? $end : self::$maxDate), $limit);
if (self::inCalendar($start, $end)) {
$branches[] = $release;
}
}
}
return $branches;
},
[]
);
}
/**
* Filtre sélectionnant les données de branches en fonction de leur état.
*
* @codeCoverageIgnore
*
* @see state()
*
* @param array<string> $states Listes des états souhaités
* @return array<mixed> les branches à afficher
*/
public static function branchesByState(array $states): array {
$branches = [];
foreach ($states as $state) {
$branches = array_merge($branches, self::getBranchesFromState($state));
}
// Tant que ce filtre n'est utilisé que pour le modèle configuration,
// on filtre les branches qui ont un atribut "sysem" avec un élément
return array_filter($branches, function ($values) {
return isset($values['system']) && count($values['system']) > 0;
});
}
/**
* Filtre fournissant un tableau de compatibilité SPIP/PHP.
*
* Conditions d'affichage:
* - une date de release initiale effective ou prévue est définie (non vide)
* - une liste de versions compatible pour PHP est fournie (non vide)
* - la date de fin de vie n'est pas communiquée (cas nominal)
*
* Si les versions en fin de vie sont demandées, la date de fin de vie
* doit être fournie et inférieure à la date du jour
*
* @param bool $eol true si on souhaite afficher les versions SPIP en fin de vie.
* @return array<mixed>
*/
public static function phpMatrix($eol = false) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$matrix = [];
$now = self::$now->format('Y-m-d');
$phpVersions = array_values(
array_unique(
array_reduce(
self::$releases,
function ($versions, $release) {
return array_merge($versions, $release['technologies']['require']['php']);
},
[]
)
)
);
foreach (self::$releases as $release) {
$toPrint = $release['initial_release'] !== '' && !empty($release['technologies']['require']['php']);
if (!$eol && $release['eol'] !== '') {
$toPrint = $toPrint && $release['eol'] >= $now;
}
if ($toPrint) {
foreach ($phpVersions as $php) {
$matrix[$release['branch']][$php] = in_array($php, $release['technologies']['require']['php']);
}
}
}
return $matrix;
}
/**
* Filtre fournissant les informations de la branche liée à la release passée en paramètre.
*
* - Si le paramètre ne correspond pas à un nommage de branch X.Y
* ou si la branche X.Y n'existe pas :
* On retourne un tableau de valeurs vide.
*
* - Sinon, on retourne toutes les données de la branche.
*
* @param string $release la version de la publication dont on souhaite récupérer les données de branche
* @return array<mixed>
*/
public static function getBranchValues(string $release): array {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$values = [
'branch' => '',
'initial_release' => '',
'active_support' => '',
'eol' => '',
'technologies' => [
'require' => [
'php' => []
],
],
'releases' => [],
];
if (preg_match('/^(\d+\.\d+)/', $release, $matches)) {
$branch = $matches[1];
$filteredValues = array_filter(self::$releases, function ($values) use ($branch) {
return $values['branch'] === $branch;
});
if (!empty($filteredValues)) {
$values = array_pop($filteredValues);
}
}
return $values;
}
/**
* Filtre fournissant les branches correspondant à un état de publication donné.
*
* l'état de publication doit correspondre à l'un des termes suivants:
* future, stable, security, eol.
* @see state()
*
* @param string $state l'état de publication souhaité
* @return array<mixed>
*/
public static function getBranchesFromState(string $state): array {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$notPlannedBranch = [
'branch' => '',
'initial_release' => '',
'active_support' => '',
'eol' => '',
'technologies' => [
'require' => [
'php' => [],
],
],
'releases' => [],
];
$callable = function ($values) {
return false;
};
if ('future' === $state) {
$callable = function ($values) {
$initial = $values['initial_release'] ? new DateTime($values['initial_release']) : null;
return $initial && $initial > self::$now;
};
}
if ('stable' === $state) {
$callable = function ($values) {
$initial = $values['initial_release'] ? new DateTime($values['initial_release']) : null;
$bug = $values['active_support'] ? new DateTime($values['active_support']) : null;
return $initial && $initial <= self::$now && (!$bug || $bug >= self::$now);
};
}
if ('security' === $state) {
$callable = function ($values) {
$bug = $values['active_support'] ? new DateTime($values['active_support']) : null;
$security = $values['eol'] ? new DateTime($values['eol']) : null;
return $bug && $bug <= self::$now && (!$security || $security >= self::$now);
};
}
if ('eol' === $state) {
$callable = function ($values) {
$security = $values['eol'] ? new DateTime($values['eol']) : null;
return $security && $security < self::$now;
};
}
$filteredBranches = array_filter(self::$releases, $callable);
return empty($filteredBranches) ? [$notPlannedBranch] : array_values($filteredBranches);
}
//Calendar Part
/**
* @internal Vérifie si l'intervalle de temps a une intersection avec le calendrier.
*
* @param DateTime $start
* @param DateTime $end
* @return bool
*/
protected static function inCalendar(DateTime $start, DateTime $end) {
return $start < self::$maxDate && $end >= self::$minDate;
}
/**
* Filtre fournissant les années (format 'Y-m-d') composant le calendrier.
*
* @return array<string> liste des années du calendrier (format 'Y-m-d')
*/
public static function years() {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
return self::$years;
}
/**
* Filtre qui calcule la position horizontale d'une date dans le calendrier.
*
* @uses dateHorizCoord
*
* @param string $date une date au format 'Y-m-d'
* @return int
*/
public static function horizCoord($date) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$horizCoord = $date ?
new DateTime($date) :
self::$maxDate;
return self::dateHorizCoord($horizCoord);
}
//SVG Part
/**
* Renvoie la coordonnée x d'une date dans le calendrier svg.
*
* - coordonnée 'x' de début du calendrier si la date est antérieure à la date de début du calendrier
* - coordonnée 'x' de fin du calendrier si la date est postérieure à la date de fin du calendrier
*
* @uses width
*
* @param DateTime $date
* @return int
*/
protected static function dateHorizCoord(DateTime $date) {
$svg = isset(self::$config['svg']) ?
self::$config['svg'] :
self::$defaultConfig['svg']
;
$diff = $date->diff(self::$minDate);
if (!$diff->invert) {
return $svg['margin_left'];
}
$diff2 = self::$maxDate->diff($date);
if (!$diff2->invert) {
return $svg['margin_left'] +
self::width($svg['margin_left']) +
$svg['year_width'];
}
return $svg['margin_left'] +
intval($diff->days / (365.24 / $svg['year_width']));
}
/**
* Filtre qui détermine l'état d'une version par rapport à la date du jour.
*
* le tableau de valeurs doit impérativement contenir les clés suivantes:
*
* initial_release:
* Une date au format 'Y-m-d' indiquant la date de sortie de la première release stable (X.Y.0)
* Si la date est vide (''), la version n'a pas de date de sortie prévue, l'état est 'not-planned'
* Si la date est supérieure à la date du jour, l'état est 'future'
* Sinon elle est 'stable'
*
* active_support:
* Une date indiquant le moment ou seuls des correctifs de sécurité sont assurés pour la version
* Si la date est vide (''), la version n'a pas de date de fin de support actif prévue, l'état reste à 'stable'
* Si la date est supérieure à la date du jour, l'état reste à 'stable'
* Sinon l'état est 'security'
*
* eol:
* Une date à partir de laquelle il n'y a plus de mise à jour pour la version
* Si la date est vide (''), la version n'a pas de date de fin de vie prévue, l'état reste à 'security'
* Si la date est supérieure à la date du jour, l'état reste à 'security'
* Sinon l'état est 'eol'
*
* @param array<string> $valeurs tableau correspondant aux dates de la version
* @return string
*/
public static function state(array $valeurs) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$state = 'not-planned';
$initial = $valeurs['initial_release'] ? new DateTime($valeurs['initial_release']) : null;
$bug = $valeurs['active_support'] ? new DateTime($valeurs['active_support']) : null;
$security = $valeurs['eol'] ? new DateTime($valeurs['eol']) : null;
if ($initial && $initial > self::$now) {
$state = 'future';
}
if ($initial && $initial <= self::$now) {
$state = 'stable';
}
if ($bug && $bug < self::$now) {
$state = 'security';
}
if ($security && $security < self::$now) {
$state = 'eol';
}
return $state;
}
/**
* Filtre servant à calculer la coordonnées 'y' dans le calendrier pour une branche données.
*
* @param string $branch
* @return int
*/
public static function top($branch) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
$svg = isset(self::$config['svg']) ?
self::$config['svg'] :
self::$defaultConfig['svg']
;
$branches = array_map(
function ($branch) {
return $branch['branch'];
},
self::branchesToShow(true)
);
$i = array_search($branch, $branches, true);
return $svg['header_height'] + ($svg['branch_height'] * $i);
}
/**
* Filtre donnant la coordonnées 'y' du nom de la branche dans le calendrier.
* Cf. modeles/supportedversions_calendar.html
*
* @codeCoverageIgnore
*
* @param string $branch
* @return int
*/
public static function topForText($branch) {
if (!self::$config) {
self::init();
}
$svg = isset(self::$config['svg']) ?
self::$config['svg'] :
self::$defaultConfig['svg']
;
$top = self::top($branch) + (0.5 * $svg['branch_height']);
return intval($top);
}
/**
* Filtre donnant la largeur du calendrier.
* Cf. modeles/supportedversions_calendar.html
*
* @codeCoverageIgnore
*
* @param int $margin_left
* @return int
*/
public static function width($margin_left) {
if (!self::$config) {
self::init();
}
$svg = isset(self::$config['svg']) ?
self::$config['svg'] :
self::$defaultConfig['svg']
;
return $margin_left +
$svg['margin_right'] +
((count(self::$years) - 1) * $svg['year_width']);
}
/**
* Filtre donnant la hauteur du calendrier.
* Cf. modeles/supportedversions_calendar.html
*
* @codeCoverageIgnore
*
* @param int $header_height
* @return int
*/
public static function height($header_height) {
if (!self::$config) {
self::init();
}
$svg = isset(self::$config['svg']) ?
self::$config['svg'] :
self::$defaultConfig['svg']
;
return intval(
$header_height +
$svg['footer_height'] +
(count(self::branchesToShow(true)) * $svg['branch_height'])
);
}
/**
* Filtre calculant la largeur du rectangle, d'un état d'une branche.
*
* @param string $branch
* @param string $state
* @return int|string empty string or value
*/
public static function rectWidth($branch, $state) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
// Etat inconnu
if (!in_array($state, ['security', 'stable', 'future'])) {
return '';
}
$filteredBranches = array_filter(self::$releases, function ($release) use ($branch) {
return $branch === $release['branch'];
});
$values = array_pop($filteredBranches);
// Branche inconnue
if (!isset($values)) {
return '';
}
$endDate = $values['eol'];
$startDate = $values['initial_release'];
if ($state == 'security') {
$startDate = $values['active_support'];
}
if ($state == 'stable') {
$endDate = $values['active_support'] ? $values['active_support'] : $endDate;
}
$startDate = new DateTime($startDate);
$endDate = new DateTime($endDate);
if (strrpos(self::stateOrGradient($values['branch'], $state), '-gradient')) {
$endDate = self::$maxDate;
}
$rectWidth = intval(
self::dateHorizCoord($endDate) - self::dateHorizCoord($startDate)
);
return $rectWidth > 0 ? $rectWidth : '';
}
/**
* Filtre calculant la class CSS d'un rectangle SVG de l'état d'une branche.
*
* Si la branche est inconnue, renvoie une chaine vide ('').
* Si l'état n'est pas affichable, renvoie une chaine vide ('').
* Si cycle de vie de la branche est en dehors des bornes du calendrier, renvoie une chaine vide ('').
* - i.e. : fin de vie avant la date de début, sortie effective ou prévue après la date de fin
* Si l'état de la branche se termine dans les bornes du calendrier, renvoie l'état.
* - i.e : stable, security, future
* Si l'état de la branche n'a pas de fin prévue, renvoie l'état et la mention gradient.
* - i.e. : stable-gradient, security-gradient, future-gradient.
*
* @param string $branch
* @param string $state
* @return string
*/
public static function stateOrGradient($branch, $state) {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
// Etat inconnu
if (!in_array($state, ['security', 'stable', 'future'])) {
return '';
}
$filteredBranches = array_filter(self::$releases, function ($release) use ($branch) {
return $branch === $release['branch'];
});
$values = array_pop($filteredBranches);
// Branche inconnue
if (!isset($values)) {
return '';
}
$endDate = $values['eol'];
$startDate = $values['initial_release'];
if ($state == 'security') {
$startDate = $values['active_support'];
}
if ($state == 'stable') {
$endDate = $values['active_support'] ? $values['active_support'] : $endDate;
}
// Date de début de l'état non prévue
if ($startDate === '') {
return '';
}
if (!self::inCalendar(new DateTime($startDate), new DateTime($endDate))) {
return '';
}
$originalState = self::state($values);
if ($state !== 'future' && $originalState == 'future') {
return '';
}
if ($state === 'future' && $originalState !== 'future') {
return '';
}
if ($endDate === '' || new DateTime($endDate) > self::$maxDate) {
$state .= '-gradient';
}
return $state;
}
//spip_loader API Part
/**
* Version par défaut de l'API pour le script spip_loader.php.
*/
public static function spipLoaderApi(): int {
// @codeCoverageIgnoreStart
if (!self::$config) {
self::init();
}
// @codeCoverageIgnoreEnd
return intval(self::$config['spip_loader_api_default'] ?? 0);
}
/**
* Transforme un tableau en chaine.
*
* @param array<mixed> $extensions liste des extensions et de leurs versions
* @param string $versionMarker
* @param string $logicalOperation
* @return string
*/
public static function configuration(
array $extensions,
string $versionMarker = '',
string $logicalOperation = 'and'
): string {
$tmpExtensions = [];
foreach ($extensions as $extension => $version) {
if (is_array($version)) {
$tmpExtensions[] = '(' . self::configuration($version, $versionMarker, $extension) . ')';
} else {
$tmpExtensions[] = $extension . ($version == '*' ? '' : $versionMarker . $version);
}
}
return implode($logicalOperation !== 'or' ? ', ' : ' | ', $tmpExtensions);
}
}