1<?php 2/** 3 * 4 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 5 * @author Myron Turner <turnermm02@shaw.ca> 6 */ 7 8if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); 9if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 10require_once(DOKU_PLUGIN.'syntax.php'); 11if(!defined('QUICK_STATS')) define ('QUICK_STATS',DOKU_PLUGIN . 'quickstats/'); 12 13//require_once('GEOIP/ccArraysDat.php'); 14//error_reporting(E_ALL); 15//ini_set('display_errors','1'); 16/** 17 * All DokuWiki plugins to extend the parser/rendering mechanism 18 * need to inherit from this class 19 */ 20class syntax_plugin_quickstats extends DokuWiki_Syntax_Plugin { 21 22 private $page_file; 23 private $ip_file; 24 private $misc_data_file; 25 private $pages; 26 private $ips; 27 private $misc_data; 28 private $cc_arrays; 29 private $long_names =-1; 30 private $show_date; 31 private $ua_file; 32 private $ua_data; 33 private $giCity; 34 private $SEP = '/'; 35 private $helper; 36 37 function __construct() { 38 39 $this->long_names = $this->getConf('long_names'); 40 if(!isset($this->long_names) || $this->long_names <= 0) $this->long_names = false; 41 $this->show_date=$this->getConf('show_date'); 42 if( preg_match('/WINNT/i', PHP_OS) ) { 43 $this->SEP='\\'; 44 } 45 $this->helper = & plugin_load('helper', 'quickstats'); 46 $this->cc_arrays = $this->helper->get_cc_arrays(); 47 } 48 49 /** 50 * Get an associative array with plugin info. 51 */ 52 function getInfo(){ 53 $pname = $this->getPluginName(); 54 $info = DOKU_PLUGIN.'/'.$pname.'/plugin.info.txt'; 55 56 if(@file_exists($info)) { 57 return parent::getInfo(); 58 } 59 60 return array( 61 'author' => 'Myron Turner', 62 'email' => 'turnermm02@shaw.ca', 63 'date' => '2011-11-02', 64 'name' => 'Quickstats Plugin', 65 'desc' => 'Output browser/user stats to wiki page', 66 'url' => 'http://www.dokuwiki.org/plugin:quickstats', 67 ); 68 } 69 70 /** 71 * Get the type of syntax this plugin defines. 72 */ 73 function getType(){ 74 return 'substition'; 75 } 76 77 /** 78 * What kind of syntax do we allow (optional) 79 */ 80// function getAllowedTypes() { 81// return array(); 82// } 83 84 /** 85 * Define how this plugin is handled regarding paragraphs. 86 * 87 * normal: The plugin can be used inside paragraphs. 88 * block: Open paragraphs need to be closed before plugin output. 89 * stack (Special case): Plugin wraps other paragraphs. 90 */ 91 function getPType(){ 92 return 'block'; 93 } 94 95 /** 96 * Where to sort in? 97 * 98 */ 99 function getSort(){ 100 return 100; 101 } 102 103 104 /** 105 * Connect lookup pattern to lexer. 106 * 107 * @param $aMode String The desired rendermode. 108 * @return none 109 * @public 110 * @see render() 111 */ 112 function connectTo($mode) { 113 $this->Lexer->addSpecialPattern('~~QUICKSTATS:.*?~~',$mode,'plugin_quickstats'); 114 } 115 116// function postConnect() { 117// $this->Lexer->addExitPattern('</TEST>','plugin_quickstats'); 118// } 119 120 121 /** 122 * Handler to prepare matched data for the rendering process. 123 * 124 */ 125 function handle($match, $state, $pos, Doku_Handler $handler){ 126 global $ID; 127 $this->helper->writeCache($ID); 128 switch ($state) { 129 case DOKU_LEXER_SPECIAL : 130 $match = trim(substr($match,13,-2)); 131 if($match) { 132 if($match == "total") { 133 return array('total',"",""); 134 } 135 $depth = false; 136 if(strpos($match,';;') !== false) { 137 list($match,$depth) = explode(';;',$match); 138 } 139 140 $date = ""; 141 if(strpos($match,'&') !== false) { 142 143 /* 144 catch syntax errors 145 assumes single parameter with trailing or prepended & 146 */ 147 if($match[strlen($match)-1] == '&' || $match[0] == '&') { 148 $match = trim($match,'&'); 149 if($this->is_date_string($match)) { 150 return array('basics',$match,$depth); 151 } 152 return array('basics',"",$depth); 153 } 154 155 /* process valid paramter string */ 156 list($m1,$m2) = explode('&',$match,2); 157 if($this->is_date_string($m1)) { 158 $date=$m1; 159 $match = $m2; 160 } 161 else { 162 $date=$m2; 163 $match = $m1; 164 } 165 } 166 else if($this->is_date_string($match)) { 167 $date = $match; 168 $match = 'basics'; 169 } 170 } 171 else { 172 return array('basics',"",$depth); 173 } 174 175 return array(strtolower($match),$date,$depth); 176 break; 177 } 178 return array(); 179 } 180 181 function is_date_string($str) { 182 return preg_match('/\d+_\d\d\d/',$str); 183 } 184 185 /** 186 * Handle the actual output creation. 187 */ 188 function render($mode, Doku_Renderer $renderer, $data) { 189 190 if($mode == 'xhtml'){ 191 192 list($which, $date_str,$depth) = $data; 193 if($which == 'total') { 194 $renderer->doc .= "<span class = 'quick-total'>" . $this->getTotal() ."</span>"; 195 return true; 196 } 197 $this->row_depth('all'); 198 if($depth) { 199 $this->row_depth($depth); 200 } 201 202 $this->load_data($date_str,$which); 203 if($which == 'basics') { 204 $renderer->doc .= "<div class='quickstats basics' style='margin: auto;width: 920px;'>" ; 205 } 206 else { 207 $class = "quickstats $which"; 208 $renderer->doc .= "<div class='$class'>"; 209 } 210 switch ($which) { 211 case 'basics': 212 $this->misc_data_xhtml($renderer); 213 $this->pages_xhtml($renderer); 214 break; 215 case 'ip': 216 $this->ip_xhtml($renderer); 217 break; 218 case 'pages': 219 $this->pages_xhtml($renderer,true); 220 break; 221 case 'misc': 222 $this->misc_data_xhtml($renderer,true,'misc'); 223 break; 224 case 'countries': 225 $this->misc_data_xhtml($renderer,true,'country'); 226 break; 227 case 'ua': 228 $this->ua_xhtml($renderer); 229 break; 230 } 231 232 $renderer->doc .= "</div>" ; 233 234 235 return true; 236 } 237 return false; 238 } 239 240 241 function sort(&$array) { 242 if(!isset($array)) { 243 $array = array(); 244 return; 245 } 246 uasort($array, 'QuickStatsCmp'); 247 } 248 249 function extended_row($num=" ", $cells, $styles="") { 250 $style = ""; 251 if($styles) $style = "style = '$styles' "; 252 $row = "<tr><td $style >$num </td>"; 253 foreach($cells as $cell_data) { 254 $row .= "<td $style >$cell_data</td>"; 255 } 256 $row .= '</tr>'; 257 return $row; 258 } 259 260 function row($name,$val,$num=" ",$date=false,$is_ip=false) { 261 $title = ""; 262 $ns = $name; 263 if($is_ip && $this->giCity) { 264 $record = geoip_record_by_addr($this->giCity, $name); 265 $title = $record->country_name; 266 if(isset($this->ua_data[$name])) { 267 $title .= ' (' . $this->ua_data[$name][1] .')'; 268 } 269 } 270 271 elseif($this->long_names && (@strlen($name) > $this->long_names)) { 272 $title = "$name"; 273 $name = substr($name,0,$this->long_names) . '...'; 274 } 275 if($date) { 276 $date = date('r',$date); 277 $title = "$title $date"; 278 } 279 280 if($title && $is_ip) { 281 $name = "<a href='javascript:void 0;' title = '$title'>$name</a>"; 282 } 283 else if(is_numeric($num) && $date !== false) { 284 $name = "<a href='javascript: QuickstatsShowPage(\"$ns\");' title = '$title'>$name</a>"; 285 } 286 else if ($title) { 287 $name = "<a href='javascript:void 0;' title = '$title'>$name</a>"; 288 } 289 return "<tr><td>$num </td><td>$name</td><td> $val</td></tr>\n"; 290 291 } 292 293 function row_depth($new_depth=false) { 294 STATIC $depth = false; 295 296 if($new_depth !== false) { 297 $depth = $new_depth; 298 return; 299 } 300 301 return $depth; 302 } 303 304 function load_data($date_str=null,$which) { 305 global $uasort_ip; 306 $today = getdate(); 307 if($date_str) { 308 list($mon,$yr) = explode('_',$date_str); 309 $today['mon'] = $mon; 310 $today['year'] = $yr; 311 } 312 $ns_prefix = "quickstats:"; 313 $ns = $ns_prefix . $today['mon'] . '_' . $today['year'] . ':'; 314 $this->page_file = metaFN($ns . 'pages' ,'.ser'); 315 $this->ip_file = metaFN($ns . 'ip' , '.ser'); 316 $this->misc_data_file = metaFN($ns . 'misc_data' , '.ser'); 317 $this->ua_file = metaFN($ns . 'ua' , '.ser'); 318 319 if($which == 'basics' || $which == 'pages') { 320 $this->pages = unserialize(io_readFile($this->page_file,false)); 321 if(!$this->pages) $this->pages = array(); 322 } 323 if($which == 'basics' || $which == 'ip') { 324 $this->ips = unserialize(io_readFile($this->ip_file,false)); 325 if(!$this->ips) $this->ips = array(); 326 } 327 if($which == 'basics' || $which == 'countries' || $which == 'misc') { 328 $this->misc_data = unserialize(io_readFile($this->misc_data_file,false)); 329 if(!$this->misc_data) $this->misc_data = array(); 330 } 331 if($which == 'ua' || $which == 'ip') { 332 $this->ua_data = unserialize(io_readFile($this->ua_file,false)); 333 if(!$this->ua_data) $this->ua_data = array(); 334 if($which == 'ip') { 335 $this->ips = unserialize(io_readFile($this->ip_file,false)); 336 if(!$this->ips) $this->ips = array(); 337 } 338 else { 339 $uasort_ip = unserialize(io_readFile($this->ip_file,false)); 340 if(!$uasort_ip) $uasort_ip = array(); 341 } 342 } 343 344 } 345 346 function geoipcity_ini() { 347 348 if($this->getConf('geoplugin')) { 349 return; 350 } 351 require_once("GEOIP/geoipcity.inc"); 352 if($this->getConf('geoip_local') && file_exists(QUICK_STATS. 'GEOIP/GeoLiteCity.dat')) { 353 $this->giCity = geoip_open(QUICK_STATS. 'GEOIP/GeoLiteCity.dat',GEOIP_STANDARD); 354 } 355 else { 356 $gcity_dir = $this->getConf('geoip_dir'); 357 $gcity_dat=rtrim($gcity_dir, "\040,/\\") . $this->SEP . 'GeoLiteCity.dat'; 358 if(!file_exists( $gcity_dat)) return; 359 $this->giCity = geoip_open($gcity_dat,GEOIP_STANDARD); 360 } 361 } 362 363 function table($data,&$renderer,$numbers=true,$date=false,$ip_array=false) { 364 365 if($numbers !== false) 366 $num = 0; 367 else $num = " "; 368 369 if($ip_array) $this->geoipcity_ini(); 370 371 $ttl = 0; 372 $depth = $this->row_depth(); 373 if($depth == 'all') $depth = 0; 374 375 if($ip_array) { 376 $this->theader($renderer, 'IP'); 377 } 378 else if ($date && $numbers) { 379 $this->theader($renderer, 'Page'); 380 } 381 else $renderer->doc .= "<table cellspacing='4'>\n"; 382 foreach($data as $item=>$count) { 383 if($numbers) $num++; 384 $ttl += $count; 385 if($depth && $num > $depth) continue; 386 $md5 =md5($item); 387 $date_str = (is_array($date) && isset($date[$md5]) ) ? $date[$md5] : false; 388 $renderer->doc .= $this->row($item,$count,$num,$date_str, $ip_array); 389 } 390 $renderer->doc .= "</table>\n"; 391 return $ttl; 392 } 393 394 function theader(&$renderer,$name,$accesses='Accesses',$num=" Num ",$other="") { 395 if($accesses=='Accesses') $accesses=$this->getLang('accesses'); 396 $renderer->doc .= "<table cellspacing='4' class='sortable'>\n"; 397 $js = "<a href='javascript:void 0;' title='sort' class='quickstats_sort_title'>"; 398 $num = $js . $num . '</a>'; 399 $name = $js . $name . '</a>'; 400 $accesses = $js . $accesses . '</a>'; 401 $renderer->doc .= '<tr><th class="quickstats_sort">'. $num .'</th><th class="quickstats_sort">'.$name .'</th><th class="quickstats_sort">' . $accesses .'</th>'; 402 if($other) { 403 $other = $js . $other . '</a>'; 404 $renderer->doc .= '<th class="quickstats_sort">'. $other . '</th>'; 405 } 406 $renderer->doc .='</tr>'; 407 } 408 409 function ip_xhtml(&$renderer) { 410 $uniq = $this->ips['uniq']; 411 unset($this->ips['uniq']); 412 $this->sort($this->ips); 413 414 // $renderer->doc .= '<div class="quickstats ip">'; 415 $renderer->doc .= '<span class="title">' .$this->getLang('uniq_ip') .'</span>'; 416 $total_accesses = $this->table($this->ips,$renderer,true,true,true); 417 $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_accesses') . "$total_accesses</span></br>\n"; 418 $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_uniq_ip') ."$uniq</span></br>\n"; 419 // $renderer->doc .= "</div>\n"; 420 } 421 422 function pages_xhtml(&$renderer, $no_align=false) { 423 424 if(!$this->pages) return array(); 425 426 $this->sort($this->pages['page']); 427 if($no_align) { 428 $renderer->doc .= '<div class="qs_noalign">'; 429 } 430 else { 431 //$renderer->doc .= '<div style="margin: 10px 250px; overflow:auto; padding: 8px; width: 300px;">'; 432 $renderer->doc .= '<div class="pages_basics" style="overflow:auto;">'; 433 } 434 $renderer->doc .= '<span class="title">'. $this->getLang('label_page_access') .'</span>'; 435 436 $date =($this->show_date && isset($this->pages['date'] )) ? $this->pages['date'] : false; 437 $page_count = $this->table($this->pages['page'],$renderer,true,$date); 438 $renderer->doc .= "<span class='total'>" . $this->getLang('pages_accessed') . count($this->pages['page']) . "</span><br />"; 439 $renderer->doc .= "<span class='total'>". $this->getLang('ttl_accesses') . $this->pages['site_total'] .'</span>'; 440 $renderer->doc .= "</div>\n"; 441 442 } 443 function misc_data_xhtml(&$renderer,$no_align=false,$which='all') { 444 445 $renderer->doc .= "\n"; 446 447 if($which == 'all' || $which == 'misc') { 448 449 $browsers = $this->misc_data['browser']; 450 $platform = $this->misc_data['platform']; 451 $version = $this->misc_data['version']; 452 $this->sort($browsers); 453 $this->sort($platform); 454 $this->sort($version); 455 456 $renderer->doc .= "\n\n<!-- start misc -->\n"; 457 if($no_align) { 458 $renderer->doc .= '<div class="qs_noalign">'; 459 } 460 else { 461 //$renderer->doc .= '<div style="float:left;width: 200px; margin-left:20px;">'; 462 $renderer->doc .= '<div class="browsers_basics" style="float:left;">'; 463 } 464 $renderer->doc .="\n\n"; 465 $renderer->doc .= '<br /><span class="title">' . $this->getLang('browsers') .'</span>'; 466 467 $num=0; 468 $renderer->doc .= "<table border='0' >\n"; 469 foreach($browsers as $browser=>$val) { 470 $num++; 471 $renderer->doc .= $this->row($browser, $val,$num); 472 $renderer->doc .= "<tr><td colspan='3' style='border-top: 1px solid black'>"; 473 $v = $this->get_subversions($browser,$version); 474 $this->table($v,$renderer, false,false); 475 $renderer->doc .= '</td></tr>'; 476 } 477 $renderer->doc .= "</table>\n\n"; 478 479 480 $renderer->doc .= '<span class="title">Platforms</span>'; 481 $this->table($platform,$renderer); 482 $renderer->doc .= "</div>\n<!--end misc -->\n\n"; 483 } 484 485 if($which == 'misc') return; 486 487 $countries = $this->misc_data['country']; 488 $this->sort($countries); 489 490 if($no_align) { 491 $renderer->doc .= '<div>'; 492 } 493 else { 494 // $renderer->doc .= "<div style='float: right; overflow: auto; width: 200px; margin-right: 1px; margin-top: 12px;'>"; 495 $renderer->doc .= "<div class='countries_basics' style='float: right; overflow: auto;'>"; 496 } 497 $renderer->doc .= '<span class="title">Countries</span>'; 498 $this->theader($renderer, $this->getLang('country') ); 499 $num = 0; 500 $total = 0; 501 $depth = $this->row_depth(); 502 if($depth == 'all') $depth = false; 503 504 foreach($countries as $cc=>$count) { 505 if(!$cc) continue; 506 $num++; 507 $total+=$count; 508 $cntry=$this->cc_arrays->get_country_name($cc) ; 509 if($depth == false) { 510 $renderer->doc .= $this->row($cntry,$count,$num); 511 } 512 else if ($num <= $depth) { 513 $renderer->doc .= $this->row($cntry,$count,$num); 514 } 515 } 516 $renderer->doc .= '</table>'; 517 $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_countries') . count($this->misc_data['country']) . "</span></br>"; 518 519 $renderer->doc .= "<span class='total'>" . $this->getLang('ttl_accesses') ."$total</span></br>"; 520 521 522 $renderer->doc .= "</div>\n"; 523 524 525 } 526 527 function getTotal() { 528 $meta_path = $this->helper->metaFilePath(true) ; 529 $page_totals = unserialize(io_readFile($meta_path . 'page_totals.ser')); 530 $page_accessesTotal = 0; 531 if(!$page_totals) $page_totals = array(); 532 if(!empty($page_totals)) { 533 foreach($page_totals as $ttl) { 534 $page_accessesTotal+=$ttl; 535 } 536 } 537 return $page_accessesTotal; 538 } 539 540 function get_subversions($a,$b) { 541 $tmp = array(); 542 543 foreach($b as $key=>$val) { 544 if(strpos($key,$a) !== false) { 545 $tmp[$key] = $val; 546 } 547 } 548 $this->sort($tmp); 549 return $tmp; 550 } 551 552 /* this sorts ua array by ip accesses 553 * the keys to both ua and ip arrays are the ip addresses 554 * $a and $b in ua_Cmp($a,$b) are ip addresses, so $uasort_ip[$a] = number of accesses for ip $a 555 */ 556 function ua_sort(&$array) { 557 global $uasort_ip; 558 559 560 function ua_Cmp($a, $b) { 561 global $uasort_ip; 562 563 $na = $uasort_ip[$a]; 564 $nb = $uasort_ip[$b]; 565 566 if ($na == $nb) { 567 return 0; 568 } 569 return ($na > $nb) ? -1 : 1; 570 571 } 572 573 uksort($array, 'ua_Cmp'); 574 } 575 576 577 function ua_xhtml(&$renderer) { 578 global $uasort_ip; // sorted IP=>acceses 579 580 $depth = $this->row_depth(); 581 if($depth == 'all') $depth = false; 582 $asize = count($this->ua_data); 583 if($depth !== false) { 584 $this->ua_sort($this->ua_data); 585 if($depth > $asize) $depth = $asize; 586 $header = " ($depth/$asize) "; 587 } 588 else { 589 $header = " ($asize/$asize) "; 590 } 591 $total_accesses = $this->ua_data['counts'] ; 592 unset($this->ua_data['counts']); 593 $renderer->doc .="\n\n<div class=ip_data>\n"; 594 $styles = " padding-bottom: 4px; "; 595 $renderer->doc .= '<br /><span class="title">'. $this->getLang('browsers_and_ua') . $header .'</span>'; 596 $n = 0; 597 $this->theader($renderer,'IP', $this->getLang('country')," " . $this->getLang('accesses'). " ", " User Agents "); 598 foreach($this->ua_data as $ip=>$data) { 599 $n++; 600 if($depth !== false && $n > $depth) break; 601 $cc = array_shift($data); 602 $country=$this->cc_arrays->get_country_name($cc) ; 603 $uas = ' ' . implode(', ',$data); 604 $renderer->doc .= $this->extended_row($uasort_ip[$ip], array($ip, " $country",$uas), $styles); 605 } 606 $renderer->doc .= "</table>\n"; 607 608 // Output total table 609 $renderer->doc .= '<br /><span class="title">' . $this->getLang('ttl_accesses_ua') .'</span><br />'; 610 $n=0; 611 $this->theader($renderer," Agents "); 612 foreach($total_accesses as $agt=>$cnt) { 613 $n++; 614 if($depth !== false && $n > $depth) continue; 615 $renderer->doc .= "<tr><td>$n</td><td>$agt </td><td> $cnt</td>\n"; 616 } 617 $renderer->doc .= "</table></div>\n\n"; 618 } 619} 620 621 function QuickStatsCmp($a, $b) { 622 if ($a == $b) { 623 return 0; 624 } 625 return ($a > $b) ? -1 : 1; 626 } 627 628?>