setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->exec('CREATE TABLE IF NOT EXISTS entries ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, msg TEXT NOT NULL, ts INTEGER NOT NULL )'); $db->exec('CREATE TABLE IF NOT EXISTS ratelimit ( ip_hash TEXT PRIMARY KEY, last_ts INTEGER NOT NULL )'); $isHtmx = isset($_SERVER['HTTP_HX_REQUEST']); $notice = ''; function client_ip(): string { $xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? ''; if ($xff !== '') { return trim(explode(',', $xff)[0]); } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $name = trim($_POST['name'] ?? ''); $msg = trim($_POST['msg'] ?? ''); $trap = trim($_POST['url'] ?? ''); $ok = false; if ($trap !== '') { $ok = true; } elseif ($name === '' || $msg === '') { $notice = 'name and message are required.'; } elseif (gb_has_link($msg)) { $notice = 'links aren\'t allowed.'; } else { $ipHash = hash('sha256', client_ip() . '|guestbook'); $now = time(); $window = 30; $stmt = $db->prepare('SELECT last_ts FROM ratelimit WHERE ip_hash = ?'); $stmt->execute([$ipHash]); $last = (int) $stmt->fetchColumn(); if ($last && $now - $last < $window) { $notice = 'you\'re posting too fast — wait a moment.'; } else { $ins = $db->prepare('INSERT INTO entries (name, msg, ts) VALUES (?, ?, ?)'); $ins->execute([ gb_censor(mb_substr($name, 0, 40)), gb_censor(mb_substr($msg, 0, 250)), $now, ]); $db->prepare('INSERT INTO ratelimit (ip_hash, last_ts) VALUES (?, ?) ON CONFLICT(ip_hash) DO UPDATE SET last_ts = excluded.last_ts') ->execute([$ipHash, $now]); $ok = true; } } if (!$isHtmx && $ok) { header('Location: /guestbook.php'); exit; } } $rows = $db->query('SELECT name, msg, ts FROM entries ORDER BY id DESC LIMIT 50'); function render_entries(iterable $rows): void { foreach ($rows as $r) { ?>

' . htmlspecialchars($notice) . '

'; } render_entries($rows); exit; } require __DIR__ . '/inc/header.php'; ?>

guestbook