Completed
Pull Request — master (#157)
by Thomas
11:57
created

Browscap::getClient()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 8
ccs 4
cts 4
cp 1
rs 9.4285
cc 2
eloc 4
nc 2
nop 0
crap 2
1
<?php
2
/**
3
 * Copyright (c) 1998-2015 Browser Capabilities Project
4
 *
5
 * Permission is hereby granted, free of charge, to any person obtaining a
6
 * copy of this software and associated documentation files (the "Software"),
7
 * to deal in the Software without restriction, including without limitation
8
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9
 * and/or sell copies of the Software, and to permit persons to whom the
10
 * Software is furnished to do so, subject to the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be included
13
 * in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
 * THE SOFTWARE.
22
 *
23
 * @category   Browscap-PHP
24
 * @package    Browscap
25
 * @copyright  1998-2015 Browser Capabilities Project
26
 * @license    http://www.opensource.org/licenses/MIT MIT License
27
 * @link       https://github.com/browscap/browscap-php/
28
 */
29
30
namespace BrowscapPHP;
31
32
use Browscap\Generator\BuildGenerator;
33
use Browscap\Helper\CollectionCreator;
34
use Browscap\Writer\Factory\PhpWriterFactory;
35
use BrowscapPHP\Cache\BrowscapCache;
36
use BrowscapPHP\Cache\BrowscapCacheInterface;
37
use BrowscapPHP\Exception\FetcherException;
38
use BrowscapPHP\Helper\Converter;
39
use BrowscapPHP\Helper\Filesystem;
40
use BrowscapPHP\Helper\IniLoader;
41
use BrowscapPHP\Helper\Quoter;
42
use BrowscapPHP\Parser\ParserInterface;
43
use Psr\Log\LoggerInterface;
44
use Psr\Log\NullLogger;
45
use WurflCache\Adapter\AdapterInterface;
46
use WurflCache\Adapter\File;
47
use GuzzleHttp\Client;
48
49
/**
50
 * Browscap.ini parsing class with caching and update capabilities
51
 *
52
 * @category   Browscap-PHP
53
 * @package    Browscap
54
 * @author     Jonathan Stoppani <[email protected]>
55
 * @author     Vítor Brandão <[email protected]>
56
 * @author     Mikołaj Misiurewicz <[email protected]>
57
 * @author     Christoph Ziegenberg <[email protected]>
58
 * @author     Thomas Müller <[email protected]>
59
 * @copyright  Copyright (c) 1998-2015 Browser Capabilities Project
60
 * @version    3.0
61
 * @license    http://www.opensource.org/licenses/MIT MIT License
62
 * @link       https://github.com/browscap/browscap-php/
63
 */
64
class Browscap
65
{
66
    /**
67
     * Parser to use
68
     *
69
     * @var \BrowscapPHP\Parser\ParserInterface
70
     */
71
    private $parser = null;
72
73
    /**
74
     * Formatter to use
75
     *
76
     * @var \BrowscapPHP\Formatter\FormatterInterface
77
     */
78
    private $formatter = null;
79
80
    /**
81
     * The cache instance
82
     *
83
     * @var \BrowscapPHP\Cache\BrowscapCacheInterface
84
     */
85
    private $cache = null;
86
87
    /**
88
     * @var @var \Psr\Log\LoggerInterface
89
     */
90
    private $logger = null;
91
92
    /**
93
     * @var \GuzzleHttp\Client
94
     */
95
    private $client = null;
96
97
    /**
98
     * Set theformatter instance to use for the getBrowser() result
99
     *
100
     * @param \BrowscapPHP\Formatter\FormatterInterface $formatter
101
     *
102
     * @return \BrowscapPHP\Browscap
103
     */
104 5
    public function setFormatter(Formatter\FormatterInterface $formatter)
105
    {
106 5
        $this->formatter = $formatter;
107
108 5
        return $this;
109
    }
110
111
    /**
112
     * @return \BrowscapPHP\Formatter\FormatterInterface
113
     */
114 3
    public function getFormatter()
115
    {
116 3
        if (null === $this->formatter) {
117 2
            $this->setFormatter(new Formatter\PhpGetBrowser());
118
        }
119
120 3
        return $this->formatter;
121
    }
122
123
    /**
124
     * Gets a cache instance
125
     *
126
     * @return \BrowscapPHP\Cache\BrowscapCacheInterface
127
     */
128 16 View Code Duplication
    public function getCache()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
129
    {
130 16
        if (null === $this->cache) {
131 2
            $cacheDirectory = __DIR__.'/../resources/';
132
133 2
            $cacheAdapter = new File(
134 2
                array(File::DIR => $cacheDirectory)
135 2
            );
136
137 2
            $this->cache = new BrowscapCache($cacheAdapter);
138
        }
139
140 16
        return $this->cache;
141
    }
142
143
    /**
144
     * Sets a cache instance
145
     *
146
     * @param \BrowscapPHP\Cache\BrowscapCacheInterface|\WurflCache\Adapter\AdapterInterface $cache
147
     *
148
     * @throws \BrowscapPHP\Exception
149
     * @return \BrowscapPHP\Browscap
150
     */
151 15
    public function setCache($cache)
152
    {
153 15
        if ($cache instanceof BrowscapCacheInterface) {
154 11
            $this->cache = $cache;
155 15
        } elseif ($cache instanceof AdapterInterface) {
156 3
            $this->cache = new BrowscapCache($cache);
157 3
        } else {
158 1
            throw new Exception(
159
                'the cache has to be an instance of \BrowscapPHP\Cache\BrowscapCacheInterface or '
160 5
                .'an instanceof of \WurflCache\Adapter\AdapterInterface',
161
                Exception::CACHE_INCOMPATIBLE
162 1
            );
163
        }
164
165 14
        return $this;
166
    }
167
168
    /**
169
     * Sets the parser instance to use
170
     *
171
     * @param \BrowscapPHP\Parser\ParserInterface $parser
172
     *
173
     * @return \BrowscapPHP\Browscap
174
     */
175 4
    public function setParser(ParserInterface $parser)
176
    {
177 4
        $this->parser = $parser;
178
179 3
        return $this;
180
    }
181
182
    /**
183
     * returns an instance of the used parser class
184
     *
185
     * @return \BrowscapPHP\Parser\ParserInterface
186
     */
187 5
    public function getParser()
188
    {
189 5
        if (null === $this->parser) {
190 2
            $cache  = $this->getCache();
191 2
            $logger = $this->getLogger();
192 2
            $quoter = new Quoter();
193
194 2
            $patternHelper = new Parser\Helper\GetPattern($cache, $logger);
195 2
            $dataHelper    = new Parser\Helper\GetData($cache, $logger, $quoter);
196
197 2
            $this->parser = new Parser\Ini($patternHelper, $dataHelper, $this->getFormatter());
198
        }
199
200 5
        return $this->parser;
201
    }
202
203
    /**
204
     * Sets a logger instance
205
     *
206
     * @param \Psr\Log\LoggerInterface $logger
207
     *
208
     * @return \BrowscapPHP\Browscap
209
     */
210 11
    public function setLogger(LoggerInterface $logger)
211
    {
212 11
        $this->logger = $logger;
213
214 11
        return $this;
215
    }
216
217
    /**
218
     * returns a logger instance
219
     *
220
     * @return \Psr\Log\LoggerInterface
221
     */
222 14
    public function getLogger()
223
    {
224 14
        if (null === $this->logger) {
225 4
            $this->logger = new NullLogger();
226
        }
227
228 14
        return $this->logger;
229
    }
230
231
    /**
232
     * @return \GuzzleHttp\Client
233
     */
234 9
    public function getClient()
235
    {
236 9
        if (null === $this->client) {
237 1
            $this->client = new Client();
238
        }
239
240 9
        return $this->client;
241
    }
242
243
    /**
244
     * @param \GuzzleHttp\Client $client
245
     */
246 9
    public function setClient(Client $client)
247
    {
248 9
        $this->client = $client;
249 9
    }
250
251
    /**
252
     * parses the given user agent to get the information about the browser
253
     *
254
     * if no user agent is given, it uses {@see \BrowscapPHP\Helper\Support} to get it
255
     *
256
     * @param string $userAgent the user agent string
257
     *
258
     * @throws \BrowscapPHP\Exception
259
     * @return \stdClass              the object containing the browsers details. Array if
260
     *                                $return_array is set to true.
261
     */
262 4
    public function getBrowser($userAgent = null)
263
    {
264
        // Automatically detect the useragent
265 4
        if (!isset($userAgent)) {
266 2
            $support   = new Helper\Support($_SERVER);
267 2
            $userAgent = $support->getUserAgent();
268
        }
269
270
        // try to get browser data
271 4
        $formatter = $this->getParser()->getBrowser($userAgent);
272
273
        // if return is still NULL, updates are disabled... in this
274
        // case we return an empty formatter instance
275 4
        if ($formatter === null) {
276 2
            return $this->getFormatter()->getData();
277
        }
278
279 2
        return $formatter->getData();
280
    }
281
282
    /**
283
     * reads and parses an ini file and writes the results into the cache
284
     *
285
     * @param  string $iniFile
286
     *
287
     * @throws \BrowscapPHP\Exception
288
     */
289 4
    public function convertFile($iniFile)
290
    {
291 4
        if (empty($iniFile)) {
292 1
            throw new Exception('the file name can not be empty');
293
        }
294
295 3
        if (!is_readable($iniFile)) {
296 1
            throw new Exception('it was not possible to read the local file ' . $iniFile);
297
        }
298
299
        try {
300 2
            $iniString = file_get_contents($iniFile);
301 2
        } catch (Helper\Exception $e) {
302
            throw new Exception('an error occured while converting the local file into the cache', 0, $e);
303
        }
304
305 2
        $this->convertString($iniString);
306 2
    }
307
308
    /**
309
     * reads and parses an ini string and writes the results into the cache
310
     *
311
     * @param string $iniString
312
     */
313 3
    public function convertString($iniString)
314
    {
315 3
        $cachedVersion = $this->getCache()->getItem('browscap.version', false, $success);
316 3
        $converter     = new Converter($this->getLogger(), $this->getCache());
317
318 3
        $this->storeContent($converter, $iniString, $cachedVersion);
319 3
    }
320
321
    /**
322
     * fetches a remote file and stores it into a local folder
323
     *
324
     * @param string $file       The name of the file where to store the remote content
325
     * @param string $remoteFile The code for the remote file to load
326
     *
327
     * @throws \BrowscapPHP\Exception\FetcherException
328
     * @throws \BrowscapPHP\Helper\Exception
329
     */
330 3
    public function fetch($file, $remoteFile = IniLoader::PHP_INI)
331
    {
332 3
        if (null === ($cachedVersion = $this->checkUpdate())) {
333
            // no newer version available
334
            return;
335
        }
336
337 3
        $this->getLogger()->debug('started fetching remote file');
338
339 3
        $uri      = (new IniLoader())->setRemoteFilename($remoteFile)->getRemoteIniUrl();
340 3
        $response = $this->getClient()->get($uri, ['timeout' => 5]);
341
342 3 View Code Duplication
        if ($response->getStatusCode() !== 200) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
343
            throw new FetcherException(
344
                'an error occured while fetching remote data from URI ' . $uri . ': StatusCode was '
345
                . $response->getStatusCode()
346
            );
347
        }
348
349
        try {
350 3
            $content = $response->getBody()->getContents();
351 3
        } catch (\Exception $e) {
352
            throw new FetcherException('an error occured while fetching remote data', 0, $e);
353
        }
354
355 3
        if (empty($content)) {
356 1
            $error = error_get_last();
357 1
            throw FetcherException::httpError($uri, $error['message']);
358
        }
359
360 2
        $this->getLogger()->debug('finished fetching remote file');
361 2
        $this->getLogger()->debug('started storing remote file into local file');
362
363 2
        $content = $this->sanitizeContent($content);
364
365 2
        $converter  = new Converter($this->getLogger(), $this->getCache());
366 2
        $iniVersion = $converter->getIniVersion($content);
367
368 2
        if ($iniVersion > $cachedVersion) {
369 2
            $fs = new Filesystem();
370 2
            $fs->dumpFile($file, $content);
371
        }
372
373 2
        $this->getLogger()->debug('finished storing remote file into local file');
374 2
    }
375
376
    /**
377
     * fetches a remote file, parses it and writes the result into the cache
378
     *
379
     * if the local stored information are in the same version as the remote data no actions are
380
     * taken
381
     *
382
     * @param string      $remoteFile The code for the remote file to load
383
     * @param string|null $buildFolder
384
     * @param int|null    $buildNumber
385
     *
386
     * @throws \BrowscapPHP\Exception\FileNotFoundException
387
     * @throws \BrowscapPHP\Helper\Exception
388
     * @throws \BrowscapPHP\Exception\FetcherException
389
     */
390 2
    public function update($remoteFile = IniLoader::PHP_INI, $buildFolder = null, $buildNumber = null)
391
    {
392 2
        $this->getLogger()->debug('started fetching remote file');
393
394 2
        $converter = new Converter($this->getLogger(), $this->getCache());
395
396 2
        if (class_exists('\Browscap\Browscap')) {
397
            $resourceFolder = 'vendor/browscap/browscap/resources/';
398
399
            if (null === $buildNumber) {
400
                $buildNumber = (int)file_get_contents('vendor/browscap/browscap/BUILD_NUMBER');
401
            }
402
403
            if (null === $buildFolder) {
404
                $buildFolder = 'resources';
405
            }
406
407
            $buildFolder .= '/browscap-ua-test-'.$buildNumber;
408
            $iniFile     = $buildFolder.'/full_php_browscap.ini';
409
410
            mkdir($buildFolder, 0777, true);
411
412
            $writerCollectionFactory = new PhpWriterFactory();
413
            $writerCollection        = $writerCollectionFactory->createCollection($this->getLogger(), $buildFolder);
414
415
            $buildGenerator = new BuildGenerator($resourceFolder, $buildFolder);
416
            $buildGenerator
417
                ->setLogger($this->getLogger())
418
                ->setCollectionCreator(new CollectionCreator())
419
                ->setWriterCollection($writerCollection)
420
                ->run($buildNumber, false)
421
            ;
422
423
            $converter
424
                ->setVersion($buildNumber)
425
                ->storeVersion()
426
                ->convertFile($iniFile)
427
            ;
428
429
            $filesystem = new Filesystem();
430
            $filesystem->remove($buildFolder);
431
        } else {
432 2
            if (null === ($cachedVersion = $this->checkUpdate())) {
433
                // no newer version available
434
                return;
435
            }
436
437 2
            $uri      = (new IniLoader())->setRemoteFilename($remoteFile)->getRemoteIniUrl();
438 2
            $response = $this->getClient()->get($uri, ['timeout' => 5]);
439
440 2 View Code Duplication
            if ($response->getStatusCode() !== 200) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
441
                throw new FetcherException(
442
                    'an error occured while fetching remote data from URI ' . $uri . ': StatusCode was '
443
                    . $response->getStatusCode()
444
                );
445
            }
446
447
            try {
448 2
                $content = $response->getBody()->getContents();
449 2
            } catch (\Exception $e) {
450 1
                throw new FetcherException('an error occured while fetching remote data', 0, $e);
451
            }
452
453 1
            if (empty($content)) {
454
                $error = error_get_last();
455
456
                throw FetcherException::httpError($uri, $error['message']);
457
            }
458
459 1
            $this->getLogger()->debug('finished fetching remote file');
460
461 1
            $this->storeContent($converter, $content, $cachedVersion);
462
        }
463 1
    }
