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