39853-vm/site.php
2026-05-02 10:15:56 +00:00

376 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
@date_default_timezone_set('Europe/Paris');
function site_asset_version(): string
{
static $version = null;
if ($version !== null) {
return $version;
}
$paths = [
__DIR__ . '/assets/css/custom.css',
__DIR__ . '/assets/js/main.js',
];
$mtime = 0;
foreach ($paths as $path) {
if (is_file($path)) {
$mtime = max($mtime, (int) filemtime($path));
}
}
$version = (string) ($mtime ?: time());
return $version;
}
function site_request_scheme(): string
{
$cfVisitor = (string) ($_SERVER['HTTP_CF_VISITOR'] ?? '');
if ($cfVisitor !== '') {
$decoded = json_decode($cfVisitor, true);
if (is_array($decoded) && isset($decoded['scheme']) && in_array($decoded['scheme'], ['http', 'https'], true)) {
return $decoded['scheme'];
}
}
$forwardedProto = (string) ($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '');
if ($forwardedProto !== '') {
$proto = strtolower(trim(explode(',', $forwardedProto)[0]));
if (in_array($proto, ['http', 'https'], true)) {
return $proto;
}
}
$requestScheme = strtolower((string) ($_SERVER['REQUEST_SCHEME'] ?? ''));
if (in_array($requestScheme, ['http', 'https'], true)) {
return $requestScheme;
}
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
}
function site_request_host(): string
{
$host = strtolower(trim((string) ($_SERVER['HTTP_HOST'] ?? '')));
if ($host === '') {
return 'programmetelecesoir.net';
}
$host = preg_replace('/:\d+$/', '', $host) ?? $host;
return $host !== '' ? $host : 'programmetelecesoir.net';
}
function site_is_local_host(string $host): bool
{
return in_array($host, ['127.0.0.1', 'localhost'], true)
|| str_ends_with($host, '.local');
}
function site_enforce_public_url(): void
{
if (PHP_SAPI === 'cli') {
return;
}
$canonicalDomain = 'programmetelecesoir.net';
$host = site_request_host();
if ($host === '' || site_is_local_host($host)) {
return;
}
$scheme = site_request_scheme();
if ($host === $canonicalDomain && $scheme === 'https') {
return;
}
$requestUri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
if ($requestUri === '') {
$requestUri = '/';
}
header('Vary: Host, X-Forwarded-Proto, CF-Visitor', false);
header('Location: https://' . $canonicalDomain . $requestUri, true, 301);
exit;
}
site_enforce_public_url();
function site_settings(): array
{
static $settings = null;
if ($settings !== null) {
return $settings;
}
$scheme = site_request_scheme();
$canonicalDomain = 'programmetelecesoir.net';
$host = site_request_host();
$projectName = (string) ($_SERVER['PROJECT_NAME'] ?? '');
if ($projectName === '' || strcasecmp($projectName, 'programmetelecesoir.fr') === 0) {
$projectName = $canonicalDomain;
} else {
$projectName = str_ireplace('programmetelecesoir.fr', $canonicalDomain, $projectName);
}
$projectDescription = str_ireplace('programmetelecesoir.fr', $canonicalDomain, (string) ($_SERVER['PROJECT_DESCRIPTION'] ?? ''));
$settings = [
'domain' => $canonicalDomain,
'project_name' => $projectName,
'project_description' => $projectDescription,
'project_image_url' => $_SERVER['PROJECT_IMAGE_URL'] ?? '',
'google_site_verification' => 'uNUUwP2X_y7thS7ulDRafX0wNLRuC1l2Xj39FaiOZoM',
'base_url' => $scheme . '://' . $host,
'canonical_base_url' => 'https://' . $canonicalDomain,
'asset_version' => site_asset_version(),
'owner_name' => 'M LORENTE CHRISTOPHE',
'dpo_name' => 'M LORENTE CHRISTOPHE',
'dpo_address' => '7 rue Lucien Deneau 28300 Mainvilliers',
'dpo_phone' => '06 58 22 59 16',
'host_name' => 'FLATLOGIC.COM',
];
return $settings;
}
function e(mixed $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}
function site_current_path(): string
{
$requestUri = $_SERVER['REQUEST_URI'] ?? '/';
$path = strtok($requestUri, '?');
if ($path === false || $path === '' || $path === '/index.php') {
return '/';
}
return $path;
}
function site_asset_url(string $relativePath): string
{
return $relativePath . '?v=' . rawurlencode(site_asset_version());
}
function site_has_saved_consent(): bool
{
$rawConsent = $_COOKIE['ptcs_consent'] ?? null;
if (!is_string($rawConsent) || $rawConsent === '') {
return false;
}
$decoded = json_decode($rawConsent, true);
return is_array($decoded) && array_key_exists('essential', $decoded);
}
function site_is_home_request(): bool
{
return in_array(site_current_path(), ['/', '/index.php'], true);
}
function site_should_lock_cookie_overlay(): bool
{
return site_is_home_request() && !site_has_saved_consent();
}
function render_site_head(string $pageTitle, string $fallbackDescription, string $keywords = '', bool $noindex = false): void
{
$site = site_settings();
$projectDescription = $site['project_description'];
$projectImageUrl = $site['project_image_url'];
$canonical = ($site['canonical_base_url'] ?? $site['base_url']) . site_current_path();
$description = $fallbackDescription !== '' ? $fallbackDescription : $projectDescription;
?>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?= e($pageTitle) ?></title>
<meta name="description" content="<?= e($description) ?>" />
<?php if (!empty($site['google_site_verification'])): ?>
<meta name="google-site-verification" content="<?= e($site['google_site_verification']) ?>" />
<?php endif; ?>
<meta property="og:description" content="<?= e($description) ?>" />
<meta property="twitter:description" content="<?= e($description) ?>" />
<?php if ($projectImageUrl): ?>
<meta property="og:image" content="<?= e($projectImageUrl) ?>" />
<meta property="twitter:image" content="<?= e($projectImageUrl) ?>" />
<?php endif; ?>
<?php if ($keywords !== ''): ?>
<meta name="keywords" content="<?= e($keywords) ?>" />
<?php endif; ?>
<meta name="author" content="<?= e($site['owner_name']) ?>" />
<meta name="theme-color" content="#111827" />
<meta property="og:site_name" content="<?= e($site['domain']) ?>" />
<meta property="og:title" content="<?= e($pageTitle) ?>" />
<meta name="twitter:title" content="<?= e($pageTitle) ?>" />
<meta property="og:type" content="website" />
<meta property="og:url" content="<?= e($canonical) ?>" />
<meta property="twitter:card" content="summary_large_image" />
<link rel="canonical" href="<?= e($canonical) ?>" />
<?php if ($noindex): ?>
<meta name="robots" content="noindex, nofollow" />
<?php else: ?>
<meta name="robots" content="index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1" />
<?php endif; ?>
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="<?= e(site_asset_url('assets/css/custom.css')) ?>">
<?php
}
function nav_link(string $label, string $href, bool $active = false, bool $sectionLink = false): string
{
$className = 'nav-link';
if ($active) {
$className .= ' active';
}
$dataAttr = $sectionLink ? ' data-section-link="1"' : '';
$current = $active ? ' aria-current="page"' : '';
return '<a class="' . e($className) . '" href="' . e($href) . '"' . $current . $dataAttr . '>' . e($label) . '</a>';
}
function render_site_nav(string $current = 'home'): void
{
$site = site_settings();
?>
<header class="site-header">
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container">
<a class="navbar-brand" href="/"><?= e($site['domain']) ?></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#mainNavbar" aria-controls="mainNavbar" aria-expanded="false" aria-label="Afficher la navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNavbar">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-1">
<li class="nav-item"><?= nav_link('En ce moment', '/#widget-section', $current === 'home', true) ?></li>
<li class="nav-item"><?= nav_link('Guide TV', '/#guide', false, true) ?></li>
<li class="nav-item"><?= nav_link('Chaînes', '/#chaines', false, true) ?></li>
<li class="nav-item"><?= nav_link('FAQ', '/#faq', false, true) ?></li>
<li class="nav-item"><?= nav_link('Cookies', '/politique-cookies.php', $current === 'cookies') ?></li>
<li class="nav-item"><?= nav_link('Confidentialité', '/politique-confidentialite.php', $current === 'privacy') ?></li>
<li class="nav-item"><?= nav_link('Règlement', '/reglement.php', $current === 'rules') ?></li>
</ul>
</div>
</div>
</nav>
</header>
<?php
}
function render_site_footer(): void
{
$site = site_settings();
?>
<footer class="site-footer">
<div class="container">
<div class="footer-grid">
<div>
<div class="footer-brand"><?= e($site['domain']) ?></div>
<p class="footer-copy">Guide éditorial et page d'accès rapide au programme TV ce soir, au direct en ce moment et aux principales chaînes TNT.</p>
</div>
<div>
<div class="footer-title">Documents</div>
<ul class="footer-links list-unstyled mb-0">
<li><a href="/politique-cookies.php">Politique de cookies</a></li>
<li><a href="/politique-confidentialite.php">Politique de confidentialité</a></li>
<li><a href="/reglement.php">Règlement d'utilisation</a></li>
</ul>
</div>
<div>
<div class="footer-title">DPO & contact</div>
<address class="footer-copy mb-0">
<?= e($site['dpo_name']) ?><br>
<?= e($site['dpo_address']) ?><br>
Tél. <?= e($site['dpo_phone']) ?>
</address>
</div>
<div>
<div class="footer-title">Hébergement</div>
<p class="footer-copy mb-0">Hébergeur déclaré : <?= e($site['host_name']) ?>.</p>
<button type="button" class="link-button mt-2" id="footer-cookie-settings">Revoir mon choix cookies</button>
</div>
</div>
</div>
</footer>
<?php
}
function render_cookie_controls(): void
{
$hasSavedConsent = site_has_saved_consent();
$showBanner = !$hasSavedConsent;
$lockHome = site_should_lock_cookie_overlay();
?>
<div class="cookie-floating"<?= $showBanner ? ' hidden' : '' ?>>
<button type="button" class="cookie-reopen" id="cookie-reopen" aria-controls="cookie-banner" aria-expanded="false">
<span class="cookie-reopen__dot" aria-hidden="true"></span>
<span id="cookie-reopen-label">Cookies : essentiels</span>
</button>
</div>
<div class="cookie-overlay" id="cookie-overlay" aria-hidden="true"<?= $lockHome ? '' : ' hidden' ?>></div>
<div class="cookie-banner<?= $lockHome ? ' cookie-banner--modal' : '' ?>" id="cookie-banner" role="dialog" aria-modal="<?= $lockHome ? 'true' : 'false' ?>" aria-labelledby="cookie-banner-title" aria-describedby="cookie-banner-desc" tabindex="-1"<?= $showBanner ? '' : ' hidden' ?>>
<div class="cookie-banner__top">
<div>
<p class="cookie-banner__eyebrow mb-2">Préférences cookies</p>
<h2 class="cookie-banner__title" id="cookie-banner-title">Gérez votre confidentialité</h2>
</div>
</div>
<p class="cookie-banner__intro" id="cookie-banner-desc">Le widget TV nécessaire au service reste actif pour afficher le programme en temps réel. Les traceurs optionnels sont désactivés par défaut tant que vous n'avez pas choisi. Si vous activez la mesure locale d'audience, un identifiant first-party anonyme alimente le compteur visiteurs en direct, journalier et total. Sur la page d'accueil, l'overlay bloque la navigation jusqu'à l'enregistrement d'un choix.</p>
<div class="cookie-banner__toggles">
<div class="cookie-switch-row">
<div>
<strong>Essentiels</strong>
<p>Conservent votre choix de consentement et la sécurité minimale du site.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-essential" checked disabled>
</div>
</div>
<div class="cookie-switch-row">
<div>
<label class="cookie-switch-label" for="cookie-personalization">Personnalisation</label>
<p>Mémorise vos préférences d'interface sur cet appareil, comme le rappel de lecture mobile.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-personalization" data-consent-control="personalization">
</div>
</div>
<div class="cookie-switch-row">
<div>
<label class="cookie-switch-label" for="cookie-audience">Mesure locale d'audience</label>
<p>Active un compteur first-party anonyme pour afficher les visiteurs en direct, les visiteurs du jour et le total, sans service tiers ni publicité.</p>
</div>
<div class="form-check form-switch m-0">
<input class="form-check-input" type="checkbox" role="switch" id="cookie-audience" data-consent-control="audience">
</div>
</div>
</div>
<div class="cookie-banner__actions">
<button type="button" class="btn btn-light btn-refined" data-cookie-action="reject">Continuer sans accepter</button>
<button type="button" class="btn btn-outline-dark btn-refined" data-cookie-action="save">Enregistrer mes choix</button>
<button type="button" class="btn btn-dark btn-refined" data-cookie-action="accept">Tout accepter</button>
</div>
<div class="cookie-banner__links">
<a href="/politique-cookies.php">Voir la politique de cookies</a>
<span aria-hidden="true">•</span>
<a href="/politique-confidentialite.php">Confidentialité</a>
</div>
</div>
<div class="toast-stack" id="toast-stack" aria-live="polite" aria-atomic="true"></div>
<?php
}
function render_site_scripts(): void
{
?>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="<?= e(site_asset_url('assets/js/main.js')) ?>" defer></script>
<?php
}