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="&nbsp;", $cells, $styles="") {
250        $style = "";
251        if($styles)  $style = "style = '$styles' ";
252        $row = "<tr><td  $style >$num&nbsp;&nbsp;</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="&nbsp;",$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&nbsp;&nbsp;</td><td>$name</td><td>&nbsp;&nbsp;&nbsp;&nbsp;$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 = "&nbsp;";
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="&nbsp;Num&nbsp;",$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'),"&nbsp;" . $this->getLang('accesses'). "&nbsp;", "&nbsp;User Agents&nbsp;");
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 = '&nbsp;&nbsp;&nbsp;&nbsp;' . implode(',&nbsp;',$data);
604                    $renderer->doc .=  $this->extended_row($uasort_ip[$ip], array($ip, "&nbsp;&nbsp;$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,"&nbsp;&nbsp;&nbsp;&nbsp;Agents&nbsp;&nbsp;&nbsp;&nbsp;");
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&nbsp;</td><td>&nbsp;&nbsp;$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?>