1
|
|
|
<?php |
2
|
|
|
declare(strict_types = 1); |
3
|
|
|
|
4
|
|
|
namespace BrowscapPHP; |
5
|
|
|
|
6
|
|
|
use BrowscapPHP\Cache\BrowscapCache; |
7
|
|
|
use BrowscapPHP\Exception\ErrorCachedVersionException; |
8
|
|
|
use BrowscapPHP\Exception\ErrorReadingFileException; |
9
|
|
|
use BrowscapPHP\Exception\FetcherException; |
10
|
|
|
use BrowscapPHP\Exception\FileNameMissingException; |
11
|
|
|
use BrowscapPHP\Exception\FileNotFoundException; |
12
|
|
|
use BrowscapPHP\Exception\NoCachedVersionException; |
13
|
|
|
use BrowscapPHP\Exception\NoNewVersionException; |
14
|
|
|
use BrowscapPHP\Helper\Converter; |
15
|
|
|
use BrowscapPHP\Helper\ConverterInterface; |
16
|
|
|
use BrowscapPHP\Helper\Filesystem; |
17
|
|
|
use BrowscapPHP\Helper\IniLoader; |
18
|
|
|
use BrowscapPHP\Helper\IniLoaderInterface; |
19
|
|
|
use GuzzleHttp\Client; |
20
|
|
|
use GuzzleHttp\ClientInterface; |
21
|
|
|
use Psr\Log\LoggerInterface; |
22
|
|
|
use Psr\SimpleCache\CacheInterface; |
23
|
|
|
use Psr\SimpleCache\InvalidArgumentException; |
24
|
|
|
|
25
|
|
|
/** |
26
|
|
|
* Browscap.ini parsing class with caching and update capabilities |
27
|
|
|
*/ |
28
|
|
|
final class BrowscapUpdater implements BrowscapUpdaterInterface |
29
|
|
|
{ |
30
|
|
|
public const DEFAULT_TIMEOUT = 5; |
31
|
|
|
|
32
|
|
|
/** |
33
|
|
|
* The cache instance |
34
|
|
|
* |
35
|
|
|
* @var \BrowscapPHP\Cache\BrowscapCacheInterface |
36
|
|
|
*/ |
37
|
|
|
private $cache; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* @var \Psr\Log\LoggerInterface |
41
|
|
|
*/ |
42
|
|
|
private $logger; |
43
|
|
|
|
44
|
|
|
/** |
45
|
|
|
* @var \GuzzleHttp\ClientInterface |
46
|
|
|
*/ |
47
|
|
|
private $client; |
48
|
|
|
|
49
|
|
|
/** |
50
|
|
|
* Curl connect timeout in seconds |
51
|
|
|
* |
52
|
|
|
* @var int |
53
|
|
|
*/ |
54
|
|
|
private $connectTimeout; |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Browscap constructor. |
58
|
|
|
* |
59
|
13 |
|
* @param \Psr\SimpleCache\CacheInterface $cache |
60
|
|
|
* @param LoggerInterface $logger |
61
|
|
|
* @param ClientInterface|null $client |
62
|
|
|
* @param int $connectTimeout |
63
|
|
|
*/ |
64
|
|
|
public function __construct( |
65
|
13 |
|
CacheInterface $cache, |
66
|
13 |
|
LoggerInterface $logger, |
67
|
|
|
?ClientInterface $client = null, |
68
|
13 |
|
int $connectTimeout = self::DEFAULT_TIMEOUT |
69
|
13 |
|
) { |
70
|
|
|
$this->cache = new BrowscapCache($cache, $logger); |
71
|
|
|
$this->logger = $logger; |
72
|
13 |
|
|
73
|
13 |
|
if (null === $client) { |
74
|
13 |
|
$client = new Client(); |
75
|
|
|
} |
76
|
|
|
|
77
|
|
|
$this->client = $client; |
|
|
|
|
78
|
|
|
$this->connectTimeout = $connectTimeout; |
79
|
|
|
} |
80
|
|
|
|
81
|
|
|
/** |
82
|
|
|
* reads and parses an ini file and writes the results into the cache |
83
|
3 |
|
* |
84
|
|
|
* @param string $iniFile |
85
|
3 |
|
* |
86
|
1 |
|
* @throws \BrowscapPHP\Exception\FileNameMissingException |
87
|
|
|
* @throws \BrowscapPHP\Exception\FileNotFoundException |
88
|
|
|
* @throws \BrowscapPHP\Exception\ErrorReadingFileException |
89
|
2 |
|
*/ |
90
|
1 |
|
public function convertFile(string $iniFile) : void |
91
|
|
|
{ |
92
|
|
|
if (empty($iniFile)) { |
93
|
|
|
throw new FileNameMissingException('the file name can not be empty'); |
94
|
1 |
|
} |
95
|
|
|
|
96
|
|
|
if (! is_readable($iniFile)) { |
97
|
|
|
throw new FileNotFoundException('it was not possible to read the local file ' . $iniFile); |
98
|
|
|
} |
99
|
1 |
|
|
100
|
1 |
|
$iniString = file_get_contents($iniFile); |
101
|
|
|
|
102
|
|
|
if (false === $iniString) { |
103
|
|
|
throw new ErrorReadingFileException('an error occured while converting the local file into the cache'); |
104
|
|
|
} |
105
|
|
|
|
106
|
|
|
$this->convertString($iniString); |
107
|
2 |
|
} |
108
|
|
|
|
109
|
|
|
/** |
110
|
2 |
|
* reads and parses an ini string and writes the results into the cache |
111
|
|
|
* |
112
|
|
|
* @param string $iniString |
113
|
|
|
*/ |
114
|
|
|
public function convertString(string $iniString) : void |
115
|
|
|
{ |
116
|
|
|
try { |
117
|
2 |
|
$cachedVersion = $this->cache->getItem('browscap.version', false, $success); |
118
|
|
|
} catch (InvalidArgumentException $e) { |
|
|
|
|
119
|
2 |
|
$this->logger->error(new \InvalidArgumentException('an error occured while reading the data version from the cache', 0, $e)); |
120
|
2 |
|
|
121
|
|
|
return; |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
$converter = new Converter($this->logger, $this->cache); |
125
|
|
|
|
126
|
|
|
$this->storeContent($converter, $iniString, $cachedVersion); |
127
|
|
|
} |
128
|
|
|
|
129
|
|
|
/** |
130
|
|
|
* fetches a remote file and stores it into a local folder |
131
|
|
|
* |
132
|
3 |
|
* @param string $file The name of the file where to store the remote content |
133
|
|
|
* @param string $remoteFile The code for the remote file to load |
134
|
|
|
* |
135
|
3 |
|
* @throws \BrowscapPHP\Exception\FetcherException |
136
|
|
|
* @throws \BrowscapPHP\Helper\Exception |
137
|
|
|
* @throws \BrowscapPHP\Exception\ErrorCachedVersionException |
138
|
|
|
*/ |
139
|
3 |
|
public function fetch(string $file, string $remoteFile = IniLoaderInterface::PHP_INI) : void |
140
|
2 |
|
{ |
141
|
|
|
try { |
142
|
|
|
$cachedVersion = $this->checkUpdate(); |
143
|
2 |
|
} catch (NoNewVersionException $e) { |
144
|
|
|
return; |
145
|
2 |
|
} catch (NoCachedVersionException $e) { |
146
|
2 |
|
$cachedVersion = 0; |
147
|
|
|
} |
148
|
2 |
|
|
149
|
|
|
$this->logger->debug('started fetching remote file'); |
150
|
|
|
|
151
|
2 |
|
$loader = new IniLoader(); |
152
|
|
|
$loader->setRemoteFilename($remoteFile); |
153
|
2 |
|
|
154
|
|
|
$uri = $loader->getRemoteIniUrl(); |
155
|
|
|
|
156
|
|
|
try { |
157
|
|
|
/** @var \Psr\Http\Message\ResponseInterface $response */ |
158
|
|
|
$response = $this->client->request('get', $uri, ['connect_timeout' => $this->connectTimeout]); |
159
|
|
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) { |
|
|
|
|
160
|
|
|
throw new FetcherException( |
161
|
2 |
|
sprintf( |
162
|
|
|
'an error occured while fetching remote data from URI %s', |
163
|
|
|
$uri |
164
|
|
|
), |
165
|
|
|
0, |
166
|
2 |
|
$e |
167
|
|
|
); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
if (200 !== $response->getStatusCode()) { |
171
|
|
|
throw new FetcherException( |
172
|
2 |
|
sprintf( |
173
|
2 |
|
'an error occured while fetching remote data from URI %s: StatusCode was %d', |
174
|
|
|
$uri, |
175
|
2 |
|
$response->getStatusCode() |
176
|
|
|
) |
177
|
2 |
|
); |
178
|
2 |
|
} |
179
|
|
|
|
180
|
2 |
|
try { |
181
|
2 |
|
$content = $response->getBody()->getContents(); |
182
|
2 |
|
} catch (\Exception $e) { |
183
|
|
|
throw new FetcherException('an error occured while fetching remote data', 0, $e); |
184
|
|
|
} |
185
|
2 |
|
|
186
|
2 |
|
if (empty($content)) { |
187
|
|
|
$error = error_get_last(); |
188
|
|
|
|
189
|
|
|
if (is_array($error)) { |
190
|
|
|
throw FetcherException::httpError($uri, $error['message']); |
191
|
|
|
} |
192
|
|
|
|
193
|
|
|
throw FetcherException::httpError( |
194
|
|
|
$uri, |
195
|
|
|
'an error occured while fetching remote data, but no error was raised' |
196
|
|
|
); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
$this->logger->debug('finished fetching remote file'); |
200
|
|
|
$this->logger->debug('started storing remote file into local file'); |
201
|
2 |
|
|
202
|
|
|
$content = $this->sanitizeContent($content); |
203
|
2 |
|
|
204
|
|
|
$converter = new Converter($this->logger, $this->cache); |
205
|
|
|
$iniVersion = $converter->getIniVersion($content); |
206
|
2 |
|
|
207
|
|
|
if ($iniVersion > $cachedVersion) { |
208
|
|
|
$fs = new Filesystem(); |
209
|
|
|
$fs->dumpFile($file, $content); |
210
|
2 |
|
} |
211
|
2 |
|
|
212
|
|
|
$this->logger->debug('finished storing remote file into local file'); |
213
|
|
|
} |
214
|
2 |
|
|
215
|
2 |
|
/** |
216
|
|
|
* fetches a remote file, parses it and writes the result into the cache |
217
|
2 |
|
* if the local stored information are in the same version as the remote data no actions are |
218
|
|
|
* taken |
219
|
|
|
* |
220
|
2 |
|
* @param string $remoteFile The code for the remote file to load |
221
|
|
|
* |
222
|
2 |
|
* @throws \BrowscapPHP\Exception\FetcherException |
223
|
|
|
* @throws \BrowscapPHP\Helper\Exception |
224
|
|
|
* @throws \BrowscapPHP\Exception\ErrorCachedVersionException |
225
|
|
|
*/ |
226
|
|
|
public function update(string $remoteFile = IniLoaderInterface::PHP_INI) : void |
227
|
|
|
{ |
228
|
|
|
$this->logger->debug('started fetching remote file'); |
229
|
|
|
|
230
|
2 |
|
try { |
231
|
|
|
$cachedVersion = $this->checkUpdate(); |
232
|
|
|
} catch (NoNewVersionException $e) { |
233
|
|
|
return; |
234
|
|
|
} catch (NoCachedVersionException $e) { |
235
|
2 |
|
$cachedVersion = 0; |
236
|
1 |
|
} |
237
|
|
|
|
238
|
1 |
|
$loader = new IniLoader(); |
239
|
|
|
$loader->setRemoteFilename($remoteFile); |
240
|
|
|
|
241
|
1 |
|
$uri = $loader->getRemoteIniUrl(); |
242
|
|
|
|
243
|
1 |
|
try { |
244
|
|
|
/** @var \Psr\Http\Message\ResponseInterface $response */ |
245
|
1 |
|
$response = $this->client->request('get', $uri, ['connect_timeout' => $this->connectTimeout]); |
246
|
1 |
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) { |
|
|
|
|
247
|
|
|
throw new FetcherException( |
248
|
|
|
sprintf( |
249
|
|
|
'an error occured while fetching remote data from URI %s', |
250
|
|
|
$uri |
251
|
|
|
), |
252
|
|
|
0, |
253
|
|
|
$e |
254
|
|
|
); |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
if (200 !== $response->getStatusCode()) { |
258
|
9 |
|
throw new FetcherException( |
259
|
|
|
sprintf( |
260
|
9 |
|
'an error occured while fetching remote data from URI %s: StatusCode was %d', |
261
|
|
|
$uri, |
262
|
|
|
$response->getStatusCode() |
263
|
9 |
|
) |
264
|
|
|
); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
try { |
268
|
9 |
|
$content = $response->getBody()->getContents(); |
269
|
|
|
} catch (\Exception $e) { |
270
|
5 |
|
throw new FetcherException('an error occured while fetching remote data', 0, $e); |
271
|
|
|
} |
272
|
|
|
|
273
|
4 |
|
if (empty($content)) { |
274
|
|
|
$error = error_get_last(); |
275
|
|
|
|
276
|
4 |
|
throw FetcherException::httpError($uri, $error['message'] ?? ''); |
277
|
|
|
} |
278
|
4 |
|
|
279
|
1 |
|
$this->logger->debug('finished fetching remote file'); |
280
|
1 |
|
$this->logger->debug('started updating cache from remote file'); |
281
|
1 |
|
|
282
|
|
|
$converter = new Converter($this->logger, $this->cache); |
283
|
|
|
$this->storeContent($converter, $content, $cachedVersion); |
284
|
|
|
|
285
|
|
|
$this->logger->debug('finished updating cache from remote file'); |
286
|
3 |
|
} |
287
|
1 |
|
|
288
|
1 |
|
/** |
289
|
1 |
|
* checks if an update on a remote location for the local file or the cache |
290
|
1 |
|
* |
291
|
1 |
|
* @throws \BrowscapPHP\Exception\FetcherException |
292
|
1 |
|
* @throws \BrowscapPHP\Exception\NoCachedVersionException |
293
|
|
|
* @throws \BrowscapPHP\Exception\ErrorCachedVersionException |
294
|
|
|
* @throws \BrowscapPHP\Exception\NoNewVersionException |
295
|
|
|
* |
296
|
2 |
|
* @return int|null The actual cached version if a newer version is available, null otherwise |
297
|
|
|
*/ |
298
|
|
|
public function checkUpdate() : ?int |
299
|
|
|
{ |
300
|
|
|
$success = null; |
301
|
|
|
|
302
|
|
|
try { |
303
|
2 |
|
$cachedVersion = $this->cache->getItem('browscap.version', false, $success); |
304
|
|
|
} catch (InvalidArgumentException $e) { |
|
|
|
|
305
|
1 |
|
throw new ErrorCachedVersionException('an error occured while reading the data version from the cache', 0, $e); |
306
|
|
|
} |
307
|
1 |
|
|
308
|
|
|
if (! $cachedVersion) { |
309
|
|
|
// could not load version from cache |
310
|
1 |
|
throw new NoCachedVersionException('there is no cached version available, please update from remote'); |
311
|
1 |
|
} |
312
|
|
|
|
313
|
|
|
$uri = (new IniLoader())->getRemoteVersionUrl(); |
314
|
1 |
|
|
315
|
|
|
try { |
316
|
|
|
/** @var \Psr\Http\Message\ResponseInterface $response */ |
317
|
5 |
|
$response = $this->client->request('get', $uri, ['connect_timeout' => $this->connectTimeout]); |
318
|
|
|
} catch (\GuzzleHttp\Exception\GuzzleException $e) { |
|
|
|
|
319
|
|
|
throw new FetcherException( |
320
|
5 |
|
sprintf( |
321
|
|
|
'an error occured while fetching version data from URI %s', |
322
|
|
|
$uri |
323
|
5 |
|
), |
324
|
|
|
0, |
325
|
|
|
$e |
326
|
|
|
); |
327
|
|
|
} |
328
|
|
|
|
329
|
|
|
if (200 !== $response->getStatusCode()) { |
330
|
|
|
throw new FetcherException( |
331
|
|
|
sprintf( |
332
|
|
|
'an error occured while fetching version data from URI %s: StatusCode was %d', |
333
|
3 |
|
$uri, |
334
|
|
|
$response->getStatusCode() |
335
|
3 |
|
) |
336
|
3 |
|
); |
337
|
|
|
} |
338
|
3 |
|
|
339
|
3 |
|
try { |
340
|
3 |
|
$remoteVersion = $response->getBody()->getContents(); |
341
|
|
|
} catch (\Throwable $e) { |
342
|
3 |
|
throw new FetcherException( |
343
|
|
|
sprintf( |
344
|
|
|
'an error occured while fetching version data from URI %s: StatusCode was %d', |
345
|
|
|
$uri, |
346
|
|
|
$response->getStatusCode() |
347
|
|
|
), |
348
|
|
|
0, |
349
|
|
|
$e |
350
|
|
|
); |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
if (! $remoteVersion) { |
354
|
|
|
// could not load remote version |
355
|
|
|
throw new FetcherException( |
356
|
|
|
'could not load version from remote location' |
357
|
|
|
); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
if ($cachedVersion && $remoteVersion && $remoteVersion <= $cachedVersion) { |
361
|
|
|
throw new NoNewVersionException('there is no newer version available'); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
$this->logger->info( |
365
|
|
|
'a newer version is available, local version: ' . $cachedVersion . ', remote version: ' . $remoteVersion |
366
|
|
|
); |
367
|
|
|
|
368
|
|
|
return (int) $cachedVersion; |
369
|
|
|
} |
370
|
|
|
|
371
|
|
|
private function sanitizeContent(string $content) : string |
372
|
|
|
{ |
373
|
|
|
// replace everything between opening and closing php and asp tags |
374
|
|
|
$content = preg_replace('/<[?%].*[?%]>/', '', $content); |
375
|
|
|
|
376
|
|
|
// replace opening and closing php and asp tags |
377
|
|
|
return str_replace(['<?', '<%', '?>', '%>'], '', (string) $content); |
378
|
|
|
} |
379
|
|
|
|
380
|
|
|
/** |
381
|
|
|
* reads and parses an ini string and writes the results into the cache |
382
|
|
|
* |
383
|
|
|
* @param \BrowscapPHP\Helper\ConverterInterface $converter |
384
|
|
|
* @param string $content |
385
|
|
|
* @param int|null $cachedVersion |
386
|
|
|
*/ |
387
|
|
|
private function storeContent(ConverterInterface $converter, string $content, ?int $cachedVersion) : void |
388
|
|
|
{ |
389
|
|
|
$iniString = $this->sanitizeContent($content); |
390
|
|
|
$iniVersion = $converter->getIniVersion($iniString); |
391
|
|
|
|
392
|
|
|
if (! $cachedVersion || $iniVersion > $cachedVersion) { |
|
|
|
|
393
|
|
|
$converter->storeVersion(); |
394
|
|
|
$converter->convertString($iniString); |
395
|
|
|
} |
396
|
|
|
} |
397
|
|
|
} |
398
|
|
|
|
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.