PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]); } catch (PDOException $e) { http_response_code(500); // Do NOT leak DB details echo 'FATAL ERROR: Database unavailable.'; exit; } if ($needsInit) { // Create table and initialize count $db->exec(" CREATE TABLE IF NOT EXISTS views ( host TEXT NOT NULL, webPage TEXT NOT NULL, accessMonth TEXT NOT NULL, hashedIP TEXT NOT NULL, count INTEGER NOT NULL, PRIMARY KEY (host, webPage, hashedIP, accessMonth) ); "); } //HTTP_HOST is the name the client knows the server by (so this might be an IP, hostname, FQDN, localhost etc.) //this is the client view and therefore this is user controlled -> sanitise as arbitrary data $rawHost = $_SERVER['HTTP_HOST'] ?? ''; $rawHost = strtolower(trim($rawHost)); $host = 'invalid-host'; //basically 'ret-buffer' with default val //validate possible HTTP_HOST types (all may be followed by port, but that is discarded) if (preg_match('/^\[([0-9a-f:]+)\](?::\d+)?$/i', $rawHost, $m)) { //IPv6 literal including the brackets that are required $ipv6 = $m[1]; if (filter_var($ipv6, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { $host = '[' . $ipv6 . ']'; //re-concat the ip and brackets } } elseif (preg_match('/^([0-9.]+)(?::\d+)?$/', $rawHost, $m)) { //IPv4 literal $ipv4 = $m[1]; if (filter_var($ipv4, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $host = $ipv4; } } elseif (preg_match('/^[a-z0-9._-]+(?::\d+)?$/', $rawHost)) { //anything else, such as hostnames and FQDNs $host = preg_replace('/:\d+$/', '', $rawHost); } $rawPage = (string)filter_input(INPUT_GET, 'page', FILTER_UNSAFE_RAW, [ 'options' => ['default' => '/'], ]); $rawPage = strtok($rawPage, '?');//effectively substring up to first '?' ; see https://www.php.net/manual/en/function.strtok.php for docs // regex-drop anything that's not a basic-ish word char [note: php behaves like sed with delimiters -> # is just the delimiter] $rawPage = preg_replace('#[^a-zA-Z0-9/_\.\-]#', '', $rawPage); $rawPage = substr($rawPage, 0, 128);//safety-truncate the string to 128 char if ($rawPage == '' || $rawPage[0] != '/') {//loose equality to fail easier //if this is empty or some weird BS not starting with /, force it to be so $rawPage = '/' . $rawPage; } $uri = $rawPage; $ip = $_SERVER['REMOTE_ADDR'] ?? ''; if (!filter_var($ip, FILTER_VALIDATE_IP)) { $ip = '0.0.0.0'; } static $pepper = null; if (is_readable($pepperFile) && filesize($pepperFile) === 4096) { $pepper = file_get_contents($pepperFile); } if ($pepper == null || $pepper == false || $pepper == '' || !$pepper) { //NOTE: this uses a loose equality check to fail easier http_response_code(500); echo 'FATAL ERROR: Pepper for hashing IP unavailable/invalid. Disabling statistics.'; exit; } $anonId = hash('sha3-256', $ip . $pepper); $month = date('Y-m'); // e.g., "2025-06" try { // atomic combined insert/update (run update if insert failed) $stmt = $db->prepare('INSERT INTO views (host, webPage, accessMonth, hashedIP, count) VALUES (?, ?, ?, ?, 1) ON CONFLICT(host, webPage, accessMonth, hashedIP) DO UPDATE SET count = count + 1 '); $stmt->execute([$host, $uri, $month, $anonId]); $aggregate1 = $db->prepare('SELECT SUM(count) FROM views WHERE webPage = ? LIMIT 1'); $aggregate1->execute([$uri]); $PageReqs = $aggregate1->fetchColumn() ?: 1; $aggregate2 = $db->prepare('SELECT SUM(count) FROM views WHERE webPage = ? AND hashedIP = ? AND accessMonth = ? LIMIT 1'); $aggregate2->execute([$uri, $anonId, $month]); $UserPageReqs = $aggregate2->fetchColumn() ?: 1; echo 'this page has been served ' . $PageReqs . ' times (' . $UserPageReqs . ' times from the current IP in the current month)'; } catch (Throwable $e) { http_response_code(500); // Do NOT expose internal error details, hence not echoing the message //$e->getMessage() echo 'Error: internal server/database error.'; } ?>