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