1<?php 2 3/** 4 * Device Detector - The Universal Device Detection library for parsing User Agents 5 * 6 * @link https://matomo.org 7 * 8 * @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later 9 */ 10 11declare(strict_types=1); 12 13namespace DeviceDetector; 14 15use DeviceDetector\Cache\CacheInterface; 16use DeviceDetector\Cache\StaticCache; 17use DeviceDetector\Parser\AbstractBotParser; 18use DeviceDetector\Parser\Bot; 19use DeviceDetector\Parser\Client\AbstractClientParser; 20use DeviceDetector\Parser\Client\Browser; 21use DeviceDetector\Parser\Client\FeedReader; 22use DeviceDetector\Parser\Client\Library; 23use DeviceDetector\Parser\Client\MediaPlayer; 24use DeviceDetector\Parser\Client\MobileApp; 25use DeviceDetector\Parser\Client\PIM; 26use DeviceDetector\Parser\Device\AbstractDeviceParser; 27use DeviceDetector\Parser\Device\Camera; 28use DeviceDetector\Parser\Device\CarBrowser; 29use DeviceDetector\Parser\Device\Console; 30use DeviceDetector\Parser\Device\HbbTv; 31use DeviceDetector\Parser\Device\Mobile; 32use DeviceDetector\Parser\Device\Notebook; 33use DeviceDetector\Parser\Device\PortableMediaPlayer; 34use DeviceDetector\Parser\Device\ShellTv; 35use DeviceDetector\Parser\OperatingSystem; 36use DeviceDetector\Parser\VendorFragment; 37use DeviceDetector\Yaml\ParserInterface as YamlParser; 38use DeviceDetector\Yaml\Spyc; 39 40/** 41 * Class DeviceDetector 42 * 43 * Magic Device Type Methods: 44 * @method bool isSmartphone() 45 * @method bool isFeaturePhone() 46 * @method bool isTablet() 47 * @method bool isPhablet() 48 * @method bool isConsole() 49 * @method bool isPortableMediaPlayer() 50 * @method bool isCarBrowser() 51 * @method bool isTV() 52 * @method bool isSmartDisplay() 53 * @method bool isSmartSpeaker() 54 * @method bool isCamera() 55 * @method bool isWearable() 56 * @method bool isPeripheral() 57 * 58 * Magic Client Type Methods: 59 * @method bool isBrowser() 60 * @method bool isFeedReader() 61 * @method bool isMobileApp() 62 * @method bool isPIM() 63 * @method bool isLibrary() 64 * @method bool isMediaPlayer() 65 */ 66class DeviceDetector 67{ 68 /** 69 * Current version number of DeviceDetector 70 */ 71 public const VERSION = '6.4.6'; 72 73 /** 74 * Constant used as value for unknown browser / os 75 */ 76 public const UNKNOWN = 'UNK'; 77 78 /** 79 * Holds all registered client types 80 * @var array 81 */ 82 protected $clientTypes = []; 83 84 /** 85 * Holds the useragent that should be parsed 86 * @var string 87 */ 88 protected $userAgent = ''; 89 90 /** 91 * Holds the client hints that should be parsed 92 * @var ?ClientHints 93 */ 94 protected $clientHints = null; 95 96 /** 97 * Holds the operating system data after parsing the UA 98 * @var ?array 99 */ 100 protected $os = null; 101 102 /** 103 * Holds the client data after parsing the UA 104 * @var ?array 105 */ 106 protected $client = null; 107 108 /** 109 * Holds the device type after parsing the UA 110 * @var ?int 111 */ 112 protected $device = null; 113 114 /** 115 * Holds the device brand data after parsing the UA 116 * @var string 117 */ 118 protected $brand = ''; 119 120 /** 121 * Holds the device model data after parsing the UA 122 * @var string 123 */ 124 protected $model = ''; 125 126 /** 127 * Holds bot information if parsing the UA results in a bot 128 * (All other information attributes will stay empty in that case) 129 * 130 * If $discardBotInformation is set to true, this property will be set to 131 * true if parsed UA is identified as bot, additional information will be not available 132 * 133 * If $skipBotDetection is set to true, bot detection will not be performed and isBot will 134 * always be false 135 * 136 * @var array|bool|null 137 */ 138 protected $bot = null; 139 140 /** 141 * @var bool 142 */ 143 protected $discardBotInformation = false; 144 145 /** 146 * @var bool 147 */ 148 protected $skipBotDetection = false; 149 150 /** 151 * Holds the cache class used for caching the parsed yml-Files 152 * @var CacheInterface|null 153 */ 154 protected $cache = null; 155 156 /** 157 * Holds the parser class used for parsing yml-Files 158 * @var YamlParser|null 159 */ 160 protected $yamlParser = null; 161 162 /** 163 * @var array<AbstractClientParser> 164 */ 165 protected $clientParsers = []; 166 167 /** 168 * @var array<AbstractDeviceParser> 169 */ 170 protected $deviceParsers = []; 171 172 /** 173 * @var array<AbstractBotParser> 174 */ 175 public $botParsers = []; 176 177 /** 178 * @var bool 179 */ 180 private $parsed = false; 181 182 /** 183 * Constructor 184 * 185 * @param string $userAgent UA to parse 186 * @param ClientHints $clientHints Browser client hints to parse 187 */ 188 public function __construct(string $userAgent = '', ?ClientHints $clientHints = null) 189 { 190 if ('' !== $userAgent) { 191 $this->setUserAgent($userAgent); 192 } 193 194 if ($clientHints instanceof ClientHints) { 195 $this->setClientHints($clientHints); 196 } 197 198 $this->addClientParser(new FeedReader()); 199 $this->addClientParser(new MobileApp()); 200 $this->addClientParser(new MediaPlayer()); 201 $this->addClientParser(new PIM()); 202 $this->addClientParser(new Browser()); 203 $this->addClientParser(new Library()); 204 205 $this->addDeviceParser(new HbbTv()); 206 $this->addDeviceParser(new ShellTv()); 207 $this->addDeviceParser(new Notebook()); 208 $this->addDeviceParser(new Console()); 209 $this->addDeviceParser(new CarBrowser()); 210 $this->addDeviceParser(new Camera()); 211 $this->addDeviceParser(new PortableMediaPlayer()); 212 $this->addDeviceParser(new Mobile()); 213 214 $this->addBotParser(new Bot()); 215 } 216 217 /** 218 * @param string $methodName 219 * @param array $arguments 220 * 221 * @return bool 222 */ 223 public function __call(string $methodName, array $arguments): bool 224 { 225 foreach (AbstractDeviceParser::getAvailableDeviceTypes() as $deviceName => $deviceType) { 226 if (\strtolower($methodName) === 'is' . \strtolower(\str_replace(' ', '', $deviceName))) { 227 return $this->getDevice() === $deviceType; 228 } 229 } 230 231 foreach ($this->clientTypes as $client) { 232 if (\strtolower($methodName) === 'is' . \strtolower(\str_replace(' ', '', $client))) { 233 return $this->getClient('type') === $client; 234 } 235 } 236 237 throw new \BadMethodCallException("Method {$methodName} not found"); 238 } 239 240 /** 241 * Sets the useragent to be parsed 242 * 243 * @param string $userAgent 244 */ 245 public function setUserAgent(string $userAgent): void 246 { 247 if ($this->userAgent !== $userAgent) { 248 $this->reset(); 249 } 250 251 $this->userAgent = $userAgent; 252 } 253 254 /** 255 * Sets the browser client hints to be parsed 256 * 257 * @param ?ClientHints $clientHints 258 */ 259 public function setClientHints(?ClientHints $clientHints = null): void 260 { 261 if ($this->clientHints !== $clientHints) { 262 $this->reset(); 263 } 264 265 $this->clientHints = $clientHints; 266 } 267 268 /** 269 * @param AbstractClientParser $parser 270 * 271 * @throws \Exception 272 */ 273 public function addClientParser(AbstractClientParser $parser): void 274 { 275 $this->clientParsers[] = $parser; 276 $this->clientTypes[] = $parser->getName(); 277 } 278 279 /** 280 * @return array<AbstractClientParser> 281 */ 282 public function getClientParsers(): array 283 { 284 return $this->clientParsers; 285 } 286 287 /** 288 * @param AbstractDeviceParser $parser 289 * 290 * @throws \Exception 291 */ 292 public function addDeviceParser(AbstractDeviceParser $parser): void 293 { 294 $this->deviceParsers[] = $parser; 295 } 296 297 /** 298 * @return array<AbstractDeviceParser> 299 */ 300 public function getDeviceParsers(): array 301 { 302 return $this->deviceParsers; 303 } 304 305 /** 306 * @param AbstractBotParser $parser 307 */ 308 public function addBotParser(AbstractBotParser $parser): void 309 { 310 $this->botParsers[] = $parser; 311 } 312 313 /** 314 * @return array<AbstractBotParser> 315 */ 316 public function getBotParsers(): array 317 { 318 return $this->botParsers; 319 } 320 321 /** 322 * Sets whether to discard additional bot information 323 * If information is discarded it's only possible check whether UA was detected as bot or not. 324 * (Discarding information speeds up the detection a bit) 325 * 326 * @param bool $discard 327 */ 328 public function discardBotInformation(bool $discard = true): void 329 { 330 $this->discardBotInformation = $discard; 331 } 332 333 /** 334 * Sets whether to skip bot detection. 335 * It is needed if we want bots to be processed as a simple clients. So we can detect if it is mobile client, 336 * or desktop, or enything else. By default all this information is not retrieved for the bots. 337 * 338 * @param bool $skip 339 */ 340 public function skipBotDetection(bool $skip = true): void 341 { 342 $this->skipBotDetection = $skip; 343 } 344 345 /** 346 * Returns if the parsed UA was identified as a Bot 347 * 348 * @return bool 349 * 350 * @see bots.yml for a list of detected bots 351 * 352 */ 353 public function isBot(): bool 354 { 355 return !empty($this->bot); 356 } 357 358 /** 359 * Returns if the parsed UA was identified as a touch enabled device 360 * 361 * Note: That only applies to windows 8 tablets 362 * 363 * @return bool 364 */ 365 public function isTouchEnabled(): bool 366 { 367 $regex = 'Touch'; 368 369 return !!$this->matchUserAgent($regex); 370 } 371 372 /** 373 * Returns if the parsed UA is detected as a mobile device 374 * 375 * @return bool 376 */ 377 public function isMobile(): bool 378 { 379 // Client hints indicate a mobile device 380 if ($this->clientHints instanceof ClientHints && $this->clientHints->isMobile()) { 381 return true; 382 } 383 384 // Mobile device types 385 if (\in_array($this->device, [ 386 AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE, 387 AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE, 388 AbstractDeviceParser::DEVICE_TYPE_TABLET, 389 AbstractDeviceParser::DEVICE_TYPE_PHABLET, 390 AbstractDeviceParser::DEVICE_TYPE_CAMERA, 391 AbstractDeviceParser::DEVICE_TYPE_PORTABLE_MEDIA_PAYER, 392 ]) 393 ) { 394 return true; 395 } 396 397 // non mobile device types 398 if (\in_array($this->device, [ 399 AbstractDeviceParser::DEVICE_TYPE_TV, 400 AbstractDeviceParser::DEVICE_TYPE_SMART_DISPLAY, 401 AbstractDeviceParser::DEVICE_TYPE_CONSOLE, 402 ]) 403 ) { 404 return false; 405 } 406 407 // Check for browsers available for mobile devices only 408 if ($this->usesMobileBrowser()) { 409 return true; 410 } 411 412 $osName = $this->getOs('name'); 413 414 if (empty($osName) || self::UNKNOWN === $osName) { 415 return false; 416 } 417 418 return !$this->isBot() && !$this->isDesktop(); 419 } 420 421 /** 422 * Returns if the parsed UA was identified as desktop device 423 * Desktop devices are all devices with an unknown type that are running a desktop os 424 * 425 * @return bool 426 * 427 * @see OperatingSystem::$desktopOsArray 428 * 429 */ 430 public function isDesktop(): bool 431 { 432 $osName = $this->getOsAttribute('name'); 433 434 if (empty($osName) || self::UNKNOWN === $osName) { 435 return false; 436 } 437 438 // Check for browsers available for mobile devices only 439 if ($this->usesMobileBrowser()) { 440 return false; 441 } 442 443 return OperatingSystem::isDesktopOs($osName); 444 } 445 446 /** 447 * Returns the operating system data extracted from the parsed UA 448 * 449 * If $attr is given only that property will be returned 450 * 451 * @param string $attr property to return(optional) 452 * 453 * @return array|string|null 454 */ 455 public function getOs(string $attr = '') 456 { 457 if ('' === $attr) { 458 return $this->os; 459 } 460 461 return $this->getOsAttribute($attr); 462 } 463 464 /** 465 * Returns the client data extracted from the parsed UA 466 * 467 * If $attr is given only that property will be returned 468 * 469 * @param string $attr property to return(optional) 470 * 471 * @return array|string|null 472 */ 473 public function getClient(string $attr = '') 474 { 475 if ('' === $attr) { 476 return $this->client; 477 } 478 479 return $this->getClientAttribute($attr); 480 } 481 482 /** 483 * Returns the device type extracted from the parsed UA 484 * 485 * @return int|null 486 * 487 * @see AbstractDeviceParser::$deviceTypes for available device types 488 * 489 */ 490 public function getDevice(): ?int 491 { 492 return $this->device; 493 } 494 495 /** 496 * Returns the device type extracted from the parsed UA 497 * 498 * @return string 499 * 500 * @see AbstractDeviceParser::$deviceTypes for available device types 501 * 502 */ 503 public function getDeviceName(): string 504 { 505 if (null !== $this->getDevice()) { 506 return AbstractDeviceParser::getDeviceName($this->getDevice()); 507 } 508 509 return ''; 510 } 511 512 /** 513 * Returns the device brand extracted from the parsed UA 514 * 515 * @return string 516 * 517 * @see self::$deviceBrand for available device brands 518 * 519 * @deprecated since 4.0 - short codes might be removed in next major release 520 */ 521 public function getBrand(): string 522 { 523 return AbstractDeviceParser::getShortCode($this->brand); 524 } 525 526 /** 527 * Returns the full device brand name extracted from the parsed UA 528 * 529 * @return string 530 * 531 * @see self::$deviceBrand for available device brands 532 * 533 */ 534 public function getBrandName(): string 535 { 536 return $this->brand; 537 } 538 539 /** 540 * Returns the device model extracted from the parsed UA 541 * 542 * @return string 543 */ 544 public function getModel(): string 545 { 546 return $this->model; 547 } 548 549 /** 550 * Returns the user agent that is set to be parsed 551 * 552 * @return string 553 */ 554 public function getUserAgent(): string 555 { 556 return $this->userAgent; 557 } 558 559 /** 560 * Returns the client hints that is set to be parsed 561 * 562 * @return ?ClientHints 563 */ 564 public function getClientHints(): ?ClientHints 565 { 566 return $this->clientHints; 567 } 568 569 /** 570 * Returns the bot extracted from the parsed UA 571 * 572 * @return array|bool|null 573 */ 574 public function getBot() 575 { 576 return $this->bot; 577 } 578 579 /** 580 * Returns true, if userAgent was already parsed with parse() 581 * 582 * @return bool 583 */ 584 public function isParsed(): bool 585 { 586 return $this->parsed; 587 } 588 589 /** 590 * Triggers the parsing of the current user agent 591 */ 592 public function parse(): void 593 { 594 if ($this->isParsed()) { 595 return; 596 } 597 598 $this->parsed = true; 599 600 // skip parsing for empty useragents or those not containing any letter (if no client hints were provided) 601 if ((empty($this->userAgent) || !\preg_match('/([a-z])/i', $this->userAgent)) 602 && empty($this->clientHints) 603 ) { 604 return; 605 } 606 607 $this->parseBot(); 608 609 if ($this->isBot()) { 610 return; 611 } 612 613 $this->parseOs(); 614 615 /** 616 * Parse Clients 617 * Clients might be browsers, Feed Readers, Mobile Apps, Media Players or 618 * any other application accessing with an parseable UA 619 */ 620 $this->parseClient(); 621 622 $this->parseDevice(); 623 } 624 625 /** 626 * Parses a useragent and returns the detected data 627 * 628 * ATTENTION: Use that method only for testing or very small applications 629 * To get fast results from DeviceDetector you need to make your own implementation, 630 * that should use one of the caching mechanisms. See README.md for more information. 631 * 632 * @param string $ua UserAgent to parse 633 * @param ?ClientHints $clientHints Client Hints to parse 634 * 635 * @return array 636 * 637 * @deprecated 638 * 639 * @internal 640 * 641 */ 642 public static function getInfoFromUserAgent(string $ua, ?ClientHints $clientHints = null): array 643 { 644 static $deviceDetector; 645 646 if (!($deviceDetector instanceof DeviceDetector)) { 647 $deviceDetector = new DeviceDetector(); 648 } 649 650 $deviceDetector->setUserAgent($ua); 651 $deviceDetector->setClientHints($clientHints); 652 653 $deviceDetector->parse(); 654 655 if ($deviceDetector->isBot()) { 656 return [ 657 'user_agent' => $deviceDetector->getUserAgent(), 658 'bot' => $deviceDetector->getBot(), 659 ]; 660 } 661 662 /** @var array $client */ 663 $client = $deviceDetector->getClient(); 664 $browserFamily = 'Unknown'; 665 666 if ($deviceDetector->isBrowser() 667 && true === \is_array($client) 668 && true === \array_key_exists('family', $client) 669 && null !== $client['family'] 670 ) { 671 $browserFamily = $client['family']; 672 } 673 674 unset($client['short_name'], $client['family']); 675 676 /** @var array $os */ 677 $os = $deviceDetector->getOs(); 678 $osFamily = $os['family'] ?? 'Unknown'; 679 680 unset($os['short_name'], $os['family']); 681 682 return [ 683 'user_agent' => $deviceDetector->getUserAgent(), 684 'os' => $os, 685 'client' => $client, 686 'device' => [ 687 'type' => $deviceDetector->getDeviceName(), 688 'brand' => $deviceDetector->getBrandName(), 689 'model' => $deviceDetector->getModel(), 690 ], 691 'os_family' => $osFamily, 692 'browser_family' => $browserFamily, 693 ]; 694 } 695 696 /** 697 * Sets the Cache class 698 * 699 * @param CacheInterface $cache 700 */ 701 public function setCache(CacheInterface $cache): void 702 { 703 $this->cache = $cache; 704 } 705 706 /** 707 * Returns Cache object 708 * 709 * @return CacheInterface 710 */ 711 public function getCache(): CacheInterface 712 { 713 if (!empty($this->cache)) { 714 return $this->cache; 715 } 716 717 return new StaticCache(); 718 } 719 720 /** 721 * Sets the Yaml Parser class 722 * 723 * @param YamlParser $yamlParser 724 */ 725 public function setYamlParser(YamlParser $yamlParser): void 726 { 727 $this->yamlParser = $yamlParser; 728 } 729 730 /** 731 * Returns Yaml Parser object 732 * 733 * @return YamlParser 734 */ 735 public function getYamlParser(): YamlParser 736 { 737 if (!empty($this->yamlParser)) { 738 return $this->yamlParser; 739 } 740 741 return new Spyc(); 742 } 743 744 /** 745 * @param string $attr 746 * 747 * @return string 748 */ 749 protected function getClientAttribute(string $attr): string 750 { 751 if (!isset($this->client[$attr])) { 752 return self::UNKNOWN; 753 } 754 755 return $this->client[$attr]; 756 } 757 758 /** 759 * @param string $attr 760 * 761 * @return string 762 */ 763 protected function getOsAttribute(string $attr): string 764 { 765 if (!isset($this->os[$attr])) { 766 return self::UNKNOWN; 767 } 768 769 return $this->os[$attr]; 770 } 771 772 /** 773 * Returns if the parsed UA contains the 'Android; Tablet;' fragment 774 * 775 * @return bool 776 */ 777 protected function hasAndroidTableFragment(): bool 778 { 779 $regex = 'Android( [.0-9]+)?; Tablet;|Tablet(?! PC)|.*\-tablet$'; 780 781 return !!$this->matchUserAgent($regex); 782 } 783 784 /** 785 * Returns if the parsed UA contains the 'Android; Mobile;' fragment 786 * 787 * @return bool 788 */ 789 protected function hasAndroidMobileFragment(): bool 790 { 791 $regex = 'Android( [.0-9]+)?; Mobile;|.*\-mobile$'; 792 793 return !!$this->matchUserAgent($regex); 794 } 795 796 /** 797 * Returns if the parsed UA contains the 'Android; Mobile VR;' fragment 798 * 799 * @return bool 800 */ 801 protected function hasAndroidVRFragment(): bool 802 { 803 $regex = 'Android( [.0-9]+)?; Mobile VR;| VR '; 804 805 return !!$this->matchUserAgent($regex); 806 } 807 808 /** 809 * Returns if the parsed UA contains the 'Desktop;', 'Desktop x32;', 'Desktop x64;' or 'Desktop WOW64;' fragment 810 * 811 * @return bool 812 */ 813 protected function hasDesktopFragment(): bool 814 { 815 $regex = 'Desktop(?: (x(?:32|64)|WOW64))?;'; 816 817 return !!$this->matchUserAgent($regex); 818 } 819 820 /** 821 * Returns if the parsed UA contains usage of a mobile only browser 822 * 823 * @return bool 824 */ 825 protected function usesMobileBrowser(): bool 826 { 827 return 'browser' === $this->getClient('type') 828 && Browser::isMobileOnlyBrowser($this->getClientAttribute('name')); 829 } 830 831 /** 832 * Parses the UA for bot information using the Bot parser 833 */ 834 protected function parseBot(): void 835 { 836 if ($this->skipBotDetection) { 837 $this->bot = false; 838 839 return; 840 } 841 842 $parsers = $this->getBotParsers(); 843 844 foreach ($parsers as $parser) { 845 $parser->setYamlParser($this->getYamlParser()); 846 $parser->setCache($this->getCache()); 847 $parser->setUserAgent($this->getUserAgent()); 848 $parser->setClientHints($this->getClientHints()); 849 850 if ($this->discardBotInformation) { 851 $parser->discardDetails(); 852 } 853 854 $bot = $parser->parse(); 855 856 if (!empty($bot)) { 857 $this->bot = $bot; 858 859 break; 860 } 861 } 862 } 863 864 /** 865 * Tries to detect the client (e.g. browser, mobile app, ...) 866 */ 867 protected function parseClient(): void 868 { 869 $parsers = $this->getClientParsers(); 870 871 foreach ($parsers as $parser) { 872 $parser->setYamlParser($this->getYamlParser()); 873 $parser->setCache($this->getCache()); 874 $parser->setUserAgent($this->getUserAgent()); 875 $parser->setClientHints($this->getClientHints()); 876 $client = $parser->parse(); 877 878 if (!empty($client)) { 879 $this->client = $client; 880 881 break; 882 } 883 } 884 } 885 886 /** 887 * Tries to detect the device type, model and brand 888 */ 889 protected function parseDevice(): void 890 { 891 $parsers = $this->getDeviceParsers(); 892 893 foreach ($parsers as $parser) { 894 $parser->setYamlParser($this->getYamlParser()); 895 $parser->setCache($this->getCache()); 896 $parser->setUserAgent($this->getUserAgent()); 897 $parser->setClientHints($this->getClientHints()); 898 899 if ($parser->parse()) { 900 $this->device = $parser->getDeviceType(); 901 $this->model = $parser->getModel(); 902 $this->brand = $parser->getBrand(); 903 904 break; 905 } 906 } 907 908 /** 909 * If no model could be parsed from useragent, we use the one from client hints if available 910 */ 911 if ($this->clientHints instanceof ClientHints && empty($this->model)) { 912 $this->model = $this->clientHints->getModel(); 913 } 914 915 /** 916 * If no brand has been assigned try to match by known vendor fragments 917 */ 918 if (empty($this->brand)) { 919 $vendorParser = new VendorFragment($this->getUserAgent()); 920 $vendorParser->setYamlParser($this->getYamlParser()); 921 $vendorParser->setCache($this->getCache()); 922 $this->brand = $vendorParser->parse()['brand'] ?? ''; 923 } 924 925 $osName = $this->getOsAttribute('name'); 926 $osFamily = $this->getOsAttribute('family'); 927 $osVersion = $this->getOsAttribute('version'); 928 $clientName = $this->getClientAttribute('name'); 929 $appleOsNames = ['iPadOS', 'tvOS', 'watchOS', 'iOS', 'Mac']; 930 931 /** 932 * if it's fake UA then it's best not to identify it as Apple running Android OS or GNU/Linux 933 */ 934 if ('Apple' === $this->brand && !\in_array($osName, $appleOsNames)) { 935 $this->device = null; 936 $this->brand = ''; 937 $this->model = ''; 938 } 939 940 /** 941 * Assume all devices running iOS / Mac OS are from Apple 942 */ 943 if (empty($this->brand) && \in_array($osName, $appleOsNames)) { 944 $this->brand = 'Apple'; 945 } 946 947 /** 948 * All devices containing VR fragment are assumed to be a wearable 949 */ 950 if (null === $this->device && $this->hasAndroidVRFragment()) { 951 $this->device = AbstractDeviceParser::DEVICE_TYPE_WEARABLE; 952 } 953 954 /** 955 * Chrome on Android passes the device type based on the keyword 'Mobile' 956 * If it is present the device should be a smartphone, otherwise it's a tablet 957 * See https://developer.chrome.com/multidevice/user-agent#chrome_for_android_user_agent 958 * Note: We do not check for browser (family) here, as there might be mobile apps using Chrome, that won't have 959 * a detected browser, but can still be detected. So we check the useragent for Chrome instead. 960 */ 961 if (null === $this->device && 'Android' === $osFamily 962 && $this->matchUserAgent('Chrome/[.0-9]*') 963 ) { 964 if ($this->matchUserAgent('(?:Mobile|eliboM)')) { 965 $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; 966 } else { 967 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 968 } 969 } 970 971 /** 972 * Some UA contain the fragment 'Pad/APad', so we assume those devices as tablets 973 */ 974 if (AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE === $this->device && $this->matchUserAgent('Pad/APad')) { 975 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 976 } 977 978 /** 979 * Some UA contain the fragment 'Android; Tablet;' or 'Opera Tablet', so we assume those devices as tablets 980 */ 981 if (null === $this->device && ($this->hasAndroidTableFragment() 982 || $this->matchUserAgent('Opera Tablet')) 983 ) { 984 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 985 } 986 987 /** 988 * Some user agents simply contain the fragment 'Android; Mobile;', so we assume those devices as smartphones 989 */ 990 if (null === $this->device && $this->hasAndroidMobileFragment()) { 991 $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; 992 } 993 994 /** 995 * Android up to 3.0 was designed for smartphones only. But as 3.0, which was tablet only, was published 996 * too late, there were a bunch of tablets running with 2.x 997 * With 4.0 the two trees were merged and it is for smartphones and tablets 998 * 999 * So were are expecting that all devices running Android < 2 are smartphones 1000 * Devices running Android 3.X are tablets. Device type of Android 2.X and 4.X+ are unknown 1001 */ 1002 if (null === $this->device && 'Android' === $osName && '' !== $osVersion) { 1003 if (-1 === \version_compare($osVersion, '2.0')) { 1004 $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; 1005 } elseif (\version_compare($osVersion, '3.0') >= 0 1006 && -1 === \version_compare($osVersion, '4.0') 1007 ) { 1008 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 1009 } 1010 } 1011 1012 /** 1013 * All detected feature phones running android are more likely a smartphone 1014 */ 1015 if (AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE === $this->device && 'Android' === $osFamily) { 1016 $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; 1017 } 1018 1019 /** 1020 * All unknown devices under running Java ME are more likely features phones 1021 */ 1022 if ('Java ME' === $osName && null === $this->device) { 1023 $this->device = AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE; 1024 } 1025 1026 /** 1027 * All devices running KaiOS are more likely features phones 1028 */ 1029 if ('KaiOS' === $osName) { 1030 $this->device = AbstractDeviceParser::DEVICE_TYPE_FEATURE_PHONE; 1031 } 1032 1033 /** 1034 * According to http://msdn.microsoft.com/en-us/library/ie/hh920767(v=vs.85).aspx 1035 * Internet Explorer 10 introduces the "Touch" UA string token. If this token is present at the end of the 1036 * UA string, the computer has touch capability, and is running Windows 8 (or later). 1037 * This UA string will be transmitted on a touch-enabled system running Windows 8 (RT) 1038 * 1039 * As most touch enabled devices are tablets and only a smaller part are desktops/notebooks we assume that 1040 * all Windows 8 touch devices are tablets. 1041 */ 1042 1043 if (null === $this->device && ('Windows RT' === $osName || ('Windows' === $osName 1044 && \version_compare($osVersion, '8') >= 0)) && $this->isTouchEnabled() 1045 ) { 1046 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 1047 } 1048 1049 /** 1050 * All devices running Puffin Secure Browser that contain letter 'D' are assumed to be desktops 1051 */ 1052 if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[LMW]D')) { 1053 $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; 1054 } 1055 1056 /** 1057 * All devices running Puffin Web Browser that contain letter 'P' are assumed to be smartphones 1058 */ 1059 if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[AIFLW]P')) { 1060 $this->device = AbstractDeviceParser::DEVICE_TYPE_SMARTPHONE; 1061 } 1062 1063 /** 1064 * All devices running Puffin Web Browser that contain letter 'T' are assumed to be tablets 1065 */ 1066 if (null === $this->device && $this->matchUserAgent('Puffin/(?:\d+[.\d]+)[AILW]T')) { 1067 $this->device = AbstractDeviceParser::DEVICE_TYPE_TABLET; 1068 } 1069 1070 /** 1071 * All devices running Opera TV Store are assumed to be a tv 1072 */ 1073 if ($this->matchUserAgent('Opera TV Store| OMI/')) { 1074 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1075 } 1076 1077 /** 1078 * All devices running Coolita OS are assumed to be a tv 1079 */ 1080 if ('Coolita OS' === $osName) { 1081 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1082 $this->brand = 'coocaa'; 1083 } 1084 1085 /** 1086 * All devices that contain Andr0id in string are assumed to be a tv 1087 */ 1088 $hasDeviceTvType = false === \in_array($this->device, [ 1089 AbstractDeviceParser::DEVICE_TYPE_TV, 1090 AbstractDeviceParser::DEVICE_TYPE_PERIPHERAL, 1091 ]) && $this->matchUserAgent('Andr0id|(?:Android(?: UHD)?|Google) TV|\(lite\) TV|BRAVIA|Firebolt| TV$'); 1092 1093 if ($hasDeviceTvType) { 1094 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1095 } 1096 1097 /** 1098 * All devices running Tizen TV or SmartTV are assumed to be a tv 1099 */ 1100 if (null === $this->device && $this->matchUserAgent('SmartTV|Tizen.+ TV .+$')) { 1101 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1102 } 1103 1104 /** 1105 * Devices running those clients are assumed to be a TV 1106 */ 1107 if (\in_array($clientName, [ 1108 'Kylo', 'Espial TV Browser', 'LUJO TV Browser', 'LogicUI TV Browser', 'Open TV Browser', 'Seraphic Sraf', 1109 'Opera Devices', 'Crow Browser', 'Vewd Browser', 'TiviMate', 'Quick Search TV', 'QJY TV Browser', 'TV Bro', 1110 ]) 1111 ) { 1112 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1113 } 1114 1115 /** 1116 * All devices containing TV fragment are assumed to be a tv 1117 */ 1118 if (null === $this->device && $this->matchUserAgent('\(TV;')) { 1119 $this->device = AbstractDeviceParser::DEVICE_TYPE_TV; 1120 } 1121 1122 /** 1123 * Set device type desktop if string ua contains desktop 1124 */ 1125 $hasDesktop = AbstractDeviceParser::DEVICE_TYPE_DESKTOP !== $this->device 1126 && false !== \strpos($this->userAgent, 'Desktop') 1127 && $this->hasDesktopFragment(); 1128 1129 if ($hasDesktop) { 1130 $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; 1131 } 1132 1133 // set device type to desktop for all devices running a desktop os that were not detected as another device type 1134 if (null !== $this->device || !$this->isDesktop()) { 1135 return; 1136 } 1137 1138 $this->device = AbstractDeviceParser::DEVICE_TYPE_DESKTOP; 1139 } 1140 1141 /** 1142 * Tries to detect the operating system 1143 */ 1144 protected function parseOs(): void 1145 { 1146 $osParser = new OperatingSystem(); 1147 $osParser->setUserAgent($this->getUserAgent()); 1148 $osParser->setClientHints($this->getClientHints()); 1149 $osParser->setYamlParser($this->getYamlParser()); 1150 $osParser->setCache($this->getCache()); 1151 $this->os = $osParser->parse(); 1152 } 1153 1154 /** 1155 * @param string $regex 1156 * 1157 * @return array|null 1158 */ 1159 protected function matchUserAgent(string $regex): ?array 1160 { 1161 $regex = '/(?:^|[^A-Z_-])(?:' . \str_replace('/', '\/', $regex) . ')/i'; 1162 1163 if (\preg_match($regex, $this->userAgent, $matches)) { 1164 return $matches; 1165 } 1166 1167 return null; 1168 } 1169 1170 /** 1171 * Resets all detected data 1172 */ 1173 protected function reset(): void 1174 { 1175 $this->bot = null; 1176 $this->client = null; 1177 $this->device = null; 1178 $this->os = null; 1179 $this->brand = ''; 1180 $this->model = ''; 1181 $this->parsed = false; 1182 } 1183} 1184