Issues (304)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

class/phpbrowscap/Browscap.php (22 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace phpbrowscap;
4
5
use Exception as BaseException;
6
7
/**
8
 * Browscap.ini parsing class with caching and update capabilities
9
 *
10
 * PHP version 5
11
 *
12
 * Copyright (c) 2006-2012 Jonathan Stoppani
13
 *
14
 * Permission is hereby granted, free of charge, to any person obtaining a
15
 * copy of this software and associated documentation files (the "Software"),
16
 * to deal in the Software without restriction, including without limitation
17
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
18
 * and/or sell copies of the Software, and to permit persons to whom the
19
 * Software is furnished to do so, subject to the following conditions:
20
 *
21
 * The above copyright notice and this permission notice shall be included
22
 * in all copies or substantial portions of the Software.
23
 *
24
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
25
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
30
 * THE SOFTWARE.
31
 *
32
 * @package    Browscap
33
 * @author     Jonathan Stoppani <[email protected]>
34
 * @author     Vítor Brandão <[email protected]>
35
 * @author     MikoÅ‚aj Misiurewicz <[email protected]>
36
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
37
 * @version    1.0
38
 * @license    http://www.opensource.org/licenses/MIT MIT License
39
 * @link       https://github.com/GaretJax/phpbrowscap/
40
 */
41
class Browscap
42
{
43
    /**
44
     * Current version of the class.
45
     */
46
    const VERSION = '2.1.1';
47
48
    const CACHE_FILE_VERSION = '2.1.0';
49
50
    /**
51
     * Different ways to access remote and local files.
52
     *
53
     * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents).
54
     * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen).
55
     * UPDATE_CURL: Uses the cURL extension.
56
     * UPDATE_LOCAL: Updates from a local file (file_get_contents).
57
     */
58
    const UPDATE_FOPEN = 'URL-wrapper';
59
    const UPDATE_FSOCKOPEN = 'socket';
60
    const UPDATE_CURL = 'cURL';
61
    const UPDATE_LOCAL = 'local';
62
63
    /**
64
     * Options for regex patterns.
65
     *
66
     * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class.
67
     * REGEX_MODIFIERS: Regex modifiers.
68
     */
69
    const REGEX_DELIMITER = '@';
70
    const REGEX_MODIFIERS = 'i';
71
    const COMPRESSION_PATTERN_START = '@';
72
    const COMPRESSION_PATTERN_DELIMITER = '|';
73
74
    /**
75
     * The values to quote in the ini file
76
     */
77
    const VALUES_TO_QUOTE = 'Browser|Parent';
78
79
    const BROWSCAP_VERSION_KEY = 'GJK_Browscap_Version';
80
81
    /**
82
     * The headers to be sent for checking the version and requesting the file.
83
     */
84
    const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n";
85
86
    /**
87
     * how many pattern should be checked at once in the first step
88
     */
89
    const COUNT_PATTERN = 100;
90
91
    /**
92
     * Options for auto update capabilities
93
     *
94
     * $remoteVerUrl: The location to use to check out if a new version of the
95
     *                browscap.ini file is available.
96
     * $remoteIniUrl: The location from which download the ini file.
97
     *                The placeholder for the file should be represented by a %s.
98
     * $timeout: The timeout for the requests.
99
     * $updateInterval: The update interval in seconds.
100
     * $errorInterval: The next update interval in seconds in case of an error.
101
     * $doAutoUpdate: Flag to disable the automatic interval based update.
102
     * $updateMethod: The method to use to update the file, has to be a value of
103
     *                an UPDATE_* constant, null or false.
104
     *
105
     * The default source file type is changed from normal to full. The performance difference
106
     * is MINIMAL, so there is no reason to use the standard file whatsoever. Either go for light,
107
     * which is blazing fast, or get the full one. (note: light version doesn't work, a fix is on its way)
108
     */
109
    public $remoteIniUrl   = 'http://browscap.org/stream?q=PHP_BrowscapINI';
110
    public $remoteVerUrl   = 'http://browscap.org/version';
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 = 'http://browscap.org/ - 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';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
164
    public $iniFilename = 'php_browscap.ini'; // irmtfan
165
166
    /**
167
     * Path to the cache directory
168
     *
169
     * @var string
170
     */
171
    public $cacheDir = null;
172
173
    /**
174
     * Flag to be set to true after loading the cache
175
     *
176
     * @var bool
177
     */
178
    protected $_cacheLoaded = false;
179
180
    /**
181
     * Where to store the value of the included PHP cache file
182
     *
183
     * @var array
184
     */
185
    protected $_userAgents = [];
186
    protected $_browsers   = [];
187
    protected $_patterns   = [];
188
    protected $_properties = [];
189
    protected $_source_version;
190
191
    /**
192
     * An associative array of associative arrays in the format
193
     * `$arr['wrapper']['option'] = $value` passed to stream_context_create()
194
     * when building a stream resource.
195
     *
196
     * Proxy settings are stored in this variable.
197
     *
198
     * @see http://www.php.net/manual/en/function.stream-context-create.php
199
     * @var array
200
     */
201
    protected $_streamContextOptions = [];
202
203
    /**
204
     * A valid context resource created with stream_context_create().
205
     *
206
     * @see http://www.php.net/manual/en/function.stream-context-create.php
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
0 ignored issues
show
Should the type for parameter $cache_dir not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
216
     *
217
     * @throws Exception
218
     */
219
    public function __construct($cache_dir = null)
220
    {
221
        // has to be set to reach E_STRICT compatibility, does not affect system/app settings
222
        date_default_timezone_set(date_default_timezone_get());
223
224
        if (!isset($cache_dir)) {
225
            throw new Exception('You have to provide a path to read/store the browscap cache file');
226
        }
227
228
        $old_cache_dir = $cache_dir;
229
        $cache_dir     = realpath($cache_dir);
230
231
        if (false === $cache_dir) {
232
            throw new Exception(sprintf(
233
                'The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?',
234
                                        $old_cache_dir
235
            ));
236
        }
237
238
        // Is the cache dir really the directory or is it directly the file?
239
        if ('.php' === substr($cache_dir, -4)) {
240
            $this->cacheFilename = basename($cache_dir);
241
            $this->cacheDir      = dirname($cache_dir);
242
        } else {
243
            $this->cacheDir = $cache_dir;
244
        }
245
246
        $this->cacheDir .= '/';
247
    }
248
249
    /**
250
     * @return mixed
251
     */
252
    public function getSourceVersion()
253
    {
254
        return $this->_source_version;
255
    }
256
257
    /**
258
     * @return bool
259
     */
260
    public function shouldCacheBeUpdated()
261
    {
262
        // Load the cache at the first request
263
        if ($this->_cacheLoaded) {
264
            return false;
265
        }
266
267
        $cache_file = $this->cacheDir . $this->cacheFilename;
268
        $ini_file   = $this->cacheDir . $this->iniFilename;
269
270
        // Set the interval only if needed
271
        if ($this->doAutoUpdate && file_exists($ini_file)) {
272
            $interval = time() - filemtime($ini_file);
273
        } else {
274
            $interval = 0;
275
        }
276
277
        $shouldBeUpdated = true;
278
279
        if (file_exists($cache_file) && file_exists($ini_file) && ($interval <= $this->updateInterval)) {
280
            if ($this->_loadCache($cache_file)) {
281
                $shouldBeUpdated = false;
282
            }
283
        }
284
285
        return $shouldBeUpdated;
286
    }
287
288
    /**
289
     * Gets the information about the browser by User Agent
290
     *
291
     * @param string $user_agent   the user agent string
0 ignored issues
show
Should the type for parameter $user_agent not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
292
     * @param bool   $return_array whether return an array or an object
293
     *
294
     * @throws Exception
295
     * @return \stdClass|array  the object containing the browsers details. Array if
296
     *                    $return_array is set to true.
297
     */
298
    public function getBrowser($user_agent = null, $return_array = false)
0 ignored issues
show
getBrowser uses the super-global variable $_SERVER which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

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

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
299
    {
300
        if ($this->shouldCacheBeUpdated()) {
301
            try {
302
                $this->updateCache();
303
            } catch (Exception $e) {
304
                $ini_file = $this->cacheDir . $this->iniFilename;
305
306
                if (file_exists($ini_file)) {
307
                    // Adjust the filemtime to the $errorInterval
308
                    touch($ini_file, time() - $this->updateInterval + $this->errorInterval);
309
                } elseif ($this->silent) {
310
                    // Return an array if silent mode is active and the ini db doesn't exsist
311
                    return [];
312
                }
313
314
                if (!$this->silent) {
315
                    throw $e;
316
                }
317
            }
318
        }
319
320
        $cache_file = $this->cacheDir . $this->cacheFilename;
321
        if (!$this->_cacheLoaded && !$this->_loadCache($cache_file)) {
322
            throw new Exception('Cannot load cache file - the cache format is not compatible.');
323
        }
324
325
        // Automatically detect the useragent
326
        if (!isset($user_agent)) {
327
            if (isset($_SERVER['HTTP_USER_AGENT'])) {
328
                $user_agent = $_SERVER['HTTP_USER_AGENT'];
329
            } else {
330
                $user_agent = '';
331
            }
332
        }
333
334
        $browser = [];
335
336
        $patterns = array_keys($this->_patterns);
337
        $chunks   = array_chunk($patterns, self::COUNT_PATTERN);
338
339
        foreach ($chunks as $chunk) {
340
            $longPattern = self::REGEX_DELIMITER . '^(?:' . implode(')|(?:', $chunk) . ')$' . self::REGEX_DELIMITER
341
                           . 'i';
342
343
            if (!preg_match($longPattern, $user_agent)) {
344
                continue;
345
            }
346
347
            foreach ($chunk as $pattern) {
348
                $patternToMatch = self::REGEX_DELIMITER . '^' . $pattern . '$' . self::REGEX_DELIMITER . 'i';
349
                $matches        = [];
350
351
                if (!preg_match($patternToMatch, $user_agent, $matches)) {
352
                    continue;
353
                }
354
355
                $patternData = $this->_patterns[$pattern];
356
357
                if (1 === count($matches)) {
358
                    // standard match
359
                    $key         = $patternData;
360
                    $simpleMatch = true;
361
                } else {
362
                    $patternData = unserialize($patternData);
363
364
                    // match with numeric replacements
365
                    array_shift($matches);
366
367
                    $matchString = self::COMPRESSION_PATTERN_START . implode(
368
                        self::COMPRESSION_PATTERN_DELIMITER,
369
                                                                             $matches
370
                    );
371
372
                    if (!isset($patternData[$matchString])) {
373
                        // partial match - numbers are not present, but everything else is ok
374
                        continue;
375
                    }
376
377
                    $key = $patternData[$matchString];
378
379
                    $simpleMatch = false;
380
                }
381
382
                $browser = [
383
                    $user_agent, // Original useragent
384
                    strtolower(trim($pattern, self::REGEX_DELIMITER)),
385
                    $this->_pregUnQuote($pattern, $simpleMatch ? false : $matches)
386
                ];
387
388
                $browser = $value = $browser + unserialize($this->_browsers[$key]);
389
390
                while (array_key_exists(3, $value)) {
391
                    $value = unserialize($this->_browsers[$value[3]]);
392
                    $browser += $value;
393
                }
394
395
                if (!empty($browser[3]) && array_key_exists($browser[3], $this->_userAgents)) {
396
                    $browser[3] = $this->_userAgents[$browser[3]];
397
                }
398
399
                break 2;
400
            }
401
        }
402
403
        // Add the keys for each property
404
        $array = [];
405
        foreach ($browser as $key => $value) {
406
            if ('true' === $value) {
407
                $value = true;
408
            } elseif ('false' === $value) {
409
                $value = false;
410
            }
411
412
            $propertyName = $this->_properties[$key];
413
414
            if ($this->lowercase) {
415
                $propertyName = strtolower($propertyName);
416
            }
417
418
            $array[$propertyName] = $value;
419
        }
420
421
        return $return_array ? $array : (object)$array;
422
    }
423
424
    /**
425
     * Load (auto-set) proxy settings from environment variables.
426
     */
427
    public function autodetectProxySettings()
428
    {
429
        $wrappers = ['http', 'https', 'ftp'];
430
431
        foreach ($wrappers as $wrapper) {
432
            $url = getenv($wrapper . '_proxy');
433
            if (!empty($url)) {
434
                $params = array_merge([
435
                                          'port' => null,
436
                                          'user' => null,
437
                                          'pass' => null
438
                                      ], parse_url($url));
439
                $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']);
440
            }
441
        }
442
    }
443
444
    /**
445
     * Add proxy settings to the stream context array.
446
     *
447
     * @param string $server   Proxy server/host
448
     * @param int    $port     Port
449
     * @param string $wrapper  Wrapper: "http", "https", "ftp", others...
450
     * @param string $username Username (when requiring authentication)
0 ignored issues
show
Should the type for parameter $username not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
451
     * @param string $password Password (when requiring authentication)
0 ignored issues
show
Should the type for parameter $password not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
452
     *
453
     * @return Browscap
454
     */
455
    public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null)
456
    {
457
        $settings = [
458
            $wrapper => [
459
                'proxy'           => sprintf('tcp://%s:%d', $server, $port),
460
                'request_fulluri' => true,
461
                'timeout'         => $this->timeout
462
            ]
463
        ];
464
465
        // Proxy authentication (optional)
466
        if (isset($username) && isset($password)) {
467
            $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic ' . base64_encode($username . ':' . $password);
468
        }
469
470
        // Add these new settings to the stream context options array
471
        $this->_streamContextOptions = array_merge($this->_streamContextOptions, $settings);
472
473
        /* Return $this so we can chain addProxySettings() calls like this:
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
474
         * $browscap->
475
         *   addProxySettings('http')->
476
         *   addProxySettings('https')->
477
         *   addProxySettings('ftp');
478
         */
479
480
        return $this;
481
    }
482
483
    /**
484
     * Clear proxy settings from the stream context options array.
485
     *
486
     * @param string $wrapper Remove settings from this wrapper only
0 ignored issues
show
Should the type for parameter $wrapper not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
487
     *
488
     * @return array Wrappers cleared
489
     */
490
    public function clearProxySettings($wrapper = null)
491
    {
492
        $wrappers = isset($wrapper) ? [$wrapper] : array_keys($this->_streamContextOptions);
493
494
        $clearedWrappers = [];
495
        $options         = ['proxy', 'request_fulluri', 'header'];
496
        foreach ($wrappers as $wrapper) {
497
498
            // remove wrapper options related to proxy settings
499
            if (isset($this->_streamContextOptions[$wrapper]['proxy'])) {
500
                foreach ($options as $option) {
501
                    unset($this->_streamContextOptions[$wrapper][$option]);
502
                }
503
504
                // remove wrapper entry if there are no other options left
505
                if (empty($this->_streamContextOptions[$wrapper])) {
506
                    unset($this->_streamContextOptions[$wrapper]);
507
                }
508
509
                $clearedWrappers[] = $wrapper;
510
            }
511
        }
512
513
        return $clearedWrappers;
514
    }
515
516
    /**
517
     * Returns the array of stream context options.
518
     *
519
     * @return array
520
     */
521
    public function getStreamContextOptions()
522
    {
523
        $streamContextOptions = $this->_streamContextOptions;
524
525
        if (empty($streamContextOptions)) {
526
            // set default context, including timeout
527
            $streamContextOptions = [
528
                'http' => [
529
                    'timeout' => $this->timeout
530
                ]
531
            ];
532
        }
533
534
        return $streamContextOptions;
535
    }
536
537
    /**
538
     * Parses the ini file and updates the cache files
539
     *
540
     * @throws Exception
541
     * @return bool whether the file was correctly written to the disk
542
     */
543
    public function updateCache()
544
    {
545
        $lockfile = $this->cacheDir . 'cache.lock';
546
547
        $lockRes = fopen($lockfile, 'w+');
548
        if (false === $lockRes) {
549
            throw new Exception(sprintf('error opening lockfile %s', $lockfile));
550
        }
551
        if (false === flock($lockRes, LOCK_EX | LOCK_NB)) {
552
            throw new Exception(sprintf('error locking lockfile %s', $lockfile));
553
        }
554
555
        $ini_path   = $this->cacheDir . $this->iniFilename;
556
        $cache_path = $this->cacheDir . $this->cacheFilename;
557
558
        // Choose the right url
559
        if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
560
            $url = realpath($this->localFile);
561
        } else {
562
            $url = $this->remoteIniUrl;
563
        }
564
565
        $this->_getRemoteIniFile($url, $ini_path);
566
567
        $this->_properties = [];
568
        $this->_browsers   = [];
569
        $this->_userAgents = [];
570
        $this->_patterns   = [];
571
572
        $iniContent = file_get_contents($ini_path);
573
574
        //$this->createCacheOldWay($iniContent);
0 ignored issues
show
Unused Code Comprehensibility introduced by
86% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
575
        $this->createCacheNewWay($iniContent);
576
577
        // Write out new cache file
578
        $dir = dirname($cache_path);
579
580
        // "tempnam" did not work with VFSStream for tests
581
        $tmpFile = $dir . '/temp_' . md5(time() . basename($cache_path));
582
583
        // asume that all will be ok
584
        if (false === ($fileRes = fopen($tmpFile, 'w+'))) {
585
            // opening the temparary file failed
586
            throw new Exception('opening temporary file failed');
587
        }
588
589
        if (false === fwrite($fileRes, $this->_buildCache())) {
590
            // writing to the temparary file failed
591
            throw new Exception('writing to temporary file failed');
592
        }
593
594
        fclose($fileRes);
595
596
        if (false === rename($tmpFile, $cache_path)) {
597
            // renaming file failed, remove temp file
598
            @unlink($tmpFile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
599
600
            throw new Exception('could not rename temporary file to the cache file');
601
        }
602
603
        @flock($lockRes, LOCK_UN);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
604
        @fclose($lockRes);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
605
        @unlink($lockfile);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
606
        $this->_cacheLoaded = false;
607
608
        return true;
609
    }
610
611
    /**
612
     * creates the cache content
613
     *
614
     * @param string $iniContent The content of the downloaded ini file
615
     * @param bool   $actLikeNewVersion
616
     */
617
    protected function createCacheOldWay($iniContent, $actLikeNewVersion = false)
618
    {
619
        $browsers = parse_ini_string($iniContent, true, INI_SCANNER_RAW);
620
621
        if ($actLikeNewVersion) {
622
            $this->_source_version = (int)$browsers[self::BROWSCAP_VERSION_KEY]['Version'];
623
        } else {
624
            $this->_source_version = $browsers[self::BROWSCAP_VERSION_KEY]['Version'];
625
        }
626
627
        unset($browsers[self::BROWSCAP_VERSION_KEY]);
628
629
        if (!$actLikeNewVersion) {
630
            unset($browsers['DefaultProperties']['RenderingEngine_Description']);
631
        }
632
633
        $this->_properties = array_keys($browsers['DefaultProperties']);
634
635
        array_unshift($this->_properties, 'browser_name', 'browser_name_regex', 'browser_name_pattern', 'Parent');
636
637
        $tmpUserAgents = array_keys($browsers);
638
639
        usort($tmpUserAgents, [$this, 'compareBcStrings']);
640
641
        $userAgentsKeys = array_flip($tmpUserAgents);
642
        $propertiesKeys = array_flip($this->_properties);
643
        $tmpPatterns    = [];
644
645
        foreach ($tmpUserAgents as $i => $userAgent) {
646
            $properties = $browsers[$userAgent];
647
648
            if (empty($properties['Comment'])
649
                || false !== strpos($userAgent, '*')
650
                || false !== strpos($userAgent, '?')
651
            ) {
652
                $pattern = $this->_pregQuote($userAgent);
653
654
                $countMatches = preg_match_all(
655
                    self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
656
                    $pattern,
657
                                               $matches
658
                );
659
660 View Code Duplication
                if (!$countMatches) {
0 ignored issues
show
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...
661
                    $tmpPatterns[$pattern] = $i;
662
                } else {
663
                    $compressedPattern = preg_replace(
664
                        self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
665
                        '(\d)',
666
                                                      $pattern
667
                    );
668
669
                    if (!isset($tmpPatterns[$compressedPattern])) {
670
                        $tmpPatterns[$compressedPattern] = ['first' => $pattern];
671
                    }
672
673
                    $tmpPatterns[$compressedPattern][$i] = $matches[0];
674
                }
675
            }
676
677 View Code Duplication
            if (!empty($properties['Parent'])) {
0 ignored issues
show
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...
678
                $parent = $properties['Parent'];
679
680
                $parentKey = $userAgentsKeys[$parent];
681
682
                $properties['Parent']                 = $parentKey;
683
                $this->_userAgents[$parentKey . '.0'] = $tmpUserAgents[$parentKey];
684
            };
685
686
            $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys);
687
        }
688
689
        // reducing memory usage by unsetting $tmp_user_agents
690
        unset($tmpUserAgents);
691
692
        $this->_patterns = $this->deduplicatePattern($tmpPatterns);
693
    }
