diff --git a/.gitignore b/.gitignore
index 612c601..53915a4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/vendor
+/.phpunit.cache
composer.lock
.phpunit.result.cache
diff --git a/composer.json b/composer.json
index db77872..f90c0ce 100644
--- a/composer.json
+++ b/composer.json
@@ -21,7 +21,8 @@
"illuminate/collections": "^8.0|^9.0|^10.0|^11.0",
"illuminate/http": "^8.0|^9.0|^10.0|^11.0",
"illuminate/view": "^8.0|^9.0|^10.0|^11.0",
- "jenssegers/agent": "^2.6"
+ "mobiledetect/mobiledetectlib": "^2.7.6",
+ "jaybizzle/crawler-detect": "^1.2"
},
"autoload": {
"psr-4": {
diff --git a/phpunit.xml b/phpunit.xml
index 49ef5fe..ac3ceb3 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -3,21 +3,13 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
bootstrap="vendor/autoload.php"
backupGlobals="false"
- backupStaticAttributes="false"
colors="true"
- verbose="true"
- convertErrorsToExceptions="true"
- convertNoticesToExceptions="true"
- convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
- xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.0/phpunit.xsd"
+ cacheDirectory=".phpunit.cache"
+ backupStaticProperties="false"
>
-
-
- src/
-
-
./tests/Unit
@@ -31,4 +23,9 @@
+
diff --git a/src/Agent.php b/src/Agent.php
new file mode 100644
index 0000000..8fe73e9
--- /dev/null
+++ b/src/Agent.php
@@ -0,0 +1,382 @@
+ 'Macintosh',
+ ];
+
+ /** @var array */
+ protected static $additionalOperatingSystems = [
+ 'Windows' => 'Windows',
+ 'Windows NT' => 'Windows NT',
+ 'OS X' => 'Mac OS X',
+ 'Debian' => 'Debian',
+ 'Ubuntu' => 'Ubuntu',
+ 'Macintosh' => 'PPC',
+ 'OpenBSD' => 'OpenBSD',
+ 'Linux' => 'Linux',
+ 'ChromeOS' => 'CrOS',
+ ];
+
+ /** @var array */
+ protected static $additionalBrowsers = [
+ 'Opera Mini' => 'Opera Mini',
+ 'Opera' => 'Opera|OPR',
+ 'Edge' => 'Edge|Edg',
+ 'Coc Coc' => 'coc_coc_browser',
+ 'UCBrowser' => 'UCBrowser',
+ 'Vivaldi' => 'Vivaldi',
+ 'Chrome' => 'Chrome',
+ 'Firefox' => 'Firefox',
+ 'Safari' => 'Safari',
+ 'IE' => 'MSIE|IEMobile|MSIEMobile|Trident/[.0-9]+',
+ 'Netscape' => 'Netscape',
+ 'Mozilla' => 'Mozilla',
+ 'WeChat' => 'MicroMessenger',
+ ];
+
+ /** @var array */
+ protected static $additionalProperties = [
+ // Operating systems
+ 'Windows' => 'Windows NT [VER]',
+ 'Windows NT' => 'Windows NT [VER]',
+ 'OS X' => 'OS X [VER]',
+ 'BlackBerryOS' => ['BlackBerry[\w]+/[VER]', 'BlackBerry.*Version/[VER]', 'Version/[VER]'],
+ 'AndroidOS' => 'Android [VER]',
+ 'ChromeOS' => 'CrOS x86_64 [VER]',
+
+ // Browsers
+ 'Opera Mini' => 'Opera Mini/[VER]',
+ 'Opera' => [' OPR/[VER]', 'Opera Mini/[VER]', 'Version/[VER]', 'Opera [VER]'],
+ 'Netscape' => 'Netscape/[VER]',
+ 'Mozilla' => 'rv:[VER]',
+ 'IE' => ['IEMobile/[VER];', 'IEMobile [VER]', 'MSIE [VER];', 'rv:[VER]'],
+ 'Edge' => ['Edge/[VER]', 'Edg/[VER]'],
+ 'Vivaldi' => 'Vivaldi/[VER]',
+ 'Coc Coc' => 'coc_coc_browser/[VER]',
+ ];
+
+ /** @var CrawlerDetect */
+ protected static $crawlerDetect;
+
+ /** @return array */
+ public static function getDetectionRulesExtended()
+ {
+ static $rules;
+
+ if (! $rules) {
+ $rules = static::mergeRules(
+ static::$desktopDevices, // NEW
+ static::$phoneDevices,
+ static::$tabletDevices,
+ static::$operatingSystems,
+ static::$additionalOperatingSystems, // NEW
+ static::$browsers,
+ static::$additionalBrowsers, // NEW
+ static::$utilities
+ );
+ }
+
+ return $rules;
+ }
+
+ public function getRules()
+ {
+ if ($this->detectionType === static::DETECTION_TYPE_EXTENDED) {
+ return static::getDetectionRulesExtended();
+ }
+
+ return static::getMobileDetectionRules();
+ }
+
+ /** @return CrawlerDetect */
+ public function getCrawlerDetect()
+ {
+ if (static::$crawlerDetect === null) {
+ static::$crawlerDetect = new CrawlerDetect();
+ }
+
+ return static::$crawlerDetect;
+ }
+
+ public static function getBrowsers()
+ {
+ return static::mergeRules(
+ static::$additionalBrowsers,
+ static::$browsers
+ );
+ }
+
+ public static function getOperatingSystems()
+ {
+ return static::mergeRules(
+ static::$operatingSystems,
+ static::$additionalOperatingSystems
+ );
+ }
+
+ public static function getPlatforms()
+ {
+ return static::mergeRules(
+ static::$operatingSystems,
+ static::$additionalOperatingSystems
+ );
+ }
+
+ public static function getDesktopDevices()
+ {
+ return static::$desktopDevices;
+ }
+
+ public static function getProperties()
+ {
+ return static::mergeRules(
+ static::$additionalProperties,
+ static::$properties
+ );
+ }
+
+ /**
+ * @param string $acceptLanguage
+ * @return array
+ */
+ public function languages($acceptLanguage = null)
+ {
+ if ($acceptLanguage === null) {
+ $acceptLanguage = $this->getHttpHeader('HTTP_ACCEPT_LANGUAGE');
+ }
+
+ if (! $acceptLanguage) {
+ return [];
+ }
+
+ $languages = [];
+
+ // Parse accept language string.
+ foreach (explode(',', $acceptLanguage) as $piece) {
+ $parts = explode(';', $piece);
+ $language = strtolower($parts[0]);
+ $priority = empty($parts[1]) ? 1. : floatval(str_replace('q=', '', $parts[1]));
+
+ $languages[$language] = $priority;
+ }
+
+ // Sort languages by priority.
+ arsort($languages);
+
+ return array_keys($languages);
+ }
+
+ /**
+ * @param array $rules
+ * @param string|null $userAgent
+ * @return string|bool
+ */
+ protected function findDetectionRulesAgainstUA(array $rules, $userAgent = null)
+ {
+ // Loop given rules
+ foreach ($rules as $key => $regex) {
+ if (empty($regex)) {
+ continue;
+ }
+
+ // Check match
+ if ($this->match($regex, $userAgent)) {
+ return $key ?: reset($this->matchesArray);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string|null $userAgent
+ * @return string|bool
+ */
+ public function browser($userAgent = null)
+ {
+ return $this->findDetectionRulesAgainstUA(static::getBrowsers(), $userAgent);
+ }
+
+ /**
+ * @param string|null $userAgent
+ * @return string|bool
+ */
+ public function platform($userAgent = null)
+ {
+ return $this->findDetectionRulesAgainstUA(static::getPlatforms(), $userAgent);
+ }
+
+ /**
+ * @param string|null $userAgent
+ * @return string|bool
+ */
+ public function device($userAgent = null)
+ {
+ $rules = static::mergeRules(
+ static::getDesktopDevices(),
+ static::getPhoneDevices(),
+ static::getTabletDevices(),
+ static::getUtilities()
+ );
+
+ return $this->findDetectionRulesAgainstUA($rules, $userAgent);
+ }
+
+ /**
+ * @param string|null $userAgent deprecated
+ * @param array $httpHeaders deprecated
+ * @return bool
+ */
+ public function isDesktop($userAgent = null, $httpHeaders = null)
+ {
+ // Check specifically for cloudfront headers if the useragent === 'Amazon CloudFront'
+ if ($this->getUserAgent() === 'Amazon CloudFront') {
+ $cfHeaders = $this->getCfHeaders();
+
+ if (array_key_exists('HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER', $cfHeaders)) {
+ return $cfHeaders['HTTP_CLOUDFRONT_IS_DESKTOP_VIEWER'] === 'true';
+ }
+ }
+
+ return ! $this->isMobile($userAgent, $httpHeaders) && ! $this->isTablet($userAgent, $httpHeaders) && ! $this->isRobot($userAgent);
+ }
+
+ /**
+ * @param string|null $userAgent deprecated
+ * @param array $httpHeaders deprecated
+ * @return bool
+ */
+ public function isPhone($userAgent = null, $httpHeaders = null)
+ {
+ return $this->isMobile($userAgent, $httpHeaders) && ! $this->isTablet($userAgent, $httpHeaders);
+ }
+
+ /**
+ * @param string|null $userAgent
+ * @return string|bool
+ */
+ public function robot($userAgent = null)
+ {
+ if ($this->getCrawlerDetect()->isCrawler($userAgent ?: $this->userAgent)) {
+ return ucfirst($this->getCrawlerDetect()->getMatches());
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string|null $userAgent
+ * @return bool
+ */
+ public function isRobot($userAgent = null)
+ {
+ return $this->getCrawlerDetect()->isCrawler($userAgent ?: $this->userAgent);
+ }
+
+ /**
+ * @param null $userAgent
+ * @param null $httpHeaders
+ * @return string
+ */
+ public function deviceType($userAgent = null, $httpHeaders = null)
+ {
+ if ($this->isDesktop($userAgent, $httpHeaders)) {
+ return 'desktop';
+ } elseif ($this->isPhone($userAgent, $httpHeaders)) {
+ return 'phone';
+ } elseif ($this->isTablet($userAgent, $httpHeaders)) {
+ return 'tablet';
+ } elseif ($this->isRobot($userAgent)) {
+ return 'robot';
+ }
+
+ return 'other';
+ }
+
+ public function version($propertyName, $type = self::VERSION_TYPE_STRING)
+ {
+ if (empty($propertyName)) {
+ return false;
+ }
+
+ // set the $type to the default if we don't recognize the type
+ if ($type !== self::VERSION_TYPE_STRING && $type !== self::VERSION_TYPE_FLOAT) {
+ $type = self::VERSION_TYPE_STRING;
+ }
+
+ $properties = self::getProperties();
+
+ // Check if the property exists in the properties array.
+ if (true === isset($properties[$propertyName])) {
+ // Prepare the pattern to be matched.
+ // Make sure we always deal with an array (string is converted).
+ $properties[$propertyName] = (array) $properties[$propertyName];
+
+ foreach ($properties[$propertyName] as $propertyMatchString) {
+ if (is_array($propertyMatchString)) {
+ $propertyMatchString = implode('|', $propertyMatchString);
+ }
+
+ $propertyPattern = str_replace('[VER]', self::VER, $propertyMatchString);
+
+ // Identify and extract the version.
+ preg_match(sprintf('#%s#is', $propertyPattern), $this->userAgent, $match);
+
+ if (false === empty($match[1])) {
+ $version = ($type === self::VERSION_TYPE_FLOAT ? $this->prepareVersionNo($match[1]) : $match[1]);
+
+ return $version;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param array $all
+ * @return array
+ */
+ protected static function mergeRules(...$all)
+ {
+ $merged = [];
+
+ foreach ($all as $rules) {
+ foreach ($rules as $key => $value) {
+ if (empty($merged[$key])) {
+ $merged[$key] = $value;
+ } elseif (is_array($merged[$key])) {
+ $merged[$key][] = $value;
+ } else {
+ $merged[$key] .= '|'.$value;
+ }
+ }
+ }
+
+ return $merged;
+ }
+
+ /** @inheritdoc */
+ public function __call($name, $arguments)
+ {
+ // Make sure the name starts with 'is', otherwise
+ if (strpos($name, 'is') !== 0) {
+ throw new BadMethodCallException("No such method exists: $name");
+ }
+
+ $this->setDetectionType(self::DETECTION_TYPE_EXTENDED);
+
+ $key = substr($name, 2);
+
+ return $this->matchUAAgainstKey($key);
+ }
+}
diff --git a/src/AnalyticsServiceProvider.php b/src/AnalyticsServiceProvider.php
index 586f1d2..0ccdb25 100644
--- a/src/AnalyticsServiceProvider.php
+++ b/src/AnalyticsServiceProvider.php
@@ -8,11 +8,6 @@
class AnalyticsServiceProvider extends ServiceProvider
{
- /**
- * Bootstrap any package services.
- *
- * @return void
- */
public function boot(): void
{
if ($this->app->runningInConsole()) {
@@ -51,11 +46,6 @@ protected function routeConfig(): array
];
}
- /**
- * Register any application services.
- *
- * @return void
- */
public function register(): void
{
$this->mergeConfigFrom(
diff --git a/src/Http/Middleware/Analytics.php b/src/Http/Middleware/Analytics.php
index 92af0d3..8666052 100644
--- a/src/Http/Middleware/Analytics.php
+++ b/src/Http/Middleware/Analytics.php
@@ -2,12 +2,12 @@
namespace AndreasElia\Analytics\Http\Middleware;
+use AndreasElia\Analytics\Agent;
use AndreasElia\Analytics\Contracts\SessionProvider;
use AndreasElia\Analytics\Models\PageView;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\App;
-use Jenssegers\Agent\Agent;
class Analytics
{
diff --git a/tests/Feature/AnalyticsTest.php b/tests/Feature/AnalyticsTest.php
index fcd8794..6afc313 100644
--- a/tests/Feature/AnalyticsTest.php
+++ b/tests/Feature/AnalyticsTest.php
@@ -9,6 +9,7 @@
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
+use PHPUnit\Framework\Attributes\Test;
class AnalyticsTest extends TestCase
{
@@ -21,7 +22,7 @@ protected function getEnvironmentSetUp($app): void
]);
}
- /** @test */
+ #[Test]
public function a_page_view_can_be_tracked()
{
$request = Request::create('/test', 'GET');
@@ -39,7 +40,7 @@ public function a_page_view_can_be_tracked()
]);
}
- /** @test */
+ #[Test]
public function page_views_arent_tracked_when_not_enabled()
{
Config::set('analytics.enabled', false);
@@ -54,7 +55,7 @@ public function page_views_arent_tracked_when_not_enabled()
]);
}
- /** @test */
+ #[Test]
public function a_page_view_can_be_masked()
{
$request = Request::create('/test/123', 'GET');
@@ -72,7 +73,7 @@ public function a_page_view_can_be_masked()
]);
}
- /** @test */
+ #[Test]
public function a_page_view_can_be_excluded()
{
$request = Request::create('/analytics/123', 'GET');
@@ -86,7 +87,7 @@ public function a_page_view_can_be_excluded()
]);
}
- /** @test */
+ #[Test]
public function methods_can_be_excluded()
{
Config::set('analytics.ignoreMethods', ['POST']);
@@ -101,7 +102,7 @@ public function methods_can_be_excluded()
]);
}
- /** @test */
+ #[Test]
public function a_page_view_from_robot_can_be_tracked_if_enabled()
{
Config::set('analytics.ignoreRobots', false);
@@ -122,7 +123,7 @@ public function a_page_view_from_robot_can_be_tracked_if_enabled()
]);
}
- /** @test */
+ #[Test]
public function a_page_view_from_robot_is_not_tracked_if_enabled()
{
Config::set('analytics.ignoreRobots', true);
@@ -142,7 +143,7 @@ public function a_page_view_from_robot_is_not_tracked_if_enabled()
]);
}
- /** @test */
+ #[Test]
public function a_page_view_from_an_excluded_ip_is_not_tracked_if_enabled()
{
Config::set('analytics.ignoredIPs', ['127.0.0.2']);
@@ -161,7 +162,7 @@ public function a_page_view_from_an_excluded_ip_is_not_tracked_if_enabled()
]);
}
- /** @test */
+ #[Test]
public function utm_details_can_be_saved_with_page_views()
{
$request = Request::create('/test', 'GET', [
@@ -190,7 +191,7 @@ public function utm_details_can_be_saved_with_page_views()
]);
}
- /** @test */
+ #[Test]
public function utm_details_will_be_trimmed()
{
$string = Str::random(300);
diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php
index 4f08d42..b21ae11 100644
--- a/tests/Feature/DashboardTest.php
+++ b/tests/Feature/DashboardTest.php
@@ -5,6 +5,7 @@
use AndreasElia\Analytics\Database\Factories\PageViewFactory;
use AndreasElia\Analytics\Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use PHPUnit\Framework\Attributes\Test;
class DashboardTest extends TestCase
{
@@ -36,7 +37,7 @@ public function setUp(): void
});
}
- /** @test */
+ #[Test]
public function it_can_get_data_from_today()
{
$this->get('analytics')
@@ -53,7 +54,7 @@ public function it_can_get_data_from_today()
]);
}
- /** @test */
+ #[Test]
public function it_can_get_data_from_yesterday()
{
$this->get(route('analytics', ['period' => 'yesterday']))
@@ -70,7 +71,7 @@ public function it_can_get_data_from_yesterday()
]);
}
- /** @test */
+ #[Test]
public function it_can_get_data_for_1_week()
{
$this->get(route('analytics', ['period' => '1_week']))
@@ -87,7 +88,7 @@ public function it_can_get_data_for_1_week()
]);
}
- /** @test */
+ #[Test]
public function it_can_get_data_for_30_days()
{
$this->get(route('analytics', ['period' => '30_days']))
@@ -104,7 +105,7 @@ public function it_can_get_data_for_30_days()
]);
}
- /** @test */
+ #[Test]
public function it_can_get_data_for_30_days_filtered_by_uri()
{
$this->get(route('analytics', [
@@ -125,7 +126,7 @@ public function it_can_get_data_for_30_days_filtered_by_uri()
]);
}
- /** @test */
+ #[Test]
public function it_can_view_sources()
{
$this->get(route('analytics', [
diff --git a/tests/Feature/TimezoneTest.php b/tests/Feature/TimezoneTest.php
index 4bff5e6..4d19d37 100644
--- a/tests/Feature/TimezoneTest.php
+++ b/tests/Feature/TimezoneTest.php
@@ -7,6 +7,7 @@
use AndreasElia\Analytics\Tests\TestCase;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use PHPUnit\Framework\Attributes\Test;
class TimezoneTest extends TestCase
{
@@ -38,7 +39,7 @@ public function setUp(): void
PageView::resolveTimezoneUsing(fn () => 'America/Los_Angeles');
}
- /** @test */
+ #[Test]
public function it_can_resolve_timezone()
{
$pageView = new PageView();
@@ -49,7 +50,7 @@ public function it_can_resolve_timezone()
$this->assertEquals(config('app.timezone'), $pageView->getTimezone());
}
- /** @test */
+ #[Test]
public function it_can_get_data_from_today()
{
$views = PageView::query()
@@ -59,7 +60,7 @@ public function it_can_get_data_from_today()
$this->assertEquals(2, $views);
}
- /** @test */
+ #[Test]
public function it_can_get_data_from_yesterday()
{
$views = PageView::query()
diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/Unit/AgentTest.php b/tests/Unit/AgentTest.php
new file mode 100644
index 0000000..e699654
--- /dev/null
+++ b/tests/Unit/AgentTest.php
@@ -0,0 +1,296 @@
+ 'Windows',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2' => 'OS X',
+ 'Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3' => 'iOS',
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0' => 'Ubuntu',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+' => 'BlackBerryOS',
+ 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' => 'AndroidOS',
+ 'Mozilla/5.0 (X11; CrOS x86_64 6680.78.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.102 Safari/537.36' => 'ChromeOS',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36' => 'Windows',
+ ];
+
+ private $browsers = [
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' => 'IE',
+ 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' => 'Safari',
+ 'Mozilla/5.0 (Windows; U; Win 9x 4.90; SG; rv:1.9.2.4) Gecko/20101104 Netscape/9.1.0285' => 'Netscape',
+ 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0' => 'Firefox',
+ 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36' => 'Chrome',
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201' => 'Mozilla',
+ 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14' => 'Opera',
+ 'Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.115 Safari/537.36 OPR/27.0.1689.76' => 'Opera',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12' => 'Edge',
+ 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5376e Safari/8536.25' => 'Safari',
+ 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 Vivaldi/1.2.490.43' => 'Vivaldi',
+ 'Mozilla/5.0 (Linux; U; Android 4.0.4; en-US; LT28h Build/6.1.E.3.7) AppleWebKit/534.31 (KHTML, like Gecko) UCBrowser/9.2.2.323 U3/0.8.0 Mobile Safari/534.31' => 'UCBrowser',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36 Edge/15.15063' => 'Edge',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.29 Safari/537.36 Edg/79.0.309.18' => 'Edge',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/86.0.180 Chrome/80.0.3987.180 Safari/537.36' => 'Coc Coc',
+ ];
+
+ private $robots = [
+ 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)' => 'Googlebot',
+ 'facebookexternalhit/1.1 (+http(s)://www.facebook.com/externalhit_uatext.php)' => 'Facebookexternalhit',
+ 'Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)' => 'Bingbot',
+ 'Twitterbot/1.0' => 'Twitterbot',
+ 'Mozilla/5.0 (compatible; YandexBot/3.0; +http://yandex.com/bots)' => 'Yandex',
+ ];
+
+ private $mobileDevices = [
+ 'Mozilla/5.0 (iPhone; U; ru; CPU iPhone OS 4_2_1 like Mac OS X; ru) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5' => 'iPhone',
+ 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' => 'iPad',
+ 'Mozilla/5.0 (Linux; U; Android 2.3.4; fr-fr; HTC Desire Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' => 'HTC',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+' => 'BlackBerry',
+ 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' => 'Nexus',
+ 'Mozilla/5.0 (Linux; U; Android 4.0.3; en-us; ASUS Transformer Pad TF300T Build/IML74K) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30' => 'AsusTablet',
+ ];
+
+ private $desktopDevices = [
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11) AppleWebKit/601.1.56 (KHTML, like Gecko) Version/9.0 Safari/601.1.56' => 'Macintosh',
+ ];
+
+ private $browserVersions = [
+ 'Mozilla/5.0 (compatible; MSIE 10.6; Windows NT 6.1; Trident/5.0; InfoPath.2; SLCC1; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729; .NET CLR 2.0.50727) 3gpp-gba UNTRUSTED/1.0' => '10.6',
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' => '11.0',
+ 'Mozilla/5.0 (iPad; CPU OS 6_0 like Mac OS X) AppleWebKit/536.26 (KHTML, like Gecko) Version/6.0 Mobile/10A5355d Safari/8536.25' => '6.0',
+ 'Mozilla/5.0 (Windows; U; Win 9x 4.90; SG; rv:1.9.2.4) Gecko/20101104 Netscape/9.1.0285' => '9.1.0285',
+ 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0' => '25.0',
+ 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36' => '32.0.1667.0',
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201' => '2.2',
+ 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14' => '12.14',
+ 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; de) Opera 11.51' => '11.51',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12' => '12',
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.29 Safari/537.36 Edg/79.0.309.18' => '79.0.309.18',
+ 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36 Vivaldi/1.2.490.43' => '1.2.490.43',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) coc_coc_browser/86.0.180 Chrome/80.0.3987.180 Safari/537.36' => '86.0.180',
+ ];
+
+ private $operatingSystemVersions = [
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko' => '6.3',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2' => '10_6_8',
+ 'Mozilla/5.0 (iPad; CPU OS 5_1 like Mac OS X) AppleWebKit/534.46 (KHTML, like Gecko ) Version/5.1 Mobile/9B176 Safari/7534.48.3' => '5_1',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+' => '7.1.0.346',
+ 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1' => '2.2',
+ 'Mozilla/5.0 (X11; CrOS x86_64 6680.78.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2272.102 Safari/537.36' => '6680.78.0',
+ ];
+
+ private $desktops = [
+ 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko',
+ 'Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0',
+ 'Mozilla/5.0 (Windows; U; Win 9x 4.90; SG; rv:1.9.2.4) Gecko/20101104 Netscape/9.1.0285',
+ 'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:25.0) Gecko/20100101 Firefox/25.0',
+ 'Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/32.0.1667.0 Safari/537.36',
+ 'Mozilla/5.0 (Windows; U; Windows NT 6.1; rv:2.2) Gecko/20110201',
+ 'Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14',
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/537.13+ (KHTML, like Gecko) Version/5.1.7 Safari/534.57.2',
+ ];
+
+ private $phones = [
+ 'Mozilla/5.0 (iPhone; U; ru; CPU iPhone OS 4_2_1 like Mac OS X; ru) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148a Safari/6533.18.5',
+ 'Mozilla/5.0 (Linux; U; Android 2.3.4; fr-fr; HTC Desire Build/GRJ22) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
+ 'Mozilla/5.0 (BlackBerry; U; BlackBerry 9900; en) AppleWebKit/534.11+ (KHTML, like Gecko) Version/7.1.0.346 Mobile Safari/534.11+',
+ 'Mozilla/5.0 (Linux; U; Android 2.2; en-us; Nexus One Build/FRF91) AppleWebKit/533.1 (KHTML, like Gecko) Version/4.0 Mobile Safari/533.1',
+ ];
+
+ #[Test]
+ public function languages()
+ {
+ $agent = new Agent();
+ $agent->setHttpHeaders([
+ 'HTTP_ACCEPT_LANGUAGE' => 'nl-NL,nl;q=0.8,en-US;q=0.6,en;q=0.4',
+ ]);
+
+ $this->assertEquals(['nl-nl', 'nl', 'en-us', 'en'], $agent->languages());
+ }
+
+ #[Test]
+ public function languages_sorted()
+ {
+ $agent = new Agent();
+ $agent->setHttpHeaders([
+ 'HTTP_ACCEPT_LANGUAGE' => 'en;q=0.4,en-US,nl;q=0.6',
+ ]);
+
+ $this->assertEquals(['en-us', 'nl', 'en'], $agent->languages());
+ }
+
+ #[Test]
+ public function operating_systems()
+ {
+ $agent = new Agent();
+
+ foreach ($this->operatingSystems as $ua => $platform) {
+ $agent->setUserAgent($ua);
+ $this->assertEquals($platform, $agent->platform(), $ua);
+ $this->assertTrue($agent->is($platform), $platform);
+
+ if (! strpos($platform, ' ')) {
+ $method = "is{$platform}";
+ $this->assertTrue($agent->{$method}(), $ua);
+ }
+ }
+ }
+
+ #[Test]
+ public function browsers()
+ {
+ $agent = new Agent();
+
+ foreach ($this->browsers as $ua => $browser) {
+ $agent->setUserAgent($ua);
+ $this->assertEquals($browser, $agent->browser(), $ua);
+ $this->assertTrue($agent->is($browser), $browser);
+
+ if (! strpos($browser, ' ')) {
+ $method = "is{$browser}";
+ $this->assertTrue($agent->{$method}(), $ua);
+ }
+ }
+ }
+
+ #[Test]
+ public function robots()
+ {
+ $agent = new Agent();
+
+ foreach ($this->robots as $ua => $robot) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isRobot(), $ua);
+ $this->assertEquals($robot, $agent->robot());
+ }
+ }
+
+ #[Test]
+ public function robot_should_return_false()
+ {
+ $agent = new Agent();
+
+ $this->assertFalse($agent->robot());
+ }
+
+ #[Test]
+ public function call_should_throw_bad_method_call_exception()
+ {
+ $this->expectException(\BadMethodCallException::class);
+
+ $agent = new Agent();
+ $agent->invalidMethod();
+ }
+
+ #[Test]
+ public function mobile_devices()
+ {
+ $agent = new Agent();
+
+ foreach ($this->mobileDevices as $ua => $device) {
+ $agent->setUserAgent($ua);
+ $this->assertEquals($device, $agent->device(), $ua);
+ $this->assertTrue($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isDesktop(), $ua);
+
+ if (! strpos($device, ' ')) {
+ $method = "is{$device}";
+ $this->assertTrue($agent->{$method}(), $ua, $method);
+ }
+ }
+ }
+
+ #[Test]
+ public function desktop_devices()
+ {
+ $agent = new Agent();
+
+ foreach ($this->desktopDevices as $ua => $device) {
+ $agent->setUserAgent($ua);
+ $this->assertEquals($device, $agent->device(), $ua);
+ $this->assertFalse($agent->isMobile(), $ua);
+ $this->assertTrue($agent->isDesktop(), $ua);
+
+ if (! strpos($device, ' ')) {
+ $method = "is{$device}";
+ $this->assertTrue($agent->{$method}(), $ua);
+ }
+ }
+ }
+
+ #[Test]
+ public function versions()
+ {
+ $agent = new Agent();
+
+ foreach ($this->browserVersions as $ua => $version) {
+ $agent->setUserAgent($ua);
+ $browser = $agent->browser();
+ $this->assertEquals($version, $agent->version($browser), $ua);
+ }
+
+ foreach ($this->operatingSystemVersions as $ua => $version) {
+ $agent->setUserAgent($ua);
+ $platform = $agent->platform();
+ $this->assertEquals($version, $agent->version($platform), $ua);
+ }
+
+ foreach ($this->browsers as $ua => $browser) {
+ $agent->setUserAgent('FAKE');
+ $this->assertFalse($agent->version($browser));
+ }
+ }
+
+ #[Test]
+ public function is_methods()
+ {
+ $agent = new Agent();
+
+ foreach ($this->desktops as $ua) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isDesktop(), $ua);
+ $this->assertFalse($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isTablet(), $ua);
+ $this->assertFalse($agent->isPhone(), $ua);
+ $this->assertFalse($agent->isRobot(), $ua);
+ }
+
+ foreach ($this->phones as $ua) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isPhone(), $ua);
+ $this->assertTrue($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isDesktop(), $ua);
+ $this->assertFalse($agent->isTablet(), $ua);
+ $this->assertFalse($agent->isRobot(), $ua);
+ }
+
+ foreach ($this->robots as $ua => $robot) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isRobot(), $ua);
+ $this->assertFalse($agent->isDesktop(), $ua);
+ $this->assertFalse($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isTablet(), $ua);
+ $this->assertFalse($agent->isPhone(), $ua);
+ }
+
+ foreach ($this->mobileDevices as $ua => $device) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isDesktop(), $ua);
+ $this->assertFalse($agent->isRobot(), $ua);
+ }
+
+ foreach ($this->desktopDevices as $ua => $device) {
+ $agent->setUserAgent($ua);
+ $this->assertTrue($agent->isDesktop(), $ua);
+ $this->assertFalse($agent->isMobile(), $ua);
+ $this->assertFalse($agent->isTablet(), $ua);
+ $this->assertFalse($agent->isPhone(), $ua);
+ $this->assertFalse($agent->isRobot(), $ua);
+ }
+ }
+}