xref: /plugin/statistics/action.php (revision 523da37276acfd8d5f4f6c331a0062602b449fec)
1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\Event;
5use dokuwiki\Extension\EventHandler;
6
7/**
8 *
9 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
10 * @author     Andreas Gohr <gohr@cosmocode.de>
11 */
12class action_plugin_statistics extends ActionPlugin
13{
14    /**
15     * register the eventhandlers and initialize some options
16     */
17    public function register(EventHandler $controller)
18    {
19        global $JSINFO;
20        global $ACT;
21        $JSINFO['act'] = $ACT;
22
23        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'initSession', []);
24        // FIXME new save event might be better:
25        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'logedits', []);
26        $controller->register_hook('SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'logsearch', []);
27        $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE', $this, 'logmedia', []);
28        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'loghistory', []);
29        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'retention', []);
30
31        // log registration and login/logout actionsonly when user tracking is enabled
32        if (!$this->getConf('nousers')) {
33            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'loglogins', []);
34            $controller->register_hook('AUTH_USER_CHANGE', 'AFTER', $this, 'logregistration', []);
35        }
36    }
37
38    /**
39     * This ensures we have a session for the statistics plugin
40     *
41     * We reset this when the user agent changes or the session is too old
42     * (15 minutes).
43     */
44    public function initSession()
45    {
46        global $INPUT;
47
48        // load session data
49        $session = $_SESSION[DOKU_COOKIE]['statistics'] ?? [];
50
51        // reset if session is too old
52        if (time() - ($session['time'] ?? 0) > 60 * 15) {
53            $session = [];
54        }
55        // reset if user agent changed
56        if ($INPUT->server->str('HTTP_USER_AGENT') != ($session['user_agent'] ?? '')) {
57            $session = [];
58        }
59
60        // update session data
61        $session['time'] = time();
62        $session['user_agent'] = $INPUT->server->str('HTTP_USER_AGENT');
63        $session['uid'] = get_doku_pref('plgstats', bin2hex(random_bytes(16)));
64        if (!isset($session['id'])) {
65            // generate a new session id if not set
66            $session['id'] = bin2hex(random_bytes(16));
67        }
68
69        // store session and cookie data
70        $_SESSION[DOKU_COOKIE]['statistics'] = $session;
71        set_doku_pref('plgstats', $session['uid']);
72    }
73
74    /**
75     * @fixme call this in the webbug call
76     */
77    public function putpixel()
78    {
79        global $ID, $INPUT;
80        $url = DOKU_BASE . 'lib/plugins/statistics/dispatch.php?p=' . rawurlencode($ID) .
81            '&amp;r=' . rawurlencode($INPUT->server->str('HTTP_REFERER')) . '&rnd=' . time();
82
83        echo '<noscript><img alt="" src="' . $url . '" width="1" height="1" /></noscript>';
84    }
85
86    /**
87     * Log page edits actions
88     */
89    public function logedits(Event $event, $param)
90    {
91        if ($event->data[3]) return; // no revision
92
93        if (file_exists($event->data[0][0])) {
94            if ($event->data[0][1] == '') {
95                $type = 'D';
96            } else {
97                $type = 'E';
98            }
99        } else {
100            $type = 'C';
101        }
102        /** @var helper_plugin_statistics $hlp */
103        $hlp = plugin_load('helper', 'statistics');
104        $hlp->getLogger()->logEdit($event->data[1] . ':' . $event->data[2], $type);
105    }
106
107    /**
108     * Log internal search
109     */
110    public function logsearch(Event $event, $param)
111    {
112        /** @var helper_plugin_statistics $hlp */
113        $hlp = plugin_load('helper', 'statistics');
114        $hlp->getLogger()->logSearch($event->data['query'], $event->data['highlight']);
115    }
116
117    /**
118     * Log login/logouts
119     */
120    public function loglogins(Event $event, $param)
121    {
122        global $INPUT;
123
124        $type = '';
125        $act = $this->actClean($event->data);
126        $user = $INPUT->server->str('REMOTE_USER');
127        if ($act == 'logout') {
128            // logout
129            $type = 'o';
130        } elseif ($INPUT->server->str('REMOTE_USER') && $act == 'login') {
131            if ($INPUT->str('r')) {
132                // permanent login
133                $type = 'p';
134            } else {
135                // normal login
136                $type = 'l';
137            }
138        } elseif ($INPUT->str('u') && !$INPUT->str('http_credentials') && !$INPUT->server->str('REMOTE_USER')) {
139            // failed attempt
140            $user = $INPUT->str('u');
141            $type = 'f';
142        }
143        if (!$type) return;
144
145        /** @var helper_plugin_statistics $hlp */
146        $hlp = plugin_load('helper', 'statistics');
147        $hlp->getLogger()->logLogin($type, $user);
148    }
149
150    /**
151     * Log user creations
152     */
153    public function logregistration(Event $event, $param)
154    {
155        if ($event->data['type'] == 'create') {
156            /** @var helper_plugin_statistics $hlp */
157            $hlp = plugin_load('helper', 'statistics');
158            $hlp->getLogger()->logLogin('C', $event->data['params'][0]);
159        }
160    }
161
162    /**
163     * Log media access
164     */
165    public function logmedia(Event $event, $param)
166    {
167        if ($event->data['status'] < 200) return;
168        if ($event->data['status'] >= 400) return;
169        if (preg_match('/^\w+:\/\//', $event->data['media'])) return;
170
171        // no size for redirect/not modified
172        if ($event->data['status'] >= 300) {
173            $size = 0;
174        } else {
175            $size = @filesize($event->data['file']);
176        }
177
178        /** @var helper_plugin_statistics $hlp */
179        $hlp = plugin_load('helper', 'statistics');
180        $hlp->getLogger()->logMedia(
181            $event->data['media'],
182            $event->data['mime'],
183            !$event->data['download'],
184            $size
185        );
186    }
187
188    /**
189     * Log the daily page and media counts for the history
190     */
191    public function loghistory(Event $event, $param)
192    {
193        echo 'Plugin Statistics: started' . DOKU_LF;
194
195        /** @var helper_plugin_statistics $hlp */
196        $hlp = plugin_load('helper', 'statistics');
197        $db = $hlp->getDB();
198
199        // check if a history was gathered already today
200        $result = $db->queryAll(
201            "SELECT info FROM history WHERE date(dt) = date('now')"
202        );
203
204        $page_ran = false;
205        $media_ran = false;
206        foreach ($result as $row) {
207            if ($row['info'] == 'page_count') $page_ran = true;
208            if ($row['info'] == 'media_count') $media_ran = true;
209        }
210
211        if ($page_ran && $media_ran) {
212            echo 'Plugin Statistics: nothing to do - finished' . DOKU_LF;
213            return;
214        }
215
216        $event->stopPropagation();
217        $event->preventDefault();
218
219        if ($page_ran) {
220            echo 'Plugin Statistics: logging media' . DOKU_LF;
221            $hlp->getLogger()->logHistoryMedia();
222        } else {
223            echo 'Plugin Statistics: logging pages' . DOKU_LF;
224            $hlp->getLogger()->logHistoryPages();
225        }
226        echo 'Plugin Statistics: finished' . DOKU_LF;
227    }
228
229    /**
230     * Prune old data
231     *
232     * This is run once a day and removes all data older than the configured
233     * retention time.
234     */
235    public function retention(Event $event, $param)
236    {
237        $retention = (int)$this->getConf('retention');
238        if ($retention <= 0) return;
239        // pruning is only done once a day
240        $touch = getCacheName('statistics_retention', '.statistics-retention');
241        if (file_exists($touch) && time() - filemtime($touch) < 24 * 3600) {
242            return;
243        }
244
245        $event->stopPropagation();
246        $event->preventDefault();
247
248        // these are the tables to be pruned
249        $tables = [
250            'edits',
251            'history',
252            'iplocation',
253            'logins',
254            'media',
255            'outlinks',
256            'pageviews',
257            'referers',
258            'search',
259            'sessions',
260        ];
261
262        /** @var helper_plugin_statistics $hlp */
263        $hlp = plugin_load('helper', 'statistics');
264        $db = $hlp->getDB();
265
266        $db->getPdo()->beginTransaction();
267        foreach ($tables as $table) {
268            echo "Plugin Statistics: pruning $table" . DOKU_LF;
269            $db->exec(
270                "DELETE FROM $table WHERE dt < datetime('now', '-$retention days')"
271            );
272        }
273        $db->getPdo()->commit();
274
275        echo "Plugin Statistics: Optimizing" . DOKU_LF;
276        $db->exec('VACUUM');
277
278        // touch the retention file to prevent multiple runs
279        io_saveFile($touch, dformat());
280    }
281
282    /**
283     * Pre-Sanitize the action command
284     *
285     * Similar to act_clean in action.php but simplified and without
286     * error messages
287     */
288    protected function actClean($act)
289    {
290        // check if the action was given as array key
291        if (is_array($act)) {
292            [$act] = array_keys($act);
293        }
294
295        //remove all bad chars
296        $act = strtolower($act);
297        $act = preg_replace('/[^a-z_]+/', '', $act);
298
299        return $act;
300    }
301}
302