1<?php 2/** 3 * Creates Simple statistics files based on incoming IP 4 * 5 * @author Myron Turner <turnermm02@shaw.ca> 6 */ 7 8if(!defined('DOKU_INC')) die(); 9if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 10if(!defined('QUICK_STATS')) define ('QUICK_STATS',DOKU_PLUGIN . 'quickstats/'); 11require_once DOKU_PLUGIN.'action.php'; 12require_once QUICK_STATS . 'scripts/php-inet6_1.0.2/valid_v6.php'; 13require_once QUICK_STATS .'GEOIP/vendor/autoload.php'; 14use GeoIp2\Database\Reader; 15/* for backward compatiblity */ 16if(!function_exists('utf8_strtolower')) { 17require_once(DOKU_INC.'inc/common.php'); 18require_once(DOKU_INC.'inc/utf8.php'); 19} 20 21/* 22error_reporting(E_ALL); 23ini_set('display_errors','1'); 24*/ 25 26class action_plugin_quickstats extends DokuWiki_Action_Plugin { 27 private $page_file; 28 private $ip_file; 29 private $misc_data_file; 30 private $pages; 31 private $ips; 32 private $misc_data; 33 private $page_totals_file; 34 private $is_edit_user=false; 35 private $year_month; 36 private $totals; 37 private $SEP = '/'; 38 private $show_date; 39 private $ua_file; 40 private $helper; 41 private $ipaddr; 42 private $qs_file; 43 private $dw_tokens; // query string names to omit from stats 44 private $page_users_file; 45 private $ipv6 = false; 46 private $id; 47 private $geocity2 = true; 48 private $db_check; 49 50 function __construct() { 51 global $ID,$INPUT; 52 $this->id = $INPUT->str('id'); 53 $ip = $_SERVER['REMOTE_ADDR']; 54 //$ip = "2001:982:acd6:1:4899:d135:226b:2e79"; 55 //$ip = "2602:304:cec0:9b00:e96b:9c78:eb14:9fb"; 56 // $ip = "76.24.190.253"; 57 if($this->is_excluded($ip, true)){ 58 exit("403: Not Available"); 59 } 60 61 $ipv6 = isValidIPv6($ip); 62 if($ipv6) { 63 $this->ipaddr = $ipv6; 64 $this->ipv6 = $ipv6; 65 } 66 else $this->ipaddr = $ip; 67 $today = getdate(); 68 69 $ns_prefix = "quickstats:"; 70 $ns = $ns_prefix . $today['mon'] . '_' . $today['year'] . ':'; 71 $this->page_file = metaFN($ns . 'pages' , '.ser'); 72 $this->ua_file = metaFN($ns . 'ua' , '.ser'); 73 $this->ip_file = metaFN($ns . 'ip' , '.ser'); 74 $this->misc_data_file = metaFN($ns . 'misc_data' , '.ser'); 75 $this->qs_file = metaFN($ns . 'qs_data' , '.ser'); 76 $this->page_users_file = metaFN($ns . 'page_users' , '.ser'); 77 $this->page_totals_file = metaFN($ns_prefix . 'page_totals' , '.ser'); 78 $this ->db_check = metaFN($ns_prefix . 'db_warning' , '.txt'); 79 if(!file_exists($this ->db_check)) { 80 io_saveFile($this ->db_check,0); 81 } 82 $this->year_month = $today['mon'] . '_' .$today['year']; 83 84 if( preg_match('/WINNT/i', PHP_OS) ) { 85 $this->SEP='\\'; 86 } 87 $this->show_date=$this->getConf('show_date'); 88 $this->dw_tokens=array('do', 'sectok', 'page', 's[]','id','rev','idx'); 89 $conf_tokens = $this->getConf('xcl_name_val'); 90 if(!empty($conf_tokens)) { 91 $conf_tokens = explode(',',$conf_tokens); 92 if(!empty($conf_tokens)) { 93 $this->dw_tokens = array_merge($this->dw_tokens,$conf_tokens); 94 } 95 } 96 $this->helper = $this->loadHelper('quickstats', true); 97 98 } 99 function test_geocity2() { 100 global $INFO; 101 $test = false; 102 $err = ""; 103 if($this->getConf('by_pass_mmdb')) { 104 $this->geocity2 = false; 105 return; 106 } 107 108 try { 109 $reader = new Reader(QUICK_STATS .'GEOIP/vendor/GeoLite2-City/GeoLite2-City.mmdb'); 110 if($test) { 111 $record = $reader->city('138.201.137.132'); 112 msg($record->country->isoCode); // 'DE' 113 msg($record->country->name); // 'Germany' 114 $ip ="2001:982:acd6:1:4899:d135:226b:2e79"; 115 $record = $reader->city($ip); 116 msg($record->country->isoCode); // 'NL' 117 msg($record->country->name); // 'Netherlands' 118 } 119 } catch (Exception $e) { 120 $this->geocity2 = false; 121 $err = $e->getMessage(); 122 $checked = io_readFile($this ->db_check,false); 123 if($checked <= 6){ 124 io_saveFile($this ->db_check,($checked+1)); 125 $err .= $this->getLang('missing_mmdb_warning'); 126 } 127 } 128 129 // if($this->getConf('hide_db_warning'))return; 130 if($INFO['isadmin'] && $err) msg($err,2); 131 } 132 /** 133 * Register its handlers with the DokuWiki's event controller 134 */ 135 function register(Doku_Event_Handler $controller) { 136 $controller->register_hook('DOKUWIKI_STARTED', 'BEFORE', $this, 'set_cookies'); 137 $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'search_queries'); 138 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this,'_ajax_handler'); 139 $controller->register_hook('DOKUWIKI_DONE', 'BEFORE', $this, '_add__data'); 140 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load_js'); 141 } 142 143 function isQSfile() { 144 global $ID; 145 if(!$this->helper->is_inConfList($ID) ) { 146 return $this->helper->is_inCache($ID) ; 147 } 148 return true; 149 } 150 151 function load_js(&$event, $param) { 152 global $ACT, $ID; 153 if($ACT != 'show' && $ACT != 'preview') return; // don't load the sortable script unless it's potentially needed 154 155 if(!$this->isQSfile()) return; 156 157 $event->data["script"][] = array ( 158 "type" => "text/javascript", 159 "src" => DOKU_BASE."lib/plugins/quickstats/scripts/sorttable-cmpr.js", 160 "_data" => "", 161 ); 162 } 163 164 function set_cookies(&$event, $param) { 165 166 global $ACT; 167 global $ID, $JSINFO; 168 global $conf; 169 $this->test_geocity2(); 170 171 if(!empty($ACT) && !is_array($ACT) ) { 172 $JSINFO['act'] = $ACT; 173 } 174 else $JSINFO['act'] = ""; 175 176 $ajax =$this->getConf('ajax'); 177 $JSINFO['ajax'] = $this->getConf('ajax') ? 'ajax' : 'event'; 178 $sidebar_ns = $this->getConf('hide_sidebar'); 179 180 if(!empty($sidebar_ns)) { 181 $quick_ns =getNS($ID); 182 $sidebar_ns = trim($sidebar_ns,':'); 183 if($quick_ns == trim($sidebar_ns,':')) $conf['sidebar'] = ""; 184 } 185 if(is_array($ACT) || $ACT=='edit') { 186 $expire = time()+3600; 187 setcookie('Quick_Stats','abc', $expire, '/'); 188 $this->is_edit_user=true; 189 return; 190 } 191 192 if(isset($_COOKIE['Quick_Stats'])) { 193 setcookie("Quick_Stats", 'abc', time()-7200, '/'); 194 $this->is_edit_user=true; 195 } 196 197 } 198 199 function search_queries(&$event, $param) { 200 global $ACT; 201 202 if(is_array($ACT) || $this->is_edit_user) return; 203 if($ACT !='show' && $ACT != 'search') return; 204 //login,admin,profile, revisions,logout 205 206 if(empty($_SERVER['QUERY_STRING']) || $this->is_excluded($this->ipaddr)) return; 207 208 $queries = unserialize(io_readFile($this->qs_file,false)); 209 if(!$queries) $queries = array('words'=>array(), 'ns'=>array(), 'extern'=>array() ); 210 211 $elems = explode('&',html_entity_decode($_SERVER['QUERY_STRING'])) ; 212 213 $data_found = false; 214 if($elems && count($elems)>1) 215 { 216 $words = array(); 217 $temp = array(); 218 foreach ($elems as $el) { 219 if(isset($el) && $el) { 220 list($name,$value) = explode('=',$el); 221 $temp[$name]=$value; 222 } 223 } 224 if(isset($temp['do']) && $temp['do'] == 'search') { 225 $data_found = true; 226 if(function_exists ('idx_get_indexer')) { 227 $ar = ft_queryParser(idx_get_indexer(), urldecode($temp['id'])); 228 } 229 else $ar = ft_queryParser(urldecode($temp['id'])); 230 231 if(!empty($ar['phrases']) && !empty($ar['not'])) { 232 $words = array_diff($ar['words'],$ar['not']); 233 } 234 else { 235 $words = $ar['words']; 236 } 237 238 if(!empty($words)) { 239 foreach($words as $word) { 240 $this->set_queries($queries,$word,'words'); 241 } 242 } 243 244 if(!empty($ar['ns'])) { 245 foreach($ar['ns'] as $ns) { 246 $this->set_queries($queries,$ns,'ns'); 247 } 248 } 249 } 250 else { 251 252 foreach($this->dw_tokens as $t) { 253 if(isset($temp[$t])) { 254 unset($temp[$t]); 255 } 256 } 257 258 if(count($temp)) { 259 $keys = array_keys($temp); 260 foreach($keys as $k) { 261 if(preg_match('/rev\d*\[\d*\]/', $k)) { 262 unset($temp[$k]); 263 } 264 } 265 if(count($temp)) $data_found = true; 266 } 267 268 foreach($temp as $name=>$val) { 269 $this->set_queries($queries['extern'],urldecode($name),'name'); 270 if(!$val) $val = '_empty_'; 271 $this->set_queries($queries['extern'],urldecode($val),'val'); 272 $this->set_named_values($queries['extern']['name'][urldecode($name)],urldecode($val)); 273 } 274 } 275 276 if($data_found) { 277 io_saveFile($this->qs_file,serialize($queries)); 278 } 279 } 280 } 281 282 function set_named_values(&$queries,$val="_empty_") { 283 284 if(!isset($queries['values'])) { 285 $queries['values'] = array(); 286 } 287 if(!isset($queries['values'][$this->ipaddr])) { 288 $queries['values'][$this->ipaddr] = array(); 289 } 290 if(!in_array($val, $queries['values'][$this->ipaddr])) { 291 $queries['values'][$this->ipaddr][] = $val; 292 } 293 } 294 295 function set_queries(&$queries,$word,$which) { 296 if(!isset($queries[$which][$word])) { 297 $queries[$which][$word]['count'] = 1; 298 } 299 else { 300 $queries[$which][$word]['count'] += 1; 301 } 302 if(!isset($queries[$which][$word][$this->ipaddr])) { 303 $queries[$which][$word][$this->ipaddr] = 1; 304 } 305 else $queries[$which][$word][$this->ipaddr] += 1; 306 } 307 308 function msg_dbg($what,$prefix="",$type="1") { 309 if(is_array($what)) { 310 $what = print_r($what,true); 311 } 312 msg("<pre>$prefix $what</pre>",$type); 313 } 314 function load_data() { 315 316 $this->pages = unserialize(io_readFile($this->page_file,false)); 317 if(!$this->pages) $this->pages = array(); 318 319 $this->ips = unserialize(io_readFile($this->ip_file,false)); 320 if(!$this->ips) $this->ips = array(); 321 322 $this->totals = unserialize(io_readFile($this->page_totals_file,false)); 323 if(!$this->totals) $this->totals = array(); 324 } 325 326 function save_data() { 327 io_saveFile($this->ip_file,serialize($this->ips)); 328 io_saveFile($this->page_file,serialize($this->pages)); 329 $this->totals[$this->year_month] = $this->pages['site_total'] ; 330 io_saveFile($this->page_totals_file,serialize($this->totals)); 331 } 332 333 function is_excluded($ip,$abort=false) { 334 if(!$abort) { 335 $xcl = $this->getConf('excludes'); 336 } 337 else $xcl = $this->getConf('aborts'); 338 $xcl=str_replace("\040", '',$xcl); 339 340 if(!$xcl) return false; 341 342 $xcludes=explode(',',$xcl); 343 $regex = '#'. implode('|',$xcludes) . '#'; 344 345 if(preg_match($regex,$ip)){ 346 return true; 347 } 348 return false; 349 } 350 351 function _ajax_handler(Doku_Event $event,$param) { 352 if ($event->data != 'quickstats') return; 353 global $INPUT,$ACT,$ID, $INFO; 354 $ip = $_SERVER['REMOTE_ADDR']; 355 $event->stopPropagation(); 356 $event->preventDefault(); 357 if(!$this->getConf('ajax')) return; 358 $qs = $INPUT->str('qs'); 359 $do = $INPUT->str('do'); 360 if(strpos($qs,'edit') !== false || $do == 'edit') { 361 $act = 'edit'; 362 } 363 else $act = $INPUT->str('act'); 364 $ACT = $act; 365 $ID = $INPUT->str('id') ; 366 367 if(isset($_COOKIE['Quick_Stats'])) $this->is_edit_user = 'edit_user'; 368 $param = 'ajax'; 369 $this->add_data($event, $param); 370 } 371 372 function _add__data($event, $param) { 373 if($this->getConf('ajax')) return; 374 $this->add_data($event, 'event'); 375 } 376 377 378 /** 379 * adds new data to stats files 380 * 381 * @author Myron Turner <turnermm02@shaw.ca> 382 */ 383 function add_data($event, $param) { 384 global $ID; 385 global $ACT; 386 $xclpages = trim($this->getConf('xcl_pages')); 387 $xclpages = str_replace(',','|',$xclpages); 388 $xclpages = str_replace('::', ':.*?', $xclpages); 389 $xclpages = preg_replace("/\s+/","",$xclpages); //remove any spaces 390 $xclpages = str_replace("|:","|",$xclpages); //remove any initial colons 391 if(preg_match("/^" . $xclpages . "$/",$ID)) return; 392 393 if($this->is_edit_user) return; 394 if($ACT != 'show') return; 395 396 $this->load_data(); 397 398 require_once("GEOIP/geoipcity.inc"); 399 require_once('db/php-local-browscap.php'); 400 401 $ip = $_SERVER['REMOTE_ADDR']; 402 403 if($this->is_excluded($ip)){ 404 return; 405 } 406 407 if($this->ipv6) { 408 $ip = $this->ipv6; 409 } 410 411 $this->misc_data = unserialize(io_readFile($this->misc_data_file,false)); 412 if(!$this->misc_data) $this->misc_data = array(); 413 414 $country = $this->get_country($ip); 415 if($country) { 416 if(!isset($this->misc_data['country'] [$country['code']])) { 417 $this->misc_data['country'] [$country['code']] =1; 418 } 419 else { 420 $this->misc_data['country'] [$country['code']] +=1; 421 } 422 } 423 424 $browser = $this->get_browser(); 425 426 io_saveFile($this->misc_data_file,serialize($this->misc_data)); 427 unset($this->misc_data); 428 429 $wiki_file = wikiFN($ID); 430 if(file_exists($wiki_file)) { 431 if(!$this->pages) { 432 $this->pages['site_total'] = 1; 433 $this->pages['page'][$ID] = 1; 434 $this->ips['uniq'] = 0; 435 } 436 else { 437 $this->pages['site_total'] += 1; 438 $this->pages['page'][$ID] += 1; 439 } 440 } 441 442 if(!array_key_exists($ip, $this->ips)) { 443 $this->ips[$ip] = 0; 444 $this->ips['uniq'] = (!isset($this->ips['uniq'])) ? 1 : $this->ips['uniq'] += 1; 445 } 446 447 $this->ips[$ip] += 1; 448 if($this->show_date) { 449 $this->pages['date'][md5($ID)] = time(); 450 } 451 $this->save_data(); 452 $this->pages=array(); 453 $this->ips=array(); 454 455 456 $this->ua = unserialize(io_readFile($this->ua_file,false)); 457 if(!$this->ua) $this->ua = array(); 458 if(!isset($this->ua['counts'])) { 459 $this->ua['counts'] = array(); 460 } 461 462 if(!isset($this->ua['counts'][$browser])) { 463 $this->ua['counts'][$browser]=1; 464 } 465 else $this->ua['counts'][$browser]++; 466 467 if(!isset($this->ua[$ip])) { 468 $this->ua[$ip] = array($country['code']); 469 } 470 if(isset($browser) && !in_array($browser, $this->ua[$ip])) { 471 $this->ua[$ip][]=$browser; 472 } 473 io_saveFile($this->ua_file,serialize($this->ua)); 474 $this->ua = array(); 475 476 $this->pusers = unserialize(io_readFile($this->page_users_file,false)); 477 if(!$this->pusers) $this->pusers = array(); 478 $page_md5 = md5($ID); 479 if(!isset($this->pusers[$page_md5])) { 480 $this->pusers[$page_md5] = array(); 481 } 482 if(!isset($this->pusers[$ip])) { 483 $this->pusers[$ip] = array(); 484 } 485 $pushed_new = false; 486 if(!in_array($ip,$this->pusers[$page_md5])) { 487 $pushed_new = true; 488 array_push($this->pusers[$page_md5],$ip); 489 } 490 if(!in_array($ID,$this->pusers[$ip],$ID)) { 491 $pushed_new = true; 492 array_push($this->pusers[$ip],$ID); 493 } 494 if($pushed_new) { 495 io_saveFile($this->page_users_file,serialize($this->pusers)); 496 } 497 } 498 499 function get_browser() { 500 501 $db= QUICK_STATS . 'db/lite_php_browscap.ini'; 502 if(!file_exists($db)) { 503 $db = QUICK_STATS . 'db/full_php_browscap.ini'; 504 } 505 if(!file_exists($db)) { 506 $db = QUICK_STATS . 'db/php_browscap.ini'; 507 } 508 if(!file_exists($db)) { 509 msg($this->getLang('no_browser_db'),1); 510 } 511 512 $browser=get_browser_local(null,true,$db); 513 if(!isset($browser['browser'])) return; 514 $this->set_browser_value($browser['browser']); 515 516 if(!isset($browser['platform'])) return; 517 $this->set_browser_value($browser['platform'],'platform'); 518 519 if(!isset($browser['version'])) return; 520 $this->set_browser_value($browser['parent'],'version'); 521 if(isset($browser['parent']) && $browser['parent']) { 522 return $browser['parent']; 523 } 524 return $browser['browser']; 525 526 } 527 528 function set_browser_value($val, $which='browser') { 529 if(!isset($this->misc_data[$which][$val])) { 530 $this->misc_data[$which] [$val] =1; 531 } 532 else { 533 $this->misc_data[$which] [$val] +=1; 534 } 535 536 } 537 538 function get_country($ip=null) { 539 540 if(!$ip) return null; 541 // $ip = '138.201.137.132'; 542 $test = false; 543 544 if($this->geocity2) { 545 try{ 546 $reader = new Reader(QUICK_STATS .'GEOIP/vendor/GeoLite2-City/GeoLite2-City.mmdb'); 547 if($reader) { 548 $record = $reader->city($ip); 549 return (array('code'=>$record->country->isoCode,'name'=>$record->country->name)); 550 } 551 } catch (Exception $e) { 552 if($test) msg($e->getMessage()); 553 } 554 } 555 556 if($this->getConf('geoplugin')) { 557 $country_data = unserialize(file_get_contents('http://www.geoplugin.net/php.gp?ip=' .$ip)); 558 return (array('code'=>$country_data['geoplugin_countryCode'],'name'=>$country_data['geoplugin_countryName'])); 559 } 560 561 if($this->ipv6) { 562 $ip = $this->ipv6; 563 $db = 'GeoIPv6.dat'; 564 } 565 else $db = 'GeoLiteCity.dat'; 566 567 if($this->getConf('geoip_local')) { 568 if(!file_exists (QUICK_STATS. 'GEOIP/' . $db)) { return array();} 569 $giCity = geoip_open(QUICK_STATS. 'GEOIP/' . $db, GEOIP_STANDARD); 570 } 571 else { 572 $gcity_dir = $this->getConf('geoip_dir'); 573 $gcity_dat=rtrim($gcity_dir, "\040,/\\") . $this->SEP . $db; 574 if(!file_exists ($gcity_dat)) { return array();} 575 $giCity = geoip_open($gcity_dat,GEOIP_STANDARD); 576 } 577 578 if($this->ipv6) { 579 return (array('code'=>geoip_country_code_by_addr_v6($giCity, $ip),'name'=>geoip_country_name_by_addr_v6($giCity, $ip) )); 580 } 581 else $record = GeoIP_record_by_addr($giCity, $ip); 582 583 if(!isset($record)) { 584 return array(); 585 } 586 587 return (array('code'=>$record->country_code,'name'=>$record->country_name)); 588 } 589 590 591}