Passed
Push — main ( bcdb57...ad9cc7 )
by Will
03:05
created

apps::getApp()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 32
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 3

Importance

Changes 0
Metric Value
cc 3
eloc 29
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 32
ccs 31
cts 31
cp 1
crap 3
rs 9.456
1
<?php
2
declare(strict_types = 1);
3
namespace hexydec\agentzero;
4
5
class apps {
6
7
	/**
8
	 * Generates a configuration array for matching apps
9
	 * 
10
	 * @return array<string,props> An array with keys representing the string to match, and values a props object defining how to generate the match and which properties to set
11
	 */
12 49
	public static function get() : array {
13 1
		$fn = [
14 1
			'appslash' => function (string $value, int $i, array $tokens, string $match) : array {
15 49
				if (\mb_stripos($value, 'AppleWebKit') !== 0 && !\str_contains($value, '://')) {
16 24
					$parts = \explode('/', $value, 4);
17 24
					$offset = isset($parts[2]) ? 1 : 0;
18 24
					$app = $parts[0 + $offset];
19 24
					if (\mb_stripos($app, \rtrim($match, '/')) !== false) { // check the match is before the slash
20 24
						return [
21 24
							'type' => 'human',
22 24
							'app' => self::getApp($app),
23 24
							'appname' => \trim($app, '[]'),
24 24
							'appversion' => $parts[1 + $offset] ?? null
25 24
						];
26
					}
27
				}
28 38
				return [];
29 1
			},
30 1
		];
31 3
		return [
32 3
			'com.google.' => new props('start', function (string $value) : array {
33 1
				$parts = \explode('/', $value, 3);
34 1
				return [
35 1
					'type' => 'human',
36 1
					'app' => self::getApp($parts[0]),
37 1
					'appname' => $parts[0],
38 1
					'appversion' => $parts[1] ?? null
39 1
				];
40 3
			}),
41 3
			'OcIdWebView' => new props('exact', function (string $value) : array {
42 1
				$data = [
43 1
					'app' => 'Google App Web View',
44 1
					'appname' => $value
45 1
				];
46 1
				return $data;
47 3
			}),
48 3
			'Instagram' => new props('any', function (string $value, int $i, array $tokens) : array {
49 8
				$parts = \explode(' ', $value, 4);
50 8
				$data = [
51 8
					'type' => 'human',
52 8
					'app' => 'Instagram',
53 8
					'appname' => 'Instagram',
54 8
					'appversion' => $parts[1] ?? null,
55 8
					'platform' => empty($parts[2]) ? null : platforms::getPlatform($parts[2])
56 8
				];
57 8
				foreach (\array_slice($tokens, $i + 1) AS $key => $item) {
58 8
					$ipad = null;
59 8
					if (($ipados = \str_starts_with($item, 'iPadOS')) || \str_starts_with($item, 'iOS ')) {
60 2
						$data['kernel'] = 'Linux';
61 2
						$data['platform'] = 'iOS';
62 2
						$data['platformversion'] = \str_replace('_', '.', \mb_substr($item, $ipados ? 7 : 4));
63 2
						$data['density'] = \floatval(\mb_substr($item, 6));
64 8
					} elseif (($iphone = \str_starts_with($item, 'iPhone')) || ($ipad = \str_starts_with($item, 'iPad')) || \str_starts_with($item, 'iPod')) {
65 2
						$data['vendor'] = 'Apple';
66 2
						$data['category'] = $ipad ? 'tablet' : 'mobile';
67 2
						$data['device'] = $iphone ? 'iPhone' : ($ipad ? 'iPad' : 'iPod');
68 2
						$data['model'] = \str_replace(',', '.', \mb_substr($item, $iphone ? 6 : 4));
69 2
						$data['architecture'] = 'arm';
70 2
						$data['bits'] = 64;
71 8
					} elseif (\str_starts_with($item, 'scale=')) {
72 2
						$data['density'] = \floatval(\mb_substr($item, 6));
73 8
					} elseif (\str_ends_with($item, 'dpi')) {
74 7
						$data['dpi'] = \intval(\mb_substr($item, 0, -3));
75 8
					} elseif (\str_contains($item, 'x') && \strspn($item, '0123456789x') === \strlen($item)) {
76 8
						list($data['width'], $data['height']) = \array_map('intval', \explode('x', $item, 2));
77
78
						// get device when the UA string starts with "Instagram"
79 8
						if ($i === 0 && !isset($data['vendor'])) {
80 1
							$device = [
81 1
								\trim(\mb_strstr($tokens[$key + 2] ?? '', '/') ?: $tokens[$key + 2] ?? '', '/ '), // sometimes the vendor name has a / with the correct name after
82 1
								$tokens[$key + 3] ?? null
83 1
							];
84
85
							// remove duplicated name
86 1
							if ($device[1] !== null && \mb_stripos($device[1], $device[0]) === 0) {
87 1
								unset($device[0]);
88
							}
89 1
							$data = \array_merge($data, devices::getDevice(\implode(' ', \array_filter($device))));
90 1
							break;
91
						}
92
					}
93
				}
94 8
				return $data;
95 3
			}),
96 3
			'imoAndroid/' => new props('start', fn (string $value, int $i, array $tokens) : array => \array_merge(
97 3
				isset($tokens[$i + 3]) ? devices::getDevice($tokens[$i + 3]) : [],
98 3
				[
99 3
					'app' => 'imo',
100 3
					'appname' => 'imoAndroid',
101 3
					'appversion' => ($ver = \mb_strstr($value, '/')) === false ? $value : \ltrim($ver, '/'),
102 3
					'platform' => 'Android',
103 3
					'platformversion' => $tokens[$i + 1] ?? null,
104 3
					'cpu' => $tokens[$i + 11] ?? null,
105 3
					'vendor' => isset($tokens[$i + 4]) ? devices::getVendor($tokens[$i + 4]) : null
106 3
				]
107 3
			)),
108 3
			'GSA/' => new props('any', $fn['appslash']),
109 3
			'DuckDuckGo/' => new props('start', $fn['appslash']),
110 3
			'FlipboardProxy/' => new props('start', $fn['appslash']),
111 3
			'Emacs/' => new props('start', $fn['appslash']),
112 3
			'AndroidDownloadManager/' => new props('start', $fn['appslash']),
113 3
			'Google-Read-Aloud' => new props('exact', $fn['appslash']),
114 3
			'Zoom ' => new props('start', fn (string $value) : array => [
115 2
				'app' => 'Zoom',
116 2
				'appname' => 'Zoom',
117 2
				'appversion' => \mb_substr($value, 5)
118 2
			]),
119 3
			'OculusBrowser/' => new props('start', $fn['appslash']),
120 3
			'YaBrowser/' => new props('start', $fn['appslash']),
121 3
			'choqok/' => new props('start', $fn['appslash']),
122 3
			'PowerShell/' => new props('start', $fn['appslash']),
123 3
			'Quora ' => new props('start', fn (string $value) : array => [
124 1
				'type' => 'human',
125 1
				'app' => 'Quora',
126 1
				'appname' => 'Quora',
127 1
				'appversion' => \explode(' ', $value, 3)[1]
128 1
			]),
129 3
			'AmazonKidsBrowser/' => new props('start', $fn['appslash']),
130 3
			'Teams/' => new props('start', $fn['appslash']),
131 3
			'Viber/' => new props('start', $fn['appslash']),
132 3
			'AppleExchangeWebServices/' => new props('start', $fn['appslash']),
133 3
			'Google Web Preview' => new props('start', $fn['appslash']),
134 3
			'MicroMessenger/' => new props('start', $fn['appslash']),
135 3
			'weibo' => new props('any', function (string $value) : array {
136 1
				$data = [
137 1
					'app' => 'Weibo',
138 1
					'appname' => 'Weibo'
139 1
				];
140 1
				$parts = \explode('_', $value);
141 1
				foreach ($parts AS $i => $item) {
142 1
					if (\mb_stripos($item, 'Weibo') !== false) {
143 1
						$data['appname'] = $item;
144 1
						$data['appversion'] = $parts[$i + (\strspn($parts[$i + 1] ?? '', '0123456789', 0, 1) === 1 ? 1 : 2)] ?? null;
145 1
						break;
146
					}
147
				}
148 1
				return $data;
149 3
			}),
150
151
			// special parser for Facebook app because it is completely different to any other
152 3
			'FBAN/' => new props('any', function (string $value) : array {
153 4
				$map = [
154 4
					'FBAN/MessengerLiteForiOS' => [
155 4
						'type' => 'human',
156 4
						'app' => 'Facebook Messenger',
157 4
						'platform' => 'iOS'
158 4
					],
159 4
					'FBAN/FB4A' => [
160 4
						'type' => 'human',
161 4
						'app' => 'Facebook',
162 4
						'platform' => 'Android'
163 4
					],
164 4
					'FBAN/FBIOS' => [
165 4
						'type' => 'human',
166 4
						'app' => 'Facebook',
167 4
						'platform' => 'iOS'
168 4
					],
169 4
					'FBAN/FB4FireTV' => [
170 4
						'type' => 'human',
171 4
						'category' => 'tv',
172 4
						'app' => 'Facebook',
173 4
						'platform' => 'Android'
174 4
					],
175 4
					'FBAN/MessengerDesktop' => [
176 4
						'type' => 'human',
177 4
						'category' => 'desktop',
178 4
						'app' => 'Facebook Messenger'
179 4
					],
180 4
					'FacebookCanvasDesktop FBAN/GamesWindowsDesktopApp' => [
181 4
						'type' => 'human',
182 4
						'platform' => 'Windows',
183 4
						'category' => 'desktop',
184 4
						'app' => 'Facebook Gamesroom'
185 4
					]
186 4
				];
187 4
				return \array_merge([
188 4
					'type' => 'human',
189 4
					'app' => 'Facebook',
190 4
					'appname' => \explode('/', $value, 2)[1]
191 4
				], $map[$value] ?? []);
192 3
			}),
193 3
			'FB_IAB/' => new props('start', fn (string $value) : array => [
194 7
				'app' => 'Facebook',
195 7
				'appname' => \mb_substr($value, 7)
196 7
			]),
197 3
			'FBAV/' => new props('start', fn (string $value) : array => [
198 9
				'appversion' => \mb_substr($value, 5)
199 9
			]),
200 3
			'FBMF/' => new props('start', fn (string $value) : array => [
201 2
				'vendor' => devices::getVendor(\mb_substr($value, 5))
202 2
			]),
203 3
			'FBDV/' => new props('start', fn (string $value) : array => devices::getDevice(\mb_substr($value, 5))),
204 3
			'FBMD/' => new props('start', fn (string $value) : array => [
205 3
				'model' => \mb_substr($value, 5)
206 3
			]),
207 3
			'FBDM/' => new props('start', function (string $value) : array {
208 1
				$data = [];
209 1
				foreach (\explode(',', \trim(\mb_substr($value, 5), '{}')) AS $item) {
210 1
					$parts = \explode('=', $item);
211 1
					if (!empty($parts[1])) {
212 1
						if (\is_numeric($parts[1])) {
213 1
							$parts[1] = \str_contains($parts[1], '.') ? \floatval($parts[1]) : \intval($parts[1]);
214
						}
215 1
						$data[$parts[0]] = $parts[1];
216
					}
217
				}
218 1
				return $data;
219 3
			}),
220 3
			'width=' => new props('start', fn (string $value) : array => [
221 2
				'width' => \intval(\mb_substr($value, 6))
222 2
			]),
223 3
			'height=' => new props('start', fn (string $value) : array => [
224 2
				'height' => \intval(\mb_substr($value, 7))
225 2
			]),
226 3
			'dpi=' => new props('start', fn (string $value) : array => [
227 1
				'dpi' => \mb_substr($value, 4)
228 1
			]),
229 3
			'FBSN/' => new props('start', fn (string $value) : array => [
230 3
				'platform' => \mb_substr($value, 5)
231 3
			]),
232 3
			'FBSV' => new props('start', fn (string $value) : array => [
233 3
				'platformversion' => \mb_substr($value, 5)
234 3
			]),
235 3
			'isDarkMode/' => new props('start', function (string $value) : array {
236 1
				$mode = \mb_substr($value, 11);
237 1
				return [
238 1
					'darkmode' => \in_array($mode, ['0', '1'], true) ? \boolval($mode) : null
239 1
				];
240 3
			}),
241 3
			'AppTheme/' => new props('start', fn (string $value) : array => [
242 1
				'darkmode' => \mb_substr($value, 9) === 'dark'
243 1
			]),
244 3
			'NetType/' => new props('start', fn (string $value) : array => [
245 5
				'nettype' => \mb_convert_case(\mb_substr($value, 8), MB_CASE_UPPER)
246 5
			]),
247 3
			'Microsoft Office' => new props('start', function (string $value, int $i, array $tokens) : array {
248 1
				$data = [
249 1
					'type' => 'human'
250 1
				];
251 1
				if (\str_contains($value, '/')) {
252 1
					foreach (\array_slice($tokens, $i + 1) AS $item) {
253 1
						if (\str_starts_with($item, 'Microsoft ')) {
254 1
							$parts = \explode(' ', $item);
255 1
							$data['app'] = $parts[0].' '.$parts[1];
256 1
							$data['appname'] = $parts[0].' '.$parts[1];
257 1
							if (isset($parts[2])) {
258 1
								$data['appversion'] = $parts[2];
259
							}
260 1
							break;
261
						}
262
					}
263 1
					if (!isset($data['app'])) {
264 1
						$parts = \explode('/', $value, 2);
265 1
						$data['app'] = $parts[0];
266 1
						$data['appname'] = $parts[0];
267 1
						if (!isset($data['appversion'])) {
268 1
							$data['appversion'] = $parts[1];
269
						}
270
					}
271
				} else {
272 1
					$parts = \explode(' ', $value);
273 1
					$data['app'] = \rtrim($parts[0].' '.($parts[1] ?? '').' '.($parts[2] ?? ''));
274 1
					$data['appname'] = $value;
275 1
					$data['appversion'] = $parts[3] ?? null;
276
				}
277 1
				return $data;
278 3
			}),
279
280
			// TikTok
281 3
			'AppName/' => new props('start', fn(string $value) : array => [
282 3
				'app' => $value === 'AppName/musical_ly' ? 'TikTok' : \mb_substr($value, 8),
283 3
				'appname' => \mb_substr($value, 8)
284 3
			]),
285 3
			'app_version/' => new props('start', fn(string $value) : array => [
286 3
				'appversion' => \mb_substr($value, 12)
287 3
			]),
288 3
			'AppVersion/' => new props('any', fn(string $value) : array => [
289 2
				'appversion' => \explode('/', $value, 2)[1]
290 2
			]),
291 3
			'musical_ly' => new props('start', fn(string $value) : array => [
292 2
				'app' => 'TikTok',
293 2
				'appname' => 'musical_ly',
294 2
				'appversion' => \str_replace('_', '.', \mb_substr(\explode(' ', $value, 2)[0], 11))
295 2
			]),
296
				
297
			// generic
298 3
			'App' => new props('any', $fn['appslash'])
299 3
		];
300
	}
301
302 24
	public static function getApp(string $value) : string {
303 24
		$map = [
304 24
			'YaApp_iOS' => 'Yandex',
305 24
			'YaApp_Android' => 'Yandex',
306 24
			'YaSearchApp' => 'Yandex',
307 24
			'YaBrowser' => 'Yandex',
308 24
			'LinkedInApp' => 'LinkedIn',
309 24
			'[LinkedInApp]' => 'LinkedIn',
310 24
			'GoogleApp' => 'Google',
311 24
			'com.zhiliaoapp.musically' => 'TikTok',
312 24
			'com.google.android.apps.searchlite' => 'Google',
313 24
			'com.google.android.googlequicksearchbox' => 'Google',
314 24
			'com.google.android.gms' => 'Google',
315 24
			'com.google.GoogleMobile' => 'Google',
316 24
			'com.google.Maps' => 'Google Maps',
317 24
			'com.google.photos' => 'Google Photos',
318 24
			'com.google.ios.youtube' => 'YouTube',
319 24
			'com.google.android.youtube' => 'YouTube',
320 24
			'com.google.GoogleMobile' => 'Google',
321 24
			'AlohaBrowserApp' => 'Aloha',
322 24
			'OculusBrowser' => 'Oculus Browser',
323 24
			'AndroidDownloadManager' => 'Android Download Manager',
324 24
			'imoAndroid' => 'imo',
325 24
			'MicroMessenger' => 'WeChat',
326 24
			'GSA' => 'Google'
327 24
		];
328 24
		if (isset($map[$value])) {
329 12
			return $map[$value];
330 14
		} elseif (($pos = \mb_strrpos($value, '.')) !== false) {
331 1
			return \mb_substr($value, $pos + 1);
332
		}
333 14
		return $value;
334
	}
335
}