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