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