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