diff --git a/404.php.sample b/404.php.sample
index 5604c9c..e988aa9 100644
--- a/404.php.sample
+++ b/404.php.sample
@@ -37,7 +37,7 @@ if (function_exists('setSecurityHeaders')) {
-
+
diff --git a/500.php.sample b/500.php.sample
index 871a124..763ef7f 100644
--- a/500.php.sample
+++ b/500.php.sample
@@ -40,7 +40,7 @@ if (function_exists('setSecurityHeaders')) {
-
+
diff --git a/categories.php b/categories.php
index a8b4cb5..866148e 100644
--- a/categories.php
+++ b/categories.php
@@ -19,8 +19,8 @@ setSecurityHeaders();
$categoryId = isset($_GET['id']) ? $_GET['id'] : null;
$categoryId = $categoryId ? validateCategoryId($categoryId) : null;
-// Récupérer les catégories disponibles
-$allCategories = PEERTUBE_CATEGORIES;
+// Récupérer les catégories disponibles (chargement paresseux, ARC-2)
+$allCategories = getPeertubeCategories();
// Récupérer les vidéos de la catégorie si un ID est fourni
if ($categoryId && isset($allCategories[$categoryId])) {
@@ -46,7 +46,7 @@ if ($categoryId && isset($allCategories[$categoryId])) {
-
+
diff --git a/direct.php b/direct.php
index 9c164d4..9f6267b 100644
--- a/direct.php
+++ b/direct.php
@@ -28,7 +28,7 @@ $liveStream = getLiveStream();
-
+
@@ -160,111 +160,9 @@ $liveStream = getLiveStream();
$showNextLiveAnnouncement = defined('NEXT_LIVE_ENABLED') && NEXT_LIVE_ENABLED === true;
if ($showNextLiveAnnouncement) {
- // Afficher l'annonce du prochain live
- // Définir l'image de fond si disponible
- $bgImageStyle = '';
- if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)) {
- $bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
- }
- ?>
-
-
-
-
-
-
-
; ?>)
-
-
-
-
- format($liveDate);
- $formattedDay = ucfirst($formattedDay);
- $dynamicTitle = NEXT_LIVE_TITLE . ' - ' . $formattedDay;
- } else {
- $dynamicTitle = NEXT_LIVE_TITLE;
- }
- ?>
-
- format('H\hi');
- $dynamicDescription = 'Rejoignez-nous à ' . $liveHour . '. ' . NEXT_LIVE_DESCRIPTION;
- } else {
- $dynamicDescription = NEXT_LIVE_DESCRIPTION;
- }
- ?>
-
-
-
-
-
- format($liveDate);
-
- $offset = $liveDate->format('P');
- echo ' (UTC' . $offset . ')';
- ?>
-
-
-
-
- 'Pacific/Tahiti',
- 'Martinique / Guadeloupe' => 'America/Martinique',
- 'Guyane' => 'America/Cayenne',
- 'France' => 'Europe/Paris',
- 'Kanaky' => 'Pacific/Noumea'
- ];
-
- foreach($timezones as $name => $timezone):
- $liveDateLocal = clone $liveDate;
- $liveDateLocal->setTimezone(new DateTimeZone($timezone));
-
- // Vérifier si c'est un jour différent
- $dayDiff = $liveDateLocal->format('j') - $liveDate->format('j');
-
- $dayIndicator = '';
- if ($dayDiff > 0) {
- $dayIndicator = ' +1j';
- } elseif ($dayDiff < 0) {
- $dayIndicator = ' -1j';
- }
- ?>
-
- : format('H\hi'); ?>
-
-
-
-
-
-
Retour à l'accueil
-
-
-
diff --git a/dons.php.sample b/dons.php.sample
index 7167743..4b0a078 100644
--- a/dons.php.sample
+++ b/dons.php.sample
@@ -83,7 +83,7 @@ function linkUrlsInText(string $text): string {
-
+
diff --git a/index.php b/index.php
index 29098b0..8133858 100644
--- a/index.php
+++ b/index.php
@@ -30,7 +30,7 @@ if (defined('CASTOPOD_ENABLED') && CASTOPOD_ENABLED && defined('CASTOPOD_URL') &
-
+
diff --git a/mentions-legales.php.sample b/mentions-legales.php.sample
index 257afd6..72626ba 100644
--- a/mentions-legales.php.sample
+++ b/mentions-legales.php.sample
@@ -13,7 +13,7 @@ setSecurityHeaders();
-
+
diff --git a/recherche.php b/recherche.php
index 7613739..5559a7e 100644
--- a/recherche.php
+++ b/recherche.php
@@ -24,26 +24,33 @@ $currentSearchPage = validatePageNumber($_GET['page'] ?? 1);
$isTagSearch = !empty($query) && substr($query, 0, 1) === '#';
$searchTag = $isTagSearch ? substr($query, 1) : '';
-// Rechercher les vidéos via l'API PeerTube si une requête est soumise
-$searchResults = !empty($query) ? searchVideos($query, COUNT_VIDEO_SEARCH) : [];
-
-// Définir le nombre total de résultats
-$resultsCount = count($searchResults);
-
-// Calculer le nombre total de pages
-$totalPages = ceil($resultsCount / VIDEOS_PER_PAGE);
-
-// S'assurer que la page actuelle est valide
-$currentSearchPage = min($currentSearchPage, max(1, $totalPages));
-
-// Calculer les indices de début et de fin pour la page actuelle
-$startIndex = ($currentSearchPage - 1) * VIDEOS_PER_PAGE;
-$endIndex = min($startIndex + VIDEOS_PER_PAGE, $resultsCount);
-
-// Extraire les vidéos pour la page actuelle
+// Rechercher les vidéos via l'API PeerTube si une requête est soumise.
+// La pagination est déléguée à l'API (paramètre start) : chaque page ne
+// récupère que VIDEOS_PER_PAGE vidéos et $resultsCount reçoit le total réel.
+$resultsCount = 0;
$currentPageVideos = [];
-if ($resultsCount > 0) {
- $currentPageVideos = array_slice($searchResults, $startIndex, VIDEOS_PER_PAGE);
+$totalPages = 1;
+if (!empty($query)) {
+ $currentPageVideos = searchVideos(
+ $query,
+ VIDEOS_PER_PAGE,
+ ($currentSearchPage - 1) * VIDEOS_PER_PAGE,
+ $resultsCount
+ );
+ $totalPages = max(1, (int) ceil($resultsCount / VIDEOS_PER_PAGE));
+
+ // Page demandée au-delà de la dernière : se replacer sur la dernière page valide
+ if ($currentSearchPage > $totalPages) {
+ $currentSearchPage = $totalPages;
+ $currentPageVideos = searchVideos(
+ $query,
+ VIDEOS_PER_PAGE,
+ ($currentSearchPage - 1) * VIDEOS_PER_PAGE,
+ $resultsCount
+ );
+ // Le total a pu changer (ex. API en échec sur un start trop élevé)
+ $totalPages = max(1, (int) ceil($resultsCount / VIDEOS_PER_PAGE));
+ }
}
?>
@@ -61,7 +68,7 @@ if ($resultsCount > 0) {
-
+
diff --git a/tests/php/rate-limit-test.php b/tests/php/rate-limit-test.php
new file mode 100644
index 0000000..6715e4a
--- /dev/null
+++ b/tests/php/rate-limit-test.php
@@ -0,0 +1,64 @@
+ 3, 'reset' => time() - 1]));
+assertTrue(
+ checkRateLimit($ip, 3, 60, $rlDir),
+ 'rate limit : le compteur repart à zéro après expiration de la fenêtre'
+);
+
+// --- Fail-open si le stockage est indisponible ---------------------------------
+
+// Un chemin qui est un fichier (pas un répertoire) : mkdir doit échouer
+$notADir = $rlDir . '/fichier-bloquant';
+file_put_contents($notADir, 'x');
+assertTrue(
+ @checkRateLimit('198.51.100.5', 1, 60, $notADir),
+ 'rate limit : fail-open si le répertoire de stockage est indisponible'
+);
+
+// --- Nettoyage -----------------------------------------------------------------
+
+foreach ([$rlDir, $rlDir . '/autre'] as $dir) {
+ foreach (glob($dir . '/rl_*.json') ?: [] as $file) {
+ unlink($file);
+ }
+ if (is_dir($dir)) {
+ rmdir($dir);
+ }
+}
+unlink($notADir);
diff --git a/tests/php/security-test.php b/tests/php/security-test.php
index 9835422..bb7f389 100644
--- a/tests/php/security-test.php
+++ b/tests/php/security-test.php
@@ -255,3 +255,85 @@ assertFalse(
isValidRemoteUrl('https://metadata.google.internal'),
'isValidRemoteUrl refuse metadata.google.internal'
);
+
+// --- cspOriginFromUrl ---------------------------------------------------------
+
+assertEquals(
+ 'https://peertube.example.com',
+ cspOriginFromUrl('https://peertube.example.com'),
+ 'cspOriginFromUrl extrait l\'origine d\'une URL simple'
+);
+assertEquals(
+ 'https://peertube.example.com',
+ cspOriginFromUrl('https://peertube.example.com/chemin/page?x=1'),
+ 'cspOriginFromUrl ignore le chemin et la query string'
+);
+assertEquals(
+ 'http://127.0.0.1',
+ cspOriginFromUrl('http://127.0.0.1'),
+ 'cspOriginFromUrl conserve le schéma http'
+);
+assertEquals('', cspOriginFromUrl(''), 'cspOriginFromUrl refuse une chaîne vide');
+assertEquals('', cspOriginFromUrl('pas-une-url'), 'cspOriginFromUrl refuse une URL mal formée');
+
+// --- buildContentSecurityPolicy ------------------------------------------------
+
+/**
+ * Extrait la valeur d'une directive CSP pour les assertions
+ */
+function csp_directive($csp, $name) {
+ if (preg_match('/(?:^| )' . preg_quote($name, '/') . ' ([^;]+)/', $csp, $m)) {
+ return $m[1];
+ }
+ return null;
+}
+
+$testNonce = 'nonce-de-test';
+$csp = buildContentSecurityPolicy($testNonce);
+
+assertContains(
+ "'nonce-{$testNonce}",
+ csp_directive($csp, 'script-src'),
+ 'CSP : script-src porte le nonce de la requête'
+);
+assertContains(
+ "'nonce-{$testNonce}",
+ csp_directive($csp, 'style-src'),
+ 'CSP : style-src porte le nonce de la requête'
+);
+
+// Domaines réellement utilisés présents
+$imgSrc = csp_directive($csp, 'img-src');
+$mediaSrc = csp_directive($csp, 'media-src');
+assertContains(cspOriginFromUrl(PEERTUBE_URL), $imgSrc, 'CSP : img-src autorise le domaine PeerTube');
+assertContains(cspOriginFromUrl(MASTODON_INSTANCE_URL), $imgSrc, 'CSP : img-src autorise le domaine Mastodon');
+assertContains(cspOriginFromUrl(PEERTUBE_URL), $mediaSrc, 'CSP : media-src autorise le domaine PeerTube');
+assertContains(cspOriginFromUrl(CASTOPOD_URL), $mediaSrc, 'CSP : media-src autorise le domaine Castopod');
+assertContains(cspOriginFromUrl(CASTOPOD_URL), $imgSrc, 'CSP : img-src autorise les pochettes Castopod');
+
+// Pas de joker https:/http: hors développement local (HTTP_HOST absent en CLI)
+assertTrue(
+ preg_match('/(^|\s)https?:($|\s)/', $imgSrc) === 0,
+ 'CSP : img-src ne contient pas de joker https: en production'
+);
+assertTrue(
+ preg_match('/(^|\s)https?:($|\s)/', $mediaSrc) === 0,
+ 'CSP : media-src ne contient pas de joker https: en production'
+);
+
+// Directives de verrouillage toujours présentes
+assertContains("object-src 'none'", $csp, 'CSP : object-src none est présent');
+assertContains("frame-ancestors 'self'", $csp, 'CSP : frame-ancestors self est présent');
+
+// En développement local, le joker HTTP(S) est réintroduit pour le contenu fédéré
+$_SERVER['HTTP_HOST'] = 'localhost:8080';
+$cspDev = buildContentSecurityPolicy($testNonce);
+assertTrue(
+ preg_match('/(^|\s)https?:($|\s)/', csp_directive($cspDev, 'img-src')) === 1,
+ 'CSP : img-src contient le joker https: en développement local'
+);
+assertTrue(
+ preg_match('/(^|\s)https?:($|\s)/', csp_directive($cspDev, 'media-src')) === 1,
+ 'CSP : media-src contient le joker https: en développement local'
+);
+unset($_SERVER['HTTP_HOST']);
diff --git a/video.php b/video.php
index b246550..54bc946 100644
--- a/video.php
+++ b/video.php
@@ -126,7 +126,7 @@ if (empty($videoData) || isset($videoData['error'])) {
-
+