vendor/matomo/device-detector/DeviceDetector.php line 188

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