xref: /plugin/statistics/action.php (revision 7a1a7c58b8012671597983163b5701969480e69a)
114d99ec0SAndreas Gohr<?php
2a8acb244SAndreas Gohr
3a8acb244SAndreas Gohruse dokuwiki\Extension\ActionPlugin;
4a8acb244SAndreas Gohruse dokuwiki\Extension\Event;
504928db4SAndreas Gohruse dokuwiki\Extension\EventHandler;
6a8acb244SAndreas Gohr
714d99ec0SAndreas Gohr/**
814d99ec0SAndreas Gohr *
914d99ec0SAndreas Gohr * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
1014d99ec0SAndreas Gohr * @author     Andreas Gohr <gohr@cosmocode.de>
1114d99ec0SAndreas Gohr */
12a8acb244SAndreas Gohrclass action_plugin_statistics extends ActionPlugin
13a8acb244SAndreas Gohr{
1414d99ec0SAndreas Gohr    /**
1514d99ec0SAndreas Gohr     * register the eventhandlers and initialize some options
1614d99ec0SAndreas Gohr     */
17a8acb244SAndreas Gohr    public function register(EventHandler $controller)
18a8acb244SAndreas Gohr    {
19eabe0d07SAndreas Gohr        global $JSINFO;
20eabe0d07SAndreas Gohr        global $ACT;
21eabe0d07SAndreas Gohr        $JSINFO['act'] = $ACT;
2214d99ec0SAndreas Gohr
2304928db4SAndreas Gohr        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'initSession', []);
2402aa9b73SAndreas Gohr        // FIXME new save event might be better:
252257e39bSAndreas Gohr        $controller->register_hook('IO_WIKIPAGE_WRITE', 'BEFORE', $this, 'logedits', []);
262257e39bSAndreas Gohr        $controller->register_hook('SEARCH_QUERY_FULLPAGE', 'AFTER', $this, 'logsearch', []);
272257e39bSAndreas Gohr        $controller->register_hook('FETCH_MEDIA_STATUS', 'BEFORE', $this, 'logmedia', []);
282257e39bSAndreas Gohr        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'loghistory', []);
29*7a1a7c58SAndreas Gohr        $controller->register_hook('INDEXER_TASKS_RUN', 'AFTER', $this, 'retention', []);
30d550a4adSAndreas Gohr
31d550a4adSAndreas Gohr        // log registration and login/logout actionsonly when user tracking is enabled
32d550a4adSAndreas Gohr        if (!$this->getConf('nousers')) {
33d550a4adSAndreas Gohr            $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'loglogins', []);
34d550a4adSAndreas Gohr            $controller->register_hook('AUTH_USER_CHANGE', 'AFTER', $this, 'logregistration', []);
35d550a4adSAndreas Gohr        }
3614d99ec0SAndreas Gohr    }
3714d99ec0SAndreas Gohr
3814d99ec0SAndreas Gohr    /**
3904928db4SAndreas Gohr     * This ensures we have a session for the statistics plugin
4004928db4SAndreas Gohr     *
4104928db4SAndreas Gohr     * We reset this when the user agent changes or the session is too old
4204928db4SAndreas Gohr     * (15 minutes).
4304928db4SAndreas Gohr     */
4404928db4SAndreas Gohr    public function initSession()
4504928db4SAndreas Gohr    {
4604928db4SAndreas Gohr        global $INPUT;
4704928db4SAndreas Gohr
4804928db4SAndreas Gohr        // load session data
4904928db4SAndreas Gohr        if (isset($_SESSION[DOKU_COOKIE]['statistics'])) {
5004928db4SAndreas Gohr            $session = $_SESSION[DOKU_COOKIE]['statistics'];
5104928db4SAndreas Gohr        } else {
5204928db4SAndreas Gohr            $session = [];
5304928db4SAndreas Gohr        }
5404928db4SAndreas Gohr        // reset if session is too old
5504928db4SAndreas Gohr        if (time() - ($session['time'] ?? 0) > 60 * 15) {
5604928db4SAndreas Gohr            $session = [];
5704928db4SAndreas Gohr        }
5804928db4SAndreas Gohr        // reset if user agent changed
5904928db4SAndreas Gohr        if ($INPUT->server->str('HTTP_USER_AGENT') != ($session['user_agent'] ?? '')) {
6004928db4SAndreas Gohr            $session = [];
6104928db4SAndreas Gohr        }
6204928db4SAndreas Gohr
6304928db4SAndreas Gohr        // update session data
6404928db4SAndreas Gohr        $session['time'] = time();
6504928db4SAndreas Gohr        $session['user_agent'] = $INPUT->server->str('HTTP_USER_AGENT');
6604928db4SAndreas Gohr        $session['uid'] = get_doku_pref('plgstats', bin2hex(random_bytes(16)));
6704928db4SAndreas Gohr        if (!isset($session['id'])) {
6804928db4SAndreas Gohr            // generate a new session id if not set
6904928db4SAndreas Gohr            $session['id'] = bin2hex(random_bytes(16));
7004928db4SAndreas Gohr        }
7104928db4SAndreas Gohr
7204928db4SAndreas Gohr        // store session and cookie data
7304928db4SAndreas Gohr        $_SESSION[DOKU_COOKIE]['statistics'] = $session;
7404928db4SAndreas Gohr        set_doku_pref('plgstats', $session['uid']);
7504928db4SAndreas Gohr    }
7604928db4SAndreas Gohr
7704928db4SAndreas Gohr    /**
7814d99ec0SAndreas Gohr     * @fixme call this in the webbug call
7914d99ec0SAndreas Gohr     */
80a8acb244SAndreas Gohr    public function putpixel()
81a8acb244SAndreas Gohr    {
82ed6e7cc1SAndreas Gohr (aider)        global $ID, $INPUT;
8387e0f0b1SAndreas Gohr        $url = DOKU_BASE . 'lib/plugins/statistics/dispatch.php?p=' . rawurlencode($ID) .
84ed6e7cc1SAndreas Gohr (aider)            '&amp;r=' . rawurlencode($INPUT->server->str('HTTP_REFERER')) . '&rnd=' . time();
8514d99ec0SAndreas Gohr
862257e39bSAndreas Gohr        echo '<noscript><img alt="" src="' . $url . '" width="1" height="1" /></noscript>';
8714d99ec0SAndreas Gohr    }
8858511ae8SAndreas Gohr
8958511ae8SAndreas Gohr    /**
905bccfe87SAndreas Gohr     * Log page edits actions
9158511ae8SAndreas Gohr     */
92a8acb244SAndreas Gohr    public function logedits(Event $event, $param)
93a8acb244SAndreas Gohr    {
9458511ae8SAndreas Gohr        if ($event->data[3]) return; // no revision
9558511ae8SAndreas Gohr
9658511ae8SAndreas Gohr        if (file_exists($event->data[0][0])) {
9758511ae8SAndreas Gohr            if ($event->data[0][1] == '') {
9858511ae8SAndreas Gohr                $type = 'D';
9958511ae8SAndreas Gohr            } else {
10058511ae8SAndreas Gohr                $type = 'E';
10158511ae8SAndreas Gohr            }
10258511ae8SAndreas Gohr        } else {
10358511ae8SAndreas Gohr            $type = 'C';
10458511ae8SAndreas Gohr        }
1051664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
10658511ae8SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
10702aa9b73SAndreas Gohr        $hlp->getLogger()->logEdit($event->data[1] . ':' . $event->data[2], $type);
10858511ae8SAndreas Gohr    }
1095bccfe87SAndreas Gohr
1105bccfe87SAndreas Gohr    /**
1115bccfe87SAndreas Gohr     * Log internal search
1125bccfe87SAndreas Gohr     */
113a8acb244SAndreas Gohr    public function logsearch(Event $event, $param)
114a8acb244SAndreas Gohr    {
1151664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1165bccfe87SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
11702aa9b73SAndreas Gohr        $hlp->getLogger()->logSearch($event->data['query'], $event->data['highlight']);
1185bccfe87SAndreas Gohr    }
119b5e880bdSAndreas Gohr
120b5e880bdSAndreas Gohr    /**
121b5e880bdSAndreas Gohr     * Log login/logouts
122b5e880bdSAndreas Gohr     */
123a8acb244SAndreas Gohr    public function loglogins(Event $event, $param)
124a8acb244SAndreas Gohr    {
125ed6e7cc1SAndreas Gohr (aider)        global $INPUT;
126ed6e7cc1SAndreas Gohr (aider)
127b5e880bdSAndreas Gohr        $type = '';
128a5dadbc1SAndreas Gohr        $act = $this->actClean($event->data);
129af93d154SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
130b5e880bdSAndreas Gohr        if ($act == 'logout') {
131af93d154SAndreas Gohr            // logout
132b5e880bdSAndreas Gohr            $type = 'o';
133ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->server->str('REMOTE_USER') && $act == 'login') {
134ed6e7cc1SAndreas Gohr (aider)            if ($INPUT->str('r')) {
135af93d154SAndreas Gohr                // permanent login
136b5e880bdSAndreas Gohr                $type = 'p';
137b5e880bdSAndreas Gohr            } else {
138af93d154SAndreas Gohr                // normal login
139b5e880bdSAndreas Gohr                $type = 'l';
140b5e880bdSAndreas Gohr            }
141ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->str('u') && !$INPUT->str('http_credentials') && !$INPUT->server->str('REMOTE_USER')) {
142af93d154SAndreas Gohr            // failed attempt
143af93d154SAndreas Gohr            $user = $INPUT->str('u');
144b5e880bdSAndreas Gohr            $type = 'f';
145b5e880bdSAndreas Gohr        }
146b5e880bdSAndreas Gohr        if (!$type) return;
147b5e880bdSAndreas Gohr
1481664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
149b5e880bdSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
150af93d154SAndreas Gohr        $hlp->getLogger()->logLogin($type, $user);
15114d99ec0SAndreas Gohr    }
15214d99ec0SAndreas Gohr
153535aeea1SAndreas Gohr    /**
154535aeea1SAndreas Gohr     * Log user creations
155535aeea1SAndreas Gohr     */
156a8acb244SAndreas Gohr    public function logregistration(Event $event, $param)
157a8acb244SAndreas Gohr    {
158535aeea1SAndreas Gohr        if ($event->data['type'] == 'create') {
1591664ba1dSAndreas Gohr            /** @var helper_plugin_statistics $hlp */
160535aeea1SAndreas Gohr            $hlp = plugin_load('helper', 'statistics');
161211caa5dSAndreas Gohr            $hlp->getLogger()->logLogin('C', $event->data['params'][0]);
162535aeea1SAndreas Gohr        }
163535aeea1SAndreas Gohr    }
164b5e880bdSAndreas Gohr
165b5e880bdSAndreas Gohr    /**
1661664ba1dSAndreas Gohr     * Log media access
1671664ba1dSAndreas Gohr     */
168a8acb244SAndreas Gohr    public function logmedia(Event $event, $param)
169a8acb244SAndreas Gohr    {
1701664ba1dSAndreas Gohr        if ($event->data['status'] < 200) return;
1711664ba1dSAndreas Gohr        if ($event->data['status'] >= 400) return;
1721664ba1dSAndreas Gohr        if (preg_match('/^\w+:\/\//', $event->data['media'])) return;
1731664ba1dSAndreas Gohr
1741664ba1dSAndreas Gohr        // no size for redirect/not modified
1751664ba1dSAndreas Gohr        if ($event->data['status'] >= 300) {
1761664ba1dSAndreas Gohr            $size = 0;
1771664ba1dSAndreas Gohr        } else {
17817978b38SAndreas Gohr            $size = @filesize($event->data['file']);
1791664ba1dSAndreas Gohr        }
1801664ba1dSAndreas Gohr
1811664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1821664ba1dSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
183211caa5dSAndreas Gohr        $hlp->getLogger()->logMedia(
1841664ba1dSAndreas Gohr            $event->data['media'],
1851664ba1dSAndreas Gohr            $event->data['mime'],
1861664ba1dSAndreas Gohr            !$event->data['download'],
1871664ba1dSAndreas Gohr            $size
1881664ba1dSAndreas Gohr        );
1891664ba1dSAndreas Gohr    }
1901664ba1dSAndreas Gohr
1911664ba1dSAndreas Gohr    /**
192cae4a1c5SAndreas Gohr     * Log the daily page and media counts for the history
193cae4a1c5SAndreas Gohr     */
194a8acb244SAndreas Gohr    public function loghistory(Event $event, $param)
195a8acb244SAndreas Gohr    {
196cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: started' . DOKU_LF;
197cae4a1c5SAndreas Gohr
198cae4a1c5SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
199cae4a1c5SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
200a9509e05SAndreas Gohr (aider)        $db = $hlp->getDB();
201cae4a1c5SAndreas Gohr
202cae4a1c5SAndreas Gohr        // check if a history was gathered already today
203a9509e05SAndreas Gohr (aider)        $result = $db->queryAll(
20402aa9b73SAndreas Gohr            "SELECT info FROM history WHERE date(dt) = date('now')"
205a9509e05SAndreas Gohr (aider)        );
206cae4a1c5SAndreas Gohr
207cae4a1c5SAndreas Gohr        $page_ran = false;
208cae4a1c5SAndreas Gohr        $media_ran = false;
209cae4a1c5SAndreas Gohr        foreach ($result as $row) {
210cae4a1c5SAndreas Gohr            if ($row['info'] == 'page_count') $page_ran = true;
211cae4a1c5SAndreas Gohr            if ($row['info'] == 'media_count') $media_ran = true;
212cae4a1c5SAndreas Gohr        }
213cae4a1c5SAndreas Gohr
214cae4a1c5SAndreas Gohr        if ($page_ran && $media_ran) {
215cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: nothing to do - finished' . DOKU_LF;
216cae4a1c5SAndreas Gohr            return;
217cae4a1c5SAndreas Gohr        }
218cae4a1c5SAndreas Gohr
219cae4a1c5SAndreas Gohr        $event->stopPropagation();
220cae4a1c5SAndreas Gohr        $event->preventDefault();
221cae4a1c5SAndreas Gohr
222cae4a1c5SAndreas Gohr        if ($page_ran) {
223cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging media' . DOKU_LF;
224211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryMedia();
225cae4a1c5SAndreas Gohr        } else {
226cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging pages' . DOKU_LF;
227211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryPages();
228cae4a1c5SAndreas Gohr        }
229cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: finished' . DOKU_LF;
230cae4a1c5SAndreas Gohr    }
231cae4a1c5SAndreas Gohr
232cae4a1c5SAndreas Gohr    /**
233*7a1a7c58SAndreas Gohr     * Prune old data
234*7a1a7c58SAndreas Gohr     *
235*7a1a7c58SAndreas Gohr     * This is run once a day and removes all data older than the configured
236*7a1a7c58SAndreas Gohr     * retention time.
237*7a1a7c58SAndreas Gohr     */
238*7a1a7c58SAndreas Gohr    public function retention(Event $event, $param)
239*7a1a7c58SAndreas Gohr    {
240*7a1a7c58SAndreas Gohr        $retention = (int)$this->getConf('retention');
241*7a1a7c58SAndreas Gohr        if ($retention <= 0) return;
242*7a1a7c58SAndreas Gohr        // pruning is only done once a day
243*7a1a7c58SAndreas Gohr        $touch = getCacheName('statistics_retention', '.statistics-retention');
244*7a1a7c58SAndreas Gohr        if (file_exists($touch) && time() - filemtime($touch) < 24 * 3600) {
245*7a1a7c58SAndreas Gohr            return;
246*7a1a7c58SAndreas Gohr        }
247*7a1a7c58SAndreas Gohr
248*7a1a7c58SAndreas Gohr        $event->stopPropagation();
249*7a1a7c58SAndreas Gohr        $event->preventDefault();
250*7a1a7c58SAndreas Gohr
251*7a1a7c58SAndreas Gohr        // these are the tables to be pruned
252*7a1a7c58SAndreas Gohr        $tables = [
253*7a1a7c58SAndreas Gohr            'edits',
254*7a1a7c58SAndreas Gohr            'history',
255*7a1a7c58SAndreas Gohr            'iplocation',
256*7a1a7c58SAndreas Gohr            'logins',
257*7a1a7c58SAndreas Gohr            'media',
258*7a1a7c58SAndreas Gohr            'outlinks',
259*7a1a7c58SAndreas Gohr            'pageviews',
260*7a1a7c58SAndreas Gohr            'referers',
261*7a1a7c58SAndreas Gohr            'search',
262*7a1a7c58SAndreas Gohr            'sessions',
263*7a1a7c58SAndreas Gohr        ];
264*7a1a7c58SAndreas Gohr
265*7a1a7c58SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
266*7a1a7c58SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
267*7a1a7c58SAndreas Gohr        $db = $hlp->getDB();
268*7a1a7c58SAndreas Gohr
269*7a1a7c58SAndreas Gohr        $db->getPdo()->beginTransaction();
270*7a1a7c58SAndreas Gohr        foreach ($tables as $table) {
271*7a1a7c58SAndreas Gohr            echo "Plugin Statistics: pruning $table" . DOKU_LF;
272*7a1a7c58SAndreas Gohr            $db->exec(
273*7a1a7c58SAndreas Gohr                "DELETE FROM $table WHERE dt < datetime('now', '-$retention days')"
274*7a1a7c58SAndreas Gohr            );
275*7a1a7c58SAndreas Gohr        }
276*7a1a7c58SAndreas Gohr        $db->getPdo()->commit();
277*7a1a7c58SAndreas Gohr
278*7a1a7c58SAndreas Gohr        echo "Plugin Statistics: Optimizing" . DOKU_LF;
279*7a1a7c58SAndreas Gohr        $db->exec('VACUUM');
280*7a1a7c58SAndreas Gohr
281*7a1a7c58SAndreas Gohr        // touch the retention file to prevent multiple runs
282*7a1a7c58SAndreas Gohr        io_saveFile($touch, dformat());
283*7a1a7c58SAndreas Gohr    }
284*7a1a7c58SAndreas Gohr
285*7a1a7c58SAndreas Gohr    /**
286b5e880bdSAndreas Gohr     * Pre-Sanitize the action command
287b5e880bdSAndreas Gohr     *
288b5e880bdSAndreas Gohr     * Similar to act_clean in action.php but simplified and without
289b5e880bdSAndreas Gohr     * error messages
290b5e880bdSAndreas Gohr     */
291a5dadbc1SAndreas Gohr    protected function actClean($act)
292a8acb244SAndreas Gohr    {
293b5e880bdSAndreas Gohr        // check if the action was given as array key
294b5e880bdSAndreas Gohr        if (is_array($act)) {
295a8acb244SAndreas Gohr            [$act] = array_keys($act);
296b5e880bdSAndreas Gohr        }
297b5e880bdSAndreas Gohr
298b5e880bdSAndreas Gohr        //remove all bad chars
299b5e880bdSAndreas Gohr        $act = strtolower($act);
300b5e880bdSAndreas Gohr        $act = preg_replace('/[^a-z_]+/', '', $act);
301b5e880bdSAndreas Gohr
302b5e880bdSAndreas Gohr        return $act;
303b5e880bdSAndreas Gohr    }
304b5e880bdSAndreas Gohr}
305