diff --git a/countdown.php b/countdown.php
index 4b52b5a..ca8701d 100644
--- a/countdown.php
+++ b/countdown.php
@@ -3,6 +3,8 @@
require_once 'includes/config.php';
// Inclure les fonctions de sécurité
require_once 'includes/security.php';
+// Inclure les fonctions de données structurées (getBaseUrl, getCurrentUrl)
+require_once 'includes/structured-data.php';
// Appliquer les en-têtes de sécurité
setSecurityHeaders();
?>
@@ -14,7 +16,7 @@ setSecurityHeaders();
- Ouverture prochaine
-
+
@@ -42,8 +44,8 @@ setSecurityHeaders();
setlocale(LC_TIME, 'fr_FR.UTF-8');
echo strftime('%e %B %Y', $targetDate->getTimestamp());
?>. Restez connectés !">
-
-
+
+
@@ -56,7 +58,7 @@ setSecurityHeaders();
setlocale(LC_TIME, 'fr_FR.UTF-8');
echo strftime('%e %B %Y', $targetDate->getTimestamp());
?>. Restez connectés !">
-
+
" (titre de vidéo, nom de chaîne, titre d'épisode…)
+ * ne peut plus fermer le bloc " ---------------------------------
+
+$maliciousTitle = 'Vidéo test';
+
+$videoData = [
+ 'thumbnailPath' => '/static/thumbnails/test.jpg',
+];
+$video = [
+ 'id' => '9c5de94d-8e1a-4b3c-9d2e-1234567890ab',
+ 'title' => $maliciousTitle,
+ 'description' => 'Une description',
+ 'duration' => 120,
+ 'channel' => 'Chaîne ',
+ 'views' => 42,
+ 'date' => '2024-01-15 10:00:00',
+ 'tags' => ['test'],
+];
+
+$jsonLd = generateVideoObjectJsonLd($videoData, $video);
+assertNotContains('', $jsonLd, 'generateVideoObjectJsonLd ne reproduit pas "" en clair');
+
+// L'équivalent encodé de "" (séquences \u00XX via JSON_HEX_TAG)
+// doit apparaître à la place de la balise en clair
+$encodedClosingTag = trim(json_encode('', JSON_UNESCAPED_SLASHES | JSON_HEX_TAG), '"');
+assertContains(
+ $encodedClosingTag,
+ $jsonLd,
+ 'generateVideoObjectJsonLd encode "<" et ">" en séquences unicode (JSON_HEX_TAG)'
+);
+
+$decoded = json_decode($jsonLd, true);
+assertTrue(is_array($decoded), 'Le JSON-LD VideoObject reste un JSON valide');
+assertEquals(
+ $maliciousTitle,
+ $decoded['name'],
+ 'Le titre malveillant est préservé à l\'identique après décodage JSON'
+);
+
+// Les autres générateurs JSON-LD bénéficient des mêmes drapeaux d'encodage
+$websiteLd = generateWebSiteJsonLd();
+assertNotContains('', $websiteLd, 'generateWebSiteJsonLd ne contient pas ""');
+assertTrue(json_decode($websiteLd, true) !== null, 'generateWebSiteJsonLd produit un JSON valide');
+
+$breadcrumbLd = generateBreadcrumbJsonLd([
+ ['name' => 'Accueil', 'url' => 'https://test.local'],
+ ['name' => 'Page ', 'url' => 'https://test.local/page'],
+]);
+assertNotContains('', $breadcrumbLd, 'generateBreadcrumbJsonLd neutralise "" dans un nom de fil d\'Ariane');
+
+$collectionLd = generateVideoCollectionJsonLd('Collection ', 'Description', [$video], 'https://test.local/col');
+assertNotContains('', $collectionLd, 'generateVideoCollectionJsonLd neutralise "" dans le nom de collection');
+
+$podcastLd = generatePodcastJsonLd([
+ [
+ 'title' => 'Épisode ',
+ 'link' => 'https://podcast.example/ep1',
+ 'pubDate' => '2024-01-01',
+ ],
+]);
+assertNotContains('', $podcastLd, 'generatePodcastJsonLd neutralise "" dans un titre d\'épisode');
+assertTrue(json_decode($podcastLd, true) !== null, 'generatePodcastJsonLd produit un JSON valide');
+
+// --- isValidAppHostName -------------------------------------------------------
+
+assertTrue(isValidAppHostName('example.com'), 'isValidAppHostName accepte un nom de domaine');
+assertTrue(isValidAppHostName('test.local'), 'isValidAppHostName accepte un domaine local');
+assertTrue(isValidAppHostName('localhost'), 'isValidAppHostName accepte localhost');
+assertTrue(isValidAppHostName('127.0.0.1:8080'), 'isValidAppHostName accepte une IPv4 avec port');
+assertTrue(isValidAppHostName('sub.example-site.com'), 'isValidAppHostName accepte sous-domaines et tirets');
+assertFalse(isValidAppHostName(''), 'isValidAppHostName refuse une chaîne vide');
+assertFalse(isValidAppHostName('evil.com">';
+$_SERVER['HTTPS'] = 'on';
+assertEquals(
+ 'https://' . APP_HOST_NAME,
+ getBaseUrl(),
+ 'getBaseUrl utilise APP_HOST_NAME et ignore un HTTP_HOST malveillant'
+);
+assertNotContains('evil.example', getBaseUrl(), 'getBaseUrl ne reflète pas l\'en-tête Host');
+assertEquals(APP_HOST_NAME, getAppHostName(), 'getAppHostName retourne APP_HOST_NAME validé');
+
+unset($_SERVER['HTTPS']);
+assertEquals('http://' . APP_HOST_NAME, getBaseUrl(), 'getBaseUrl retombe en http sans HTTPS');
+
+// --- getCurrentUrl : REQUEST_URI assaini --------------------------------------
+
+$_SERVER['REQUEST_URI'] = '/video.php?id=abc">';
+$currentUrl = getCurrentUrl();
+assertNotContains('"', $currentUrl, 'getCurrentUrl supprime les guillemets de REQUEST_URI');
+assertNotContains('<', $currentUrl, 'getCurrentUrl supprime les chevrons de REQUEST_URI');
+assertNotContains(' ', $currentUrl, 'getCurrentUrl supprime les espaces de REQUEST_URI');
+assertEquals(
+ 0,
+ strpos($currentUrl, 'http://' . APP_HOST_NAME . '/video.php?id=abc'),
+ 'getCurrentUrl préfixe par l\'hôte validé et conserve le chemin'
+);
+
+$_SERVER['REQUEST_URI'] = 'sans-slash-initial';
+assertEquals(
+ 'http://' . APP_HOST_NAME . '/sans-slash-initial',
+ getCurrentUrl(),
+ 'getCurrentUrl ajoute le slash initial manquant'
+);
+
+// Nettoyage des superglobales modifiées pour ne pas impacter d'autres tests
+unset($_SERVER['HTTPS']);
+$_SERVER['HTTP_HOST'] = APP_HOST_NAME;
+$_SERVER['REQUEST_URI'] = '/';