Browscap::updateCache()   B
last analyzed

Complexity

Conditions 7
Paths 40

Size

Total Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
cc 7
nc 40
nop 0
dl 0
loc 77
ccs 0
cts 58
cp 0
crap 56
rs 7.5684
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * BrowsCap.php
4
 * Author: Federico Mestrone
5
 * Created: 17/12/2012 12:16
6
 * Copyright: 2012, Moodsdesign Ltd
7
 */
8
9
namespace phpbrowscap;
10
11
use \Exception as BaseException;
12
13
/**
14
 * Browscap.ini parsing class with caching and update capabilities
15
 *
16
 * PHP version 5
17
 *
18
 * Copyright (c) 2006-2012 Jonathan Stoppani
19
 *
20
 * Permission is hereby granted, free of charge, to any person obtaining a
21
 * copy of this software and associated documentation files (the "Software"),
22
 * to deal in the Software without restriction, including without limitation
23
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
24
 * and/or sell copies of the Software, and to permit persons to whom the
25
 * Software is furnished to do so, subject to the following conditions:
26
 *
27
 * The above copyright notice and this permission notice shall be included
28
 * in all copies or substantial portions of the Software.
29
 *
30
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
31
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
32
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
33
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
34
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
35
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
36
 * THE SOFTWARE.
37
 *
38
 * @package    Browscap
39
 * @author     Jonathan Stoppani <[email protected]>
40
 * @author     Vítor Brandão <[email protected]>
41
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
42
 * @version    1.0
43
 * @license    http://www.opensource.org/licenses/MIT MIT License
44
 * @link       https://github.com/GaretJax/phpbrowscap/
45
 */
