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) . '\');'; - } - ?> - - - -
- -
- <?php echo htmlspecialchars(NEXT_LIVE_TITLE); ?> -
- -
- - 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'])) { - +