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 /** 137 * Print an introductionary screen 138 * 139 * @fixme the sql statements probably need to go into their own functions 140 * to be reused in the syntax plugins to follow 141 */ 142 function html_dashboard(){ 143 echo '<div class="plg_stats_dashboard">'; 144 145 146 // top pages today 147 echo '<div>'; 148 echo '<h2>Most popular pages</h2>'; 149 $sql = "SELECT page, COUNT(*) as cnt 150 FROM ".$this->getConf('db_prefix')."access as A 151 WHERE ".$this->tlimit." 152 AND ua_type = 'browser' 153 GROUP BY page 154 ORDER BY cnt DESC, page 155 LIMIT 20"; 156 $result = $this->runSQL($sql); 157 $this->html_resulttable($result,array('Pages','Count')); 158 echo '</div>'; 159 160 // top referer today 161 echo '<div>'; 162 echo '<h2>Top incoming links</h2>'; 163 $sql = "SELECT ref as url, COUNT(*) as cnt 164 FROM ".$this->getConf('db_prefix')."access as A 165 WHERE ".$this->tlimit." 166 AND ua_type = 'browser' 167 AND ref_type = 'external' 168 GROUP BY ref_md5 169 ORDER BY cnt DESC, url 170 LIMIT 20"; 171 $result = $this->runSQL($sql); 172 $this->html_resulttable($result,array('Incoming Links','Count')); 173 echo '</div>'; 174 175 // top countries today 176 echo '<div>'; 177 echo '<h2>Visitor\'s top countries</h2>'; 178 $sql = "SELECT B.code AS cflag, B.country, COUNT(*) as cnt 179 FROM ".$this->getConf('db_prefix')."access as A, 180 ".$this->getConf('db_prefix')."iplocation as B 181 WHERE ".$this->tlimit." 182 AND A.ip = B.ip 183 GROUP BY B.country 184 ORDER BY cnt DESC, B.country 185 LIMIT 20"; 186 $result = $this->runSQL($sql); 187 $this->html_resulttable($result,array('','Countries','Count')); 188 echo '</div>'; 189 190 echo '</div>'; 191 } 192 193 /** 194 * Display a result in a HTML table 195 */ 196 function html_resulttable($result,$header){ 197 echo '<table>'; 198 echo '<tr>'; 199 foreach($header as $h){ 200 echo '<th>'.hsc($h).'</th>'; 201 } 202 echo '</tr>'; 203 204 foreach($result as $row){ 205 echo '<tr>'; 206 foreach($row as $k => $v){ 207 echo '<td class="stats_'.$k.'">'; 208 if($k == 'page'){ 209 echo '<a href="'.wl($v).'" class="wikilink1">'; 210 echo hsc($v); 211 echo '</a>'; 212 }elseif($k == 'url'){ 213 $url = hsc($v); 214 if(strlen($url) > 50){ 215 $url = substr($url,0,30).' … '.substr($url,-20); 216 } 217 echo '<a href="'.$v.'" class="urlextern">'; 218 echo $url; 219 echo '</a>'; 220 }elseif($k == 'html'){ 221 echo $v; 222 }elseif($k == 'cflag'){ 223 echo '<img src="'.DOKU_BASE.'lib/plugin/statistics/flags/'.hsc($v).'.png" alt="'.hsc($v).'" width="18" height="12"/>'; 224 }else{ 225 echo hsc($v); 226 } 227 echo '</td>'; 228 } 229 echo '</tr>'; 230 } 231 echo '</table>'; 232 } 233 234 235 /** 236 * Return a link to the DB, opening the connection if needed 237 */ 238 function dbLink(){ 239 // connect to DB if needed 240 if(!$this->dblink){ 241 $this->dblink = mysql_connect($this->getConf('db_server'), 242 $this->getConf('db_user'), 243 $this->getConf('db_password')); 244 if(!$this->dblink){ 245 msg('DB Error: connection failed',-1); 246 return null; 247 } 248 // set utf-8 249 if(!mysql_db_query($this->getConf('db_database'),'set names utf8',$this->dblink)){ 250 msg('DB Error: could not set UTF-8 ('.mysql_error($this->dblink).')',-1); 251 return null; 252 } 253 } 254 return $this->dblink; 255 } 256 257 /** 258 * Simple function to run a DB query 259 */ 260 function runSQL($sql_string) { 261 $link = $this->dbLink(); 262 263 $result = mysql_db_query($this->conf['db_database'],$sql_string,$link); 264 if(!$result){ 265 msg('DB Error: '.mysql_error($link),-1); 266 return null; 267 } 268 269 $resultarray = array(); 270 271 //mysql_db_query returns 1 on a insert statement -> no need to ask for results 272 if ($result != 1) { 273 for($i=0; $i< mysql_num_rows($result); $i++) { 274 $temparray = mysql_fetch_assoc($result); 275 $resultarray[]=$temparray; 276 } 277 mysql_free_result($result); 278 } 279 280 if (mysql_insert_id($link)) { 281 $resultarray = mysql_insert_id($link); //give back ID on insert 282 } 283 284 return $resultarray; 285 } 286 287 /** 288 * Returns a short name for a User Agent and sets type, version and os info 289 */ 290 function ua_info($ua,&$type,&$ver,&$os){ 291 $ua = strtr($ua,' +','__'); 292 $ua = strtolower($ua); 293 294 // common browsers 295 $regvermsie = '/msie([+_ ]|)([\d\.]*)/i'; 296 $regvernetscape = '/netscape.?\/([\d\.]*)/i'; 297 $regverfirefox = '/firefox\/([\d\.]*)/i'; 298 $regversvn = '/svn\/([\d\.]*)/i'; 299 $regvermozilla = '/mozilla(\/|)([\d\.]*)/i'; 300 $regnotie = '/webtv|omniweb|opera/i'; 301 $regnotnetscape = '/gecko|compatible|opera|galeon|safari/i'; 302 303 $name = ''; 304 # IE ? 305 if(preg_match($regvermsie,$ua,$m) && !preg_match($regnotie,$ua)){ 306 $type = 'browser'; 307 $ver = $m[2]; 308 $name = 'msie'; 309 } 310 # Firefox ? 311 elseif (preg_match($regverfirefox,$ua,$m)){ 312 $type = 'browser'; 313 $ver = $m[1]; 314 $name = 'firefox'; 315 } 316 # Subversion ? 317 elseif (preg_match($regversvn,$ua,$m)){ 318 $type = 'rcs'; 319 $ver = $m[1]; 320 $name = 'svn'; 321 } 322 # Netscape 6.x, 7.x ... ? 323 elseif (preg_match($regvernetscape,$ua,$m)){ 324 $type = 'browser'; 325 $ver = $m[1]; 326 $name = 'netscape'; 327 } 328 # Netscape 3.x, 4.x ... ? 329 elseif(preg_match($regvermozilla,$ua,$m) && !preg_match($regnotnetscape,$ua)){ 330 $type = 'browser'; 331 $ver = $m[2]; 332 $name = 'netscape'; 333 }else{ 334 include(dirname(__FILE__).'/inc/browsers.php'); 335 foreach($BrowsersSearchIDOrder as $regex){ 336 if(preg_match('/'.$regex.'/',$ua)){ 337 // it's a browser! 338 $type = 'browser'; 339 $name = strtolower($regex); 340 break; 341 } 342 } 343 } 344 345 // check OS for browsers 346 if($type == 'browser'){ 347 include(dirname(__FILE__).'/inc/operating_systems.php'); 348 foreach($OSSearchIDOrder as $regex){ 349 if(preg_match('/'.$regex.'/',$ua)){ 350 $os = $OSHashID[$regex]; 351 break; 352 } 353 } 354 355 } 356 357 // are we done now? 358 if($name) return $name; 359 360 include(dirname(__FILE__).'/inc/robots.php'); 361 foreach($RobotsSearchIDOrder as $regex){ 362 if(preg_match('/'.$regex.'/',$ua)){ 363 // it's a robot! 364 $type = 'robot'; 365 return strtolower($regex); 366 } 367 } 368 369 // dunno 370 return ''; 371 } 372 373 /** 374 * 375 * @fixme: put search engine queries in seperate table here 376 */ 377 function log_search($referer,&$type){ 378 $referer = strtr($referer,' +','__'); 379 $referer = strtolower($referer); 380 381 include(dirname(__FILE__).'/inc/search_engines.php'); 382 383 foreach($SearchEnginesSearchIDOrder as $regex){ 384 if(preg_match('/'.$regex.'/',$referer)){ 385 if(!$NotSearchEnginesKeys[$regex] || 386 !preg_match('/'.$NotSearchEnginesKeys[$regex].'/',$referer)){ 387 // it's a search engine! 388 $type = 'search'; 389 break; 390 } 391 } 392 } 393 if($type != 'search') return; // we're done here 394 395 #fixme now do the keyword magic! 396 } 397 398 /** 399 * Resolve IP to country/city 400 */ 401 function log_ip($ip){ 402 // check if IP already known and up-to-date 403 $sql = "SELECT ip 404 FROM ".$this->getConf('db_prefix')."iplocation 405 WHERE ip ='".addslashes($ip)."' 406 AND lastupd > DATE_SUB(CURDATE(),INTERVAL 30 DAY)"; 407 $result = $this->runSQL($sql); 408 if($result[0]['ip']) return; 409 410 $http = new DokuHTTPClient(); 411 $http->timeout = 10; 412 $data = $http->get('http://api.hostip.info/get_html.php?ip='.$ip); 413 414 if(preg_match('/^Country: (.*?) \((.*?)\)\nCity: (.*?)$/s',$data,$match)){ 415 $country = addslashes(trim($match[1])); 416 $code = addslashes(strtolower(trim($match[2]))); 417 $city = addslashes(trim($match[3])); 418 $host = addslashes(gethostbyaddr($ip)); 419 $ip = addslashes($ip); 420 421 $sql = "REPLACE INTO ".$this->getConf('db_prefix')."iplocation 422 SET ip = '$ip', 423 country = '$country', 424 code = '$code', 425 city = '$city', 426 host = '$host'"; 427 $this->runSQL($sql); 428 } 429 } 430 431 /** 432 * log a page access 433 * 434 * called from log.php 435 */ 436 function log_access(){ 437 if(!$_REQUEST['p']) return; 438 439 # FIXME check referer against blacklist and drop logging for bad boys 440 441 // handle referer 442 $referer = trim($_REQUEST['r']); 443 if($referer){ 444 $ref = addslashes($referer); 445 $ref_md5 = ($ref) ? md5($referer) : ''; 446 if(strpos($referer,DOKU_URL) === 0){ 447 $ref_type = 'internal'; 448 }else{ 449 $ref_type = 'external'; 450 $this->log_search($referer,$ref_type); 451 } 452 }else{ 453 $ref = ''; 454 $ref_md5 = ''; 455 $ref_type = ''; 456 } 457 458 // handle user agent 459 $agent = trim($_SERVER['HTTP_USER_AGENT']); 460 461 $ua = addslashes($agent); 462 $ua_type = ''; 463 $ua_ver = ''; 464 $os = ''; 465 $ua_info = addslashes($this->ua_info($agent,$ua_type,$ua_ver,$os)); 466 467 $page = addslashes($_REQUEST['p']); 468 $ip = addslashes($_SERVER['REMOTE_ADDR']); 469 $sx = (int) $_REQUEST['sx']; 470 $sy = (int) $_REQUEST['sy']; 471 $vx = (int) $_REQUEST['vx']; 472 $vy = (int) $_REQUEST['vy']; 473 $user = addslashes($_SERVER['REMOTE_USER']); 474 $session = addslashes(session_id()); 475 476 $sql = "INSERT DELAYED INTO ".$this->getConf('db_prefix')."access 477 SET page = '$page', 478 ip = '$ip', 479 ua = '$ua', 480 ua_info = '$ua_info', 481 ua_type = '$ua_type', 482 ua_ver = '$ua_ver', 483 os = '$os', 484 ref = '$ref', 485 ref_md5 = '$ref_md5', 486 ref_type = '$ref_type', 487 screen_x = '$sx', 488 screen_y = '$sy', 489 view_x = '$vx', 490 view_y = '$vy', 491 user = '$user', 492 session = '$session'"; 493 $ok = $this->runSQL($sql); 494 if(is_null($ok)){ 495 global $MSG; 496 print_r($MSG); 497 } 498 499 // resolve the IP 500 $this->log_ip($_SERVER['REMOTE_ADDR']); 501 } 502 503 /** 504 * Just send a 1x1 pixel blank gif to the browser 505 * 506 * @called from log.php 507 * 508 * @author Andreas Gohr <andi@splitbrain.org> 509 * @author Harry Fuecks <fuecks@gmail.com> 510 */ 511 function sendGIF(){ 512 $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7'); 513 header('Content-Type: image/gif'); 514 header('Content-Length: '.strlen($img)); 515 header('Connection: Close'); 516 print $img; 517 flush(); 518 // Browser should drop connection after this 519 // Thinks it's got the whole image 520 } 521 522} 523