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        uasort($array, 'QuickStatsCmp');
243    }
244
245    function extended_row($num="&nbsp;", $cells, $styles="") {
246        $style = "";
247        if($styles)  $style = "style = '$styles' ";
248        $row = "<tr><td  $style >$num&nbsp;&nbsp;</td>";
249        foreach($cells as $cell_data) {
250            $row .= "<td  $style >$cell_data</td>";
251        }
252        $row .= '</tr>';
253        return $row;
254    }
255
256    function row($name,$val,$num="&nbsp;",$date=false,$is_ip=false) {
257        $title = "";
258        $ns = $name;
259        if($is_ip  && $this->giCity) {
260            $record = geoip_record_by_addr($this->giCity, $name);
261            $title = $record->country_name;
262            if(isset($this->ua_data[$name])) {
263            $title .= ' (' . $this->ua_data[$name][1] .')';
264            }
265        }
266
267        elseif($this->long_names && (@strlen($name) > $this->long_names)) {
268            $title = "$name";
269            $name = substr($name,0,$this->long_names) . '...';
270        }
271        if($date) {
272            $date = date('r',$date);
273            $title = "$title $date";
274        }
275
276        if($title && $is_ip) {
277                $name = "<a href='javascript:void 0;' title = '$title'>$name</a>";
278        }
279        else if(is_numeric($num)  && $date !== false) {
280           $name = "<a href='javascript: QuickstatsShowPage(\"$ns\");' title = '$title'>$name</a>";
281        }
282        else if ($title) {
283             $name = "<a href='javascript:void 0;' title = '$title'>$name</a>";
284        }
285        return "<tr><td>$num&nbsp;&nbsp;</td><td>$name</td><td>&nbsp;&nbsp;&nbsp;&nbsp;$val</td></tr>\n";
286
287    }
288
289    function row_depth($new_depth=false) {
290        STATIC $depth = false;
291
292        if($new_depth !== false) {
293            $depth = $new_depth;
294            return;
295        }
296
297        return $depth;
298    }
299
300    function load_data($date_str=null,$which) {
301        global $uasort_ip;
302        $today = getdate();
303        if($date_str) {
304           list($mon,$yr) = explode('_',$date_str);
305           $today['mon'] = $mon;
306           $today['year'] = $yr;
307        }
308        $ns_prefix = "quickstats:";
309        $ns =  $ns_prefix . $today['mon'] . '_'  . $today['year'] . ':';
310        $this->page_file = metaFN($ns . 'pages' ,'.ser');
311        $this->ip_file = metaFN($ns . 'ip' , '.ser');
312        $this->misc_data_file = metaFN($ns . 'misc_data' , '.ser');
313        $this->ua_file = metaFN($ns . 'ua' , '.ser');
314
315        if($which == 'basics' || $which == 'pages') {
316            $this->pages = unserialize(io_readFile($this->page_file,false));
317            if(!$this->pages) $this->pages = array();
318        }
319        if($which == 'basics' || $which == 'ip') {
320            $this->ips = unserialize(io_readFile($this->ip_file,false));
321            if(!$this->ips) $this->ips = array();
322        }
323        if($which == 'basics' || $which == 'countries'  || $which == 'misc') {
324            $this->misc_data = unserialize(io_readFile($this->misc_data_file,false));
325            if(!$this->misc_data) $this->misc_data = array();
326        }
327        if($which == 'ua' || $which == 'ip') {
328               $this->ua_data = unserialize(io_readFile($this->ua_file,false));
329            if(!$this->ua_data) $this->ua_data = array();
330            if($which == 'ip') {
331                $this->ips = unserialize(io_readFile($this->ip_file,false));
332                if(!$this->ips) $this->ips = array();
333            }
334            else {
335                $uasort_ip = unserialize(io_readFile($this->ip_file,false));
336                if(!$uasort_ip) $uasort_ip = array();
337            }
338        }
339
340    }
341
342    function geoipcity_ini() {
343
344         if($this->getConf('geoplugin')) {
345            return;
346         }
347        require_once("GEOIP/geoipcity.inc");
348        if($this->getConf('geoip_local')) {
349             $this->giCity = geoip_open(QUICK_STATS. 'GEOIP/GeoLiteCity.dat',GEOIP_STANDARD);
350        }
351        else {
352            $gcity_dir = $this->getConf('geoip_dir');
353            $gcity_dat=rtrim($gcity_dir, "\040,/\\") . $this->SEP  . 'GeoLiteCity.dat';
354            if(!file_exists( $gcity_dat)) return;
355            $this->giCity = geoip_open($gcity_dat,GEOIP_STANDARD);
356        }
357    }
358
359    function table($data,&$renderer,$numbers=true,$date=false,$ip_array=false) {
360
361        if($numbers !== false)
362           $num = 0;
363         else  $num = "&nbsp;";
364
365       if($ip_array) $this->geoipcity_ini();
366
367       $ttl = 0;
368       $depth = $this->row_depth();
369       if($depth == 'all') $depth = 0;
370
371        if($ip_array) {
372             $this->theader($renderer, 'IP');
373        }
374        else if ($date && $numbers) {
375            $this->theader($renderer, 'Page');
376        }
377        else  $renderer->doc .= "<table cellspacing='4'>\n";
378        foreach($data as $item=>$count) {
379            if($numbers) $num++;
380            $ttl += $count;
381            if($depth  && $num > $depth) continue;
382            $md5 =md5($item);
383            $date_str = (is_array($date) &&  isset($date[$md5]) ) ? $date[$md5] : false;
384            $renderer->doc .= $this->row($item,$count,$num,$date_str, $ip_array);
385        }
386       $renderer->doc .= "</table>\n";
387       return $ttl;
388    }
389
390    function theader(&$renderer,$name,$accesses='Accesses',$num="&nbsp;Num&nbsp;",$other="") {
391         if($accesses=='Accesses') $accesses=$this->getLang('accesses');
392         $renderer->doc .= "<table cellspacing='4' class='sortable'>\n";
393         $js = "<a href='javascript:void 0;' title='sort' class='quickstats_sort_title'>";
394         $num = $js . $num . '</a>';
395         $name = $js . $name . '</a>';
396         $accesses = $js . $accesses . '</a>';
397         $renderer->doc .= '<tr><th class="quickstats_sort">'. $num .'</th><th class="quickstats_sort">'.$name .'</th><th class="quickstats_sort">' . $accesses .'</th>';
398         if($other) {
399               $other = $js . $other .  '</a>';
400               $renderer->doc .= '<th class="quickstats_sort">'. $other . '</th>';
401         }
402         $renderer->doc .='</tr>';
403    }
404
405    function ip_xhtml(&$renderer) {
406       $uniq = $this->ips['uniq'];
407       unset($this->ips['uniq']);
408       $this->sort($this->ips);
409
410    //  $renderer->doc .= '<div class="quickstats ip">';
411       $renderer->doc .= '<span class="title">' .$this->getLang('uniq_ip') .'</span>';
412       $total_accesses = $this->table($this->ips,$renderer,true,true,true);
413       $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_accesses') . "$total_accesses</span></br>\n";
414       $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_uniq_ip') ."$uniq</span></br>\n";
415      // $renderer->doc .= "</div>\n";
416    }
417
418    function pages_xhtml(&$renderer, $no_align=false) {
419
420        if(!$this->pages) return array();
421
422            $this->sort($this->pages['page']);
423            if($no_align) {
424                    $renderer->doc .= '<div class="qs_noalign">';
425            }
426            else {
427                //$renderer->doc .= '<div style="margin: 10px 250px; overflow:auto; padding: 8px; width: 300px;">';
428                $renderer->doc .= '<div   class="pages_basics"  style="overflow:auto;">';
429                }
430            $renderer->doc .= '<span class="title">'. $this->getLang('label_page_access') .'</span>';
431
432            $date =($this->show_date && isset($this->pages['date'] )) ? $this->pages['date'] : false;
433            $page_count = $this->table($this->pages['page'],$renderer,true,$date);
434            $renderer->doc .=  "<span class='total'>" . $this->getLang('pages_accessed')  . count($this->pages['page']) . "</span><br />";
435            $renderer->doc .=  "<span class='total'>". $this->getLang('ttl_accesses')  . $this->pages['site_total'] .'</span>';
436            $renderer->doc .= "</div>\n";
437
438    }
439    function misc_data_xhtml(&$renderer,$no_align=false,$which='all') {
440
441       $renderer->doc .= "\n";
442
443       if($which == 'all' || $which == 'misc') {
444
445            $browsers = $this->misc_data['browser'];
446            $platform = $this->misc_data['platform'];
447            $version = $this->misc_data['version'];
448            $this->sort($browsers);
449            $this->sort($platform);
450            $this->sort($version);
451
452              $renderer->doc .= "\n\n<!-- start misc -->\n";
453               if($no_align) {
454                    $renderer->doc .= '<div class="qs_noalign">';
455               }
456              else {
457                    //$renderer->doc .= '<div style="float:left;width: 200px; margin-left:20px;">';
458                    $renderer->doc .= '<div class="browsers_basics"  style="float:left;">';
459                }
460                $renderer->doc .="\n\n";
461                $renderer->doc .= '<br /><span class="title">' . $this->getLang('browsers') .'</span>';
462
463                $num=0;
464                $renderer->doc .= "<table border='0' >\n";
465                foreach($browsers as $browser=>$val) {
466                   $num++;
467                   $renderer->doc .= $this->row($browser, $val,$num);
468                   $renderer->doc .= "<tr><td colspan='3' style='border-top: 1px solid black'>";
469                   $v = $this->get_subversions($browser,$version);
470                   $this->table($v,$renderer, false,false);
471                   $renderer->doc .= '</td></tr>';
472                }
473               $renderer->doc .= "</table>\n\n";
474
475
476            $renderer->doc .= '<span class="title">Platforms</span>';
477            $this->table($platform,$renderer);
478            $renderer->doc .= "</div>\n<!--end misc -->\n\n";
479       }
480
481        if($which == 'misc') return;
482
483        $countries = $this->misc_data['country'];
484        $this->sort($countries);
485
486            if($no_align) {
487                    $renderer->doc .= '<div>';
488            }
489            else {
490               //  $renderer->doc .= "<div style='float: right; overflow: auto; width: 200px; margin-right: 1px; margin-top: 12px;'>";
491                   $renderer->doc .= "<div  class='countries_basics' style='float: right; overflow: auto;'>";
492            }
493           $renderer->doc .= '<span class="title">Countries</span>';
494                $this->theader($renderer,  $this->getLang('country') );
495                $num = 0;
496                $total = 0;
497                $depth = $this->row_depth();
498                if($depth == 'all') $depth = false;
499
500                foreach($countries as $cc=>$count) {
501                    if(!$cc) continue;
502                     $num++;
503                     $total+=$count;
504                     $cntry=$this->cc_arrays->get_country_name($cc) ;
505                     if($depth == false)  {
506                         $renderer->doc .= $this->row($cntry,$count,$num);
507                     }
508                     else if ($num <= $depth) {
509                          $renderer->doc .= $this->row($cntry,$count,$num);
510                     }
511                }
512              $renderer->doc .= '</table>';
513              $renderer->doc .= "<span class='total'>" .$this->getLang('ttl_countries') . count($this->misc_data['country'])  . "</span></br>";
514
515              $renderer->doc .= "<span class='total'>" . $this->getLang('ttl_accesses') ."$total</span></br>";
516
517
518             $renderer->doc .= "</div>\n";
519
520
521    }
522
523    function getTotal() {
524       $meta_path = $this->helper->metaFilePath(true) ;
525       $page_totals = unserialize(io_readFile($meta_path .  'page_totals.ser'));
526       $page_accessesTotal = 0;
527       if(!$page_totals) $page_totals = array();
528       if(!empty($page_totals)) {
529           foreach($page_totals as $ttl) {
530              $page_accessesTotal+=$ttl;
531           }
532       }
533       return $page_accessesTotal;
534    }
535
536    function get_subversions($a,$b) {
537        $tmp = array();
538
539         foreach($b as $key=>$val) {
540            if(strpos($key,$a) !== false) {
541                $tmp[$key] = $val;
542            }
543        }
544       $this->sort($tmp);
545       return  $tmp;
546    }
547
548    /*  this sorts ua array by ip accesses
549     *  the keys to both ua and ip arrays are the ip addresses
550     *  $a and $b in ua_Cmp($a,$b) are ip addresses, so $uasort_ip[$a] = number of accesses for ip $a
551    */
552    function ua_sort(&$array) {
553     global $uasort_ip;
554
555
556       function ua_Cmp($a, $b) {
557            global $uasort_ip;
558
559            $na = $uasort_ip[$a];
560            $nb = $uasort_ip[$b];
561
562           if ($na == $nb) {
563                return 0;
564            }
565            return ($na > $nb) ? -1 : 1;
566
567         }
568
569       uksort($array, 'ua_Cmp');
570    }
571
572
573    function ua_xhtml(&$renderer) {
574                global $uasort_ip;   // sorted IP=>acceses
575
576                $depth = $this->row_depth();
577                if($depth == 'all') $depth = false;
578                $asize = count($this->ua_data);
579                if($depth !== false) {
580                        $this->ua_sort($this->ua_data);
581                        if($depth > $asize) $depth = $asize;
582                        $header = " ($depth/$asize) ";
583                }
584                else {
585                    $header = " ($asize/$asize) ";
586                }
587                $total_accesses = $this->ua_data['counts'] ;
588                unset($this->ua_data['counts']);
589                $renderer->doc .="\n\n<div class=ip_data>\n";
590                $styles = " padding-bottom: 4px; ";
591                $renderer->doc .= '<br /><span class="title">'. $this->getLang('browsers_and_ua') . $header   .'</span>';
592                $n = 0;
593               $this->theader($renderer,'IP', $this->getLang('country'),"&nbsp;" . $this->getLang('accesses'). "&nbsp;", "&nbsp;User Agents&nbsp;");
594                foreach($this->ua_data as $ip=>$data) {
595                    $n++;
596                    if($depth !== false && $n > $depth) break;
597                    $cc = array_shift($data);
598                    $country=$this->cc_arrays->get_country_name($cc) ;
599                    $uas = '&nbsp;&nbsp;&nbsp;&nbsp;' . implode(',&nbsp;',$data);
600                    $renderer->doc .=  $this->extended_row($uasort_ip[$ip], array($ip, "&nbsp;&nbsp;$country",$uas), $styles);
601                }
602               $renderer->doc .= "</table>\n";
603
604                // Output total table
605              $renderer->doc .= '<br /><span class="title">' . $this->getLang('ttl_accesses_ua') .'</span><br />';
606               $n=0;
607               $this->theader($renderer,"&nbsp;&nbsp;&nbsp;&nbsp;Agents&nbsp;&nbsp;&nbsp;&nbsp;");
608               foreach($total_accesses as $agt=>$cnt) {
609                  $n++;
610                  if($depth !== false && $n > $depth) continue;
611                  $renderer->doc .= "<tr><td>$n</td><td>$agt&nbsp;</td><td>&nbsp;&nbsp;$cnt</td>\n";
612               }
613              $renderer->doc .= "</table></div>\n\n";
614    }
615}
616
617          function QuickStatsCmp($a, $b) {
618            if ($a == $b) {
619                return 0;
620            }
621            return ($a > $b) ? -1 : 1;
622         }
623
624?>