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