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}