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