694
695
    /**
696
     * creates the cache content
697
     *
698
     * @param string $iniContent The content of the downloaded ini file
699
     *
700
     * @throws \phpbrowscap\Exception
701
     */
702
    protected function createCacheNewWay($iniContent)
703
    {
704
        $patternPositions = [];
705
706
        // get all patterns from the ini file in the correct order,
707
        // so that we can calculate with index number of the resulting array,
708
        // which part to use when the ini file is split into its sections.
709
        preg_match_all('/(?<=\[)(?:[^\r\n]+)(?=\])/m', $iniContent, $patternPositions);
710
711
        if (!isset($patternPositions[0])) {
712
            throw new Exception('could not extract patterns from ini file');
713
        }
714
715
        $patternPositions = $patternPositions[0];
716
717
        if (!count($patternPositions)) {
718
            throw new Exception('no patterns were found inside the ini file');
719
        }
720
721
        // split the ini file into sections and save the data in one line with a hash of the belonging
722
        // pattern (filtered in the previous step)
723
        $iniParts       = preg_split('/\[[^\r\n]+\]/', $iniContent);
724
        $tmpPatterns    = [];
725
        $propertiesKeys = [];
726
        $matches        = [];
727
728
        if (preg_match('/.*\[DefaultProperties\]([^[]*).*/', $iniContent, $matches)) {
729
            $properties = parse_ini_string($matches[1], true, INI_SCANNER_RAW);
730
731
            $this->_properties = array_keys($properties);
732
733
            array_unshift($this->_properties, 'browser_name', 'browser_name_regex', 'browser_name_pattern', 'Parent');
734
735
            $propertiesKeys = array_flip($this->_properties);
736
        }
737
738
        $key                   = $this->_pregQuote(self::BROWSCAP_VERSION_KEY);
739
        $this->_source_version = 0;
740
        $matches               = [];
741
742
        if (preg_match("/\\.*[" . $key . "\\][^[]*Version=(\\d+)\\D.*/", $iniContent, $matches)) {
743
            if (isset($matches[1])) {
744
                $this->_source_version = (int)$matches[1];
745
            }
746
        }
747
748
        $userAgentsKeys = array_flip($patternPositions);
749
        foreach ($patternPositions as $position => $userAgent) {
750
            if (self::BROWSCAP_VERSION_KEY === $userAgent) {
751
                continue;
752
            }
753
754
            $properties = parse_ini_string($iniParts[$position + 1], true, INI_SCANNER_RAW);
755
756
            if (empty($properties['Comment'])
757
                || false !== strpos($userAgent, '*')
758
                || false !== strpos($userAgent, '?')
759
            ) {
760
                $pattern      = $this->_pregQuote(strtolower($userAgent));
761
                $matches      = [];
762
                $i            = $position - 1;
763
                $countMatches = preg_match_all(
764
                    self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
765
                    $pattern,
766
                                               $matches
767
                );
768
769 View Code Duplication
                if (!$countMatches) {
0 ignored issues
show
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...
770
                    $tmpPatterns[$pattern] = $i;
771
                } else {
772
                    $compressedPattern = preg_replace(
773
                        self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
774
                        '(\d)',
775
                                                      $pattern
776
                    );
777
778
                    if (!isset($tmpPatterns[$compressedPattern])) {
779
                        $tmpPatterns[$compressedPattern] = ['first' => $pattern];
780
                    }
781
782
                    $tmpPatterns[$compressedPattern][$i] = $matches[0];
783
                }
784
            }
785
786 View Code Duplication
            if (!empty($properties['Parent'])) {
0 ignored issues
show
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...
787
                $parent    = $properties['Parent'];
788
                $parentKey = $userAgentsKeys[$parent];
789
790
                $properties['Parent']                       = $parentKey - 1;
791
                $this->_userAgents[($parentKey - 1) . '.0'] = $patternPositions[$parentKey];
792
            };
793
794
            $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys);
795
        }
