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