Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
/**
3
 * Browscap.ini parsing class with caching and update capabilities
4
 *
5
 * PHP version 5
6
 *
7
 * Copyright (c) 2006-2012 Jonathan Stoppani
8
 *
9
 * Permission is hereby granted, free of charge, to any person obtaining a
10
 * copy of this software and associated documentation files (the "Software"),
11
 * to deal in the Software without restriction, including without limitation
12
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
13
 * and/or sell copies of the Software, and to permit persons to whom the
14
 * Software is furnished to do so, subject to the following conditions:
15
 *
16
 * The above copyright notice and this permission notice shall be included
17
 * in all copies or substantial portions of the Software.
18
 *
19
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
20
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25
 * THE SOFTWARE.
26
 *
27
 * @package    report_deviceanalytics
28
 * @subpackage Browscap
29
 * @author     Jonathan Stoppani <jonathan@stoppani.name>
30
 * @author     Vítor Brandão <noisebleed@noiselabs.org>
31
 * @author     Mikołaj Misiurewicz <quentin389+phpb@gmail.com>
32
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
33
 * @license    http://www.opensource.org/licenses/MIT MIT License
34
 * @link       https://github.com/GaretJax/phpbrowscap/
35
 */
36
 
37
namespace phpbrowscap;
38
 
39
/**
40
 * Browscap Class
41
 *
42
 * @package    report_deviceanalytics
43
 * @subpackage Browscap
44
 * @author     Jonathan Stoppani <jonathan@stoppani.name>
45
 * @author     Vítor Brandão <noisebleed@noiselabs.org>
46
 * @author     Mikołaj Misiurewicz <quentin389+phpb@gmail.com>
47
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
48
 * @license    http://www.opensource.org/licenses/MIT MIT License
49
 * @link       https://github.com/GaretJax/phpbrowscap/
50
 */