796
797
        $patternList = $this->deduplicatePattern($tmpPatterns);
798
799
        $positionIndex = [];
800
        $lengthIndex   = [];
801
        $shortLength   = [];
802
        $patternArray  = [];
803
        $counter       = 0;
804
805
        foreach (array_keys($patternList) as $pattern) {
806
            $decodedPattern = str_replace('(\d)', 0, $this->_pregUnQuote($pattern, false));
807
808
            // force "defaultproperties" (if available) to first position, and "*" to last position
809
            if ('defaultproperties' === $decodedPattern) {
810
                $positionIndex[$pattern] = 0;
811
            } elseif ('*' === $decodedPattern) {
812
                $positionIndex[$pattern] = 2;
813
            } else {
814
                $positionIndex[$pattern] = 1;
815
            }
816
817
            // sort by length
818
            $lengthIndex[$pattern] = strlen($decodedPattern);
819
            $shortLength[$pattern] = strlen(str_replace(['*', '?'], '', $decodedPattern));
820
821
            // sort by original order
822
            $patternArray[$pattern] = $counter;
823
824
            $counter++;
825
        }
826
827
        array_multisort(
828
            $positionIndex,
829
            SORT_ASC,
830
            SORT_NUMERIC,
831
            $lengthIndex,
832
            SORT_DESC,
833
            SORT_NUMERIC,
834
            $shortLength,
835
                        SORT_DESC,
836
            SORT_NUMERIC,
837
            $patternArray,
838
            SORT_ASC,
839
            SORT_NUMERIC,
840
            $patternList
841
        );
