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