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