842
843
        $this->_patterns = $patternList;
844
    }
845
846
    /**
847
     * @param array $properties
848
     * @param array $propertiesKeys
849
     *
850
     * @return array
851
     */
852
    protected function resortProperties(array $properties, array $propertiesKeys)
853
    {
854
        $browser = [];
855
856
        foreach ($properties as $propertyName => $propertyValue) {
857
            if (!isset($propertiesKeys[$propertyName])) {
858
                continue;
859
            }
860
861
            $browser[$propertiesKeys[$propertyName]] = $propertyValue;
862
        }
863
864
        return $browser;
865
    }
866
867
    /**
868
     * @param array $tmpPatterns
869
     *
870
     * @return array
871
     */
872
    protected function deduplicatePattern(array $tmpPatterns)
873
    {
874
        $patternList = [];
875
876
        foreach ($tmpPatterns as $pattern => $patternData) {
877
            if (is_int($patternData)) {
878
                $data = $patternData;
879
            } elseif (2 == count($patternData)) {
880
                end($patternData);
881
882
                $pattern = $patternData['first'];
883
                $data    = key($patternData);
884
            } else {
885
                unset($patternData['first']);
886
887
                $data = $this->deduplicateCompressionPattern($patternData, $pattern);
888
            }
889
890
            $patternList[$pattern] = $data;
891
        }
892
893
        return $patternList;
894
    }