51
class Browscap
52
{
53
    /**
54
     * Current version of the class.
55
     */
56
    const VERSION = '2.1.1';
57
 
58
    /**
59
     * Current version of the cache system
60
     */
61
    const CACHE_FILE_VERSION = '2.1.0';
62
 
63
    /**
64
     * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents).
65
     */
66
    const UPDATE_FOPEN     = 'URL-wrapper';
67
    /**
68
     * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen).
69
     */
70
    const UPDATE_FSOCKOPEN = 'socket';
71
    /**
72
     * UPDATE_CURL: Uses the cURL extension.
73
     */
74
    const UPDATE_CURL      = 'cURL';
75
    /**
76
     * UPDATE_LOCAL: Updates from a local file (file_get_contents).
77
     */
78
    const UPDATE_LOCAL     = 'local';
79
 
80
    /**
81
     * Options for regex patterns.
82
     *
83
     * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class.
84
     */
85
    const REGEX_DELIMITER               = '@';
86
    /**
87
     * Options for regex patterns.
88
     *
89
     * REGEX_MODIFIERS: Regex modifiers.
90
     */
91
    const REGEX_MODIFIERS               = 'i';
92
    /**
93
     * Options for regex patterns.
94
     *
95
     * COMPRESSION_PATTERN_START: Compression modifiers.
96
     */
97
    const COMPRESSION_PATTERN_START     = '@';
98
    /**
99
     * Options for regex patterns.
100
     *
101
     * COMPRESSION_PATTERN_DELIMITER: Compression Delimiter.
102
     */
103
    const COMPRESSION_PATTERN_DELIMITER = '|';
104
 
105
    /**
106
     * The values to quote in the ini file
107
     */
108
    const VALUES_TO_QUOTE = 'Browser|Parent';
109
 
110
    /**
111
     * The version key
112
     */
113
    const BROWSCAP_VERSION_KEY = 'GJK_Browscap_Version';
114
 
115
    /**
116
     * The headers to be sent for checking the version and requesting the file.
117
     */
118
    const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n";
119
 
120
    /**
121
     * how many pattern should be checked at once in the first step
122
     */
123
    const COUNT_PATTERN = 100;
124
 
125
    /**
126
     * @var $remoteIniUrl: The location from which download the ini file.
127
     *                     The placeholder for the file should be represented by a %s.
128
     */
129
    public $remoteIniUrl = 'http://browscap.org/stream?q=PHP_BrowscapINI';
130
    /**
131
     * @var $remoteVerUrl: The location to use to check out if a new version of the
132
     *                     browscap.ini file is available.
133
     */
134
    public $remoteVerUrl = 'http://browscap.org/version';
135
    /**
136
     * @var $timeout: The timeout for the requests.
137
     */
138
    public $timeout = 5;
139
    /**
140
     * @var $updateInterval: The update interval in seconds.
141
     */
142
    public $updateInterval = 432000;
143
    /**
144
     * @var $errorInterval: The next update interval in seconds in case of an error.
145
     */
146
    public $errorInterval = 7200;
147
    /**
148
     * @var $doAutoUpdate: Flag to disable the automatic interval based update.
149
     */
150
    public $doAutoUpdate = true;
151
    /**
152
     * @var $updateMethod: The method to use to update the file, has to be a value of
153
     *                an UPDATE_* constant, null or false.
154
     */
155
    public $updateMethod = null;
156
 
157
    /**
158
     * The path of the local version of the browscap.ini file from which to
159
     * update (to be set only if used).
160
     *
161
     * @var string
162
     */
163
    public $localFile = null;
164
 
165
    /**
166
     * The useragent to include in the requests made by the class during the
167
     * update process.
168
     *
169
     * @var string
170
     */
171
    public $userAgent = 'http://browscap.org/ - PHP Browscap/%v %m';
172
 
173
    /**
174
     * Flag to enable only lowercase indexes in the result.
175
     * The cache has to be rebuilt in order to apply this option.
176
     *
177
     * @var bool
178
     */
179
    public $lowercase = false;
180
 
181
    /**
182
     * Flag to enable/disable silent error management.
183
     * In case of an error during the update process the class returns an empty
184
     * array/object if the update process can't take place and the browscap.ini
185
     * file does not exist.
186
     *
187
     * @var bool
188
     */
189
    public $silent = false;
190
 
191
    /**
192
     * Where to store the cached PHP arrays.
193
     *
194
     * @var string
195
     */
196
    public $cacheFilename = 'cache.php';
197
 
198
    /**
199
     * Where to store the downloaded ini file.
200
     *
201
     * @var string
202
     */
203
    public $iniFilename = 'browscap.ini';
204
 
205
    /**
206
     * Path to the cache directory
207
     *
208
     * @var string
209
     */
210
    public $cacheDir = null;
211
 
212
    /**
213
     * Flag to be set to true after loading the cache
214
     *
215
     * @var bool
216
     */
217
    protected $_cacheLoaded = false;
218
 
219
    /**
220
     * Where to store the value of the included PHP cache file
221
     *
222
     * @var array
223
     */
224
    protected $_userAgents = array();
225
    /**
226
     * Where to store browsers
227
     *
228
     * @var array
229
     */
230
    protected $_browsers = array();
231
    /**
232
     * Where to store patterns
233
     *
234
     * @var array
235
     */
236
    protected $_patterns = array();
237
    /**
238
     * Where to store properties
239
     *
240
     * @var array
241
     */
242
    protected $_properties = array();
243
    /**
244
     * Where to store the source version
245
     *
246
     * @var array
247
     */
248
    protected $_source_version;
249
 
250
    /**
251
     * An associative array of associative arrays in the format
252
     * `$arr['wrapper']['option'] = $value` passed to stream_context_create()
253
     * when building a stream resource.
254
     *
255
     * Proxy settings are stored in this variable.
256
     *
257
     * @see http://www.php.net/manual/en/function.stream-context-create.php
258
     * @var array
259
     */
260
    protected $_streamContextOptions = array();
261
 
262
    /**
263
     * A valid context resource created with stream_context_create().
264
     *
265
     * @see http://www.php.net/manual/en/function.stream-context-create.php
266
     * @var resource
267
     */
268
    protected $_streamContext = null;
269
 
270
    /**
271
     * Constructor class, checks for the existence of (and loads) the cache and
272
     * if needed updated the definitions
273
     *
274
     * @param string $cache_dir
275
     *
276
     * @throws Exception
277
     */
278
    public function __construct($cache_dir = null)
279
    {
280
        // has to be set to reach E_STRICT compatibility, does not affect system/app settings
281
        date_default_timezone_set(date_default_timezone_get());
282
 
283
        if (!isset($cache_dir)) {
284
            throw new Exception('You have to provide a path to read/store the browscap cache file');
285
        }
286
 
287
        $old_cache_dir = $cache_dir;
288
        $cache_dir     = realpath($cache_dir);
289
 
290
        if (false === $cache_dir) {
291
            throw new Exception(
292
                sprintf(
293
                    'The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?',
294
                    $old_cache_dir
295
                )
296
            );
297
        }
298
 
299
        // Is the cache dir really the directory or is it directly the file?
300
        if (substr($cache_dir, -4) === '.php') {
301
            $this->cacheFilename = basename($cache_dir);
302
            $this->cacheDir      = dirname($cache_dir);
303
        } else {
304
            $this->cacheDir = $cache_dir;
305
        }
306
 
307
        $this->cacheDir .= DIRECTORY_SEPARATOR;
308
    }
309
 
310
    /**
311
     * Get the current source version
312
     * @return mixed
313
     */
314
    public function getSourceVersion()
315
    {
316
        return $this->_source_version;
317
    }
318
 
319
    /**
320
     * Check if the cache needs to be updated
321
     * @return bool
322
     */
323
    public function shouldCacheBeUpdated()
324
    {
325
        // Load the cache at the first request
326
        if ($this->_cacheLoaded) {
327
            return false;
328
        }
329
 
330
        $cache_file = $this->cacheDir . $this->cacheFilename;
331
        $ini_file   = $this->cacheDir . $this->iniFilename;
332
 
333
        // Set the interval only if needed
334
        if ($this->doAutoUpdate && file_exists($ini_file)) {
335
            $interval = time() - filemtime($ini_file);
336
        } else {
337
            $interval = 0;
338
        }
339
 
340
        $shouldBeUpdated = true;
341
 
342
        if (file_exists($cache_file) && file_exists($ini_file) && ($interval <= $this->updateInterval)) {
343
            if ($this->_loadCache($cache_file)) {
344
                $shouldBeUpdated = false;
345
            }
346
        }
347
 
348
        return $shouldBeUpdated;
349
    }
350
 
351
    /**
352
     * Gets the information about the browser by User Agent
353
     *
354
     * @param string $user_agent   the user agent string
355
     * @param bool   $return_array whether return an array or an object
356
     *
357
     * @throws Exception
358
     * @return \stdClass|array  the object containing the browsers details. Array if
359
     *                    $return_array is set to true.
360
     */
361
    public function getBrowser($user_agent = null, $return_array = false)
362
    {
363
        if ($this->shouldCacheBeUpdated()) {
364
            try {
365
                $this->updateCache();
366
            } catch (Exception $e) {
367
                $ini_file = $this->cacheDir . $this->iniFilename;
368
 
369
                if (file_exists($ini_file)) {
370
                    // Adjust the filemtime to the $errorInterval
371
                    touch($ini_file, time() - $this->updateInterval + $this->errorInterval);
372
                } elseif ($this->silent) {
373
                    // Return an array if silent mode is active and the ini db doesn't exsist
374
                    return array();
375
                }
376
 
377
                if (!$this->silent) {
378
                    throw $e;
379
                }
380
            }
381
        }
382
 
383
        $cache_file = $this->cacheDir . $this->cacheFilename;
384
        if (!$this->_cacheLoaded && !$this->_loadCache($cache_file)) {
385
            throw new Exception('Cannot load cache file - the cache format is not compatible.');
386
        }
387
 
388
        // Automatically detect the useragent
389
        if (!isset($user_agent)) {
390
            if (isset($_SERVER['HTTP_USER_AGENT'])) {
391
                $user_agent = $_SERVER['HTTP_USER_AGENT'];
392
            } else {
393
                $user_agent = '';
394
            }
395
        }
396
 
397
        $browser = array();
398
 
399
        $patterns = array_keys($this->_patterns);
400
        $chunks   = array_chunk($patterns, self::COUNT_PATTERN);
401
 
402
        foreach ($chunks as $chunk) {
403
            $longPattern = self::REGEX_DELIMITER
404
                . '^(?:' . implode(')|(?:', $chunk) . ')$'
405
                . self::REGEX_DELIMITER . 'i';
406
 
407
            if (!preg_match($longPattern, $user_agent)) {
408
                continue;
409
            }
410
 
411
            foreach ($chunk as $pattern) {
412
                $patternToMatch = self::REGEX_DELIMITER . '^' . $pattern . '$' . self::REGEX_DELIMITER . 'i';
413
                $matches        = array();
414
 
415
                if (!preg_match($patternToMatch, $user_agent, $matches)) {
416
                    continue;
417
                }
418
 
419
                $patternData = $this->_patterns[$pattern];
420
 
421
                if (1 === count($matches)) {
422
                    // standard match
423
                    $key         = $patternData;
424
                    $simpleMatch = true;
425
                } else {
426
                    $patternData = unserialize($patternData);
427
 
428
                    // match with numeric replacements
429
                    array_shift($matches);
430
 
431
                    $matchString = self::COMPRESSION_PATTERN_START
432
                        . implode(self::COMPRESSION_PATTERN_DELIMITER, $matches);
433
 
434
                    if (!isset($patternData[$matchString])) {
435
                        // partial match - numbers are not present, but everything else is ok
436
                        continue;
437
                    }
438
 
439
                    $key = $patternData[$matchString];
440
 
441
                    $simpleMatch = false;
442
                }
443
 
444
                $browser = array(
445
                    $user_agent, // Original useragent
446
                    trim(strtolower($pattern), self::REGEX_DELIMITER),
447
                    $this->_pregUnQuote($pattern, $simpleMatch ? false : $matches)
448
                );
449
 
450
                $browser = $value = $browser + unserialize($this->_browsers[$key]);
451
 
452
                while (array_key_exists(3, $value)) {
453
                    $value = unserialize($this->_browsers[$value[3]]);
454
                    $browser += $value;
455
                }
456
 
457
                if (!empty($browser[3]) && array_key_exists($browser[3], $this->_userAgents)) {
458
                    $browser[3] = $this->_userAgents[$browser[3]];
459
                }
460
 
461
                break 2;
462
            }
463
        }
464
 
465
        // Add the keys for each property
466
        $array = array();
467
        foreach ($browser as $key => $value) {
468
            if ($value === 'true') {
469
                $value = true;
470
            } elseif ($value === 'false') {
471
                $value = false;
472
            }
473
 
474
            $propertyName = $this->_properties[$key];
475
 
476
            if ($this->lowercase) {
477
                $propertyName = strtolower($propertyName);
478
            }
479
 
480
            $array[$propertyName] = $value;
481
        }
482
 
483
        return $return_array ? $array : (object) $array;
484
    }
485
 
486
    /**
487
     * Load (auto-set) proxy settings from environment variables.
488
     */
489
    public function autodetectProxySettings()
490
    {
491
        $wrappers = array('http', 'https', 'ftp');
492
 
493
        foreach ($wrappers as $wrapper) {
494
            $url = getenv($wrapper . '_proxy');
495
            if (!empty($url)) {
496
                $params = array_merge(
497
                    array(
498
                        'port' => null,
499
                        'user' => null,
500
                        'pass' => null,
501
                    ),
502
                    parse_url($url)
503
                );
504
                $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']);
505
            }
506
        }
507
    }
508
 
509
    /**
510
     * Add proxy settings to the stream context array.
511
     *
512
     * @param string $server   Proxy server/host
513
     * @param int    $port     Port
514
     * @param string $wrapper  Wrapper: "http", "https", "ftp", others...
515
     * @param string $username Username (when requiring authentication)
516
     * @param string $password Password (when requiring authentication)
517
     *
518
     * @return Browscap
519
     */
520
    public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null)
