1 <?php
2 /**
3  * Creates Simple statistics files based on incoming IP
4  *
5  * @author  Myron Turner <turnermm02@shaw.ca>
6  */
7 
8 if(!defined('DOKU_INC')) die();
9 if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
10 if(!defined('QUICK_STATS')) define ('QUICK_STATS',DOKU_PLUGIN . 'quickstats/');
11 require_once DOKU_PLUGIN.'action.php';
12 require_once QUICK_STATS . 'scripts/php-inet6_1.0.2/valid_v6.php';
13 require_once QUICK_STATS .'GEOIP/vendor/autoload.php';
14 use GeoIp2\Database\Reader;
15 /* for backward compatiblity */
16 if(!function_exists('utf8_strtolower')) {
17 require_once(DOKU_INC.'inc/common.php');
18 require_once(DOKU_INC.'inc/utf8.php');
19 }
20 
21 /*
22 error_reporting(E_ALL);
23 ini_set('display_errors','1');
24 */
25 
26 class 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 }