46
class Browscap
47
{
48
    /**
49
     * Current version of the class.
50
     */
51
    const VERSION = '1.0';
52
53
    /**
54
     * Different ways to access remote and local files.
55
     *
56
     * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents).
57
     * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen).
58
     * UPDATE_CURL: Uses the cURL extension.
59
     * UPDATE_LOCAL: Updates from a local file (file_get_contents).
60
     */
61
    const UPDATE_FOPEN = 'URL-wrapper';
62
    const UPDATE_FSOCKOPEN = 'socket';
63
    const UPDATE_CURL = 'cURL';
64
    const UPDATE_LOCAL = 'local';
65
66
    /**
67
     * Options for regex patterns.
68
     *
69
     * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class.
70
     * REGEX_MODIFIERS: Regex modifiers.
71
     */
72
    const REGEX_DELIMITER = '@';
73
    const REGEX_MODIFIERS = 'i';
74
75
    /**
76
     * The values to quote in the ini file
77
     */
78
    const VALUES_TO_QUOTE = 'Browser|Parent';
79
80
    /**
81
     * Definitions of the function used by the uasort() function to order the
82
     * userAgents array.
83
     *
84
     * ORDER_FUNC_ARGS: Arguments that the function will take.
85
     * ORDER_FUNC_LOGIC: Internal logic of the function.
86
     */
87
    const ORDER_FUNC_ARGS = '$a, $b';
88
    const ORDER_FUNC_LOGIC = '$a=strlen($a);$b=strlen($b);return$a==$b?0:($a<$b?1:-1);';
89
90
    /**
91
     * The headers to be sent for checking the version and requesting the file.
92
     */
93
    const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n";
94
95
    /**
96
     * Options for auto update capabilities
97
     *
98
     * $remoteVerUrl: The location to use to check out if a new version of the
99
     *                browscap.ini file is available.
100
     * $remoteIniUrl: The location from which download the ini file.
101
     *                The placeholder for the file should be represented by a %s.
102
     * $timeout: The timeout for the requests.
103
     * $updateInterval: The update interval in seconds.
104
     * $errorInterval: The next update interval in seconds in case of an error.
105
     * $doAutoUpdate: Flag to disable the automatic interval based update.
106
     * $updateMethod: The method to use to update the file, has to be a value of
107
     *                an UPDATE_* constant, null or false.
108
     */
109
    public $remoteIniUrl = 'http://tempdownloads.browserscap.com/stream.php?Full_PHP_BrowscapINI';
110
    public $remoteVerUrl = 'http://tempdownloads.browserscap.com/versions/version-date.php';
111
    public $timeout = 5;
112
    public $updateInterval = 432000;  // 5 days
113
    public $errorInterval = 7200;  // 2 hours
114
    public $doAutoUpdate = true;
115
    public $updateMethod = null;
116
117
    /**
118
     * The path of the local version of the browscap.ini file from which to
119
     * update (to be set only if used).
120
     *
121
     * @var string
122
     */
123
    public $localFile = null;
124
125
    /**
126
     * The useragent to include in the requests made by the class during the
127
     * update process.
128
     *
129
     * @var string
130
     */
131
    public $userAgent = 'Browser Capabilities Project - PHP Browscap/%v %m';
132
133
    /**
134
     * Flag to enable only lowercase indexes in the result.
135
     * The cache has to be rebuilt in order to apply this option.
136
     *
137
     * @var bool
138
     */
139
    public $lowercase = false;
140
141
    /**
142
     * Flag to enable/disable silent error management.
143
     * In case of an error during the update process the class returns an empty
144
     * array/object if the update process can't take place and the browscap.ini
145
     * file does not exist.
146
     *
147
     * @var bool
148
     */
149
    public $silent = false;
150
151
    /**
152
     * Where to store the cached PHP arrays.
153
     *
154
     * @var string
155
     */
156
    public $cacheFilename = 'cache.php';
157
158
    /**
159
     * Where to store the downloaded ini file.
160
     *
161
     * @var string
162
     */
163
    public $iniFilename = 'browscap.ini';
164
165
    /**
166
     * Path to the cache directory
167
     *
168
     * @var string
169
     */
170
    public $cacheDir = null;
171
172
    /**
173
     * Flag to be set to true after loading the cache
174
     *
175
     * @var bool
176
     */
177
    protected $_cacheLoaded = false;
178
179
    /**
180
     * Where to store the value of the included PHP cache file
181
     *
182
     * @var array
183
     */
184
    protected $_userAgents = array();
185
    protected $_browsers = array();
186
    protected $_patterns = array();
187
    protected $_properties = array();
188
189
    /**
190
     * An associative array of associative arrays in the format
191
     * `$arr['wrapper']['option'] = $value` passed to stream_context_create()
192
     * when building a stream resource.
193
     *
194
     * Proxy settings are stored in this variable.
195
     *
196
     * @see http://www.php.net/manual/en/function.stream-context-create.php
197
     *
198
     * @var array
199
     */
200
    protected $_streamContextOptions = array();
201
202
    /**
203
     * A valid context resource created with stream_context_create().
204
     *
205
     * @see http://www.php.net/manual/en/function.stream-context-create.php
206
     *
207
     * @var resource
208
     */
209
    protected $_streamContext = null;
210
211
    /**
212
     * Constructor class, checks for the existence of (and loads) the cache and
213
     * if needed updated the definitions
214
     *
215
     * @param string $cache_dir
216
     */
217
    public function __construct($cache_dir)
218
    {
219
        // has to be set to reach E_STRICT compatibility, does not affect system/app settings
220
        date_default_timezone_set(date_default_timezone_get());
221
222
        if (!isset($cache_dir)) {
223
            throw new Exception(
224
                'You have to provide a path to read/store the browscap cache file'
225
            );
226
        }
227
228
        $old_cache_dir = $cache_dir;
229
        $cache_dir = realpath($cache_dir);
230
231
        if (false === $cache_dir) {
232
            throw new Exception(
233
                sprintf('The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?', $old_cache_dir)
234
            );
235
        }
236
237
        // Is the cache dir really the directory or is it directly the file?
238
        if (substr($cache_dir, -4) === '.php') {
239
            $this->cacheFilename = basename($cache_dir);
240
            $this->cacheDir = dirname($cache_dir);
241
        } else {
242
            $this->cacheDir = $cache_dir;
243
        }
244
245
        $this->cacheDir .= DIRECTORY_SEPARATOR;
246
    }
247
248
    /**
249
     * Gets the information about the browser by User Agent
250
     *
251
     * @param string $user_agent  the user agent string
252
     * @param bool $return_array  whether return an array or an object
253
     * @throws Exception
254
     * @return stdObject  the object containing the browsers details. Array if
255
     *                    $return_array is set to true.
256
     */
257
    public function getBrowser($user_agent = null, $return_array = false)
258
    {
259
        // Load the cache at the first request
260
        if (!$this->_cacheLoaded) {
261
            $cache_file = $this->cacheDir . $this->cacheFilename;
262
            $ini_file = $this->cacheDir . $this->iniFilename;
263
264
            // Set the interval only if needed
265
            if ($this->doAutoUpdate && file_exists($ini_file)) {
266
                $interval = time() - filemtime($ini_file);
267
            } else {
268
                $interval = 0;
269
            }
270
271
            // Find out if the cache needs to be updated
272
            if (!file_exists($cache_file) || !file_exists($ini_file) || ($interval > $this->updateInterval)) {
273
                try {
274
                    $this->updateCache();
275
                } catch (Exception $e) {
276
                    if (file_exists($ini_file)) {
277
                        // Adjust the filemtime to the $errorInterval
278
                        touch($ini_file, time() - $this->updateInterval + $this->errorInterval);
279
                    } elseif ($this->silent) {
280
                        // Return an array if silent mode is active and the ini db doesn't exsist
281
                        return array();
0 ignored issues
show
Bug Best Practice introduced by
The return type of return array(); (array) is incompatible with the return type documented by phpbrowscap\Browscap::getBrowser of type phpbrowscap\stdObject.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
282
                    }
283
284
                    if (!$this->silent) {
285
                        throw $e;
286
                    }
287
                }
288
            }
289
290
            $this->_loadCache($cache_file);
291
        }
292
293
        // Automatically detect the useragent
294
        if (!isset($user_agent)) {
295
            if (isset($_SERVER['HTTP_USER_AGENT'])) {
296
                $user_agent = $_SERVER['HTTP_USER_AGENT'];
297
            } else {
298
                $user_agent = '';
299
            }
300
        }
301
302
        $browser = array();
303
        foreach ($this->_patterns as $key => $pattern) {
304
            if (preg_match($pattern . 'i', $user_agent)) {
305
                $browser = array(
306
                    $user_agent, // Original useragent
307
                    trim(strtolower($pattern), self::REGEX_DELIMITER),
308
                    $this->_userAgents[$key]
309
                );
310
311
                $browser = $value = $browser + $this->_browsers[$key];
312
313
                while (array_key_exists(3, $value) && $value[3]) {
314
                    $value = $this->_browsers[$value[3]];
315
                    $browser += $value;
316
                }
317
318
                if (!empty($browser[3])) {
319
                    $browser[3] = $this->_userAgents[$browser[3]];
320
                }
321
322
                break;
323
            }
324
        }
325
326
        // Add the keys for each property
327
        $array = array();
328
        foreach ($browser as $key => $value) {
329
            if ($value === 'true') {
330
                $value = true;
331
            } elseif ($value === 'false') {
332
                $value = false;
333
            }
334
            $array[$this->_properties[$key]] = $value;
335
        }
336
337
        return $return_array ? $array : (object) $array;
338
    }
339
340
    /**
341
     * Load (auto-set) proxy settings from environment variables.
342
     */
343
    public function autodetectProxySettings()
344
    {
345
        $wrappers = array('http', 'https', 'ftp');
346
347
        foreach ($wrappers as $wrapper) {
348
            $url = getenv($wrapper.'_proxy');
349
            if (!empty($url)) {
350
                $params = array_merge(array(
351
                    'port'  => null,
352
                    'user'  => null,
353
                    'pass'  => null,
354
                ), parse_url($url));
355
                $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']);
356
            }
357
        }
358
    }
359
360
    /**
361
     * Add proxy settings to the stream context array.
362
     *
363
     * @param string $server    Proxy server/host
364
     * @param int    $port      Port
365
     * @param string $wrapper   Wrapper: "http", "https", "ftp", others...
366
     * @param string $username  Username (when requiring authentication)
367
     * @param string $password  Password (when requiring authentication)
368
     *
369
     * @return Browscap
370
     */
371
    public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null)
