fix: markdown double-encoding and robust date/API access
This commit is contained in:
+41
-11
@@ -18,6 +18,10 @@ function markdown_to_html($markdown) {
|
|||||||
$links = [];
|
$links = [];
|
||||||
$link_count = 0;
|
$link_count = 0;
|
||||||
|
|
||||||
|
// Note : le texte étant déjà échappé ci-dessus, les URLs extraites le sont
|
||||||
|
// aussi (« & » est devenu « & »). Il ne faut PAS les ré-échapper dans
|
||||||
|
// les callbacks ci-dessous, sinon on obtient un double encodage (« & »).
|
||||||
|
|
||||||
// Conversion des liens Markdown [texte](url)
|
// Conversion des liens Markdown [texte](url)
|
||||||
$markdown = preg_replace_callback('/\[([^\]]+)\]\(([^)]+)\)/s', function($matches) use (&$links, &$link_count) {
|
$markdown = preg_replace_callback('/\[([^\]]+)\]\(([^)]+)\)/s', function($matches) use (&$links, &$link_count) {
|
||||||
$text = $matches[1];
|
$text = $matches[1];
|
||||||
@@ -30,7 +34,7 @@ function markdown_to_html($markdown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$placeholder = "___LINK_{$link_count}___";
|
$placeholder = "___LINK_{$link_count}___";
|
||||||
$links[$placeholder] = '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $text . '</a>';
|
$links[$placeholder] = '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $text . '</a>';
|
||||||
$link_count++;
|
$link_count++;
|
||||||
|
|
||||||
return $placeholder;
|
return $placeholder;
|
||||||
@@ -43,7 +47,7 @@ function markdown_to_html($markdown) {
|
|||||||
$url = $matches[1];
|
$url = $matches[1];
|
||||||
|
|
||||||
$placeholder = "___LINK_{$link_count}___";
|
$placeholder = "___LINK_{$link_count}___";
|
||||||
$links[$placeholder] = '<a href="' . htmlspecialchars($url, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>';
|
$links[$placeholder] = '<a href="' . $url . '" target="_blank" rel="noopener noreferrer">' . $url . '</a>';
|
||||||
$link_count++;
|
$link_count++;
|
||||||
|
|
||||||
return $placeholder;
|
return $placeholder;
|
||||||
@@ -61,7 +65,7 @@ function markdown_to_html($markdown) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$placeholder = "___LINK_{$link_count}___";
|
$placeholder = "___LINK_{$link_count}___";
|
||||||
$links[$placeholder] = '<a href="http://' . htmlspecialchars($domain, ENT_QUOTES, 'UTF-8') . '" target="_blank" rel="noopener noreferrer">' . $domain . '</a>';
|
$links[$placeholder] = '<a href="http://' . $domain . '" target="_blank" rel="noopener noreferrer">' . $domain . '</a>';
|
||||||
$link_count++;
|
$link_count++;
|
||||||
|
|
||||||
return $placeholder;
|
return $placeholder;
|
||||||
@@ -71,19 +75,45 @@ function markdown_to_html($markdown) {
|
|||||||
$markdown = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $markdown);
|
$markdown = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $markdown);
|
||||||
$markdown = preg_replace('/\*(.*?)\*/s', '<em>$1</em>', $markdown);
|
$markdown = preg_replace('/\*(.*?)\*/s', '<em>$1</em>', $markdown);
|
||||||
|
|
||||||
// Conversion des listes à puces
|
// Conversion des listes (à puces et numérotées) en une seule passe ligne à
|
||||||
$markdown = preg_replace('/^- (.*?)$/m', '<li>$1</li>', $markdown);
|
// ligne : les items consécutifs de même type forment une seule liste ; une
|
||||||
$markdown = preg_replace('/(<li>.*?<\/li>\n?)+/s', '<ul>$0</ul>', $markdown);
|
// ligne hors liste ou un changement de type ferme la liste courante.
|
||||||
|
$lines = explode("\n", $markdown);
|
||||||
|
$markdown = '';
|
||||||
|
$listType = null; // 'ul', 'ol' ou null (hors liste)
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$itemType = null;
|
||||||
|
$itemText = null;
|
||||||
|
if (preg_match('/^- (.*)$/', $line, $matches)) {
|
||||||
|
$itemType = 'ul';
|
||||||
|
$itemText = $matches[1];
|
||||||
|
} elseif (preg_match('/^\d+\. (.*)$/', $line, $matches)) {
|
||||||
|
$itemType = 'ol';
|
||||||
|
$itemText = $matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
// Conversion des listes numérotées
|
if ($itemType !== $listType) {
|
||||||
$markdown = preg_replace('/^\d+\. (.*?)$/m', '<li>$1</li>', $markdown);
|
if ($listType !== null) {
|
||||||
$markdown = preg_replace('/(<li>.*?<\/li>\n?)+/s', '<ol>$0</ol>', $markdown);
|
$markdown .= '</' . $listType . ">\n";
|
||||||
|
}
|
||||||
|
if ($itemType !== null) {
|
||||||
|
$markdown .= '<' . $itemType . ">\n";
|
||||||
|
}
|
||||||
|
$listType = $itemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
$markdown .= ($itemType !== null ? '<li>' . $itemText . '</li>' : $line) . "\n";
|
||||||
|
}
|
||||||
|
if ($listType !== null) {
|
||||||
|
$markdown .= '</' . $listType . ">\n";
|
||||||
|
}
|
||||||
|
$markdown = rtrim($markdown, "\n");
|
||||||
|
|
||||||
// Gestion des retours à la ligne
|
// Gestion des retours à la ligne
|
||||||
$markdown = nl2br($markdown);
|
$markdown = nl2br($markdown);
|
||||||
|
|
||||||
// Nettoyage des balises br dans les listes
|
// Nettoyage des balises br autour des listes
|
||||||
$markdown = preg_replace('/<\/li><br \/>/', '</li>', $markdown);
|
$markdown = preg_replace('/(<\/li>|<ul>|<ol>)<br \/>/', '$1', $markdown);
|
||||||
|
|
||||||
// Restaurer les liens
|
// Restaurer les liens
|
||||||
foreach ($links as $placeholder => $link) {
|
foreach ($links as $placeholder => $link) {
|
||||||
|
|||||||
@@ -56,6 +56,21 @@ assertEquals(
|
|||||||
'formatDate affiche les années au pluriel'
|
'formatDate affiche les années au pluriel'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// --- formatDate : dates malformées (repli sur la chaîne brute) ---------------
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
'pas une date',
|
||||||
|
formatDate('pas une date'),
|
||||||
|
'formatDate retourne la chaîne brute si la date est invalide'
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
'2024-13-45T99:99:99Z',
|
||||||
|
formatDate('2024-13-45T99:99:99Z'),
|
||||||
|
'formatDate retourne une date ISO malformée telle quelle'
|
||||||
|
);
|
||||||
|
assertEquals('', formatDate(''), 'formatDate retourne une chaîne vide telle quelle');
|
||||||
|
assertEquals(' ', formatDate(' '), 'formatDate retourne une chaîne blanche telle quelle');
|
||||||
|
|
||||||
// --- formatVideosData -------------------------------------------------------
|
// --- formatVideosData -------------------------------------------------------
|
||||||
|
|
||||||
$rawVideos = [
|
$rawVideos = [
|
||||||
@@ -125,6 +140,58 @@ assertEquals('', $videos[1]['description'], 'formatVideosData met une descriptio
|
|||||||
assertEquals([], $videos[1]['tags'], 'formatVideosData met des tags vides par défaut');
|
assertEquals([], $videos[1]['tags'], 'formatVideosData met des tags vides par défaut');
|
||||||
assertFalse($videos[1]['isLive'], 'formatVideosData met isLive à false par défaut');
|
assertFalse($videos[1]['isLive'], 'formatVideosData met isLive à false par défaut');
|
||||||
|
|
||||||
|
// --- formatVideosData : données API incomplètes -------------------------------
|
||||||
|
|
||||||
|
$incompleteVideos = formatVideosData([
|
||||||
|
[
|
||||||
|
// Sans uuid : doit être ignorée
|
||||||
|
'name' => 'Sans uuid',
|
||||||
|
'duration' => 10,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// uuid vide : doit être ignorée aussi
|
||||||
|
'uuid' => '',
|
||||||
|
'name' => 'Uuid vide',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// uuid seul : valeurs par défaut partout ailleurs
|
||||||
|
'uuid' => 'ghi-789',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
// channel présent mais sans displayName ni avatars
|
||||||
|
'uuid' => 'jkl-012',
|
||||||
|
'channel' => ['name' => 'compte'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
assertEquals(2, count($incompleteVideos), 'formatVideosData ignore les entrées sans uuid');
|
||||||
|
|
||||||
|
assertEquals('ghi-789', $incompleteVideos[0]['id'], 'formatVideosData conserve l\'uuid seul');
|
||||||
|
assertEquals('', $incompleteVideos[0]['title'], 'formatVideosData met un titre vide par défaut');
|
||||||
|
assertEquals(0, $incompleteVideos[0]['duration'], 'formatVideosData met une durée à 0 par défaut');
|
||||||
|
assertEquals('', $incompleteVideos[0]['channel'], 'formatVideosData met une chaîne vide par défaut');
|
||||||
|
assertEquals(0, $incompleteVideos[0]['views'], 'formatVideosData met les vues à 0 par défaut');
|
||||||
|
assertEquals('', $incompleteVideos[0]['date'], 'formatVideosData met une date vide par défaut');
|
||||||
|
assertNull($incompleteVideos[0]['aspectRatio'], 'formatVideosData met aspectRatio à null par défaut');
|
||||||
|
assertFalse($incompleteVideos[0]['isLive'], 'formatVideosData met isLive à false par défaut');
|
||||||
|
assertEquals(
|
||||||
|
'img/default-thumbnail.jpg',
|
||||||
|
$incompleteVideos[0]['thumbnail'],
|
||||||
|
'formatVideosData met la vignette par défaut pour une entrée minimale'
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
'img/default-avatar.png',
|
||||||
|
$incompleteVideos[0]['channelAvatar'],
|
||||||
|
'formatVideosData met l\'avatar par défaut pour une entrée minimale'
|
||||||
|
);
|
||||||
|
|
||||||
|
assertEquals('', $incompleteVideos[1]['channel'], 'formatVideosData met une chaîne vide si displayName absent');
|
||||||
|
assertEquals(
|
||||||
|
'img/default-avatar.png',
|
||||||
|
$incompleteVideos[1]['channelAvatar'],
|
||||||
|
'formatVideosData met l\'avatar par défaut si le tableau avatars est absent'
|
||||||
|
);
|
||||||
|
|
||||||
// --- truncateText (includes/structured-data.php) ----------------------------
|
// --- truncateText (includes/structured-data.php) ----------------------------
|
||||||
|
|
||||||
assertEquals('court', truncateText('court', 200), 'truncateText laisse un texte court intact');
|
assertEquals('court', truncateText('court', 200), 'truncateText laisse un texte court intact');
|
||||||
|
|||||||
+34
-11
@@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* Tests unitaires pour markdown_to_html (includes/lib/markdown.php)
|
* Tests unitaires pour markdown_to_html (includes/lib/markdown.php)
|
||||||
*
|
|
||||||
* Les sorties attendues reflètent le comportement actuel de la fonction,
|
|
||||||
* y compris ses particularités (voir la note sur les listes à puces).
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// --- Échappement XSS ---------------------------------------------------------
|
// --- Échappement XSS ---------------------------------------------------------
|
||||||
@@ -63,21 +60,47 @@ assertEquals(
|
|||||||
'markdown_to_html ne transforme pas un numéro de version en lien'
|
'markdown_to_html ne transforme pas un numéro de version en lien'
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Listes ------------------------------------------------------------------
|
// --- URLs contenant « & » : pas de double encodage ---------------------------
|
||||||
|
|
||||||
// Note : la passe des listes numérotées s'applique aussi aux <li> déjà
|
// Le texte est échappé une seule fois : « & » devient « & », jamais « &amp; »
|
||||||
// produits par la passe des puces, d'où un double enveloppement <ul><ol>.
|
|
||||||
// C'est le comportement actuel, verrouillé ici contre toute régression.
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"<ul><ol><li>a</li>\n<li>b</li></ol></ul>",
|
'Voir <a href="https://exemple.com/page?a=1&b=2" target="_blank" rel="noopener noreferrer">https://exemple.com/page?a=1&b=2</a> suite',
|
||||||
markdown_to_html("- a\n- b"),
|
markdown_to_html('Voir https://exemple.com/page?a=1&b=2 suite'),
|
||||||
'markdown_to_html convertit les listes à puces (double enveloppement actuel)'
|
'markdown_to_html n\'encode pas deux fois le & des URLs brutes'
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"<ol><li>a</li>\n<li>b</li></ol>",
|
'<a href="https://exemple.com/?x=1&y=2" target="_blank" rel="noopener noreferrer">lien</a>',
|
||||||
|
markdown_to_html('[lien](https://exemple.com/?x=1&y=2)'),
|
||||||
|
'markdown_to_html n\'encode pas deux fois le & des liens Markdown'
|
||||||
|
);
|
||||||
|
assertNotContains(
|
||||||
|
'&amp;',
|
||||||
|
markdown_to_html('https://exemple.com/page?a=1&b=2'),
|
||||||
|
'markdown_to_html ne produit jamais de &amp;'
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Listes ------------------------------------------------------------------
|
||||||
|
|
||||||
|
assertEquals(
|
||||||
|
"<ul>\n<li>a</li>\n<li>b</li>\n</ul>",
|
||||||
|
markdown_to_html("- a\n- b"),
|
||||||
|
'markdown_to_html convertit les listes à puces en <ul> uniquement'
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
"<ol>\n<li>a</li>\n<li>b</li>\n</ol>",
|
||||||
markdown_to_html("1. a\n2. b"),
|
markdown_to_html("1. a\n2. b"),
|
||||||
'markdown_to_html convertit les listes numérotées en <ol>'
|
'markdown_to_html convertit les listes numérotées en <ol>'
|
||||||
);
|
);
|
||||||
|
assertEquals(
|
||||||
|
"<ul>\n<li>a</li>\n</ul><br />\n<ol>\n<li>b</li>\n</ol><br />\n<ul>\n<li>c</li>\n</ul>",
|
||||||
|
markdown_to_html("- a\n1. b\n- c"),
|
||||||
|
'markdown_to_html sépare les listes de types différents'
|
||||||
|
);
|
||||||
|
assertEquals(
|
||||||
|
"<ul>\n<li>a</li>\n</ul><br />\ntexte",
|
||||||
|
markdown_to_html("- a\ntexte"),
|
||||||
|
'markdown_to_html ferme la liste avant le texte qui suit'
|
||||||
|
);
|
||||||
|
|
||||||
// --- Retours à la ligne ------------------------------------------------------
|
// --- Retours à la ligne ------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user