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