521
    {
522
        $settings = array(
523
            $wrapper => array(
524
                'proxy'           => sprintf('tcp://%s:%d', $server, $port),
525
                'request_fulluri' => true,
526
                'timeout'         => $this->timeout,
527
            )
528
        );
529
 
530
        // Proxy authentication (optional)
531
        if (isset($username) && isset($password)) {
532
            $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic ' . base64_encode($username . ':' . $password);
533
        }
534
 
535
        // Add these new settings to the stream context options array
536
        $this->_streamContextOptions = array_merge(
537
            $this->_streamContextOptions,
538
            $settings
539
        );
540
 
541
        /* Return $this so we can chain addProxySettings() calls like this:
542
         * $browscap->
543
         *   addProxySettings('http')->
544
         *   addProxySettings('https')->
545
         *   addProxySettings('ftp');
546
         */
547
        return $this;
548
    }
549
 
550
    /**
551
     * Clear proxy settings from the stream context options array.
552
     *
553
     * @param string $wrapper Remove settings from this wrapper only
554
     *
555
     * @return array Wrappers cleared
556
     */
557
    public function clearProxySettings($wrapper = null)
558
    {
559
        $wrappers = isset($wrapper) ? array($wrapper) : array_keys($this->_streamContextOptions);
560
 
561
        $clearedWrappers = array();
562
        $options         = array('proxy', 'request_fulluri', 'header');
563
        foreach ($wrappers as $wrapper) {
564
 
565
            // remove wrapper options related to proxy settings
566
            if (isset($this->_streamContextOptions[$wrapper]['proxy'])) {
567
                foreach ($options as $option) {
568
                    unset($this->_streamContextOptions[$wrapper][$option]);
569
                }
570
 
571
                // remove wrapper entry if there are no other options left
572
                if (empty($this->_streamContextOptions[$wrapper])) {
573
                    unset($this->_streamContextOptions[$wrapper]);
574
                }
575
 
576
                $clearedWrappers[] = $wrapper;
577
            }
578
        }
579
 
580
        return $clearedWrappers;
581
    }
582
 
583
    /**
584
     * Returns the array of stream context options.
585
     *
586
     * @return array
587
     */
588
    public function getStreamContextOptions()
589
    {
590
        $streamContextOptions = $this->_streamContextOptions;
591
 
592
        if (empty($streamContextOptions)) {
593
            // set default context, including timeout
594
            $streamContextOptions = array(
595
                'http' => array(
596
                    'timeout' => $this->timeout,
597
                )
598
            );
599
        }
600
 
601
        return $streamContextOptions;
602
    }
603
 
604
    /**
605
     * Parses the ini file and updates the cache files
606
     *
607
     * @throws Exception
608
     * @return bool whether the file was correctly written to the disk
609
     */
610
    public function updateCache()
