fix: load markdown in direct.php, allow video-channels, guard modals

This commit is contained in:
2026-07-27 01:54:34 +04:00
parent 9b460a2550
commit 442c262539
5 changed files with 227 additions and 83 deletions
+47 -37
View File
@@ -9,6 +9,9 @@ require_once __DIR__ . '/simple-cache.php';
// Charger les fonctions WordPress
require_once __DIR__ . '/wordpress.php';
// Charger le partial de carte vidéo (échappement centralisé des données API)
require_once __DIR__ . '/partials/video-card.php';
// Charger d'abord la configuration locale si elle existe
$config_local_file = __DIR__ . '/config.local.php';
if (file_exists($config_local_file)) {
@@ -172,37 +175,13 @@ function callPeerTubeApi($endpoint, $params = []) {
/**
* Valide l'URL PeerTube pour prévenir les attaques SSRF
* Alias historique de isValidRemoteUrl() (includes/security.php).
*
* @param string $url URL à valider
* @return bool True si l'URL est valide et sûre
*/
function isValidPeerTubeUrl($url) {
// Vérifier que l'URL est bien formée
$parsed = parse_url($url);
if (!$parsed || !isset($parsed['scheme']) || !isset($parsed['host'])) {
return false;
}
// Autoriser uniquement HTTPS (ou HTTP en développement)
if (!in_array($parsed['scheme'], ['https', 'http'])) {
return false;
}
// Bloquer les adresses IP privées et locales
$host = $parsed['host'];
if (filter_var($host, FILTER_VALIDATE_IP)) {
if (!filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return false;
}
}
// Bloquer localhost et autres domaines dangereux
$blockedHosts = ['localhost', '127.0.0.1', '::1', '0.0.0.0', 'metadata.google.internal'];
if (in_array(strtolower($host), $blockedHosts)) {
return false;
}
return true;
return isValidRemoteUrl($url);
}
/**
@@ -230,7 +209,8 @@ function isValidApiEndpoint($endpoint) {
'videos/.*', // Pour les endpoints dynamiques comme videos/{id}
'videos/.*/comment-threads', // Pour les commentaires
'accounts',
'accounts/.*/videos' // Pour les vidéos d'un compte spécifique
'accounts/.*/videos', // Pour les vidéos d'un compte spécifique
'video-channels/.*/videos' // Pour les vidéos d'une chaîne spécifique
];
foreach ($allowedEndpoints as $pattern) {
@@ -407,30 +387,35 @@ function formatVideosData($videosData) {
$videos = [];
foreach ($videosData as $video) {
// Ignorer les entrées sans uuid : l'identifiant est indispensable
if (empty($video['uuid'])) {
continue;
}
// Récupérer la vignette (thumbnail)
$thumbnail = isset($video['previewPath'])
? PEERTUBE_URL . $video['previewPath']
: 'img/default-thumbnail.jpg';
// Récupérer l'avatar de la chaîne
$channelAvatar = isset($video['channel']['avatars'][0]['path']) && isset($video['channel']['avatars'][0]['path'])
$channelAvatar = isset($video['channel']['avatars'][0]['path'])
? PEERTUBE_URL . $video['channel']['avatars'][0]['path']
: 'img/default-avatar.png';
// Formater les données
// Formater les données (valeurs par défaut pour les champs absents)
$videos[] = [
'id' => $video['uuid'],
'title' => $video['name'],
'title' => $video['name'] ?? '',
'thumbnail' => $thumbnail,
'duration' => $video['duration'],
'channel' => $video['channel']['displayName'],
'duration' => $video['duration'] ?? 0,
'channel' => $video['channel']['displayName'] ?? '',
'channelAvatar' => $channelAvatar,
'views' => $video['views'],
'date' => $video['publishedAt'],
'aspectRatio' => $video['aspectRatio'],
'views' => $video['views'] ?? 0,
'date' => $video['publishedAt'] ?? '',
'aspectRatio' => $video['aspectRatio'] ?? null,
'description' => $video['description'] ?? '',
'tags' => $video['tags'] ?? [],
'isLive' => isset($video['isLive']) ? $video['isLive'] : false
'isLive' => $video['isLive'] ?? false
];
}
@@ -461,7 +446,18 @@ function formatViewCount($views) {
}
function formatDate($dateString) {
$date = new DateTime($dateString);
// Chaîne vide : rien à formater (new DateTime('') renverrait « maintenant »)
if (!is_string($dateString) || trim($dateString) === '') {
return (string) $dateString;
}
try {
$date = new DateTime($dateString);
} catch (Exception $e) {
// Date malformée (données API inattendues) : afficher la chaîne brute
return $dateString;
}
$now = new DateTime();
$interval = $now->diff($date);
@@ -667,6 +663,12 @@ function getCastopodEpisodes($castopodUrl = null, $podcastSlugs = null, $count =
$podcastSlugs = $podcastSlugs ?? (defined('CASTOPOD_PODCAST_SLUGS') ? CASTOPOD_PODCAST_SLUGS : ['annu_kute_cedric']);
$count = $count ?? CASTOPOD_EPISODES_COUNT;
// Validation de l'URL Castopod pour prévenir SSRF
if (!isValidRemoteUrl($castopodUrl)) {
error_log('SECURITY: Invalid Castopod URL detected: ' . $castopodUrl);
return [];
}
// Convertir en tableau si c'est une chaîne unique (rétrocompatibilité)
if (is_string($podcastSlugs)) {
$podcastSlugs = [$podcastSlugs];
@@ -702,6 +704,7 @@ function getCastopodEpisodes($castopodUrl = null, $podcastSlugs = null, $count =
curl_setopt($ch, CURLOPT_URL, $feedUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$xmlContent = curl_exec($ch);
@@ -890,6 +893,12 @@ function getFunkwhaleTracks($funkwhaleUrl = null, $count = null) {
$funkwhaleUrl = $funkwhaleUrl ?? FUNKWHALE_URL;
$count = $count ?? FUNKWHALE_TRACKS_COUNT;
// Validation de l'URL Funkwhale pour prévenir SSRF
if (!isValidRemoteUrl($funkwhaleUrl)) {
error_log('SECURITY: Invalid Funkwhale URL detected: ' . $funkwhaleUrl);
return [];
}
// Clé de cache - on récupère un grand nombre de morceaux pour le cache
$cacheKey = 'funkwhale_' . md5($funkwhaleUrl);
$cacheFetchSize = 50; // Nombre de morceaux à mettre en cache
@@ -912,6 +921,7 @@ function getFunkwhaleTracks($funkwhaleUrl = null, $count = null) {
curl_setopt($ch, CURLOPT_URL, $apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$jsonContent = curl_exec($ch);