Completed
Push — master ( 301f3c...656cd4 )
by James
03:43
created

BrowscapUpdater   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 80.67%

Importance

Changes 7
Bugs 1 Features 1
Metric Value
wmc 34
lcom 1
cbo 14
dl 0
loc 321
ccs 96
cts 119
cp 0.8067
rs 9.2
c 7
b 1
f 1

8 Methods

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

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
339 3
            $converter->storeVersion();
340 3
            $converter->convertString($iniString);
341
        }
342 3
    }
343
}
344