895
896
    /**
897
     * @param string $a
898
     * @param string $b
899
     *
900
     * @return int
901
     */
902
    protected function compareBcStrings($a, $b)
903
    {
904
        $a_len = strlen($a);
905
        $b_len = strlen($b);
906
907
        if ($a_len > $b_len) {
908
            return -1;
909
        }
910
911
        if ($a_len < $b_len) {
912
            return 1;
913
        }
914
915
        $a_len = strlen(str_replace(['*', '?'], '', $a));
916
        $b_len = strlen(str_replace(['*', '?'], '', $b));
917
918
        if ($a_len > $b_len) {
919
            return -1;
920
        }
921
922
        if ($a_len < $b_len) {
923
            return 1;
924
        }
925
926
        return 0;
927
    }
928
929
    /**
930
     * That looks complicated...
931
     *
932
     * All numbers are taken out into $matches, so we check if any of those numbers are identical
933
     * in all the $matches and if they are we restore them to the $pattern, removing from the $matches.
934
     * This gives us patterns with "(\d)" only in places that differ for some matches.
935
     *
936
     * @param array  $matches
937
     * @param string $pattern
938
     *
939
     * @return array of $matches
940
     */
941
    protected function deduplicateCompressionPattern($matches, &$pattern)
942
    {
943
        $tmp_matches = $matches;
944
        $first_match = array_shift($tmp_matches);
945
        $differences = [];
946
947
        foreach ($tmp_matches as $some_match) {
948
            $differences += array_diff_assoc($first_match, $some_match);
949
        }
950
951
        $identical = array_diff_key($first_match, $differences);
952
953
        $prepared_matches = [];
954
955
        foreach ($matches as $i => $some_match) {
956
            $key = self::COMPRESSION_PATTERN_START . implode(
957
                self::COMPRESSION_PATTERN_DELIMITER,
958
                                                             array_diff_assoc($some_match, $identical)
959
            );
960
961
            $prepared_matches[$key] = $i;
962
        }
963
964
        $pattern_parts = explode('(\d)', $pattern);
965
966
        foreach ($identical as $position => $value) {
967
            $pattern_parts[$position + 1] = $pattern_parts[$position] . $value . $pattern_parts[$position + 1];
968
            unset($pattern_parts[$position]);
969
        }
970
971
        $pattern = implode('(\d)', $pattern_parts);
972
973
        return $prepared_matches;
974
    }