611
    {
612
        $lockfile = $this->cacheDir . 'cache.lock';
613
 
614
        $lockRes = fopen($lockfile, 'w+');
615
        if (false === $lockRes) {
616
            throw new Exception(sprintf('error opening lockfile %s', $lockfile));
617
        }
618
        if (false === flock($lockRes, LOCK_EX | LOCK_NB)) {
619
            throw new Exception(sprintf('error locking lockfile %s', $lockfile));
620
        }
621
 
622
        $ini_path   = $this->cacheDir . $this->iniFilename;
623
        $cache_path = $this->cacheDir . $this->cacheFilename;
624
 
625
        // Choose the right url
626
        if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
627
            $url = realpath($this->localFile);
628
        } else {
629
            $url = $this->remoteIniUrl;
630
        }
631
 
632
        $this->_getRemoteIniFile($url, $ini_path);
633
 
634
        $this->_properties = array();
635
        $this->_browsers   = array();
636
        $this->_userAgents = array();
637
        $this->_patterns   = array();
638
 
639
        $iniContent = file_get_contents($ini_path);
640
 
641
        //$this->createCacheOldWay($iniContent);
642
        $this->createCacheNewWay($iniContent);
643
 
644
        // Write out new cache file
645
        $dir = dirname($cache_path);
646
 
647
        // "tempnam" did not work with VFSStream for tests
648
        $tmpFile = $dir . '/temp_' . md5(time() . basename($cache_path));
649
 
650
        // asume that all will be ok
651
        if (false === ($fileRes = fopen($tmpFile, 'w+'))) {
652
            // opening the temparary file failed
653
            throw new Exception('opening temporary file failed');
654
        }
655
 
656
        if (false === fwrite($fileRes, $this->_buildCache())) {
657
            // writing to the temparary file failed
658
            throw new Exception('writing to temporary file failed');
659
        }
660
 
661
        fclose($fileRes);
662
 
663
        if (false === rename($tmpFile, $cache_path)) {
664
            // renaming file failed, remove temp file
665
            @unlink($tmpFile);
666
 
667
            throw new Exception('could not rename temporary file to the cache file');
668
        }
669
 
670
        @flock($lockRes, LOCK_UN);
671
        @fclose($lockRes);
672
        @unlink($lockfile);
673
        $this->_cacheLoaded = false;
674
 
675
        return true;
676
    }
677
 
678
    /**
679
     * creates the cache content
680
     *
681
     * @param string $iniContent The content of the downloaded ini file
682
     * @param bool   $actLikeNewVersion
683
     */
684
    protected function createCacheOldWay($iniContent, $actLikeNewVersion = false)
685
    {
686
        $browsers = parse_ini_string($iniContent, true, INI_SCANNER_RAW);
687
 
688
        if ($actLikeNewVersion) {
689
            $this->_source_version = (int) $browsers[self::BROWSCAP_VERSION_KEY]['Version'];
690
        } else {
691
            $this->_source_version = $browsers[self::BROWSCAP_VERSION_KEY]['Version'];
692
        }
693
 
694
        unset($browsers[self::BROWSCAP_VERSION_KEY]);
695
 
696
        if (!$actLikeNewVersion) {
697
            unset($browsers['DefaultProperties']['RenderingEngine_Description']);
698
        }
699
 
700
        $this->_properties = array_keys($browsers['DefaultProperties']);
701
 
702
        array_unshift(
703
            $this->_properties,
704
            'browser_name',
705
            'browser_name_regex',
706
            'browser_name_pattern',
707
            'Parent'
708
        );
709
 
710
        $tmpUserAgents = array_keys($browsers);
711
 
712
        usort($tmpUserAgents, array($this, 'compareBcStrings'));
713
 
714
        $userAgentsKeys = array_flip($tmpUserAgents);
715
        $propertiesKeys = array_flip($this->_properties);
716
        $tmpPatterns    = array();
717
 
718
        foreach ($tmpUserAgents as $i => $userAgent) {
719
            $properties = $browsers[$userAgent];
720
 
721
            if (empty($properties['Comment'])
722
                || false !== strpos($userAgent, '*')
723
                || false !== strpos($userAgent, '?')
724
            ) {
725
                $pattern = $this->_pregQuote($userAgent);
726
 
727
                $countMatches = preg_match_all(
728
                    self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
729
                    $pattern,
730
                    $matches
731
                );
732
 
733
                if (!$countMatches) {
734
                    $tmpPatterns[$pattern] = $i;
735
                } else {
736
                    $compressedPattern = preg_replace(
737
                        self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
738
                        '(\d)',
739
                        $pattern
740
                    );
741
 
742
                    if (!isset($tmpPatterns[$compressedPattern])) {
743
                        $tmpPatterns[$compressedPattern] = array('first' => $pattern);
744
                    }
745
 
746
                    $tmpPatterns[$compressedPattern][$i] = $matches[0];
747
                }
748
            }
749
 
750
            if (!empty($properties['Parent'])) {
751
                $parent = $properties['Parent'];
752
 
753
                $parentKey = $userAgentsKeys[$parent];
754
 
755
                $properties['Parent']                 = $parentKey;
756
                $this->_userAgents[$parentKey . '.0'] = $tmpUserAgents[$parentKey];
757
            };
758
 
759
            $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys);
760
        }
761
 
762
        // reducing memory usage by unsetting $tmp_user_agents
763
        unset($tmpUserAgents);
764
 
765
        $this->_patterns = $this->deduplicatePattern($tmpPatterns);
766
    }
767
 
768
    /**
769
     * creates the cache content
770
     *
771
     * @param string $iniContent The content of the downloaded ini file
772
     *
773
     * @throws \phpbrowscap\Exception
774
     */
775
    protected function createCacheNewWay($iniContent)
