xref: /plugin/statistics/admin.php (revision 264f174475038a470b3d48b20967a072539aa5db)
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    function html_dashboard(){
137        echo '<div class="plg_stats_dashboard">';
138
139
140        // top pages today
141        echo '<div>';
142        echo '<h2>Most popular pages</h2>';
143        $sql = "SELECT page, COUNT(*) as cnt
144                  FROM ".$this->getConf('db_prefix')."access as A
145                 WHERE ".$this->tlimit."
146                   AND ua_type = 'browser'
147              GROUP BY page
148              ORDER BY cnt DESC, page
149                 LIMIT 20";
150        $result = $this->runSQL($sql);
151        $this->html_resulttable($result,array('Pages','Count'));
152        echo '</div>';
153
154        // top referer today
155        echo '<div>';
156        echo '<h2>Top incoming links</h2>';
157        $sql = "SELECT ref as url, COUNT(*) as cnt
158                  FROM ".$this->getConf('db_prefix')."access as A
159                 WHERE ".$this->tlimit."
160                   AND ua_type = 'browser'
161                   AND ref_type = 'external'
162              GROUP BY ref_md5
163              ORDER BY cnt DESC, url
164                 LIMIT 20";
165        $result = $this->runSQL($sql);
166        $this->html_resulttable($result,array('Incoming Links','Count'));
167        echo '</div>';
168
169        // top countries today
170        echo '<div>';
171        echo '<h2>Visitor\'s top countries</h2>';
172        $sql = "SELECT B.country, COUNT(*) as cnt
173                  FROM ".$this->getConf('db_prefix')."access as A,
174                       ".$this->getConf('db_prefix')."iplocation as B
175                 WHERE ".$this->tlimit."
176                   AND A.ip = B.ip
177              GROUP BY B.country
178              ORDER BY cnt DESC, B.country
179                 LIMIT 20";
180        $result = $this->runSQL($sql);
181        $this->html_resulttable($result,array('Countries','Count'));
182        echo '</div>';
183
184        echo '</div>';
185    }
186
187    /**
188     * Display a result in a HTML table
189     */
190    function html_resulttable($result,$header){
191        echo '<table>';
192        echo '<tr>';
193        foreach($header as $h){
194            echo '<th>'.hsc($h).'</th>';
195        }
196        echo '</tr>';
197
198        foreach($result as $row){
199            echo '<tr>';
200            foreach($row as $k => $v){
201                echo '<td class="stats_'.$k.'">';
202                if($k == 'page'){
203                    echo '<a href="'.wl($v).'" class="wikilink1">';
204                    echo hsc($v);
205                    echo '</a>';
206                }elseif($k == 'url'){
207                    $url = hsc($v);
208                    if(strlen($url) > 50){
209                        $url = substr($url,0,30).' &hellip; '.substr($url,-20);
210                    }
211                    echo '<a href="'.$v.'" class="urlextern">';
212                    echo $url;
213                    echo '</a>';
214                }elseif($k == 'html'){
215                    echo $v;
216                }else{
217                    echo hsc($v);
218                }
219                echo '</td>';
220            }
221            echo '</tr>';
222        }
223        echo '</table>';
224    }
225
226
227    /**
228     * Return a link to the DB, opening the connection if needed
229     */
230    function dbLink(){
231        // connect to DB if needed
232        if(!$this->dblink){
233            $this->dblink = mysql_connect($this->getConf('db_server'),
234                                          $this->getConf('db_user'),
235                                          $this->getConf('db_password'));
236            if(!$this->dblink){
237                msg('DB Error: connection failed',-1);
238                return null;
239            }
240            // set utf-8
241            if(!mysql_db_query($this->getConf('db_database'),'set names utf8',$this->dblink)){
242                msg('DB Error: could not set UTF-8 ('.mysql_error($this->dblink).')',-1);
243                return null;
244            }
245        }
246        return $this->dblink;
247    }
248
249    /**
250     * Simple function to run a DB query
251     */
252    function runSQL($sql_string) {
253        $link = $this->dbLink();
254
255        $result = mysql_db_query($this->conf['db_database'],$sql_string,$link);
256        if(!$result){
257            msg('DB Error: '.mysql_error($link),-1);
258            return null;
259        }
260
261        $resultarray = array();
262
263        //mysql_db_query returns 1 on a insert statement -> no need to ask for results
264        if ($result != 1) {
265            for($i=0; $i< mysql_num_rows($result); $i++) {
266                $temparray = mysql_fetch_assoc($result);
267                $resultarray[]=$temparray;
268            }
269            mysql_free_result($result);
270        }
271
272        if (mysql_insert_id($link)) {
273            $resultarray = mysql_insert_id($link); //give back ID on insert
274        }
275
276        return $resultarray;
277    }
278
279    /**
280     * Returns a short name for a User Agent and sets type, version and os info
281     */
282    function ua_info($ua,&$type,&$ver,&$os){
283        $ua = strtr($ua,' +','__');
284        $ua = strtolower($ua);
285
286        // common browsers
287        $regvermsie     = '/msie([+_ ]|)([\d\.]*)/i';
288        $regvernetscape = '/netscape.?\/([\d\.]*)/i';
289        $regverfirefox  = '/firefox\/([\d\.]*)/i';
290        $regversvn      = '/svn\/([\d\.]*)/i';
291        $regvermozilla  = '/mozilla(\/|)([\d\.]*)/i';
292        $regnotie       = '/webtv|omniweb|opera/i';
293        $regnotnetscape = '/gecko|compatible|opera|galeon|safari/i';
294
295        $name = '';
296        # IE ?
297        if(preg_match($regvermsie,$ua,$m) && !preg_match($regnotie,$ua)){
298            $type = 'browser';
299            $ver  = $m[2];
300            $name = 'msie';
301        }
302        # Firefox ?
303        elseif (preg_match($regverfirefox,$ua,$m)){
304            $type = 'browser';
305            $ver  = $m[1];
306            $name = 'firefox';
307        }
308        # Subversion ?
309        elseif (preg_match($regversvn,$ua,$m)){
310            $type = 'rcs';
311            $ver  = $m[1];
312            $name = 'svn';
313        }
314        # Netscape 6.x, 7.x ... ?
315        elseif (preg_match($regvernetscape,$ua,$m)){
316            $type = 'browser';
317            $ver  = $m[1];
318            $name = 'netscape';
319        }
320        # Netscape 3.x, 4.x ... ?
321        elseif(preg_match($regvermozilla,$ua,$m) && !preg_match($regnotnetscape,$ua)){
322            $type = 'browser';
323            $ver  = $m[2];
324            $name = 'netscape';
325        }else{
326            include(dirname(__FILE__).'/inc/browsers.php');
327            foreach($BrowsersSearchIDOrder as $regex){
328                if(preg_match('/'.$regex.'/',$ua)){
329                    // it's a browser!
330                    $type = 'browser';
331                    $name = strtolower($regex);
332                    break;
333                }
334            }
335        }
336
337        // check OS for browsers
338        if($type == 'browser'){
339            include(dirname(__FILE__).'/inc/operating_systems.php');
340            foreach($OSSearchIDOrder as $regex){
341                if(preg_match('/'.$regex.'/',$ua)){
342                    $os = $OSHashID[$regex];
343                    break;
344                }
345            }
346
347        }
348
349        // are we done now?
350        if($name) return $name;
351
352        include(dirname(__FILE__).'/inc/robots.php');
353        foreach($RobotsSearchIDOrder as $regex){
354            if(preg_match('/'.$regex.'/',$ua)){
355                    // it's a robot!
356                    $type = 'robot';
357                    return strtolower($regex);
358            }
359        }
360
361        // dunno
362        return '';
363    }
364
365    /**
366     *
367     * @fixme: put search engine queries in seperate table here
368     */
369    function log_search($referer,&$type){
370        $referer = strtr($referer,' +','__');
371        $referer = strtolower($referer);
372
373        include(dirname(__FILE__).'/inc/search_engines.php');
374
375        foreach($SearchEnginesSearchIDOrder as $regex){
376            if(preg_match('/'.$regex.'/',$referer)){
377                if(!$NotSearchEnginesKeys[$regex] ||
378                   !preg_match('/'.$NotSearchEnginesKeys[$regex].'/',$referer)){
379                    // it's a search engine!
380                    $type = 'search';
381                    break;
382                }
383            }
384        }
385        if($type != 'search') return; // we're done here
386
387        #fixme now do the keyword magic!
388    }
389
390    /**
391     * Resolve IP to country/city
392     */
393    function log_ip($ip){
394        // check if IP already known and up-to-date
395        $sql = "SELECT ip
396                  FROM ".$this->getConf('db_prefix')."iplocation
397                 WHERE ip ='".addslashes($ip)."'
398                   AND lastupd > DATE_SUB(CURDATE(),INTERVAL 30 DAY)";
399        $result = $this->runSQL($sql);
400        if($result[0]['ip']) return;
401
402        $http = new DokuHTTPClient();
403        $http->timeout = 10;
404        $data = $http->get('http://api.hostip.info/get_html.php?ip='.$ip);
405
406        if(preg_match('/^Country: (.*?) \((.*?)\)\nCity: (.*?)$/s',$data,$match)){
407            $country = addslashes(trim($match[1]));
408            $code    = addslashes(strtolower(trim($match[2])));
409            $city    = addslashes(trim($match[3]));
410            $host    = addslashes(gethostbyaddr($ip));
411            $ip      = addslashes($ip);
412
413            $sql = "REPLACE INTO ".$this->getConf('db_prefix')."iplocation
414                        SET ip = '$ip',
415                            country = '$country',
416                            code    = '$code',
417                            city    = '$city',
418                            host    = '$host'";
419            $this->runSQL($sql);
420        }
421    }
422
423    /**
424     * log a page access
425     *
426     * called from log.php
427     */
428    function log_access(){
429        if(!$_REQUEST['p']) return;
430
431        # FIXME check referer against blacklist and drop logging for bad boys
432
433        // handle referer
434        $referer = trim($_REQUEST['r']);
435        if($referer){
436            $ref     = addslashes($referer);
437            $ref_md5 = ($ref) ? md5($referer) : '';
438            if(strpos($referer,DOKU_URL) === 0){
439                $ref_type = 'internal';
440            }else{
441                $ref_type = 'external';
442                $this->log_search($referer,$ref_type);
443            }
444        }else{
445            $ref      = '';
446            $ref_md5  = '';
447            $ref_type = '';
448        }
449
450        // handle user agent
451        $agent   = trim($_SERVER['HTTP_USER_AGENT']);
452
453        $ua      = addslashes($agent);
454        $ua_type = '';
455        $ua_ver  = '';
456        $os      = '';
457        $ua_info = addslashes($this->ua_info($agent,$ua_type,$ua_ver,$os));
458
459        $page    = addslashes($_REQUEST['p']);
460        $ip      = addslashes($_SERVER['REMOTE_ADDR']);
461        $sx      = (int) $_REQUEST['sx'];
462        $sy      = (int) $_REQUEST['sy'];
463        $vx      = (int) $_REQUEST['vx'];
464        $vy      = (int) $_REQUEST['vy'];
465        $user    = addslashes($_SERVER['REMOTE_USER']);
466        $session = addslashes(session_id());
467
468        $sql  = "INSERT DELAYED INTO ".$this->getConf('db_prefix')."access
469                    SET page     = '$page',
470                        ip       = '$ip',
471                        ua       = '$ua',
472                        ua_info  = '$ua_info',
473                        ua_type  = '$ua_type',
474                        ua_ver   = '$ua_ver',
475                        os       = '$os',
476                        ref      = '$ref',
477                        ref_md5  = '$ref_md5',
478                        ref_type = '$ref_type',
479                        screen_x = '$sx',
480                        screen_y = '$sy',
481                        view_x   = '$vx',
482                        view_y   = '$vy',
483                        user     = '$user',
484                        session  = '$session'";
485        $ok = $this->runSQL($sql);
486        if(is_null($ok)){
487            global $MSG;
488            print_r($MSG);
489        }
490
491        // resolve the IP
492        $this->log_ip($_SERVER['REMOTE_ADDR']);
493    }
494
495    /**
496     * Just send a 1x1 pixel blank gif to the browser
497     *
498     * @called from log.php
499     *
500     * @author Andreas Gohr <andi@splitbrain.org>
501     * @author Harry Fuecks <fuecks@gmail.com>
502     */
503    function sendGIF(){
504        $img = base64_decode('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAEALAAAAAABAAEAAAIBTAA7');
505        header('Content-Type: image/gif');
506        header('Content-Length: '.strlen($img));
507        header('Connection: Close');
508        print $img;
509        flush();
510        // Browser should drop connection after this
511        // Thinks it's got the whole image
512    }
513
514}
515