372
    {
373
        $settings = array($wrapper => array(
374
            'proxy'             => sprintf('tcp://%s:%d', $server, $port),
375
            'request_fulluri'   => true,
376
        ));
377
378
        // Proxy authentication (optional)
379
        if (isset($username) && isset($password)) {
380
            $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic '.base64_encode($username.':'.$password);
381
        }
382
383
        // Add these new settings to the stream context options array
384
        $this->_streamContextOptions = array_merge(
385
            $this->_streamContextOptions,
386
            $settings
387
        );
388
389
        /* Return $this so we can chain addProxySettings() calls like this:
390
         * $browscap->
391
         *   addProxySettings('http')->
392
         *   addProxySettings('https')->
393
         *   addProxySettings('ftp');
394
         */
395
        return $this;
396
    }
397
398
    /**
399
     * Clear proxy settings from the stream context options array.
400
     *
401
     * @param string $wrapper Remove settings from this wrapper only
402
     *
403
     * @return array Wrappers cleared
404
     */
405
    public function clearProxySettings($wrapper = null)
406
    {
407
        $wrappers = isset($wrapper) ? array($wrappers) : array_keys($this->_streamContextOptions);
0 ignored issues
show
Bug introduced by
The variable $wrappers seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
408
409
        $affectedProtocols = array();
410
        $options = array('proxy', 'request_fulluri', 'header');
411
        foreach ($wrappers as $wrapper) {
412
413
            // remove wrapper options related to proxy settings
414
            if (isset($this->_streamContextOptions[$wrapper]['proxy'])) {
415
                foreach ($options as $option){
416
                    unset($this->_streamContextOptions[$wrapper][$option]);
417
                }
418
419
                // remove wrapper entry if there are no other options left
420
                if (empty($this->_streamContextOptions[$wrapper])) {
421
                    unset($this->_streamContextOptions[$wrapper]);
422
                }
423
424
                $clearedWrappers[] = $wrapper;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$clearedWrappers was never initialized. Although not strictly required by PHP, it is generally a good practice to add $clearedWrappers = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
425
            }
426
        }
427
428
        return $clearedWrappers;
0 ignored issues
show
Bug introduced by
The variable $clearedWrappers does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
429
    }