776
    {
777
        $patternPositions = array();
778
 
779
        // get all patterns from the ini file in the correct order,
780
        // so that we can calculate with index number of the resulting array,
781
        // which part to use when the ini file is split into its sections.
782
        preg_match_all('/(?<=\[)(?:[^\r\n]+)(?=\])/m', $iniContent, $patternPositions);
783
 
784
        if (!isset($patternPositions[0])) {
785
            throw new Exception('could not extract patterns from ini file');
786
        }
787
 
788
        $patternPositions = $patternPositions[0];
789
 
790
        if (!count($patternPositions)) {
791
            throw new Exception('no patterns were found inside the ini file');
792
        }
793
 
794
        // split the ini file into sections and save the data in one line with a hash of the belonging
795
        // pattern (filtered in the previous step)
796
        $iniParts       = preg_split('/\[[^\r\n]+\]/', $iniContent);
797
        $tmpPatterns    = array();
798
        $propertiesKeys = array();
799
        $matches        = array();
800
 
801
        if (preg_match('/.*\[DefaultProperties\]([^[]*).*/', $iniContent, $matches)) {
802
            $properties = parse_ini_string($matches[1], true, INI_SCANNER_RAW);
803
 
804
            $this->_properties = array_keys($properties);
805
 
806
            array_unshift(
807
                $this->_properties,
808
                'browser_name',
809
                'browser_name_regex',
810
                'browser_name_pattern',
811
                'Parent'
812
            );
813
 
814
            $propertiesKeys = array_flip($this->_properties);
815
        }
816
 
817
        $key                   = $this->_pregQuote(self::BROWSCAP_VERSION_KEY);
818
        $this->_source_version = 0;
819
        $matches               = array();
820
 
821
        if (preg_match("/\\.*[" . $key . "\\][^[]*Version=(\\d+)\\D.*/", $iniContent, $matches)) {
822
            if (isset($matches[1])) {
823
                $this->_source_version = (int)$matches[1];
824
            }
825
        }
826
 
827
        $userAgentsKeys = array_flip($patternPositions);
828
        foreach ($patternPositions as $position => $userAgent) {
829
            if (self::BROWSCAP_VERSION_KEY === $userAgent) {
830
                continue;
831
            }
832
 
833
            $properties = parse_ini_string($iniParts[($position + 1)], true, INI_SCANNER_RAW);
834
 
835
            if (empty($properties['Comment'])
836
                || false !== strpos($userAgent, '*')
837
                || false !== strpos($userAgent, '?')
838
            ) {
839
                $pattern      = $this->_pregQuote(strtolower($userAgent));
840
                $matches      = array();
841
                $i            = $position - 1;
842
                $countMatches = preg_match_all(
843
                    self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
844
                    $pattern,
845
                    $matches
846
                );
847
 
848
                if (!$countMatches) {
849
                    $tmpPatterns[$pattern] = $i;
850
                } else {
851
                    $compressedPattern = preg_replace(
852
                        self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER,
853
                        '(\d)',
854
                        $pattern
855
                    );
856
 
857
                    if (!isset($tmpPatterns[$compressedPattern])) {
858
                        $tmpPatterns[$compressedPattern] = array('first' => $pattern);
859
                    }
860
 
861
                    $tmpPatterns[$compressedPattern][$i] = $matches[0];
862
                }
863
            }
864
 
865
            if (!empty($properties['Parent'])) {
866
                $parent    = $properties['Parent'];
867
                $parentKey = $userAgentsKeys[$parent];
868
 
869
                $properties['Parent']                       = $parentKey - 1;
870
                $this->_userAgents[($parentKey - 1) . '.0'] = $patternPositions[$parentKey];
871
            };
872
 
873
            $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys);
874
        }
875
 
876
        $patternList = $this->deduplicatePattern($tmpPatterns);
877
 
878
        $positionIndex = array();
879
        $lengthIndex   = array();
880
        $shortLength   = array();
881
        $patternArray  = array();
882
        $counter       = 0;
883
 
884
        foreach (array_keys($patternList) as $pattern) {
885
            $decodedPattern = str_replace('(\d)', 0, $this->_pregUnQuote($pattern, false));
886
 
887
            // force "defaultproperties" (if available) to first position, and "*" to last position
888
            if ($decodedPattern === 'defaultproperties') {
889
                $positionIndex[$pattern] = 0;
890
            } elseif ($decodedPattern === '*') {
891
                $positionIndex[$pattern] = 2;
892
            } else {
893
                $positionIndex[$pattern] = 1;
894
            }
895
 
896
            // sort by length
897
            $lengthIndex[$pattern] = strlen($decodedPattern);
898
            $shortLength[$pattern] = strlen(str_replace(array('*', '?'), '', $decodedPattern));
899
 
900
            // sort by original order
901
            $patternArray[$pattern] = $counter;
902
 
903
            $counter++;
904
        }
905
 
906
        array_multisort(
907
            $positionIndex,
908
            SORT_ASC,
909
            SORT_NUMERIC,
910
            $lengthIndex,
911
            SORT_DESC,
912
            SORT_NUMERIC,
913
            $shortLength,
914
            SORT_DESC,
915
            SORT_NUMERIC,
916
            $patternArray,
917
            SORT_ASC,
918
            SORT_NUMERIC,
919
            $patternList
920
        );
921
 
922
        $this->_patterns = $patternList;
923
    }
924
 
925
    /**
926
     * Sort the properties by keys
927
     * @param array $properties
928
     * @param array $propertiesKeys
929
     *
930
     * @return array
931
     */
932
    protected function resortProperties(array $properties, array $propertiesKeys)
933
    {
934
        $browser = array();
935
 
936
        foreach ($properties as $propertyName => $propertyValue) {
937
            if (!isset($propertiesKeys[$propertyName])) {
938
                continue;
939
            }
940
 
941
            $browser[$propertiesKeys[$propertyName]] = $propertyValue;
942
        }
943
 
944
        return $browser;
945
    }
946
 
947
    /**
948
     * Deduplicate Patterns
949
     * @param array $tmpPatterns
950
     *
951
     * @return array
952
     */
953
    protected function deduplicatePattern(array $tmpPatterns)
954
    {
955
        $patternList = array();
956
 
957
        foreach ($tmpPatterns as $pattern => $patternData) {
958
            if (is_int($patternData)) {
959
                $data = $patternData;
960
            } elseif (2 == count($patternData)) {
961
                end($patternData);
962
 
963
                $pattern = $patternData['first'];
964
                $data    = key($patternData);
965
            } else {
966
                unset($patternData['first']);
967
 
968
                $data = $this->deduplicateCompressionPattern($patternData, $pattern);
969
            }
970
 
971
            $patternList[$pattern] = $data;
972
        }
973
 
974
        return $patternList;
975
    }
976
 
977
    /**
978
     * Compare function for strings
979
     * @param string $a
980
     * @param string $b
981
     *
982
     * @return int
983
     */