464
465
    /**
466
     * checks if an update on a remote location for the local file or the cache
467
     *
468
     * @return int|null The actual cached version if a newer version is available, null otherwise
469
     * @throws \BrowscapPHP\Helper\Exception
470
     * @throws \BrowscapPHP\Exception\FetcherException
471
     */
472 9
    public function checkUpdate()
473
    {
474 9
        $success       = null;
475 9
        $cachedVersion = $this->getCache()->getItem('browscap.version', false, $success);
476
477 9
        if (!$cachedVersion) {
478
            // could not load version from cache
479 5
            $this->getLogger()->info('there is no cached version available, please update from remote');
480
481 5
            return 0;
482
        }
483
484 4
        $uri      = (new IniLoader())->getRemoteVersionUrl();
485 4
        $response = $this->getClient()->get($uri, ['timeout' => 5]);
486
487 4 View Code Duplication
        if ($response->getStatusCode() !== 200) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
488
            throw new FetcherException(
489
                'an error occured while fetching version data from URI ' . $uri . ': StatusCode was '
490
                . $response->getStatusCode()
491
            );
492
        }
493
494
        try {
495 4
            $remoteVersion = $response->getBody()->getContents();
496 4
        } catch (\Exception $e) {
497 1
            throw new FetcherException('an error occured while fetching remote data', 0, $e);
498
        }
