validate([ 'browser' => ['nullable', 'string', 'max:64'], 'browser_version' => ['nullable', 'string', 'max:32'], 'os' => ['nullable', 'string', 'max:64'], 'os_version' => ['nullable', 'string', 'max:32'], 'screen' => ['nullable', 'string', 'max:32'], 'cpu_cores' => ['nullable', 'integer', 'min:1', 'max:256'], 'device_memory_gb' => ['nullable', 'numeric', 'min:0', 'max:1024'], 'gpu' => ['nullable', 'string', 'max:255'], 'language' => ['nullable', 'string', 'max:32'], 'timezone' => ['nullable', 'string', 'max:64'], 'webrtc_ip' => ['nullable', 'string', 'max:45'], ]); $ua = $request->userAgent() ?? ''; $parsed = $this->parseUserAgent($ua); return array_filter([ 'browser' => $data['browser'] ?? $parsed['browser'], 'browser_version' => $data['browser_version'] ?? $parsed['browser_version'], 'os' => $data['os'] ?? $parsed['os'], 'os_version' => $data['os_version'] ?? $parsed['os_version'], 'screen' => $data['screen'] ?? null, 'cpu_cores' => isset($data['cpu_cores']) ? (int) $data['cpu_cores'] : null, 'device_memory_gb' => isset($data['device_memory_gb']) ? (float) $data['device_memory_gb'] : null, 'gpu' => $data['gpu'] ?? null, 'language' => $data['language'] ?? null, 'timezone' => $data['timezone'] ?? null, 'webrtc_ip' => $data['webrtc_ip'] ?? null, 'user_agent' => mb_substr($ua, 0, 500), ], fn ($value) => $value !== null && $value !== ''); } public function storeInSession(Request $request, array $context): void { $request->session()->put('audit_client_context', $context); } public function fromSession(Request $request): array { $context = $request->session()->get('audit_client_context'); return is_array($context) ? $context : []; } /** * @return array{browser: ?string, browser_version: ?string, os: ?string, os_version: ?string} */ public function parseUserAgent(string $ua): array { $browser = null; $browserVersion = null; $os = null; $osVersion = null; if (preg_match('/Edg\/([\d.]+)/', $ua, $m)) { $browser = 'Edge'; $browserVersion = $m[1]; } elseif (preg_match('/OPR\/([\d.]+)/', $ua, $m) || preg_match('/Opera\/([\d.]+)/', $ua, $m)) { $browser = 'Opera'; $browserVersion = $m[1]; } elseif (preg_match('/Chrome\/([\d.]+)/', $ua, $m) && ! str_contains($ua, 'Edg/')) { $browser = 'Chrome'; $browserVersion = $m[1]; } elseif (preg_match('/Firefox\/([\d.]+)/', $ua, $m)) { $browser = 'Firefox'; $browserVersion = $m[1]; } elseif (preg_match('/Version\/([\d.]+).*Safari/', $ua, $m)) { $browser = 'Safari'; $browserVersion = $m[1]; } if (preg_match('/Windows NT ([\d.]+)/', $ua, $m)) { $os = 'Windows'; $osVersion = match ($m[1]) { '10.0' => '10/11', '6.3' => '8.1', '6.2' => '8', '6.1' => '7', default => $m[1], }; } elseif (preg_match('/Android ([\d.]+)/', $ua, $m)) { $os = 'Android'; $osVersion = $m[1]; } elseif (preg_match('/iPhone OS ([\d_]+)/', $ua, $m) || preg_match('/iPad.*OS ([\d_]+)/', $ua, $m)) { $os = 'iOS'; $osVersion = str_replace('_', '.', $m[1]); } elseif (str_contains($ua, 'Mac OS X')) { $os = 'macOS'; if (preg_match('/Mac OS X ([\d_]+)/', $ua, $m)) { $osVersion = str_replace('_', '.', $m[1]); } } elseif (str_contains($ua, 'Linux')) { $os = 'Linux'; } return [ 'browser' => $browser, 'browser_version' => $browserVersion, 'os' => $os, 'os_version' => $osVersion, ]; } public function summary(array $context): string { $parts = []; if (! empty($context['browser'])) { $parts[] = $context['browser'].(! empty($context['browser_version']) ? ' '.$context['browser_version'] : ''); } if (! empty($context['os'])) { $parts[] = $context['os'].(! empty($context['os_version']) ? ' '.$context['os_version'] : ''); } if (! empty($context['screen'])) { $parts[] = $context['screen']; } return $parts !== [] ? implode(' · ', $parts) : '—'; } }