984
    protected function compareBcStrings($a, $b)
985
    {
986
        $a_len = strlen($a);
987
        $b_len = strlen($b);
988
 
989
        if ($a_len > $b_len) {
990
            return -1;
991
        }
992
 
993
        if ($a_len < $b_len) {
994
            return 1;
995
        }
996
 
997
        $a_len = strlen(str_replace(array('*', '?'), '', $a));
998
        $b_len = strlen(str_replace(array('*', '?'), '', $b));
999
 
1000
        if ($a_len > $b_len) {
1001
            return -1;
1002
        }
1003
 
1004
        if ($a_len < $b_len) {
1005
            return 1;
1006
        }
1007
 
1008
        return 0;
1009
    }
1010
 
1011
    /**
1012
     * That looks complicated...
1013
     *
1014
     * All numbers are taken out into $matches, so we check if any of those numbers are identical
1015
     * in all the $matches and if they are we restore them to the $pattern, removing from the $matches.
1016
     * This gives us patterns with "(\d)" only in places that differ for some matches.
1017
     *
1018
     * @param array  $matches
1019
     * @param string $pattern
1020
     *
1021
     * @return array of $matches
1022
     */
1023
    protected function deduplicateCompressionPattern($matches, &$pattern)
1024
    {
1025
        $tmp_matches = $matches;
1026
        $first_match = array_shift($tmp_matches);
1027
        $differences = array();
1028
 
1029
        foreach ($tmp_matches as $some_match) {
1030
            $differences += array_diff_assoc($first_match, $some_match);
1031
        }
1032
 
1033
        $identical = array_diff_key($first_match, $differences);
1034
 
1035
        $prepared_matches = array();
1036
 
1037
        foreach ($matches as $i => $some_match) {
1038
            $key = self::COMPRESSION_PATTERN_START
1039
                . implode(self::COMPRESSION_PATTERN_DELIMITER, array_diff_assoc($some_match, $identical));
1040
 
1041
            $prepared_matches[$key] = $i;
1042
        }
1043
 
1044
        $pattern_parts = explode('(\d)', $pattern);
1045
 
1046
        foreach ($identical as $position => $value) {
1047
            $pattern_parts[$position + 1] = $pattern_parts[$position] . $value . $pattern_parts[$position + 1];
1048
            unset($pattern_parts[$position]);
1049
        }
1050
 
1051
        $pattern = implode('(\d)', $pattern_parts);
1052
 
1053
        return $prepared_matches;
1054
    }
1055
 
1056
    /**
1057
     * Converts browscap match patterns into preg match patterns.
1058
     *
1059
     * @param string $user_agent
1060
     *
1061
     * @return string
1062
     */
1063
    protected function _pregQuote($user_agent)
1064
    {
1065
        $pattern = preg_quote($user_agent, self::REGEX_DELIMITER);
1066
 
1067
        // the \\x replacement is a fix for "Der gro\xdfe BilderSauger 2.00u" user agent match
1068
 
1069
        return str_replace(
1070
            array('\*', '\?', '\\x'),
1071
            array('.*', '.', '\\\\x'),
1072
            $pattern
1073
        );
1074
    }
1075
 
1076
    /**
1077
     * Converts preg match patterns back to browscap match patterns.
1078
     *
1079
     * @param string        $pattern
1080
     * @param array|boolean $matches
1081
     *
1082
     * @return string
1083
     */
1084
    protected function _pregUnQuote($pattern, $matches)
1085
    {
1086
        // list of escaped characters: http://www.php.net/manual/en/function.preg-quote.php
1087
        // to properly unescape '?' which was changed to '.', I replace '\.' (real dot) with '\?',
1088
        // then change '.' to '?' and then '\?' to '.'.
1089
        $search  = array(
1090
            '\\' . self::REGEX_DELIMITER, '\\.', '\\\\', '\\+', '\\[', '\\^', '\\]', '\\$', '\\(', '\\)', '\\{', '\\}',
1091
            '\\=', '\\!', '\\<', '\\>', '\\|', '\\:', '\\-', '.*', '.', '\\?'
1092
        );
1093
        $replace = array(
1094
            self::REGEX_DELIMITER, '\\?', '\\', '+', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|',
1095
            ':', '-', '*', '?', '.'
1096
        );
1097
 
1098
        $result = substr(str_replace($search, $replace, $pattern), 2, -2);
1099
 
1100
        if ($matches) {
1101
            foreach ($matches as $oneMatch) {
1102
                $position = strpos($result, '(\d)');
1103
                $result   = substr_replace($result, $oneMatch, $position, 4);
1104
            }
1105
        }
1106
 
1107
        return $result;
1108
    }
1109
 
1110
    /**
1111
     * Loads the cache into object's properties
1112
     *
1113
     * @param string $cache_file
1114
     *
1115
     * @return boolean
1116
     */
1117
    protected function _loadCache($cache_file)
1118
    {
1119
        $cache_version  = null;
1120
        $source_version = null;
1121
        $browsers       = array();
1122
        $userAgents     = array();
1123
        $patterns       = array();
1124
        $properties     = array();
1125
 
1126
        $this->_cacheLoaded = false;
1127
 
1128
        require $cache_file;
1129
 
1130
        if (!isset($cache_version) || $cache_version != self::CACHE_FILE_VERSION) {
1131
            return false;
1132
        }
1133
 
1134
        $this->_source_version = $source_version;
1135
        $this->_browsers       = $browsers;
1136
        $this->_userAgents     = $userAgents;
1137
        $this->_patterns       = $patterns;
1138
        $this->_properties     = $properties;
1139
 
1140
        $this->_cacheLoaded = true;
1141
 
1142
        return true;
1143
    }
1144
 
1145
    /**
1146
     * Parses the array to cache and writes the resulting PHP string to disk
1147
     *
1148
     * @return boolean False on write error, true otherwise
1149
     */
1150
    protected function _buildCache()
1151
    {
1152
        $content = sprintf(
1153
            "<?php\n\$source_version=%s;\n\$cache_version=%s",
1154
            "'" . $this->_source_version . "'",
1155
            "'" . self::CACHE_FILE_VERSION . "'"
1156
        );
1157
 
1158
        $content .= ";\n\$properties=";
1159
        $content .= $this->_array2string($this->_properties);
1160
 
1161
        $content .= ";\n\$browsers=";
1162
        $content .= $this->_array2string($this->_browsers);
1163
 
1164
        $content .= ";\n\$userAgents=";
1165
        $content .= $this->_array2string($this->_userAgents);
1166
 
1167
        $content .= ";\n\$patterns=";
1168
        $content .= $this->_array2string($this->_patterns) . ";\n";
1169
 
1170
        return $content;
1171
    }
