chore: remove dead code and unused legacy files
This commit is contained in:
+11
-13
@@ -2,6 +2,13 @@
|
|||||||
// Inclure la configuration
|
// Inclure la configuration
|
||||||
require_once '../includes/config.php';
|
require_once '../includes/config.php';
|
||||||
|
|
||||||
|
// Limitation de débit : 30 requêtes / minute par IP (l'endpoint proxifie PeerTube)
|
||||||
|
if (!checkRateLimit($_SERVER['REMOTE_ADDR'] ?? 'unknown', 30, 60)) {
|
||||||
|
http_response_code(429); // Trop de requêtes
|
||||||
|
echo json_encode(['error' => 'Trop de requêtes, réessayez plus tard']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
// Vérifier que la requête est faite par AJAX
|
// Vérifier que la requête est faite par AJAX
|
||||||
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
|
if (!isset($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
|
||||||
http_response_code(403); // Accès non autorisé
|
http_response_code(403); // Accès non autorisé
|
||||||
@@ -25,11 +32,13 @@ if (!isset($_POST['csrf_token']) || !validateCSRFToken($_POST['csrf_token'])) {
|
|||||||
|
|
||||||
// Récupérer les paramètres
|
// Récupérer les paramètres
|
||||||
$type = isset($_GET['type']) ? $_GET['type'] : '';
|
$type = isset($_GET['type']) ? $_GET['type'] : '';
|
||||||
$page = isset($_GET['page']) ? intval($_GET['page']) : 1;
|
// Borner la page à [1, 1000] : au-delà il n'y a plus de contenu réel, et des
|
||||||
|
// pages arbitrairement grandes contourneraient le cache de l'API PeerTube.
|
||||||
|
$page = min(1000, validatePageNumber($_GET['page'] ?? 1));
|
||||||
$categoryId = isset($_GET['category']) ? intval($_GET['category']) : 0;
|
$categoryId = isset($_GET['category']) ? intval($_GET['category']) : 0;
|
||||||
|
|
||||||
// Vérifier que le type est valide
|
// Vérifier que le type est valide
|
||||||
if (!in_array($type, ['recent', 'trending', 'independence', 'category'])) {
|
if (!in_array($type, ['recent', 'trending', 'category'])) {
|
||||||
http_response_code(400); // Requête incorrecte
|
http_response_code(400); // Requête incorrecte
|
||||||
echo json_encode(['error' => 'Type de vidéos non valide']);
|
echo json_encode(['error' => 'Type de vidéos non valide']);
|
||||||
exit;
|
exit;
|
||||||
@@ -69,17 +78,6 @@ switch ($type) {
|
|||||||
$videos = formatVideosData($data['data'] ?? []);
|
$videos = formatVideosData($data['data'] ?? []);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'independence':
|
|
||||||
// Récupérer les vidéos sur l'indépendance
|
|
||||||
$data = callPeerTubeApi('videos', [
|
|
||||||
'tagsOneOf' => TAG_INDEPENDENCE,
|
|
||||||
'count' => LOAD_MORE_COUNT,
|
|
||||||
'start' => $offset,
|
|
||||||
'isLocal' => true
|
|
||||||
]);
|
|
||||||
$videos = formatVideosData($data['data'] ?? []);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'category':
|
case 'category':
|
||||||
// Récupérer les vidéos de la catégorie
|
// Récupérer les vidéos de la catégorie
|
||||||
$data = callPeerTubeApi('videos', [
|
$data = callPeerTubeApi('videos', [
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ if (!defined('ORGANIZATION_NAME')) define('ORGANIZATION_NAME', 'ANNU KUTE CED');
|
|||||||
if (!defined('PEERTUBE_URL')) define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
if (!defined('PEERTUBE_URL')) define('PEERTUBE_URL', 'https://gade.o-k-i.net');
|
||||||
if (!defined('PEERTUBE_DISPLAY_NAME')) define('PEERTUBE_DISPLAY_NAME', 'gade.o-k-i.net');
|
if (!defined('PEERTUBE_DISPLAY_NAME')) define('PEERTUBE_DISPLAY_NAME', 'gade.o-k-i.net');
|
||||||
if (!defined('API_KEY')) define('API_KEY', '');
|
if (!defined('API_KEY')) define('API_KEY', '');
|
||||||
if (!defined('TAG_INDEPENDENCE')) define('TAG_INDEPENDENCE', 'indépendance');
|
|
||||||
if (!defined('SHORTS_MAX_DURATION')) define('SHORTS_MAX_DURATION', 180); // 3 minutes max pour les shorts
|
if (!defined('SHORTS_MAX_DURATION')) define('SHORTS_MAX_DURATION', 180); // 3 minutes max pour les shorts
|
||||||
|
|
||||||
// Pagination et affichage
|
// Pagination et affichage
|
||||||
@@ -27,7 +26,6 @@ if (!defined('RECENT_VIDEOS_COUNT')) define('RECENT_VIDEOS_COUNT', 6);
|
|||||||
if (!defined('SHORTS_COUNT')) define('SHORTS_COUNT', 6);
|
if (!defined('SHORTS_COUNT')) define('SHORTS_COUNT', 6);
|
||||||
if (!defined('SHORTS_COUNT_SEARCH')) define('SHORTS_COUNT_SEARCH', 100);
|
if (!defined('SHORTS_COUNT_SEARCH')) define('SHORTS_COUNT_SEARCH', 100);
|
||||||
if (!defined('TRENDING_VIDEOS_COUNT')) define('TRENDING_VIDEOS_COUNT', 6);
|
if (!defined('TRENDING_VIDEOS_COUNT')) define('TRENDING_VIDEOS_COUNT', 6);
|
||||||
if (!defined('INDEPENDENCE_VIDEOS_COUNT')) define('INDEPENDENCE_VIDEOS_COUNT', 6);
|
|
||||||
if (!defined('CATEGORY_VIDEOS_COUNT')) define('CATEGORY_VIDEOS_COUNT', 6);
|
if (!defined('CATEGORY_VIDEOS_COUNT')) define('CATEGORY_VIDEOS_COUNT', 6);
|
||||||
if (!defined('LOAD_MORE_COUNT')) define('LOAD_MORE_COUNT', 6);
|
if (!defined('LOAD_MORE_COUNT')) define('LOAD_MORE_COUNT', 6);
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,6 @@ define('APP_HOST_NAME', 'example.com');
|
|||||||
// Filtres et tags
|
// Filtres et tags
|
||||||
// =========================================
|
// =========================================
|
||||||
|
|
||||||
// Tag pour les vidéos sur l'indépendance
|
|
||||||
// define('TAG_INDEPENDANCE', 'indépendance');
|
|
||||||
|
|
||||||
// Tag pour les shorts
|
// Tag pour les shorts
|
||||||
// define('TAG_SHORT', 'short');
|
// define('TAG_SHORT', 'short');
|
||||||
|
|
||||||
@@ -113,9 +110,6 @@ define('SHORTS_MAX_DURATION', 180); // 3 minutes
|
|||||||
// Nombre de vidéos tendances
|
// Nombre de vidéos tendances
|
||||||
// define('TRENDING_VIDEOS_COUNT', 6);
|
// define('TRENDING_VIDEOS_COUNT', 6);
|
||||||
|
|
||||||
// Nombre de vidéos indépendance
|
|
||||||
// define('INDEPENDENCE_VIDEOS_COUNT', 6);
|
|
||||||
|
|
||||||
// Nombre de vidéos par catégorie
|
// Nombre de vidéos par catégorie
|
||||||
// define('CATEGORY_VIDEOS_COUNT', 6);
|
// define('CATEGORY_VIDEOS_COUNT', 6);
|
||||||
|
|
||||||
|
|||||||
+22
-1005
File diff suppressed because it is too large
Load Diff
@@ -1,77 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Inclure la configuration si ce n'est pas déjà fait
|
|
||||||
if (!function_exists('getTrendingVideos')) {
|
|
||||||
require_once __DIR__ . '/config.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les vidéos tendances depuis l'API PeerTube
|
|
||||||
$featuredVideos = getTrendingVideos(FEATURED_VIDEOS_COUNT);
|
|
||||||
|
|
||||||
// Affichage des vidéos
|
|
||||||
foreach ($featuredVideos as $video):
|
|
||||||
?>
|
|
||||||
<div class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
|
||||||
<div class="video-thumbnail">
|
|
||||||
<img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>" data-src="<?php echo $video['thumbnail']; ?>">
|
|
||||||
<div class="video-duration"><?php echo formatDuration($video['duration']); ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="video-info">
|
|
||||||
<h3 class="video-title"><?php echo $video['title']; ?></h3>
|
|
||||||
<div class="video-channel"><?php echo $video['channel']; ?></div>
|
|
||||||
<div class="video-metadata">
|
|
||||||
<?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?>
|
|
||||||
<span class="video-views"><?php echo formatViewCount($video['views']); ?> vues</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="video-date"><?php echo formatDate($video['date']); ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
|
|
||||||
<?php
|
|
||||||
// Fonctions utilitaires (dans un vrai projet, ces fonctions seraient dans un fichier séparé)
|
|
||||||
function formatDuration($seconds) {
|
|
||||||
$hours = floor($seconds / 3600);
|
|
||||||
$minutes = floor(($seconds % 3600) / 60);
|
|
||||||
$remainingSeconds = $seconds % 60;
|
|
||||||
|
|
||||||
if ($hours > 0) {
|
|
||||||
return sprintf('%d:%02d:%02d', $hours, $minutes, $remainingSeconds);
|
|
||||||
} else {
|
|
||||||
return sprintf('%d:%02d', $minutes, $remainingSeconds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatViewCount($views) {
|
|
||||||
if ($views >= 1000000) {
|
|
||||||
return round($views / 1000000, 1) . 'M';
|
|
||||||
} elseif ($views >= 1000) {
|
|
||||||
return round($views / 1000, 1) . 'K';
|
|
||||||
} else {
|
|
||||||
return $views;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate($dateString) {
|
|
||||||
$date = new DateTime($dateString);
|
|
||||||
$now = new DateTime();
|
|
||||||
$interval = $now->diff($date);
|
|
||||||
|
|
||||||
if ($interval->days == 0) {
|
|
||||||
return 'Aujourd\'hui';
|
|
||||||
} elseif ($interval->days == 1) {
|
|
||||||
return 'Hier';
|
|
||||||
} elseif ($interval->days < 7) {
|
|
||||||
return 'Il y a ' . $interval->days . ' jours';
|
|
||||||
} elseif ($interval->days < 30) {
|
|
||||||
$weeks = floor($interval->days / 7);
|
|
||||||
return 'Il y a ' . $weeks . ' semaine' . ($weeks > 1 ? 's' : '');
|
|
||||||
} elseif ($interval->days < 365) {
|
|
||||||
$months = floor($interval->days / 30);
|
|
||||||
return 'Il y a ' . $months . ' mois';
|
|
||||||
} else {
|
|
||||||
$years = floor($interval->days / 365);
|
|
||||||
return 'Il y a ' . $years . ' an' . ($years > 1 ? 's' : '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Fichier d'initialisation PWA à inclure dans toutes les pages
|
|
||||||
function addPWAHeaders() {
|
|
||||||
// Meta tags PWA
|
|
||||||
echo '<meta name="mobile-web-app-capable" content="yes">' . "\n";
|
|
||||||
echo '<meta name="apple-mobile-web-app-capable" content="yes">' . "\n";
|
|
||||||
echo '<meta name="apple-mobile-web-app-status-bar-style" content="default">' . "\n";
|
|
||||||
echo '<meta name="apple-mobile-web-app-title" content="' . SITE_NAME . '">' . "\n";
|
|
||||||
echo '<meta name="application-name" content="' . SITE_NAME . '">' . "\n";
|
|
||||||
echo '<meta name="msapplication-TileColor" content="#FF0000">' . "\n";
|
|
||||||
echo '<meta name="msapplication-config" content="browserconfig.xml">' . "\n";
|
|
||||||
echo '<meta name="theme-color" content="#FF0000">' . "\n";
|
|
||||||
|
|
||||||
// Manifest
|
|
||||||
echo '<link rel="manifest" href="site.webmanifest">' . "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
function addPWAScripts() {
|
|
||||||
?>
|
|
||||||
<!-- PWA Service Worker -->
|
|
||||||
<script nonce="<?php echo getCspNonce(); ?>">
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', function() {
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(function(registration) {
|
|
||||||
console.log('Service Worker enregistré avec succès:', registration.scope);
|
|
||||||
|
|
||||||
// Écouter les mises à jour
|
|
||||||
registration.addEventListener('updatefound', function() {
|
|
||||||
const newWorker = registration.installing;
|
|
||||||
newWorker.addEventListener('statechange', function() {
|
|
||||||
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
|
|
||||||
// Nouvelle version disponible
|
|
||||||
console.log('Nouvelle version disponible');
|
|
||||||
if (confirm('Une nouvelle version est disponible. Voulez-vous recharger la page ?')) {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(function(err) {
|
|
||||||
console.log('Échec de l\'enregistrement du Service Worker:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gestion de l'installation PWA
|
|
||||||
let deferredPrompt;
|
|
||||||
const installButton = document.getElementById('install-pwa');
|
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', function(e) {
|
|
||||||
e.preventDefault();
|
|
||||||
deferredPrompt = e;
|
|
||||||
|
|
||||||
// Afficher le bouton d'installation s'il existe
|
|
||||||
if (installButton) {
|
|
||||||
installButton.classList.remove('is-hidden');
|
|
||||||
installButton.addEventListener('click', function() {
|
|
||||||
deferredPrompt.prompt();
|
|
||||||
deferredPrompt.userChoice.then(function(choiceResult) {
|
|
||||||
if (choiceResult.outcome === 'accepted') {
|
|
||||||
console.log('PWA installée');
|
|
||||||
}
|
|
||||||
deferredPrompt = null;
|
|
||||||
installButton.classList.add('is-hidden');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Masquer le bouton après installation
|
|
||||||
window.addEventListener('appinstalled', function() {
|
|
||||||
console.log('PWA installée avec succès');
|
|
||||||
if (installButton) {
|
|
||||||
installButton.classList.add('is-hidden');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
// Inclure la configuration si ce n'est pas déjà fait
|
|
||||||
if (!function_exists('getRecentVideos')) {
|
|
||||||
require_once __DIR__ . '/config.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Récupérer les vidéos récentes depuis l'API PeerTube
|
|
||||||
$recentVideos = getRecentVideos();
|
|
||||||
|
|
||||||
// Affichage des vidéos
|
|
||||||
foreach ($recentVideos as $video):
|
|
||||||
?>
|
|
||||||
<div class="video-card" data-video-id="<?php echo $video['id']; ?>">
|
|
||||||
<div class="video-thumbnail">
|
|
||||||
<img src="<?php echo $video['thumbnail']; ?>" alt="<?php echo $video['title']; ?>" data-src="<?php echo $video['thumbnail']; ?>">
|
|
||||||
<div class="video-duration"><?php echo formatDuration($video['duration']); ?></div>
|
|
||||||
</div>
|
|
||||||
<div class="video-info">
|
|
||||||
<h3 class="video-title"><?php echo $video['title']; ?></h3>
|
|
||||||
<div class="video-channel"><?php echo $video['channel']; ?></div>
|
|
||||||
<div class="video-metadata">
|
|
||||||
<?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?>
|
|
||||||
<span class="video-views"><?php echo formatViewCount($video['views']); ?> vues</span>
|
|
||||||
<?php endif; ?>
|
|
||||||
<span class="video-date"><?php echo formatDate($video['date']); ?></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
+157
-150
@@ -92,69 +92,6 @@ function validateCategoryId($categoryId) {
|
|||||||
return $categoryId;
|
return $categoryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Valide et assainit un User-Agent
|
|
||||||
*
|
|
||||||
* @param string $userAgent User-Agent à valider
|
|
||||||
* @return bool True si valide
|
|
||||||
*/
|
|
||||||
function validateUserAgent($userAgent) {
|
|
||||||
if (empty($userAgent)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bloquer les User-Agents suspects
|
|
||||||
$blockedPatterns = [
|
|
||||||
'/curl/i',
|
|
||||||
'/wget/i',
|
|
||||||
'/python/i',
|
|
||||||
'/bot/i',
|
|
||||||
'/scanner/i',
|
|
||||||
'/sqlmap/i'
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($blockedPatterns as $pattern) {
|
|
||||||
if (preg_match($pattern, $userAgent)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Valide les en-têtes HTTP pour détecter les tentatives d'attaque
|
|
||||||
*
|
|
||||||
* @return bool True si les en-têtes sont sûrs
|
|
||||||
*/
|
|
||||||
function validateHttpHeaders() {
|
|
||||||
// Vérifier le User-Agent
|
|
||||||
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
||||||
if (!validateUserAgent($userAgent)) {
|
|
||||||
error_log('SECURITY: Suspicious User-Agent detected: ' . $userAgent);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vérifier les en-têtes suspects
|
|
||||||
$suspiciousHeaders = [
|
|
||||||
'HTTP_X_FORWARDED_FOR',
|
|
||||||
'HTTP_X_REAL_IP',
|
|
||||||
'HTTP_CLIENT_IP'
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($suspiciousHeaders as $header) {
|
|
||||||
if (isset($_SERVER[$header])) {
|
|
||||||
$value = $_SERVER[$header];
|
|
||||||
// Bloquer les IPs privées dans les en-têtes de forwarding
|
|
||||||
if (filter_var($value, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
|
|
||||||
error_log('SECURITY: Suspicious IP in header ' . $header . ': ' . $value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valeur par défaut livrée dans config.default.php : si CSRF_SECRET vaut
|
* Valeur par défaut livrée dans config.default.php : si CSRF_SECRET vaut
|
||||||
* encore cette valeur, le secret n'a pas été configuré pour l'instance.
|
* encore cette valeur, le secret n'a pas été configuré pour l'instance.
|
||||||
@@ -252,6 +189,104 @@ function getCspNonce() {
|
|||||||
return $GLOBALS['csp_nonce'];
|
return $GLOBALS['csp_nonce'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrait l'origine « scheme://host » d'une URL configurée (pour la CSP).
|
||||||
|
*
|
||||||
|
* @param string $url URL à analyser
|
||||||
|
* @return string Origine normalisée, ou chaîne vide si l'URL est vide/invalide
|
||||||
|
*/
|
||||||
|
function cspOriginFromUrl($url) {
|
||||||
|
if (empty($url)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
$parsed = parse_url($url);
|
||||||
|
if (!$parsed || !isset($parsed['scheme'], $parsed['host'])) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return $parsed['scheme'] . '://' . $parsed['host'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construit la Content Security Policy de la requête courante.
|
||||||
|
*
|
||||||
|
* img-src et media-src sont restreints aux domaines réellement utilisés :
|
||||||
|
* PeerTube (vignettes), Mastodon (avatars/médias) et son S3 éventuel,
|
||||||
|
* Castopod (pochettes/flux audio), Funkwhale (pochettes/flux audio) et
|
||||||
|
* WordPress (images mises en avant). En développement local, HTTP(S) général
|
||||||
|
* reste autorisé pour faciliter les tests avec du contenu fédéré.
|
||||||
|
*
|
||||||
|
* @param string $nonce Nonce CSP de la requête
|
||||||
|
* @return string Politique CSP complète
|
||||||
|
*/
|
||||||
|
function buildContentSecurityPolicy($nonce) {
|
||||||
|
$mastodonDomain = defined('MASTODON_INSTANCE_URL') ? cspOriginFromUrl(MASTODON_INSTANCE_URL) : '';
|
||||||
|
$peertubeDomain = defined('PEERTUBE_URL') ? cspOriginFromUrl(PEERTUBE_URL) : '';
|
||||||
|
$castopodDomain = (defined('CASTOPOD_ENABLED') && CASTOPOD_ENABLED && defined('CASTOPOD_URL'))
|
||||||
|
? cspOriginFromUrl(CASTOPOD_URL) : '';
|
||||||
|
$funkwhaleDomain = (defined('FUNKWHALE_ENABLED') && FUNKWHALE_ENABLED && defined('FUNKWHALE_URL'))
|
||||||
|
? cspOriginFromUrl(FUNKWHALE_URL) : '';
|
||||||
|
$wordpressDomain = defined('WORDPRESS_URL') ? cspOriginFromUrl(WORDPRESS_URL) : '';
|
||||||
|
$s3Domain = defined('MASTODON_S3_MEDIA_URL') ? cspOriginFromUrl(MASTODON_S3_MEDIA_URL) : '';
|
||||||
|
|
||||||
|
// Détecter si on est en développement local
|
||||||
|
$isLocalDev = in_array($_SERVER['HTTP_HOST'] ?? '', ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']);
|
||||||
|
|
||||||
|
$csp = "default-src 'self'; ";
|
||||||
|
$csp .= "style-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; ";
|
||||||
|
$csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; ";
|
||||||
|
|
||||||
|
// Images : uniquement les services réellement affichés (https: général en dev uniquement)
|
||||||
|
$imgSrc = "'self' data:";
|
||||||
|
foreach ([$mastodonDomain, $peertubeDomain, $castopodDomain, $funkwhaleDomain, $wordpressDomain, $s3Domain] as $domain) {
|
||||||
|
if ($domain !== '') {
|
||||||
|
$imgSrc .= ' ' . $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isLocalDev) {
|
||||||
|
$imgSrc .= ' https: http:';
|
||||||
|
}
|
||||||
|
$csp .= "img-src " . $imgSrc . "; ";
|
||||||
|
|
||||||
|
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
|
||||||
|
|
||||||
|
// Frames : autoriser PeerTube uniquement
|
||||||
|
$frameSrc = "'self'" . ($peertubeDomain !== '' ? ' ' . $peertubeDomain : '');
|
||||||
|
if ($isLocalDev) {
|
||||||
|
$frameSrc .= ' https: http:';
|
||||||
|
}
|
||||||
|
$csp .= "frame-src " . $frameSrc . "; ";
|
||||||
|
|
||||||
|
// Connexions : autoriser Mastodon, PeerTube et Plausible
|
||||||
|
$connectSrc = "'self' https://plausible.io";
|
||||||
|
foreach ([$mastodonDomain, $peertubeDomain] as $domain) {
|
||||||
|
if ($domain !== '') {
|
||||||
|
$connectSrc .= ' ' . $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isLocalDev) {
|
||||||
|
$connectSrc .= ' ws: wss:'; // WebSockets pour le dev
|
||||||
|
}
|
||||||
|
$csp .= "connect-src " . $connectSrc . "; ";
|
||||||
|
|
||||||
|
// Médias : flux audio Castopod/Funkwhale, médias Mastodon (instance ou S3)
|
||||||
|
$mediaSrc = "'self'";
|
||||||
|
foreach ([$mastodonDomain, $peertubeDomain, $castopodDomain, $funkwhaleDomain, $s3Domain] as $domain) {
|
||||||
|
if ($domain !== '') {
|
||||||
|
$mediaSrc .= ' ' . $domain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($isLocalDev) {
|
||||||
|
$mediaSrc .= ' https: http:';
|
||||||
|
}
|
||||||
|
$csp .= "media-src " . $mediaSrc . "; ";
|
||||||
|
|
||||||
|
$csp .= "object-src 'none'; ";
|
||||||
|
$csp .= "base-uri 'self'; ";
|
||||||
|
$csp .= "frame-ancestors 'self';";
|
||||||
|
|
||||||
|
return $csp;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applique des en-têtes de sécurité HTTP
|
* Applique des en-têtes de sécurité HTTP
|
||||||
*/
|
*/
|
||||||
@@ -262,8 +297,8 @@ function setSecurityHeaders() {
|
|||||||
// Protection contre le MIME sniffing
|
// Protection contre le MIME sniffing
|
||||||
header('X-Content-Type-Options: nosniff');
|
header('X-Content-Type-Options: nosniff');
|
||||||
|
|
||||||
// Protection XSS basique
|
// X-XSS-Protection volontairement absent : en-tête obsolète, supplanté par
|
||||||
header('X-XSS-Protection: 1; mode=block');
|
// la CSP et pouvant introduire des vulnérabilités dans les anciens navigateurs.
|
||||||
|
|
||||||
// Politique de référent
|
// Politique de référent
|
||||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||||
@@ -277,91 +312,8 @@ function setSecurityHeaders() {
|
|||||||
// Permissions Policy (feature policy)
|
// Permissions Policy (feature policy)
|
||||||
header('Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()');
|
header('Permissions-Policy: accelerometer=(), autoplay=(), camera=(), cross-origin-isolated=(), display-capture=(), encrypted-media=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(self), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=()');
|
||||||
|
|
||||||
// Content Security Policy avec support Mastodon et PeerTube
|
// Content Security Policy (domaines réellement utilisés uniquement)
|
||||||
$nonce = getCspNonce();
|
header('Content-Security-Policy: ' . buildContentSecurityPolicy(getCspNonce()));
|
||||||
$mastodonDomain = '';
|
|
||||||
$peertubeDomain = '';
|
|
||||||
|
|
||||||
// Extraire le domaine Mastodon si configuré
|
|
||||||
if (defined('MASTODON_INSTANCE_URL')) {
|
|
||||||
$mastodonParsed = parse_url(MASTODON_INSTANCE_URL);
|
|
||||||
if ($mastodonParsed && isset($mastodonParsed['host'])) {
|
|
||||||
$mastodonDomain = $mastodonParsed['scheme'] . '://' . $mastodonParsed['host'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extraire le domaine PeerTube si configuré
|
|
||||||
if (defined('PEERTUBE_URL')) {
|
|
||||||
$peertubeParsed = parse_url(PEERTUBE_URL);
|
|
||||||
if ($peertubeParsed && isset($peertubeParsed['host'])) {
|
|
||||||
$peertubeDomain = $peertubeParsed['scheme'] . '://' . $peertubeParsed['host'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Détecter si on est en développement local
|
|
||||||
$isLocalDev = in_array($_SERVER['HTTP_HOST'] ?? '', ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']);
|
|
||||||
|
|
||||||
$csp = "default-src 'self'; ";
|
|
||||||
$csp .= "style-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com; ";
|
|
||||||
$csp .= "script-src 'self' 'nonce-{$nonce}' https://cdnjs.cloudflare.com https://plausible.io; ";
|
|
||||||
|
|
||||||
// Images : autoriser les domaines connus plus HTTPS général pour le contenu fédéré
|
|
||||||
$imgSrc = "'self' data: " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
|
|
||||||
if ($isLocalDev) {
|
|
||||||
$imgSrc .= " https: http:";
|
|
||||||
} else {
|
|
||||||
$imgSrc .= " https:";
|
|
||||||
}
|
|
||||||
$csp .= "img-src " . $imgSrc . "; ";
|
|
||||||
|
|
||||||
$csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
|
|
||||||
|
|
||||||
// Frames : autoriser PeerTube uniquement
|
|
||||||
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : '');
|
|
||||||
if ($isLocalDev) {
|
|
||||||
$frameSrc .= " https: http:";
|
|
||||||
}
|
|
||||||
$csp .= "frame-src " . $frameSrc . "; ";
|
|
||||||
|
|
||||||
// Connexions : autoriser Mastodon, PeerTube et Plausible
|
|
||||||
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
|
|
||||||
if ($isLocalDev) {
|
|
||||||
$connectSrc .= " ws: wss:"; // WebSockets pour le dev
|
|
||||||
}
|
|
||||||
$csp .= "connect-src " . $connectSrc . "; ";
|
|
||||||
|
|
||||||
// Médias : autoriser 'self', Mastodon, PeerTube et S3 Mastodon
|
|
||||||
$mediaSrc = "'self'";
|
|
||||||
|
|
||||||
if ($mastodonDomain) {
|
|
||||||
$mediaSrc .= " " . $mastodonDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($peertubeDomain) {
|
|
||||||
$mediaSrc .= " " . $peertubeDomain;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ajouter l'URL S3 Mastodon si configurée (pour les médias externalisés)
|
|
||||||
if (defined('MASTODON_S3_MEDIA_URL') && !empty(MASTODON_S3_MEDIA_URL)) {
|
|
||||||
$s3Parsed = parse_url(MASTODON_S3_MEDIA_URL);
|
|
||||||
if ($s3Parsed && isset($s3Parsed['host'])) {
|
|
||||||
$s3Domain = $s3Parsed['scheme'] . '://' . $s3Parsed['host'];
|
|
||||||
$mediaSrc .= " " . $s3Domain;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isLocalDev) {
|
|
||||||
$mediaSrc .= " https: http:";
|
|
||||||
} else {
|
|
||||||
$mediaSrc .= " https:";
|
|
||||||
}
|
|
||||||
$csp .= "media-src " . $mediaSrc . "; ";
|
|
||||||
|
|
||||||
$csp .= "object-src 'none'; ";
|
|
||||||
$csp .= "base-uri 'self'; ";
|
|
||||||
$csp .= "frame-ancestors 'self';";
|
|
||||||
|
|
||||||
header('Content-Security-Policy: ' . $csp);
|
|
||||||
|
|
||||||
// HTTPS strict transport security (seulement si HTTPS)
|
// HTTPS strict transport security (seulement si HTTPS)
|
||||||
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
|
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
|
||||||
@@ -400,6 +352,61 @@ function validateAjaxOrigin() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Limitation de débit simple par identifiant (typiquement l'IP cliente).
|
||||||
|
*
|
||||||
|
* Fenêtre fixe stockée dans un fichier par identifiant (cache/rate-limit/),
|
||||||
|
* verrouillée par flock() pour rester cohérente entre requêtes concurrentes.
|
||||||
|
* En cas d'indisponibilité du stockage, la requête est autorisée (fail-open) :
|
||||||
|
* l'endpoint reste protégé par les gardes AJAX, Origin et CSRF.
|
||||||
|
*
|
||||||
|
* @param string $identifier Identifiant du client (ex. REMOTE_ADDR)
|
||||||
|
* @param int $maxRequests Nombre maximal de requêtes dans la fenêtre
|
||||||
|
* @param int $windowSeconds Durée de la fenêtre en secondes
|
||||||
|
* @param string|null $dir Répertoire de stockage (surtout pour les tests)
|
||||||
|
* @return bool True si la requête est autorisée, false si la limite est atteinte
|
||||||
|
*/
|
||||||
|
function checkRateLimit($identifier, $maxRequests = 30, $windowSeconds = 60, $dir = null) {
|
||||||
|
$dir = $dir ?? (__DIR__ . '/../cache/rate-limit');
|
||||||
|
if (!is_dir($dir) && !mkdir($dir, 0755, true)) {
|
||||||
|
error_log('SECURITY: rate limit storage unavailable: ' . $dir);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$file = $dir . '/rl_' . hash('sha256', (string) $identifier) . '.json';
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
$handle = fopen($file, 'c+');
|
||||||
|
if ($handle === false) {
|
||||||
|
error_log('SECURITY: rate limit file unavailable: ' . $file);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = true;
|
||||||
|
if (flock($handle, LOCK_EX)) {
|
||||||
|
$raw = stream_get_contents($handle);
|
||||||
|
$state = $raw !== false ? json_decode($raw, true) : null;
|
||||||
|
|
||||||
|
if (!is_array($state) || !isset($state['reset']) || $now >= $state['reset']) {
|
||||||
|
$state = ['count' => 0, 'reset' => $now + $windowSeconds];
|
||||||
|
}
|
||||||
|
|
||||||
|
$state['count']++;
|
||||||
|
if ($state['count'] > $maxRequests) {
|
||||||
|
$allowed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rewind($handle);
|
||||||
|
ftruncate($handle, 0);
|
||||||
|
fwrite($handle, json_encode($state));
|
||||||
|
fflush($handle);
|
||||||
|
flock($handle, LOCK_UN);
|
||||||
|
}
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return $allowed;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Valide une URL distante (PeerTube, Castopod, Funkwhale…) pour prévenir
|
* Valide une URL distante (PeerTube, Castopod, Funkwhale…) pour prévenir
|
||||||
* les attaques SSRF avant tout appel sortant.
|
* les attaques SSRF avant tout appel sortant.
|
||||||
|
|||||||
@@ -428,8 +428,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
videoType = 'recent';
|
videoType = 'recent';
|
||||||
} else if (sectionTitle.includes('tendances')) {
|
} else if (sectionTitle.includes('tendances')) {
|
||||||
videoType = 'trending';
|
videoType = 'trending';
|
||||||
} else if (sectionTitle.includes('indépendance')) {
|
|
||||||
videoType = 'independence';
|
|
||||||
} else {
|
} else {
|
||||||
// Vérifier si c'est une section de catégorie
|
// Vérifier si c'est une section de catégorie
|
||||||
const categorySection = section.querySelector('[data-category-id]');
|
const categorySection = section.querySelector('[data-category-id]');
|
||||||
|
|||||||
@@ -5,6 +5,10 @@
|
|||||||
const STATIC_CACHE_NAME = 'annu-kute-ced-static-26072026-1946';
|
const STATIC_CACHE_NAME = 'annu-kute-ced-static-26072026-1946';
|
||||||
const DYNAMIC_CACHE_NAME = 'annu-kute-ced-dynamic-26072026-1946';
|
const DYNAMIC_CACHE_NAME = 'annu-kute-ced-dynamic-26072026-1946';
|
||||||
|
|
||||||
|
// Nombre maximal d'entrées conservées dans le cache dynamique (LRU :
|
||||||
|
// les entrées les moins récemment utilisées sont évincées en premier).
|
||||||
|
const DYNAMIC_CACHE_LIMIT = 50;
|
||||||
|
|
||||||
// Ressources à mettre en cache immédiatement
|
// Ressources à mettre en cache immédiatement
|
||||||
const STATIC_ASSETS = [
|
const STATIC_ASSETS = [
|
||||||
'/',
|
'/',
|
||||||
@@ -29,15 +33,6 @@ const STATIC_ASSETS = [
|
|||||||
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css'
|
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/7.0.1/css/all.min.css'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Pages à mettre en cache
|
|
||||||
const PAGES_TO_CACHE = [
|
|
||||||
'/',
|
|
||||||
'/index.php',
|
|
||||||
'/categories.php',
|
|
||||||
'/recherche.php',
|
|
||||||
'/mentions-legales.php'
|
|
||||||
];
|
|
||||||
|
|
||||||
// Installation du Service Worker
|
// Installation du Service Worker
|
||||||
self.addEventListener('install', event => {
|
self.addEventListener('install', event => {
|
||||||
console.log('Service Worker: Installation');
|
console.log('Service Worker: Installation');
|
||||||
@@ -133,7 +128,13 @@ self.addEventListener('fetch', event => {
|
|||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
const responseClone = response.clone();
|
const responseClone = response.clone();
|
||||||
caches.open(DYNAMIC_CACHE_NAME)
|
caches.open(DYNAMIC_CACHE_NAME)
|
||||||
.then(cache => cache.put(request, responseClone));
|
.then(cache => {
|
||||||
|
// cache.put() replace une entrée existante en fin de liste
|
||||||
|
// (clés ordonnées par insertion) : la taille limitée évince
|
||||||
|
// donc bien les entrées les moins récemment utilisées.
|
||||||
|
return cache.put(request, responseClone)
|
||||||
|
.then(() => limitCacheSize(cache, DYNAMIC_CACHE_LIMIT));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
})
|
})
|
||||||
@@ -175,6 +176,15 @@ self.addEventListener('fetch', event => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Fonctions utilitaires
|
// Fonctions utilitaires
|
||||||
|
// Évince les plus anciennes clés tant que le cache dépasse maxItems entrées.
|
||||||
|
function limitCacheSize(cache, maxItems) {
|
||||||
|
return cache.keys().then(keys => {
|
||||||
|
if (keys.length > maxItems) {
|
||||||
|
return cache.delete(keys[0]).then(() => limitCacheSize(cache, maxItems));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isStaticAsset(url) {
|
function isStaticAsset(url) {
|
||||||
return url.includes('/css/') ||
|
return url.includes('/css/') ||
|
||||||
url.includes('/js/') ||
|
url.includes('/js/') ||
|
||||||
|
|||||||
Reference in New Issue
Block a user