<?php
declare(strict_types=1);
session_name('voleurdimages_guestbook');
session_set_cookie_params(['lifetime'=>0,'path'=>'/','secure'=>true,'httponly'=>true,'samesite'=>'Strict']);
session_start();
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: strict-origin-when-cross-origin');
header('X-Frame-Options: DENY');
header('Cache-Control: no-store, max-age=0');

const MAX_NAME = 60;
const MAX_MESSAGE = 1200;
const RETENTION_DAYS = 365;
$dataDir = __DIR__ . '/livre-or-data';
$dataFile = $dataDir . '/messages.json';

function e(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function load_messages(string $file, bool $prune = false): array {
    $fp = @fopen($file, 'c+');
    if (!$fp) return [];
    if (!flock($fp, LOCK_EX)) { fclose($fp); return []; }
    rewind($fp); $raw = stream_get_contents($fp); $rows = json_decode($raw ?: '[]', true);
    if (!is_array($rows)) $rows = [];
    $cutoff = time() - RETENTION_DAYS * 86400;
    $rows = array_values(array_filter($rows, static fn($r) => is_array($r) && isset($r['created']) && (int)$r['created'] >= $cutoff));
    if ($prune) { rewind($fp); ftruncate($fp, 0); fwrite($fp, json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); fflush($fp); }
    flock($fp, LOCK_UN); fclose($fp);
    return $rows;
}
function append_message(string $file, array $newRow): bool {
    $fp = @fopen($file, 'c+');
    if (!$fp || !flock($fp, LOCK_EX)) { if ($fp) fclose($fp); return false; }
    rewind($fp); $raw = stream_get_contents($fp); $rows = json_decode($raw ?: '[]', true);
    if (!is_array($rows)) $rows = [];
    $cutoff = time() - RETENTION_DAYS * 86400;
    $rows = array_values(array_filter($rows, static fn($r) => is_array($r) && isset($r['created']) && (int)$r['created'] >= $cutoff));
    $rows[] = $newRow;
    rewind($fp); ftruncate($fp, 0);
    $ok = fwrite($fp, json_encode($rows, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)) !== false;
    fflush($fp); flock($fp, LOCK_UN); fclose($fp);
    return $ok;
}
function valid_facebook_url(string $url): bool {
    if (strlen($url) > 255 || preg_match('/[\x00-\x20\x7F]/', $url)) return false;
    $parts = parse_url($url);
    if (!is_array($parts) || strtolower((string)($parts['scheme'] ?? '')) !== 'https') return false;
    $host = strtolower(rtrim((string)($parts['host'] ?? ''), '.'));
    if (!in_array($host, ['facebook.com', 'www.facebook.com', 'm.facebook.com'], true)) return false;
    if (isset($parts['user']) || isset($parts['pass']) || (isset($parts['port']) && (int)$parts['port'] !== 443)) return false;
    return isset($parts['path']) && $parts['path'] !== '/' && filter_var($url, FILTER_VALIDATE_URL) !== false;
}
function clean_text(string $s, int $max): string {
    $s = trim(str_replace(["\r\n", "\r"], "\n", $s));
    $s = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $s) ?? '';
    if (function_exists('mb_substr')) return mb_substr($s, 0, $max, 'UTF-8');
    return substr($s, 0, $max);
}

