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
+784
View File
@@ -0,0 +1,784 @@
/* ============================================================
HERO SECTION
============================================================ */
.social-hero {
background: linear-gradient(135deg, #0a0a0a 0%, #1a0000 60%, #2d0000 100%);
border-radius: 12px;
padding: 48px 40px;
color: white;
margin-bottom: 0;
}
.social-hero-inner {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
text-align: center;
}
.social-hero-brand {
display: flex;
align-items: center;
gap: 24px;
flex-wrap: wrap;
justify-content: center;
}
.social-hero-logo {
height: 90px;
width: auto;
filter: drop-shadow(0 4px 16px rgba(255, 0, 0, 0.4));
}
.social-hero-text h1 {
font-size: 2.2rem;
font-weight: 800;
color: white;
margin-bottom: 8px;
letter-spacing: -0.5px;
}
.social-hero-text p {
font-size: 1rem;
color: rgba(255, 255, 255, 0.75);
max-width: 480px;
line-height: 1.5;
}
/* Platform Navigation Badges */
.platform-nav {
display: flex;
flex-wrap: wrap;
gap: 12px;
justify-content: center;
align-items: flex-start;
}
.platform-badge-wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
}
.platform-badge-count {
font-size: 12px;
font-weight: 700;
color: rgba(255, 255, 255, 0.85);
letter-spacing: 0.3px;
line-height: 1;
}
.platform-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 11px 22px;
border-radius: 50px;
font-size: 15px;
font-weight: 600;
text-decoration: none;
transition: all 0.25s cubic-bezier(0.25, 0.46, 0.45, 0.94);
border: 2px solid transparent;
color: white !important;
letter-spacing: 0.2px;
}
.platform-badge:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
}
.platform-badge:active {
transform: scale(0.93) translateY(0);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition-duration: 0.08s;
}
.platform-badge.platform-youtube { background: #FF0000; }
.platform-badge.platform-instagram {
background: linear-gradient(135deg, #F58529, #DD2A7B, #8134AF);
}
.platform-badge.platform-tiktok {
background: #000;
border-color: #EE1D52;
}
.platform-badge.platform-wp {
background: linear-gradient(135deg, #1a0000, #2d0000);
border-color: rgba(255, 255, 255, 0.2);
}
/* ============================================================
SOCIAL SECTIONS
============================================================ */
.social-section {
margin-bottom: 0;
}
.social-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 18px 24px;
border-radius: 10px 10px 0 0;
flex-wrap: wrap;
gap: 12px;
}
.social-section-header-left {
display: flex;
align-items: center;
gap: 12px;
min-width: 0;
}
.social-section-header h2 {
font-size: 22px;
font-weight: 700;
color: white;
margin: 0;
white-space: nowrap;
}
.social-section-header > .social-section-header-left > i {
font-size: 26px;
color: white;
flex-shrink: 0;
}
.platform-handle {
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
font-weight: 400;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.platform-stat-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
color: white;
background: rgba(255, 255, 255, 0.18);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 20px;
padding: 3px 10px;
white-space: nowrap;
flex-shrink: 0;
}
.platform-stat-badge i {
font-size: 11px;
opacity: 0.85;
}
/* Platform header gradients */
.platform-youtube-header { background: linear-gradient(135deg, #a80000, #FF0000); }
.platform-facebook-header { background: linear-gradient(135deg, #0d47a1, #1877F2); }
.platform-instagram-header {
background: linear-gradient(135deg, #6a0dad, #DD2A7B, #F77737);
}
.platform-tiktok-header {
background: linear-gradient(135deg, #010101, #1a1a1a);
border-bottom: 3px solid #EE1D52;
}
.platform-x-header {
background: linear-gradient(135deg, #000, #1a1a1a);
border-bottom: 3px solid rgba(255, 255, 255, 0.15);
}
/* CTA Buttons */
.platform-cta-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition: all 0.2s ease;
color: white !important;
flex-shrink: 0;
white-space: nowrap;
}
.platform-cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
}
.platform-youtube-btn { background: rgba(255, 255, 255, 0.2); }
.platform-youtube-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-facebook-btn { background: rgba(255, 255, 255, 0.2); }
.platform-facebook-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-instagram-btn { background: rgba(255, 255, 255, 0.2); }
.platform-instagram-btn:hover { background: rgba(255, 255, 255, 0.3); }
.platform-tiktok-btn { background: rgba(238, 29, 82, 0.7); }
.platform-tiktok-btn:hover { background: #EE1D52; }
.platform-x-btn { background: rgba(255, 255, 255, 0.15); }
.platform-x-btn:hover { background: rgba(255, 255, 255, 0.25); }
/* ============================================================
TWO-COLUMN LAYOUT
============================================================ */
.social-two-col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30px;
margin-bottom: 0;
}
/* Colonnes alignées en haut (chacune prend sa hauteur naturelle) */
.social-two-col--top {
align-items: start;
}
@media (max-width: 900px) {
.social-two-col {
grid-template-columns: 1fr;
}
}
/* ============================================================
EMBED CONTAINERS (Facebook, X)
============================================================ */
.social-embed-container {
background: var(--card-bg);
border-radius: 0 0 10px 10px;
overflow: hidden;
min-height: 500px;
display: flex;
align-items: flex-start;
justify-content: center;
box-shadow: var(--card-shadow);
padding: 12px;
max-width: 100%;
box-sizing: border-box;
}
/* Empêche les iframes tierces de dépasser du conteneur */
.social-embed-container iframe,
.social-embed-container > div {
max-width: 100%;
}
.social-embed-container .twitter-timeline {
display: block;
width: 100%;
}
/* Profile iframes (TikTok creator embed, Instagram profile embed) */
.platform-profile-embed {
padding: 0;
min-height: unset;
align-items: stretch;
}
.platform-profile-iframe {
width: 100%;
height: 560px;
border: 0;
display: block;
}
.platform-profile-iframe--instagram {
height: 360px;
}
@media (max-width: 768px) {
.platform-profile-iframe {
height: 480px;
}
.platform-profile-iframe--instagram {
height: 300px;
}
}
/* ============================================================
YOUTUBE VIDEO GRID
============================================================ */
.social-video-grid {
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 20px;
box-shadow: var(--card-shadow);
}
.social-video-card {
display: flex;
flex-direction: column;
text-decoration: none !important;
color: var(--text-color) !important;
transition: transform 0.25s ease, box-shadow 0.25s ease;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
box-shadow: var(--card-shadow);
}
.social-video-card:hover {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
color: var(--text-color) !important;
}
.social-video-card .video-thumbnail {
position: relative;
padding-top: 56.25%;
overflow: hidden;
background: #000;
}
.social-video-card .video-thumbnail img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.social-video-card:hover .video-thumbnail img {
transform: scale(1.05);
}
/* Sous-titre de section (Shorts / Vidéos) */
.yt-subgrid-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
font-weight: 600;
color: var(--text-muted, #888);
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 16px 20px 4px;
}
.yt-subgrid-label i {
font-size: 12px;
}
/* Grille Shorts : cartes portrait centrées */
.social-shorts-grid {
grid-template-columns: repeat(auto-fit, minmax(160px, 220px));
justify-content: center;
}
/* Shorts — ratio portrait 9:16 */
.video-card--short .video-thumbnail {
padding-top: 177.78%;
}
.short-badge {
position: absolute;
bottom: 8px;
left: 8px;
display: inline-flex;
align-items: center;
gap: 4px;
background: #FF0000;
color: white;
font-size: 11px;
font-weight: 700;
padding: 3px 8px;
border-radius: 4px;
letter-spacing: 0.3px;
pointer-events: none;
text-transform: uppercase;
}
.youtube-play-icon {
font-size: 44px !important;
color: #FF0000 !important;
filter: drop-shadow(0 2px 10px rgba(0, 0, 0, 0.6));
}
/* ============================================================
FALLBACK CARDS
============================================================ */
.platform-fallback-card {
display: flex;
align-items: center;
gap: 32px;
padding: 48px 40px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
box-shadow: var(--card-shadow);
flex-wrap: wrap;
justify-content: center;
text-align: center;
min-height: 240px;
}
.platform-fallback-icon {
font-size: 90px;
line-height: 1;
flex-shrink: 0;
opacity: 0.9;
}
.youtube-fallback-icon i { color: #FF0000; }
.instagram-fallback-icon i {
background: linear-gradient(45deg, #F58529, #DD2A7B, #8134AF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.tiktok-fallback-icon i { color: #010101; }
[data-theme="dark"] .tiktok-fallback-icon i { color: #ffffff; }
.platform-fallback-content h3 {
font-size: 24px;
font-weight: 700;
margin-bottom: 10px;
}
.platform-fallback-content p {
font-size: 16px;
color: var(--text-secondary);
margin-bottom: 24px;
max-width: 380px;
line-height: 1.5;
}
/* ============================================================
INSTAGRAM POSTS GRID
============================================================ */
.instagram-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.instagram-post-wrapper {
display: flex;
justify-content: center;
}
/* ============================================================
TIKTOK VIDEOS GRID
============================================================ */
.tiktok-videos-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.tiktok-video-wrapper {
display: flex;
justify-content: center;
}
/* ============================================================
WORDPRESS ARTICLES
============================================================ */
.wp-section-header { background: linear-gradient(135deg, #0a0a0a 0%, #1a0000 60%, #2d0000 100%); }
.wp-section-logo {
height: 28px;
width: auto;
filter: drop-shadow(0 2px 6px rgba(255, 0, 0, 0.4));
flex-shrink: 0;
}
.wp-cta-btn { background: rgba(255, 255, 255, 0.2); }
.wp-cta-btn:hover { background: rgba(255, 255, 255, 0.3); }
.wp-posts-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
background: var(--card-bg);
border-radius: 0 0 10px 10px;
padding: 24px;
box-shadow: var(--card-shadow);
}
.wp-post-card {
display: flex;
flex-direction: column;
text-decoration: none !important;
color: var(--text-color) !important;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg);
box-shadow: var(--card-shadow);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.wp-post-card:hover {
transform: translateY(-6px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
color: var(--text-color) !important;
}
.wp-post-thumbnail {
position: relative;
padding-top: 56.25%;
overflow: hidden;
background: var(--border-color);
}
.wp-post-thumbnail img {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.wp-post-card:hover .wp-post-thumbnail img {
transform: scale(1.05);
}
.wp-post-thumbnail--placeholder {
display: flex;
align-items: center;
justify-content: center;
}
.wp-post-thumbnail--placeholder i {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
font-size: 40px;
color: var(--text-secondary);
opacity: 0.4;
}
.wp-post-info {
padding: 14px 16px 16px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
}
.wp-post-title {
font-size: 15px;
font-weight: 700;
line-height: 1.4;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.wp-post-excerpt {
font-size: 13px;
color: var(--text-secondary);
line-height: 1.5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.wp-post-meta {
display: flex;
gap: 14px;
font-size: 12px;
color: var(--text-secondary);
margin-top: auto;
flex-wrap: wrap;
}
.wp-post-meta span {
display: flex;
align-items: center;
gap: 5px;
}
@media (max-width: 768px) {
.wp-posts-grid {
grid-template-columns: 1fr;
padding: 16px;
}
}
/* ============================================================
HEADER BRAND
============================================================ */
.header-brand {
display: flex;
align-items: center;
flex-shrink: 0;
}
.header-logo-link {
display: flex;
align-items: center;
gap: 10px;
text-decoration: none;
color: var(--text-color);
}
.header-logo-img {
height: 40px;
width: auto;
}
.header-site-name {
font-size: 16px;
font-weight: 700;
white-space: nowrap;
}
/* ============================================================
DARK THEME
============================================================ */
[data-theme="dark"] .social-embed-container,
[data-theme="dark"] .platform-fallback-card,
[data-theme="dark"] .social-video-grid,
[data-theme="dark"] .instagram-posts-grid,
[data-theme="dark"] .tiktok-videos-grid {
background: var(--card-bg);
}
/* ============================================================
RESPONSIVE
============================================================ */
@media (max-width: 1000px) {
.header-site-name {
display: none;
}
}
@media (max-width: 768px) {
.social-hero {
padding: 28px 20px;
border-radius: 8px;
}
.social-hero-text h1 {
font-size: 1.6rem;
}
.social-hero-logo {
height: 70px;
}
.platform-badge span {
display: none;
}
.platform-badge {
width: 50px;
height: 50px;
padding: 0;
justify-content: center;
border-radius: 50%;
font-size: 18px;
}
.platform-badge-count {
font-size: 10px;
}
.social-section-header {
padding: 14px 16px;
}
.social-section-header h2 {
font-size: 18px;
}
.platform-handle {
display: none;
}
.platform-fallback-card {
padding: 32px 20px;
flex-direction: column;
gap: 20px;
}
.platform-fallback-icon {
font-size: 70px;
}
.platform-fallback-content h3 {
font-size: 20px;
}
.instagram-posts-grid,
.tiktok-videos-grid {
grid-template-columns: 1fr;
padding: 16px;
}
.social-embed-container {
min-height: 400px;
padding: 4px;
}
.platform-profile-embed {
padding: 0;
}
}
@media (max-width: 480px) {
.platform-nav {
gap: 8px;
}
.social-hero-brand {
flex-direction: column;
}
.social-hero-logo {
height: 60px;
}
}
/* ============================================================
SCROLL ARRIVAL HIGHLIGHT
============================================================ */
@keyframes section-arrive {
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.5); }
40% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0.15); }
100% { box-shadow: 0 0 0 18px rgba(255, 0, 0, 0); }
}
.section-arrive {
animation: section-arrive 0.9s cubic-bezier(0.22, 1, 0.36, 1) forwards;
border-radius: 12px;
}
+48 -56
View File
@@ -1,80 +1,72 @@
<!-- Footer --> <?php
$currentPage = basename($_SERVER['PHP_SELF']);
?>
<div class="footer"> <div class="footer">
<div class="footer-header"> <div class="footer-header">
<div class="footer-logo"> <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>
<div class="footer-contact-info"> <div class="footer-contact-info">
<div class="footer-contact">CONTACT</div> <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> </div>
<div class="footer-columns"> <div class="footer-columns">
<div class="footer-column"> <div class="footer-column">
<h3 class="footer-title">Catégories</h3> <h3 class="footer-title">Réseaux sociaux</h3>
<div> <ul class="footer-links">
<ul class="footer-links"> <li>
<li><a href="index.php" <?php echo ($currentPage === 'index.php') ? 'class="active"' : ''; ?>>Accueil</a></li> <a href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" target="_blank" rel="noopener noreferrer">
<li><a href="direct.php" <?php echo ($currentPage === 'direct.php') ? 'class="active"' : ''; ?>>Direct</a></li> <i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
</a>
<?php </li>
if (defined('PRIORITY_CATEGORIES') && !empty(PRIORITY_CATEGORIES)) { <li>
foreach (PRIORITY_CATEGORIES as $id => $name) { <a href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" target="_blank" rel="noopener noreferrer">
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id); <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
echo '<li><a href="categories.php?id=' . $id . '"' . ($isActive ? ' class="active"' : '') . '>' . htmlspecialchars($name) . '</a></li>'; </a>
} </li>
} <li>
?> <a href="<?php echo htmlspecialchars(TIKTOK_URL); ?>" target="_blank" rel="noopener noreferrer">
</ul> <i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i> TikTok
</div> </a>
</div> </li>
</ul>
<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>
</div> </div>
<div class="footer-column"> <div class="footer-column">
<h3 class="footer-title">Informations légales</h3> <h3 class="footer-title">Informations légales</h3>
<div> <ul class="footer-links">
<ul class="footer-links"> <li>
<li><a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>Mentions légales</a></li> <a href="mentions-legales.php" <?php echo ($currentPage === 'mentions-legales.php') ? 'class="active"' : ''; ?>>
<li> Mentions légales
<a href="<?php echo LEGAL_SOURCE_CODE_URL; ?>" target="_blank" rel="noopener noreferrer"> </a>
<i class="fab fa-git-alt"></i> Code source </li>
</a> <li>
</li> <a href="<?php echo htmlspecialchars(LEGAL_SOURCE_CODE_URL); ?>" target="_blank" rel="noopener noreferrer">
</ul> <i class="fab fa-git-alt" aria-hidden="true"></i> Code source
</div> </a>
</li>
</ul>
</div> </div>
</div> </div>
<div class="footer-social"> <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="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a> <i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a> </a>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a> <a target="_blank" rel="noopener noreferrer" href="<?php echo htmlspecialchars(INSTAGRAM_URL); ?>" aria-label="Instagram">
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a> <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
<a target="_blank" rel="noreferrer" href="<?php echo TIKTOK_URL; ?>"><i class="fab fa-tiktok icon-tiktok"></i></a> </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 class="footer-copyright"> <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>
</div> </div>
+15 -32
View File
@@ -1,47 +1,30 @@
<!-- Header avec barre de recherche et icônes --> <!-- Header -->
<header class="header" role="banner"> <header class="header" role="banner">
<div class="search-container"> <div class="header-brand">
<form action="recherche.php" method="get" role="search" aria-label="Recherche de vidéos"> <a href="/" class="header-logo-link" aria-label="Accueil <?php echo htmlspecialchars(SITE_NAME); ?>">
<label for="search-input" class="sr-only">Rechercher des vidéos</label> <img src="img/logo.png" alt="<?php echo htmlspecialchars(SITE_NAME); ?>" class="header-logo-img">
<input type="text" id="search-input" name="q" placeholder="Rechercher..." aria-describedby="search-help"> <span class="header-site-name"><?php echo htmlspecialchars(SITE_NAME); ?></span>
<button type="submit" aria-label="Lancer la recherche"> </a>
<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> </div>
<nav class="social-icons" aria-label="Réseaux sociaux"> <nav class="social-icons" aria-label="Nos réseaux sociaux">
<a target="_blank" rel="me noreferrer" href="<?php echo MASTODON_URL; ?>" class="icon-button" aria-label="Suivre sur Mastodon"> <a target="_blank" rel="noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>"
<i class="fab fa-mastodon icon-mastodon" aria-hidden="true"></i> class="icon-button" aria-label="YouTube">
<i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
</a> </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> <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
</a> </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> <i class="fab fa-tiktok icon-tiktok" aria-hidden="true"></i>
</a> </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> </nav>
<div class="action-icons"> <div class="action-icons">
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?> <?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> <i class="fas fa-heart" aria-hidden="true"></i>
</a> </a>
<?php endif; ?> <?php endif; ?>
+46 -76
View File
@@ -1,92 +1,62 @@
<!-- Menu mobile (masqué par défaut) --> <?php
<div class="mobile-menu"> $currentPage = basename($_SERVER['PHP_SELF']);
<button class="mobile-menu-close"> $isHome = ($currentPage === 'index.php' || $currentPage === '/');
<i class="fas fa-times"></i> ?>
<!-- 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> </button>
<div class="search-container"> <nav>
<form action="recherche.php" method="get"> <a href="/"
<input type="text" name="q" placeholder="Rechercher..."> class="nav-item <?php echo $isHome ? 'active' : ''; ?>">
<button type="submit"><i class="fas fa-search"></i></button> <i class="fas fa-home" aria-hidden="true"></i> Accueil
</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
</a> </a>
<hr class="nav-divider"> <hr class="nav-divider">
<?php <p class="mobile-section-title">Nos réseaux</p>
// 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) { <a href="<?php echo $isHome ? '#youtube' : 'index.php#youtube'; ?>"
$isActive = ($currentPage === 'categories.php' && $currentCategoryId === $id); class="nav-item">
$icon = isset($categoryIcons[$id]) ? $categoryIcons[$id] : 'fas fa-folder'; <i class="fab fa-youtube icon-youtube" aria-hidden="true"></i> YouTube
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> </a>
<?php <a href="<?php echo $isHome ? '#instagram' : 'index.php#instagram'; ?>"
endforeach; class="nav-item">
endif; <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i> Instagram
?> </a>
</div> <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"> <hr class="nav-divider">
<div> <div>
<h3 class="mobile-section-title">Suivez-nous</h3> <p class="mobile-section-title">Suivez-nous</p>
<div class="mobile-social-icons"> <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="noopener noreferrer" href="<?php echo htmlspecialchars(YOUTUBE_URL); ?>" aria-label="YouTube">
<a target="_blank" rel="noreferrer" href="<?php echo FACEBOOK_URL; ?>"><i class="fab fa-facebook icon-facebook"></i></a> <i class="fab fa-youtube icon-youtube" aria-hidden="true"></i>
<a target="_blank" rel="noreferrer" href="<?php echo INSTAGRAM_URL; ?>"><i class="fab fa-instagram icon-instagram"></i></a> </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(INSTAGRAM_URL); ?>" aria-label="Instagram">
<a target="_blank" rel="noreferrer" href="<?php echo YOUTUBE_URL; ?>"><i class="fab fa-youtube icon-youtube"></i></a> <i class="fab fa-instagram icon-instagram" aria-hidden="true"></i>
<a target="_blank" rel="noreferrer" href="<?php echo X_URL; ?>"><i class="fab fa-x-twitter icon-x"></i></a> </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> </div>
</div> </div>
+32 -92
View File
@@ -181,107 +181,47 @@ function validateCSRFToken($token) {
* Applique des en-têtes de sécurité HTTP * Applique des en-têtes de sécurité HTTP
*/ */
function setSecurityHeaders() { function setSecurityHeaders() {
// Protection contre le clickjacking (permettre les iframes du même site)
header('X-Frame-Options: SAMEORIGIN'); header('X-Frame-Options: SAMEORIGIN');
// Protection contre le MIME sniffing
header('X-Content-Type-Options: nosniff'); header('X-Content-Type-Options: nosniff');
// Protection XSS basique
header('X-XSS-Protection: 1; mode=block'); header('X-XSS-Protection: 1; mode=block');
// Politique de référent
header('Referrer-Policy: strict-origin-when-cross-origin'); header('Referrer-Policy: strict-origin-when-cross-origin');
// Content Security Policy avec support Mastodon et PeerTube $isLocalDev = in_array(
$mastodonDomain = ''; $_SERVER['HTTP_HOST'] ?? '',
$peertubeDomain = ''; ['127.0.0.1:8080', '127.0.0.1:8001', 'localhost:8080', 'localhost:8001', '127.0.0.1', 'localhost']
);
// Extraire le domaine Mastodon si configuré
if (defined('MASTODON_INSTANCE_URL')) { // Domaines des 5 plateformes sociales
$mastodonParsed = parse_url(MASTODON_INSTANCE_URL); $fbScripts = 'https://connect.facebook.net';
if ($mastodonParsed && isset($mastodonParsed['host'])) { $fbFrames = 'https://www.facebook.com https://staticxx.facebook.com https://www.facebook.net';
$mastodonDomain = $mastodonParsed['scheme'] . '://' . $mastodonParsed['host']; $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';
// Extraire le domaine PeerTube si configuré $ttScripts = 'https://www.tiktok.com https://lf16-tiktok-web.ttwstatic.com';
if (defined('PEERTUBE_URL')) { $ttFrames = 'https://www.tiktok.com';
$peertubeParsed = parse_url(PEERTUBE_URL); $ytImages = 'https://i.ytimg.com https://yt3.ggpht.com';
if ($peertubeParsed && isset($peertubeParsed['host'])) {
$peertubeDomain = $peertubeParsed['scheme'] . '://' . $peertubeParsed['host']; $csp = "default-src 'self'; ";
}
}
// 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' 'unsafe-inline' https://cdnjs.cloudflare.com; "; $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; "; $csp .= "font-src 'self' https://cdnjs.cloudflare.com; ";
$csp .= "script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://plausible.io "
// Frames : autoriser PeerTube et HTTPS général . "{$fbScripts} {$xScripts} {$igScripts} {$ttScripts}; ";
$frameSrc = "'self' " . ($peertubeDomain ? $peertubeDomain : ''); $csp .= "img-src 'self' data: {$ytImages} https://www.facebook.com https://pbs.twimg.com https://abs.twimg.com"
if ($isLocalDev) { . ($isLocalDev ? " https: http:" : " https:") . "; ";
$frameSrc .= " https: http:"; $csp .= "frame-src 'self' {$fbFrames} {$xFrames} {$igFrames} {$ttFrames} https://www.youtube.com"
} else { . ($isLocalDev ? " http:" : "") . "; ";
$frameSrc .= " https:"; $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 "
$csp .= "frame-src " . $frameSrc . "; "; . "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"
// Connexions : autoriser Mastodon et PeerTube . ($isLocalDev ? " ws: wss:" : "") . "; ";
$connectSrc = "'self' https://plausible.io " . ($mastodonDomain ? $mastodonDomain : '') . " " . ($peertubeDomain ? $peertubeDomain : ''); $csp .= "media-src 'self' https:; ";
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 .= "object-src 'none'; "; $csp .= "object-src 'none'; ";
$csp .= "base-uri 'self';"; $csp .= "base-uri 'self';";
header('Content-Security-Policy: ' . $csp); header('Content-Security-Policy: ' . $csp);
// HTTPS strict transport security (seulement si HTTPS)
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') { if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') {
header('Strict-Transport-Security: max-age=31536000; includeSubDomains'); header('Strict-Transport-Security: max-age=31536000; includeSubDomains');
} }
+33 -68
View File
@@ -1,88 +1,53 @@
<!-- Sidebar de navigation --> <!-- Sidebar de navigation -->
<nav class="sidebar" role="navigation" aria-label="Navigation principale"> <nav class="sidebar" role="navigation" aria-label="Navigation principale">
<a href="/" class="logo" aria-label="Retour à l'accueil"> <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> </a>
<?php <?php
// Détecter la page courante et ses paramètres
$currentPage = basename($_SERVER['PHP_SELF']); $currentPage = basename($_SERVER['PHP_SELF']);
$currentCategoryId = isset($_GET['id']) ? intval($_GET['id']) : null; $isHome = ($currentPage === 'index.php' || $currentPage === '/');
$currentQuery = isset($_GET['q']) ? trim($_GET['q']) : '';
$isTagSearch = !empty($currentQuery) && substr($currentQuery, 0, 1) === '#';
$currentTag = $isTagSearch ? substr($currentQuery, 1) : '';
?> ?>
<div class="sidebar-nav"> <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> <i class="fas fa-home" aria-hidden="true"></i> <span>Accueil</span>
</a> </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> </a>
<?php if (defined('DONATIONS_ENABLED') && DONATIONS_ENABLED && !empty(PAYPAL_ME_URL)): ?> <?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> <i class="fas fa-heart" aria-hidden="true"></i> <span>Soutenir</span>
</a> </a>
<?php endif; ?> <?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> </div>
</nav> </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; 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 * Récupère les articles WordPress récents
* *
@@ -157,7 +197,7 @@ function formatWordPressPosts($posts) {
// Nettoyer l'extrait HTML // Nettoyer l'extrait HTML
$excerpt = ''; $excerpt = '';
if (isset($post['excerpt']['rendered'])) { 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); $excerpt = trim($excerpt);
// Limiter à 150 caractères // Limiter à 150 caractères
if (strlen($excerpt) > 150) { if (strlen($excerpt) > 150) {
+348 -581
View File
File diff suppressed because it is too large Load Diff