1<?php 2 3/** 4 * Browscap.ini parsing class with caching and update capabilities 5 * 6 * PHP version 5 7 * 8 * Copyright (c) 2006-2012 Jonathan Stoppani 9 * 10 * Permission is hereby granted, free of charge, to any person obtaining a 11 * copy of this software and associated documentation files (the "Software"), 12 * to deal in the Software without restriction, including without limitation 13 * the rights to use, copy, modify, merge, publish, distribute, sublicense, 14 * and/or sell copies of the Software, and to permit persons to whom the 15 * Software is furnished to do so, subject to the following conditions: 16 * 17 * The above copyright notice and this permission notice shall be included 18 * in all copies or substantial portions of the Software. 19 * 20 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 21 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 22 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 23 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 24 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 25 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 26 * THE SOFTWARE. 27 * 28 * @package 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 * @version 1.0 34 * @license http://www.opensource.org/licenses/MIT MIT License 35 * @link https://github.com/GaretJax/phpbrowscap/ 36 */ 37class Browscap 38{ 39 /** 40 * Current version of the class. 41 */ 42 const VERSION = '2.0'; 43 44 const CACHE_FILE_VERSION = '2.0b'; 45 46 /** 47 * Different ways to access remote and local files. 48 * 49 * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents). 50 * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen). 51 * UPDATE_CURL: Uses the cURL extension. 52 * UPDATE_LOCAL: Updates from a local file (file_get_contents). 53 */ 54 const UPDATE_FOPEN = 'URL-wrapper'; 55 const UPDATE_FSOCKOPEN = 'socket'; 56 const UPDATE_CURL = 'cURL'; 57 const UPDATE_LOCAL = 'local'; 58 59 /** 60 * Options for regex patterns. 61 * 62 * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class. 63 * REGEX_MODIFIERS: Regex modifiers. 64 */ 65 const REGEX_DELIMITER = '@'; 66 const REGEX_MODIFIERS = 'i'; 67 68 const COMPRESSION_PATTERN_START = '@'; 69 const COMPRESSION_PATTERN_DELIMITER = '|'; 70 71 /** 72 * The values to quote in the ini file 73 */ 74 const VALUES_TO_QUOTE = 'Browser|Parent'; 75 76 const BROWSCAP_VERSION_KEY = 'GJK_Browscap_Version'; 77 78 /** 79 * The headers to be sent for checking the version and requesting the file. 80 */ 81 const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n"; 82 83 /** 84 * Options for auto update capabilities 85 * 86 * $remoteVerUrl: The location to use to check out if a new version of the 87 * browscap.ini file is available. 88 * $remoteIniUrl: The location from which download the ini file. 89 * The placeholder for the file should be represented by a %s. 90 * $timeout: The timeout for the requests. 91 * $updateInterval: The update interval in seconds. 92 * $errorInterval: The next update interval in seconds in case of an error. 93 * $doAutoUpdate: Flag to disable the automatic interval based update. 94 * $updateMethod: The method to use to update the file, has to be a value of 95 * an UPDATE_* constant, null or false. 96 * 97 * The default source file type is changed from normal to full. The performance difference 98 * is MINIMAL, so there is no reason to use the standard file whatsoever. Either go for light, 99 * which is blazing fast, or get the full one. (note: light version doesn't work, a fix is on its way) 100 */ 101 public $remoteIniUrl = 'http://browscap.org/stream?q=Full_PHP_BrowsCapINI'; 102 public $remoteVerUrl = 'http://browscap.org/version'; 103 public $timeout = 5; 104 public $updateInterval = 432000; // 5 days 105 public $errorInterval = 7200; // 2 hours 106 public $doAutoUpdate = true; 107 public $updateMethod = null; 108 109 /** 110 * The path of the local version of the browscap.ini file from which to 111 * update (to be set only if used). 112 * 113 * @var string 114 */ 115 public $localFile = null; 116 117 /** 118 * The useragent to include in the requests made by the class during the 119 * update process. 120 * 121 * @var string 122 */ 123 public $userAgent = 'Browser Capabilities Project - PHP Browscap/%v %m'; 124 125 /** 126 * Flag to enable only lowercase indexes in the result. 127 * The cache has to be rebuilt in order to apply this option. 128 * 129 * @var bool 130 */ 131 public $lowercase = false; 132 133 /** 134 * Flag to enable/disable silent error management. 135 * In case of an error during the update process the class returns an empty 136 * array/object if the update process can't take place and the browscap.ini 137 * file does not exist. 138 * 139 * @var bool 140 */ 141 public $silent = false; 142 143 /** 144 * Where to store the cached PHP arrays. 145 * 146 * @var string 147 */ 148 public $cacheFilename = 'cache.php'; 149 150 /** 151 * Where to store the downloaded ini file. 152 * 153 * @var string 154 */ 155 public $iniFilename = 'browscap.ini'; 156 157 /** 158 * Path to the cache directory 159 * 160 * @var string 161 */ 162 public $cacheDir = null; 163 164 /** 165 * Flag to be set to true after loading the cache 166 * 167 * @var bool 168 */ 169 protected $_cacheLoaded = false; 170 171 /** 172 * Where to store the value of the included PHP cache file 173 * 174 * @var array 175 */ 176 protected $_userAgents = array(); 177 protected $_browsers = array(); 178 protected $_patterns = array(); 179 protected $_properties = array(); 180 protected $_source_version; 181 182 /** 183 * An associative array of associative arrays in the format 184 * `$arr['wrapper']['option'] = $value` passed to stream_context_create() 185 * when building a stream resource. 186 * 187 * Proxy settings are stored in this variable. 188 * 189 * @see http://www.php.net/manual/en/function.stream-context-create.php 190 * 191 * @var array 192 */ 193 protected $_streamContextOptions = array(); 194 195 /** 196 * A valid context resource created with stream_context_create(). 197 * 198 * @see http://www.php.net/manual/en/function.stream-context-create.php 199 * 200 * @var resource 201 */ 202 protected $_streamContext = null; 203 204 /** 205 * Constructor class, checks for the existence of (and loads) the cache and 206 * if needed updated the definitions 207 * 208 * @param string $cache_dir 209 * @throws Exception 210 */ 211 public function __construct($cache_dir) 212 { 213 // has to be set to reach E_STRICT compatibility, does not affect system/app settings 214 date_default_timezone_set(date_default_timezone_get()); 215 216 if (!isset($cache_dir)) { 217 throw new Exception( 218 'You have to provide a path to read/store the browscap cache file' 219 ); 220 } 221 222 $old_cache_dir = $cache_dir; 223 $cache_dir = realpath($cache_dir); 224 225 if (false === $cache_dir) { 226 throw new Exception( 227 sprintf('The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?', $old_cache_dir) 228 ); 229 } 230 231 // Is the cache dir really the directory or is it directly the file? 232 if (substr($cache_dir, -4) === '.php') { 233 $this->cacheFilename = basename($cache_dir); 234 $this->cacheDir = dirname($cache_dir); 235 } else { 236 $this->cacheDir = $cache_dir; 237 } 238 239 $this->cacheDir .= DIRECTORY_SEPARATOR; 240 } 241 242 public function getSourceVersion() 243 { 244 return $this->_source_version; 245 } 246 247 /** 248 * XXX parse 249 * 250 * Gets the information about the browser by User Agent 251 * 252 * @param string $user_agent the user agent string 253 * @param bool $return_array whether return an array or an object 254 * @throws Exception 255 * @return stdClass|array the object containing the browsers details. Array if 256 * $return_array is set to true. 257 */ 258 public function getBrowser($user_agent = null, $return_array = false) 259 { 260 // Load the cache at the first request 261 if (!$this->_cacheLoaded) { 262 $cache_file = $this->cacheDir . $this->cacheFilename; 263 $ini_file = $this->cacheDir . $this->iniFilename; 264 265 // Set the interval only if needed 266 if ($this->doAutoUpdate && file_exists($ini_file)) { 267 $interval = time() - filemtime($ini_file); 268 } else { 269 $interval = 0; 270 } 271 272 $update_cache = true; 273 274 if (file_exists($cache_file) && file_exists($ini_file) && ($interval <= $this->updateInterval)) 275 { 276 if ($this->_loadCache($cache_file)) 277 { 278 $update_cache = false; 279 } 280 } 281 282 if ($update_cache) { 283 try { 284 $this->updateCache(); 285 } catch (Exception $e) { 286 if (file_exists($ini_file)) { 287 // Adjust the filemtime to the $errorInterval 288 touch($ini_file, time() - $this->updateInterval + $this->errorInterval); 289 } elseif ($this->silent) { 290 // Return an array if silent mode is active and the ini db doesn't exsist 291 return array(); 292 } 293 294 if (!$this->silent) { 295 throw $e; 296 } 297 } 298 299 if (!$this->_loadCache($cache_file)) 300 { 301 throw new Exception("Cannot load this cache version - the cache format is not compatible."); 302 } 303 } 304 305 } 306 307 // Automatically detect the useragent 308 if (!isset($user_agent)) { 309 if (isset($_SERVER['HTTP_USER_AGENT'])) { 310 $user_agent = $_SERVER['HTTP_USER_AGENT']; 311 } else { 312 $user_agent = ''; 313 } 314 } 315 316 $browser = array(); 317 foreach ($this->_patterns as $pattern => $pattern_data) { 318 if (preg_match($pattern . 'i', $user_agent, $matches)) { 319 if (1 == count($matches)) { 320 // standard match 321 $key = $pattern_data; 322 323 $simple_match = true; 324 } else { 325 $pattern_data = unserialize($pattern_data); 326 327 // match with numeric replacements 328 array_shift($matches); 329 330 $match_string = self::COMPRESSION_PATTERN_START . implode(self::COMPRESSION_PATTERN_DELIMITER, $matches); 331 332 if (!isset($pattern_data[$match_string])) { 333 // partial match - numbers are not present, but everything else is ok 334 continue; 335 } 336 337 $key = $pattern_data[$match_string]; 338 339 $simple_match = false; 340 } 341 342 $browser = array( 343 $user_agent, // Original useragent 344 trim(strtolower($pattern), self::REGEX_DELIMITER), 345 $this->_pregUnQuote($pattern, $simple_match ? false : $matches) 346 ); 347 348 $browser = $value = $browser + unserialize($this->_browsers[$key]); 349 350 while (array_key_exists(3, $value)) { 351 $value = unserialize($this->_browsers[$value[3]]); 352 $browser += $value; 353 } 354 355 if (!empty($browser[3])) { 356 $browser[3] = $this->_userAgents[$browser[3]]; 357 } 358 359 break; 360 } 361 } 362 363 // Add the keys for each property 364 $array = array(); 365 foreach ($browser as $key => $value) { 366 if ($value === 'true') { 367 $value = true; 368 } elseif ($value === 'false') { 369 $value = false; 370 } 371 $array[$this->_properties[$key]] = $value; 372 } 373 374 return $return_array ? $array : (object) $array; 375 } 376 377 /** 378 * Load (auto-set) proxy settings from environment variables. 379 */ 380 public function autodetectProxySettings() 381 { 382 $wrappers = array('http', 'https', 'ftp'); 383 384 foreach ($wrappers as $wrapper) { 385 $url = getenv($wrapper.'_proxy'); 386 if (!empty($url)) { 387 $params = array_merge(array( 388 'port' => null, 389 'user' => null, 390 'pass' => null, 391 ), parse_url($url)); 392 $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']); 393 } 394 } 395 } 396 397 /** 398 * Add proxy settings to the stream context array. 399 * 400 * @param string $server Proxy server/host 401 * @param int $port Port 402 * @param string $wrapper Wrapper: "http", "https", "ftp", others... 403 * @param string $username Username (when requiring authentication) 404 * @param string $password Password (when requiring authentication) 405 * 406 * @return Browscap 407 */ 408 public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null) 409 { 410 $settings = array($wrapper => array( 411 'proxy' => sprintf('tcp://%s:%d', $server, $port), 412 'request_fulluri' => true, 413 )); 414 415 // Proxy authentication (optional) 416 if (isset($username) && isset($password)) { 417 $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic '.base64_encode($username.':'.$password); 418 } 419 420 // Add these new settings to the stream context options array 421 $this->_streamContextOptions = array_merge( 422 $this->_streamContextOptions, 423 $settings 424 ); 425 426 /* Return $this so we can chain addProxySettings() calls like this: 427 * $browscap-> 428 * addProxySettings('http')-> 429 * addProxySettings('https')-> 430 * addProxySettings('ftp'); 431 */ 432 return $this; 433 } 434 435 /** 436 * Clear proxy settings from the stream context options array. 437 * 438 * @param string $wrapper Remove settings from this wrapper only 439 * 440 * @return array Wrappers cleared 441 */ 442 public function clearProxySettings($wrapper = null) 443 { 444 $wrappers = isset($wrapper) ? array($wrapper) : array_keys($this->_streamContextOptions); 445 446 $clearedWrappers = array(); 447 $options = array('proxy', 'request_fulluri', 'header'); 448 foreach ($wrappers as $wrapper) { 449 450 // remove wrapper options related to proxy settings 451 if (isset($this->_streamContextOptions[$wrapper]['proxy'])) { 452 foreach ($options as $option){ 453 unset($this->_streamContextOptions[$wrapper][$option]); 454 } 455 456 // remove wrapper entry if there are no other options left 457 if (empty($this->_streamContextOptions[$wrapper])) { 458 unset($this->_streamContextOptions[$wrapper]); 459 } 460 461 $clearedWrappers[] = $wrapper; 462 } 463 } 464 465 return $clearedWrappers; 466 } 467 468 /** 469 * Returns the array of stream context options. 470 * 471 * @return array 472 */ 473 public function getStreamContextOptions() 474 { 475 return $this->_streamContextOptions; 476 } 477 478 /** 479 * XXX save 480 * 481 * Parses the ini file and updates the cache files 482 * 483 * @return bool whether the file was correctly written to the disk 484 */ 485 public function updateCache() 486 { 487 $ini_path = $this->cacheDir . $this->iniFilename; 488 $cache_path = $this->cacheDir . $this->cacheFilename; 489 490 // Choose the right url 491 if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { 492 $url = $this->localFile; 493 } else { 494 $url = $this->remoteIniUrl; 495 } 496 497 $this->_getRemoteIniFile($url, $ini_path); 498 499 if (version_compare(PHP_VERSION, '5.3.0', '>=')) { 500 $browsers = parse_ini_file($ini_path, true, INI_SCANNER_RAW); 501 } else { 502 $browsers = parse_ini_file($ini_path, true); 503 } 504 505 $this->_source_version = $browsers[self::BROWSCAP_VERSION_KEY]['Version']; 506 unset($browsers[self::BROWSCAP_VERSION_KEY]); 507 508 unset($browsers['DefaultProperties']['RenderingEngine_Description']); 509 510 $this->_properties = array_keys($browsers['DefaultProperties']); 511 512 array_unshift( 513 $this->_properties, 514 'browser_name', 515 'browser_name_regex', 516 'browser_name_pattern', 517 'Parent' 518 ); 519 520 $tmp_user_agents = array_keys($browsers); 521 522 523 usort($tmp_user_agents, array($this, 'compareBcStrings')); 524 525 $user_agents_keys = array_flip($tmp_user_agents); 526 $properties_keys = array_flip($this->_properties); 527 528 $tmp_patterns = array(); 529 530 foreach ($tmp_user_agents as $i => $user_agent) { 531 532 if (empty($browsers[$user_agent]['Comment']) || strpos($user_agent, '*') !== false || strpos($user_agent, '?') !== false) 533 { 534 $pattern = $this->_pregQuote($user_agent); 535 536 $matches_count = preg_match_all('@\d@', $pattern, $matches); 537 538 if (!$matches_count) { 539 $tmp_patterns[$pattern] = $i; 540 } else { 541 $compressed_pattern = preg_replace('@\d@', '(\d)', $pattern); 542 543 if (!isset($tmp_patterns[$compressed_pattern])) { 544 $tmp_patterns[$compressed_pattern] = array('first' => $pattern); 545 } 546 547 $tmp_patterns[$compressed_pattern][$i] = $matches[0]; 548 } 549 } 550 551 if (!empty($browsers[$user_agent]['Parent'])) { 552 $parent = $browsers[$user_agent]['Parent']; 553 $parent_key = $user_agents_keys[$parent]; 554 $browsers[$user_agent]['Parent'] = $parent_key; 555 $this->_userAgents[$parent_key . '.0'] = $tmp_user_agents[$parent_key]; 556 }; 557 558 $browser = array(); 559 foreach ($browsers[$user_agent] as $key => $value) { 560 if (!isset($properties_keys[$key])) 561 { 562 continue; 563 } 564 565 $key = $properties_keys[$key]; 566 $browser[$key] = $value; 567 } 568 569 570 $this->_browsers[] = $browser; 571 } 572 573 foreach ($tmp_patterns as $pattern => $pattern_data) { 574 if (is_int($pattern_data)) { 575 $this->_patterns[$pattern] = $pattern_data; 576 } elseif (2 == count($pattern_data)) { 577 end($pattern_data); 578 $this->_patterns[$pattern_data['first']] = key($pattern_data); 579 } else { 580 unset($pattern_data['first']); 581 582 $pattern_data = $this->deduplicateCompressionPattern($pattern_data, $pattern); 583 584 $this->_patterns[$pattern] = $pattern_data; 585 } 586 } 587 588 // Save the keys lowercased if needed 589 if ($this->lowercase) { 590 $this->_properties = array_map('strtolower', $this->_properties); 591 } 592 593 // Get the whole PHP code 594 $cache = $this->_buildCache(); 595 596 // Save and return 597 return (bool) file_put_contents($cache_path, $cache, LOCK_EX); 598 } 599 600 protected function compareBcStrings($a, $b) 601 { 602 $a_len = strlen($a); 603 $b_len = strlen($b); 604 605 if ($a_len > $b_len) return -1; 606 if ($a_len < $b_len) return 1; 607 608 $a_len = strlen(str_replace(array('*', '?'), '', $a)); 609 $b_len = strlen(str_replace(array('*', '?'), '', $b)); 610 611 if ($a_len > $b_len) return -1; 612 if ($a_len < $b_len) return 1; 613 614 return 0; 615 } 616 617 /** 618 * That looks complicated... 619 * 620 * All numbers are taken out into $matches, so we check if any of those numbers are identical 621 * in all the $matches and if they are we restore them to the $pattern, removing from the $matches. 622 * This gives us patterns with "(\d)" only in places that differ for some matches. 623 * 624 * @param array $matches 625 * @param string $pattern 626 * 627 * @return array of $matches 628 */ 629 protected function deduplicateCompressionPattern($matches, &$pattern) 630 { 631 $tmp_matches = $matches; 632 633 $first_match = array_shift($tmp_matches); 634 635 $differences = array(); 636 637 foreach ($tmp_matches as $some_match) 638 { 639 $differences += array_diff_assoc($first_match, $some_match); 640 } 641 642 $identical = array_diff_key($first_match, $differences); 643 644 $prepared_matches = array(); 645 646 foreach ($matches as $i => $some_match) 647 { 648 $prepared_matches[self::COMPRESSION_PATTERN_START . implode(self::COMPRESSION_PATTERN_DELIMITER, array_diff_assoc($some_match, $identical))] = $i; 649 } 650 651 $pattern_parts = explode('(\d)', $pattern); 652 653 foreach ($identical as $position => $value) 654 { 655 $pattern_parts[$position + 1] = $pattern_parts[$position] . $value . $pattern_parts[$position + 1]; 656 unset($pattern_parts[$position]); 657 } 658 659 $pattern = implode('(\d)', $pattern_parts); 660 661 return $prepared_matches; 662 } 663 664 /** 665 * Converts browscap match patterns into preg match patterns. 666 * 667 * @param string $user_agent 668 * 669 * @return string 670 */ 671 protected function _pregQuote($user_agent) 672 { 673 $pattern = preg_quote($user_agent, self::REGEX_DELIMITER); 674 675 // the \\x replacement is a fix for "Der gro\xdfe BilderSauger 2.00u" user agent match 676 677 return self::REGEX_DELIMITER 678 . '^' 679 . str_replace(array('\*', '\?', '\\x'), array('.*', '.', '\\\\x'), $pattern) 680 . '$' 681 . self::REGEX_DELIMITER; 682 } 683 684 /** 685 * Converts preg match patterns back to browscap match patterns. 686 * 687 * @param string $pattern 688 * @param array $matches 689 * 690 * @return string 691 */ 692 protected function _pregUnQuote($pattern, $matches) 693 { 694 // list of escaped characters: http://www.php.net/manual/en/function.preg-quote.php 695 // to properly unescape '?' which was changed to '.', I replace '\.' (real dot) with '\?', then change '.' to '?' and then '\?' to '.'. 696 $search = array('\\' . self::REGEX_DELIMITER, '\\.', '\\\\', '\\+', '\\[', '\\^', '\\]', '\\$', '\\(', '\\)', '\\{', '\\}', '\\=', '\\!', '\\<', '\\>', '\\|', '\\:', '\\-', '.*', '.', '\\?'); 697 $replace = array(self::REGEX_DELIMITER, '\\?', '\\', '+', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '-', '*', '?', '.'); 698 699 $result = substr(str_replace($search, $replace, $pattern), 2, -2); 700 701 if ($matches) 702 { 703 foreach ($matches as $one_match) 704 { 705 $num_pos = strpos($result, '(\d)'); 706 $result = substr_replace($result, $one_match, $num_pos, 4); 707 } 708 } 709 710 return $result; 711 } 712 713 /** 714 * Loads the cache into object's properties 715 * 716 * @param $cache_file 717 * 718 * @return boolean 719 */ 720 protected function _loadCache($cache_file) 721 { 722 require $cache_file; 723 724 if (!isset($cache_version) || $cache_version != self::CACHE_FILE_VERSION) 725 { 726 return false; 727 } 728 729 $this->_source_version = $source_version; 730 $this->_browsers = $browsers; 731 $this->_userAgents = $userAgents; 732 $this->_patterns = $patterns; 733 $this->_properties = $properties; 734 735 $this->_cacheLoaded = true; 736 737 return true; 738 } 739 740 /** 741 * Parses the array to cache and creates the PHP string to write to disk 742 * 743 * @return string the PHP string to save into the cache file 744 */ 745 protected function _buildCache() 746 { 747 $cacheTpl = "<?php\n\$source_version=%s;\n\$cache_version=%s;\n\$properties=%s;\n\$browsers=%s;\n\$userAgents=%s;\n\$patterns=%s;\n"; 748 749 $propertiesArray = $this->_array2string($this->_properties); 750 $patternsArray = $this->_array2string($this->_patterns); 751 $userAgentsArray = $this->_array2string($this->_userAgents); 752 $browsersArray = $this->_array2string($this->_browsers); 753 754 return sprintf( 755 $cacheTpl, 756 "'" . $this->_source_version . "'", 757 "'" . self::CACHE_FILE_VERSION . "'", 758 $propertiesArray, 759 $browsersArray, 760 $userAgentsArray, 761 $patternsArray 762 ); 763 } 764 765 /** 766 * Lazy getter for the stream context resource. 767 * 768 * @param bool $recreate 769 * 770 * @return resource 771 */ 772 protected function _getStreamContext($recreate = false) 773 { 774 if (!isset($this->_streamContext) || true === $recreate) { 775 $this->_streamContext = stream_context_create($this->_streamContextOptions); 776 } 777 778 return $this->_streamContext; 779 } 780 781 /** 782 * Updates the local copy of the ini file (by version checking) and adapts 783 * his syntax to the PHP ini parser 784 * 785 * @param string $url the url of the remote server 786 * @param string $path the path of the ini file to update 787 * @throws Exception 788 * @return bool if the ini file was updated 789 */ 790 protected function _getRemoteIniFile($url, $path) 791 { 792 // Check version 793 if (file_exists($path) && filesize($path)) { 794 $local_tmstp = filemtime($path); 795 796 if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { 797 $remote_tmstp = $this->_getLocalMTime(); 798 } else { 799 $remote_tmstp = $this->_getRemoteMTime(); 800 } 801 802 if ($remote_tmstp < $local_tmstp) { 803 // No update needed, return 804 touch($path); 805 806 return false; 807 } 808 } 809 810 // Get updated .ini file 811 $browscap = $this->_getRemoteData($url); 812 813 814 $browscap = explode("\n", $browscap); 815 816 $pattern = self::REGEX_DELIMITER 817 . '(' 818 . self::VALUES_TO_QUOTE 819 . ')="?([^"]*)"?$' 820 . self::REGEX_DELIMITER; 821 822 823 // Ok, lets read the file 824 $content = ''; 825 foreach ($browscap as $subject) { 826 $subject = trim($subject); 827 $content .= preg_replace($pattern, '$1="$2"', $subject) . "\n"; 828 } 829 830 if ($url != $path) { 831 if (!file_put_contents($path, $content)) { 832 throw new Exception("Could not write .ini content to $path"); 833 } 834 } 835 836 return true; 837 } 838 839 /** 840 * Gets the remote ini file update timestamp 841 * 842 * @throws Exception 843 * @return int the remote modification timestamp 844 */ 845 protected function _getRemoteMTime() 846 { 847 $remote_datetime = $this->_getRemoteData($this->remoteVerUrl); 848 $remote_tmstp = strtotime($remote_datetime); 849 850 if (!$remote_tmstp) { 851 throw new Exception("Bad datetime format from {$this->remoteVerUrl}"); 852 } 853 854 return $remote_tmstp; 855 } 856 857 /** 858 * Gets the local ini file update timestamp 859 * 860 * @throws Exception 861 * @return int the local modification timestamp 862 */ 863 protected function _getLocalMTime() 864 { 865 if (!is_readable($this->localFile) || !is_file($this->localFile)) { 866 throw new Exception("Local file is not readable"); 867 } 868 869 return filemtime($this->localFile); 870 } 871 872 /** 873 * Converts the given array to the PHP string which represent it. 874 * This method optimizes the PHP code and the output differs form the 875 * var_export one as the internal PHP function does not strip whitespace or 876 * convert strings to numbers. 877 * 878 * @param array $array the array to parse and convert 879 * @return string the array parsed into a PHP string 880 */ 881 protected function _array2string($array) 882 { 883 $strings = array(); 884 885 foreach ($array as $key => $value) { 886 if (is_int($key)) { 887 $key = ''; 888 } elseif (ctype_digit((string) $key) || '.0' === substr($key, -2)) { 889 $key = intval($key) . '=>' ; 890 } else { 891 $key = "'" . str_replace("'", "\'", $key) . "'=>" ; 892 } 893 894 if (is_array($value)) { 895 $value = "'" . addcslashes(serialize($value), "'") . "'"; 896 } elseif (ctype_digit((string) $value)) { 897 $value = intval($value); 898 } else { 899 $value = "'" . str_replace("'", "\'", $value) . "'"; 900 } 901 902 $strings[] = $key . $value; 903 } 904 905 return "array(\n" . implode(",\n", $strings) . "\n)"; 906 } 907 908 /** 909 * Checks for the various possibilities offered by the current configuration 910 * of PHP to retrieve external HTTP data 911 * 912 * @return string the name of function to use to retrieve the file 913 */ 914 protected function _getUpdateMethod() 915 { 916 // Caches the result 917 if ($this->updateMethod === null) { 918 if ($this->localFile !== null) { 919 $this->updateMethod = self::UPDATE_LOCAL; 920 } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { 921 $this->updateMethod = self::UPDATE_FOPEN; 922 } elseif (function_exists('fsockopen')) { 923 $this->updateMethod = self::UPDATE_FSOCKOPEN; 924 } elseif (extension_loaded('curl')) { 925 $this->updateMethod = self::UPDATE_CURL; 926 } else { 927 $this->updateMethod = false; 928 } 929 } 930 931 return $this->updateMethod; 932 } 933 934 /** 935 * Retrieve the data identified by the URL 936 * 937 * @param string $url the url of the data 938 * @throws Exception 939 * @return string the retrieved data 940 */ 941 protected function _getRemoteData($url) 942 { 943 ini_set('user_agent', $this->_getUserAgent()); 944 945 switch ($this->_getUpdateMethod()) { 946 case self::UPDATE_LOCAL: 947 $file = file_get_contents($url); 948 949 if ($file !== false) { 950 return $file; 951 } else { 952 throw new Exception('Cannot open the local file'); 953 } 954 case self::UPDATE_FOPEN: 955 // include proxy settings in the file_get_contents() call 956 $context = $this->_getStreamContext(); 957 $file = file_get_contents($url, false, $context); 958 959 if ($file !== false) { 960 return $file; 961 } // else try with the next possibility (break omitted) 962 case self::UPDATE_FSOCKOPEN: 963 $remote_url = parse_url($url); 964 $remote_handler = fsockopen($remote_url['host'], 80, $c, $e, $this->timeout); 965 966 if ($remote_handler) { 967 stream_set_timeout($remote_handler, $this->timeout); 968 969 if (isset($remote_url['query'])) { 970 $remote_url['path'] .= '?' . $remote_url['query']; 971 } 972 973 $out = sprintf( 974 self::REQUEST_HEADERS, 975 $remote_url['path'], 976 $remote_url['host'], 977 $this->_getUserAgent() 978 ); 979 980 fwrite($remote_handler, $out); 981 982 $response = fgets($remote_handler); 983 if (strpos($response, '200 OK') !== false) { 984 $file = ''; 985 while (!feof($remote_handler)) { 986 $file .= fgets($remote_handler); 987 } 988 989 $file = str_replace("\r\n", "\n", $file); 990 $file = explode("\n\n", $file); 991 array_shift($file); 992 993 $file = implode("\n\n", $file); 994 995 fclose($remote_handler); 996 997 return $file; 998 } 999 } // else try with the next possibility 1000 case self::UPDATE_CURL: 1001 $ch = curl_init($url); 1002 1003 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 1004 curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); 1005 curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent()); 1006 1007 $file = curl_exec($ch); 1008 1009 curl_close($ch); 1010 1011 if ($file !== false) { 1012 return $file; 1013 } // else try with the next possibility 1014 case false: 1015 throw new Exception('Your server can\'t connect to external resources. Please update the file manually.'); 1016 } 1017 1018 return ''; 1019 } 1020 1021 /** 1022 * Format the useragent string to be used in the remote requests made by the 1023 * class during the update process. 1024 * 1025 * @return string the formatted user agent 1026 */ 1027 protected function _getUserAgent() 1028 { 1029 $ua = str_replace('%v', self::VERSION, $this->userAgent); 1030 $ua = str_replace('%m', $this->_getUpdateMethod(), $ua); 1031 1032 return $ua; 1033 } 1034} 1035