1
|
|
|
<?php |
2
|
|
|
namespace vipnytt; |
3
|
|
|
|
4
|
|
|
use vipnytt\UserAgentParser\Exceptions\ProductException; |
5
|
|
|
use vipnytt\UserAgentParser\Exceptions\VersionException; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* Class UserAgentParser |
9
|
|
|
* |
10
|
|
|
* @link https://tools.ietf.org/html/rfc7231#section-5.5.3 |
11
|
|
|
* @link https://tools.ietf.org/html/rfc7230 |
12
|
|
|
* |
13
|
|
|
* @package vipnytt |
14
|
|
|
*/ |
15
|
|
|
class UserAgentParser |
16
|
|
|
{ |
17
|
|
|
const RFC_README = 'https://tools.ietf.org/html/rfc7231#section-5.5.3'; |
18
|
|
|
|
19
|
|
|
/** |
20
|
|
|
* Product |
21
|
|
|
* @var string |
22
|
|
|
*/ |
23
|
|
|
private $product; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Version |
27
|
|
|
* @var string |
28
|
|
|
*/ |
29
|
|
|
private $version; |
30
|
|
|
|
31
|
|
|
/** |
32
|
|
|
* Constructor |
33
|
|
|
* |
34
|
|
|
* @param string $product |
35
|
|
|
* @param int|string|null $version |
36
|
|
|
*/ |
37
|
|
|
public function __construct($product, $version = null) |
38
|
|
|
{ |
39
|
|
|
$this->product = $product; |
40
|
|
|
$this->version = $version; |
|
|
|
|
41
|
|
|
if (strpos($this->product, '/') !== false) { |
42
|
|
|
$this->split(); |
43
|
|
|
} |
44
|
|
|
$this->validateProduct(); |
45
|
|
|
$this->validateVersion(); |
46
|
|
|
} |
47
|
|
|
|
48
|
|
|
private function split() |
49
|
|
|
{ |
50
|
|
|
if (count($parts = explode('/', trim($this->product . '/' . $this->version, '/'), 2)) === 2) { |
51
|
|
|
$this->product = $parts[0]; |
52
|
|
|
$this->version = $parts[1]; |
53
|
|
|
} |
54
|
|
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Validate the Product format |
58
|
|
|
* @link https://tools.ietf.org/html/rfc7230#section-3.2.4 |
59
|
|
|
* |
60
|
|
|
* @throws ProductException |
61
|
|
|
*/ |
62
|
|
|
private function validateProduct() |
63
|
|
|
{ |
64
|
|
|
foreach ( |
65
|
|
|
[ |
66
|
|
|
'mozilla', |
67
|
|
|
'compatible', |
68
|
|
|
'(', |
69
|
|
|
')', |
70
|
|
|
' ', |
71
|
|
|
] as $blacklisted |
72
|
|
|
) { |
73
|
|
|
if ( |
74
|
|
|
stripos($this->product, $blacklisted) !== false || |
75
|
|
|
empty($this->product) |
76
|
|
|
) { |
77
|
|
|
throw new ProductException('Invalid product format (`' . $this->product . '`). Examples of valid User-agents: `MyCustomBot`, `MyFetcher-news`, `MyCrawler/2.1` and `MyBot-images/1.2`. See also ' . self::RFC_README); |
78
|
|
|
} |
79
|
|
|
} |
80
|
|
|
if ($this->product !== ($new = preg_replace('/[^\x21-\x7E]/', '', $this->product))) { |
81
|
|
|
trigger_error("Product name contains invalid characters. Truncated to `$new`.", E_USER_WARNING); |
82
|
|
|
} |
83
|
|
|
} |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* Validate the Version and it's format |
87
|
|
|
* |
88
|
|
|
* @throws VersionException |
89
|
|
|
*/ |
90
|
|
|
private function validateVersion() |
91
|
|
|
{ |
92
|
|
|
if ( |
93
|
|
|
$this->version !== null && |
94
|
|
|
( |
95
|
|
|
empty($this->version) || |
96
|
|
|
preg_match('/[^0-9.]/', $this->version) || |
97
|
|
|
version_compare($this->version, '0.0.1', '>=') === false |
98
|
|
|
) |
99
|
|
|
) { |
100
|
|
|
throw new VersionException('Invalid version format (`' . $this->version . '`). See http://semver.org/ for guidelines. In addition, dev/alpha/beta/rc tags is disallowed. See also ' . self::RFC_README); |
101
|
|
|
} |
102
|
|
|
} |
103
|
|
|
|
104
|
|
|
/** |
105
|
|
|
* Find the best matching User-agent |
106
|
|
|
* |
107
|
|
|
* @param string[] $userAgents |
108
|
|
|
* @return string|false |
109
|
|
|
*/ |
110
|
|
|
public function getMostSpecific(array $userAgents) |
111
|
|
|
{ |
112
|
|
|
$array = []; |
113
|
|
|
foreach ($userAgents as $string) { |
114
|
|
|
// Strip non-US-ASCII characters |
115
|
|
|
$array[$string] = strtolower(preg_replace('/[^\x21-\x7E]/', '', $string)); |
116
|
|
|
} |
117
|
|
|
foreach (array_map('strtolower', $this->getUserAgents()) as $generated) { |
118
|
|
|
if (($result = array_search($generated, $array)) !== false) { |
119
|
|
|
// Match found |
120
|
|
|
return $result; |
121
|
|
|
} |
122
|
|
|
} |
123
|
|
|
return false; |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Get an array of all possible User-agent combinations |
128
|
|
|
* |
129
|
|
|
* @return array |
130
|
|
|
*/ |
131
|
|
|
public function getUserAgents() |
132
|
|
|
{ |
133
|
|
|
return array_merge( |
134
|
|
|
preg_filter('/^/', $this->product . '/', $this->getVersions()), |
135
|
|
|
$this->getProducts() |
136
|
|
|
); |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
/** |
140
|
|
|
* Get versions |
141
|
|
|
* |
142
|
|
|
* @return array |
143
|
|
|
*/ |
144
|
|
|
public function getVersions() |
145
|
|
|
{ |
146
|
|
|
while (substr_count($this->version, '.') < 2) { |
147
|
|
|
$this->version .= '.0'; |
148
|
|
|
} |
149
|
|
|
// Remove part by part of the version. |
150
|
|
|
$result = array_merge( |
151
|
|
|
[$this->version], |
152
|
|
|
$this->explode($this->version, '.') |
153
|
|
|
); |
154
|
|
|
asort($result); |
155
|
|
|
usort($result, function ($a, $b) { |
156
|
|
|
return strlen($b) - strlen($a); |
157
|
|
|
}); |
158
|
|
|
return $this->filter($result); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Explode |
163
|
|
|
* |
164
|
|
|
* @param string $string |
165
|
|
|
* @param string $delimiter |
166
|
|
|
* @return array |
167
|
|
|
*/ |
168
|
|
|
private function explode($string, $delimiter) |
169
|
|
|
{ |
170
|
|
|
$result = []; |
171
|
|
|
while (($pos = strrpos($string, $delimiter)) !== false) { |
172
|
|
|
$result[] = ($string = substr($string, 0, $pos)); |
173
|
|
|
} |
174
|
|
|
return $result; |
175
|
|
|
} |
176
|
|
|
|
177
|
|
|
/** |
178
|
|
|
* Filter duplicates from an array |
179
|
|
|
* |
180
|
|
|
* @param string[] $array |
181
|
|
|
* @return array |
182
|
|
|
*/ |
183
|
|
|
private function filter($array) |
184
|
|
|
{ |
185
|
|
|
$result = []; |
186
|
|
|
foreach ($array as $value) { |
187
|
|
|
if (!in_array($array, $result)) { |
188
|
|
|
$result[] = $value; |
189
|
|
|
} |
190
|
|
|
} |
191
|
|
|
return array_filter($result); |
192
|
|
|
} |
193
|
|
|
|
194
|
|
|
/** |
195
|
|
|
* Get products |
196
|
|
|
* |
197
|
|
|
* @return array |
198
|
|
|
*/ |
199
|
|
|
public function getProducts() |
200
|
|
|
{ |
201
|
|
|
$result = array_merge( |
202
|
|
|
[ |
203
|
|
|
$this->product, |
204
|
|
|
preg_replace('/[^A-Za-z0-9]/', '', $this->product), // in case of special characters |
205
|
|
|
], |
206
|
|
|
$this->explode($this->product, '-') |
207
|
|
|
); |
208
|
|
|
return $this->filter($result); |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
/** |
212
|
|
|
* Get User-agent |
213
|
|
|
* |
214
|
|
|
* @return string |
215
|
|
|
*/ |
216
|
|
|
public function getUserAgent() |
217
|
|
|
{ |
218
|
|
|
return $this->getVersion() === null ? $this->getProduct() : $this->getProduct() . '/' . $this->getVersion(); |
219
|
|
|
} |
220
|
|
|
|
221
|
|
|
/** |
222
|
|
|
* Get version |
223
|
|
|
* |
224
|
|
|
* @return string |
225
|
|
|
*/ |
226
|
|
|
public function getVersion() |
227
|
|
|
{ |
228
|
|
|
if (empty($this->version)) { |
229
|
|
|
return null; |
230
|
|
|
} |
231
|
|
|
return trim($this->version, '.0'); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Get product |
236
|
|
|
* |
237
|
|
|
* @return string |
238
|
|
|
*/ |
239
|
|
|
public function getProduct() |
240
|
|
|
{ |
241
|
|
|
return $this->product; |
242
|
|
|
} |
243
|
|
|
} |
244
|
|
|
|
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.
For example, imagine you have a variable
$accountId
that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to theid
property of an instance of theAccount
class. This class holds a proper account, so the id value must no longer be false.Either this assignment is in error or a type check should be added for that assignment.