430
431
    /**
432
     * Returns the array of stream context options.
433
     *
434
     * @return array
435
     */
436
    public function getStreamContextOptions()
437
    {
438
        return $this->_streamContextOptions;
439
    }
440
441
    /**
442
     * Parses the ini file and updates the cache files
443
     *
444
     * @return bool whether the file was correctly written to the disk
445
     */
446
    public function updateCache()
447
    {
448
        $ini_path = $this->cacheDir . $this->iniFilename;
449
        $cache_path = $this->cacheDir . $this->cacheFilename;
450
451
        // Choose the right url
452
        if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
453
            $url = $this->localFile;
454
        } else {
455
            $url = $this->remoteIniUrl;
456
        }
457
458
        $this->_getRemoteIniFile($url, $ini_path);
459
460
        if (version_compare(PHP_VERSION, '5.3.0', '>=')) {
461
            $browsers = parse_ini_file($ini_path, true, INI_SCANNER_RAW);
462
        } else {
463
            $browsers = parse_ini_file($ini_path, true);
464
        }
465
466
        array_shift($browsers);
467
468
        $this->_properties = array_keys($browsers['DefaultProperties']);
469
        array_unshift(
470
            $this->_properties,
471
            'browser_name',
472
            'browser_name_regex',
473
            'browser_name_pattern',
474
            'Parent'
475
        );
476
477
        $this->_userAgents = array_keys($browsers);
