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