* @author Vítor Brandão * @author Mikołaj Misiurewicz * @copyright Copyright (c) 2006-2012 Jonathan Stoppani * @version 1.0 * @license http://www.opensource.org/licenses/MIT MIT License * @link https://github.com/GaretJax/phpbrowscap/ */ class Browscap { /** * Current version of the class. */ const VERSION = '2.0.5'; const CACHE_FILE_VERSION = '2.0.5'; /** * Different ways to access remote and local files. * * UPDATE_FOPEN: Uses the fopen url wrapper (use file_get_contents). * UPDATE_FSOCKOPEN: Uses the socket functions (fsockopen). * UPDATE_CURL: Uses the cURL extension. * UPDATE_LOCAL: Updates from a local file (file_get_contents). */ const UPDATE_FOPEN = 'URL-wrapper'; const UPDATE_FSOCKOPEN = 'socket'; const UPDATE_CURL = 'cURL'; const UPDATE_LOCAL = 'local'; /** * Options for regex patterns. * * REGEX_DELIMITER: Delimiter of all the regex patterns in the whole class. * REGEX_MODIFIERS: Regex modifiers. */ const REGEX_DELIMITER = '@'; const REGEX_MODIFIERS = 'i'; const COMPRESSION_PATTERN_START = '@'; const COMPRESSION_PATTERN_DELIMITER = '|'; /** * The values to quote in the ini file */ const VALUES_TO_QUOTE = 'Browser|Parent'; const BROWSCAP_VERSION_KEY = 'GJK_Browscap_Version'; /** * The headers to be sent for checking the version and requesting the file. */ const REQUEST_HEADERS = "GET %s HTTP/1.0\r\nHost: %s\r\nUser-Agent: %s\r\nConnection: Close\r\n\r\n"; /** * how many pattern should be checked at once in the first step */ const COUNT_PATTERN = 100; /** * Options for auto update capabilities * * $remoteVerUrl: The location to use to check out if a new version of the * browscap.ini file is available. * $remoteIniUrl: The location from which download the ini file. * The placeholder for the file should be represented by a %s. * $timeout: The timeout for the requests. * $updateInterval: The update interval in seconds. * $errorInterval: The next update interval in seconds in case of an error. * $doAutoUpdate: Flag to disable the automatic interval based update. * $updateMethod: The method to use to update the file, has to be a value of * an UPDATE_* constant, null or false. * * The default source file type is changed from normal to full. The performance difference * is MINIMAL, so there is no reason to use the standard file whatsoever. Either go for light, * which is blazing fast, or get the full one. (note: light version doesn't work, a fix is on its way) */ public $remoteIniUrl = 'http://browscap.org/stream?q=PHP_BrowscapINI'; public $remoteVerUrl = 'http://browscap.org/version'; public $timeout = 5; public $updateInterval = 432000; // 5 days public $errorInterval = 7200; // 2 hours public $doAutoUpdate = true; public $updateMethod = null; /** * The path of the local version of the browscap.ini file from which to * update (to be set only if used). * * @var string */ public $localFile = null; /** * The useragent to include in the requests made by the class during the * update process. * * @var string */ public $userAgent = 'http://browscap.org/ - PHP Browscap/%v %m'; /** * Flag to enable only lowercase indexes in the result. * The cache has to be rebuilt in order to apply this option. * * @var bool */ public $lowercase = false; /** * Flag to enable/disable silent error management. * In case of an error during the update process the class returns an empty * array/object if the update process can't take place and the browscap.ini * file does not exist. * * @var bool */ public $silent = false; /** * Where to store the cached PHP arrays. * * @var string */ public $cacheFilename = 'cache.php'; /** * Where to store the downloaded ini file. * * @var string */ public $iniFilename = 'browscap.ini'; /** * Path to the cache directory * * @var string */ public $cacheDir = null; /** * Flag to be set to true after loading the cache * * @var bool */ protected $_cacheLoaded = false; /** * Where to store the value of the included PHP cache file * * @var array */ protected $_userAgents = array(); protected $_browsers = array(); protected $_patterns = array(); protected $_properties = array(); protected $_source_version; /** * An associative array of associative arrays in the format * `$arr['wrapper']['option'] = $value` passed to stream_context_create() * when building a stream resource. * * Proxy settings are stored in this variable. * * @see http://www.php.net/manual/en/function.stream-context-create.php * @var array */ protected $_streamContextOptions = array(); /** * A valid context resource created with stream_context_create(). * * @see http://www.php.net/manual/en/function.stream-context-create.php * @var resource */ protected $_streamContext = null; /** * Constructor class, checks for the existence of (and loads) the cache and * if needed updated the definitions * * @param string $cache_dir * * @throws Exception */ public function __construct($cache_dir = null) { // has to be set to reach E_STRICT compatibility, does not affect system/app settings date_default_timezone_set(date_default_timezone_get()); if (!isset($cache_dir)) { throw new Exception('You have to provide a path to read/store the browscap cache file'); } $old_cache_dir = $cache_dir; $cache_dir = realpath($cache_dir); if (false === $cache_dir) { throw new Exception( sprintf( 'The cache path %s is invalid. Are you sure that it exists and that you have permission to access it?', $old_cache_dir ) ); } // Is the cache dir really the directory or is it directly the file? if (substr($cache_dir, -4) === '.php') { $this->cacheFilename = basename($cache_dir); $this->cacheDir = dirname($cache_dir); } else { $this->cacheDir = $cache_dir; } $this->cacheDir .= DIRECTORY_SEPARATOR; } /** * @return mixed */ public function getSourceVersion() { return $this->_source_version; } /** * @return bool */ public function shouldCacheBeUpdated() { // Load the cache at the first request if ($this->_cacheLoaded) { return false; } $cache_file = $this->cacheDir . $this->cacheFilename; $ini_file = $this->cacheDir . $this->iniFilename; // Set the interval only if needed if ($this->doAutoUpdate && file_exists($ini_file)) { $interval = time() - filemtime($ini_file); } else { $interval = 0; } $shouldBeUpdated = true; if (file_exists($cache_file) && file_exists($ini_file) && ($interval <= $this->updateInterval)) { if ($this->_loadCache($cache_file)) { $shouldBeUpdated = false; } } return $shouldBeUpdated; } /** * Gets the information about the browser by User Agent * * @param string $user_agent the user agent string * @param bool $return_array whether return an array or an object * * @throws Exception * @return \stdClass|array the object containing the browsers details. Array if * $return_array is set to true. */ public function getBrowser($user_agent = null, $return_array = false) { if ($this->shouldCacheBeUpdated()) { try { $this->updateCache(); } catch (Exception $e) { $ini_file = $this->cacheDir . $this->iniFilename; if (file_exists($ini_file)) { // Adjust the filemtime to the $errorInterval touch($ini_file, time() - $this->updateInterval + $this->errorInterval); } elseif ($this->silent) { // Return an array if silent mode is active and the ini db doesn't exsist return array(); } if (!$this->silent) { throw $e; } } } $cache_file = $this->cacheDir . $this->cacheFilename; if (!$this->_cacheLoaded && !$this->_loadCache($cache_file)) { throw new Exception('Cannot load cache file - the cache format is not compatible.'); } // Automatically detect the useragent if (!isset($user_agent)) { if (isset($_SERVER['HTTP_USER_AGENT'])) { $user_agent = $_SERVER['HTTP_USER_AGENT']; } else { $user_agent = ''; } } $browser = array(); $patterns = array_keys($this->_patterns); $chunks = array_chunk($patterns, self::COUNT_PATTERN); foreach ($chunks as $chunk) { $longPattern = self::REGEX_DELIMITER . '^(?:' . implode(')|(?:', $chunk) . ')$' . self::REGEX_DELIMITER . 'i'; if (!preg_match($longPattern, $user_agent)) { continue; } foreach ($chunk as $pattern) { $patternToMatch = self::REGEX_DELIMITER . '^' . $pattern . '$' . self::REGEX_DELIMITER . 'i'; $matches = array(); if (!preg_match($patternToMatch, $user_agent, $matches)) { continue; } $patternData = $this->_patterns[$pattern]; if (1 === count($matches)) { // standard match $key = $patternData; $simpleMatch = true; } else { $patternData = unserialize($patternData); // match with numeric replacements array_shift($matches); $matchString = self::COMPRESSION_PATTERN_START . implode(self::COMPRESSION_PATTERN_DELIMITER, $matches); if (!isset($patternData[$matchString])) { // partial match - numbers are not present, but everything else is ok continue; } $key = $patternData[$matchString]; $simpleMatch = false; } $browser = array( $user_agent, // Original useragent trim(strtolower($pattern), self::REGEX_DELIMITER), $this->_pregUnQuote($pattern, $simpleMatch ? false : $matches) ); $browser = $value = $browser + unserialize($this->_browsers[$key]); while (array_key_exists(3, $value)) { $value = unserialize($this->_browsers[$value[3]]); $browser += $value; } if (!empty($browser[3]) && array_key_exists($browser[3], $this->_userAgents)) { $browser[3] = $this->_userAgents[$browser[3]]; } break 2; } } // Add the keys for each property $array = array(); foreach ($browser as $key => $value) { if ($value === 'true') { $value = true; } elseif ($value === 'false') { $value = false; } $propertyName = $this->_properties[$key]; if ($this->lowercase) { $propertyName = strtolower($propertyName); } $array[$propertyName] = $value; } return $return_array ? $array : (object) $array; } /** * Load (auto-set) proxy settings from environment variables. */ public function autodetectProxySettings() { $wrappers = array('http', 'https', 'ftp'); foreach ($wrappers as $wrapper) { $url = getenv($wrapper . '_proxy'); if (!empty($url)) { $params = array_merge( array( 'port' => null, 'user' => null, 'pass' => null, ), parse_url($url) ); $this->addProxySettings($params['host'], $params['port'], $wrapper, $params['user'], $params['pass']); } } } /** * Add proxy settings to the stream context array. * * @param string $server Proxy server/host * @param int $port Port * @param string $wrapper Wrapper: "http", "https", "ftp", others... * @param string $username Username (when requiring authentication) * @param string $password Password (when requiring authentication) * * @return Browscap */ public function addProxySettings($server, $port = 3128, $wrapper = 'http', $username = null, $password = null) { $settings = array( $wrapper => array( 'proxy' => sprintf('tcp://%s:%d', $server, $port), 'request_fulluri' => true, 'timeout' => $this->timeout, ) ); // Proxy authentication (optional) if (isset($username) && isset($password)) { $settings[$wrapper]['header'] = 'Proxy-Authorization: Basic ' . base64_encode($username . ':' . $password); } // Add these new settings to the stream context options array $this->_streamContextOptions = array_merge( $this->_streamContextOptions, $settings ); /* Return $this so we can chain addProxySettings() calls like this: * $browscap-> * addProxySettings('http')-> * addProxySettings('https')-> * addProxySettings('ftp'); */ return $this; } /** * Clear proxy settings from the stream context options array. * * @param string $wrapper Remove settings from this wrapper only * * @return array Wrappers cleared */ public function clearProxySettings($wrapper = null) { $wrappers = isset($wrapper) ? array($wrapper) : array_keys($this->_streamContextOptions); $clearedWrappers = array(); $options = array('proxy', 'request_fulluri', 'header'); foreach ($wrappers as $wrapper) { // remove wrapper options related to proxy settings if (isset($this->_streamContextOptions[$wrapper]['proxy'])) { foreach ($options as $option) { unset($this->_streamContextOptions[$wrapper][$option]); } // remove wrapper entry if there are no other options left if (empty($this->_streamContextOptions[$wrapper])) { unset($this->_streamContextOptions[$wrapper]); } $clearedWrappers[] = $wrapper; } } return $clearedWrappers; } /** * Returns the array of stream context options. * * @return array */ public function getStreamContextOptions() { $streamContextOptions = $this->_streamContextOptions; if (empty($streamContextOptions)) { // set default context, including timeout $streamContextOptions = array( 'http' => array( 'timeout' => $this->timeout, ) ); } return $streamContextOptions; } /** * Parses the ini file and updates the cache files * * @throws Exception * @return bool whether the file was correctly written to the disk */ public function updateCache() { $lockfile = $this->cacheDir . 'cache.lock'; $lockRes = fopen($lockfile, 'w+'); if (false === $lockRes) { throw new Exception(sprintf('error opening lockfile %s', $lockfile)); } if (false === flock($lockRes, LOCK_EX | LOCK_NB)) { throw new Exception(sprintf('error locking lockfile %s', $lockfile)); } $ini_path = $this->cacheDir . $this->iniFilename; $cache_path = $this->cacheDir . $this->cacheFilename; // Choose the right url if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { $url = realpath($this->localFile); } else { $url = $this->remoteIniUrl; } $this->_getRemoteIniFile($url, $ini_path); $this->_properties = array(); $this->_browsers = array(); $this->_userAgents = array(); $this->_patterns = array(); $iniContent = file_get_contents($ini_path); //$this->createCacheOldWay($iniContent); $this->createCacheNewWay($iniContent); // Write out new cache file $dir = dirname($cache_path); // "tempnam" did not work with VFSStream for tests $tmpFile = $dir . '/temp_' . md5(time() . basename($cache_path)); // asume that all will be ok if (false === ($fileRes = fopen($tmpFile, 'w+'))) { // opening the temparary file failed throw new Exception('opening temporary file failed'); } if (false === fwrite($fileRes, $this->_buildCache())) { // writing to the temparary file failed throw new Exception('writing to temporary file failed'); } fclose($fileRes); if (false === rename($tmpFile, $cache_path)) { // renaming file failed, remove temp file @unlink($tmpFile); throw new Exception('could not rename temporary file to the cache file'); } @flock($lockRes, LOCK_UN); @fclose($lockRes); @unlink($lockfile); $this->_cacheLoaded = false; return true; } /** * creates the cache content * * @param string $iniContent The content of the downloaded ini file * @param bool $actLikeNewVersion */ protected function createCacheOldWay($iniContent, $actLikeNewVersion = false) { $browsers = parse_ini_string($iniContent, true, INI_SCANNER_RAW); if ($actLikeNewVersion) { $this->_source_version = (int) $browsers[self::BROWSCAP_VERSION_KEY]['Version']; } else { $this->_source_version = $browsers[self::BROWSCAP_VERSION_KEY]['Version']; } unset($browsers[self::BROWSCAP_VERSION_KEY]); if (!$actLikeNewVersion) { unset($browsers['DefaultProperties']['RenderingEngine_Description']); } $this->_properties = array_keys($browsers['DefaultProperties']); array_unshift( $this->_properties, 'browser_name', 'browser_name_regex', 'browser_name_pattern', 'Parent' ); $tmpUserAgents = array_keys($browsers); usort($tmpUserAgents, array($this, 'compareBcStrings')); $userAgentsKeys = array_flip($tmpUserAgents); $propertiesKeys = array_flip($this->_properties); $tmpPatterns = array(); foreach ($tmpUserAgents as $i => $userAgent) { $properties = $browsers[$userAgent]; if (empty($properties['Comment']) || false !== strpos($userAgent, '*') || false !== strpos($userAgent, '?') ) { $pattern = $this->_pregQuote($userAgent); $countMatches = preg_match_all( self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, $pattern, $matches ); if (!$countMatches) { $tmpPatterns[$pattern] = $i; } else { $compressedPattern = preg_replace( self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, '(\d)', $pattern ); if (!isset($tmpPatterns[$compressedPattern])) { $tmpPatterns[$compressedPattern] = array('first' => $pattern); } $tmpPatterns[$compressedPattern][$i] = $matches[0]; } } if (!empty($properties['Parent'])) { $parent = $properties['Parent']; $parentKey = $userAgentsKeys[$parent]; $properties['Parent'] = $parentKey; $this->_userAgents[$parentKey . '.0'] = $tmpUserAgents[$parentKey]; }; $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys); } // reducing memory usage by unsetting $tmp_user_agents unset($tmpUserAgents); $this->_patterns = $this->deduplicatePattern($tmpPatterns); } /** * creates the cache content * * @param string $iniContent The content of the downloaded ini file * * @throws \phpbrowscap\Exception */ protected function createCacheNewWay($iniContent) { $patternPositions = array(); // get all patterns from the ini file in the correct order, // so that we can calculate with index number of the resulting array, // which part to use when the ini file is split into its sections. preg_match_all('/(?<=\[)(?:[^\r\n]+)(?=\])/m', $iniContent, $patternPositions); if (!isset($patternPositions[0])) { throw new Exception('could not extract patterns from ini file'); } $patternPositions = $patternPositions[0]; if (!count($patternPositions)) { throw new Exception('no patterns were found inside the ini file'); } // split the ini file into sections and save the data in one line with a hash of the belonging // pattern (filtered in the previous step) $iniParts = preg_split('/\[[^\r\n]+\]/', $iniContent); $tmpPatterns = array(); $propertiesKeys = array(); $matches = array(); if (preg_match('/.*\[DefaultProperties\]([^[]*).*/', $iniContent, $matches)) { $properties = parse_ini_string($matches[1], true, INI_SCANNER_RAW); $this->_properties = array_keys($properties); array_unshift( $this->_properties, 'browser_name', 'browser_name_regex', 'browser_name_pattern', 'Parent' ); $propertiesKeys = array_flip($this->_properties); } $key = $this->_pregQuote(self::BROWSCAP_VERSION_KEY); $this->_source_version = 0; $matches = array(); if (preg_match("/\\.*[" . $key . "\\][^[]*Version=(\\d+)\\D.*/", $iniContent, $matches)) { if (isset($matches[1])) { $this->_source_version = (int)$matches[1]; } } $userAgentsKeys = array_flip($patternPositions); foreach ($patternPositions as $position => $userAgent) { if (self::BROWSCAP_VERSION_KEY === $userAgent) { continue; } $properties = parse_ini_string($iniParts[($position + 1)], true, INI_SCANNER_RAW); if (empty($properties['Comment']) || false !== strpos($userAgent, '*') || false !== strpos($userAgent, '?') ) { $pattern = $this->_pregQuote(strtolower($userAgent)); $matches = array(); $i = $position - 1; $countMatches = preg_match_all( self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, $pattern, $matches ); if (!$countMatches) { $tmpPatterns[$pattern] = $i; } else { $compressedPattern = preg_replace( self::REGEX_DELIMITER . '\d' . self::REGEX_DELIMITER, '(\d)', $pattern ); if (!isset($tmpPatterns[$compressedPattern])) { $tmpPatterns[$compressedPattern] = array('first' => $pattern); } $tmpPatterns[$compressedPattern][$i] = $matches[0]; } } if (!empty($properties['Parent'])) { $parent = $properties['Parent']; $parentKey = $userAgentsKeys[$parent]; $properties['Parent'] = $parentKey - 1; $this->_userAgents[($parentKey - 1) . '.0'] = $patternPositions[$parentKey]; }; $this->_browsers[] = $this->resortProperties($properties, $propertiesKeys); } $patternList = $this->deduplicatePattern($tmpPatterns); $positionIndex = array(); $lengthIndex = array(); $shortLength = array(); $patternArray = array(); $counter = 0; foreach (array_keys($patternList) as $pattern) { $decodedPattern = str_replace('(\d)', 0, $this->_pregUnQuote($pattern, false)); // force "defaultproperties" (if available) to first position, and "*" to last position if ($decodedPattern === 'defaultproperties') { $positionIndex[$pattern] = 0; } elseif ($decodedPattern === '*') { $positionIndex[$pattern] = 2; } else { $positionIndex[$pattern] = 1; } // sort by length $lengthIndex[$pattern] = strlen($decodedPattern); $shortLength[$pattern] = strlen(str_replace(array('*', '?'), '', $decodedPattern)); // sort by original order $patternArray[$pattern] = $counter; $counter++; } array_multisort( $positionIndex, SORT_ASC, SORT_NUMERIC, $lengthIndex, SORT_DESC, SORT_NUMERIC, $shortLength, SORT_DESC, SORT_NUMERIC, $patternArray, SORT_ASC, SORT_NUMERIC, $patternList ); $this->_patterns = $patternList; } /** * @param array $properties * @param array $propertiesKeys * * @return array */ protected function resortProperties(array $properties, array $propertiesKeys) { $browser = array(); foreach ($properties as $propertyName => $propertyValue) { if (!isset($propertiesKeys[$propertyName])) { continue; } $browser[$propertiesKeys[$propertyName]] = $propertyValue; } return $browser; } /** * @param array $tmpPatterns * * @return array */ protected function deduplicatePattern(array $tmpPatterns) { $patternList = array(); foreach ($tmpPatterns as $pattern => $patternData) { if (is_int($patternData)) { $data = $patternData; } elseif (2 == count($patternData)) { end($patternData); $pattern = $patternData['first']; $data = key($patternData); } else { unset($patternData['first']); $data = $this->deduplicateCompressionPattern($patternData, $pattern); } $patternList[$pattern] = $data; } return $patternList; } /** * @param string $a * @param string $b * * @return int */ protected function compareBcStrings($a, $b) { $a_len = strlen($a); $b_len = strlen($b); if ($a_len > $b_len) { return -1; } if ($a_len < $b_len) { return 1; } $a_len = strlen(str_replace(array('*', '?'), '', $a)); $b_len = strlen(str_replace(array('*', '?'), '', $b)); if ($a_len > $b_len) { return -1; } if ($a_len < $b_len) { return 1; } return 0; } /** * That looks complicated... * * All numbers are taken out into $matches, so we check if any of those numbers are identical * in all the $matches and if they are we restore them to the $pattern, removing from the $matches. * This gives us patterns with "(\d)" only in places that differ for some matches. * * @param array $matches * @param string $pattern * * @return array of $matches */ protected function deduplicateCompressionPattern($matches, &$pattern) { $tmp_matches = $matches; $first_match = array_shift($tmp_matches); $differences = array(); foreach ($tmp_matches as $some_match) { $differences += array_diff_assoc($first_match, $some_match); } $identical = array_diff_key($first_match, $differences); $prepared_matches = array(); foreach ($matches as $i => $some_match) { $key = self::COMPRESSION_PATTERN_START . implode(self::COMPRESSION_PATTERN_DELIMITER, array_diff_assoc($some_match, $identical)); $prepared_matches[$key] = $i; } $pattern_parts = explode('(\d)', $pattern); foreach ($identical as $position => $value) { $pattern_parts[$position + 1] = $pattern_parts[$position] . $value . $pattern_parts[$position + 1]; unset($pattern_parts[$position]); } $pattern = implode('(\d)', $pattern_parts); return $prepared_matches; } /** * Converts browscap match patterns into preg match patterns. * * @param string $user_agent * * @return string */ protected function _pregQuote($user_agent) { $pattern = preg_quote($user_agent, self::REGEX_DELIMITER); // the \\x replacement is a fix for "Der gro\xdfe BilderSauger 2.00u" user agent match return str_replace( array('\*', '\?', '\\x'), array('.*', '.', '\\\\x'), $pattern ); } /** * Converts preg match patterns back to browscap match patterns. * * @param string $pattern * @param array|boolean $matches * * @return string */ protected function _pregUnQuote($pattern, $matches) { // list of escaped characters: http://www.php.net/manual/en/function.preg-quote.php // to properly unescape '?' which was changed to '.', I replace '\.' (real dot) with '\?', // then change '.' to '?' and then '\?' to '.'. $search = array( '\\' . self::REGEX_DELIMITER, '\\.', '\\\\', '\\+', '\\[', '\\^', '\\]', '\\$', '\\(', '\\)', '\\{', '\\}', '\\=', '\\!', '\\<', '\\>', '\\|', '\\:', '\\-', '.*', '.', '\\?' ); $replace = array( self::REGEX_DELIMITER, '\\?', '\\', '+', '[', '^', ']', '$', '(', ')', '{', '}', '=', '!', '<', '>', '|', ':', '-', '*', '?', '.' ); $result = substr(str_replace($search, $replace, $pattern), 2, -2); if ($matches) { foreach ($matches as $oneMatch) { $position = strpos($result, '(\d)'); $result = substr_replace($result, $oneMatch, $position, 4); } } return $result; } /** * Loads the cache into object's properties * * @param string $cache_file * * @return boolean */ protected function _loadCache($cache_file) { $cache_version = null; $source_version = null; $browsers = array(); $userAgents = array(); $patterns = array(); $properties = array(); $this->_cacheLoaded = false; require $cache_file; if (!isset($cache_version) || $cache_version != self::CACHE_FILE_VERSION) { return false; } $this->_source_version = $source_version; $this->_browsers = $browsers; $this->_userAgents = $userAgents; $this->_patterns = $patterns; $this->_properties = $properties; $this->_cacheLoaded = true; return true; } /** * Parses the array to cache and writes the resulting PHP string to disk * * @return boolean False on write error, true otherwise */ protected function _buildCache() { $content = sprintf( "_source_version . "'", "'" . self::CACHE_FILE_VERSION . "'" ); $content .= ";\n\$properties="; $content .= $this->_array2string($this->_properties); $content .= ";\n\$browsers="; $content .= $this->_array2string($this->_browsers); $content .= ";\n\$userAgents="; $content .= $this->_array2string($this->_userAgents); $content .= ";\n\$patterns="; $content .= $this->_array2string($this->_patterns) . ";\n"; return $content; } /** * Lazy getter for the stream context resource. * * @param bool $recreate * * @return resource */ protected function _getStreamContext($recreate = false) { if (!isset($this->_streamContext) || true === $recreate) { $this->_streamContext = stream_context_create($this->getStreamContextOptions()); } return $this->_streamContext; } /** * Updates the local copy of the ini file (by version checking) and adapts * his syntax to the PHP ini parser * * @param string $url the url of the remote server * @param string $path the path of the ini file to update * * @throws Exception * @return bool if the ini file was updated */ protected function _getRemoteIniFile($url, $path) { // local and remote file are the same, no update possible if ($url == $path) { return false; } // Check version if (file_exists($path) && filesize($path)) { $local_tmstp = filemtime($path); if ($this->_getUpdateMethod() == self::UPDATE_LOCAL) { $remote_tmstp = $this->_getLocalMTime(); } else { $remote_tmstp = $this->_getRemoteMTime(); } if ($remote_tmstp <= $local_tmstp) { // No update needed, return touch($path); return false; } } // Check if it's possible to write to the .ini file. if (is_file($path)) { if (!is_writable($path)) { throw new Exception( 'Could not write to "' . $path . '" (check the permissions of the current/old ini file).' ); } } else { // Test writability by creating a file only if one already doesn't exist, so we can safely delete it after // the test. $test_file = fopen($path, 'a'); if ($test_file) { fclose($test_file); unlink($path); } else { throw new Exception( 'Could not write to "' . $path . '" (check the permissions of the cache directory).' ); } } // Get updated .ini file $content = $this->_getRemoteData($url); if (!is_string($content) || strlen($content) < 1) { throw new Exception('Could not load .ini content from "' . $url . '"'); } if (false !== strpos('rate limit', $content)) { throw new Exception( 'Could not load .ini content from "' . $url . '" because the rate limit is exeeded for your IP' ); } // replace opening and closing php and asp tags $content = $this->sanitizeContent($content); if (!file_put_contents($path, $content)) { throw new Exception('Could not write .ini content to "' . $path . '"'); } return true; } /** * @param string $content * * @return mixed */ protected function sanitizeContent($content) { // replace everything between opening and closing php and asp tags $content = preg_replace('/<[?%].*[?%]>/', '', $content); // replace opening and closing php and asp tags return str_replace(array('', '%>'), '', $content); } /** * Gets the remote ini file update timestamp * * @throws Exception * @return int the remote modification timestamp */ protected function _getRemoteMTime() { $remote_datetime = $this->_getRemoteData($this->remoteVerUrl); $remote_tmstp = strtotime($remote_datetime); if (!$remote_tmstp) { throw new Exception("Bad datetime format from {$this->remoteVerUrl}"); } return $remote_tmstp; } /** * Gets the local ini file update timestamp * * @throws Exception * @return int the local modification timestamp */ protected function _getLocalMTime() { if (!is_readable($this->localFile) || !is_file($this->localFile)) { throw new Exception('Local file is not readable'); } return filemtime($this->localFile); } /** * Converts the given array to the PHP string which represent it. * This method optimizes the PHP code and the output differs form the * var_export one as the internal PHP function does not strip whitespace or * convert strings to numbers. * * @param array $array The array to parse and convert * * @return boolean False on write error, true otherwise */ protected function _array2string($array) { $content = "array(\n"; foreach ($array as $key => $value) { if (is_int($key)) { $key = ''; } elseif (ctype_digit((string) $key)) { $key = intval($key) . ' => '; } elseif ('.0' === substr($key, -2) && !preg_match('/[^\d\.]/', $key)) { $key = intval($key) . ' => '; } else { $key = "'" . str_replace("'", "\'", $key) . "' => "; } if (is_array($value)) { $value = "'" . addcslashes(serialize($value), "'") . "'"; } elseif (ctype_digit((string) $value)) { $value = intval($value); } else { $value = "'" . str_replace("'", "\'", $value) . "'"; } $content .= $key . $value . ",\n"; } $content .= "\n)"; return $content; } /** * Checks for the various possibilities offered by the current configuration * of PHP to retrieve external HTTP data * * @return string|false the name of function to use to retrieve the file or false if no methods are available */ protected function _getUpdateMethod() { // Caches the result if ($this->updateMethod === null) { if ($this->localFile !== null) { $this->updateMethod = self::UPDATE_LOCAL; } elseif (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { $this->updateMethod = self::UPDATE_FOPEN; } elseif (function_exists('fsockopen')) { $this->updateMethod = self::UPDATE_FSOCKOPEN; } elseif (extension_loaded('curl')) { $this->updateMethod = self::UPDATE_CURL; } else { $this->updateMethod = false; } } return $this->updateMethod; } /** * Retrieve the data identified by the URL * * @param string $url the url of the data * * @throws Exception * @return string the retrieved data */ protected function _getRemoteData($url) { ini_set('user_agent', $this->_getUserAgent()); switch ($this->_getUpdateMethod()) { case self::UPDATE_LOCAL: $file = file_get_contents($url); if ($file !== false) { return $file; } else { throw new Exception('Cannot open the local file'); } case self::UPDATE_FOPEN: if (ini_get('allow_url_fopen') && function_exists('file_get_contents')) { // include proxy settings in the file_get_contents() call $context = $this->_getStreamContext(); $file = file_get_contents($url, false, $context); if ($file !== false) { return $file; } }// else try with the next possibility (break omitted) case self::UPDATE_FSOCKOPEN: if (function_exists('fsockopen')) { $remote_url = parse_url($url); $contextOptions = $this->getStreamContextOptions(); $errno = 0; $errstr = ''; if (empty($contextOptions)) { $port = (empty($remote_url['port']) ? 80 : $remote_url['port']); $remote_handler = fsockopen($remote_url['host'], $port, $errno, $errstr, $this->timeout); } else { $context = $this->_getStreamContext(); $remote_handler = stream_socket_client( $url, $errno, $errstr, $this->timeout, STREAM_CLIENT_CONNECT, $context ); } if ($remote_handler) { stream_set_timeout($remote_handler, $this->timeout); if (isset($remote_url['query'])) { $remote_url['path'] .= '?' . $remote_url['query']; } $out = sprintf( self::REQUEST_HEADERS, $remote_url['path'], $remote_url['host'], $this->_getUserAgent() ); fwrite($remote_handler, $out); $response = fgets($remote_handler); if (strpos($response, '200 OK') !== false) { $file = ''; while (!feof($remote_handler)) { $file .= fgets($remote_handler); } $file = str_replace("\r\n", "\n", $file); $file = explode("\n\n", $file); array_shift($file); $file = implode("\n\n", $file); fclose($remote_handler); return $file; } } }// else try with the next possibility case self::UPDATE_CURL: if (extension_loaded('curl')) { // make sure curl is loaded $ch = curl_init($url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $this->timeout); curl_setopt($ch, CURLOPT_USERAGENT, $this->_getUserAgent()); $file = curl_exec($ch); curl_close($ch); if ($file !== false) { return $file; } }// else try with the next possibility case false: throw new Exception( 'Your server can\'t connect to external resources. Please update the file manually.' ); } return ''; } /** * Format the useragent string to be used in the remote requests made by the * class during the update process. * * @return string the formatted user agent */ protected function _getUserAgent() { $ua = str_replace('%v', self::VERSION, $this->userAgent); $ua = str_replace('%m', $this->_getUpdateMethod(), $ua); return $ua; } } /** * Browscap.ini parsing class exception * * @package Browscap * @author Jonathan Stoppani * @copyright Copyright (c) 2006-2012 Jonathan Stoppani * @version 1.0 * @license http://www.opensource.org/licenses/MIT MIT License * @link https://github.com/GaretJax/phpbrowscap/ */ class Exception extends \Exception { // nothing to do here }