478
        usort(
479
            $this->_userAgents,
480
            create_function(self::ORDER_FUNC_ARGS, self::ORDER_FUNC_LOGIC)
481
        );
482
483
        $user_agents_keys = array_flip($this->_userAgents);
484
        $properties_keys = array_flip($this->_properties);
485
486
        $search = array('\*', '\?');
487
        $replace = array('.*', '.');
488
489
        foreach ($this->_userAgents as $user_agent) {
490
            $pattern = preg_quote($user_agent, self::REGEX_DELIMITER);
491
            $this->_patterns[] = self::REGEX_DELIMITER
492
                . '^'
493
                . str_replace($search, $replace, $pattern)
494
                . '$'
495
                . self::REGEX_DELIMITER;
496
497
            if (!empty($browsers[$user_agent]['Parent'])) {
498
                $parent = $browsers[$user_agent]['Parent'];
499
                $browsers[$user_agent]['Parent'] = $user_agents_keys[$parent];
500
            }
501
502
            foreach ($browsers[$user_agent] as $key => $value) {
503
                $key = $properties_keys[$key] . ".0";
504
                $browser[$key] = $value;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$browser was never initialized. Although not strictly required by PHP, it is generally a good practice to add $browser = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
505
            }
506
507
            $this->_browsers[] = $browser;
0 ignored issues
show
Bug introduced by
The variable $browser does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
508
            unset($browser);
509
        }
510
        unset($user_agents_keys, $properties_keys, $browsers);
511
512
        // Save the keys lowercased if needed
513
        if ($this->lowercase) {
514
            $this->_properties = array_map('strtolower', $this->_properties);
515
        }
516
517
        // Get the whole PHP code
518
        $cache = $this->_buildCache();
519
520
        // Save and return
521
        return (bool) file_put_contents($cache_path, $cache, LOCK_EX);
522
    }
523
524
    /**
525
     * Loads the cache into object's properties
526
     *
527
     * @return void
528
     */
529
    protected function _loadCache($cache_file)
530
    {
531
        require $cache_file;
532
533
        $this->_browsers = $browsers;
0 ignored issues
show
Bug introduced by
The variable $browsers does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
534
        $this->_userAgents = $userAgents;
0 ignored issues
show
Bug introduced by
The variable $userAgents does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
535
        $this->_patterns = $patterns;
0 ignored issues
show
Bug introduced by
The variable $patterns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
536
        $this->_properties = $properties;
0 ignored issues
show
Bug introduced by
The variable $properties does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
537
538
        $this->_cacheLoaded = true;
539
    }
540
541
    /**
542
     * Parses the array to cache and creates the PHP string to write to disk
543
     *
544
     * @return string the PHP string to save into the cache file
545
     */
546
    protected function _buildCache()
547
    {
548
        $cacheTpl = "<?php\n\$properties=%s;\n\$browsers=%s;\n\$userAgents=%s;\n\$patterns=%s;\n";
549
550
        $propertiesArray = $this->_array2string($this->_properties);
551
        $patternsArray = $this->_array2string($this->_patterns);
552
        $userAgentsArray = $this->_array2string($this->_userAgents);
553
        $browsersArray = $this->_array2string($this->_browsers);
554
555
        return sprintf(
556
            $cacheTpl,
557
            $propertiesArray,
558
            $browsersArray,
559
            $userAgentsArray,
560
            $patternsArray
561
        );
562
    }
563
564
    /**
565
     * Lazy getter for the stream context resource.
566
     *
567
     * @return resource
568
     */
569
    protected function _getStreamContext($recreate = false)
570
    {
571
        if (!isset($this->_streamContext) || true === $recreate) {
572
            $this->_streamContext = stream_context_create($this->_streamContextOptions);
573
        }
574
575
        return $this->_streamContext;
576
    }
577
578
    /**
579
     * Updates the local copy of the ini file (by version checking) and adapts
580
     * his syntax to the PHP ini parser
581
     *
582
     * @param string $url  the url of the remote server
583
     * @param string $path  the path of the ini file to update
584
     * @throws Exception
585
     * @return bool if the ini file was updated
586
     */
