Files
filament-short-url/src/Services/UserAgentParser.php

183 lines
5.4 KiB
PHP

<?php
namespace Bjanczak\FilamentShortUrl\Services;
/**
* Pure-PHP User Agent parser.
*
* No external dependencies. Uses ordered regex matching for accuracy.
* Detects: browsers, browser versions, operating systems, OS versions, device types.
*
* @internal
*/
class UserAgentParser
{
/**
* @return array{
* browser: string|null,
* browser_version: string|null,
* operating_system: string|null,
* operating_system_version: string|null,
* device_type: string,
* }
*/
public function parse(string $userAgent): array
{
return [
'browser' => $this->parseBrowser($userAgent),
'browser_version' => $this->parseBrowserVersion($userAgent),
'operating_system' => $this->parseOs($userAgent),
'operating_system_version' => $this->parseOsVersion($userAgent),
'device_type' => $this->parseDeviceType($userAgent),
];
}
private function parseBrowser(string $ua): ?string
{
// Order matters — check specific first, generic last
$patterns = [
'Edg/' => 'Edge',
'EdgiOS' => 'Edge',
'EdgA' => 'Edge',
'OPR/' => 'Opera',
'Opera' => 'Opera',
'SamsungBrowser' => 'Samsung Browser',
'Chrome' => 'Chrome',
'CriOS' => 'Chrome', // Chrome iOS
'Firefox' => 'Firefox',
'FxiOS' => 'Firefox', // Firefox iOS
'Safari' => 'Safari',
'MSIE' => 'Internet Explorer',
'Trident' => 'Internet Explorer',
'Googlebot' => 'Googlebot',
'bingbot' => 'Bingbot',
'DuckDuckBot' => 'DuckDuckBot',
'Slurp' => 'Yahoo! Slurp',
'curl' => 'cURL',
];
foreach ($patterns as $token => $name) {
if (stripos($ua, $token) !== false) {
return $name;
}
}
return null;
}
private function parseBrowserVersion(string $ua): ?string
{
$patterns = [
'/Edg\/([0-9.]+)/',
'/EdgiOS\/([0-9.]+)/',
'/EdgA\/([0-9.]+)/',
'/OPR\/([0-9.]+)/',
'/SamsungBrowser\/([0-9.]+)/',
'/CriOS\/([0-9.]+)/',
'/Chrome\/([0-9.]+)/',
'/FxiOS\/([0-9.]+)/',
'/Firefox\/([0-9.]+)/',
'/Version\/([0-9.]+).*Safari/',
'/MSIE ([0-9.]+)/',
'/rv:([0-9.]+).*Trident/',
];
foreach ($patterns as $pattern) {
if (preg_match($pattern, $ua, $matches)) {
// Return major.minor only
$parts = explode('.', $matches[1]);
return implode('.', array_slice($parts, 0, 2));
}
}
return null;
}
private function parseOs(string $ua): ?string
{
return match (true) {
stripos($ua, 'Windows') !== false => 'Windows',
stripos($ua, 'iPad') !== false => 'iPadOS',
stripos($ua, 'iPhone') !== false => 'iOS',
stripos($ua, 'Mac OS X') !== false => 'macOS',
stripos($ua, 'Android') !== false => 'Android',
stripos($ua, 'Linux') !== false => 'Linux',
stripos($ua, 'CrOS') !== false => 'Chrome OS',
default => null,
};
}
private function parseOsVersion(string $ua): ?string
{
// Windows: Windows NT 10.0
if (preg_match('/Windows NT ([0-9.]+)/', $ua, $m)) {
return $this->windowsVersion($m[1]);
}
// Android: Android 13
if (preg_match('/Android ([0-9.]+)/', $ua, $m)) {
return $m[1];
}
// iOS / iPadOS: OS 17_0
if (preg_match('/OS ([0-9_]+) like Mac/', $ua, $m)) {
return str_replace('_', '.', $m[1]);
}
// macOS: Mac OS X 10_15_7 or 14.0
if (preg_match('/Mac OS X ([0-9_.]+)/', $ua, $m)) {
return str_replace('_', '.', $m[1]);
}
// Linux — usually no version
if (preg_match('/Linux ([0-9.]+)/', $ua, $m)) {
return $m[1];
}
return null;
}
private function parseDeviceType(string $ua): string
{
// Robots first
$robots = ['Googlebot', 'bingbot', 'Slurp', 'DuckDuckBot', 'Baiduspider', 'YandexBot', 'facebookexternalhit', 'Twitterbot', 'Applebot', 'curl', 'python-requests', 'Go-http-client'];
foreach ($robots as $robot) {
if (stripos($ua, $robot) !== false) {
return 'robot';
}
}
// Tablets before phones — iPad + Android tablets
if (
stripos($ua, 'iPad') !== false ||
(stripos($ua, 'Android') !== false && stripos($ua, 'Mobile') === false)
) {
return 'tablet';
}
// Mobile phones
$mobileKeywords = ['Mobile', 'iPhone', 'iPod', 'BlackBerry', 'IEMobile', 'Opera Mini', 'SamsungBrowser', 'Kindle'];
foreach ($mobileKeywords as $keyword) {
if (stripos($ua, $keyword) !== false) {
return 'mobile';
}
}
return 'desktop';
}
private function windowsVersion(string $ntVersion): string
{
return match ($ntVersion) {
'10.0' => '10/11',
'6.3' => '8.1',
'6.2' => '8',
'6.1' => '7',
'6.0' => 'Vista',
'5.1' => 'XP',
default => $ntVersion,
};
}
}