From b2c1e9d49f854f575ed4bb8b6a69a31a8facbf6f Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Mar 2026 17:19:19 +0100 Subject: [PATCH 1/2] Reduce statistics visits and visitors identifiability --- formwork/src/Statistics/Statistics.php | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/formwork/src/Statistics/Statistics.php b/formwork/src/Statistics/Statistics.php index 52b0a98c5..e2374cddc 100644 --- a/formwork/src/Statistics/Statistics.php +++ b/formwork/src/Statistics/Statistics.php @@ -61,13 +61,15 @@ public function trackVisit(): void return; } + $timestamp = time(); + $date = date(self::DATE_FORMAT, $timestamp); + $ip = IpAnonymizer::anonymize($this->request->ip()); $uri = Str::append(Uri::make(['query' => '', 'fragment' => ''], $this->request->uri()), '/'); + $userAgent = $this->request->userAgent(); // Prefer speed over security for hashing, as it's not a security-critical operation - $hash = hash('xxh3', "{$ip}@{$uri}"); - - $timestamp = time(); + $hash = hash('xxh3', "{$date}{$ip}{$uri}{$userAgent}"); if ( $this->registries['sessions']->has($hash) @@ -79,18 +81,27 @@ public function trackVisit(): void $this->registries['sessions']->set($hash, $timestamp); - $date = date(self::DATE_FORMAT, $timestamp); - $todayVisits = $this->registries['visits']->has($date) ? (int) $this->registries['visits']->get($date) : 0; $this->registries['visits']->set($date, $todayVisits + 1); $todayUniqueVisits = $this->registries['uniqueVisits']->has($date) ? (int) $this->registries['uniqueVisits']->get($date) : 0; - if (!$this->registries['visitors']->has($ip) || $this->registries['visitors']->get($ip) !== $date) { + + $visitor = hash('xxh3', "{$date}{$ip}{$userAgent}"); + + // Remove legacy trackable visitor key based on IP + /** @todo remove this logic in Formwork 3.0.0 */ + if ($this->registries['visitors']->has($ip)) { + $lastVisit = $this->registries['visitors']->get($ip); + $this->registries['visitors']->remove($ip); + $this->registries['visitors']->set($visitor, $lastVisit); + } + + if (!$this->registries['visitors']->has($visitor) || $this->registries['visitors']->get($visitor) !== $date) { $this->registries['uniqueVisits']->set($date, $todayUniqueVisits + 1); $this->registries['uniqueVisits']->save(); } - $this->registries['visitors']->set($ip, $date); + $this->registries['visitors']->set($visitor, $date); $pageViews = $this->registries['pageViews']->has($uri) ? (int) $this->registries['pageViews']->get($uri) : 0; $this->registries['pageViews']->set($uri, $pageViews + 1); From 8bc309fb651176eecdabe3ba5e0e8c0ddc5cc450 Mon Sep 17 00:00:00 2001 From: Giuseppe Criscione <18699708+giuscris@users.noreply.github.com> Date: Sun, 1 Mar 2026 18:13:48 +0100 Subject: [PATCH 2/2] Introduce delimiters to avoid hash collisions --- formwork/src/Statistics/Statistics.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/formwork/src/Statistics/Statistics.php b/formwork/src/Statistics/Statistics.php index e2374cddc..88e75ba97 100644 --- a/formwork/src/Statistics/Statistics.php +++ b/formwork/src/Statistics/Statistics.php @@ -69,7 +69,7 @@ public function trackVisit(): void $userAgent = $this->request->userAgent(); // Prefer speed over security for hashing, as it's not a security-critical operation - $hash = hash('xxh3', "{$date}{$ip}{$uri}{$userAgent}"); + $hash = hash('xxh3', "{$date}|{$ip}|{$uri}|{$userAgent}"); if ( $this->registries['sessions']->has($hash) @@ -86,7 +86,7 @@ public function trackVisit(): void $todayUniqueVisits = $this->registries['uniqueVisits']->has($date) ? (int) $this->registries['uniqueVisits']->get($date) : 0; - $visitor = hash('xxh3', "{$date}{$ip}{$userAgent}"); + $visitor = hash('xxh3', "{$date}|{$ip}|{$userAgent}"); // Remove legacy trackable visitor key based on IP /** @todo remove this logic in Formwork 3.0.0 */