1172
 
1173
    /**
1174
     * Lazy getter for the stream context resource.
1175
     *
1176
     * @param bool $recreate
1177
     *
1178
     * @return resource
1179
     */
1180
    protected function _getStreamContext($recreate = false)
1181
    {
1182
        if (!isset($this->_streamContext) || true === $recreate) {
1183
            $this->_streamContext = stream_context_create($this->getStreamContextOptions());
1184
        }
1185
 
1186
        return $this->_streamContext;
1187
    }
1188
 
1189
    /**
1190
     * Updates the local copy of the ini file (by version checking) and adapts
1191
     * his syntax to the PHP ini parser
1192
     *
1193
     * @param string $url  the url of the remote server
1194
     * @param string $path the path of the ini file to update
1195
     *
1196
     * @throws Exception
1197
     * @return bool if the ini file was updated
1198
     */
1199
    protected function _getRemoteIniFile($url, $path)
1200
    {
1201
        // local and remote file are the same, no update possible
1202
        if ($url == $path) {
1203
            return false;
1204
        }
1205
 
1206
        // Check version
1207
        if (file_exists($path) && filesize($path)) {
1208
            $local_tmstp = filemtime($path);
1209
 
1210
            if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) {
1211
                $remote_tmstp = $this->_getLocalMTime();
1212
            } else {
1213
                $remote_tmstp = $this->_getRemoteMTime();
1214
            }
1215
 
1216
            if ($remote_tmstp <= $local_tmstp) {
1217
                // No update needed, return
1218
                touch($path);
1219
 
1220
                return false;
1221
            }
1222
        }
1223
 
1224
        // Check if it's possible to write to the .ini file.
1225
        if (is_file($path)) {
1226
            if (!is_writable($path)) {
1227
                throw new Exception(
1228
                    'Could not write to "' . $path . '" (check the permissions of the current/old ini file).'
1229
                );
1230
            }
1231
        } else {
1232
            // Test writability by creating a file only if one already doesn't exist, so we can safely delete it after
1233
            // the test.
1234
            $test_file = fopen($path, 'a');
1235
            if ($test_file) {
1236
                fclose($test_file);
1237
                unlink($path);
1238
            } else {
1239
                throw new Exception(
1240
                    'Could not write to "' . $path . '" (check the permissions of the cache directory).'
1241
                );
1242
            }
1243
        }
1244
 
1245
        // Get updated .ini file
1246
        $content = $this->_getRemoteData($url);
1247
 
1248
        if (!is_string($content) || strlen($content) < 1) {
1249
            throw new Exception('Could not load .ini content from "' . $url . '"');
1250
        }
1251
 
1252
        if (false !== strpos('rate limit', $content)) {
1253
            throw new Exception(
1254
                'Could not load .ini content from "' . $url . '" because the rate limit is exeeded for your IP'
1255
            );
1256
        }
1257
 
1258
        // replace opening and closing php and asp tags
1259
        $content = $this->sanitizeContent($content);
1260
 
1261
        if (!file_put_contents($path, $content)) {
1262
            throw new Exception('Could not write .ini content to "' . $path . '"');
1263
        }
1264
 
1265
        return true;
1266
    }
1267
 
1268
    /**
1269
     * Sanitize conten by regex
1270
     * @param string $content
1271
     *
1272
     * @return mixed
1273
     */
1274
    protected function sanitizeContent($content)
1275
    {
1276
        // replace everything between opening and closing php and asp tags
1277
        $content = preg_replace('/<[?%].*[?%]>/', '', $content);
1278
 
1279
        // replace opening and closing php and asp tags
1280
        return str_replace(array('<?', '<%', '?>', '%>'), '', $content);
1281
    }
1282
 
1283
    /**
1284
     * Gets the remote ini file update timestamp
1285
     *
1286
     * @throws Exception
1287
     * @return int the remote modification timestamp
1288
     */
1289
    protected function _getRemoteMTime()
1290
    {
1291
        $remote_datetime = $this->_getRemoteData($this->remoteVerUrl);
1292
        $remote_tmstp    = strtotime($remote_datetime);
1293
 
1294
        if (!$remote_tmstp) {
1295
            throw new Exception("Bad datetime format from {$this->remoteVerUrl}");
1296
        }
1297
 
1298
        return $remote_tmstp;
1299
    }
1300
 
1301
    /**
1302
     * Gets the local ini file update timestamp
1303
     *
1304
     * @throws Exception
1305
     * @return int the local modification timestamp
1306
     */
1307
    protected function _getLocalMTime()
1308
    {
1309
        if (!is_readable($this->localFile) || !is_file($this->localFile)) {
1310
            throw new Exception('Local file is not readable');
1311
        }
1312
 
1313
        return filemtime($this->localFile);
1314
    }
1315
 
1316
    /**
1317
     * Converts the given array to the PHP string which represent it.
1318
     * This method optimizes the PHP code and the output differs form the
1319
     * var_export one as the internal PHP function does not strip whitespace or
1320
     * convert strings to numbers.
1321
     *
1322
     * @param array $array The array to parse and convert
1323
     *
1324
     * @return boolean False on write error, true otherwise
1325
     */
1326
    protected function _array2string($array)
1327
    {
1328
        $content = "array(\n";
1329
 
1330
        foreach ($array as $key => $value) {
1331
            if (is_int($key)) {
1332
                $key = '';
1333
            } elseif (ctype_digit((string) $key)) {
1334
                $key = intval($key) . ' => ';
1335
            } elseif ('.0' === substr($key, -2) && !preg_match('/[^\d\.]/', $key)) {
1336
                $key = intval($key) . ' => ';
1337
            } else {
1338
                $key = "'" . str_replace("'", "\'", $key) . "' => ";
1339
            }
1340
 
1341
            if (is_array($value)) {
1342
                $value = "'" . addcslashes(serialize($value), "'") . "'";
1343
            } elseif (ctype_digit((string) $value)) {
1344
                $value = intval($value);
1345
            } else {
1346
                $value = "'" . str_replace("'", "\'", $value) . "'";
1347
            }
1348
 
1349
            $content .= $key . $value . ",\n";
1350
        }
1351
 
1352
        $content .= "\n)";
1353
 
1354
        return $content;
1355
    }
