fix: load markdown in direct.php, allow video-channels, guard modals
This commit is contained in:
+11
-4
@@ -9,6 +9,8 @@ if (defined('COUNTDOWN_ENABLED') && COUNTDOWN_ENABLED === true) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Inclure le convertisseur Markdown
|
||||
require_once 'includes/lib/markdown.php';
|
||||
// Inclure les fonctions de données structurées
|
||||
require_once 'includes/structured-data.php';
|
||||
// Appliquer les en-têtes de sécurité
|
||||
@@ -127,7 +129,7 @@ $liveStream = getLiveStream();
|
||||
</div>
|
||||
<div class="live-player">
|
||||
<iframe
|
||||
src="<?php echo PEERTUBE_URL; ?>/videos/embed/<?php echo $liveStream['id']; ?>?autoplay=1"
|
||||
src="<?php echo e(PEERTUBE_URL . '/videos/embed/' . $liveStream['id'] . '?autoplay=1'); ?>"
|
||||
frameborder="0"
|
||||
allowfullscreen="allowfullscreen"
|
||||
allow="autoplay; fullscreen"
|
||||
@@ -142,9 +144,9 @@ $liveStream = getLiveStream();
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<img src="<?php echo $liveStream['channelAvatar']; ?>" alt="<?php echo $liveStream['channel']; ?>" class="channel-avatar">
|
||||
<img src="<?php echo e($liveStream['channelAvatar']); ?>" alt="<?php echo e($liveStream['channel']); ?>" class="channel-avatar">
|
||||
<?php endif; ?>
|
||||
<span class="channel-name"><?php echo $liveStream['channel']; ?></span>
|
||||
<span class="channel-name"><?php echo e($liveStream['channel']); ?></span>
|
||||
</div>
|
||||
<?php if (!empty($liveStream['description'])): ?>
|
||||
<div class="live-description">
|
||||
@@ -165,7 +167,12 @@ $liveStream = getLiveStream();
|
||||
$bgImageStyle = 'background-image: url(\'' . htmlspecialchars(NEXT_LIVE_IMAGE) . '\');';
|
||||
}
|
||||
?>
|
||||
<div class="next-live-announcement" style="<?php echo $bgImageStyle; ?>" nonce="<?php echo getCspNonce(); ?>">
|
||||
<?php if (!empty($bgImageStyle)): ?>
|
||||
<style nonce="<?php echo getCspNonce(); ?>">
|
||||
.next-live-announcement { <?php echo $bgImageStyle; ?> }
|
||||
</style>
|
||||
<?php endif; ?>
|
||||
<div class="next-live-announcement">
|
||||
<?php if (!empty(NEXT_LIVE_IMAGE) && file_exists(NEXT_LIVE_IMAGE)): ?>
|
||||
<div class="next-live-image-container">
|
||||
<img src="<?php echo htmlspecialchars(NEXT_LIVE_IMAGE); ?>"
|
||||
|
||||
+46
-36
@@ -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) {
|
||||
// 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);
|
||||
|
||||
@@ -88,3 +88,28 @@ def test_video_page_invalid_id_redirects(page, base_url):
|
||||
page.goto(base_url + "/video.php?id=pas-un-uuid-valide",
|
||||
wait_until="domcontentloaded")
|
||||
assert page.url.endswith("/index.php"), f"URL après redirection : {page.url}"
|
||||
|
||||
|
||||
def test_video_page_not_found_no_js_errors(page, base_url):
|
||||
"""Un UUID bien formé mais inexistant affiche « Vidéo non trouvée » sans erreur JS."""
|
||||
erreurs_js = []
|
||||
page.on("pageerror", lambda exc: erreurs_js.append(str(exc)))
|
||||
page.goto(base_url + "/video.php?id=00000000-0000-0000-0000-000000000000",
|
||||
wait_until="load")
|
||||
|
||||
# La page d'erreur est affichée (pas de redirection : l'UUID est bien formé ;
|
||||
# si l'API est indisponible, la réponse vide mène au même état)
|
||||
assert page.locator(".error-message").count() == 1, "Le message d'erreur est absent"
|
||||
assert page.locator("#error-heading").inner_text() == "Vidéo non trouvée"
|
||||
|
||||
# Les modales téléchargement/partage et leur script ne doivent pas être rendus
|
||||
assert page.locator("#download-modal").count() == 0, (
|
||||
"La modale de téléchargement est présente sur la page d'erreur"
|
||||
)
|
||||
assert page.locator("#share-modal").count() == 0, (
|
||||
"La modale de partage est présente sur la page d'erreur"
|
||||
)
|
||||
|
||||
assert erreurs_js == [], (
|
||||
f"Erreurs JS sur la page « vidéo introuvable » : {erreurs_js}"
|
||||
)
|
||||
|
||||
@@ -141,6 +141,18 @@ assertTrue(
|
||||
isValidApiEndpoint('accounts/annu_kute_ced/videos'),
|
||||
'isValidApiEndpoint accepte les vidéos d\'un compte'
|
||||
);
|
||||
assertTrue(
|
||||
isValidApiEndpoint('video-channels/annu_kute_ced/videos'),
|
||||
'isValidApiEndpoint accepte les vidéos d\'une chaîne'
|
||||
);
|
||||
assertFalse(
|
||||
isValidApiEndpoint('video-channels/annu_kute_ced'),
|
||||
'isValidApiEndpoint refuse une chaîne sans sous-chemin /videos'
|
||||
);
|
||||
assertFalse(
|
||||
isValidApiEndpoint('video-channels'),
|
||||
'isValidApiEndpoint refuse "video-channels" seul'
|
||||
);
|
||||
assertFalse(isValidApiEndpoint('../config'), 'isValidApiEndpoint refuse le path traversal');
|
||||
assertFalse(isValidApiEndpoint('videos/../x'), 'isValidApiEndpoint refuse ".." dans le chemin');
|
||||
assertFalse(isValidApiEndpoint('videos//categories'), 'isValidApiEndpoint refuse un double slash');
|
||||
@@ -155,3 +167,91 @@ $nonce = getCspNonce();
|
||||
assertTrue(is_string($nonce) && $nonce !== '', 'getCspNonce retourne une chaîne non vide');
|
||||
assertEquals(16, strlen(base64_decode($nonce, true)), 'getCspNonce est un base64 de 16 octets aléatoires');
|
||||
assertEquals($nonce, getCspNonce(), 'getCspNonce retourne le même nonce durant toute la requête');
|
||||
|
||||
// --- getCsrfSecret ------------------------------------------------------------
|
||||
|
||||
assertEquals(
|
||||
CSRF_SECRET,
|
||||
getCsrfSecret(),
|
||||
'getCsrfSecret retourne CSRF_SECRET quand il est configuré'
|
||||
);
|
||||
|
||||
// Repli éphémère : impossible à tester dans ce processus (CSRF_SECRET est déjà
|
||||
// défini par le bootstrap), on passe par un sous-processus PHP isolé dont la
|
||||
// sortie d'erreur est redirigée vers stdout pour capter l'avertissement.
|
||||
$securityFile = dirname(__DIR__, 2) . '/includes/security.php';
|
||||
$snippet = <<<'PHP'
|
||||
require $argv[1];
|
||||
define('CSRF_SECRET', CSRF_SECRET_PLACEHOLDER);
|
||||
$s1 = getCsrfSecret();
|
||||
$s2 = getCsrfSecret();
|
||||
echo 'len=' . strlen($s1) . "\n";
|
||||
echo 'same=' . ($s1 === $s2 ? 'yes' : 'no') . "\n";
|
||||
echo 'placeholder=' . ($s1 === CSRF_SECRET ? 'yes' : 'no') . "\n";
|
||||
$token = generateCSRFToken();
|
||||
echo 'token=' . (validateCSRFToken($token) ? 'valid' : 'invalid') . "\n";
|
||||
PHP;
|
||||
|
||||
$cmd = escapeshellarg(PHP_BINARY)
|
||||
. ' -d log_errors=1 -d error_log=/dev/stdout -r '
|
||||
. escapeshellarg($snippet)
|
||||
. ' ' . escapeshellarg($securityFile);
|
||||
$fallbackOutput = function_exists('shell_exec') ? shell_exec($cmd) : null;
|
||||
|
||||
if ($fallbackOutput === null) {
|
||||
test_record(false, 'getCsrfSecret : sous-processus de test du repli éphémère (shell_exec indisponible)');
|
||||
} else {
|
||||
assertContains(
|
||||
'SECURITY CRITICAL',
|
||||
$fallbackOutput,
|
||||
'getCsrfSecret enregistre un avertissement critique avec la valeur par défaut'
|
||||
);
|
||||
assertContains(
|
||||
'CSRF_SECRET',
|
||||
$fallbackOutput,
|
||||
'l\'avertissement critique mentionne CSRF_SECRET'
|
||||
);
|
||||
assertContains(
|
||||
'len=64',
|
||||
$fallbackOutput,
|
||||
'le secret éphémère fait 64 caractères hexadécimaux (32 octets)'
|
||||
);
|
||||
assertContains(
|
||||
'same=yes',
|
||||
$fallbackOutput,
|
||||
'le secret éphémère est stable durant tout le processus'
|
||||
);
|
||||
assertContains(
|
||||
'placeholder=no',
|
||||
$fallbackOutput,
|
||||
'le secret éphémère diffère de la valeur par défaut'
|
||||
);
|
||||
assertContains(
|
||||
'token=valid',
|
||||
$fallbackOutput,
|
||||
'un token signé avec le secret éphémère est validé dans le même processus'
|
||||
);
|
||||
}
|
||||
|
||||
// --- isValidRemoteUrl (includes/security.php) ---------------------------------
|
||||
|
||||
assertTrue(isValidRemoteUrl('https://peertube.example.com'), 'isValidRemoteUrl accepte une URL HTTPS publique');
|
||||
assertTrue(isValidRemoteUrl('https://kute.o-k-i.net'), 'isValidRemoteUrl accepte une instance Castopod publique');
|
||||
assertTrue(isValidRemoteUrl('https://mizik.o-k-i.net'), 'isValidRemoteUrl accepte une instance Funkwhale publique');
|
||||
assertTrue(isValidRemoteUrl('http://castopod.example.com'), 'isValidRemoteUrl accepte HTTP (développement)');
|
||||
assertFalse(isValidRemoteUrl('ftp://kute.o-k-i.net'), 'isValidRemoteUrl refuse un schéma non HTTP(S)');
|
||||
assertFalse(isValidRemoteUrl('pas-une-url'), 'isValidRemoteUrl refuse une chaîne mal formée');
|
||||
assertFalse(isValidRemoteUrl(''), 'isValidRemoteUrl refuse une chaîne vide');
|
||||
assertFalse(isValidRemoteUrl('https://localhost'), 'isValidRemoteUrl refuse localhost');
|
||||
assertFalse(isValidRemoteUrl('http://127.0.0.1'), 'isValidRemoteUrl refuse 127.0.0.1');
|
||||
assertFalse(isValidRemoteUrl('http://192.168.1.1'), 'isValidRemoteUrl refuse une IP privée (192.168.x)');
|
||||
assertFalse(isValidRemoteUrl('http://10.0.0.5'), 'isValidRemoteUrl refuse une IP privée (10.x)');
|
||||
assertFalse(isValidRemoteUrl('http://172.16.0.1'), 'isValidRemoteUrl refuse une IP privée (172.16.x)');
|
||||
assertFalse(
|
||||
isValidRemoteUrl('http://169.254.169.254/latest/meta-data'),
|
||||
'isValidRemoteUrl refuse l\'IP de métadonnées cloud (link-local)'
|
||||
);
|
||||
assertFalse(
|
||||
isValidRemoteUrl('https://metadata.google.internal'),
|
||||
'isValidRemoteUrl refuse metadata.google.internal'
|
||||
);
|
||||
|
||||
@@ -140,9 +140,9 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<meta property="og:title" content="<?php echo !empty($video['title']) ? htmlspecialchars($video['title']) : 'Vidéo'; ?> - <?php echo SITE_NAME; ?>">
|
||||
<meta property="og:description" content="<?php echo !empty($video['description']) ? htmlspecialchars(substr(strip_tags($video['description']), 0, 200)) . '...' : 'Regardez cette vidéo sur ' . SITE_NAME; ?>">
|
||||
<?php if (isset($videoData['thumbnailPath'])): ?>
|
||||
<meta property="og:image" content="<?php echo PEERTUBE_URL . $videoData['thumbnailPath']; ?>">
|
||||
<meta property="og:image" content="<?php echo e(PEERTUBE_URL . $videoData['thumbnailPath']); ?>">
|
||||
<?php endif; ?>
|
||||
<meta property="og:url" content="<?php echo (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; ?>">
|
||||
<meta property="og:url" content="<?php echo htmlspecialchars(getCurrentUrl()); ?>">
|
||||
<meta property="og:type" content="video.other">
|
||||
<meta property="og:site_name" content="<?php echo SITE_NAME; ?>">
|
||||
<meta property="og:locale" content="fr_FR">
|
||||
@@ -152,7 +152,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<meta name="twitter:title" content="<?php echo !empty($video['title']) ? htmlspecialchars($video['title']) : 'Vidéo'; ?> - <?php echo SITE_NAME; ?>">
|
||||
<meta name="twitter:description" content="<?php echo !empty($video['description']) ? htmlspecialchars(substr(strip_tags($video['description']), 0, 200)) . '...' : 'Regardez cette vidéo sur ' . SITE_NAME; ?>">
|
||||
<?php if (isset($videoData['thumbnailPath'])): ?>
|
||||
<meta name="twitter:image" content="<?php echo PEERTUBE_URL . $videoData['thumbnailPath']; ?>">
|
||||
<meta name="twitter:image" content="<?php echo e(PEERTUBE_URL . $videoData['thumbnailPath']); ?>">
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (!isset($videoNotFound) && !empty($video)): ?>
|
||||
@@ -224,7 +224,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?>
|
||||
<span class="video-views"><i class="fas fa-eye" aria-hidden="true"></i> <?php echo formatViewCount($video['views']); ?> vues</span>
|
||||
<?php endif; ?>
|
||||
<span class="video-date"><i class="far fa-calendar-alt" aria-hidden="true"></i> <time datetime="<?php echo $video['date']; ?>"><?php echo formatDate($video['date']); ?></time></span>
|
||||
<span class="video-date"><i class="far fa-calendar-alt" aria-hidden="true"></i> <time datetime="<?php echo e($video['date']); ?>"><?php echo formatDate($video['date']); ?></time></span>
|
||||
</div>
|
||||
|
||||
<div class="video-actions">
|
||||
@@ -301,20 +301,20 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
$channelUrl = PEERTUBE_URL . '/c/' . $video['channelHandle'];
|
||||
?>
|
||||
<?php if (strpos($channelAvatar, 'default-avatar') !== false || empty($channelAvatar)): ?>
|
||||
<a href="<?php echo $channelUrl; ?>" target="_blank" rel="noopener noreferrer" class="channel-avatar-link" aria-label="Voir la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
<a href="<?php echo e($channelUrl); ?>" target="_blank" rel="noopener noreferrer" class="channel-avatar-link" aria-label="Voir la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
<div class="channel-avatar-placeholder" role="img" aria-label="Avatar par défaut">
|
||||
<i class="fas fa-user-circle" aria-hidden="true"></i>
|
||||
</div>
|
||||
</a>
|
||||
<?php else: ?>
|
||||
<a href="<?php echo $channelUrl; ?>" target="_blank" rel="noopener noreferrer" class="channel-avatar-link" aria-label="Voir la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
<a href="<?php echo e($channelUrl); ?>" target="_blank" rel="noopener noreferrer" class="channel-avatar-link" aria-label="Voir la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
<div class="channel-avatar">
|
||||
<img src="<?php echo $channelAvatar; ?>" alt="Avatar de la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
<img src="<?php echo e($channelAvatar); ?>" alt="Avatar de la chaîne <?php echo htmlspecialchars($video['channel']); ?>">
|
||||
</div>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="channel-details">
|
||||
<a href="<?php echo $channelUrl; ?>" target="_blank" rel="noopener noreferrer" class="channel-name-link">
|
||||
<a href="<?php echo e($channelUrl); ?>" target="_blank" rel="noopener noreferrer" class="channel-name-link">
|
||||
<h2 class="channel-name"><?php echo htmlspecialchars($video['channel']); ?></h2>
|
||||
</a>
|
||||
</div>
|
||||
@@ -335,7 +335,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<i class="fas fa-chevron-down" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div id="full-description" class="full-description" style="display: none;" nonce="<?php echo getCspNonce(); ?>">
|
||||
<div id="full-description" class="full-description is-hidden">
|
||||
<?php echo markdown_to_html($video['description']); ?>
|
||||
<button class="show-less-btn" aria-expanded="true" aria-controls="full-description">
|
||||
<span>Voir moins</span>
|
||||
@@ -362,7 +362,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="section-title-wrapper">
|
||||
<h2 id="comments-heading" class="section-title">Commentaires</h2>
|
||||
</div>
|
||||
<a href="<?php echo PEERTUBE_URL; ?>/videos/watch/<?php echo $videoData['uuid']; ?>" target="_blank" class="view-on-peertube" aria-label="Voir cette vidéo sur <?php echo PEERTUBE_DISPLAY_NAME; ?>">
|
||||
<a href="<?php echo e(PEERTUBE_URL . '/videos/watch/' . $videoData['uuid']); ?>" target="_blank" class="view-on-peertube" aria-label="Voir cette vidéo sur <?php echo PEERTUBE_DISPLAY_NAME; ?>">
|
||||
<i class="fas fa-external-link-alt" aria-hidden="true"></i> Voir sur <?php echo PEERTUBE_DISPLAY_NAME; ?>
|
||||
</a>
|
||||
</header>
|
||||
@@ -375,7 +375,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<article class="comment">
|
||||
<div class="comment-avatar">
|
||||
<?php if (isset($comment['account']['avatar']) && !empty($comment['account']['avatar']['path'])): ?>
|
||||
<img src="<?php echo PEERTUBE_URL . $comment['account']['avatar']['path']; ?>" alt="<?php echo htmlspecialchars($comment['account']['displayName']); ?>">
|
||||
<img src="<?php echo e(PEERTUBE_URL . $comment['account']['avatar']['path']); ?>" alt="<?php echo htmlspecialchars($comment['account']['displayName']); ?>">
|
||||
<?php else: ?>
|
||||
<div class="channel-avatar-placeholder mini" role="img" aria-label="Avatar par défaut">
|
||||
<i class="fas fa-user-circle" aria-hidden="true"></i>
|
||||
@@ -385,14 +385,14 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="comment-content">
|
||||
<header class="comment-header">
|
||||
<span class="comment-author"><?php echo htmlspecialchars($comment['account']['displayName']); ?></span>
|
||||
<time class="comment-date" datetime="<?php echo $comment['createdAt']; ?>"><?php echo formatDate($comment['createdAt']); ?></time>
|
||||
<time class="comment-date" datetime="<?php echo e($comment['createdAt']); ?>"><?php echo formatDate($comment['createdAt']); ?></time>
|
||||
</header>
|
||||
<div class="comment-text"><?php echo nl2br(htmlspecialchars($comment['text'])); ?></div>
|
||||
|
||||
<?php if (isset($comment['totalReplies']) && $comment['totalReplies'] > 0): ?>
|
||||
<div class="comment-replies-toggle">
|
||||
<a href="<?php echo PEERTUBE_URL; ?>/videos/watch/<?php echo $videoData['uuid']; ?>" target="_blank" class="show-replies" aria-label="Voir les réponses sur PeerTube">
|
||||
<i class="fas fa-reply" aria-hidden="true"></i> Voir les <?php echo $comment['totalReplies']; ?> réponse<?php echo $comment['totalReplies'] > 1 ? 's' : ''; ?>
|
||||
<a href="<?php echo e(PEERTUBE_URL . '/videos/watch/' . $videoData['uuid']); ?>" target="_blank" class="show-replies" aria-label="Voir les réponses sur PeerTube">
|
||||
<i class="fas fa-reply" aria-hidden="true"></i> Voir les <?php echo e($comment['totalReplies']); ?> réponse<?php echo $comment['totalReplies'] > 1 ? 's' : ''; ?>
|
||||
</a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
@@ -404,13 +404,13 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="comments-info">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<p>Les commentaires sont visibles mais l'ajout de commentaires et les threads de réponses sont désactivés sur cette page.</p>
|
||||
<p>Pour ajouter des commentaires ou voir les réponses, veuillez vous rendre sur <a href="<?php echo PEERTUBE_URL; ?>/videos/watch/<?php echo $videoData['uuid']; ?>" target="_blank" class="show-replies"> <?php echo PEERTUBE_DISPLAY_NAME; ?></a>.</p>
|
||||
<p>Pour ajouter des commentaires ou voir les réponses, veuillez vous rendre sur <a href="<?php echo e(PEERTUBE_URL . '/videos/watch/' . $videoData['uuid']); ?>" target="_blank" class="show-replies"> <?php echo PEERTUBE_DISPLAY_NAME; ?></a>.</p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="no-comments">
|
||||
<i class="fas fa-comments"></i>
|
||||
<p>Aucun commentaire pour cette vidéo.</p>
|
||||
<p>Pour ajouter des commentaires, veuillez vous rendre sur <a href="<?php echo PEERTUBE_URL; ?>/videos/watch/<?php echo $videoData['uuid']; ?>" target="_blank" class="show-replies"> <?php echo PEERTUBE_DISPLAY_NAME; ?></a>.</p>
|
||||
<p>Pour ajouter des commentaires, veuillez vous rendre sur <a href="<?php echo e(PEERTUBE_URL . '/videos/watch/' . $videoData['uuid']); ?>" target="_blank" class="show-replies"> <?php echo PEERTUBE_DISPLAY_NAME; ?></a>.</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
@@ -424,13 +424,13 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="suggestion-list">
|
||||
<?php foreach ($suggestedVideos as $suggestedVideo): ?>
|
||||
<article class="suggested-video">
|
||||
<a href="video.php?id=<?php echo $suggestedVideo['id']; ?>" class="suggested-video-link" aria-labelledby="suggestion-title-<?php echo $suggestedVideo['id']; ?>">
|
||||
<a href="video.php?id=<?php echo e($suggestedVideo['id']); ?>" class="suggested-video-link" aria-labelledby="suggestion-title-<?php echo e($suggestedVideo['id']); ?>">
|
||||
<div class="suggested-video-thumbnail">
|
||||
<img src="<?php echo $suggestedVideo['thumbnail']; ?>" alt="<?php echo $suggestedVideo['title']; ?>">
|
||||
<img src="<?php echo e($suggestedVideo['thumbnail']); ?>" alt="<?php echo e($suggestedVideo['title']); ?>">
|
||||
</div>
|
||||
<div class="suggested-video-info">
|
||||
<span class="suggested-video-duration"><?php echo formatDuration($suggestedVideo['duration']); ?></span>
|
||||
<h3 id="suggestion-title-<?php echo $suggestedVideo['id']; ?>" class="suggested-video-title"><?php echo htmlspecialchars($suggestedVideo['title']); ?></h3>
|
||||
<h3 id="suggestion-title-<?php echo e($suggestedVideo['id']); ?>" class="suggested-video-title"><?php echo htmlspecialchars($suggestedVideo['title']); ?></h3>
|
||||
<div class="suggested-video-channel">
|
||||
<?php
|
||||
// Vérifier si un avatar de chaîne est disponible pour la vidéo suggérée
|
||||
@@ -442,9 +442,9 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<i class="fas fa-user-circle"></i>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<img src="<?php echo $suggestedAvatar; ?>" alt="<?php echo $suggestedVideo['channel']; ?>" class="channel-avatar mini">
|
||||
<img src="<?php echo e($suggestedAvatar); ?>" alt="<?php echo e($suggestedVideo['channel']); ?>" class="channel-avatar mini">
|
||||
<?php endif; ?>
|
||||
<span class="channel-name"><?php echo $suggestedVideo['channel']; ?></span>
|
||||
<span class="channel-name"><?php echo e($suggestedVideo['channel']); ?></span>
|
||||
</div>
|
||||
<div class="suggested-video-metadata">
|
||||
<?php if (defined('SHOW_VIDEO_VIEWS') && SHOW_VIDEO_VIEWS): ?>
|
||||
@@ -467,6 +467,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<?php include 'includes/footer.php'; ?>
|
||||
<?php include 'includes/mobile-menu.php'; ?>
|
||||
|
||||
<?php if (!isset($videoNotFound)): ?>
|
||||
<!-- Modal de téléchargement -->
|
||||
<div id="download-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
@@ -482,13 +483,13 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
if (!empty($downloadOptions)):
|
||||
foreach ($downloadOptions as $option):
|
||||
?>
|
||||
<a href="<?php echo $option['url']; ?>" class="download-option" download>
|
||||
<a href="<?php echo e($option['url']); ?>" class="download-option" download>
|
||||
<div class="download-resolution">
|
||||
<i class="fas fa-film"></i>
|
||||
<span><?php echo $option['resolution']; ?></span>
|
||||
<span><?php echo e($option['resolution']); ?></span>
|
||||
</div>
|
||||
<div class="download-info">
|
||||
<span class="download-size"><?php echo $option['size']; ?></span>
|
||||
<span class="download-size"><?php echo e($option['size']); ?></span>
|
||||
<i class="fas fa-download"></i>
|
||||
</div>
|
||||
</a>
|
||||
@@ -517,7 +518,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="share-link-container">
|
||||
<p>Lien de la vidéo :</p>
|
||||
<div class="share-link-box">
|
||||
<input type="text" id="share-link" value="<?php echo htmlspecialchars((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" readonly>
|
||||
<input type="text" id="share-link" value="<?php echo htmlspecialchars(getCurrentUrl()); ?>" readonly>
|
||||
<button id="copy-link-btn" class="copy-btn" title="Copier le lien">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
@@ -526,32 +527,32 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
|
||||
<p class="share-platforms-title">Partager sur :</p>
|
||||
<div class="share-platforms">
|
||||
<a href="mailto:?subject=<?php echo urlencode(htmlspecialchars($video['title'])); ?>&body=<?php echo urlencode(htmlspecialchars($video['title']) . ' - ' . (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" class="share-platform-btn" title="Partager par e-mail">
|
||||
<a href="mailto:?subject=<?php echo urlencode(htmlspecialchars($video['title'])); ?>&body=<?php echo urlencode(htmlspecialchars($video['title']) . ' - ' . getCurrentUrl()); ?>" class="share-platform-btn" title="Partager par e-mail">
|
||||
<i class="fas fa-envelope"></i>
|
||||
<span>E-mail</span>
|
||||
</a>
|
||||
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" target="_blank" class="share-platform-btn" title="Partager sur Facebook">
|
||||
<a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo urlencode(getCurrentUrl()); ?>" target="_blank" class="share-platform-btn" title="Partager sur Facebook">
|
||||
<i class="fab fa-facebook-f"></i>
|
||||
<span>Facebook</span>
|
||||
</a>
|
||||
|
||||
<a href="https://twitter.com/intent/tweet?text=<?php echo urlencode(htmlspecialchars($video['title'])); ?>&url=<?php echo urlencode((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" target="_blank" class="share-platform-btn" title="Partager sur X/Twitter">
|
||||
<a href="https://twitter.com/intent/tweet?text=<?php echo urlencode(htmlspecialchars($video['title'])); ?>&url=<?php echo urlencode(getCurrentUrl()); ?>" target="_blank" class="share-platform-btn" title="Partager sur X/Twitter">
|
||||
<i class="fab fa-x-twitter"></i>
|
||||
<span>X</span>
|
||||
</a>
|
||||
|
||||
<a href="https://wa.me/?text=<?php echo urlencode(htmlspecialchars($video['title']) . ' - ' . (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" target="_blank" class="share-platform-btn" title="Partager sur WhatsApp">
|
||||
<a href="https://wa.me/?text=<?php echo urlencode(htmlspecialchars($video['title']) . ' - ' . getCurrentUrl()); ?>" target="_blank" class="share-platform-btn" title="Partager sur WhatsApp">
|
||||
<i class="fab fa-whatsapp"></i>
|
||||
<span>WhatsApp</span>
|
||||
</a>
|
||||
|
||||
<a href="https://www.linkedin.com/sharing/share-offsite/?url=<?php echo urlencode((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>" target="_blank" class="share-platform-btn" title="Partager sur LinkedIn">
|
||||
<a href="https://www.linkedin.com/sharing/share-offsite/?url=<?php echo urlencode(getCurrentUrl()); ?>" target="_blank" class="share-platform-btn" title="Partager sur LinkedIn">
|
||||
<i class="fab fa-linkedin-in"></i>
|
||||
<span>LinkedIn</span>
|
||||
</a>
|
||||
|
||||
<a href="https://t.me/share/url?url=<?php echo urlencode((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"); ?>&text=<?php echo urlencode($video['title']); ?>" target="_blank" class="share-platform-btn" title="Partager sur Telegram">
|
||||
<a href="https://t.me/share/url?url=<?php echo urlencode(getCurrentUrl()); ?>&text=<?php echo urlencode($video['title']); ?>" target="_blank" class="share-platform-btn" title="Partager sur Telegram">
|
||||
<i class="fab fa-telegram-plane"></i>
|
||||
<span>Telegram</span>
|
||||
</a>
|
||||
@@ -560,7 +561,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
<div class="share-embed">
|
||||
<p>Intégrer la vidéo :</p>
|
||||
<div class="share-link-box">
|
||||
<input type="text" id="embed-code" value='<iframe width="560" height="315" src="<?php echo $video['url']; ?>" frameborder="0" allowfullscreen></iframe>' readonly>
|
||||
<input type="text" id="embed-code" value='<iframe width="560" height="315" src="<?php echo e($video['url']); ?>" frameborder="0" allowfullscreen></iframe>' readonly>
|
||||
<button id="copy-embed-btn" class="copy-btn" title="Copier le code d'intégration">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
@@ -580,15 +581,15 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
|
||||
if (showMoreBtn) {
|
||||
showMoreBtn.addEventListener('click', function() {
|
||||
document.querySelector('.truncated-description').style.display = 'none';
|
||||
document.querySelector('.full-description').style.display = 'block';
|
||||
document.querySelector('.truncated-description').classList.add('is-hidden');
|
||||
document.querySelector('.full-description').classList.remove('is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
if (showLessBtn) {
|
||||
showLessBtn.addEventListener('click', function() {
|
||||
document.querySelector('.full-description').style.display = 'none';
|
||||
document.querySelector('.truncated-description').style.display = 'block';
|
||||
document.querySelector('.full-description').classList.add('is-hidden');
|
||||
document.querySelector('.truncated-description').classList.remove('is-hidden');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -672,6 +673,7 @@ if (empty($videoData) || isset($videoData['error'])) {
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<?php endif; ?>
|
||||
|
||||
<script src="js/pwa-update.js?v=<?php echo filemtime('js/pwa-update.js'); ?>"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user