if (!isset($_SESSION['guestbook_csrf'])) $_SESSION['guestbook_csrf'] = bin2hex(random_bytes(32));
$formTime = time();
$formSig = hash_hmac('sha256', (string)$formTime, (string)$_SESSION['guestbook_csrf']);
$notice = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $provided = is_string($_POST['csrf'] ?? null) ? $_POST['csrf'] : '';
    $trap = is_string($_POST['website'] ?? null) ? trim($_POST['website']) : '';
    $postedTime = is_string($_POST['form_time'] ?? null) ? $_POST['form_time'] : '';
    $postedSig = is_string($_POST['form_sig'] ?? null) ? $_POST['form_sig'] : '';
    $now = time(); $last = (int)($_SESSION['guestbook_last_post'] ?? 0);
    $nameRaw = is_string($_POST['name'] ?? null) ? $_POST['name'] : '';
    $messageRaw = is_string($_POST['message'] ?? null) ? $_POST['message'] : '';
    $name = clean_text($nameRaw, MAX_NAME);
    $message = clean_text($messageRaw, MAX_MESSAGE);
    $facebookUrl = is_string($_POST['facebook_url'] ?? null) ? trim($_POST['facebook_url']) : '';
    $consent = isset($_POST['consent']) && $_POST['consent'] === 'yes';
    $timingValid = ctype_digit($postedTime) && hash_equals(hash_hmac('sha256', $postedTime, (string)$_SESSION['guestbook_csrf']), $postedSig) && $now - (int)$postedTime >= 3 && $now - (int)$postedTime <= 7200;
    $facebookValid = $facebookUrl === '' || valid_facebook_url($facebookUrl);
    if (!hash_equals((string)$_SESSION['guestbook_csrf'], $provided) || !$timingValid || $trap !== '' || ($last && $now - $last < 60)) {
        $notice = 'Votre message n’a pas pu être envoyé. Vérifiez le formulaire et réessayez dans un instant.';
    } elseif (!$consent || $message === '' || strlen($nameRaw) > 512 || strlen($messageRaw) > 10000 || preg_match('//u', $name) !== 1 || preg_match('//u', $message) !== 1 || !$facebookValid) {
        $notice = 'Merci de vérifier le message, le lien Facebook et la case de publication.';
    } elseif (!is_dir($dataDir) || !is_writable($dataDir)) {
        $notice = 'Le Livre d’or est momentanément indisponible.';
    } else {
        $ok = append_message($dataFile, ['name' => $name !== '' ? $name : 'Visiteur', 'message' => $message, 'facebook_url' => $facebookUrl, 'created' => $now, 'approved' => false]);
        if ($ok) {
            $_SESSION['guestbook_last_post'] = $now;
            $_SESSION['guestbook_csrf'] = bin2hex(random_bytes(32));
            $notice = 'Merci pour votre message. Il sera visible après validation.';
        } else $notice = 'Le Livre d’or est momentanément indisponible.';
    }
}
$rows = load_messages($dataFile, true);
$approved = array_values(array_filter($rows, static fn($r) => !empty($r['approved'])));
?><!doctype html>
<html lang="fr"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="robots" content="index,follow"><title>Livre d’or — Voleur d’images</title><meta property="og:type" content="website"><meta property="og:site_name" content="Voleur d’images"><meta property="og:title" content="Livre d’or — Voleur d’images"><meta property="og:description" content="Laissez un message dans le Livre d’or de Voleur d’images."><meta property="og:url" content="https://voleurdimages.fr/livre-or.php"><meta property="og:image" content="https://voleurdimages.fr/livre-or-share.png?v=20261020"><meta property="og:image:secure_url" content="https://voleurdimages.fr/livre-or-share.png?v=20261020"><meta property="og:image:type" content="image/png"><meta property="og:image:width" content="1734"><meta property="og:image:height" content="907"><meta property="og:image:alt" content="Livre d’or ouvert avec un stylo et des photos de villages français"><meta name="twitter:card" content="summary_large_image"><meta name="twitter:image" content="https://voleurdimages.fr/livre-or-share.png?v=20261020">
<style>
:root{color-scheme:light;--ink:#302720;--wine:#694235;--gold:#a77b45;--paper:#f7f3ec;--card:#fffdf9;--muted:#71685f}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.65 system-ui,-apple-system,"Segoe UI",sans-serif}header,main,footer{width:min(900px,calc(100% - 2rem));margin:auto}header{padding:1.25rem 0}header a,footer a{color:var(--wine)}.brand{font-weight:700;letter-spacing:.08em;text-decoration:none}main{padding:1.5rem 0 4rem}.eyebrow{color:var(--gold);font-size:.75rem;letter-spacing:.18em;text-transform:uppercase;font-weight:700}h1,h2{color:var(--wine);font-weight:500;line-height:1.2}h1{font-size:clamp(2.2rem,6vw,3.8rem);margin:.2rem 0}h2{font-size:1.5rem}.intro{color:var(--muted);max-width:650px}.panel,.entry,.privacy{background:var(--card);border:1px solid #e7ddd0;border-radius:10px;padding:clamp(1rem,4vw,2rem);margin:1.5rem 0;box-shadow:0 8px 28px #3027200d}label{display:block;font-weight:650;margin:.8rem 0 .25rem}input,textarea{width:100%;padding:.8rem;border:1px solid #c9b9a5;border-radius:5px;background:white;font:inherit}textarea{min-height:150px;resize:vertical}.help{font-size:.88rem;color:var(--muted)}.check{display:flex;align-items:flex-start;gap:.65rem;font-weight:400;margin:1rem 0}.check input{width:auto;margin-top:.4rem}.trap{position:absolute;left:-10000px}button{border:0;border-radius:5px;background:var(--wine);color:white;padding:.75rem 1.15rem;font:inherit;font-weight:700;cursor:pointer}.notice{padding:.8rem 1rem;border-left:4px solid var(--gold);background:#f1e8da}.entry h3{margin:0;color:var(--wine)}.entry time{font-size:.85rem;color:var(--muted)}.privacy{font-size:.93rem;color:#554b42}footer{border-top:1px solid #ddd0c0;padding:1.25rem 0 2rem;color:var(--muted)}.facebook-share{display:inline-flex;align-items:center;gap:.6rem;margin:.4rem 0 1rem;padding:.7rem 1rem;border-radius:5px;background:#1877f2;color:#fff;text-decoration:none;font-weight:700;box-shadow:0 3px 10px #30272018}.facebook-share:hover{background:#145fbe}.facebook-share:focus-visible{outline:3px solid var(--gold);outline-offset:3px}.facebook-mark{display:grid;place-items:center;width:1.4rem;height:1.4rem;border-radius:50%;background:#fff;color:#1877f2;font:bold 1rem Arial,sans-serif}
</style></head><body><header><a class="brand" href="index.php">← Voleur d’images</a></header><main><p class="eyebrow">Un mot de votre visite</p><h1>Livre d’or</h1><p class="intro">Merci de partager vos impressions après votre visite. Les messages sont vérifiés avant leur publication.</p><a class="facebook-share" href="https://www.facebook.com/sharer/sharer.php?u=<?= rawurlencode('https://voleurdimages.fr/livre-or.php') ?>" target="_blank" rel="noopener noreferrer" aria-label="Partager le Livre d’or sur Facebook"><span class="facebook-mark" aria-hidden="true">f</span>Partager le Livre d’or sur Facebook</a>
<?php if ($notice !== ''): ?><p class="notice" role="status"><?= e($notice) ?></p><?php endif; ?>
<section class="panel" aria-labelledby="form-title"><h2 id="form-title">Laisser un message</h2><form method="post" action="livre-or.php"><input type="hidden" name="csrf" value="<?= e((string)$_SESSION['guestbook_csrf']) ?>"><input type="hidden" name="form_time" value="<?= (int)$formTime ?>"><input type="hidden" name="form_sig" value="<?= e($formSig) ?>"><div class="trap" aria-hidden="true"><label for="website">Ne pas remplir</label><input id="website" name="website" type="text" tabindex="-1" autocomplete="off"></div><label for="name">Nom ou pseudonyme <span class="help">(facultatif, 60 caractères maximum)</span></label><input id="name" name="name" maxlength="60" autocomplete="nickname"><label for="facebook_url">Profil Facebook <span class="help">(facultatif)</span></label><input id="facebook_url" name="facebook_url" type="url" maxlength="255" inputmode="url" placeholder="https://www.facebook.com/votre.profil" autocomplete="url" aria-describedby="facebook-help"><p id="facebook-help" class="help">Ajoutez le lien public de votre profil. Il sera affiché comme lien vers Facebook après validation, uniquement si vous le renseignez.</p><label for="message">Votre message</label><textarea id="message" name="message" maxlength="1200" required aria-describedby="message-help"></textarea><p id="message-help" class="help">1 200 caractères maximum. N’indiquez pas d’adresse, de numéro de téléphone ni d’autre information personnelle.</p><label class="check"><input type="checkbox" name="consent" value="yes" required><span>J’accepte que mon nom ou pseudonyme et mon message soient affichés publiquement après validation. Si je renseigne un profil Facebook, j’accepte également l’affichage de son lien. J’ai pris connaissance des informations de confidentialité ci-dessous.</span></label><button type="submit">Envoyer pour validation</button></form></section>
<section aria-labelledby="entries-title"><h2 id="entries-title">Les mots des visiteurs</h2><?php if (!$approved): ?><p class="help">Aucun message publié pour le moment.</p><?php endif; ?><?php foreach ($approved as $row): ?><article class="entry"><h3><?= e((string)($row['name'] ?? 'Visiteur')) ?></h3><?php if (!empty($row['facebook_url']) && valid_facebook_url((string)$row['facebook_url'])): ?><p><a href="<?= e((string)$row['facebook_url']) ?>" target="_blank" rel="nofollow noopener noreferrer">Profil Facebook</a></p><?php endif; ?><time datetime="<?= gmdate('Y-m-d', (int)$row['created']) ?>"><?= date('d/m/Y', (int)$row['created']) ?></time><p><?= nl2br(e((string)($row['message'] ?? ''))) ?></p></article><?php endforeach; ?></section>
<section class="privacy" id="confidentialite"><h2>Confidentialité et données personnelles</h2><p>Le formulaire ne demande ni adresse e-mail ni adresse postale. Le nom ou pseudonyme et le message, ainsi que le lien Facebook si vous le fournissez, sont utilisés uniquement pour gérer et, après validation et avec votre accord, afficher votre contribution. Ils sont conservés au maximum 12 mois, puis supprimés automatiquement lors d’une visite du Livre d’or. Aucun outil publicitaire ni traceur n’est installé par cette page. Le code du Livre d’or ne conserve pas votre adresse IP ; l’hébergeur peut toutefois traiter des données techniques dans ses journaux selon ses propres règles.</p><p>Vous pouvez demander la suppression de votre contribution au responsable du site, Helis Denis, par le moyen de contact indiqué sur le site. Ne publiez pas d’informations concernant une autre personne. Les messages sont soumis à validation et peuvent être refusés ou supprimés.</p></section></main><footer><a href="index.php">Accueil</a> · Livre d’or de Voleur d’images</footer></body></html>