1356
 
1357
    /**
1358
     * Checks for the various possibilities offered by the current configuration
1359
     * of PHP to retrieve external HTTP data
1360
     *
1361
     * @return string|false the name of function to use to retrieve the file or false if no methods are available
1362
     */
1363
    protected function _getUpdateMethod()
1364
    {
1365
        // Caches the result
1366
        if ($this->updateMethod === null) {
1367
            if ($this->localFile !== null) {
1368
                $this->updateMethod = self::UPDATE_LOCAL;
1369
            } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) {
1370
                $this->updateMethod = self::UPDATE_FOPEN;
1371
            } elseif (function_exists('fsockopen')) {
1372
                $this->updateMethod = self::UPDATE_FSOCKOPEN;
1373
            } elseif (extension_loaded('curl')) {
1374
                $this->updateMethod = self::UPDATE_CURL;
1375
            } else {
1376
                $this->updateMethod = false;
1377
            }
1378
        }
1379
 
1380
        return $this->updateMethod;
1381
    }
1382
 
1383
    /**
1384
     * Retrieve the data identified by the URL
1385
     *
1386
     * @param string $url the url of the data
1387
     *
1388
     * @throws Exception
1389
     * @return string the retrieved data
1390
     */
1391
    protected function _getRemoteData($url)
1392
    {
1393
        ini_set('user_agent', $this->_getUserAgent());
1394
 
1395
        switch ($this->_getUpdateMethod()) {
1396
            case self::UPDATE_LOCAL:
1397
                $file = file_get_contents($url);
1398
 
1399
                if ($file !== false) {
1400
                    return $file;
1401
                } else {
1402
                    throw new Exception('Cannot open the local file');
1403
                }
1404
            case self::UPDATE_FOPEN:
1405
                if (ini_get('allow_url_fopen') && function_exists('file_get_contents')) {
1406
                    // include proxy settings in the file_get_contents() call
1407
                    $context = $this->_getStreamContext();
1408
                    $file    = file_get_contents($url, false, $context);
1409
 
1410
                    if ($file !== false) {
1411
                        return $file;
1412
                    }
1413
                }// else try with the next possibility (break omitted)
1414
            case self::UPDATE_FSOCKOPEN:
1415
                if (function_exists('fsockopen')) {
1416
                    $remote_url     = parse_url($url);
1417
                    $contextOptions = $this->getStreamContextOptions();
1418
 
1419
                    $errno  = 0;
1420
                    $errstr = '';
1421
 
1422
                    if (empty($contextOptions)) {
1423
                        $port           = (empty($remote_url['port']) ? 80 : $remote_url['port']);
1424
                        $remote_handler = fsockopen($remote_url['host'], $port, $errno, $errstr, $this->timeout);
1425
                    } else {
1426
                        $context = $this->_getStreamContext();
1427
 
1428
                        $remote_handler = stream_socket_client(
1429
                            $url,
1430
                            $errno,
1431
                            $errstr,
1432
                            $this->timeout,
1433
                            STREAM_CLIENT_CONNECT,
1434
                            $context
1435
                        );
1436
                    }
1437
 
1438
                    if ($remote_handler) {
1439
                        stream_set_timeout($remote_handler, $this->timeout);
1440
 
1441
                        if (isset($remote_url['query'])) {
1442
                            $remote_url['path'] .= '?' . $remote_url['query'];
1443
                        }
1444
 
1445
                        $out = sprintf(
1446
                            self::REQUEST_HEADERS,
1447
                            $remote_url['path'],
1448
                            $remote_url['host'],
1449
                            $this->_getUserAgent()
1450
                        );
1451
 
1452
                        fwrite($remote_handler, $out);
1453
 
1454
                        $response = fgets($remote_handler);
1455
                        if (strpos($response, '200 OK') !== false) {
1456
                            $file = '';
1457
                            while (!feof($remote_handler)) {
1458
                                $file .= fgets($remote_handler);
1459
                            }
1460
 
1461
                            $file = str_replace("\r\n", "\n", $file);
1462
                            $file = explode("\n\n", $file);
1463
                            array_shift($file);
1464
 
1465
                            $file = implode("\n\n", $file);
1466
 
1467
                            fclose($remote_handler);
1468
 
1469
                            return $file;
1470
                        }
1471
                    }
1472
                }// else try with the next possibility
1473
            case self::UPDATE_CURL:
1474
                if (extension_loaded('curl')) { // make sure curl is loaded
1475
                    $ch = curl_init($url);
1476
 
1477
                    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
1478
                    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout);
1479
                    curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent());
1480
 
1481
                    $file = curl_exec($ch);
1482
 
1483
                    curl_close($ch);
1484
 
1485
                    if ($file !== false) {
1486
                        return $file;
1487
                    }
1488
                }// else try with the next possibility
1489
            case false:
1490
                throw new Exception(
1491
                    'Your server can\'t connect to external resources. Please update the file manually.'
1492
                );
1493
        }
1494
 
1495
        return '';
1496
    }
1497
 
1498
    /**
1499
     * Format the useragent string to be used in the remote requests made by the
1500
     * class during the update process.
1501
     *
1502
     * @return string the formatted user agent
1503
     */
1504
    protected function _getUserAgent()
1505
    {
1506
        $ua = str_replace('%v', self::VERSION, $this->userAgent);
1507
        $ua = str_replace('%m', $this->_getUpdateMethod(), $ua);
1508
 
1509
        return $ua;
1510
    }
1511
}
1512
 
1513
/**
1514
 * Browscap.ini parsing class exception
1515
 *
1516
 * @package    report_deviceanalytics
1517
 * @subpackage Browscap
1518
 * @author     Jonathan Stoppani <jonathan@stoppani.name>
1519
 * @copyright  Copyright (c) 2006-2012 Jonathan Stoppani
1520
 * @license    http://www.opensource.org/licenses/MIT MIT License
1521
 * @link       https://github.com/GaretJax/phpbrowscap/
1522
 */
1523
class Exception extends \Exception
1524
{
1525
    // nothing to do here
1526
}