975
976
    /**
977
     * Converts browscap match patterns into preg match patterns.
978
     *
979
     * @param string $user_agent
980
     *
981
     * @return string
982
     */
983
    protected function _pregQuote($user_agent)
984
    {
985
        $pattern = preg_quote($user_agent, self::REGEX_DELIMITER);
986
987
        // the \\x replacement is a fix for "Der gro\xdfe BilderSauger 2.00u" user agent match
988
989
        return str_replace(['\*', '\?', '\\x'], ['.*', '.', '\\\\x'], $pattern);
990
    }
991
992
    /**
993
     * Converts preg match patterns back to browscap match patterns.
994
     *
995
     * @param string        $pattern
996
     * @param array|boolean $matches
997
     *
998
     * @return string
999
     */
1000
    protected function _pregUnQuote($pattern, $matches)
1001
    {
1002
        // list of escaped characters: http://www.php.net/manual/en/function.preg-quote.php
1003
        // to properly unescape '?' which was changed to '.', I replace '\.' (real dot) with '\?',
1004
        // then change '.' to '?' and then '\?' to '.'.
1005
        $search  = [
1006
            '\\' . self::REGEX_DELIMITER,
1007
            '\\.',
1008
            '\\\\',
1009
            '\\+',
1010
            '\\[',
1011
            '\\^',
1012
            '\\]',
1013
            '\\$',
1014
            '\\(',
1015
            '\\)',
1016
            '\\{',
1017
            '\\}',
1018
            '\\=',
1019
            '\\!',
1020
            '\\<',
1021
            '\\>',
1022
            '\\|',
1023
            '\\:',
1024
            '\\-',
1025
            '.*',
1026
            '.',
1027
            '\\?'
1028
        ];
1029
        $replace = [
1030
            self::REGEX_DELIMITER,
1031
            '\\?',
1032
            '\\',
1033
            '+',
1034
            '[',
1035
            '^',
1036
            ']',
1037
            '$',
1038
            '(',
1039
            ')',
1040
            '{',
1041
            '}',
1042
            '=',
1043
            '!',
1044
            '<',
1045
            '>',
1046
            '|',
1047
            ':',
1048
            '-',
1049
            '*',
1050
            '?',
1051
            '.'
1052
        ];
1053
1054
        $result = substr(str_replace($search, $replace, $pattern), 2, -2);
1055
1056
        if ($matches) {
1057
            foreach ($matches as $oneMatch) {
0 ignored issues
show
The expression $matches of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1058
                $position = strpos($result, '(\d)');
1059
                $result   = substr_replace($result, $oneMatch, $position, 4);
1060
            }
1061
        }
1062
1063
        return $result;
1064
    }
1065
1066
    /**
1067
     * Loads the cache into object's properties
1068
     *
1069
     * @param string $cache_file
1070
     *
1071
     * @return boolean
1072
     */
1073
    protected function _loadCache($cache_file)
1074
    {
1075
        $cache_version  = null;
1076
        $source_version = null;
1077
        $browsers       = [];
1078
        $userAgents     = [];
1079
        $patterns       = [];
1080
        $properties     = [];
1081
1082
        $this->_cacheLoaded = false;
1083
1084
        require $cache_file;
1085
1086
        if (!isset($cache_version) || $cache_version != self::CACHE_FILE_VERSION) {
1087
            return false;
1088
        }
1089
1090
        $this->_source_version = $source_version;
1091
        $this->_browsers       = $browsers;
1092
        $this->_userAgents     = $userAgents;
1093
        $this->_patterns       = $patterns;
1094
        $this->_properties     = $properties;
1095
1096
        $this->_cacheLoaded = true;
1097
1098
        return true;
1099
    }
1100
1101
    /**
1102
     * Parses the array to cache and writes the resulting PHP string to disk
1103
     *
1104
     * @return boolean False on write error, true otherwise
0 ignored issues
show
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1105
     */
1106
    protected function _buildCache()
1107
    {
1108
        $content = sprintf(
1109
            "<?php\n\$source_version=%s;\n\$cache_version=%s",
1110
            "'" . $this->_source_version . "'",
1111
                           "'" . self::CACHE_FILE_VERSION . "'"
1112
        );
1113
1114
        $content .= ";\n\$properties=";
1115
        $content .= $this->_array2string($this->_properties);
1116
1117
        $content .= ";\n\$browsers=";
1118
        $content .= $this->_array2string($this->_browsers);
1119
1120
        $content .= ";\n\$userAgents=";
1121
        $content .= $this->_array2string($this->_userAgents);
1122
1123
        $content .= ";\n\$patterns=";
1124
        $content .= $this->_array2string($this->_patterns) . ";\n";
1125
1126
        return $content;
1127
    }
1128
1129
    /**
1130
     * Lazy getter for the stream context resource.
1131
     *
1132
     * @param bool $recreate
1133
     *
1134
     * @return resource
1135
     */