587
    protected function _getRemoteIniFile($url, $path)
588
    {
589
        // Check version
590
        if (file_exists($path) && filesize($path)) {
591
            $local_tmstp = filemtime($path);
592
593
            if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
594
                $remote_tmstp = $this->_getLocalMTime();
595
            } else {
596
                $remote_tmstp = $this->_getRemoteMTime();
597
            }
598
599
            if ($remote_tmstp < $local_tmstp) {
600
                // No update needed, return
601
                touch($path);
602
603
                return false;
604
            }
605
        }
606
607
        // Get updated .ini file
608
        $browscap = $this->_getRemoteData($url);
609
610
611
        $browscap = explode("\n", $browscap);
612
613
        $pattern = self::REGEX_DELIMITER
614
            . '('
615
            . self::VALUES_TO_QUOTE
616
            . ')="?([^"]*)"?$'
617
            . self::REGEX_DELIMITER;
618
619
620
        // Ok, lets read the file
621
        $content = '';
622
        foreach ($browscap as $subject) {
623
            $subject = trim($subject);
624
            $content .= preg_replace($pattern, '$1="$2"', $subject) . "\n";
625
        }
626
627
        if ($url != $path) {
628
            if (!file_put_contents($path, $content)) {
629
                throw new Exception("Could not write .ini content to $path");
630
            }
631
        }
632
633
        return true;
634
    }
635
636
    /**
637
     * Gets the remote ini file update timestamp
638
     *
639
     * @throws Exception
640
     * @return int the remote modification timestamp
641
     */
642
    protected function _getRemoteMTime()
643
    {
644
        $remote_datetime = $this->_getRemoteData($this->remoteVerUrl);
645
        $remote_tmstp = strtotime($remote_datetime);
646
647
        if (!$remote_tmstp) {
648
            throw new Exception("Bad datetime format from {$this->remoteVerUrl}");
649
        }
650
651
        return $remote_tmstp;
652
    }
653
654
    /**
655
     * Gets the local ini file update timestamp
656
     *
657
     * @throws Exception
658
     * @return int the local modification timestamp
659
     */
660
    protected function _getLocalMTime()
661
    {
662
        if (!is_readable($this->localFile) || !is_file($this->localFile)) {
663
            throw new Exception("Local file is not readable");
664
        }
665
666
        return filemtime($this->localFile);
667
    }
668
669
    /**
670
     * Converts the given array to the PHP string which represent it.
671
     * This method optimizes the PHP code and the output differs form the
672
     * var_export one as the internal PHP function does not strip whitespace or
673
     * convert strings to numbers.
674
     *
675
     * @param array $array the array to parse and convert
676
     * @return string the array parsed into a PHP string
677
     */
678
    protected function _array2string($array)
679
    {
680
        $strings = array();
681
682
        foreach ($array as $key => $value) {
683
            if (is_int($key)) {
684
                $key = '';
685
            } elseif (ctype_digit((string) $key) || strpos($key, '.0')) {
686
                $key = intval($key) . '=>' ;
687
            } else {
688
                $key = "'" . str_replace("'", "\'", $key) . "'=>" ;
689
            }
690
691
            if (is_array($value)) {
692
                $value = $this->_array2string($value);
693
            } elseif (ctype_digit((string) $value)) {
694
                $value = intval($value);
695
            } else {
696
                $value = "'" . str_replace("'", "\'", $value) . "'";
697
            }
698
699
            $strings[] = $key . $value;
700
        }
701
702
        return 'array(' . implode(',', $strings) . ')';
703
    }
704
705
    /**
706
     * Checks for the various possibilities offered by the current configuration
707
     * of PHP to retrieve external HTTP data
708
     *
709
     * @return string the name of function to use to retrieve the file
710
     */
711
    protected function _getUpdateMethod()
