1<?php 2/** 3 * statistics plugin 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8 9// must be run within Dokuwiki 10if(!defined('DOKU_INC')) die(); 11 12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 13require_once(DOKU_PLUGIN.'admin.php'); 14 15/** 16 * All DokuWiki plugins to extend the admin function 17 * need to inherit from this class 18 */ 19class admin_plugin_statistics extends DokuWiki_Admin_Plugin { 20 var $dblink = null; 21 22 /** 23 * return some info 24 */ 25 function getInfo(){ 26 return confToHash(dirname(__FILE__).'/info.txt'); 27 } 28 29 /** 30 * Access for managers allowed 31 */ 32 function forAdminOnly(){ 33 return false; 34 } 35 36 /** 37 * return sort order for position in admin menu 38 */ 39 function getMenuSort() { 40 return 150; 41 } 42 43 /** 44 * handle user request 45 */ 46 function handle() { 47 } 48 49 /** 50 * fixme build statistics here 51 */ 52 function html() { 53 // fixme build a navigation menu in a TOC here 54 55 switch($_REQUEST['opt']){ 56 57 default: 58 echo $this->locale_xhtml('intro'); 59 echo $this->html_dashboard(); 60 } 61 } 62 63 function html_timeselect(){ 64 echo '<form>'; 65 66 echo '</form>'; 67 } 68 69 70 function html_dashboard(){ 71 72 // top pages today 73 $sql = "SELECT page, COUNT(*) as cnt 74 FROM ".$this->getConf('db_prefix')."access 75 WHERE DATE(dt) = CURDATE() 76 AND ua_type = 'browser' 77 GROUP BY page 78 ORDER BY cnt DESC, page 79 LIMIT 20"; 80 $result = $this->runSQL($sql); 81 $this->html_resulttable($result,array('Pages','Count')); 82 83 // top referer today 84 $sql = "SELECT ref as url, COUNT(*) as cnt 85 FROM ".$this->getConf('db_prefix')."access 86 WHERE DATE(dt) = CURDATE() 87 AND ua_type = 'browser' 88 AND ref_type = 'external' 89 GROUP BY ref_md5 90 ORDER BY cnt DESC, url 91 LIMIT 20"; 92 $result = $this->runSQL($sql); 93 $this->html_resulttable($result,array('Incoming Links','Count')); 94 95 // top countries today 96 $sql = "SELECT B.country, COUNT(*) as cnt 97 FROM ".$this->getConf('db_prefix')."access as A, 98 ".$this->getConf('db_prefix')."iplocation as B 99 WHERE DATE(A.dt) = CURDATE() 100 AND A.ip = B.ip 101 GROUP BY B.country 102 ORDER BY cnt DESC, B.country 103 LIMIT 20"; 104 $result = $this->runSQL($sql); 105 $this->html_resulttable($result,array('Countries','Count')); 106 } 107 108 /** 109 * Display a result in a HTML table 110 */ 111 function html_resulttable($result,$header){ 112 echo '<table>'; 113 echo '<tr>'; 114 foreach($header as $h){ 115 echo '<th>'.hsc($h).'</th>'; 116 } 117 echo '</tr>'; 118 119 foreach($result as $row){ 120 echo '<tr>'; 121 foreach($row as $k => $v){ 122 echo '<td class="stats_'.$k.'">'; 123 if($k == 'page'){ 124 echo '<a href="'.wl($v).'" class="wikilink1">'; 125 echo hsc($v); 126 echo '</a>'; 127 }elseif($k == 'url'){ 128 $url = hsc($v); 129 if(strlen($url) > 50){ 130 $url = substr($url,0,30).' … '.substr($url,-20); 131 } 132 echo '<a href="'.$v.'" class="urlextern">'; 133 echo $url; 134 echo '</a>'; 135 }elseif($k == 'html'){ 136 echo $v; 137 }else{ 138 echo hsc($v); 139 } 140 echo '</td>'; 141 } 142 echo '</tr>'; 143 } 144 echo '</table>'; 145 } 146 147 148 /** 149 * Return a link to the DB, opening the connection if needed 150 */ 151 function dbLink(){ 152 // connect to DB if needed 153 if(!$this->dblink){ 154 $this->dblink = mysql_connect($this->getConf('db_server'), 155 $this->getConf('db_user'), 156 $this->getConf('db_password')); 157 if(!$this->dblink){ 158 msg('DB Error: connection failed',-1); 159 return null; 160 } 161 // set utf-8 162 if(!mysql_db_query($this->getConf('db_database'),'set names utf8',$this->dblink)){ 163 msg('DB Error: could not set UTF-8 ('.mysql_error($this->dblink).')',-1); 164 return null; 165 } 166 } 167 return $this->dblink; 168 } 169 170 /** 171 * Simple function to run a DB query 172 */ 173 function runSQL($sql_string) { 174 $link = $this->dbLink(); 175 176 $result = mysql_db_query($this->conf['db_database'],$sql_string,$link); 177 if(!$result){ 178 msg('DB Error: '.mysql_error($link),-1); 179 return null; 180 } 181 182 $resultarray = array(); 183 184 //mysql_db_query returns 1 on a insert statement -> no need to ask for results 185 if ($result != 1) { 186 for($i=0; $i< mysql_num_rows($result); $i++) { 187 $temparray = mysql_fetch_assoc($result); 188 $resultarray[]=$temparray; 189 } 190 mysql_free_result($result); 191 } 192 193 if (mysql_insert_id($link)) { 194 $resultarray = mysql_insert_id($link); //give back ID on insert 195 } 196 197 return $resultarray; 198 } 199 200 /** 201 * Returns a short name for a User Agent and sets type, version and os info 202 */ 203 function ua_info($ua,&$type,&$ver,&$os){ 204 $ua = strtr($ua,' +','__'); 205 $ua = strtolower($ua); 206 207 // common browsers 208 $regvermsie = '/msie([+_ ]|)([\d\.]*)/i'; 209 $regvernetscape = '/netscape.?\/([\d\.]*)/i'; 210 $regverfirefox = '/firefox\/([\d\.]*)/i'; 211 $regversvn = '/svn\/([\d\.]*)/i'; 212 $regvermozilla = '/mozilla(\/|)([\d\.]*)/i'; 213 $regnotie = '/webtv|omniweb|opera/i'; 214 $regnotnetscape = '/gecko|compatible|opera|galeon|safari/i'; 215 216 $name = ''; 217 # IE ? 218 if(preg_match($regvermsie,$ua,$m) && !preg_match($regnotie,$ua)){ 219 $type = 'browser'; 220 $ver = $m[2]; 221 $name = 'msie'; 222 } 223 # Firefox ? 224 elseif (preg_match($regverfirefox,$ua,$m)){ 225 $type = 'browser'; 226 $ver = $m[1]; 227 $name = 'firefox'; 228 } 229 # Subversion ? 230 elseif (preg_match($regversvn,$ua,$m)){ 231 $type = 'rcs'; 232 $ver = $m[1]; 233 $name = 'svn'; 234 } 235 # Netscape 6.x, 7.x ... ? 236 elseif (preg_match($regvernetscape,$ua,$m)){ 237 $type = 'browser'; 238 $ver = $m[1]; 239 $name = 'netscape'; 240 } 241 # Netscape 3.x, 4.x ... ? 242 elseif(preg_match($regvermozilla,$ua,$m) && !preg_match($regnotnetscape,$ua)){ 243 $type = 'browser'; 244 $ver = $m[2]; 245 $name = 'netscape'; 246 }else{ 247 include(dirname(__FILE__).'/inc/browsers.php'); 248 foreach($BrowsersSearchIDOrder as $regex){ 249 if(preg_match('/'.$regex.'/',$ua)){ 250 // it's a browser! 251 $type = 'browser'; 252 $name = strtolower($regex); 253 break; 254 } 255 } 256 } 257 258 // check OS for browsers 259 if($type == 'browser'){ 260 include(dirname(__FILE__).'/inc/operating_systems.php'); 261 foreach($OSSearchIDOrder as $regex){ 262 if(preg_match('/'.$regex.'/',$ua)){ 263 $os = $OSHashID[$regex]; 264 break; 265 } 266 } 267 268 } 269 270 // are we done now? 271 if($name) return $name; 272 273 include(dirname(__FILE__).'/inc/robots.php'); 274 foreach($RobotsSearchIDOrder as $regex){ 275 if(preg_match('/'.$regex.'/',$ua)){ 276 // it's a robot! 277 $type = 'robot'; 278 return strtolower($regex); 279 } 280 } 281 282 // dunno 283 return ''; 284 } 285 286 /** 287 * 288 * @fixme: put search engine queries in seperate table here 289 */ 290 function log_search($referer,&$type){ 291 $referer = strtr($referer,' +','__'); 292 $referer = strtolower($referer); 293 294 include(dirname(__FILE__).'/inc/search_engines.php'); 295 296 foreach($SearchEnginesSearchIDOrder as $regex){ 297 if(preg_match('/'.$regex.'/',$referer)){ 298 if(!$NotSearchEnginesKeys[$regex] || 299 !preg_match('/'.$NotSearchEnginesKeys[$regex].'/',$referer)){ 300 // it's a search engine! 301 $type = 'search'; 302 break; 303 } 304 } 305 } 306 if($type != 'search') return; // we're done here 307 308 #fixme now do the keyword magic! 309 } 310 311 /** 312 * Resolve IP to country/city 313 */ 314 function log_ip($ip){ 315 // check if IP already known and up-to-date 316 $sql = "SELECT ip 317 FROM ".$this->getConf('db_prefix')."iplocation 318 WHERE ip ='".addslashes($ip)."' 319 AND lastupd > DATE_SUB(CURDATE(),INTERVAL 30 DAY)"; 320 $result = $this->runSQL($sql); 321 if($result[0]['ip']) return; 322 323 $http = new DokuHTTPClient(); 324 $http->timeout = 10; 325 $data = $http->get('http://api.hostip.info/get_html.php?ip='.$ip); 326 327 if(preg_match('/^Country: (.*?) \((.*?)\)\nCity: (.*?)$/s',$data,$match)){ 328 $country = addslashes(trim($match[1])); 329 $code = addslashes(strtolower(trim($match[2]))); 330 $city = addslashes(trim($match[3])); 331 $host = addslashes(gethostbyaddr($ip)); 332 $ip = addslashes($ip); 333 334 $sql = "REPLACE INTO ".$this->getConf('db_prefix')."iplocation 335 SET ip = '$ip', 336 country = '$country', 337 code = '$code', 338 city = '$city', 339 host = '$host'"; 340 $this->runSQL($sql); 341 } 342 } 343 344 /** 345 * log a page access 346 * 347 * called from log.php 348 */ 349 function log_access(){ 350 if(!$_REQUEST['p']) return; 351 352 # FIXME check referer against blacklist and drop logging for bad boys 353 354 // handle referer 355 $referer = trim($_REQUEST['r']); 356 if($referer){ 357 $ref = addslashes($referer); 358 $ref_md5 = ($ref) ? md5($referer) : ''; 359 if(strpos($referer,DOKU_URL) === 0){ 360 $ref_type = 'internal'; 361 }else{ 362 $ref_type = 'external'; 363 $this->log_search($referer,$ref_type); 364 } 365 }else{ 366 $ref = ''; 367 $ref_md5 = ''; 368 $ref_type = ''; 369 } 370 371 // handle user agent 372 $agent = trim($_SERVER['HTTP_USER_AGENT']); 373 374 $ua = addslashes($agent); 375 $ua_type = ''; 376 $ua_ver = ''; 377 $os = ''; 378 $ua_info = addslashes($this->ua_info($agent,$ua_type,$ua_ver,$os)); 379 380 $page = addslashes($_REQUEST['p']); 381 $ip = addslashes($_SERVER['REMOTE_ADDR']); 382 $sx = (int) $_REQUEST['sx']; 383 $sy = (int) $_REQUEST['sy']; 384 $vx = (int) $_REQUEST['vx']; 385 $vy = (int) $_REQUEST['vy']; 386 $user = addslashes($_SERVER['REMOTE_USER']); 387 $session = addslashes(session_id()); 388 389 $sql = "INSERT DELAYED INTO ".$this->getConf('db_prefix')."access 390 SET page = '$page', 391 ip = '$ip', 392 ua = '$ua', 393 ua_info = '$ua_info', 394 ua_type = '$ua_type', 395 ua_ver = '$ua_ver', 396 os = '$os', 397 ref = '$ref', 398 ref_md5 = '$ref_md5', 399 ref_type = '$ref_type', 400 screen_x = '$sx', 401 screen_y = '$sy', 402 view_x = '$vx', 403 view_y = '$vy', 404 user = '$user', 405 session = '$session'"; 406 $ok = $this->runSQL($sql); 407 if(is_null($ok)){ 408 global $MSG; 409 print_r($MSG); 410 } 411 412 // resolve the IP 413 $this->log_ip($_SERVER['REMOTE_ADDR']); 414 } 415 416 /** 417 * Just send a 1x1 pixel blank gif to the browser 418 * 419 * @called from log.php 420 * 421 * @author Andreas Gohr <andi@splitbrain.org> 422 * @author Harry Fuecks <fuecks@gmail.com> 423 */ 424 function sendGIF(){ 425 $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'); 426 header('Content-Type: image/gif'); 427 header('Content-Length: '.strlen($img)); 428 header('Connection: Close'); 429 print $img; 430 flush(); 431 // Browser should drop connection after this 432 // Thinks it's got the whole image 433 } 434 435} 436