Merge pull request #11 from tinymins/master

修改数据库架构 加快查询与插入速度
This commit is contained in:
やかみそら 2017-07-13 21:37:10 +08:00 committed by GitHub
commit 06cd5fd799
6 changed files with 696 additions and 367 deletions

View File

@ -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) {}
}

View File

@ -1,163 +0,0 @@
<?php
if (!defined('__ACCESS_PLUGIN_ROOT__')) {
throw new Exception('Boostrap file not found');
}
class Access_Parser
{
public $bots = array(
'TencentTraveler',
'Baiduspider',
'BaiduGame',
'Googlebot',
'msnbot',
'Sosospider+',
'Sogou web spider',
'ia_archiver',
'Yahoo! Slurp',
'YoudaoBot',
'Yahoo Slurp',
'MSNBot',
'Java (Often spam bot)',
'BaiDuSpider',
'Voila',
'Yandex bot',
'BSpider',
'twiceler',
'Sogou Spider',
'Speedy Spider',
'Google AdSense',
'Heritrix',
'Python-urllib',
'Alexa (IA Archiver)',
'Ask',
'Exabot',
'Custo',
'OutfoxBot/YodaoBot',
'yacy',
'SurveyBot',
'legs',
'lwp-trivial',
'Nutch',
'StackRambler',
'The web archive (IA Archiver)',
'Perl tool',
'MJ12bot',
'Netcraft',
'MSIECrawler',
'WGet tools',
'larbin',
'Fish search',
'crawler',
'bingbot',
'YisouSpider',
'Bot',
'Spider',
);
protected $currentBot = null;
public function getBrowser($ua)
{
$os = null;
if ($this->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);
}
}

324
Access_UA.php Normal file
View File

@ -0,0 +1,324 @@
<?php
if (!defined('__ACCESS_PLUGIN_ROOT__')) {
throw new Exception('Boostrap file not found');
}
class Access_UA
{
private static $robots = array(
'TencentTraveler',
'Baiduspider',
'BaiduGame',
'Googlebot',
'msnbot',
'Sosospider+',
'Sogou web spider',
'ia_archiver',
'Yahoo! Slurp',
'YoudaoBot',
'Yahoo Slurp',
'MSNBot',
'Java (Often spam bot)',
'BaiDuSpider',
'Voila',
'Yandex bot',
'BSpider',
'twiceler',
'Sogou Spider',
'Speedy Spider',
'Google AdSense',
'Heritrix',
'Python-urllib',
'Alexa (IA Archiver)',
'Ask',
'Exabot',
'Custo',
'OutfoxBot/YodaoBot',
'yacy',
'SurveyBot',
'legs',
'lwp-trivial',
'Nutch',
'StackRambler',
'The web archive (IA Archiver)',
'Perl tool',
'MJ12bot',
'Netcraft',
'MSIECrawler',
'WGet tools',
'larbin',
'Fish search',
'crawler',
'bingbot',
'YisouSpider',
);
private $ua;
private $ual;
private $osID = null;
private $osName = null;
private $osVersion = null;
private $robotID = null;
private $robotName = null;
private $robotVersion = null;
private $browserID = null;
private $browserName = null;
private $browserVersion = null;
function __construct($ua) {
$this->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 : '';
}
}

View File

@ -1,19 +1,28 @@
<?php
/**
* 获取访客信息
* 获取访客信息,生成统计图表,由<a href="https://zhaiyiming.com/">@一名宅</a> 部分优化重构。
*
* @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 = '<a href="' . Helper::options()->adminUrl . 'options-plugin.php?config=Access' . '">请设置</a>';
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 = '<a href="' . Helper::options()->adminUrl . 'options-plugin.php?config=Access">' . _t('前往设置') . '</a>';
# 初始化数据库如果不存在
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 '<script type="text/javascript">(function(){var xhr=new XMLHttpRequest();xhr.open("GET","' . rtrim(Helper::options()->index, '/') . '/access/log/write.json?u="+location.pathname+location.search+location.hash' . ',true);xhr.send();})();</script>';
$index = rtrim(Helper::options()->index, '/');
echo '<script type="text/javascript">(function(){var xhr=new XMLHttpRequest();xhr.open("GET","' .
"{$index}/access/log/write.json?u=\"+location.pathname+location.search+location.hash,true);xhr.send();})();</script>";
}
}
@ -148,8 +210,8 @@ class Access_Plugin implements Typecho_Plugin_Interface
echo '<script>
$(document).ready(function() {
$("#start-link").append("<li><a href=\"';
Helper::options()->adminUrl('extending.php?panel=' . Access_Plugin::$panel);
echo '\">Access控制台</a></li>");
Helper::options()->adminUrl('extending.php?panel=' . self::$panel);
echo '\">'. _t('Access控制台') . '</a></li>");
});
</script>';
}

View File

@ -77,10 +77,10 @@ $access = new Access_Core();
<tr id="<?php echo $log['id']; ?>" data-id="<?php echo $log['id']; ?>">
<td><input type="checkbox" data-id="<?php echo $log['id']; ?>" value="<?php echo $log['id']; ?>" name="id[]"/></td>
<td><a target="_blank" href="<?php echo str_replace("%23", "#", $log['url']); ?>"><?php echo urldecode(str_replace("%23", "#", $log['url'])); ?></a></td>
<td><a data-action="ua" href="#" title="<?php echo $log['ua'];?>"><?php echo $access->parser->getBrowser($log['ua']); ?></a></td>
<td><a data-action="ip" data-ip="<?php echo $log['ip']; ?>" href="#"><?php echo $log['ip']; ?></a></td>
<td><a data-action="ua" href="#" title="<?php echo $log['ua'];?>"><?php echo $log['display_name']; ?></a></td>
<td><a data-action="ip" data-ip="<?php echo long2ip($log['ip']); ?>" href="#"><?php echo long2ip($log['ip']); ?></a></td>
<td><a target="_blank" data-action="referer" href="<?php echo $log['referer']; ?>"><?php echo $log['referer']; ?></a></td>
<td><?php echo date('Y-m-d H:i:s',$log['date']); ?></td>
<td><?php echo date('Y-m-d H:i:s',$log['time']); ?></td>
</tr>
<?php endforeach; ?>
<?php else: ?>
@ -184,7 +184,7 @@ $access = new Access_Core();
<tr>
<td><?php echo $key +1 ?></td>
<td><?php echo $value['count']?></td>
<td><?php echo $value['referer_domain']?></td>
<td><?php echo $value['value']?></td>
</tr>
<?php endforeach;?>
</tbody>
@ -212,7 +212,7 @@ $access = new Access_Core();
<tr>
<td><?php echo $key +1 ?></td>
<td><?php echo $value['count']?></td>
<td><?php echo $value['referer']?></td>
<td><?php echo $value['value']?></td>
</tr>
<?php endforeach;?>
</tbody>
@ -416,4 +416,4 @@ $(document).ready(function() {
<?php endif;?>
<?php
include 'footer.php';
?>
?>

38
sql/Mysql.sql Normal file
View File

@ -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%;