diff --git a/Access_Core.php b/Access_Core.php index 31a8eb9..d3af8c3 100644 --- a/Access_Core.php +++ b/Access_Core.php @@ -6,89 +6,107 @@ if (!defined('__ACCESS_PLUGIN_ROOT__')) { class Access_Core { protected $db; - protected $prefix; - protected $table; - public $config; - protected $response; protected $request; - protected $pageSize; - protected $isDrop; - public $parser; + protected $response; + + public $ua; + public $config; public $action; public $title; public $logs = array(); public $overview = array(); public $referer = array(); + /** + * 构造函数,根据不同类型的请求,计算不同的数据并渲染输出 + * + * @access public + * @return void + */ public function __construct() { - $this->db = Typecho_Db::get(); - $this->prefix = $this->db->getPrefix(); - $this->table = $this->prefix . 'access'; - $this->config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); - $this->response = Typecho_Response::getInstance(); - $this->request = Typecho_Request::getInstance(); - $this->pageSize = $this->config->pageSize; - $this->isDrop = $this->config->isDrop; - if ($this->pageSize == null || $this->isDrop == null) { - throw new Typecho_Plugin_Exception('请先设置插件!'); + # Load language pack + if (Typecho_I18n::getLang() != 'zh_CN') { + $file = __TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__ . + '/Access/lang/' . Typecho_I18n::getLang() . '.mo'; + file_exists($file) && Typecho_I18n::addLang($file); } - $this->parser = new Access_Parser(); + # Init variables + $this->db = Typecho_Db::get(); + $this->config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); + $this->request = Typecho_Request::getInstance(); + $this->response = Typecho_Response::getInstance(); + if ($this->config->pageSize == null || $this->config->isDrop == null) { + throw new Typecho_Plugin_Exception(_t('请先设置插件!')); + } + $this->ua = new Access_UA($this->request->getAgent()); switch ($this->request->get('action')) { - case 'logs': - default: - $this->action = 'logs'; - $this->title = '访问日志'; - $this->parseLogs(); - break; case 'overview': $this->action = 'overview'; - $this->title = '访问概览'; + $this->title = _t('访问概览'); $this->parseOverview(); $this->parseReferer(); break; - } - } - - protected function getWhere($type) - { - $where_str = ''; - foreach ($this->parser->bots as $value) { - $where_str .= "replace(LOWER(`ua`), ' ', '') {1} LIKE " . "'%{$this->parser->filter($value)}%' {2} "; - } - $where_str = rtrim($where_str, '{2} '); - switch ($type) { - case 1: - $where = str_replace('{1}', 'NOT', $where_str); - $where = str_replace('{2}', 'and', $where); - break; - case 2: - $where = str_replace('{1}', '', $where_str); - $where = str_replace('{2}', 'or', $where); - break; - case 3: - $where = '1=1'; - break; + case 'logs': default: - throw new Typecho_Plugin_Exception('参数不正确!'); + $this->action = 'logs'; + $this->title = _t('访问日志'); + $this->parseLogs(); + break; } - return 'WHERE ' . $where; } + /** + * 生成详细访问日志数据,提供给页面渲染使用 + * + * @access public + * @return void + */ protected function parseLogs() { $type = $this->request->get('type', 1); - $p = $this->request->get('page', 1); - $offset = (max(intval($p), 1) - 1) * $this->pageSize; - $where = $this->getWhere($type); + $pagenum = $this->request->get('page', 1); + $offset = (max(intval($pagenum), 1) - 1) * $this->config->pageSize; + $query = $this->db->select()->from('table.access_log') + ->order('time', Typecho_Db::SORT_DESC) + ->offset($offset)->limit($this->config->pageSize); + $qcount = $this->db->select('count(1) AS count')->from('table.access_log'); + switch ($type) { + case 1: + $query->where('robot = ?', 0); + $qcount->where('robot = ?', 0); + break; + case 2: + $query->where('robot = ?', 1); + $qcount->where('robot = ?', 1); + break; + default: + break; + } + $this->logs['list'] = $this->db->fetchAll($query); + foreach ($this->logs['list'] as &$row) { + $ua = new Access_UA($row['ua']); + if ($ua->isRobot()) { + $name = $ua->getRobotID(); + $version = $ua->getRobotVersion(); + } else { + $name = $ua->getBrowserName(); + $version = $ua->getBrowserVersion(); + } + if ($name == '') { + $row['display_name'] = _t('未知'); + } elseif ($version == '') { + $row['display_name'] = $name; + } else { + $row['display_name'] = $name . ' / ' . $version; + } + } - $this->logs['list'] = $this->db->fetchAll("SELECT * FROM {$this->table} {$where} ORDER BY id DESC LIMIT {$this->pageSize} OFFSET {$offset}"); + $this->htmlEncode($this->logs['list']); - $this->cleanArray($this->logs['list']); - - $this->logs['rows'] = count($this->db->fetchAll("SELECT * FROM {$this->table} {$where}")); - - $page = new Access_Page($this->pageSize, $this->logs['rows'], $p, 10, array( + $this->logs['rows'] = $this->db->fetchAll($qcount)[0]['count']; + + $page = new Access_Page($this->config->pageSize, $this->logs['rows'], $pagenum, 10, array( 'panel' => Access_Plugin::$panel, 'action' => 'logs', 'type' => $type, @@ -96,85 +114,105 @@ class Access_Core $this->logs['page'] = $page->show(); } + /** + * 生成来源统计数据,提供给页面渲染使用 + * + * @access public + * @return void + */ protected function parseReferer() { - $this->referer['url'] = $this->db->fetchAll("SELECT DISTINCT referer, COUNT(*) as count FROM {$this->table} WHERE referer <> '' GROUP BY referer ORDER BY count DESC LIMIT {$this->pageSize}"); - $this->referer['domain'] = $this->db->fetchAll("SELECT DISTINCT referer_domain, COUNT(*) as count FROM {$this->table} WHERE referer_domain <> '' GROUP BY referer_domain ORDER BY count DESC LIMIT {$this->pageSize}"); - $this->cleanArray($this->referer); + $this->referer['url'] = $this->db->fetchAll($this->db->select('DISTINCT entrypoint AS value, COUNT(1) as count') + ->from('table.access_log')->where("entrypoint <> ''")->group('entrypoint') + ->order('count', Typecho_Db::SORT_DESC)->limit($this->config->pageSize)); + $this->referer['domain'] = $this->db->fetchAll($this->db->select('DISTINCT entrypoint_domain AS value, COUNT(1) as count') + ->from('table.access_log')->where("entrypoint_domain <> ''")->group('entrypoint_domain') + ->order('count', Typecho_Db::SORT_DESC)->limit($this->config->pageSize)); + $this->htmlEncode($this->referer); } + /** + * 生成总览数据,提供给页面渲染使用 + * + * @access public + * @return void + */ protected function parseOverview() { - - $where = 'WHERE 1=1'; - - $this->overview['ip']['today']['total'] = 0; - $this->overview['uv']['today']['total'] = 0; - $this->overview['pv']['today']['total'] = 0; - $this->overview['ip']['yesterday']['total'] = 0; - $this->overview['uv']['yesterday']['total'] = 0; - $this->overview['pv']['yesterday']['total'] = 0; - - for ($i = 0; $i < 24; $i++) { - $today = date("Y-m-d"); - $start = strtotime(date("{$today} {$i}:00:00")); - $end = strtotime(date("{$today} {$i}:59:59")); - $this->overview['ip']['today']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['ip']['today']['total'] += $this->overview['ip']['today']['hours'][$i]; - $this->overview['uv']['today']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['uv']['today']['total'] += $this->overview['uv']['today']['hours'][$i]; - $this->overview['pv']['today']['hours'][] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['pv']['today']['total'] += $this->overview['pv']['today']['hours'][$i]; - } - - for ($i = 0; $i < 24; $i++) { - $yesterday = date("Y-m-d", time() - 24 * 60 * 60); - $start = strtotime(date("{$yesterday} {$i}:00:00")); - $end = strtotime(date("{$yesterday} {$i}:59:59")); - $this->overview['ip']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['ip']['yesterday']['total'] += $this->overview['ip']['yesterday']['hours'][$i]; - $this->overview['uv']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['uv']['yesterday']['total'] += $this->overview['uv']['yesterday']['hours'][$i]; - $this->overview['pv']['yesterday']['hours'][] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where} AND date BETWEEN {$start} AND {$end}")); - $this->overview['pv']['yesterday']['total'] += $this->overview['pv']['yesterday']['hours'][$i]; - } - - $this->overview['ip']['all']['total'] = count($this->db->fetchAll("SELECT DISTINCT ip FROM {$this->table} {$where}")); - $this->overview['uv']['all']['total'] = count($this->db->fetchAll("SELECT DISTINCT ip,ua FROM {$this->table} {$where}")); - $this->overview['pv']['all']['total'] = count($this->db->fetchAll("SELECT ip FROM {$this->table} {$where}")); - - $this->overview['chart']['title']['text'] = date("Y-m-d 统计"); - $this->overview['chart']['xAxis']['categories'] = $this->buildObject(array(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23), true); - $this->overview['chart']['series']['pv'] = $this->buildObject($this->overview['pv']['today']['hours'], false); - $this->overview['chart']['series']['uv'] = $this->buildObject($this->overview['uv']['today']['hours'], false); - $this->overview['chart']['series']['ip'] = $this->buildObject($this->overview['ip']['today']['hours'], false); - - } - - protected function cleanArray(&$array) - { - if (is_array($array)) { - foreach ($array as &$value) { - if (!is_array($value)) { - $value = htmlspecialchars(urldecode($value)); - } else { - $this->cleanArray($value); - } + # 初始化统计数组 + foreach (['ip', 'uv', 'pv'] as $type) { + foreach (['today', 'yesterday'] as $day) { + $this->overview[$type][$day]['total'] = 0; } } + + # 分类分时段统计数据 + foreach (['today' => date("Y-m-d"), 'yesterday'=> date("Y-m-d", time() - 24 * 60 * 60)] as $day => $time) { + for ($i = 0; $i < 24; $i++) { + $time = date("Y-m-d"); + $start = strtotime(date("{$time} {$i}:00:00")); + $end = strtotime(date("{$time} {$i}:59:59")); + // "SELECT DISTINCT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}")); + $this->overview['ip'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('(' . $this->db->select('DISTINCT ip')->from('table.access_log') + ->where('time >= ? AND time <= ?', $start, $end) . ') AS tmp'))[0]['count']); + $this->overview['ip'][$day]['total'] += $this->overview['ip'][$day]['hours'][$i]; + // "SELECT DISTINCT ip,ua FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}")); + $this->overview['uv'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('(' . $this->db->select('DISTINCT ip,ua')->from('table.access_log') + ->where('time >= ? AND time <= ?', $start, $end) . ') AS tmp'))[0]['count']); + $this->overview['uv'][$day]['total'] += $this->overview['uv'][$day]['hours'][$i]; + // "SELECT ip FROM {$this->table} {$where} AND `time` BETWEEN {$start} AND {$end}")); + $this->overview['pv'][$day]['hours'][$i] = intval($this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('table.access_log')->where('time >= ? AND time <= ?', $start, $end))[0]['count']); + $this->overview['pv'][$day]['total'] += $this->overview['pv'][$day]['hours'][$i]; + } + } + + # 总统计数据 + // "SELECT DISTINCT ip FROM {$this->table} {$where}")); + $this->overview['ip']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('(' . $this->db->select('DISTINCT ip')->from('table.access_log') . ') AS tmp'))[0]['count']; + // "SELECT DISTINCT ip,ua FROM {$this->table} {$where}")); + $this->overview['uv']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('(' . $this->db->select('DISTINCT ip,ua')->from('table.access_log') . ') AS tmp'))[0]['count']; + // "SELECT ip FROM {$this->table} {$where}")); + $this->overview['pv']['all']['total'] = $this->db->fetchAll($this->db->select('COUNT(1) AS count') + ->from('table.access_log'))[0]['count']; + + # 分类型绘制24小时访问图 + $this->overview['chart']['xAxis']['categories'] = json_encode([ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23 + ]); + foreach (['ip', 'uv', 'pv'] as $type) { + $this->overview['chart']['series'][$type] = json_encode($this->overview[$type]['today']['hours']); + } + $this->overview['chart']['title']['text'] = _t('%s 统计', date("Y-m-d")); } - protected function buildObject($array, $quote) + /** + * 转义特殊字符,防止XSS等攻击 + * + * @access public + * @return void + */ + protected function htmlEncode(&$variable) { - $obj = Json::encode($array); - $obj = str_replace("\"", "'", $obj); - if ($quote) { - return $obj; - } else { - return str_replace("'", '', $obj); + if (is_array($variable)) { + foreach ($variable as &$value) { + $this->htmlEncode($value); + } + } elseif (is_string($variable)) { + $variable = htmlspecialchars(urldecode($variable)); } } + /** + * 判断是否是管理员登录状态 + * + * @access public + * @return bool + */ public function isAdmin() { $hasLogin = Typecho_Widget::widget('Widget_User')->hasLogin(); @@ -185,58 +223,88 @@ class Access_Core return $isAdmin; } + /** + * 删除记录 + * + * @access public + * @return void + */ public function deleteLogs($ids) { foreach ($ids as $id) { - $this->db->query($this->db->delete($this->table) - ->where('id = ?', $id) + $this->db->query($this->db->delete('table.access_log') + ->where('id = ?', $id) ); } } - public function getReferer() + /** + * 获取首次进入网站时的来源 + * + * @access public + * @return string + */ + public function getEntryPoint() { - $referer = Typecho_Cookie::get('__typecho_access_referer'); - if ($referer == null) { - $referer = $this->request->getReferer(); - if (strpos($referer, rtrim(Helper::options()->siteUrl, '/')) !== false) { - $referer = null; + $entrypoint = Typecho_Cookie::get('__typecho_access_entrypoint'); + if ($entrypoint == null) { + $entrypoint = $this->request->getReferer(); + if (strpos($entrypoint, rtrim(Helper::options()->siteUrl, '/')) !== false) { + $entrypoint = null; } - if ($referer != null) { - Typecho_Cookie::set('__typecho_access_referer', $referer); + if ($entrypoint != null) { + Typecho_Cookie::set('__typecho_access_entrypoint', $entrypoint); } } - return $referer; + return $entrypoint; } + /** + * 记录当前访问(管理员登录不会记录) + * + * @access public + * @return void + */ public function writeLogs($url = null) { if ($this->isAdmin()) { return; } - $ip = $this->request->getIp(); if ($url == null) { $url = $this->request->getServer('REQUEST_URI'); } + $ip = $this->request->getIp(); if ($ip == null) { - $ip = 'UnKnown'; + $ip = '0.0.0.0'; } - - $timeStamp = Helper::options()->gmtTime; - $offset = Helper::options()->timezone - Helper::options()->serverTimezone; - $gtime = $timeStamp + $offset; - $referer = $this->getReferer(); + $ip = bindec(decbin(ip2long($ip))); + + $entrypoint = $this->getEntryPoint(); + $referer = $this->request->getReferer(); + $time = Helper::options()->gmtTime + (Helper::options()->timezone - Helper::options()->serverTimezone); $rows = array( - 'ua' => $this->request->getAgent(), - 'url' => $url, - 'ip' => $ip, - 'referer' => $referer, - 'referer_domain' => parse_url($referer, PHP_URL_HOST), - 'date' => $gtime, + 'ua' => $this->ua->getUA(), + 'browser_id' => $this->ua->getBrowserID(), + 'browser_version' => $this->ua->getBrowserVersion(), + 'os_id' => $this->ua->getOSID(), + 'os_version' => $this->ua->getOSVersion(), + 'url' => $url, + 'path' => parse_url($url, PHP_URL_PATH), + 'query_string' => parse_url($url, PHP_URL_QUERY), + 'ip' => $ip, + 'referer' => $referer, + 'referer_domain' => parse_url($referer, PHP_URL_HOST), + 'entrypoint' => $entrypoint, + 'entrypoint_domain' => parse_url($entrypoint, PHP_URL_HOST), + 'time' => $time, + // 'content_id' => , + 'robot' => $this->ua->isRobot() ? 1 : 0, + 'robot_id' => $this->ua->getRobotID(), + 'robot_version' => $this->ua->getRobotVersion(), ); try { - $this->db->query($this->db->insert('table.access')->rows($rows)); + $this->db->query($this->db->insert('table.access_log')->rows($rows)); } catch (Exception $e) {} catch (Typecho_Db_Query_Exception $e) {} } diff --git a/Access_Parser.php b/Access_Parser.php deleted file mode 100644 index 6aaa5e8..0000000 --- a/Access_Parser.php +++ /dev/null @@ -1,163 +0,0 @@ -isBot($ua)) { - return $this->currentBot; - } elseif (preg_match('/Windows NT 6.0/i', $ua)) { - $os = 'Windows Vista'; - } elseif (preg_match('/Windows NT 6.1/i', $ua)) { - $os = 'Windows 7'; - } elseif (preg_match('/Windows NT 6.2/i', $ua)) { - $os = 'Windows 8'; - } elseif (preg_match('/Windows NT 6.3/i', $ua)) { - $os = 'Windows 8.1'; - } elseif (preg_match('/Windows NT 10.0/i', $ua)) { - $os = 'Windows 10'; - } elseif (preg_match('/Windows NT 5.1/i', $ua)) { - $os = 'Windows XP'; - } elseif (preg_match('/Windows NT 5.2/i', $ua) && preg_match('/Win64/i', $ua)) { - $os = 'Windows XP 64 bit'; - } elseif (preg_match('/Android ([0-9.]+)/i', $ua, $matches)) { - $os = 'Android ' . $matches[1]; - } elseif (preg_match('/iPhone OS ([_0-9]+)/i', $ua, $matches)) { - $os = 'iPhone ' . $matches[1]; - } elseif (preg_match('/Ubuntu/i', $ua, $matches)) { - $os = 'Ubuntu '; - } elseif (preg_match('/Mac OS X ([0-9_]+)/i', $ua, $matches)) { - $os = 'Mac OS X ' . $matches[1]; - } elseif (preg_match('/Linux/i', $ua, $matches)) { - $os = 'Linux'; - } else { - $os = '未知'; - } - - if ($this->isBot($ua)) { - return $this->currentBot; - } elseif (preg_match('#(Camino|Chimera)[ /]([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Camino ' . $matches[2]; - } elseif (preg_match('#SE 2([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = '搜狗浏览器 2' . $matches[1]; - } elseif (preg_match('#360([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = '360浏览器 ' . $matches[1]; - } elseif (preg_match('#Maxthon( |\/)([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Maxthon ' . $matches[2]; - } elseif (preg_match('#Edge/([a-zA-Z0-9.]+)#i', $ua, $matches)) { - //Win10中Microsoft Edge浏览器 - $browser = 'Edge ' . $matches[1]; - } elseif (preg_match('#Chrome/([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Chrome ' . $matches[1]; - } elseif (preg_match('#XiaoMi/MiuiBrowser/([0-9.]+)#i', $ua, $matches)) { - $browser = '小米浏览器 ' . $matches[1]; - } elseif (preg_match('#Safari/([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Safari ' . $matches[1]; - } elseif (preg_match('#opera mini#i', $ua)) { - preg_match('#Opera/([a-zA-Z0-9.]+)#i', $ua, $matches); - $browser = 'Opera Mini ' . $matches[1]; - } elseif (preg_match('#Opera.([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Opera ' . $matches[1]; - } elseif (preg_match('#TencentTraveler ([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = '腾讯TT浏览器 ' . $matches[1]; - } elseif (preg_match('#UCWEB([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'UCWEB ' . $matches[1]; - } elseif (preg_match('#MSIE ([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Internet Explorer ' . $matches[1]; - } elseif (preg_match('#Trident#', $ua, $matches)) { - $browser = 'Internet Explorer 11'; - } elseif (preg_match('#(Firefox|Phoenix|Firebird|BonEcho|GranParadiso|Minefield|Iceweasel)/([a-zA-Z0-9.]+)#i', $ua, $matches)) { - $browser = 'Firefox ' . $matches[2]; - } else { - $browser = '未知'; - } - return $os . ' / ' . $browser; - } - - public function isBot($ua) - { - $ua = $this->filter($ua); - if (!empty($ua)) { - foreach ($this->bots as $val) { - if (($val == 'Bot' || $val == 'Spider') - && (preg_match('#([a-zA-Z0-9]+(bot|spider))[ /]*([0-9.]*)#i', $ua, $matches))) { - $this->currentBot = $matches[1] . ' ' . $matches[3]; - return true; - } - $str = $this->filter($val); - if (strpos($ua, $str) !== false) { - $this->currentBot = $str; - return true; - } - } - } else { - return false; - } - } - - public function filter($str) - { - return $this->removeSpace(strtolower($str)); - } - - protected function removeSpace($str) - { - return preg_replace('/\s+/', '', $str); - } -} diff --git a/Access_UA.php b/Access_UA.php new file mode 100644 index 0000000..051828e --- /dev/null +++ b/Access_UA.php @@ -0,0 +1,324 @@ +ua = $ua; + $this->ual = $this->filter($ua); + } + + public static function filter($str) + { + return self::removeSpace(strtolower($str)); + } + + protected static function removeSpace($str) + { + return preg_replace('/\s+/', '', $str); + } + + /** + * 获取完整UA信息 + * + * @access public + * @return string + */ + public function getUA() { + return $this->ua; + } + + /** + * 获取是否是爬虫 + * + * @access public + * @return bool + */ + public function isRobot() + { + if ($this->robotID === null) { + if (!empty($this->ua)) { + if (preg_match('#([a-zA-Z0-9]+\s*(?:bot|spider))[ /]*([0-9.]*)#i', $this->ua, $matches)) { + $this->robotID = $this->robotName = $matches[1]; + $this->robotVersion = $matches[2]; + } + foreach (self::$robots as $val) { + if (strpos($this->ual, $this->filter($val)) !== false) { + $this->robotID = $this->robotName = $val; + $this->robotVersion = ''; + } + } + } + if ($this->robotID == null) $this->robotID = ''; + if ($this->robotName == null) $this->robotName = ''; + if ($this->robotVersion == null) $this->robotVersion = ''; + } + return $this->robotID !== ''; + } + + /** + * 获取爬虫ID + * + * @access public + * @return string + */ + public function getRobotID() + { + return $this->isRobot() ? $this->robotID : ''; + } + + /** + * 获取爬虫版本 + * + * @access public + * @return string + */ + public function getRobotVersion() + { + return $this->isRobot() ? $this->robotVersion : ''; + } + + /** + * 解析操作系统信息 + * + * @access private + * @return bool + */ + private function parseOS() + { + if ($this->osID === null) { + if (preg_match('/Windows NT 6.0/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = 'Vista'; + } elseif (preg_match('/Windows NT 6.1/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = '7'; + } elseif (preg_match('/Windows NT 6.2/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = '8'; + } elseif (preg_match('/Windows NT 6.3/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = '8.1'; + } elseif (preg_match('/Windows NT 10.0/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = '10'; + } elseif (preg_match('/Windows NT 5.1/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = 'XP'; + } elseif (preg_match('/Windows NT 5.2/i', $this->ua) && preg_match('/Win64/i', $this->ua)) { + $this->osID = $this->osName = 'Windows'; + $this->osVersion = 'XP (64 bit)'; + } elseif (preg_match('/Android ([0-9.]+)/i', $this->ua, $matches)) { + $this->osID = $this->osName = 'Android'; + $this->osVersion = $matches[1]; + } elseif (preg_match('/iPhone OS ([_0-9]+)/i', $this->ua, $matches)) { + $this->osID = $this->osName = 'iPhone OS'; + $this->osVersion = $matches[1]; + } elseif (preg_match('/Ubuntu/i', $this->ua, $matches)) { + $this->osID = $this->osName = 'Ubuntu'; + $this->osVersion = ''; + } elseif (preg_match('/Mac OS X ([0-9_]+)/i', $this->ua, $matches)) { + $this->osID = $this->osName = 'Mac OS X'; + $this->osVersion = $matches[1]; + } elseif (preg_match('/Linux/i', $this->ua, $matches)) { + $this->osID = $this->osName = 'Linux'; + $this->osVersion = ''; + } else { + $this->osID = ''; + $this->osName = ''; + $this->osVersion = ''; + } + } + return $this->osID !== '' || $this->osName !== ''; + } + + /** + * 获取操作系统ID + * + * @access public + * @return string + */ + public function getOSID() { + return $this->parseOS() ? $this->osID : ''; + } + + /** + * 获取操作系统名字 + * + * @access public + * @return string + */ + public function getOSName() { + return $this->parseOS() ? $this->osName : ''; + } + + /** + * 获取操作系统版本号 + * + * @access public + * @return string + */ + public function getOSVersion() { + return $this->parseOS() ? $this->osVersion : ''; + } + + /** + * 解析浏览器信息 + * + * @access private + * @return bool + */ + private function parseBrowser() { + if ($this->browserName === null) { + if (preg_match('#(Camino|Chimera)[ /]([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Camino'; + $this->browserVersion = $matches[2]; + } elseif (preg_match('#SE 2([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = 'SE 2'; + $this->browserName = '搜狗浏览器 2'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#360([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = '360'; + $this->browserName = '360浏览器'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#Maxthon( |\/)([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Maxthon'; + $this->browserVersion = $matches[2]; + } elseif (preg_match('#Edge/([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Edge'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#Chrome/([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Chrome'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#XiaoMi/MiuiBrowser/([0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = '小米浏览器'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#Safari/([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Safari'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#opera mini#i', $this->ua)) { + preg_match('#Opera/([a-zA-Z0-9.]+)#i', $this->ua, $matches); + $this->browserID = $this->browserName = 'Opera Mini'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#Opera.([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Opera'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#TencentTraveler ([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = 'TencentTraveler'; + $this->browserName = '腾讯TT浏览器'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#UCWEB([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'UCWEB'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#MSIE ([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Internet Explorer'; + $this->browserVersion = $matches[1]; + } elseif (preg_match('#Trident#', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Internet Explorer'; + $this->browserVersion = '11'; + } elseif (preg_match('#(Firefox|Phoenix|Firebird|BonEcho|GranParadiso|Minefield|Iceweasel)/([a-zA-Z0-9.]+)#i', $this->ua, $matches)) { + $this->browserID = $this->browserName = 'Firefox'; + $this->browserVersion = $matches[2]; + } else { + $this->browserID = ''; + $this->browserName = ''; + $this->browserVersion = ''; + } + } + return $this->browserID !== '' || $this->browserName !== ''; + } + + /** + * 获取浏览器ID + * + * @access public + * @return string + */ + public function getBrowserID() { + return $this->parseBrowser() ? $this->browserID : ''; + } + + /** + * 获取浏览器名字 + * + * @access public + * @return string + */ + public function getBrowserName() { + return $this->parseBrowser() ? $this->browserName : ''; + } + + /** + * 获取浏览器版本号 + * + * @access public + * @return string + */ + public function getBrowserVersion() { + return $this->parseBrowser() ? $this->browserVersion : ''; + } +} diff --git a/Plugin.php b/Plugin.php index 398fffb..2057b8c 100644 --- a/Plugin.php +++ b/Plugin.php @@ -1,19 +1,28 @@ @一名宅 部分优化重构。 * * @package Access * @author Kokororin - * @version 1.6 + * @version 2.0.0 * @link https://kotori.love */ class Access_Plugin implements Typecho_Plugin_Interface { + public const VERSION = 2; public static $panel = 'Access/page/console.php'; + + /** + * 激活插件方法,如果激活失败,直接抛出异常 + * + * @access public + * @return string + * @throws Typecho_Plugin_Exception + */ public static function activate() { $msg = Access_Plugin::install(); - Helper::addPanel(1, self::$panel, 'Access控制台', 'Access插件控制台', 'subscriber'); + Helper::addPanel(1, self::$panel, _t('Access控制台'), _t('Access插件控制台'), 'subscriber'); Helper::addRoute("access_write_logs", "/access/log/write.json", "Access_Action", 'writeLogs'); Helper::addRoute("access_ip", "/access/ip.json", "Access_Action", 'ip'); Helper::addRoute("access_delete_logs", "/access/log/delete.json", "Access_Action", 'deleteLogs'); @@ -23,14 +32,20 @@ class Access_Plugin implements Typecho_Plugin_Interface return _t($msg); } + /** + * 禁用插件方法,如果禁用失败,直接抛出异常 + * + * @static + * @access public + * @return void + * @throws Typecho_Plugin_Exception + */ public static function deactivate() { $config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); - $isDrop = $config->isDrop; - if ($isDrop == 0) { + if ($config->isDrop == 0) { $db = Typecho_Db::get(); - $prefix = $db->getPrefix(); - $db->query("DROP TABLE `" . $prefix . "access`", Typecho_Db::WRITE); + $db->query("DROP TABLE `{$db->getPrefix()}access_log`", Typecho_Db::WRITE); } Helper::removePanel(1, self::$panel); Helper::removeRoute("access_write_logs"); @@ -38,6 +53,13 @@ class Access_Plugin implements Typecho_Plugin_Interface Helper::removeRoute("access_delete_logs"); } + /** + * 获取插件配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form 配置面板 + * @return void + */ public static function config(Typecho_Widget_Helper_Form $form) { $pageSize = new Typecho_Widget_Helper_Form_Element_Text( @@ -64,67 +86,99 @@ class Access_Plugin implements Typecho_Plugin_Interface $form->addInput($canAnalytize); } - public static function personalConfig(Typecho_Widget_Helper_Form $form) - { - } + /** + * 个人用户的配置面板 + * + * @access public + * @param Typecho_Widget_Helper_Form $form + * @return void + */ + public static function personalConfig(Typecho_Widget_Helper_Form $form) { } + /** + * 初始化以及升级插件数据库,如初始化失败,直接抛出异常 + * + * @access public + * @return string + * @throws Typecho_Plugin_Exception + */ public static function install() { - $configLink = '请设置'; if (substr(trim(dirname(__FILE__), '/'), -6) != 'Access') { - throw new Typecho_Plugin_Exception('插件目录名必须为Access'); + throw new Typecho_Plugin_Exception(_t('插件目录名必须为Access')); } - $installDb = Typecho_Db::get(); - $type = explode('_', $installDb->getAdapterName()); - $type = array_pop($type); - $prefix = $installDb->getPrefix(); - $scripts = "CREATE TABLE `typecho_access` ( - `id` int(10) unsigned NOT NULL auto_increment, - `ua` varchar(255) default NULL, - `url` varchar(64) default NULL, - `ip` varchar(48) default NULL, - `referer` varchar(255) default NULL, - `referer_domain` varchar(100) default NULL, - `date` int(10) unsigned default '0', - PRIMARY KEY (`id`) -) ENGINE=MYISAM DEFAULT CHARSET=%charset%;"; + $db = Typecho_Db::get(); + $adapterName = $db->getAdapterName(); + if (false === strpos($adapterName, 'Mysql')) { + throw new Typecho_Plugin_Exception(_t('你的适配器为%s,目前只支持Mysql', $adapterName)); + } + + $prefix = $db->getPrefix(); + $scripts = file_get_contents('usr/plugins/Access/sql/Mysql.sql'); $scripts = str_replace('typecho_', $prefix, $scripts); $scripts = str_replace('%charset%', 'utf8', $scripts); $scripts = explode(';', $scripts); try { - foreach ($scripts as $script) { - $script = trim($script); - if ($script) { - $installDb->query($script, Typecho_Db::WRITE); + $configLink = '' . _t('前往设置') . ''; + # 初始化数据库如果不存在 + if (!$db->fetchRow($db->query("SHOW TABLES LIKE '{$prefix}access_log';", Typecho_Db::READ))) { + foreach ($scripts as $script) { + $script = trim($script); + if ($script) { + $db->query($script, Typecho_Db::WRITE); + } } + $msg = _t('成功创建数据表,插件启用成功,') . $configLink; } - return '成功创建数据表,插件启用成功,' . $configLink; + # 处理旧版本数据 + if ($db->fetchRow($db->query("SHOW TABLES LIKE '{$prefix}access';", Typecho_Db::READ))) { + require_once __DIR__ . '/Access_Bootstrap.php'; + $rows = $db->fetchAll($db->select()->from('table.access')); + foreach ($rows as $row) { + $ua = new Access_UA($row['ua']); + $time = Helper::options()->gmtTime + (Helper::options()->timezone - Helper::options()->serverTimezone); + $row['browser_id' ] = $ua->getBrowserID(); + $row['browser_version' ] = $ua->getBrowserVersion(); + $row['os_id' ] = $ua->getOSID(); + $row['os_version' ] = $ua->getOSVersion(); + $row['path' ] = parse_url($row['url'], PHP_URL_PATH); + $row['query_string' ] = parse_url($row['url'], PHP_URL_QUERY); + $row['ip' ] = bindec(decbin(ip2long($row['ip']))); + $row['entrypoint' ] = $row['referer']; + $row['entrypoint_domain'] = $row['referer_domain']; + $row['time' ] = $row['date']; + $row['robot' ] = $ua->isRobot() ? 1 : 0; + $row['robot_id' ] = $ua->getRobotID(); + $row['robot_version' ] = $ua->getRobotVersion(); + unset($row['date']); + try { + $db->query($db->insert('table.access_log')->rows($row)); + } catch (Typecho_Db_Exception $e) { + if ($e->getCode() != 23000) + throw new Typecho_Plugin_Exception(_t('导入旧版数据失败,插件启用失败,错误信息:%s。', $e->getMessage())); + } + } + $db->query("DROP TABLE `{$prefix}access`;", Typecho_Db::WRITE); + $msg = _t('成功创建数据表并更新数据,插件启用成功,') . $configLink; + } + return $msg; } catch (Typecho_Db_Exception $e) { - $code = $e->getCode(); - if ($type != 'Mysql') { - throw new Typecho_Plugin_Exception('你的适配器为' . $type . ',目前只支持Mysql'); - } - if ($code == (1050 || '42S01')) { - $script = 'SELECT * from `' . $prefix . 'access`'; - $installDb->query($script, Typecho_Db::READ); - if (!array_key_exists('referer', $installDb->fetchRow($installDb->select()->from('table.access')))) { - $installDb->query('ALTER TABLE `' . $prefix . 'access` ADD `referer` varchar(255) NULL AFTER `ip`, ADD `referer_domain` varchar(100) NULL AFTER `referer`;'); - return '数据表结构已更新,插件启用成功,' . $configLink; - } - return '数据表已存在,插件启用成功,' . $configLink; - } else { - throw new Typecho_Plugin_Exception('数据表建立失败,插件启用失败。错误号:' . $code); - } + throw new Typecho_Plugin_Exception(_t('数据表建立失败,插件启用失败,错误信息:%s。', $e->getMessage())); } catch (Exception $e) { throw new Typecho_Plugin_Exception($e->getMessage()); } } + /** + * 获取后端统计,该统计方法可以统计到一切访问 + * + * @access public + * @return void + */ public static function backend($archive) { require_once __DIR__ . '/Access_Bootstrap.php'; $access = new Access_Core(); - $access->getReferer(); $config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); if ($config->writeType == 0) { @@ -132,11 +186,19 @@ class Access_Plugin implements Typecho_Plugin_Interface } } + /** + * 获取前端统计,该方法要求客户端必须渲染网页,所以不能统计RSS等直接抓取PHP页面的方式 + * + * @access public + * @return void + */ public static function frontend() { $config = Typecho_Widget::widget('Widget_Options')->plugin('Access'); if ($config->writeType == 1) { - echo ''; + $index = rtrim(Helper::options()->index, '/'); + echo '"; } } @@ -148,8 +210,8 @@ class Access_Plugin implements Typecho_Plugin_Interface echo ''; } diff --git a/page/console.php b/page/console.php index 8966fff..b6cf490 100644 --- a/page/console.php +++ b/page/console.php @@ -77,10 +77,10 @@ $access = new Access_Core(); "> - parser->getBrowser($log['ua']); ?> - + + - + @@ -184,7 +184,7 @@ $access = new Access_Core(); - + @@ -212,7 +212,7 @@ $access = new Access_Core(); - + @@ -416,4 +416,4 @@ $(document).ready(function() { \ No newline at end of file +?> diff --git a/sql/Mysql.sql b/sql/Mysql.sql new file mode 100644 index 0000000..5dd7e47 --- /dev/null +++ b/sql/Mysql.sql @@ -0,0 +1,38 @@ +CREATE TABLE `typecho_access_log` ( + `id` int(10) unsigned NOT NULL auto_increment, + `ua` varchar(255) default '' , + `browser_id` varchar(32) default '' , + `browser_version` varchar(32) default '' , + `os_id` varchar(32) default '' , + `os_version` varchar(32) default '' , + `url` varchar(255) default '' , + `path` varchar(255) default '' , + `query_string` varchar(255) default '' , + `ip` int(32) unsigned default '0' , + `entrypoint` varchar(255) default '' , + `entrypoint_domain` varchar(100) default '' , + `referer` varchar(255) default '' , + `referer_domain` varchar(100) default '' , + `time` int(32) unsigned default '0' , + `content_id` int(10) unsigned default NULL, + `meta_id` int(10) unsigned default NULL, + `robot` tinyint(1) default '0' , + `robot_id` varchar(32) default '' , + `robot_version` varchar(32) default '' , + PRIMARY KEY (`id`), + KEY `idx_time` (`time` ), + KEY `idx_path` (`path` ), + KEY `idx_ip_ua` (`ip`,`ua` ), + KEY `idx_robot` (`robot`, `time` ), + KEY `idx_os_id` (`os_id` ), + KEY `idx_robot_id` (`robot_id` ), + KEY `idx_browser_id` (`browser_id` ), + KEY `idx_content_id` (`content_id` ), + KEY `idx_meta_id` (`meta_id` ), + KEY `idx_entrypoint` (`entrypoint` ), + KEY `idx_entrypoint_domain` (`entrypoint_domain`), + KEY `idx_referer` (`referer` ), + KEY `idx_referer_domain` (`referer_domain` ), + CONSTRAINT `typecho_access_log_ibfk_cid` FOREIGN KEY (`content_id`) REFERENCES `typecho_contents` (`cid`) ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT `typecho_access_log_ibfk_mid` FOREIGN KEY (`meta_id` ) REFERENCES `typecho_metas` (`mid`) ON DELETE SET NULL ON UPDATE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=%charset%;