1<?php 2 3/** 4 * Device Detector - The Universal Device Detection library for parsing User Agents 5 * 6 * @link https://matomo.org 7 * 8 * @license http://www.gnu.org/licenses/lgpl.html LGPL v3 or later 9 */ 10 11declare(strict_types=1); 12 13namespace DeviceDetector; 14 15class ClientHints 16{ 17 /** 18 * Represents `Sec-CH-UA-Arch` header field: The underlying architecture's instruction set 19 * 20 * @var string 21 */ 22 protected $architecture = ''; 23 24 /** 25 * Represents `Sec-CH-UA-Bitness` header field: The underlying architecture's bitness 26 * 27 * @var string 28 */ 29 protected $bitness = ''; 30 31 /** 32 * Represents `Sec-CH-UA-Mobile` header field: whether the user agent should receive a specifically "mobile" UX 33 * 34 * @var bool 35 */ 36 protected $mobile = false; 37 38 /** 39 * Represents `Sec-CH-UA-Model` header field: the user agent's underlying device model 40 * 41 * @var string 42 */ 43 protected $model = ''; 44 45 /** 46 * Represents `Sec-CH-UA-Platform` header field: the platform's brand 47 * 48 * @var string 49 */ 50 protected $platform = ''; 51 52 /** 53 * Represents `Sec-CH-UA-Platform-Version` header field: the platform's major version 54 * 55 * @var string 56 */ 57 protected $platformVersion = ''; 58 59 /** 60 * Represents `Sec-CH-UA-Full-Version` header field: the platform's major version 61 * 62 * @var string 63 */ 64 protected $uaFullVersion = ''; 65 66 /** 67 * Represents `Sec-CH-UA-Full-Version-List` header field: the full version for each brand in its brand list 68 * 69 * @var array 70 */ 71 protected $fullVersionList = []; 72 73 /** 74 * Represents `x-requested-with` header field: Android app id 75 * @var string 76 */ 77 protected $app = ''; 78 79 /** 80 * Represents `Sec-CH-UA-Form-Factors` header field: form factor device type name 81 * 82 * @var array 83 */ 84 protected $formFactors = []; 85 86 /** 87 * Constructor 88 * 89 * @param string $model `Sec-CH-UA-Model` header field 90 * @param string $platform `Sec-CH-UA-Platform` header field 91 * @param string $platformVersion `Sec-CH-UA-Platform-Version` header field 92 * @param string $uaFullVersion `Sec-CH-UA-Full-Version` header field 93 * @param array $fullVersionList `Sec-CH-UA-Full-Version-List` header field 94 * @param bool $mobile `Sec-CH-UA-Mobile` header field 95 * @param string $architecture `Sec-CH-UA-Arch` header field 96 * @param string $bitness `Sec-CH-UA-Bitness` 97 * @param string $app `HTTP_X-REQUESTED-WITH` 98 * @param array $formFactors `Sec-CH-UA-Form-Factors` header field 99 */ 100 public function __construct(string $model = '', string $platform = '', string $platformVersion = '', string $uaFullVersion = '', array $fullVersionList = [], bool $mobile = false, string $architecture = '', string $bitness = '', string $app = '', array $formFactors = []) // phpcs:ignore Generic.Files.LineLength 101 { 102 $this->model = $model; 103 $this->platform = $platform; 104 $this->platformVersion = $platformVersion; 105 $this->uaFullVersion = $uaFullVersion; 106 $this->fullVersionList = $fullVersionList; 107 $this->mobile = $mobile; 108 $this->architecture = $architecture; 109 $this->bitness = $bitness; 110 $this->app = $app; 111 $this->formFactors = $formFactors; 112 } 113 114 /** 115 * Magic method to directly allow accessing the protected properties 116 * 117 * @param string $variable 118 * 119 * @return mixed 120 * 121 * @throws \Exception 122 */ 123 public function __get(string $variable) 124 { 125 if (\property_exists($this, $variable)) { 126 return $this->$variable; 127 } 128 129 throw new \Exception('Invalid ClientHint property requested.'); 130 } 131 132 /** 133 * Returns if the client hints 134 * 135 * @return bool 136 */ 137 public function isMobile(): bool 138 { 139 return $this->mobile; 140 } 141 142 /** 143 * Returns the Architecture 144 * 145 * @return string 146 */ 147 public function getArchitecture(): string 148 { 149 return $this->architecture; 150 } 151 152 /** 153 * Returns the Bitness 154 * 155 * @return string 156 */ 157 public function getBitness(): string 158 { 159 return $this->bitness; 160 } 161 162 /** 163 * Returns the device model 164 * 165 * @return string 166 */ 167 public function getModel(): string 168 { 169 return $this->model; 170 } 171 172 /** 173 * Returns the Operating System 174 * 175 * @return string 176 */ 177 public function getOperatingSystem(): string 178 { 179 return $this->platform; 180 } 181 182 /** 183 * Returns the Operating System version 184 * 185 * @return string 186 */ 187 public function getOperatingSystemVersion(): string 188 { 189 return $this->platformVersion; 190 } 191 192 /** 193 * Returns the Browser name 194 * 195 * @return array<string, string> 196 */ 197 public function getBrandList(): array 198 { 199 if (\is_array($this->fullVersionList) && \count($this->fullVersionList)) { 200 $brands = \array_column($this->fullVersionList, 'brand'); 201 $versions = \array_column($this->fullVersionList, 'version'); 202 203 if (\count($brands) === \count($versions)) { 204 // @phpstan-ignore-next-line 205 return \array_combine($brands, $versions); 206 } 207 } 208 209 return []; 210 } 211 212 /** 213 * Returns the Browser version 214 * 215 * @return string 216 */ 217 public function getBrandVersion(): string 218 { 219 if (!empty($this->uaFullVersion)) { 220 return $this->uaFullVersion; 221 } 222 223 return ''; 224 } 225 226 /** 227 * Returns the Android app id 228 * 229 * @return string 230 */ 231 public function getApp(): string 232 { 233 return $this->app; 234 } 235 236 /** 237 * Returns the formFactor device type name 238 * 239 * @return array 240 */ 241 public function getFormFactors(): array 242 { 243 return $this->formFactors; 244 } 245 246 /** 247 * Factory method to easily instantiate this class using an array containing all available (client hint) headers 248 * 249 * @param array $headers 250 * 251 * @return ClientHints 252 */ 253 public static function factory(array $headers): ClientHints 254 { 255 $model = $platform = $platformVersion = $uaFullVersion = $architecture = $bitness = ''; 256 $app = ''; 257 $mobile = false; 258 $fullVersionList = []; 259 $formFactors = []; 260 261 foreach ($headers as $name => $value) { 262 if (empty($value)) { 263 continue; 264 } 265 266 switch (\str_replace('_', '-', \strtolower((string) $name))) { 267 case 'http-sec-ch-ua-arch': 268 case 'sec-ch-ua-arch': 269 case 'arch': 270 case 'architecture': 271 $architecture = \trim($value, '"'); 272 273 break; 274 case 'http-sec-ch-ua-bitness': 275 case 'sec-ch-ua-bitness': 276 case 'bitness': 277 $bitness = \trim($value, '"'); 278 279 break; 280 case 'http-sec-ch-ua-mobile': 281 case 'sec-ch-ua-mobile': 282 case 'mobile': 283 $mobile = true === $value || '1' === $value || '?1' === $value; 284 285 break; 286 case 'http-sec-ch-ua-model': 287 case 'sec-ch-ua-model': 288 case 'model': 289 $model = \trim($value, '"'); 290 291 break; 292 case 'http-sec-ch-ua-full-version': 293 case 'sec-ch-ua-full-version': 294 case 'uafullversion': 295 $uaFullVersion = \trim($value, '"'); 296 297 break; 298 case 'http-sec-ch-ua-platform': 299 case 'sec-ch-ua-platform': 300 case 'platform': 301 $platform = \trim($value, '"'); 302 303 break; 304 case 'http-sec-ch-ua-platform-version': 305 case 'sec-ch-ua-platform-version': 306 case 'platformversion': 307 $platformVersion = \trim($value, '"'); 308 309 break; 310 case 'brands': 311 if (!empty($fullVersionList)) { 312 break; 313 } 314 // use this only if no other header already set the list 315 case 'fullversionlist': 316 $fullVersionList = \is_array($value) ? $value : $fullVersionList; 317 318 break; 319 case 'http-sec-ch-ua': 320 case 'sec-ch-ua': 321 if (!empty($fullVersionList)) { 322 break; 323 } 324 // use this only if no other header already set the list 325 case 'http-sec-ch-ua-full-version-list': 326 case 'sec-ch-ua-full-version-list': 327 $reg = '/^"([^"]+)"; ?v="([^"]+)"(?:, )?/'; 328 $list = []; 329 330 while (\preg_match($reg, $value, $matches)) { 331 $list[] = ['brand' => $matches[1], 'version' => $matches[2]]; 332 $value = \substr($value, \strlen($matches[0])); 333 } 334 335 if (\count($list)) { 336 $fullVersionList = $list; 337 } 338 339 break; 340 case 'http-x-requested-with': 341 case 'x-requested-with': 342 if ('xmlhttprequest' !== \strtolower($value)) { 343 $app = $value; 344 } 345 346 break; 347 case 'formfactors': 348 case 'http-sec-ch-ua-form-factors': 349 case 'sec-ch-ua-form-factors': 350 if (\is_array($value)) { 351 $formFactors = \array_map('\strtolower', $value); 352 } elseif (\preg_match_all('~"([a-z]+)"~i', \strtolower($value), $matches)) { 353 $formFactors = $matches[1]; 354 } 355 356 break; 357 } 358 } 359 360 return new self( 361 $model, 362 $platform, 363 $platformVersion, 364 $uaFullVersion, 365 $fullVersionList, 366 $mobile, 367 $architecture, 368 $bitness, 369 $app, 370 $formFactors 371 ); 372 } 373} 374