Passed
Push — main ( e64338...9a8bf5 )
by Will
12:22
created

agentzero::getTokens()   A

Complexity

Conditions 6
Paths 5

Size

Total Lines 31
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 31
rs 9.2222
cc 6
nc 5
nop 2
1
<?php
2
declare(strict_types = 1);
3
namespace hexydec\agentzero;
4
5
class agentzero {
6
7
	// categories
8
	public readonly ?string $type;
9
	public readonly ?string $category;
10
11
	// device
12
	public readonly ?string $vendor;
13
	public readonly ?string $device;
14
	public readonly ?string $model;
15
	public readonly ?string $build;
16
17
	// architecture
18
	public readonly ?string $processor;
19
	public readonly ?string $architecture;
20
	public readonly ?int $bits;
21
22
	// platform
23
	public readonly ?string $kernel;
24
	public readonly ?string $platform;
25
	public readonly ?string $platformversion;
26
27
	// browser
28
	public readonly ?string $engine;
29
	public readonly ?string $engineversion;
30
	public readonly ?string $browser;
31
	public readonly ?string $browserversion;
32
	public readonly ?string $language;
33
34
	// app
35
	public readonly ?string $app;
36
	public readonly ?string $appversion;
37
	public readonly ?string $url;
38
39
	// network
40
	public readonly ?string $proxy;
41
42
	/**
43
	 * Constructs a new AgentZero object, private because it can only be created internally
44
	 * 
45
	 * @param \stdClass $data A stdClass object containing the UA details
46
	 */
47
	private function __construct(\stdClass $data) {
48
		foreach (\array_keys(\get_class_vars(__CLASS__)) AS $key) {
49
			$this->{$key} = $data->{$key} ?? null;
50
		}
51
	}
52
53
	/**
54
	 * Retrieves calculated properties
55
	 * 
56
	 * @param string $key The name of the property to retrieve
57
	 * @return string|int|null The requested property or null if it doesn't exist
58
	 */
59
	public function __get(string $key) : string|int|null {
60
		switch ($key) {
61
			case 'host':
62
				if ($this->url !== null && ($host = \parse_url($this->url, PHP_URL_HOST)) !== null) {
63
					return \str_starts_with($host, 'www.') ? \substr($host, 4) : $host;
64
				}
65
				return null;
66
			case 'browsermajorversion':
67
			case 'enginemajorversion':
68
			case 'platformmajorversion':
69
			case 'appmajorversion':
70
				$item = \str_replace('major', '', $key);
71
				$value = $this->{$item} ?? null;
72
				return $value === null ? null : \intval(\substr($value, 0, \strspn($value, '0123456789')));
73
		}
74
		return $this->{$key} ?? null;
75
	}
76
77
	/**
78
	 * Extracts tokens from a UA string
79
	 * 
80
	 * @param string $ua The User Agent string to be tokenised
81
	 * @param array $config An array of configuration values
82
	 * @return array|false An array of tokens, or false if no tokens could be extracted
83
	 */
84
	protected static function getTokens(string $ua, array $config) : array|false {
85
86
		// prepare regexp
87
		$single = \implode('|', \array_map('preg_quote', $config['single']));
88
		$pattern = '/[^()\[\];\/ _-](?:(?<!'.$single.') (?!https?:\/\/)|[^()\[\];\/ ]*)*[^()\[\];\/ _-](?:\/[^;()\[\] ]++)?/i';
89
90
		// split up ua string
91
		if (\preg_match_all($pattern, $ua, $match)) {
92
93
			// remove ignore values
94
			$tokens = \array_diff($match[0], $config['ignore']);
95
96
			// special case for handling like
97
			foreach ($tokens AS $key => $item) {
98
				if (\str_starts_with($item, 'like ')) {
99
100
					// chop off words up to a useful token e.g. Platform/Version
101
					if (\str_contains($item, '/') && ($pos = \mb_strrpos($item, ' ')) !== false) {
102
						$tokens[$key] = \mb_substr($item, $pos + 1);
103
104
					// just remove the token
105
					} else {
106
						unset($tokens[$key]);
107
					}
108
				}
109
			}
110
111
			// rekey and return
112
			return \array_values($tokens);
113
		}
114
		return false;
115
	}
116
117
	/**
118
	 * Parses a User Agent string
119
	 * 
120
	 * @param string $ua The User Agent string to be parsed
121
	 * @return agentzero|false An agentzero object containing the parsed values of the input UA, or false if it could not be parsed
122
	 */
123
	public static function parse(string $ua) : agentzero|false {
124
		$config = config::get();
125
		if (($tokens = self::getTokens($ua, $config)) !== false) {
126
127
			// extract UA info
128
			$browser = new \stdClass();
129
			foreach ($config['match'] AS $key => $item) {
130
				$keylower = \mb_strtolower($key);
131
				foreach ($tokens AS $i => $token) {
132
					$tokenlower = \mb_strtolower($token);
133
					switch ($item['match']) {
134
135
						// match from start of string
136
						case 'start':
137
							if (\str_starts_with($tokenlower, $keylower)) {
138
								self::setProps($browser, $item['categories'], $token, $i, $tokens);
139
							}
140
							break;
141
142
						// match anywhere in the string
143
						case 'any':
144
							if (\str_contains($tokenlower, $keylower)) {
145
								self::setProps($browser, $item['categories'], $token, $i, $tokens);
146
							}
147
							break;
148
149
						// match anywhere in the string
150
						case 'exact':
151
							if ($tokenlower === $keylower) {
152
								self::setProps($browser, $item['categories'], $token, $i, $tokens);
153
								break; // don't match this token again
154
							}
155
							break;
156
					}
157
				}
158
			}
159
			return new agentzero($browser);
160
		}
161
		return false;
162
	}
163
164
	/**
165
	 * Sets parsed UA properties, and calls callbacks to generate properties and sets them to the output object
166
	 * 
167
	 * @param \stdClass $browser A stdClass object to which the properties will be set
168
	 * @param array|\Closure $props An array of properties or a Closure to generate properties
169
	 * @param string $value The current token value
170
	 * @param int $i The ID of the current token
171
	 * @param array $tokens The tokens array
172
	 * @return void
173
	 */
174
	protected static function setProps(\stdClass $browser, array|\Closure $props, string $value, int $i, array $tokens) : void {
175
		if ($props instanceof \Closure) {
0 ignored issues
show
introduced by
$props is never a sub-type of Closure.
Loading history...
176
			$props = $props($value, $i, $tokens);
177
		}
178
		if (\is_array($props)) {
0 ignored issues
show
introduced by
The condition is_array($props) is always true.
Loading history...
179
			foreach ($props AS $key => $item) {
180
				if ($item !== null && !isset($browser->{$key})) {
181
					$browser->{$key} = $item;
182
				}
183
			}
184
		}
185
	}
186
}