Completed
Pull Request — master (#218)
by Thomas
05:43
created

BrowscapUpdater   A

Complexity

Total Complexity 30

Size/Duplication

Total Lines 299
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 12

Test Coverage

Coverage 83.78%

Importance

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