499
500 3
        if (!$remoteVersion) {
501
            // could not load remote version
502 1
            $this->getLogger()->info('could not load version from remote location');
503
504 1
            return 0;
505
        }
506
507 2
        if ($cachedVersion && $remoteVersion && $remoteVersion <= $cachedVersion) {
508
            // no newer version available
509 1
            $this->getLogger()->info('there is no newer version available');
510
511 1
            return null;
512
        }
513
514 1
        $this->getLogger()->info(
515 1
            'a newer version is available, local version: ' . $cachedVersion . ', remote version: ' . $remoteVersion
516 1
        );
517
518 1
        return (int) $cachedVersion;
519
    }
520
521
    /**
522
     * @param string $content
523
     *
524
     * @return mixed
525
     */
526 6
    private function sanitizeContent($content)
527
    {
528
        // replace everything between opening and closing php and asp tags
529 6
        $content = preg_replace('/<[?%].*[?%]>/', '', $content);
530
531
        // replace opening and closing php and asp tags
532 6
        return str_replace(array('<?', '<%', '?>', '%>'), '', $content);
533
    }
534
535
    /**
536
     * reads and parses an ini string and writes the results into the cache
537
     *
538
     * @param \BrowscapPHP\Helper\Converter $converter
539
     * @param string                        $content
540
     * @param int|null                      $cachedVersion
541
     */
542 4
    private function storeContent(Converter $converter, $content, $cachedVersion)
543
    {
544 4
        $iniString  = $this->sanitizeContent($content);
545 4
        $iniVersion = $converter->getIniVersion($iniString);
546
547 4
        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...
548
            $converter
549 4
                ->storeVersion()
550 4
                ->convertString($iniString)
551
            ;
552
        }
553 4
    }
554
}
555