'datetime', ]; } public static function normalizeDomain(?string $value): ?string { $value = trim((string) $value); if ($value === '') { return null; } $value = preg_replace('#^https?://#i', '', $value) ?? $value; $value = explode('/', $value, 2)[0]; $value = strtolower(rtrim($value)); if ($value === '' || ! str_contains($value, '.')) { return null; } return substr($value, 0, 255); } public const VISITOR_UID_LENGTH = 16; /** * Stable visitor id: same domain+ip always yields the same 16-hex value. */ public static function visitorUid(?string $domain, ?string $ip): string { $domainKey = self::normalizeDomain($domain) ?? strtolower(trim((string) $domain)); $ipKey = self::normalizeIp($ip); return substr(hash('sha256', $domainKey."\0".$ipKey), 0, self::VISITOR_UID_LENGTH); } /** * Unwrap IPv4-mapped IPv6 (`::ffff:1.2.3.4` → `1.2.3.4`) and canonicalize. */ public static function normalizeIp(?string $ip): string { $ip = strtolower(trim((string) $ip)); if ($ip === '') { return ''; } if (str_starts_with($ip, '::ffff:')) { $mapped = substr($ip, 7); if (filter_var($mapped, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { return $mapped; } } $packed = filter_var($ip, FILTER_VALIDATE_IP) ? inet_pton($ip) : false; if ($packed !== false) { $canon = inet_ntop($packed); if (is_string($canon) && $canon !== '') { return strtolower($canon); } } return $ip; } /** * @return list */ public static function ipLookupKeys(?string $ip): array { $norm = self::normalizeIp($ip); if ($norm === '') { return []; } $keys = [$norm]; if (filter_var($norm, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { $keys[] = '::ffff:'.$norm; } return array_values(array_unique($keys)); } }