diff --git a/adm/admin.menu100.php b/adm/admin.menu100.php index 44f93712a..6ac1e32ba 100644 --- a/adm/admin.menu100.php +++ b/adm/admin.menu100.php @@ -12,6 +12,7 @@ $menu['menu100'] = array ( array('100910', '캡챠파일 일괄삭제',G5_ADMIN_URL.'/captcha_file_delete.php', 'cf_captcha', 1), array('100920', '썸네일파일 일괄삭제',G5_ADMIN_URL.'/thumbnail_file_delete.php', 'cf_thumbnail', 1), array('100500', 'phpinfo()', G5_ADMIN_URL.'/phpinfo.php', 'cf_phpinfo'), + array('100510', 'Browscap 업데이트', G5_ADMIN_URL.'/browscap.php', 'cf_browscap'), array('100400', '부가서비스', G5_ADMIN_URL.'/service.php', 'cf_service') ); ?> \ No newline at end of file diff --git a/adm/browscap.php b/adm/browscap.php new file mode 100644 index 000000000..6db2aa589 --- /dev/null +++ b/adm/browscap.php @@ -0,0 +1,42 @@ + + +
+

Browscap 정보를 업데이트하시려면 아래 업데이트 버튼을 클릭해 주세요.

+
+
+ + + + \ No newline at end of file diff --git a/adm/browscap_update.php b/adm/browscap_update.php new file mode 100644 index 000000000..2b27c237f --- /dev/null +++ b/adm/browscap_update.php @@ -0,0 +1,21 @@ +updateMethod = 'cURL'; +$browscap->cacheFilename = 'browscap_cache.php'; +$browscap->updateCache(); + +die(''); +?> \ No newline at end of file diff --git a/adm/config_form.php b/adm/config_form.php index 410bbc503..2619ad458 100644 --- a/adm/config_form.php +++ b/adm/config_form.php @@ -186,6 +186,14 @@ if(!isset($config['cf_sms_type'])) { ADD `cf_sms_type` varchar(10) NOT NULL DEFAULT '' AFTER `cf_sms_use` ", true); } +// 접속자 정보 필드 추가 +if(!sql_query(" select vi_browser from {$g5['visit_table']} limit 1 ")) { + sql_query(" ALTER TABLE `{$g5['visit_table']}` + ADD `vi_browser` varchar(255) NOT NULL DEFAULT '' AFTER `vi_agent`, + ADD `vi_os` varchar(255) NOT NULL DEFAULT '' AFTER `vi_browser`, + ADD `vi_device` varchar(255) NOT NULL DEFAULT '' AFTER `vi_os` ", true); +} + if(!$config['cf_faq_skin']) $config['cf_faq_skin'] = "basic"; if(!$config['cf_mobile_faq_skin']) $config['cf_mobile_faq_skin'] = "basic"; diff --git a/adm/css/admin.css b/adm/css/admin.css index feecc59d8..4028c3b63 100644 --- a/adm/css/admin.css +++ b/adm/css/admin.css @@ -395,6 +395,9 @@ td.td_grpset {width:160px;border-left:1px solid #e9ecee;text-align:center} .td_tdiv {border-bottom:1px solid #c9c9c9 !important} .td_tel {width:80px;text-align:center} .td_test {width:50px;text-align:center} +.td_category1{width:130px;text-align:center} +.td_category2{width:100px;text-align:center} +.td_category3{width:80px;text-align:center} .txt_true {color:#e8180c} .txt_false {color:#ccc} @@ -730,7 +733,7 @@ strong.sodr_nonpay {display:block;padding:5px 0;text-align:right} #theme_list li .tmli_if:hover button.tmli_dt{display:block} #theme_list li .theme_sl{float:left;border:none;margin-top:5px;padding:0 5px;height:26px;background:#999;color:#fff} #theme_list li .theme_sl:hover{background:#ff3061} -#theme_list li .theme_deactive{margin-left:4px} +#theme_list li .theme_deactive{margin-left:4px} #theme_list li .theme_sl_use{background:#ff3061;line-height:26px} #theme_list li .theme_pr{float:right;margin-top:5px;padding:0 5px;height:24px;line-height:24px; border: 1px solid #ccc; background: #fafafa; } #theme_list li .theme_preview{ float: right; margin-top: 5px; padding:0 5px;height:26px; border: 1px solid #ccc; background: #fafafa; margin-right:3px} @@ -763,4 +766,11 @@ strong.sodr_nonpay {display:block;padding:5px 0;text-align:right} /*전송실패 문자 재전송 내역*/ .sms_table{padding:0 20px 40px;} .sms_table table th{border:1px solid #ddd;padding:9px 0} -.sms_table table td{border:1px solid #ddd;text-align:center;width:16%;padding:9px 0} \ No newline at end of file +.sms_table table td{border:1px solid #ddd;text-align:center;width:16%;padding:9px 0} + +/* Browscap */ +.update_processing{width:300px;height:300px;margin:0 auto;background:url(../img/ajax_loader.gif) no-repeat 0 0} +#processing{margin: 0 auto;padding: 70px 0;max-width: 800px;border: 1px solid #eee;background: #f9f9f9;text-align:center;} +#processing p{font-size:1.2em} +.check_processing {width:300px;height:300px;margin:0 auto;background:url(../img/check.png) no-repeat 50% 50% } +#processing button{background:#ff3061;border:none;color:#fff;padding: 15px;width:150px;margin-top:15px} \ No newline at end of file diff --git a/adm/img/ajax_loader.gif b/adm/img/ajax_loader.gif new file mode 100644 index 000000000..5169619be Binary files /dev/null and b/adm/img/ajax_loader.gif differ diff --git a/adm/img/check.png b/adm/img/check.png new file mode 100644 index 000000000..c3c6abd9d Binary files /dev/null and b/adm/img/check.png differ diff --git a/adm/visit.sub.php b/adm/visit.sub.php index b153152b1..a9b3d7468 100644 --- a/adm/visit.sub.php +++ b/adm/visit.sub.php @@ -29,6 +29,7 @@ $query_string = $qstr ? '?'.$qstr : '';
  • 도메인
  • 브라우저
  • 운영체제
  • +
  • 접속기기
  • 시간
  • 요일
  • diff --git a/adm/visit_browser.php b/adm/visit_browser.php index 49b2e77d6..aca685012 100644 --- a/adm/visit_browser.php +++ b/adm/visit_browser.php @@ -15,7 +15,9 @@ $sql = " select * from {$g5['visit_table']} where vi_date between '{$fr_date}' and '{$to_date}' "; $result = sql_query($sql); while ($row=sql_fetch_array($result)) { - $s = get_brow($row['vi_agent']); + $s = $row['vi_browser']; + if(!$s) + $s = get_brow($row['vi_agent']); $arr[$s]++; @@ -70,7 +72,7 @@ while ($row=sql_fetch_array($result)) { - +
    diff --git a/adm/visit_device.php b/adm/visit_device.php new file mode 100644 index 000000000..629a87d18 --- /dev/null +++ b/adm/visit_device.php @@ -0,0 +1,101 @@ + $max) $max = $arr[$s]; + + $sum_count++; +} +?> + +
    + + + + + + + + + + + + + + + + + + + + $value) { + $count = $arr[$key]; + if ($save_count != $count) { + $i++; + $no = $i; + $save_count = $count; + } else { + $no = ''; + } + + if (!$key) { + $key = 'Unknown'; + } + + $rate = ($count / $sum_count * 100); + $s_rate = number_format($rate, 1); + + $bg = 'bg'.($i%2); + ?> + + + + + + + + + + '; + } + ?> + +
    목록
    순위접속기기그래프접속자수비율(%)
    합계100%
    +
    + +
    +
    자료가 없습니다.
    +
    + + diff --git a/adm/visit_list.php b/adm/visit_list.php index afc950164..4dd2cdc1c 100644 --- a/adm/visit_list.php +++ b/adm/visit_list.php @@ -7,7 +7,7 @@ auth_check($auth[$sub_menu], 'r'); $g5['title'] = '접속자집계'; include_once('./visit.sub.php'); -$colspan = 5; +$colspan = 6; $sql_common = " from {$g5['visit_table']} "; $sql_search = " where vi_date between '{$fr_date}' and '{$to_date}' "; @@ -41,15 +41,25 @@ $result = sql_query($sql); IP 접속 경로 브라우저 - 운영체제 + OS + 접속기기 일시 "> - - + + + diff --git a/adm/visit_os.php b/adm/visit_os.php index c0e043683..5e6c86022 100644 --- a/adm/visit_os.php +++ b/adm/visit_os.php @@ -15,7 +15,9 @@ $sql = " select * from {$g5['visit_table']} where vi_date between '$fr_date' and '$to_date' "; $result = sql_query($sql); while ($row=sql_fetch_array($result)) { - $s = get_os($row['vi_agent']); + $s = $row['vi_os']; + if(!$s) + $s = get_os($row['vi_agent']); $arr[$s]++; @@ -63,7 +65,7 @@ while ($row=sql_fetch_array($result)) { } if (!$key) { - $key = '직접'; + $key = 'Unknown'; } $rate = ($count / $sum_count * 100); diff --git a/adm/visit_search.php b/adm/visit_search.php index 23f1af471..269c20b52 100644 --- a/adm/visit_search.php +++ b/adm/visit_search.php @@ -9,7 +9,7 @@ $g5['title'] = '접속자검색'; include_once('./admin.head.php'); include_once(G5_PLUGIN_PATH.'/jquery-ui/datepicker.php'); -$colspan = 5; +$colspan = 6; $listall = '처음'; //페이지 처음으로 (초기화용도) ?> @@ -36,6 +36,7 @@ $listall = '처음'; //페이지 처 접속 경로 브라우저 OS + 접속기기 일시 @@ -68,8 +69,17 @@ $listall = '처음'; //페이지 처 $result = sql_query($sql); for ($i=0; $row=sql_fetch_array($result); $i++) { - $brow = get_brow($row['vi_agent']); - $os = get_os($row['vi_agent']); + $brow = $row['vi_browser']; + if(!$brow) + $brow = get_brow($row['vi_agent']); + + $os = $row['vi_os']; + if(!$os) + $os = get_os($row['vi_agent']); + + $device = $row['vi_device']; + if(!$device) + $device = get_device($row['vi_agent']); $link = ""; $referer = ""; @@ -92,16 +102,14 @@ $listall = '처음'; //페이지 처 else $ip = preg_replace("/([0-9]+).([0-9]+).([0-9]+).([0-9]+)/", G5_IP_DISPLAY, $row['vi_ip']); - if ($brow == '기타') $brow = ''.$brow.''; - if ($os == '기타') $os = ''.$os.''; - $bg = 'bg'.($i%2); ?> - - + + + diff --git a/bbs/visit_insert.inc.php b/bbs/visit_insert.inc.php index 538201b53..994dcb1e7 100644 --- a/bbs/visit_insert.inc.php +++ b/bbs/visit_insert.inc.php @@ -15,7 +15,14 @@ if (get_cookie('ck_visit_ip') != $_SERVER['REMOTE_ADDR']) if (isset($_SERVER['HTTP_REFERER'])) $referer = escape_trim(clean_xss_tags($_SERVER['HTTP_REFERER'])); $user_agent = escape_trim(clean_xss_tags($_SERVER['HTTP_USER_AGENT'])); - $sql = " insert {$g5['visit_table']} ( vi_id, vi_ip, vi_date, vi_time, vi_referer, vi_agent ) values ( '{$vi_id}', '{$remote_addr}', '".G5_TIME_YMD."', '".G5_TIME_HIS."', '{$referer}', '{$user_agent}' ) "; + // Browscap 캐시 파일이 있으면 실행 + if(is_file(G5_DATA_PATH.'/cache/browscap_cache.php')) { + $browscap = get_browscap_info($_SERVER['HTTP_USER_AGENT']); + $vi_browser = $browscap->Comment; + $vi_os = $browscap->Platform; + $vi_device = $browscap->Device_Type; + } + $sql = " insert {$g5['visit_table']} ( vi_id, vi_ip, vi_date, vi_time, vi_referer, vi_agent, vi_browser, vi_os, vi_device ) values ( '{$vi_id}', '{$remote_addr}', '".G5_TIME_YMD."', '".G5_TIME_HIS."', '{$referer}', '{$user_agent}', '{$vi_browser}', '{$vi_os}', '{$vi_device}' ) "; $result = sql_query($sql, FALSE); // 정상으로 INSERT 되었다면 방문자 합계에 반영 diff --git a/lib/common.lib.php b/lib/common.lib.php index 8313f6e18..deca88652 100644 --- a/lib/common.lib.php +++ b/lib/common.lib.php @@ -3162,4 +3162,22 @@ function check_vaild_callback($callback){ return true; } } + +// Browscap 정보 얻기 +function get_browscap_info($agent) +{ + if(!$agent) + return false; + + include_once(G5_PLUGIN_PATH.'/browscap/Browscap.php'); + + $browscap = new phpbrowscap\Browscap(G5_DATA_PATH.'/cache'); + $browscap->updateMethod = 'cURL'; + $browscap->doAutoUpdate = false; + $browscap->cacheFilename = 'browscap_cache.php'; + + $info = $browscap->getBrowser($agent); + + return $info; +} ?> \ No newline at end of file diff --git a/lib/thumbnail.lib.php b/lib/thumbnail.lib.php index 0033c2aed..c3dac55a2 100644 --- a/lib/thumbnail.lib.php +++ b/lib/thumbnail.lib.php @@ -1,7 +1,7 @@ "; - - if (preg_match("/msie ([1-9][0-9]\.[0-9]+)/", $agent, $m)) { $s = 'MSIE '.$m[1]; } - else if(preg_match("/firefox/", $agent)) { $s = "FireFox"; } - else if(preg_match("/chrome/", $agent)) { $s = "Chrome"; } - else if(preg_match("/x11/", $agent)) { $s = "Netscape"; } - else if(preg_match("/opera/", $agent)) { $s = "Opera"; } - else if(preg_match("/gec/", $agent)) { $s = "Gecko"; } - else if(preg_match("/bot|slurp/", $agent)) { $s = "Robot"; } - else if(preg_match("/internet explorer/", $agent)) { $s = "IE"; } - else if(preg_match("/mozilla/", $agent)) { $s = "Mozilla"; } - else { $s = "기타"; } - - return $s; + return $info->Comment; } function get_os($agent) { - $agent = strtolower($agent); + $info = get_browscap_info($agent); - //echo $agent; echo "
    "; + return $info->Platform; +} - if (preg_match("/windows 98/", $agent)) { $s = "98"; } - else if(preg_match("/windows 95/", $agent)) { $s = "95"; } - else if(preg_match("/windows nt 4\.[0-9]*/", $agent)) { $s = "NT"; } - else if(preg_match("/windows nt 5\.0/", $agent)) { $s = "2000"; } - else if(preg_match("/windows nt 5\.1/", $agent)) { $s = "XP"; } - else if(preg_match("/windows nt 5\.2/", $agent)) { $s = "2003"; } - else if(preg_match("/windows nt 6\.0/", $agent)) { $s = "Vista"; } - else if(preg_match("/windows nt 6\.1/", $agent)) { $s = "Windows7"; } - else if(preg_match("/windows nt 6\.2/", $agent)) { $s = "Windows8"; } - else if(preg_match("/windows 9x/", $agent)) { $s = "ME"; } - else if(preg_match("/windows ce/", $agent)) { $s = "CE"; } - else if(preg_match("/mac/", $agent)) { $s = "MAC"; } - else if(preg_match("/linux/", $agent)) { $s = "Linux"; } - else if(preg_match("/sunos/", $agent)) { $s = "sunOS"; } - else if(preg_match("/irix/", $agent)) { $s = "IRIX"; } - else if(preg_match("/phone/", $agent)) { $s = "Phone"; } - else if(preg_match("/bot|slurp/", $agent)) { $s = "Robot"; } - else if(preg_match("/internet explorer/", $agent)) { $s = "IE"; } - else if(preg_match("/mozilla/", $agent)) { $s = "Mozilla"; } - else { $s = "기타"; } +function get_device($agent) +{ + $info = get_browscap_info($agent); - return $s; + return $info->Device_Type; } ?> \ No newline at end of file diff --git a/plugin/browscap/Browscap.php b/plugin/browscap/Browscap.php new file mode 100644 index 000000000..921cc9449 --- /dev/null +++ b/plugin/browscap/Browscap.php @@ -0,0 +1,1459 @@ + + * @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 +} +?> \ No newline at end of file