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