Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
20 / 20
CRAP
100.00% covered (success)
100.00%
1 / 1
UserAgent
100.00% covered (success)
100.00%
78 / 78
100.00% covered (success)
100.00%
20 / 20
44
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 compileData
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setPlatform
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setBrowser
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 setMobile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 setRobot
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getAsString
n/a
0 / 0
n/a
0 / 0
1
 toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBrowser
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBrowserVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMobile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPlatform
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRobot
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBrowser
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isMobile
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 isRobot
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 getType
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getName
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 jsonSerialize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php declare(strict_types=1);
2/*
3 * This file is part of Aplus Framework HTTP Library.
4 *
5 * (c) Natan Felles <natanfelles@gmail.com>
6 *
7 * For the full copyright and license information, please view the LICENSE
8 * file that was distributed with this source code.
9 */
10namespace Framework\HTTP;
11
12use JetBrains\PhpStorm\Deprecated;
13use JetBrains\PhpStorm\Pure;
14
15/**
16 * Class UserAgent.
17 *
18 * @package http
19 */
20class UserAgent implements \JsonSerializable, \Stringable
21{
22    protected ?string $agent = null;
23    protected ?string $browser = null;
24    protected ?string $browserVersion = null;
25    protected ?string $mobile = null;
26    protected ?string $platform = null;
27    protected ?string $robot = null;
28    protected bool $isBrowser = false;
29    protected bool $isMobile = false;
30    protected bool $isRobot = false;
31    /**
32     * @var array<string,array<string,string>>
33     */
34    protected static array $config = [
35        'platforms' => [
36            'windows nt 10.0' => 'Windows 10',
37            'windows nt 6.3' => 'Windows 8.1',
38            'windows nt 6.2' => 'Windows 8',
39            'windows nt 6.1' => 'Windows 7',
40            'windows nt 6.0' => 'Windows Vista',
41            'windows nt 5.2' => 'Windows 2003',
42            'windows nt 5.1' => 'Windows XP',
43            'windows nt 5.0' => 'Windows 2000',
44            'windows nt 4.0' => 'Windows NT 4.0',
45            'winnt4.0' => 'Windows NT 4.0',
46            'winnt 4.0' => 'Windows NT',
47            'winnt' => 'Windows NT',
48            'windows 98' => 'Windows 98',
49            'win98' => 'Windows 98',
50            'windows 95' => 'Windows 95',
51            'win95' => 'Windows 95',
52            'windows phone' => 'Windows Phone',
53            'windows' => 'Unknown Windows OS',
54            'android' => 'Android',
55            'blackberry' => 'BlackBerry',
56            'iphone' => 'iOS',
57            'ipad' => 'iOS',
58            'ipod' => 'iOS',
59            'os x' => 'Mac OS X',
60            'ppc mac' => 'Power PC Mac',
61            'freebsd' => 'FreeBSD',
62            'ppc' => 'Macintosh',
63            'ubuntu' => 'Ubuntu',
64            'debian' => 'Debian',
65            'fedora' => 'Fedora',
66            'linux' => 'Linux',
67            'sunos' => 'Sun Solaris',
68            'beos' => 'BeOS',
69            'apachebench' => 'ApacheBench',
70            'aix' => 'AIX',
71            'irix' => 'Irix',
72            'osf' => 'DEC OSF',
73            'hp-ux' => 'HP-UX',
74            'netbsd' => 'NetBSD',
75            'bsdi' => 'BSDi',
76            'openbsd' => 'OpenBSD',
77            'gnu' => 'GNU/Linux',
78            'unix' => 'Unknown Unix OS',
79            'symbian' => 'Symbian OS',
80        ],
81        // The order of this array should NOT be changed. Many browsers return
82        // multiple browser types so we want to identify the sub-type first.
83        'browsers' => [
84            'curl' => 'Curl',
85            'PostmanRuntime' => 'Postman',
86            'OPR' => 'Opera',
87            'Flock' => 'Flock',
88            'Edge' => 'Spartan',
89            'Edg' => 'Edge',
90            'EdgA' => 'Edge',
91            'Chrome' => 'Chrome',
92            // Opera 10+ always reports Opera/9.80 and appends
93            // Version/<real version> to the user agent string
94            'Opera.*?Version' => 'Opera',
95            'Opera' => 'Opera',
96            'MSIE' => 'Internet Explorer',
97            'Internet Explorer' => 'Internet Explorer',
98            'Trident.* rv' => 'Internet Explorer',
99            'Shiira' => 'Shiira',
100            'Firefox' => 'Firefox',
101            'Chimera' => 'Chimera',
102            'Phoenix' => 'Phoenix',
103            'Firebird' => 'Firebird',
104            'Camino' => 'Camino',
105            'Netscape' => 'Netscape',
106            'OmniWeb' => 'OmniWeb',
107            'Safari' => 'Safari',
108            'Mozilla' => 'Mozilla',
109            'Konqueror' => 'Konqueror',
110            'icab' => 'iCab',
111            'Lynx' => 'Lynx',
112            'Links' => 'Links',
113            'hotjava' => 'HotJava',
114            'amaya' => 'Amaya',
115            'IBrowse' => 'IBrowse',
116            'Maxthon' => 'Maxthon',
117            'Ubuntu' => 'Ubuntu Web Browser',
118            'Vivaldi' => 'Vivaldi',
119        ],
120        'mobiles' => [
121            'mobileexplorer' => 'Mobile Explorer',
122            'palmsource' => 'Palm',
123            'palmscape' => 'Palmscape',
124            // Phones and Manufacturers
125            'motorola' => 'Motorola',
126            'nokia' => 'Nokia',
127            'palm' => 'Palm',
128            'iphone' => 'Apple iPhone',
129            'ipad' => 'iPad',
130            'ipod' => 'Apple iPod Touch',
131            'sony' => 'Sony Ericsson',
132            'ericsson' => 'Sony Ericsson',
133            'blackberry' => 'BlackBerry',
134            'cocoon' => 'O2 Cocoon',
135            'blazer' => 'Treo',
136            'lg' => 'LG',
137            'amoi' => 'Amoi',
138            'xda' => 'XDA',
139            'mda' => 'MDA',
140            'vario' => 'Vario',
141            'htc' => 'HTC',
142            'samsung' => 'Samsung',
143            'sharp' => 'Sharp',
144            'sie-' => 'Siemens',
145            'alcatel' => 'Alcatel',
146            'benq' => 'BenQ',
147            'ipaq' => 'HP iPaq',
148            'mot-' => 'Motorola',
149            'playstation portable' => 'PlayStation Portable',
150            'playstation 3' => 'PlayStation 3',
151            'playstation vita' => 'PlayStation Vita',
152            'hiptop' => 'Danger Hiptop',
153            'nec-' => 'NEC',
154            'panasonic' => 'Panasonic',
155            'philips' => 'Philips',
156            'sagem' => 'Sagem',
157            'sanyo' => 'Sanyo',
158            'spv' => 'SPV',
159            'zte' => 'ZTE',
160            'sendo' => 'Sendo',
161            'nintendo dsi' => 'Nintendo DSi',
162            'nintendo ds' => 'Nintendo DS',
163            'nintendo 3ds' => 'Nintendo 3DS',
164            'wii' => 'Nintendo Wii',
165            'open web' => 'Open Web',
166            'openweb' => 'OpenWeb',
167            // Operating Systems
168            'android' => 'Android',
169            'symbian' => 'Symbian',
170            'SymbianOS' => 'SymbianOS',
171            'elaine' => 'Palm',
172            'series60' => 'Symbian S60',
173            'windows ce' => 'Windows CE',
174            // Browsers
175            'obigo' => 'Obigo',
176            'netfront' => 'Netfront Browser',
177            'openwave' => 'Openwave Browser',
178            'mobilexplorer' => 'Mobile Explorer',
179            'operamini' => 'Opera Mini',
180            'opera mini' => 'Opera Mini',
181            'opera mobi' => 'Opera Mobile',
182            'fennec' => 'Firefox Mobile',
183            // Other
184            'digital paths' => 'Digital Paths',
185            'avantgo' => 'AvantGo',
186            'xiino' => 'Xiino',
187            'novarra' => 'Novarra Transcoder',
188            'vodafone' => 'Vodafone',
189            'docomo' => 'NTT DoCoMo',
190            'o2' => 'O2',
191            // Fallback
192            'mobile' => 'Generic Mobile',
193            'wireless' => 'Generic Mobile',
194            'j2me' => 'Generic Mobile',
195            'midp' => 'Generic Mobile',
196            'cldc' => 'Generic Mobile',
197            'up.link' => 'Generic Mobile',
198            'up.browser' => 'Generic Mobile',
199            'smartphone' => 'Generic Mobile',
200            'cellphone' => 'Generic Mobile',
201        ],
202        'robots' => [
203            'googlebot' => 'Googlebot',
204            'msnbot' => 'MSNBot',
205            'baiduspider' => 'Baiduspider',
206            'bingbot' => 'Bing',
207            'slurp' => 'Inktomi Slurp',
208            'yahoo' => 'Yahoo',
209            'ask jeeves' => 'Ask Jeeves',
210            'fastcrawler' => 'FastCrawler',
211            'infoseek' => 'InfoSeek Robot 1.0',
212            'lycos' => 'Lycos',
213            'yandex' => 'YandexBot',
214            'mediapartners-google' => 'MediaPartners Google',
215            'CRAZYWEBCRAWLER' => 'Crazy Webcrawler',
216            'adsbot-google' => 'AdsBot Google',
217            'feedfetcher-google' => 'Feedfetcher Google',
218            'curious george' => 'Curious George',
219            'ia_archiver' => 'Alexa Crawler',
220            'MJ12bot' => 'Majestic-12',
221            'Uptimebot' => 'Uptimebot',
222        ],
223    ];
224
225    /**
226     * UserAgent constructor.
227     *
228     * @param string $userAgent User-Agent string
229     */
230    public function __construct(string $userAgent)
231    {
232        $this->parse($userAgent);
233    }
234
235    #[Pure]
236    public function __toString() : string
237    {
238        return $this->toString();
239    }
240
241    /**
242     * @param string $string
243     *
244     * @return static
245     */
246    protected function parse(string $string) : static
247    {
248        $this->isBrowser = false;
249        $this->isRobot = false;
250        $this->isMobile = false;
251        $this->browser = null;
252        $this->browserVersion = null;
253        $this->mobile = null;
254        $this->robot = null;
255        $this->agent = $string;
256        $this->compileData();
257        return $this;
258    }
259
260    protected function compileData() : void
261    {
262        $this->setPlatform();
263        foreach (['setRobot', 'setBrowser', 'setMobile'] as $function) {
264            if ($this->{$function}()) {
265                break;
266            }
267        }
268    }
269
270    protected function setPlatform() : bool
271    {
272        foreach (static::$config['platforms'] as $key => $val) {
273            if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) {
274                $this->platform = $val;
275                return true;
276            }
277        }
278        return false;
279    }
280
281    protected function setBrowser() : bool
282    {
283        foreach (static::$config['browsers'] as $key => $val) {
284            if (\preg_match(
285                '#' . \preg_quote($key, '#') . '.*?([0-9\.]+)#i',
286                $this->agent,
287                $match
288            )) {
289                $this->isBrowser = true;
290                $this->browserVersion = $match[1];
291                $this->browser = $val;
292                $this->setMobile();
293                return true;
294            }
295        }
296        return false;
297    }
298
299    protected function setMobile() : bool
300    {
301        foreach (static::$config['mobiles'] as $key => $val) {
302            if (\stripos($this->agent, $key) !== false) {
303                $this->isMobile = true;
304                $this->mobile = $val;
305                return true;
306            }
307        }
308        return false;
309    }
310
311    protected function setRobot() : bool
312    {
313        foreach (static::$config['robots'] as $key => $val) {
314            if (\preg_match('#' . \preg_quote($key, '#') . '#i', $this->agent)) {
315                $this->isRobot = true;
316                $this->robot = $val;
317                $this->setMobile();
318                return true;
319            }
320        }
321        return false;
322    }
323
324    /**
325     * @return string
326     *
327     * @deprecated Use {@see UserAgent::toString()}
328     *
329     * @codeCoverageIgnore
330     */
331    #[Deprecated(
332        reason: 'since HTTP Library version 5.3, use toString() instead',
333        replacement: '%class%->toString()'
334    )]
335    public function getAsString() : string
336    {
337        \trigger_error(
338            'Method ' . __METHOD__ . ' is deprecated',
339            \E_USER_DEPRECATED
340        );
341        return $this->toString();
342    }
343
344    /**
345     * Get the User-Agent as string.
346     *
347     * @since 5.3
348     *
349     * @return string
350     */
351    #[Pure]
352    public function toString() : string
353    {
354        return $this->agent;
355    }
356
357    /**
358     * Gets the Browser name.
359     *
360     * @return string|null
361     */
362    #[Pure]
363    public function getBrowser() : ?string
364    {
365        return $this->browser;
366    }
367
368    /**
369     * Gets the Browser Version.
370     *
371     * @return string|null
372     */
373    #[Pure]
374    public function getBrowserVersion() : ?string
375    {
376        return $this->browserVersion;
377    }
378
379    /**
380     * Gets the Mobile device.
381     *
382     * @return string|null
383     */
384    #[Pure]
385    public function getMobile() : ?string
386    {
387        return $this->mobile;
388    }
389
390    /**
391     * Gets the OS Platform.
392     *
393     * @return string|null
394     */
395    #[Pure]
396    public function getPlatform() : ?string
397    {
398        return $this->platform;
399    }
400
401    /**
402     * Gets the Robot name.
403     *
404     * @return string|null
405     */
406    #[Pure]
407    public function getRobot() : ?string
408    {
409        return $this->robot;
410    }
411
412    /**
413     * Is Browser.
414     *
415     * @param string|null $key
416     *
417     * @return bool
418     */
419    #[Pure]
420    public function isBrowser(string $key = null) : bool
421    {
422        if ($key === null || $this->isBrowser === false) {
423            return $this->isBrowser;
424        }
425        $config = static::$config['browsers'] ?? [];
426        return isset($config[$key])
427            && $this->browser === $config[$key];
428    }
429
430    /**
431     * Is Mobile.
432     *
433     * @param string|null $key
434     *
435     * @return bool
436     */
437    #[Pure]
438    public function isMobile(string $key = null) : bool
439    {
440        if ($key === null || $this->isMobile === false) {
441            return $this->isMobile;
442        }
443        $config = static::$config['mobiles'] ?? [];
444        return isset($config[$key])
445            && $this->mobile === $config[$key];
446    }
447
448    /**
449     * Is Robot.
450     *
451     * @param string|null $key
452     *
453     * @return bool
454     */
455    #[Pure]
456    public function isRobot(string $key = null) : bool
457    {
458        if ($key === null || $this->isRobot === false) {
459            return $this->isRobot;
460        }
461        $config = static::$config['robots'] ?? [];
462        return isset($config[$key])
463            && $this->robot === $config[$key];
464    }
465
466    #[Pure]
467    public function getType() : string
468    {
469        if ($this->isBrowser()) {
470            return 'Browser';
471        }
472        if ($this->isRobot()) {
473            return 'Robot';
474        }
475        return 'Unknown';
476    }
477
478    #[Pure]
479    public function getName() : string
480    {
481        if ($this->isBrowser()) {
482            return $this->getBrowser();
483        }
484        if ($this->isRobot()) {
485            return $this->getRobot();
486        }
487        return 'Unknown';
488    }
489
490    #[Pure]
491    public function jsonSerialize() : string
492    {
493        return $this->toString();
494    }
495}