1136
    protected function _getStreamContext($recreate = false)
1137
    {
1138
        if (!isset($this->_streamContext) || true === $recreate) {
1139
            $this->_streamContext = stream_context_create($this->getStreamContextOptions());
1140
        }
1141
1142
        return $this->_streamContext;
1143
    }
1144
1145
    /**
1146
     * Updates the local copy of the ini file (by version checking) and adapts
1147
     * his syntax to the PHP ini parser
1148
     *
1149
     * @param string $url  the url of the remote server
1150
     * @param string $path the path of the ini file to update
1151
     *
1152
     * @throws Exception
1153
     * @return bool if the ini file was updated
1154
     */
1155
    protected function _getRemoteIniFile($url, $path)
1156
    {
1157
        // local and remote file are the same, no update possible
1158
        if ($url == $path) {
1159
            return false;
1160
        }
1161
1162
        // Check version
1163
        if (file_exists($path) && filesize($path)) {
1164
            $local_tmstp = filemtime($path);
1165
1166
            if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
1167
                $remote_tmstp = $this->_getLocalMTime();
1168
            } else {
1169
                $remote_tmstp = $this->_getRemoteMTime();
1170
            }
1171
1172
            if ($remote_tmstp <= $local_tmstp) {
1173
                // No update needed, return
1174
                touch($path);
1175
1176
                return false;
1177
            }
1178
        }
1179
1180
        // Check if it's possible to write to the .ini file.
1181
        if (is_file($path)) {
1182
            if (!is_writable($path)) {
1183
                throw new Exception('Could not write to "' . $path
1184
                                    . '" (check the permissions of the current/old ini file).');
1185
            }
1186
        } else {
1187
            // Test writability by creating a file only if one already doesn't exist, so we can safely delete it after
1188
            // the test.
1189
            $test_file = fopen($path, 'a');
1190
            if ($test_file) {
1191
                fclose($test_file);
1192
                unlink($path);
1193
            } else {
1194
                throw new Exception('Could not write to "' . $path
1195
                                    . '" (check the permissions of the cache directory).');
1196
            }
1197
        }
1198
1199
        // Get updated .ini file
1200
        $content = $this->_getRemoteData($url);
1201
1202
        if (!is_string($content) || strlen($content) < 1) {
1203
            throw new Exception('Could not load .ini content from "' . $url . '"');
1204
        }
1205
1206
        if (false !== strpos('rate limit', $content)) {
1207
            throw new Exception('Could not load .ini content from "' . $url
1208
                                . '" because the rate limit is exeeded for your IP');
1209
        }
1210
1211
        // replace opening and closing php and asp tags
1212
        $content = $this->sanitizeContent($content);
1213
1214
        if (!file_put_contents($path, $content)) {
1215
            throw new Exception('Could not write .ini content to "' . $path . '"');
1216
        }
1217
1218
        return true;
1219
    }
1220
1221
    /**
1222
     * @param string $content
1223
     *
1224
     * @return mixed
1225
     */
1226
    protected function sanitizeContent($content)
1227
    {
1228
        // replace everything between opening and closing php and asp tags
1229
        $content = preg_replace('/<[?%].*[?%]>/', '', $content);
1230
1231
        // replace opening and closing php and asp tags
1232
        return str_replace(['<?', '<%', '?>', '%>'], '', $content);
1233
    }
1234
1235
    /**
1236
     * Gets the remote ini file update timestamp
1237
     *
1238
     * @throws Exception
1239
     * @return int the remote modification timestamp
1240
     */
1241
    protected function _getRemoteMTime()
1242
    {
1243
        $remote_datetime = $this->_getRemoteData($this->remoteVerUrl);
1244
        $remote_tmstp    = strtotime($remote_datetime);
1245
1246
        if (!$remote_tmstp) {
1247
            throw new Exception("Bad datetime format from {$this->remoteVerUrl}");
1248
        }
1249
1250
        return $remote_tmstp;
1251
    }
1252
1253
    /**
1254
     * Gets the local ini file update timestamp
1255
     *
1256
     * @throws Exception
1257
     * @return int the local modification timestamp
1258
     */
1259
    protected function _getLocalMTime()
1260
    {
1261
        if (!is_readable($this->localFile) || !is_file($this->localFile)) {
1262
            throw new Exception('Local file is not readable');
1263
        }
1264
1265
        return filemtime($this->localFile);
1266
    }
1267
1268
    /**
1269
     * Converts the given array to the PHP string which represent it.
1270
     * This method optimizes the PHP code and the output differs form the
1271
     * var_export one as the internal PHP function does not strip whitespace or
1272
     * convert strings to numbers.
1273
     *
1274
     * @param array $array The array to parse and convert
1275
     *
1276
     * @return boolean False on write error, true otherwise
0 ignored issues
show
Should the return type not be string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
1277
     */
1278
    protected function _array2string($array)
1279
    {
1280
        $content = "array(\n";
1281
1282
        foreach ($array as $key => $value) {
1283
            if (is_int($key)) {
1284
                $key = '';
1285
            } elseif (ctype_digit((string)$key)) {
1286
                $key = (int)$key . ' => ';
1287
            } elseif ('.0' === substr($key, -2) && !preg_match('/[^\d\.]/', $key)) {
1288
                $key = (int)$key . ' => ';
1289
            } else {
1290
                $key = "'" . str_replace("'", "\'", $key) . "' => ";
1291
            }
1292
1293
            if (is_array($value)) {
1294
                $value = "'" . addcslashes(serialize($value), "'") . "'";
1295
            } elseif (ctype_digit((string)$value)) {
1296
                $value = (int)$value;
1297
            } else {
1298
                $value = "'" . str_replace("'", "\'", $value) . "'";
1299
            }
1300
1301
            $content .= $key . $value . ",\n";
1302
        }
1303
1304
        $content .= "\n)";
1305
1306
        return $content;
1307
    }