712
    {
713
        // Caches the result
714
        if ($this->updateMethod === null) {
715
            if ($this->localFile !== null) {
716
                $this->updateMethod = self::UPDATE_LOCAL;
717
            } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) {
718
                $this->updateMethod = self::UPDATE_FOPEN;
719
            } elseif (function_exists('fsockopen')) {
720
                $this->updateMethod = self::UPDATE_FSOCKOPEN;
721
            } elseif (extension_loaded('curl')) {
722
                $this->updateMethod = self::UPDATE_CURL;
723
            } else {
724
                $this->updateMethod = false;
725
            }
726
        }
727
728
        return $this->updateMethod;
729
    }
730
731
    /**
732
     * Retrieve the data identified by the URL
733
     *
734
     * @param string $url the url of the data
735
     * @throws Exception
736
     * @return string the retrieved data
737
     */
738
    protected function _getRemoteData($url)
739
    {
740
        ini_set('user_agent', $this->_getUserAgent());
741
742
        switch ($this->_getUpdateMethod()) {
743
            case self::UPDATE_LOCAL:
744
                $file = file_get_contents($url);
745
746
                if ($file !== false) {
747
                    return $file;
748
                } else {
749
                    throw new Exception('Cannot open the local file');
750
                }
751
            case self::UPDATE_FOPEN:
752
                // include proxy settings in the file_get_contents() call
753
                $context = $this->_getStreamContext();
754
                $file = file_get_contents($url, false, $context);
755
756
                if ($file !== false) {
757
                    return $file;
758
                } // else try with the next possibility (break omitted)
759
            case self::UPDATE_FSOCKOPEN:
760
                $remote_url = parse_url($url);
761
                $remote_handler = fsockopen($remote_url['host'], 80, $c, $e, $this->timeout);
762
763
                if ($remote_handler) {
764
                    stream_set_timeout($remote_handler, $this->timeout);
765
766
                    if (isset($remote_url['query'])) {
767
                        $remote_url['path'] .= '?' . $remote_url['query'];
768
                    }
769
770
                    $out = sprintf(
771
                        self::REQUEST_HEADERS,
772
                        $remote_url['path'],
773
                        $remote_url['host'],
774
                        $this->_getUserAgent()
775
                    );
776
777
                    fwrite($remote_handler, $out);
778
779
                    $response = fgets($remote_handler);
780
                    if (strpos($response, '200 OK') !== false) {
781
                        $file = '';
782
                        while (!feof($remote_handler)) {
783
                            $file .= fgets($remote_handler);
784
                        }
785
786
                        $file = str_replace("\r\n", "\n", $file);
787
                        $file = explode("\n\n", $file);
788
                        array_shift($file);
789
790
                        $file = implode("\n\n", $file);
791
792
                        fclose($remote_handler);
793
794
                        return $file;
795
                    }
796
                } // else try with the next possibility
797
            case self::UPDATE_CURL:
798
                $ch = curl_init($url);
799
800
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
801
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
802
                curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent());
803
804
                $file = curl_exec($ch);
805
806
                curl_close($ch);
807
808
                if ($file !== false) {
809
                    return $file;
810
                } // else try with the next possibility
811
            case false:
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing $this->_getUpdateMethod() of type string to the boolean false. If you are specifically checking for an empty string, consider using the more explicit === '' instead.
Loading history...
812
                throw new Exception('Your server can\'t connect to external resources. Please update the file manually.');
813
        }
814
    }
815
816
    /**
817
     * Format the useragent string to be used in the remote requests made by the
818
     * class during the update process.
819
     *
820
     * @return string the formatted user agent
821
     */
822
    protected function _getUserAgent()
823
    {
824
        $ua = str_replace('%v', self::VERSION, $this->userAgent);
825
        $ua = str_replace('%m', $this->_getUpdateMethod(), $ua);
826
827
        return $ua;
828
    }
829
}
830
831
/**
832
 * Browscap.ini parsing class exception
833
 *
834
 * @package    Browscap
835
 * @author     Jonathan Stoppani <[email protected]>
836
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
837
 * @version    1.0
838
 * @license    http://www.opensource.org/licenses/MIT MIT License
839
 * @link       https://github.com/GaretJax/phpbrowscap/*/
840
class Exception extends BaseException
841
{}
842