xref: /plugin/statistics/vendor/matomo/device-detector/Parser/AbstractParser.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\Parser;
14*d5ef99ddSAndreas Gohr
15*d5ef99ddSAndreas Gohruse DeviceDetector\Cache\CacheInterface;
16*d5ef99ddSAndreas Gohruse DeviceDetector\Cache\StaticCache;
17*d5ef99ddSAndreas Gohruse DeviceDetector\ClientHints;
18*d5ef99ddSAndreas Gohruse DeviceDetector\DeviceDetector;
19*d5ef99ddSAndreas Gohruse DeviceDetector\Yaml\ParserInterface as YamlParser;
20*d5ef99ddSAndreas Gohruse DeviceDetector\Yaml\Spyc;
21*d5ef99ddSAndreas Gohr
22*d5ef99ddSAndreas Gohr/**
23*d5ef99ddSAndreas Gohr * Class AbstractParser
24*d5ef99ddSAndreas Gohr */
25*d5ef99ddSAndreas Gohrabstract class AbstractParser
26*d5ef99ddSAndreas Gohr{
27*d5ef99ddSAndreas Gohr    /**
28*d5ef99ddSAndreas Gohr     * Holds the path to the yml file containing regexes
29*d5ef99ddSAndreas Gohr     * @var string
30*d5ef99ddSAndreas Gohr     */
31*d5ef99ddSAndreas Gohr    protected $fixtureFile;
32*d5ef99ddSAndreas Gohr
33*d5ef99ddSAndreas Gohr    /**
34*d5ef99ddSAndreas Gohr     * Holds the internal name of the parser
35*d5ef99ddSAndreas Gohr     * Used for caching
36*d5ef99ddSAndreas Gohr     * @var string
37*d5ef99ddSAndreas Gohr     */
38*d5ef99ddSAndreas Gohr    protected $parserName;
39*d5ef99ddSAndreas Gohr
40*d5ef99ddSAndreas Gohr    /**
41*d5ef99ddSAndreas Gohr     * Holds the user agent to be parsed
42*d5ef99ddSAndreas Gohr     * @var string
43*d5ef99ddSAndreas Gohr     */
44*d5ef99ddSAndreas Gohr    protected $userAgent;
45*d5ef99ddSAndreas Gohr
46*d5ef99ddSAndreas Gohr    /**
47*d5ef99ddSAndreas Gohr     * Holds the client hints to be parsed
48*d5ef99ddSAndreas Gohr     * @var ?ClientHints
49*d5ef99ddSAndreas Gohr     */
50*d5ef99ddSAndreas Gohr    protected $clientHints = null;
51*d5ef99ddSAndreas Gohr
52*d5ef99ddSAndreas Gohr    /**
53*d5ef99ddSAndreas Gohr     * Contains a list of mappings from names we use to known client hint values
54*d5ef99ddSAndreas Gohr     * @var array<string, array<string>>
55*d5ef99ddSAndreas Gohr     */
56*d5ef99ddSAndreas Gohr    protected static $clientHintMapping = [];
57*d5ef99ddSAndreas Gohr
58*d5ef99ddSAndreas Gohr    /**
59*d5ef99ddSAndreas Gohr     * Holds an array with method that should be available global
60*d5ef99ddSAndreas Gohr     * @var array
61*d5ef99ddSAndreas Gohr     */
62*d5ef99ddSAndreas Gohr    protected $globalMethods;
63*d5ef99ddSAndreas Gohr
64*d5ef99ddSAndreas Gohr    /**
65*d5ef99ddSAndreas Gohr     * Holds an array with regexes to parse, if already loaded
66*d5ef99ddSAndreas Gohr     * @var array
67*d5ef99ddSAndreas Gohr     */
68*d5ef99ddSAndreas Gohr    protected $regexList;
69*d5ef99ddSAndreas Gohr
70*d5ef99ddSAndreas Gohr    /**
71*d5ef99ddSAndreas Gohr     * Holds the concatenated regex for all items in regex list
72*d5ef99ddSAndreas Gohr     * @var string
73*d5ef99ddSAndreas Gohr     */
74*d5ef99ddSAndreas Gohr    protected $overAllMatch;
75*d5ef99ddSAndreas Gohr
76*d5ef99ddSAndreas Gohr    /**
77*d5ef99ddSAndreas Gohr     * Indicates how deep versioning will be detected
78*d5ef99ddSAndreas Gohr     * if $maxMinorParts is 0 only the major version will be returned
79*d5ef99ddSAndreas Gohr     * @var int
80*d5ef99ddSAndreas Gohr     */
81*d5ef99ddSAndreas Gohr    protected static $maxMinorParts = 1;
82*d5ef99ddSAndreas Gohr
83*d5ef99ddSAndreas Gohr    /**
84*d5ef99ddSAndreas Gohr     * Versioning constant used to set max versioning to major version only
85*d5ef99ddSAndreas Gohr     * Version examples are: 3, 5, 6, 200, 123, ...
86*d5ef99ddSAndreas Gohr     */
87*d5ef99ddSAndreas Gohr    public const VERSION_TRUNCATION_MAJOR = 0;
88*d5ef99ddSAndreas Gohr
89*d5ef99ddSAndreas Gohr    /**
90*d5ef99ddSAndreas Gohr     * Versioning constant used to set max versioning to minor version
91*d5ef99ddSAndreas Gohr     * Version examples are: 3.4, 5.6, 6.234, 0.200, 1.23, ...
92*d5ef99ddSAndreas Gohr     */
93*d5ef99ddSAndreas Gohr    public const VERSION_TRUNCATION_MINOR = 1;
94*d5ef99ddSAndreas Gohr
95*d5ef99ddSAndreas Gohr    /**
96*d5ef99ddSAndreas Gohr     * Versioning constant used to set max versioning to path level
97*d5ef99ddSAndreas Gohr     * Version examples are: 3.4.0, 5.6.344, 6.234.2, 0.200.3, 1.2.3, ...
98*d5ef99ddSAndreas Gohr     */
99*d5ef99ddSAndreas Gohr    public const VERSION_TRUNCATION_PATCH = 2;
100*d5ef99ddSAndreas Gohr
101*d5ef99ddSAndreas Gohr    /**
102*d5ef99ddSAndreas Gohr     * Versioning constant used to set versioning to build number
103*d5ef99ddSAndreas Gohr     * Version examples are: 3.4.0.12, 5.6.334.0, 6.234.2.3, 0.200.3.1, 1.2.3.0, ...
104*d5ef99ddSAndreas Gohr     */
105*d5ef99ddSAndreas Gohr    public const VERSION_TRUNCATION_BUILD = 3;
106*d5ef99ddSAndreas Gohr
107*d5ef99ddSAndreas Gohr    /**
108*d5ef99ddSAndreas Gohr     * Versioning constant used to set versioning to unlimited (no truncation)
109*d5ef99ddSAndreas Gohr     */
110*d5ef99ddSAndreas Gohr    public const VERSION_TRUNCATION_NONE = -1;
111*d5ef99ddSAndreas Gohr
112*d5ef99ddSAndreas Gohr    /**
113*d5ef99ddSAndreas Gohr     * @var CacheInterface|null
114*d5ef99ddSAndreas Gohr     */
115*d5ef99ddSAndreas Gohr    protected $cache = null;
116*d5ef99ddSAndreas Gohr
117*d5ef99ddSAndreas Gohr    /**
118*d5ef99ddSAndreas Gohr     * @var YamlParser|null
119*d5ef99ddSAndreas Gohr     */
120*d5ef99ddSAndreas Gohr    protected $yamlParser = null;
121*d5ef99ddSAndreas Gohr
122*d5ef99ddSAndreas Gohr    /**
123*d5ef99ddSAndreas Gohr     * parses the currently set useragents and returns possible results
124*d5ef99ddSAndreas Gohr     *
125*d5ef99ddSAndreas Gohr     * @return array|null
126*d5ef99ddSAndreas Gohr     */
127*d5ef99ddSAndreas Gohr    abstract public function parse(): ?array;
128*d5ef99ddSAndreas Gohr
129*d5ef99ddSAndreas Gohr    /**
130*d5ef99ddSAndreas Gohr     * AbstractParser constructor.
131*d5ef99ddSAndreas Gohr     *
132*d5ef99ddSAndreas Gohr     * @param string       $ua
133*d5ef99ddSAndreas Gohr     * @param ?ClientHints $clientHints
134*d5ef99ddSAndreas Gohr     */
135*d5ef99ddSAndreas Gohr    public function __construct(string $ua = '', ?ClientHints $clientHints = null)
136*d5ef99ddSAndreas Gohr    {
137*d5ef99ddSAndreas Gohr        $this->setUserAgent($ua);
138*d5ef99ddSAndreas Gohr        $this->setClientHints($clientHints);
139*d5ef99ddSAndreas Gohr    }
140*d5ef99ddSAndreas Gohr
141*d5ef99ddSAndreas Gohr    /**
142*d5ef99ddSAndreas Gohr     * @inheritdoc
143*d5ef99ddSAndreas Gohr     */
144*d5ef99ddSAndreas Gohr    public function restoreUserAgentFromClientHints(): void
145*d5ef99ddSAndreas Gohr    {
146*d5ef99ddSAndreas Gohr        if (null === $this->clientHints) {
147*d5ef99ddSAndreas Gohr            return;
148*d5ef99ddSAndreas Gohr        }
149*d5ef99ddSAndreas Gohr
150*d5ef99ddSAndreas Gohr        $deviceModel = $this->clientHints->getModel();
151*d5ef99ddSAndreas Gohr
152*d5ef99ddSAndreas Gohr        if ('' === $deviceModel) {
153*d5ef99ddSAndreas Gohr            return;
154*d5ef99ddSAndreas Gohr        }
155*d5ef99ddSAndreas Gohr
156*d5ef99ddSAndreas Gohr        // Restore Android User Agent
157*d5ef99ddSAndreas Gohr        if ($this->hasUserAgentClientHintsFragment()) {
158*d5ef99ddSAndreas Gohr            $osVersion = $this->clientHints->getOperatingSystemVersion();
159*d5ef99ddSAndreas Gohr            $this->setUserAgent((string) \preg_replace(
160*d5ef99ddSAndreas Gohr                '(Android (?:10[.\d]*; K|1[1-5]))',
161*d5ef99ddSAndreas Gohr                \sprintf('Android %s; %s', '' !== $osVersion ? $osVersion : '10', $deviceModel),
162*d5ef99ddSAndreas Gohr                $this->userAgent
163*d5ef99ddSAndreas Gohr            ));
164*d5ef99ddSAndreas Gohr        }
165*d5ef99ddSAndreas Gohr
166*d5ef99ddSAndreas Gohr        // Restore Desktop User Agent
167*d5ef99ddSAndreas Gohr        if (!$this->hasDesktopFragment()) {
168*d5ef99ddSAndreas Gohr            return;
169*d5ef99ddSAndreas Gohr        }
170*d5ef99ddSAndreas Gohr
171*d5ef99ddSAndreas Gohr        $this->setUserAgent((string) \preg_replace(
172*d5ef99ddSAndreas Gohr            '(X11; Linux x86_64)',
173*d5ef99ddSAndreas Gohr            \sprintf('X11; Linux x86_64; %s', $deviceModel),
174*d5ef99ddSAndreas Gohr            $this->userAgent
175*d5ef99ddSAndreas Gohr        ));
176*d5ef99ddSAndreas Gohr    }
177*d5ef99ddSAndreas Gohr
178*d5ef99ddSAndreas Gohr    /**
179*d5ef99ddSAndreas Gohr     * Set how DeviceDetector should return versions
180*d5ef99ddSAndreas Gohr     * @param int $type Any of the VERSION_TRUNCATION_* constants
181*d5ef99ddSAndreas Gohr     */
182*d5ef99ddSAndreas Gohr    public static function setVersionTruncation(int $type): void
183*d5ef99ddSAndreas Gohr    {
184*d5ef99ddSAndreas Gohr        if (!\in_array($type, [
185*d5ef99ddSAndreas Gohr            self::VERSION_TRUNCATION_BUILD,
186*d5ef99ddSAndreas Gohr            self::VERSION_TRUNCATION_NONE,
187*d5ef99ddSAndreas Gohr            self::VERSION_TRUNCATION_MAJOR,
188*d5ef99ddSAndreas Gohr            self::VERSION_TRUNCATION_MINOR,
189*d5ef99ddSAndreas Gohr            self::VERSION_TRUNCATION_PATCH,
190*d5ef99ddSAndreas Gohr        ])
191*d5ef99ddSAndreas Gohr        ) {
192*d5ef99ddSAndreas Gohr            return;
193*d5ef99ddSAndreas Gohr        }
194*d5ef99ddSAndreas Gohr
195*d5ef99ddSAndreas Gohr        static::$maxMinorParts = $type;
196*d5ef99ddSAndreas Gohr    }
197*d5ef99ddSAndreas Gohr
198*d5ef99ddSAndreas Gohr    /**
199*d5ef99ddSAndreas Gohr     * Sets the user agent to parse
200*d5ef99ddSAndreas Gohr     *
201*d5ef99ddSAndreas Gohr     * @param string $ua user agent
202*d5ef99ddSAndreas Gohr     */
203*d5ef99ddSAndreas Gohr    public function setUserAgent(string $ua): void
204*d5ef99ddSAndreas Gohr    {
205*d5ef99ddSAndreas Gohr        $this->userAgent = $ua;
206*d5ef99ddSAndreas Gohr    }
207*d5ef99ddSAndreas Gohr
208*d5ef99ddSAndreas Gohr    /**
209*d5ef99ddSAndreas Gohr     * Sets the client hints to parse
210*d5ef99ddSAndreas Gohr     *
211*d5ef99ddSAndreas Gohr     * @param ?ClientHints $clientHints client hints
212*d5ef99ddSAndreas Gohr     */
213*d5ef99ddSAndreas Gohr    public function setClientHints(?ClientHints $clientHints): void
214*d5ef99ddSAndreas Gohr    {
215*d5ef99ddSAndreas Gohr        $this->clientHints = $clientHints;
216*d5ef99ddSAndreas Gohr    }
217*d5ef99ddSAndreas Gohr
218*d5ef99ddSAndreas Gohr    /**
219*d5ef99ddSAndreas Gohr     * Returns the internal name of the parser
220*d5ef99ddSAndreas Gohr     *
221*d5ef99ddSAndreas Gohr     * @return string
222*d5ef99ddSAndreas Gohr     */
223*d5ef99ddSAndreas Gohr    public function getName(): string
224*d5ef99ddSAndreas Gohr    {
225*d5ef99ddSAndreas Gohr        return $this->parserName;
226*d5ef99ddSAndreas Gohr    }
227*d5ef99ddSAndreas Gohr
228*d5ef99ddSAndreas Gohr    /**
229*d5ef99ddSAndreas Gohr     * Sets the Cache class
230*d5ef99ddSAndreas Gohr     *
231*d5ef99ddSAndreas Gohr     * @param CacheInterface $cache
232*d5ef99ddSAndreas Gohr     */
233*d5ef99ddSAndreas Gohr    public function setCache(CacheInterface $cache): void
234*d5ef99ddSAndreas Gohr    {
235*d5ef99ddSAndreas Gohr        $this->cache = $cache;
236*d5ef99ddSAndreas Gohr    }
237*d5ef99ddSAndreas Gohr
238*d5ef99ddSAndreas Gohr    /**
239*d5ef99ddSAndreas Gohr     * Returns Cache object
240*d5ef99ddSAndreas Gohr     *
241*d5ef99ddSAndreas Gohr     * @return CacheInterface
242*d5ef99ddSAndreas Gohr     */
243*d5ef99ddSAndreas Gohr    public function getCache(): CacheInterface
244*d5ef99ddSAndreas Gohr    {
245*d5ef99ddSAndreas Gohr        if (!empty($this->cache)) {
246*d5ef99ddSAndreas Gohr            return $this->cache;
247*d5ef99ddSAndreas Gohr        }
248*d5ef99ddSAndreas Gohr
249*d5ef99ddSAndreas Gohr        return new StaticCache();
250*d5ef99ddSAndreas Gohr    }
251*d5ef99ddSAndreas Gohr
252*d5ef99ddSAndreas Gohr    /**
253*d5ef99ddSAndreas Gohr     * Sets the YamlParser class
254*d5ef99ddSAndreas Gohr     *
255*d5ef99ddSAndreas Gohr     * @param YamlParser $yamlParser
256*d5ef99ddSAndreas Gohr     */
257*d5ef99ddSAndreas Gohr    public function setYamlParser(YamlParser $yamlParser): void
258*d5ef99ddSAndreas Gohr    {
259*d5ef99ddSAndreas Gohr        $this->yamlParser = $yamlParser;
260*d5ef99ddSAndreas Gohr    }
261*d5ef99ddSAndreas Gohr
262*d5ef99ddSAndreas Gohr    /**
263*d5ef99ddSAndreas Gohr     * Returns YamlParser object
264*d5ef99ddSAndreas Gohr     *
265*d5ef99ddSAndreas Gohr     * @return YamlParser
266*d5ef99ddSAndreas Gohr     */
267*d5ef99ddSAndreas Gohr    public function getYamlParser(): YamlParser
268*d5ef99ddSAndreas Gohr    {
269*d5ef99ddSAndreas Gohr        if (!empty($this->yamlParser)) {
270*d5ef99ddSAndreas Gohr            return $this->yamlParser;
271*d5ef99ddSAndreas Gohr        }
272*d5ef99ddSAndreas Gohr
273*d5ef99ddSAndreas Gohr        return new Spyc();
274*d5ef99ddSAndreas Gohr    }
275*d5ef99ddSAndreas Gohr
276*d5ef99ddSAndreas Gohr    /**
277*d5ef99ddSAndreas Gohr     * Returns the result of the parsed yml file defined in $fixtureFile
278*d5ef99ddSAndreas Gohr     *
279*d5ef99ddSAndreas Gohr     * @return array
280*d5ef99ddSAndreas Gohr     */
281*d5ef99ddSAndreas Gohr    protected function getRegexes(): array
282*d5ef99ddSAndreas Gohr    {
283*d5ef99ddSAndreas Gohr        if (empty($this->regexList)) {
284*d5ef99ddSAndreas Gohr            $cacheKey     = 'DeviceDetector-' . DeviceDetector::VERSION . 'regexes-' . $this->getName();
285*d5ef99ddSAndreas Gohr            $cacheKey     = (string) \preg_replace('/([^a-z0-9_-]+)/i', '', $cacheKey);
286*d5ef99ddSAndreas Gohr            $cacheContent = $this->getCache()->fetch($cacheKey);
287*d5ef99ddSAndreas Gohr
288*d5ef99ddSAndreas Gohr            if (\is_array($cacheContent)) {
289*d5ef99ddSAndreas Gohr                $this->regexList = $cacheContent;
290*d5ef99ddSAndreas Gohr            }
291*d5ef99ddSAndreas Gohr
292*d5ef99ddSAndreas Gohr            if (empty($this->regexList)) {
293*d5ef99ddSAndreas Gohr                $parsedContent = $this->getYamlParser()->parseFile(
294*d5ef99ddSAndreas Gohr                    $this->getRegexesDirectory() . DIRECTORY_SEPARATOR . $this->fixtureFile
295*d5ef99ddSAndreas Gohr                );
296*d5ef99ddSAndreas Gohr
297*d5ef99ddSAndreas Gohr                if (!\is_array($parsedContent)) {
298*d5ef99ddSAndreas Gohr                    $parsedContent = [];
299*d5ef99ddSAndreas Gohr                }
300*d5ef99ddSAndreas Gohr
301*d5ef99ddSAndreas Gohr                $this->regexList = $parsedContent;
302*d5ef99ddSAndreas Gohr                $this->getCache()->save($cacheKey, $this->regexList);
303*d5ef99ddSAndreas Gohr            }
304*d5ef99ddSAndreas Gohr        }
305*d5ef99ddSAndreas Gohr
306*d5ef99ddSAndreas Gohr        return $this->regexList;
307*d5ef99ddSAndreas Gohr    }
308*d5ef99ddSAndreas Gohr
309*d5ef99ddSAndreas Gohr    /**
310*d5ef99ddSAndreas Gohr     * Returns the provided name after applying client hint mappings.
311*d5ef99ddSAndreas Gohr     * This is used to map names provided in client hints to the names we use.
312*d5ef99ddSAndreas Gohr     *
313*d5ef99ddSAndreas Gohr     * @param string $name
314*d5ef99ddSAndreas Gohr     *
315*d5ef99ddSAndreas Gohr     * @return string
316*d5ef99ddSAndreas Gohr     */
317*d5ef99ddSAndreas Gohr    protected function applyClientHintMapping(string $name): string
318*d5ef99ddSAndreas Gohr    {
319*d5ef99ddSAndreas Gohr        foreach (static::$clientHintMapping as $mappedName => $clientHints) {
320*d5ef99ddSAndreas Gohr            foreach ($clientHints as $clientHint) {
321*d5ef99ddSAndreas Gohr                if (\strtolower($name) === \strtolower($clientHint)) {
322*d5ef99ddSAndreas Gohr                    return $mappedName;
323*d5ef99ddSAndreas Gohr                }
324*d5ef99ddSAndreas Gohr            }
325*d5ef99ddSAndreas Gohr        }
326*d5ef99ddSAndreas Gohr
327*d5ef99ddSAndreas Gohr        return $name;
328*d5ef99ddSAndreas Gohr    }
329*d5ef99ddSAndreas Gohr
330*d5ef99ddSAndreas Gohr    /**
331*d5ef99ddSAndreas Gohr     * @return string
332*d5ef99ddSAndreas Gohr     */
333*d5ef99ddSAndreas Gohr    protected function getRegexesDirectory(): string
334*d5ef99ddSAndreas Gohr    {
335*d5ef99ddSAndreas Gohr        return \dirname(__DIR__);
336*d5ef99ddSAndreas Gohr    }
337*d5ef99ddSAndreas Gohr
338*d5ef99ddSAndreas Gohr    /**
339*d5ef99ddSAndreas Gohr     * Returns if the parsed UA contains the 'Windows NT;' or 'X11; Linux x86_64' fragments
340*d5ef99ddSAndreas Gohr     *
341*d5ef99ddSAndreas Gohr     * @return bool
342*d5ef99ddSAndreas Gohr     */
343*d5ef99ddSAndreas Gohr    protected function hasDesktopFragment(): bool
344*d5ef99ddSAndreas Gohr    {
345*d5ef99ddSAndreas Gohr        $regexExcludeDesktopFragment = \implode('|', [
346*d5ef99ddSAndreas Gohr            'CE-HTML',
347*d5ef99ddSAndreas Gohr            ' Mozilla/|Andr[o0]id|Tablet|Mobile|iPhone|Windows Phone|ricoh|OculusBrowser',
348*d5ef99ddSAndreas Gohr            'PicoBrowser|Lenovo|compatible; MSIE|Trident/|Tesla/|XBOX|FBMD/|ARM; ?([^)]+)',
349*d5ef99ddSAndreas Gohr        ]);
350*d5ef99ddSAndreas Gohr
351*d5ef99ddSAndreas Gohr        return
352*d5ef99ddSAndreas Gohr            $this->matchUserAgent('(?:Windows (?:NT|IoT)|X11; Linux x86_64)') &&
353*d5ef99ddSAndreas Gohr            !$this->matchUserAgent($regexExcludeDesktopFragment);
354*d5ef99ddSAndreas Gohr    }
355*d5ef99ddSAndreas Gohr
356*d5ef99ddSAndreas Gohr    /**
357*d5ef99ddSAndreas Gohr     * Returns if the parsed UA contains the 'Android 10 K;' or Android 10 K Build/` fragment
358*d5ef99ddSAndreas Gohr     *
359*d5ef99ddSAndreas Gohr     * @return bool
360*d5ef99ddSAndreas Gohr     */
361*d5ef99ddSAndreas Gohr    protected function hasUserAgentClientHintsFragment(): bool
362*d5ef99ddSAndreas Gohr    {
363*d5ef99ddSAndreas Gohr        return (bool) \preg_match('~Android (?:10[.\d]*; K(?: Build/|[;)])|1[1-5]\)) AppleWebKit~i', $this->userAgent);
364*d5ef99ddSAndreas Gohr    }
365*d5ef99ddSAndreas Gohr
366*d5ef99ddSAndreas Gohr    /**
367*d5ef99ddSAndreas Gohr     * Matches the useragent against the given regex
368*d5ef99ddSAndreas Gohr     *
369*d5ef99ddSAndreas Gohr     * @param string $regex
370*d5ef99ddSAndreas Gohr     *
371*d5ef99ddSAndreas Gohr     * @return ?array
372*d5ef99ddSAndreas Gohr     *
373*d5ef99ddSAndreas Gohr     * @throws \Exception
374*d5ef99ddSAndreas Gohr     */
375*d5ef99ddSAndreas Gohr    protected function matchUserAgent(string $regex): ?array
376*d5ef99ddSAndreas Gohr    {
377*d5ef99ddSAndreas Gohr        $matches = [];
378*d5ef99ddSAndreas Gohr
379*d5ef99ddSAndreas Gohr        // only match if useragent begins with given regex or there is no letter before it
380*d5ef99ddSAndreas Gohr        $regex = '/(?:^|[^A-Z0-9_-]|[^A-Z0-9-]_|sprd-|MZ-)(?:' . \str_replace('/', '\/', $regex) . ')/i';
381*d5ef99ddSAndreas Gohr
382*d5ef99ddSAndreas Gohr        try {
383*d5ef99ddSAndreas Gohr            if (\preg_match($regex, $this->userAgent, $matches)) {
384*d5ef99ddSAndreas Gohr                return $matches;
385*d5ef99ddSAndreas Gohr            }
386*d5ef99ddSAndreas Gohr        } catch (\Exception $exception) {
387*d5ef99ddSAndreas Gohr            throw new \Exception(
388*d5ef99ddSAndreas Gohr                \sprintf("%s\nRegex: %s", $exception->getMessage(), $regex),
389*d5ef99ddSAndreas Gohr                $exception->getCode(),
390*d5ef99ddSAndreas Gohr                $exception
391*d5ef99ddSAndreas Gohr            );
392*d5ef99ddSAndreas Gohr        }
393*d5ef99ddSAndreas Gohr
394*d5ef99ddSAndreas Gohr        return null;
395*d5ef99ddSAndreas Gohr    }
396*d5ef99ddSAndreas Gohr
397*d5ef99ddSAndreas Gohr    /**
398*d5ef99ddSAndreas Gohr     * @param string $item
399*d5ef99ddSAndreas Gohr     * @param array  $matches
400*d5ef99ddSAndreas Gohr     *
401*d5ef99ddSAndreas Gohr     * @return string
402*d5ef99ddSAndreas Gohr     */
403*d5ef99ddSAndreas Gohr    protected function buildByMatch(string $item, array $matches): string
404*d5ef99ddSAndreas Gohr    {
405*d5ef99ddSAndreas Gohr        $search  = [];
406*d5ef99ddSAndreas Gohr        $replace = [];
407*d5ef99ddSAndreas Gohr
408*d5ef99ddSAndreas Gohr        for ($nb = 1; $nb <= \count($matches); $nb++) {
409*d5ef99ddSAndreas Gohr            $search[]  = '$' . $nb;
410*d5ef99ddSAndreas Gohr            $replace[] = $matches[$nb] ?? '';
411*d5ef99ddSAndreas Gohr        }
412*d5ef99ddSAndreas Gohr
413*d5ef99ddSAndreas Gohr        return \trim(\str_replace($search, $replace, $item));
414*d5ef99ddSAndreas Gohr    }
415*d5ef99ddSAndreas Gohr
416*d5ef99ddSAndreas Gohr    /**
417*d5ef99ddSAndreas Gohr     * Builds the version with the given $versionString and $matches
418*d5ef99ddSAndreas Gohr     *
419*d5ef99ddSAndreas Gohr     * Example:
420*d5ef99ddSAndreas Gohr     * $versionString = 'v$2'
421*d5ef99ddSAndreas Gohr     * $matches = ['version_1_0_1', '1_0_1']
422*d5ef99ddSAndreas Gohr     * return value would be v1.0.1
423*d5ef99ddSAndreas Gohr     *
424*d5ef99ddSAndreas Gohr     * @param string $versionString
425*d5ef99ddSAndreas Gohr     * @param array  $matches
426*d5ef99ddSAndreas Gohr     *
427*d5ef99ddSAndreas Gohr     * @return string
428*d5ef99ddSAndreas Gohr     */
429*d5ef99ddSAndreas Gohr    protected function buildVersion(string $versionString, array $matches): string
430*d5ef99ddSAndreas Gohr    {
431*d5ef99ddSAndreas Gohr        $versionString = $this->buildByMatch($versionString, $matches);
432*d5ef99ddSAndreas Gohr        $versionString = \str_replace('_', '.', $versionString);
433*d5ef99ddSAndreas Gohr
434*d5ef99ddSAndreas Gohr        if (self::VERSION_TRUNCATION_NONE !== static::$maxMinorParts
435*d5ef99ddSAndreas Gohr            && \substr_count($versionString, '.') > static::$maxMinorParts
436*d5ef99ddSAndreas Gohr        ) {
437*d5ef99ddSAndreas Gohr            $versionParts  = \explode('.', $versionString);
438*d5ef99ddSAndreas Gohr            $versionParts  = \array_slice($versionParts, 0, 1 + static::$maxMinorParts);
439*d5ef99ddSAndreas Gohr            $versionString = \implode('.', $versionParts);
440*d5ef99ddSAndreas Gohr        }
441*d5ef99ddSAndreas Gohr
442*d5ef99ddSAndreas Gohr        return \trim($versionString, ' .');
443*d5ef99ddSAndreas Gohr    }
444*d5ef99ddSAndreas Gohr
445*d5ef99ddSAndreas Gohr    /**
446*d5ef99ddSAndreas Gohr     * Tests the useragent against a combination of all regexes
447*d5ef99ddSAndreas Gohr     *
448*d5ef99ddSAndreas Gohr     * All regexes returned by getRegexes() will be reversed and concatenated with '|'
449*d5ef99ddSAndreas Gohr     * Afterwards the big regex will be tested against the user agent
450*d5ef99ddSAndreas Gohr     *
451*d5ef99ddSAndreas Gohr     * Method can be used to speed up detections by making a big check before doing checks for every single regex
452*d5ef99ddSAndreas Gohr     *
453*d5ef99ddSAndreas Gohr     * @return ?array
454*d5ef99ddSAndreas Gohr     */
455*d5ef99ddSAndreas Gohr    protected function preMatchOverall(): ?array
456*d5ef99ddSAndreas Gohr    {
457*d5ef99ddSAndreas Gohr        $regexes = $this->getRegexes();
458*d5ef99ddSAndreas Gohr
459*d5ef99ddSAndreas Gohr        $cacheKey = $this->parserName . DeviceDetector::VERSION . '-all';
460*d5ef99ddSAndreas Gohr        $cacheKey = (string) \preg_replace('/([^a-z0-9_-]+)/i', '', $cacheKey);
461*d5ef99ddSAndreas Gohr
462*d5ef99ddSAndreas Gohr        if (empty($this->overAllMatch)) {
463*d5ef99ddSAndreas Gohr            $overAllMatch = $this->getCache()->fetch($cacheKey);
464*d5ef99ddSAndreas Gohr
465*d5ef99ddSAndreas Gohr            if (\is_string($overAllMatch)) {
466*d5ef99ddSAndreas Gohr                $this->overAllMatch = $overAllMatch;
467*d5ef99ddSAndreas Gohr            }
468*d5ef99ddSAndreas Gohr        }
469*d5ef99ddSAndreas Gohr
470*d5ef99ddSAndreas Gohr        if (empty($this->overAllMatch)) {
471*d5ef99ddSAndreas Gohr            // reverse all regexes, so we have the generic one first, which already matches most patterns
472*d5ef99ddSAndreas Gohr            $this->overAllMatch = \array_reduce(\array_reverse($regexes), static function ($val1, $val2) {
473*d5ef99ddSAndreas Gohr                return !empty($val1) ? $val1 . '|' . $val2['regex'] : $val2['regex'];
474*d5ef99ddSAndreas Gohr            });
475*d5ef99ddSAndreas Gohr            $this->getCache()->save($cacheKey, $this->overAllMatch);
476*d5ef99ddSAndreas Gohr        }
477*d5ef99ddSAndreas Gohr
478*d5ef99ddSAndreas Gohr        return $this->matchUserAgent($this->overAllMatch);
479*d5ef99ddSAndreas Gohr    }
480*d5ef99ddSAndreas Gohr
481*d5ef99ddSAndreas Gohr    /**
482*d5ef99ddSAndreas Gohr     * Compares if two strings equals after lowering their case and removing spaces
483*d5ef99ddSAndreas Gohr     *
484*d5ef99ddSAndreas Gohr     * @param string $value1
485*d5ef99ddSAndreas Gohr     * @param string $value2
486*d5ef99ddSAndreas Gohr     *
487*d5ef99ddSAndreas Gohr     * @return bool
488*d5ef99ddSAndreas Gohr     */
489*d5ef99ddSAndreas Gohr    protected function fuzzyCompare(string $value1, string $value2): bool
490*d5ef99ddSAndreas Gohr    {
491*d5ef99ddSAndreas Gohr        return \str_replace(' ', '', \strtolower($value1)) ===
492*d5ef99ddSAndreas Gohr            \str_replace(' ', '', \strtolower($value2));
493*d5ef99ddSAndreas Gohr    }
494*d5ef99ddSAndreas Gohr}
495