1308
1309
    /**
1310
     * Checks for the various possibilities offered by the current configuration
1311
     * of PHP to retrieve external HTTP data
1312
     *
1313
     * @return string|false the name of function to use to retrieve the file or false if no methods are available
1314
     */
1315
    protected function _getUpdateMethod()
1316
    {
1317
        // Caches the result
1318
        if (null === $this->updateMethod) {
1319
            if (null !== $this->localFile) {
1320
                $this->updateMethod = self::UPDATE_LOCAL;
1321
            } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) {
1322
                $this->updateMethod = self::UPDATE_FOPEN;
1323
            } elseif (function_exists('fsockopen')) {
1324
                $this->updateMethod = self::UPDATE_FSOCKOPEN;
1325
            } elseif (extension_loaded('curl')) {
1326
                $this->updateMethod = self::UPDATE_CURL;
1327
            } else {
1328
                $this->updateMethod = false;
1329
            }
1330
        }
1331
1332
        return $this->updateMethod;
1333
    }
1334
1335
    /**
1336
     * Retrieve the data identified by the URL
1337
     *
1338
     * @param string $url the url of the data
1339
     *
1340
     * @throws Exception
1341
     * @return string the retrieved data
1342
     */
1343
    protected function _getRemoteData($url)
1344
    {
1345
        ini_set('user_agent', $this->_getUserAgent());
1346
1347
        switch ($this->_getUpdateMethod()) {
1348
            case self::UPDATE_LOCAL:
1349
                $file = file_get_contents($url);
1350
1351
                if (false !== $file) {
1352
                    return $file;
1353
                }
1354
1355
                throw new Exception('Cannot open the local file');
1356
            case self::UPDATE_FOPEN:
1357
                if (ini_get('allow_url_fopen') && function_exists('file_get_contents')) {
1358
                    // include proxy settings in the file_get_contents() call
1359
                    $context = $this->_getStreamContext();
1360
                    $file    = file_get_contents($url, false, $context);
1361
1362
                    if (false !== $file) {
1363
                        return $file;
1364
                    }
1365
                }// else try with the next possibility (break omitted)
1366
                // no break
1367
            case self::UPDATE_FSOCKOPEN:
1368
                if (function_exists('fsockopen')) {
1369
                    $remote_url     = parse_url($url);
1370
                    $contextOptions = $this->getStreamContextOptions();
1371
1372
                    $errno  = 0;
1373
                    $errstr = '';
1374
1375
                    if (empty($contextOptions)) {
1376
                        $port           = (empty($remote_url['port']) ? 80 : $remote_url['port']);
1377
                        $remote_handler = fsockopen($remote_url['host'], $port, $errno, $errstr, $this->timeout);
1378
                    } else {
1379
                        $context = $this->_getStreamContext();
1380
1381
                        $remote_handler = stream_socket_client(
1382
                            $url,
1383
                            $errno,
1384
                            $errstr,
1385
                            $this->timeout,
1386
                                                               STREAM_CLIENT_CONNECT,
1387
                            $context
1388
                        );
1389
                    }
1390
1391
                    if ($remote_handler) {
1392
                        stream_set_timeout($remote_handler, $this->timeout);
1393
1394
                        if (isset($remote_url['query'])) {
1395
                            $remote_url['path'] .= '?' . $remote_url['query'];
1396
                        }
1397
1398
                        $out = sprintf(
1399
                            self::REQUEST_HEADERS,
1400
                            $remote_url['path'],
1401
                            $remote_url['host'],
1402
                                       $this->_getUserAgent()
1403
                        );
1404
1405
                        fwrite($remote_handler, $out);
1406
1407
                        $response = fgets($remote_handler);
1408
                        if (false !== strpos($response, '200 OK')) {
1409
                            $file = '';
1410
                            while (!feof($remote_handler)) {
1411
                                $file .= fgets($remote_handler);
1412
                            }
1413
1414
                            $file = str_replace("\r\n", "\n", $file);
1415
                            $file = explode("\n\n", $file);
1416
                            array_shift($file);
1417
1418
                            $file = implode("\n\n", $file);
1419
1420
                            fclose($remote_handler);
1421
1422
                            return $file;
1423
                        }
1424
                    }
1425
                }// else try with the next possibility
1426
                // no break
1427
            case self::UPDATE_CURL:
1428
                if (extension_loaded('curl')) { // make sure curl is loaded
1429
                    $ch = curl_init($url);
1430
1431
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1432
                    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
1433
                    curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent());
1434
1435
                    $file = curl_exec($ch);
1436
1437
                    curl_close($ch);
1438
1439
                    if (false !== $file) {
1440
                        return $file;
1441
                    }
1442
                }// else try with the next possibility
1443
                // no break
1444
            case false:
0 ignored issues
show
It seems like you are loosely comparing $this->_getUpdateMethod() of type string|false against false; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
1445
                throw new Exception('Your server can\'t connect to external resources. Please update the file manually.');
1446
        }
1447
1448
        return '';
1449
    }
1450
1451
    /**
1452
     * Format the useragent string to be used in the remote requests made by the
1453
     * class during the update process.
1454
     *
1455
     * @return string the formatted user agent
1456
     */
1457
    protected function _getUserAgent()
1458
    {
1459
        $ua = str_replace('%v', self::VERSION, $this->userAgent);
1460
        $ua = str_replace('%m', $this->_getUpdateMethod(), $ua);
1461
1462
        return $ua;
1463
    }
1464
}
1465
1466
/**
1467
 * Browscap.ini parsing class exception
1468
 *
1469
 * @package    Browscap
1470
 * @author     Jonathan Stoppani <[email protected]>
1471
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
1472
 * @version    1.0
1473
 * @license    http://www.opensource.org/licenses/MIT MIT License
1474
 * @link       https://github.com/GaretJax/phpbrowscap/
1475
 */
1476
class Exception extends \Exception
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1477
{
1478
    // nothing to do here
1479
}
1480