xref: /plugin/statistics/admin.php (revision 9da6395d738943db132258012a01609b922e0afa)
1<?php
2/**
3 * statistics plugin
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Andreas Gohr <gohr@cosmocode.de>
7 */
8
9// must be run within Dokuwiki
10if(!defined('DOKU_INC')) die();
11
12if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');
13require_once(DOKU_PLUGIN.'admin.php');
14
15/**
16 * All DokuWiki plugins to extend the admin function
17 * need to inherit from this class
18 */
19class admin_plugin_statistics extends DokuWiki_Admin_Plugin {
20    var $dblink = null;
21    var $opt    = '';
22    var $from   = '';
23    var $to     = '';
24    var $start  = '';
25    var $tlimit = '';
26
27    /**
28     * return some info
29     */
30    function getInfo(){
31        return confToHash(dirname(__FILE__).'/info.txt');
32    }
33
34    /**
35     * Access for managers allowed
36     */
37    function forAdminOnly(){
38        return false;
39    }
40
41    /**
42     * return sort order for position in admin menu
43     */
44    function getMenuSort() {
45        return 150;
46    }
47
48    /**
49     * handle user request
50     */
51    function handle() {
52        $this->opt = preg_replace('/[^a-z]+/','',$_REQUEST['opt']);
53
54        $this->start = (int) $_REQUEST['s'];
55
56        // fixme add better sanity checking here:
57        $this->from = preg_replace('/[^\d\-]+/','',$_REQUEST['f']);
58        $this->to = preg_replace('/[^\d\-]+/','',$_REQUEST['t']);
59        if(!$this->from) $this->from = date('Y-m-d');
60        if(!$this->to) $this->to     = date('Y-m-d');
61
62        //setup limit clause
63        if($this->from != $this->to){
64            $this->tlimit = "DATE(A.dt) >= DATE('".$this->from."') AND DATE(A.dt) <= DATE('".$this->to."')";
65        }else{
66            $this->tlimit = "DATE(A.dt) = DATE('".$this->from."')";
67        }
68    }
69
70    /**
71     * fixme build statistics here
72     */
73    function html() {
74        $this->html_toc();
75        echo '<h1>Access Statistics</h1>';
76        $this->html_timeselect();
77
78        switch($this->opt){
79            case 'country':
80                $this->html_country();
81                break;
82            case 'page':
83                $this->html_page();
84                break;
85            case 'referer':
86                $this->html_referer();
87                break;
88            default:
89                $this->html_dashboard();
90        }
91    }
92
93    function html_toc(){
94        echo '<div class="toc">';
95        echo '<div class="tocheader toctoggle" id="toc__header">';
96        echo 'Detailed Statistics';
97        echo '</div>';
98        echo '<div id="toc__inside">';
99        echo '<ul class="toc">';
100
101        echo '<li><div class="li">';
102        echo '<a href="?do=admin&amp;page=statistics&amp;opt=&amp;f='.$this->from.'&amp;t='.$this->to.'&amp;s='.$this->start.'">Dashboard</a>';
103        echo '</div></li>';
104
105        echo '<li><div class="li">';
106        echo '<a href="?do=admin&amp;page=statistics&amp;opt=page&amp;f='.$this->from.'&amp;t='.$this->to.'&amp;s='.$this->start.'">Pages</a>';
107        echo '</div></li>';
108
109        echo '<li><div class="li">';
110        echo '<a href="?do=admin&amp;page=statistics&amp;opt=referer&amp;f='.$this->from.'&amp;t='.$this->to.'&amp;s='.$this->start.'">Incoming Links</a>';
111        echo '</div></li>';
112
113        echo '<li><div class="li">';
114        echo '<a href="?do=admin&amp;page=statistics&amp;opt=country&amp;f='.$this->from.'&amp;t='.$this->to.'&amp;s='.$this->start.'">Countries</a>';
115        echo '</div></li>';
116
117        echo '</ul>';
118        echo '</div>';
119        echo '</div>';
120    }
121
122    /**
123     * Print the time selection menu
124     */
125    function html_timeselect(){
126        $now   = date('Y-m-d');
127        $yday  = date('Y-m-d',time()-(60*60*24));
128        $week  = date('Y-m-d',time()-(60*60*24*7));
129        $month = date('Y-m-d',time()-(60*60*24*30));
130
131        echo '<div class="plg_stats_timeselect">';
132        echo '<span>Select the timeframe:</span>';
133        echo '<ul>';
134
135        echo '<li>';
136        echo '<a href="?do=admin&amp;page=statistics&amp;opt='.$this->opt.'&amp;f='.$now.'&amp;t='.$now.'&amp;s='.$this->start.'">';
137        echo 'today';
138        echo '</a>';
139        echo '</li>';
140
141        echo '<li>';
142        echo '<a href="?do=admin&amp;page=statistics&amp;opt='.$this->opt.'&amp;f='.$yday.'&amp;t='.$yday.'&amp;s='.$this->start.'">';
143        echo 'yesterday';
144        echo '</a>';
145        echo '</li>';
146
147        echo '<li>';
148        echo '<a href="?do=admin&amp;page=statistics&amp;opt='.$this->opt.'&amp;f='.$week.'&amp;t='.$now.'&amp;s='.$this->start.'">';
149        echo 'last 7 days';
150        echo '</a>';
151        echo '</li>';
152
153        echo '<li>';
154        echo '<a href="?do=admin&amp;page=statistics&amp;opt='.$this->opt.'&amp;f='.$month.'&amp;t='.$now.'&amp;s='.$this->start.'">';
155        echo 'last 30 days';
156        echo '</a>';
157        echo '</li>';
158
159        echo '</ul>';
160
161
162        echo '<form action="" method="get">';
163        echo '<input type="hidden" name="do" value="admin" />';
164        echo '<input type="hidden" name="page" value="statistics" />';
165        echo '<input type="hidden" name="opt" value="'.$this->opt.'" />';
166        echo '<input type="hidden" name="s" value="'.$this->start.'" />';
167        echo '<input type="text" name="f" value="'.$this->from.'" class="edit" />';
168        echo '<input type="text" name="t" value="'.$this->to.'" class="edit" />';
169        echo '<input type="submit" value="go" class="button" />';
170        echo '</form>';
171
172        echo '</div>';
173    }
174
175
176    /**
177     * Print an introductionary screen
178     */
179    function html_dashboard(){
180        echo '<div class="plg_stats_dashboard">';
181
182
183        // top pages today
184        echo '<div>';
185        echo '<h2>Most popular pages</h2>';
186        $result = $this->sql_pages($this->tlimit,$this->start,15);
187        $this->html_resulttable($result,array('Pages','Count'));
188        echo '</div>';
189
190        // top referer today
191        echo '<div>';
192        echo '<h2>Top incoming links</h2>';
193        $result = $this->sql_referer($this->tlimit,$this->start,15);
194        $this->html_resulttable($result,array('Incoming Links','Count'));
195        echo '</div>';
196
197        // top countries today
198        echo '<div>';
199        echo '<h2>Visitor\'s top countries</h2>';
200        echo '<img src="'.DOKU_BASE.'lib/plugins/statistics/img.php?img=country&amp;f='.$this->from.'&amp;t='.$this->to.'" />';
201//        $result = $this->sql_countries($this->tlimit,$this->start,15);
202//        $this->html_resulttable($result,array('','Countries','Count'));
203        echo '</div>';
204
205        echo '</div>';
206    }
207
208    function html_country(){
209        echo '<div class="plg_stats_full">';
210        echo '<h2>Visitor\'s Countries</h2>';
211        $result = $this->sql_countries($this->tlimit,$this->start,150);
212        $this->html_resulttable($result,array('','Countries','Count'));
213        echo '</div>';
214    }
215
216    function html_page(){
217        echo '<div class="plg_stats_full">';
218        echo '<h2>Popular Pages</h2>';
219        $result = $this->sql_pages($this->tlimit,$this->start,150);
220        $this->html_resulttable($result,array('Page','Count'));
221        echo '</div>';
222    }
223
224    function html_referer(){
225        echo '<div class="plg_stats_full">';
226        echo '<h2>Incoming Links</h2>';
227        $result = $this->sql_referer($this->tlimit,$this->start,150);
228        $this->html_resulttable($result,array('Incoming Link','Count'));
229        echo '</div>';
230    }
231
232
233
234    /**
235     * Display a result in a HTML table
236     */
237    function html_resulttable($result,$header){
238        echo '<table>';
239        echo '<tr>';
240        foreach($header as $h){
241            echo '<th>'.hsc($h).'</th>';
242        }
243        echo '</tr>';
244
245        foreach($result as $row){
246            echo '<tr>';
247            foreach($row as $k => $v){
248                echo '<td class="stats_'.$k.'">';
249                if($k == 'page'){
250                    echo '<a href="'.wl($v).'" class="wikilink1">';
251                    echo hsc($v);
252                    echo '</a>';
253                }elseif($k == 'url'){
254                    $url = hsc($v);
255                    if(strlen($url) > 50){
256                        $url = substr($url,0,30).' &hellip; '.substr($url,-20);
257                    }
258                    echo '<a href="'.$v.'" class="urlextern">';
259                    echo $url;
260                    echo '</a>';
261                }elseif($k == 'html'){
262                    echo $v;
263                }elseif($k == 'cflag'){
264                    echo '<img src="'.DOKU_BASE.'lib/plugins/statistics/flags/'.hsc($v).'.png" alt="'.hsc($v).'" width="18" height="12"/>';
265                }else{
266                    echo hsc($v);
267                }
268                echo '</td>';
269            }
270            echo '</tr>';
271        }
272        echo '</table>';
273    }
274
275    /**
276     * Create an image
277     */
278    function img_build($img){
279        include(dirname(__FILE__).'/inc/AGC.class.php');
280
281        switch($img){
282            case 'country':
283                // build top countries + other
284                $result = $this->sql_countries($this->tlimit,$this->start,0);
285                $data = array();
286                $top = 0;
287                foreach($result as $row){
288                    if($top < 7){
289                        $data[$row['country']] = $row['cnt'];
290                    }else{
291                        $data['other'] += $row['cnt'];
292                    }
293                    $top++;
294                }
295                $pie = new AGC(300, 200);
296                $pie->setProp("showkey",true);
297                $pie->setProp("showval",false);
298                $pie->setProp("showgrid",false);
299                $pie->setProp("type","pie");
300                $pie->setProp("keyinfo",1);
301                $pie->setProp("keysize",8);
302                $pie->setProp("keywidspc",-50);
303                $pie->setProp("key",array_keys($data));
304                $pie->addBulkPoints(array_values($data));
305                @$pie->graph();
306                $pie->showGraph();
307                break;
308            default:
309                $this->sendGIF();
310        }
311    }
312
313
314    function sql_pages($tlimit,$start=0,$limit=20){
315        $sql = "SELECT page, COUNT(*) as cnt
316                  FROM ".$this->getConf('db_prefix')."access as A
317                 WHERE $tlimit
318                   AND ua_type = 'browser'
319              GROUP BY page
320              ORDER BY cnt DESC, page".
321              $this->sql_limit($start,$limit);
322        return $this->runSQL($sql);
323    }
324
325    function sql_referer($tlimit,$start=0,$limit=20){
326        $sql = "SELECT ref as url, COUNT(*) as cnt
327                  FROM ".$this->getConf('db_prefix')."access as A
328                 WHERE $tlimit
329                   AND ua_type = 'browser'
330                   AND ref_type = 'external'
331              GROUP BY ref_md5
332              ORDER BY cnt DESC, url".
333              $this->sql_limit($start,$limit);
334        return $this->runSQL($sql);
335    }
336
337    function sql_countries($tlimit,$start=0,$limit=20){
338        $sql = "SELECT B.code AS cflag, B.country, COUNT(*) as cnt
339                  FROM ".$this->getConf('db_prefix')."access as A,
340                       ".$this->getConf('db_prefix')."iplocation as B
341                 WHERE $tlimit
342                   AND A.ip = B.ip
343              GROUP BY B.country
344              ORDER BY cnt DESC, B.country".
345              $this->sql_limit($start,$limit);
346        return $this->runSQL($sql);
347    }
348
349    /**
350     * Builds a limit clause
351     */
352    function sql_limit($start,$limit){
353        $start = (int) $start;
354        $limit = (int) $limit;
355        if($limit){
356            return " LIMIT $start,$limit";
357        }elseif($start){
358            return " OFFSET $start";
359        }
360        return '';
361    }
362
363    /**
364     * Return a link to the DB, opening the connection if needed
365     */
366    function dbLink(){
367        // connect to DB if needed
368        if(!$this->dblink){
369            $this->dblink = mysql_connect($this->getConf('db_server'),
370                                          $this->getConf('db_user'),
371                                          $this->getConf('db_password'));
372            if(!$this->dblink){
373                msg('DB Error: connection failed',-1);
374                return null;
375            }
376            // set utf-8
377            if(!mysql_db_query($this->getConf('db_database'),'set names utf8',$this->dblink)){
378                msg('DB Error: could not set UTF-8 ('.mysql_error($this->dblink).')',-1);
379                return null;
380            }
381        }
382        return $this->dblink;
383    }
384
385    /**
386     * Simple function to run a DB query
387     */
388    function runSQL($sql_string) {
389        $link = $this->dbLink();
390
391        $result = mysql_db_query($this->conf['db_database'],$sql_string,$link);
392        if(!$result){
393            msg('DB Error: '.mysql_error($link),-1);
394            return null;
395        }
396
397        $resultarray = array();
398
399        //mysql_db_query returns 1 on a insert statement -> no need to ask for results
400        if ($result != 1) {
401            for($i=0; $i< mysql_num_rows($result); $i++) {
402                $temparray = mysql_fetch_assoc($result);
403                $resultarray[]=$temparray;
404            }
405            mysql_free_result($result);
406        }
407
408        if (mysql_insert_id($link)) {
409            $resultarray = mysql_insert_id($link); //give back ID on insert
410        }
411
412        return $resultarray;
413    }
414
415    /**
416     * Returns a short name for a User Agent and sets type, version and os info
417     */
418    function ua_info($ua,&$type,&$ver,&$os){
419        $ua = strtr($ua,' +','__');
420        $ua = strtolower($ua);
421
422        // common browsers
423        $regvermsie     = '/msie([+_ ]|)([\d\.]*)/i';
424        $regvernetscape = '/netscape.?\/([\d\.]*)/i';
425        $regverfirefox  = '/firefox\/([\d\.]*)/i';
426        $regversvn      = '/svn\/([\d\.]*)/i';
427        $regvermozilla  = '/mozilla(\/|)([\d\.]*)/i';
428        $regnotie       = '/webtv|omniweb|opera/i';
429        $regnotnetscape = '/gecko|compatible|opera|galeon|safari/i';
430
431        $name = '';
432        # IE ?
433        if(preg_match($regvermsie,$ua,$m) && !preg_match($regnotie,$ua)){
434            $type = 'browser';
435            $ver  = $m[2];
436            $name = 'msie';
437        }
438        # Firefox ?
439        elseif (preg_match($regverfirefox,$ua,$m)){
440            $type = 'browser';
441            $ver  = $m[1];
442            $name = 'firefox';
443        }
444        # Subversion ?
445        elseif (preg_match($regversvn,$ua,$m)){
446            $type = 'rcs';
447            $ver  = $m[1];
448            $name = 'svn';
449        }
450        # Netscape 6.x, 7.x ... ?
451        elseif (preg_match($regvernetscape,$ua,$m)){
452            $type = 'browser';
453            $ver  = $m[1];
454            $name = 'netscape';
455        }
456        # Netscape 3.x, 4.x ... ?
457        elseif(preg_match($regvermozilla,$ua,$m) && !preg_match($regnotnetscape,$ua)){
458            $type = 'browser';
459            $ver  = $m[2];
460            $name = 'netscape';
461        }else{
462            include(dirname(__FILE__).'/inc/browsers.php');
463            foreach($BrowsersSearchIDOrder as $regex){
464                if(preg_match('/'.$regex.'/',$ua)){
465                    // it's a browser!
466                    $type = 'browser';
467                    $name = strtolower($regex);
468                    break;
469                }
470            }
471        }
472
473        // check OS for browsers
474        if($type == 'browser'){
475            include(dirname(__FILE__).'/inc/operating_systems.php');
476            foreach($OSSearchIDOrder as $regex){
477                if(preg_match('/'.$regex.'/',$ua)){
478                    $os = $OSHashID[$regex];
479                    break;
480                }
481            }
482
483        }
484
485        // are we done now?
486        if($name) return $name;
487
488        include(dirname(__FILE__).'/inc/robots.php');
489        foreach($RobotsSearchIDOrder as $regex){
490            if(preg_match('/'.$regex.'/',$ua)){
491                    // it's a robot!
492                    $type = 'robot';
493                    return strtolower($regex);
494            }
495        }
496
497        // dunno
498        return '';
499    }
500
501    /**
502     *
503     * @fixme: put search engine queries in seperate table here
504     */
505    function log_search($referer,&$type){
506        $referer = strtr($referer,' +','__');
507        $referer = strtolower($referer);
508
509        include(dirname(__FILE__).'/inc/search_engines.php');
510
511        foreach($SearchEnginesSearchIDOrder as $regex){
512            if(preg_match('/'.$regex.'/',$referer)){
513                if(!$NotSearchEnginesKeys[$regex] ||
514                   !preg_match('/'.$NotSearchEnginesKeys[$regex].'/',$referer)){
515                    // it's a search engine!
516                    $type = 'search';
517                    break;
518                }
519            }
520        }
521        if($type != 'search') return; // we're done here
522
523        #fixme now do the keyword magic!
524    }
525
526    /**
527     * Resolve IP to country/city
528     */
529    function log_ip($ip){
530        // check if IP already known and up-to-date
531        $sql = "SELECT ip
532                  FROM ".$this->getConf('db_prefix')."iplocation
533                 WHERE ip ='".addslashes($ip)."'
534                   AND lastupd > DATE_SUB(CURDATE(),INTERVAL 30 DAY)";
535        $result = $this->runSQL($sql);
536        if($result[0]['ip']) return;
537
538        $http = new DokuHTTPClient();
539        $http->timeout = 10;
540        $data = $http->get('http://api.hostip.info/get_html.php?ip='.$ip);
541
542        if(preg_match('/^Country: (.*?) \((.*?)\)\nCity: (.*?)$/s',$data,$match)){
543            $country = addslashes(trim($match[1]));
544            $code    = addslashes(strtolower(trim($match[2])));
545            $city    = addslashes(trim($match[3]));
546            $host    = addslashes(gethostbyaddr($ip));
547            $ip      = addslashes($ip);
548
549            $sql = "REPLACE INTO ".$this->getConf('db_prefix')."iplocation
550                        SET ip = '$ip',
551                            country = '$country',
552                            code    = '$code',
553                            city    = '$city',
554                            host    = '$host'";
555            $this->runSQL($sql);
556        }
557    }
558
559    /**
560     * log a page access
561     *
562     * called from log.php
563     */
564    function log_access(){
565        if(!$_REQUEST['p']) return;
566
567        # FIXME check referer against blacklist and drop logging for bad boys
568
569        // handle referer
570        $referer = trim($_REQUEST['r']);
571        if($referer){
572            $ref     = addslashes($referer);
573            $ref_md5 = ($ref) ? md5($referer) : '';
574            if(strpos($referer,DOKU_URL) === 0){
575                $ref_type = 'internal';
576            }else{
577                $ref_type = 'external';
578                $this->log_search($referer,$ref_type);
579            }
580        }else{
581            $ref      = '';
582            $ref_md5  = '';
583            $ref_type = '';
584        }
585
586        // handle user agent
587        $agent   = trim($_SERVER['HTTP_USER_AGENT']);
588
589        $ua      = addslashes($agent);
590        $ua_type = '';
591        $ua_ver  = '';
592        $os      = '';
593        $ua_info = addslashes($this->ua_info($agent,$ua_type,$ua_ver,$os));
594
595        $page    = addslashes($_REQUEST['p']);
596        $ip      = addslashes($_SERVER['REMOTE_ADDR']);
597        $sx      = (int) $_REQUEST['sx'];
598        $sy      = (int) $_REQUEST['sy'];
599        $vx      = (int) $_REQUEST['vx'];
600        $vy      = (int) $_REQUEST['vy'];
601        $user    = addslashes($_SERVER['REMOTE_USER']);
602        $session = addslashes(session_id());
603
604        $sql  = "INSERT DELAYED INTO ".$this->getConf('db_prefix')."access
605                    SET page     = '$page',
606                        ip       = '$ip',
607                        ua       = '$ua',
608                        ua_info  = '$ua_info',
609                        ua_type  = '$ua_type',
610                        ua_ver   = '$ua_ver',
611                        os       = '$os',
612                        ref      = '$ref',
613                        ref_md5  = '$ref_md5',
614                        ref_type = '$ref_type',
615                        screen_x = '$sx',
616                        screen_y = '$sy',
617                        view_x   = '$vx',
618                        view_y   = '$vy',
619                        user     = '$user',
620                        session  = '$session'";
621        $ok = $this->runSQL($sql);
622        if(is_null($ok)){
623            global $MSG;
624            print_r($MSG);
625        }
626
627        // resolve the IP
628        $this->log_ip($_SERVER['REMOTE_ADDR']);
629    }
630
631    /**
632     * Just send a 1x1 pixel blank gif to the browser
633     *
634     * @called from log.php
635     *
636     * @author Andreas Gohr <andi@splitbrain.org>
637     * @author Harry Fuecks <fuecks@gmail.com>
638     */
639    function sendGIF(){
640        $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7');
641        header('Content-Type: image/gif');
642        header('Content-Length: '.strlen($img));
643        header('Connection: Close');
644        print $img;
645        flush();
646        // Browser should drop connection after this
647        // Thinks it's got the whole image
648    }
649
650}
651