feat: social media aggregator (YouTube, Instagram, TikTok, WordPress)

This commit is contained in:
2026-05-18 17:49:23 +04:00
parent 18cf71a7da
commit 5c3b35cb3c
10 changed files with 1679 additions and 906 deletions
+48 -56
View File
@@ -1,80 +1,72 @@
<!-- Footer -->
<?php
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<div class="footer">
<div class="footer-header">
<div class="footer-logo">
<img src="img/logo.png" alt="kaubuntu.re">
<img src="img/logo.png" alt="<?php echo htmlspecialchars(SITE_NAME); ?>">
</div>
<div class="footer-contact-info">
<div class="footer-contact">CONTACT</div>
<div class="footer-email"><a href="mailto:<?php echo CONTACT_EMAIL; ?>"><?php echo CONTACT_EMAIL; ?></a></div>
<div class="footer-email">
<a href="mailto:<?php echo htmlspecialchars(CONTACT_EMAIL); ?>"><?php echo htmlspecialchars(CONTACT_EMAIL); ?></a>
</div>
</div>
</div>
<div class="footer-columns">
<div class="footer-column">
<h3 class="footer-title">Catégories</h3>
<div>
<ul class="footer-links">
<li><a href="index.php" <?php echo ($currentPage === 'index.php') ? 'class="active"' : ''; ?>>Accueil</a></li>
<li><a href="direct.php" <?php echo ($currentPage === 'direct.php') ? 'class="active"' : ''; ?>>Direct</a></li>
<?php
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
echo '<li><a href="categories.php?id=' . $id . '"' . ($isActive ? ' class="active"' : '') . '>' . htmlspecialchars($name) . '</a></li>';
}
}
?>
</ul>
</div>
</div>
<div class="footer-column">
<h3 class="footer-title">Hashtags</h3>
<div>
<ul class="footer-links">
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<li><a href="recherche.php?q=<?php echo $encodedTag; ?>" <?php echo $isActive ? 'class="active"' : ''; ?>><?php echo htmlspecialchars($tag); ?></a></li>
<?php
endforeach;
endif;
?>
</ul>
</div>
<h3 class="footer-title">Réseaux sociaux</h3>
<ul class="footer-links">
<li>
<a href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> TikTok
</a>
</li>
</ul>
</div>
<div class="footer-column">
<h3 class="footer-title">Informations légales</h3>
<div>
<ul class="footer-links">
<li><a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>Mentions légales</a></li>
<li>
<a href="<?php echo LEGAL_SOURCE_CODE_URL; ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-git-alt"></i> Code source
</a>
</li>
</ul>
</div>
<ul class="footer-links">
<li>
<a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>
Mentions légales
</a>
</li>
<li>
<a href="<?php echo htmlspecialchars(LEGAL_SOURCE_CODE_URL); ?>" target="_blank" rel="noopener noreferrer">
<i class="fab fa-git-alt" aria-hidden="true"></i> Code source
</a>
</li>
</ul>
</div>
</div>
<div class="footer-social">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
</div>
<div class="footer-copyright">
<?php echo LEGAL_COPYRIGHT; ?> <?php echo date('Y'); ?> - Licence libre <a href="<?php echo LEGAL_LICENSE_URL; ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a>
<?php echo htmlspecialchars(LEGAL_COPYRIGHT); ?> <?php echo date('Y'); ?> &mdash;
Licence libre <a href="<?php echo htmlspecialchars(LEGAL_LICENSE_URL); ?>" target="_blank" rel="noopener noreferrer">GNU AGPL-V3</a>
</div>
</div>
+15 -32
View File
@@ -1,47 +1,30 @@
<!-- Header avec barre de recherche et icônes -->
<!-- Header -->
<header class="header" role="banner">
<div class="search-container">
<form action="recherche.php" method="get" role="search" aria-label="Recherche de vidéos">
<label for="search-input" class="sr-only">Rechercher des vidéos</label>
<input type="text" id="search-input" name="q" placeholder="Rechercher..." aria-describedby="search-help">
<button type="submit" aria-label="Lancer la recherche">
<i class="fas fa-search" aria-hidden="true"></i>
</button>
<div id="search-help" class="sr-only">Tapez vos mots-clés pour rechercher des vidéos</div>
</form>
<div class="header-brand">
<a href="/" class="header-logo-link" aria-label="Accueil <?php echo htmlspecialchars(SITE_NAME); ?>">
<img src="img/logo.png" alt="<?php echo htmlspecialchars(SITE_NAME); ?>" class="header-logo-img">
<span class="header-site-name"><?php echo htmlspecialchars(SITE_NAME); ?></span>
</a>
</div>
<nav class="social-icons" aria-label="Réseaux sociaux">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>" class="icon-button" aria-label="Suivre sur Mastodon">
<i class="fab fa-mastodon icon-mastodon" aria-hidden="true"></i>
<nav class="social-icons" aria-label="Nos réseaux sociaux">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>"
class="icon-button" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>" class="icon-button" aria-label="Suivre sur Instagram">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>"
class="icon-button" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>" class="icon-button" aria-label="Suivre sur TikTok">
<a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>"
class="icon-button" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
<div class="more-social-container">
<button type="button" class="icon-button more-social-toggle" aria-expanded="false" aria-controls="social-dropdown" aria-label="Voir plus de réseaux sociaux">
<i class="fas fa-ellipsis-h" aria-hidden="true"></i>
</button>
<div id="social-dropdown" class="more-social-dropdown" role="menu">
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-facebook icon-facebook" aria-hidden="true"></i> Facebook
</a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>" class="more-social-item" role="menuitem">
<i class="fab fa-x-twitter icon-x" aria-hidden="true"></i> X
</a>
</div>
</div>
</nav>
<div class="action-icons">
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<a href="dons.php" class="icon-button donation-link" aria-label="Soutenir KA UBUNTU" title="Faire un don">
<a href="dons.php" class="icon-button donation-link" aria-label="Soutenir <?php echo htmlspecialchars(SITE_NAME); ?>" title="Faire un don">
<i class="fas fa-heart" aria-hidden="true"></i>
</a>
<?php endif; ?>
+46 -76
View File
@@ -1,92 +1,62 @@
<!-- Menu mobile (masqué par défaut) -->
<div class="mobile-menu">
<button class="mobile-menu-close">
<i class="fas fa-times"></i>
<?php
$currentPage = basename($_SERVER['PHP_SELF']);
$isHome = ($currentPage === 'index.php' || $currentPage === '/');
?>
<!-- Menu mobile -->
<div class="mobile-menu" id="mobile-menu" role="dialog" aria-label="Menu de navigation" aria-modal="true">
<button class="mobile-menu-close" aria-label="Fermer le menu">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
<div class="search-container">
<form action="recherche.php" method="get">
<input type="text" name="q" placeholder="Rechercher...">
<button type="submit"><i class="fas fa-search"></i></button>
</form>
</div>
<div>
<a href="index.php" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>">
<i class="fas fa-home"></i> Accueil
</a>
<a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>">
<i class="fas fa-broadcast-tower"></i> Direct
<nav>
<a href="/"
class="nav-item <?php echo $isHome ? 'active' : ''; ?>">
<i class="fas fa-home" aria-hidden="true"></i> Accueil
</a>
<hr class="nav-divider">
<?php
// Afficher les catégories prioritaires
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
// Tableau associatif des icônes pour les catégories
$categoryIcons = [
1 => 'fas fa-music', // Musique
2 => 'fas fa-film', // Films
3 => 'fas fa-car', // Véhicules
4 => 'fas fa-palette', // Jeux
5 => 'fas fa-running', // Sport
6 => 'fas fa-laugh', // Humour
7 => 'fas fa-gamepad', // Art
8 => 'fas fa-person', // Personnalités
9 => 'fas fa-face-grin-tears', // Comédie
10 => 'fas fa-tv', // Divertissement
11 => 'fas fa-globe', // Actualité & Politique
12 => 'fas fa-chalkboard-user', // Tutoriel
13 => 'fas fa-graduation-cap', // Education
14 => 'fas fa-fist-raised', // Activisme
15 => 'fas fa-microscope', // Science & Technologie
16 => 'fas fa-paw', // Animaux
17 => 'fas fa-child', // Enfants
18 => 'fas fa-utensils' // Cuisine
];
<p class="mobile-section-title">Nos réseaux</p>
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
$icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder';
echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '">';
echo '<i class="' . $icon . '"></i> ' . htmlspecialchars($name);
echo '</a>';
}
}
?>
<hr class="nav-divider">
</div>
<div>
<h3 class="mobile-section-title">Hashtags populaires</h3>
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<a href="recherche.php?q=<?php echo $encodedTag; ?>" class="nav-item <?php echo $isActive ? 'active' : ''; ?>">
<i class="fas fa-hashtag"></i> <?php echo htmlspecialchars($tag); ?>
<a href="<?php echo $isHome ? '#youtube' : 'index.php#youtube'; ?>"
class="nav-item">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
<?php
endforeach;
endif;
?>
</div>
<a href="<?php echo $isHome ? '#instagram' : 'index.php#instagram'; ?>"
class="nav-item">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
</a>
<a href="<?php echo $isHome ? '#tiktok' : 'index.php#tiktok'; ?>"
class="nav-item">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> TikTok
</a>
<a href="<?php echo $isHome ? '#actualites' : 'index.php#actualites'; ?>"
class="nav-item">
<i class="fas fa-newspaper" aria-hidden="true"></i> Actualités
</a>
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<hr class="nav-divider">
<a href="dons.php" class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>">
<i class="fas fa-heart" aria-hidden="true"></i> Soutenir
</a>
<?php endif; ?>
</nav>
<hr class="nav-divider">
<div>
<h3 class="mobile-section-title">Suivez-nous</h3>
<p class="mobile-section-title">Suivez-nous</p>
<div class="mobile-social-icons">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>"><i class="fab fa-mastodon icon-mastodon"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" aria-label="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a>
<a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" aria-label="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a>
</div>
</div>
</div>
+32 -92
View File
@@ -181,107 +181,47 @@ function validateCSRFToken($token) {
* Applique des en-têtes de sécurité HTTP
*/
function setSecurityHeaders() {
// Protection contre le clickjacking (permettre les iframes du même site)
header('X-Frame-Options: SAMEORIGIN');
// Protection contre le MIME sniffing
header('X-Content-Type-Options: nosniff');
// Protection XSS basique
header('X-XSS-Protection: 1; mode=block');
// Politique de référent
header('Referrer-Policy: strict-origin-when-cross-origin');
// Content Security Policy avec support Mastodon et PeerTube
$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'; ";
$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']
);
// Domaines des 5 plateformes sociales
$fbScripts = 'https://connect.facebook.net';
$fbFrames = 'https://www.facebook.com https://staticxx.facebook.com https://www.facebook.net';
$xScripts = 'https://platform.twitter.com';
$xFrames = 'https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com';
$igScripts = 'https://www.instagram.com';
$igFrames = 'https://www.instagram.com';
$ttScripts = 'https://www.tiktok.com https://lf16-tiktok-web.ttwstatic.com';
$ttFrames = 'https://www.tiktok.com';
$ytImages = 'https://i.ytimg.com https://yt3.ggpht.com';
$csp = "default-src 'self'; ";
$csp .= "style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com; ";
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io; "; // PLAUSIBLE UPDATED
// Images : autoriser les domaines externes plus HTTPS général en dev
$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 et HTTPS général
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) {
$frameSrc .= " https: http:";
} else {
$frameSrc .= " https:";
}
$csp .= "frame-src " . $frameSrc . "; ";
// Connexions : autoriser Mastodon et PeerTube
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : '');
if ($isLocalDev) {
$connectSrc .= " ws: wss:"; // WebSockets pour le dev
}
$csp .= "connect-src " . $connectSrc . "; ";
// Médias : toujours autoriser 'self', Mastodon et PeerTube
$mediaSrc = "'self'";
// Ajouter l'instance Mastodon (pour les médias stockés sur l'instance)
if ($mastodonDomain) {
$mediaSrc .= " " . $mastodonDomain;
}
// Ajouter PeerTube
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 .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io "
. "{$fbScripts} {$xScripts} {$igScripts} {$ttScripts}; ";
$csp .= "img-src 'self' data: {$ytImages} https://www.facebook.com https://pbs.twimg.com https://abs.twimg.com"
. ($isLocalDev ? " https: http:" : " https:") . "; ";
$csp .= "frame-src 'self' {$fbFrames} {$xFrames} {$igFrames} {$ttFrames} https://www.youtube.com"
. ($isLocalDev ? " http:" : "") . "; ";
$csp .= "connect-src 'self' https://plausible.io https://www.googleapis.com https://www.youtube.com "
. "https://www.facebook.com https://graph.facebook.com https://connect.facebook.net "
. "https://platform.twitter.com https://syndication.twitter.com https://cdn.syndication.twimg.com https://api.twitter.com "
. "https://www.instagram.com https://www.tiktok.com"
. ($isLocalDev ? " ws: wss:" : "") . "; ";
$csp .= "media-src 'self' https:; ";
$csp .= "object-src 'none'; ";
$csp .= "base-uri 'self';";
header('Content-Security-Policy: ' . $csp);
// HTTPS strict transport security (seulement si HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
}
+33 -68
View File
@@ -1,88 +1,53 @@
<!-- Sidebar de navigation -->
<nav class="sidebar" role="navigation" aria-label="Navigation principale">
<a href="/" class="logo" aria-label="Retour à l'accueil">
<img src="img/logo.png" alt="Logo kaubuntu.re">
<img src="img/logo.png" alt="Logo <?php echo htmlspecialchars(SITE_NAME); ?>">
</a>
<?php
// Détecter la page courante et ses paramètres
$currentPage = basename($_SERVER['PHP_SELF']);
$currentCategoryId = isset($_GET['id']) ? intval($_GET['id']) : null;
$currentQuery = isset($_GET['q']) ? trim($_GET['q']) : '';
$isTagSearch = !empty($currentQuery) && substr($currentQuery, 0, 1) === '#';
$currentTag = $isTagSearch ? substr($currentQuery, 1) : '';
$isHome = ($currentPage === 'index.php' || $currentPage === '/');
?>
<div class="sidebar-nav">
<a href="/" class="nav-item <?php echo ($currentPage === 'index.php') ? 'active' : ''; ?>" data-title="Accueil" aria-current="<?php echo ($currentPage === 'index.php') ? 'page' : 'false'; ?>">
<a href="/"
class="nav-item <?php echo $isHome ? 'active' : ''; ?>"
data-title="Accueil"
aria-current="<?php echo $isHome ? 'page' : 'false'; ?>">
<i class="fas fa-home" aria-hidden="true"></i> <span>Accueil</span>
</a>
<a href="direct.php" class="nav-item <?php echo ($currentPage === 'direct.php') ? 'active' : ''; ?>" data-title="Direct" aria-current="<?php echo ($currentPage === 'direct.php') ? 'page' : 'false'; ?>">
<i class="fas fa-broadcast-tower" aria-hidden="true"></i> <span>Direct</span>
<div class="nav-divider"></div>
<a href="<?php echo $isHome ? '#youtube' : 'index.php#youtube'; ?>"
class="nav-item"
data-title="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> <span>YouTube</span>
</a>
<a href="<?php echo $isHome ? '#instagram' : 'index.php#instagram'; ?>"
class="nav-item"
data-title="Instagram">
<i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> <span>Instagram</span>
</a>
<a href="<?php echo $isHome ? '#tiktok' : 'index.php#tiktok'; ?>"
class="nav-item"
data-title="TikTok">
<i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> <span>TikTok</span>
</a>
<a href="<?php echo $isHome ? '#actualites' : 'index.php#actualites'; ?>"
class="nav-item"
data-title="Actualités">
<i class="fas fa-newspaper" aria-hidden="true"></i> <span>Actualités</span>
</a>
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?>
<a href="dons.php" class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>" data-title="Soutenir" aria-current="<?php echo ($currentPage === 'dons.php') ? 'page' : 'false'; ?>">
<div class="nav-divider"></div>
<a href="dons.php"
class="nav-item donation-nav-link <?php echo ($currentPage === 'dons.php') ? 'active' : ''; ?>"
data-title="Soutenir"
aria-current="<?php echo ($currentPage === 'dons.php') ? 'page' : 'false'; ?>">
<i class="fas fa-heart" aria-hidden="true"></i> <span>Soutenir</span>
</a>
<?php endif; ?>
<div class="nav-divider"></div>
<?php
// Afficher les catégories prioritaires
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) {
// Tableau associatif des icônes pour les catégories
$categoryIcons = [
1 => 'fas fa-music', // Musique
2 => 'fas fa-film', // Films
3 => 'fas fa-car', // Véhicules
4 => 'fas fa-palette', // Jeux
5 => 'fas fa-running', // Sport
6 => 'fas fa-laugh', // Humour
7 => 'fas fa-gamepad', // Art
8 => 'fas fa-person', // Personnalités
9 => 'fas fa-face-grin-tears', // Comédie
10 => 'fas fa-tv', // Divertissement
11 => 'fas fa-globe', // Actualité & Politique
12 => 'fas fa-chalkboard-user', // Tutoriel
13 => 'fas fa-graduation-cap', // Education
14 => 'fas fa-fist-raised', // Activisme
15 => 'fas fa-microscope', // Science & Technologie
16 => 'fas fa-paw', // Animaux
17 => 'fas fa-child', // Enfants
18 => 'fas fa-utensils' // Cuisine
];
foreach (PRIORITY_CATEGORIES as $id => $name) {
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id);
$icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder';
echo '<a href="categories.php?id=' . $id . '" class="nav-item ' . ($isActive ? 'active' : '') . '" data-title="' . htmlspecialchars($name) . '" aria-current="' . ($isActive ? 'page' : 'false') . '">';
echo '<i class="' . $icon . '" aria-hidden="true"></i> <span>' . htmlspecialchars($name) . '</span>';
echo '</a>';
}
}
?>
<div class="nav-divider"></div>
<div class="category-title" role="heading" aria-level="2">
<i class="fas fa-hashtag" aria-hidden="true"></i> <span>Hashtags</span>
</div>
<?php
if (defined('IMPORTANT_TAGS') && !empty(IMPORTANT_TAGS)):
foreach (IMPORTANT_TAGS as $tag):
$encodedTag = urlencode('#' . $tag);
$isActive = ($isTagSearch && strtolower($currentTag) === strtolower($tag));
?>
<a href="recherche.php?q=<?php echo $encodedTag; ?>" class="tag-item <?php echo $isActive ? 'active' : ''; ?>" data-title="<?php echo htmlspecialchars($tag); ?>" aria-current="<?php echo $isActive ? 'page' : 'false'; ?>">
<i class="fas fa-hashtag tag-icon" aria-hidden="true"></i> <span><?php echo htmlspecialchars($tag); ?></span>
</a>
<?php
endforeach;
endif;
?>
</div>
</nav>
+129
View File
@@ -0,0 +1,129 @@
<?php
/**
* Récupération des statistiques de followers pour chaque plateforme sociale.
* Toutes les fonctions retournent null en cas d'échec — l'affichage reste optionnel.
* Cache 24h pour limiter les appels externes.
*/
function formatFollowerCount(int $n): string {
if ($n >= 1_000_000) return number_format($n / 1_000_000, 1, '.', '') . 'M';
if ($n >= 1_000) return number_format($n / 1_000, 1, '.', '') . 'K';
return (string) $n;
}
function _statsCurlFetch(string $url, array $headers = []): ?string {
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 8,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/124.0 Safari/537.36',
CURLOPT_HTTPHEADER => $headers,
]);
$body = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return ($body && $httpCode === 200) ? $body : null;
}
function getYouTubeSubscriberCount(): ?int {
// Priority 1: YouTube Data API v3 (exact count)
if (defined('YOUTUBE_API_KEY') && !empty(YOUTUBE_API_KEY)) {
$channelId = getYouTubeChannelId();
if ($channelId) {
$cacheKey = 'stat_yt_' . $channelId;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$data = callYouTubeApi('channels', ['part' => 'statistics', 'id' => $channelId]);
$count = isset($data['items'][0]['statistics']['subscriberCount'])
? (int) $data['items'][0]['statistics']['subscriberCount']
: null;
if ($count !== null) {
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
return $count;
}
}
}
// Priority 2: parse ytInitialData from public channel page
$handle = '';
if (defined('YOUTUBE_HANDLE') && !empty(YOUTUBE_HANDLE)) {
$handle = ltrim(YOUTUBE_HANDLE, '@');
} elseif (defined('YOUTUBE_CHANNEL_HANDLE') && !empty(YOUTUBE_CHANNEL_HANDLE)) {
$handle = ltrim(YOUTUBE_CHANNEL_HANDLE, '@');
}
$pageUrl = $handle
? 'https://www.youtube.com/@' . urlencode($handle)
: (defined('YOUTUBE_CHANNEL_ID') && YOUTUBE_CHANNEL_ID
? 'https://www.youtube.com/channel/' . urlencode(YOUTUBE_CHANNEL_ID)
: null);
if (!$pageUrl) return null;
$cacheKey = 'stat_yt_page_' . preg_replace('/[^a-z0-9]/i', '_', $handle ?: YOUTUBE_CHANNEL_ID);
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch($pageUrl, ['Accept-Language: fr-FR,fr;q=0.9,en;q=0.8']);
if (!$body) return null;
// ytInitialData embeds exact subscriberCount as a quoted integer string
if (preg_match('/"subscriberCount"\s*:\s*"(\d+)"/', $body, $m)) {
$count = (int) $m[1];
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
return $count;
}
return null;
}
function getInstagramFollowers(): ?int {
if (!defined('INSTAGRAM_HANDLE') || empty(INSTAGRAM_HANDLE)) return null;
$handle = ltrim(INSTAGRAM_HANDLE, '@');
$cacheKey = 'stat_ig_' . $handle;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch(
'https://www.instagram.com/api/v1/users/web_profile_info/?username=' . urlencode($handle),
['X-IG-App-ID: 936619743392459', 'Accept: application/json']
);
if (!$body) return null;
$data = json_decode($body, true);
$count = $data['data']['user']['edge_followed_by']['count'] ?? null;
if ($count !== null) {
$count = (int) $count;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
}
return $count;
}
function getTikTokFollowers(): ?int {
if (!defined('TIKTOK_HANDLE') || empty(TIKTOK_HANDLE)) return null;
$handle = ltrim(TIKTOK_HANDLE, '@');
$cacheKey = 'stat_tt_' . $handle;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return (int) $cached ?: null;
$body = _statsCurlFetch('https://www.tiktok.com/embed/@' . urlencode($handle));
if (!$body) return null;
preg_match('/"followerCount"\s*:\s*(\d+)/', $body, $m);
$count = isset($m[1]) ? (int) $m[1] : null;
if ($count !== null) {
$GLOBALS['simple_api_cache']->set($cacheKey, [], $count, 86400);
}
return $count;
}
+203
View File
@@ -0,0 +1,203 @@
<?php
/**
* Intégration YouTube Data API v3
*/
function callYouTubeApi(string $endpoint, array $params = []): array {
if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return [];
$params['key'] = YOUTUBE_API_KEY;
$url = 'https://www.googleapis.com/youtube/v3/' . $endpoint . '?' . http_build_query($params);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_CONNECTTIMEOUT => 8,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe/2.0',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false || !empty($error) || $httpCode < 200 || $httpCode >= 300) {
error_log('YouTube API error: ' . $error . ' (HTTP ' . $httpCode . ')');
return [];
}
return json_decode($response, true) ?: [];
}
function getYouTubeChannelId(): string {
if (defined('YOUTUBE_CHANNEL_ID') && !empty(YOUTUBE_CHANNEL_ID)) {
return YOUTUBE_CHANNEL_ID;
}
if (!defined('YOUTUBE_API_KEY') || empty(YOUTUBE_API_KEY)) return '';
if (!defined('YOUTUBE_CHANNEL_HANDLE') || empty(YOUTUBE_CHANNEL_HANDLE)) return '';
$handle = ltrim(YOUTUBE_CHANNEL_HANDLE, '@');
$cached = $GLOBALS['simple_api_cache']->get('yt_channel_id_' . $handle, []);
if ($cached !== null) return (string) $cached;
$data = callYouTubeApi('channels', ['part' => 'snippet', 'forHandle' => $handle]);
$channelId = $data['items'][0]['id'] ?? '';
if ($channelId) {
$GLOBALS['simple_api_cache']->set('yt_channel_id_' . $handle, [], $channelId, 86400);
}
return $channelId;
}
function isYouTubeShort(string $isoDuration): bool {
preg_match('/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/', $isoDuration, $m);
$seconds = (int)($m[1] ?? 0) * 3600 + (int)($m[2] ?? 0) * 60 + (int)($m[3] ?? 0);
return $seconds > 0 && $seconds <= 60;
}
function getYouTubeLatestVideos(int $count = 6): array {
// Priorité 1 : YouTube Data API v3
if (defined('YOUTUBE_API_KEY') && !empty(YOUTUBE_API_KEY)) {
$channelId = getYouTubeChannelId();
if ($channelId) {
$shortsTarget = min(3, $count - 1);
$regularTarget = $count - $shortsTarget;
$cacheKey = 'yt_videos2_' . $channelId . '_' . $count;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return $cached;
// On récupère 50 vidéos pour avoir suffisamment de chaque type
$searchData = callYouTubeApi('search', [
'part' => 'snippet',
'channelId' => $channelId,
'order' => 'date',
'type' => 'video',
'maxResults' => 50,
]);
if (!empty($searchData['items'])) {
$videoIds = array_map(fn($i) => $i['id']['videoId'], $searchData['items']);
// videos.list coûte 1 unité quota (vs 100 pour search)
$detailData = callYouTubeApi('videos', [
'part' => 'contentDetails',
'id' => implode(',', $videoIds),
]);
$durations = [];
foreach ($detailData['items'] ?? [] as $item) {
$durations[$item['id']] = $item['contentDetails']['duration'] ?? '';
}
$shorts = [];
$regular = [];
foreach ($searchData['items'] as $item) {
$videoId = $item['id']['videoId'];
$isShort = isset($durations[$videoId])
? isYouTubeShort($durations[$videoId])
: false;
$video = [
'id' => $videoId,
'title' => html_entity_decode($item['snippet']['title'], ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'thumbnail' => $item['snippet']['thumbnails']['high']['url']
?? $item['snippet']['thumbnails']['default']['url'],
'publishedAt' => $item['snippet']['publishedAt'],
'url' => $isShort
? 'https://www.youtube.com/shorts/' . $videoId
: 'https://www.youtube.com/watch?v=' . $videoId,
'isShort' => $isShort,
];
if ($isShort && count($shorts) < $shortsTarget) {
$shorts[] = $video;
} elseif (!$isShort && count($regular) < $regularTarget) {
$regular[] = $video;
}
if (count($shorts) >= $shortsTarget && count($regular) >= $regularTarget) break;
}
// Shorts en premier, puis vidéos normales
$videos = array_merge($shorts, $regular);
if (!empty($videos)) {
$ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 900;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl);
return $videos;
}
}
}
}
// Priorité 2 : flux RSS public YouTube (sans clé API, nécessite YOUTUBE_CHANNEL_ID)
return getYouTubeVideosFromRSS($count);
}
function getYouTubeVideosFromRSS(int $count = 6): array {
if (!defined('YOUTUBE_CHANNEL_ID') || empty(YOUTUBE_CHANNEL_ID)) return [];
$cacheKey = 'yt_rss_' . YOUTUBE_CHANNEL_ID . '_' . $count;
$cached = $GLOBALS['simple_api_cache']->get($cacheKey, []);
if ($cached !== null) return $cached;
$feedUrl = 'https://www.youtube.com/feeds/videos.xml?channel_id=' . urlencode(YOUTUBE_CHANNEL_ID);
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $feedUrl,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe/2.0',
]);
$xmlString = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!$xmlString || $httpCode !== 200) {
error_log('YouTube RSS error: HTTP ' . $httpCode);
return [];
}
$xml = @simplexml_load_string($xmlString);
if (!$xml) return [];
$videos = [];
$ytNs = 'http://www.youtube.com/xml/schemas/2015';
foreach ($xml->entry as $entry) {
if (count($videos) >= $count) break;
$yt = $entry->children($ytNs);
$videoId = (string) $yt->videoId;
if (!$videoId) continue;
$videos[] = [
'id' => $videoId,
'title' => html_entity_decode((string) $entry->title, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
'thumbnail' => "https://i.ytimg.com/vi/{$videoId}/hqdefault.jpg",
'publishedAt' => (string) $entry->published,
'url' => "https://www.youtube.com/watch?v={$videoId}",
'isShort' => false,
];
}
if (!empty($videos)) {
$ttl = defined('CACHE_DURATION') ? CACHE_DURATION : 3600;
$GLOBALS['simple_api_cache']->set($cacheKey, [], $videos, $ttl);
}
return $videos;
}
+41 -1
View File
@@ -111,6 +111,46 @@ function isValidWordPressUrl($url) {
return true;
}
/**
* Retourne le nombre total d'articles publiés sur WordPress.
* Lit le header X-WP-Total renvoyé par l'API REST.
*/
function getWordPressPostCount(): ?int {
if (!defined('WORDPRESS_ENABLED') || !WORDPRESS_ENABLED || empty(WORDPRESS_URL)) return null;
if (!isValidWordPressUrl(WORDPRESS_URL)) return null;
$cached = $GLOBALS['simple_api_cache']->get('wp_post_count', []);
if ($cached !== null) return (int) $cached ?: null;
$url = WORDPRESS_URL . '/wp-json/wp/v2/posts?per_page=1&_fields=id';
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 10,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_FOLLOWLOCATION => false,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_USERAGENT => 'KaubuntuRe-WordPress-Integration/1.0',
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
if (!$response || $httpCode < 200 || $httpCode >= 300) return null;
$headers = substr($response, 0, $headerSize);
if (!preg_match('/X-WP-Total:\s*(\d+)/i', $headers, $m)) return null;
$count = (int) $m[1];
$GLOBALS['simple_api_cache']->set('wp_post_count', [], $count, 86400);
return $count;
}
/**
* Récupère les articles WordPress récents
*
@@ -157,7 +197,7 @@ function formatWordPressPosts($posts) {
// Nettoyer l'extrait HTML
$excerpt = '';
if (isset($post['excerpt']['rendered'])) {
$excerpt = wp_strip_all_tags($post['excerpt']['rendered']);
$excerpt = html_entity_decode(wp_strip_all_tags($post['excerpt']['rendered']), ENT_QUOTES | ENT_HTML5, 'UTF-8');
$excerpt = trim($excerpt);
// Limiter à 150 caractères
if (strlen($excerpt) > 150) {