xref: /plugin/statistics/action.php (revision 1b4104be445fe3e0a8eef15bec5057039b02f852)
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
234dd555b7SAndreas Gohr        $controller->register_hook('DOKUWIKI_INIT_DONE', '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', []);
297a1a7c58SAndreas 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
49523da372SAndreas Gohr        $session = $_SESSION[DOKU_COOKIE]['statistics'] ?? [];
50523da372SAndreas Gohr
5104928db4SAndreas Gohr        // reset if session is too old
5204928db4SAndreas Gohr        if (time() - ($session['time'] ?? 0) > 60 * 15) {
5304928db4SAndreas Gohr            $session = [];
5404928db4SAndreas Gohr        }
5504928db4SAndreas Gohr        // reset if user agent changed
5604928db4SAndreas Gohr        if ($INPUT->server->str('HTTP_USER_AGENT') != ($session['user_agent'] ?? '')) {
5704928db4SAndreas Gohr            $session = [];
5804928db4SAndreas Gohr        }
5904928db4SAndreas Gohr
6004928db4SAndreas Gohr        // update session data
6104928db4SAndreas Gohr        $session['time'] = time();
6204928db4SAndreas Gohr        $session['user_agent'] = $INPUT->server->str('HTTP_USER_AGENT');
6304928db4SAndreas Gohr        $session['uid'] = get_doku_pref('plgstats', bin2hex(random_bytes(16)));
6404928db4SAndreas Gohr        if (!isset($session['id'])) {
6504928db4SAndreas Gohr            // generate a new session id if not set
6604928db4SAndreas Gohr            $session['id'] = bin2hex(random_bytes(16));
6704928db4SAndreas Gohr        }
6804928db4SAndreas Gohr
6904928db4SAndreas Gohr        // store session and cookie data
7004928db4SAndreas Gohr        $_SESSION[DOKU_COOKIE]['statistics'] = $session;
71*1b4104beSAndreas Gohr
72*1b4104beSAndreas Gohr        // Workaround for dokuwiki/dokuwiki#4544
73*1b4104beSAndreas Gohr        $old = get_doku_pref('plgstats', false);
74*1b4104beSAndreas Gohr        if ($old !== $session['uid']) {
7504928db4SAndreas Gohr            set_doku_pref('plgstats', $session['uid']);
7604928db4SAndreas Gohr        }
77*1b4104beSAndreas Gohr    }
7804928db4SAndreas Gohr
7904928db4SAndreas Gohr    /**
8014d99ec0SAndreas Gohr     * @fixme call this in the webbug call
8114d99ec0SAndreas Gohr     */
82a8acb244SAndreas Gohr    public function putpixel()
83a8acb244SAndreas Gohr    {
84ed6e7cc1SAndreas Gohr (aider)        global $ID, $INPUT;
8587e0f0b1SAndreas Gohr        $url = DOKU_BASE . 'lib/plugins/statistics/dispatch.php?p=' . rawurlencode($ID) .
86ed6e7cc1SAndreas Gohr (aider)            '&amp;r=' . rawurlencode($INPUT->server->str('HTTP_REFERER')) . '&rnd=' . time();
8714d99ec0SAndreas Gohr
882257e39bSAndreas Gohr        echo '<noscript><img alt="" src="' . $url . '" width="1" height="1" /></noscript>';
8914d99ec0SAndreas Gohr    }
9058511ae8SAndreas Gohr
9158511ae8SAndreas Gohr    /**
925bccfe87SAndreas Gohr     * Log page edits actions
9358511ae8SAndreas Gohr     */
94a8acb244SAndreas Gohr    public function logedits(Event $event, $param)
95a8acb244SAndreas Gohr    {
9658511ae8SAndreas Gohr        if ($event->data[3]) return; // no revision
9758511ae8SAndreas Gohr
9858511ae8SAndreas Gohr        if (file_exists($event->data[0][0])) {
9958511ae8SAndreas Gohr            if ($event->data[0][1] == '') {
10058511ae8SAndreas Gohr                $type = 'D';
10158511ae8SAndreas Gohr            } else {
10258511ae8SAndreas Gohr                $type = 'E';
10358511ae8SAndreas Gohr            }
10458511ae8SAndreas Gohr        } else {
10558511ae8SAndreas Gohr            $type = 'C';
10658511ae8SAndreas Gohr        }
1071664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
10858511ae8SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
10902aa9b73SAndreas Gohr        $hlp->getLogger()->logEdit($event->data[1] . ':' . $event->data[2], $type);
11058511ae8SAndreas Gohr    }
1115bccfe87SAndreas Gohr
1125bccfe87SAndreas Gohr    /**
1135bccfe87SAndreas Gohr     * Log internal search
1145bccfe87SAndreas Gohr     */
115a8acb244SAndreas Gohr    public function logsearch(Event $event, $param)
116a8acb244SAndreas Gohr    {
1171664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1185bccfe87SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
11902aa9b73SAndreas Gohr        $hlp->getLogger()->logSearch($event->data['query'], $event->data['highlight']);
1205bccfe87SAndreas Gohr    }
121b5e880bdSAndreas Gohr
122b5e880bdSAndreas Gohr    /**
123b5e880bdSAndreas Gohr     * Log login/logouts
124b5e880bdSAndreas Gohr     */
125a8acb244SAndreas Gohr    public function loglogins(Event $event, $param)
126a8acb244SAndreas Gohr    {
127ed6e7cc1SAndreas Gohr (aider)        global $INPUT;
128ed6e7cc1SAndreas Gohr (aider)
129b5e880bdSAndreas Gohr        $type = '';
130a5dadbc1SAndreas Gohr        $act = $this->actClean($event->data);
131af93d154SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
132b5e880bdSAndreas Gohr        if ($act == 'logout') {
133af93d154SAndreas Gohr            // logout
134b5e880bdSAndreas Gohr            $type = 'o';
135ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->server->str('REMOTE_USER') && $act == 'login') {
136ed6e7cc1SAndreas Gohr (aider)            if ($INPUT->str('r')) {
137af93d154SAndreas Gohr                // permanent login
138b5e880bdSAndreas Gohr                $type = 'p';
139b5e880bdSAndreas Gohr            } else {
140af93d154SAndreas Gohr                // normal login
141b5e880bdSAndreas Gohr                $type = 'l';
142b5e880bdSAndreas Gohr            }
143ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->str('u') && !$INPUT->str('http_credentials') && !$INPUT->server->str('REMOTE_USER')) {
144af93d154SAndreas Gohr            // failed attempt
145af93d154SAndreas Gohr            $user = $INPUT->str('u');
146b5e880bdSAndreas Gohr            $type = 'f';
147b5e880bdSAndreas Gohr        }
148b5e880bdSAndreas Gohr        if (!$type) return;
149b5e880bdSAndreas Gohr
1501664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
151b5e880bdSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
152af93d154SAndreas Gohr        $hlp->getLogger()->logLogin($type, $user);
15314d99ec0SAndreas Gohr    }
15414d99ec0SAndreas Gohr
155535aeea1SAndreas Gohr    /**
156535aeea1SAndreas Gohr     * Log user creations
157535aeea1SAndreas Gohr     */
158a8acb244SAndreas Gohr    public function logregistration(Event $event, $param)
159a8acb244SAndreas Gohr    {
160535aeea1SAndreas Gohr        if ($event->data['type'] == 'create') {
1611664ba1dSAndreas Gohr            /** @var helper_plugin_statistics $hlp */
162535aeea1SAndreas Gohr            $hlp = plugin_load('helper', 'statistics');
163211caa5dSAndreas Gohr            $hlp->getLogger()->logLogin('C', $event->data['params'][0]);
164535aeea1SAndreas Gohr        }
165535aeea1SAndreas Gohr    }
166b5e880bdSAndreas Gohr
167b5e880bdSAndreas Gohr    /**
1681664ba1dSAndreas Gohr     * Log media access
1691664ba1dSAndreas Gohr     */
170a8acb244SAndreas Gohr    public function logmedia(Event $event, $param)
171a8acb244SAndreas Gohr    {
1721664ba1dSAndreas Gohr        if ($event->data['status'] < 200) return;
1731664ba1dSAndreas Gohr        if ($event->data['status'] >= 400) return;
1741664ba1dSAndreas Gohr        if (preg_match('/^\w+:\/\//', $event->data['media'])) return;
1751664ba1dSAndreas Gohr
1761664ba1dSAndreas Gohr        // no size for redirect/not modified
1771664ba1dSAndreas Gohr        if ($event->data['status'] >= 300) {
1781664ba1dSAndreas Gohr            $size = 0;
1791664ba1dSAndreas Gohr        } else {
18017978b38SAndreas Gohr            $size = @filesize($event->data['file']);
1811664ba1dSAndreas Gohr        }
1821664ba1dSAndreas Gohr
1831664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1841664ba1dSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
185211caa5dSAndreas Gohr        $hlp->getLogger()->logMedia(
1861664ba1dSAndreas Gohr            $event->data['media'],
1871664ba1dSAndreas Gohr            $event->data['mime'],
1881664ba1dSAndreas Gohr            !$event->data['download'],
1891664ba1dSAndreas Gohr            $size
1901664ba1dSAndreas Gohr        );
1911664ba1dSAndreas Gohr    }
1921664ba1dSAndreas Gohr
1931664ba1dSAndreas Gohr    /**
194cae4a1c5SAndreas Gohr     * Log the daily page and media counts for the history
195cae4a1c5SAndreas Gohr     */
196a8acb244SAndreas Gohr    public function loghistory(Event $event, $param)
197a8acb244SAndreas Gohr    {
198cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: started' . DOKU_LF;
199cae4a1c5SAndreas Gohr
200cae4a1c5SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
201cae4a1c5SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
202a9509e05SAndreas Gohr (aider)        $db = $hlp->getDB();
203cae4a1c5SAndreas Gohr
204cae4a1c5SAndreas Gohr        // check if a history was gathered already today
205a9509e05SAndreas Gohr (aider)        $result = $db->queryAll(
20602aa9b73SAndreas Gohr            "SELECT info FROM history WHERE date(dt) = date('now')"
207a9509e05SAndreas Gohr (aider)        );
208cae4a1c5SAndreas Gohr
209cae4a1c5SAndreas Gohr        $page_ran = false;
210cae4a1c5SAndreas Gohr        $media_ran = false;
211cae4a1c5SAndreas Gohr        foreach ($result as $row) {
212cae4a1c5SAndreas Gohr            if ($row['info'] == 'page_count') $page_ran = true;
213cae4a1c5SAndreas Gohr            if ($row['info'] == 'media_count') $media_ran = true;
214cae4a1c5SAndreas Gohr        }
215cae4a1c5SAndreas Gohr
216cae4a1c5SAndreas Gohr        if ($page_ran && $media_ran) {
217cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: nothing to do - finished' . DOKU_LF;
218cae4a1c5SAndreas Gohr            return;
219cae4a1c5SAndreas Gohr        }
220cae4a1c5SAndreas Gohr
221cae4a1c5SAndreas Gohr        $event->stopPropagation();
222cae4a1c5SAndreas Gohr        $event->preventDefault();
223cae4a1c5SAndreas Gohr
224cae4a1c5SAndreas Gohr        if ($page_ran) {
225cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging media' . DOKU_LF;
226211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryMedia();
227cae4a1c5SAndreas Gohr        } else {
228cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging pages' . DOKU_LF;
229211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryPages();
230cae4a1c5SAndreas Gohr        }
231cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: finished' . DOKU_LF;
232cae4a1c5SAndreas Gohr    }
233cae4a1c5SAndreas Gohr
234cae4a1c5SAndreas Gohr    /**
2357a1a7c58SAndreas Gohr     * Prune old data
2367a1a7c58SAndreas Gohr     *
2377a1a7c58SAndreas Gohr     * This is run once a day and removes all data older than the configured
2387a1a7c58SAndreas Gohr     * retention time.
2397a1a7c58SAndreas Gohr     */
2407a1a7c58SAndreas Gohr    public function retention(Event $event, $param)
2417a1a7c58SAndreas Gohr    {
2427a1a7c58SAndreas Gohr        $retention = (int)$this->getConf('retention');
2437a1a7c58SAndreas Gohr        if ($retention <= 0) return;
2447a1a7c58SAndreas Gohr        // pruning is only done once a day
2457a1a7c58SAndreas Gohr        $touch = getCacheName('statistics_retention', '.statistics-retention');
2467a1a7c58SAndreas Gohr        if (file_exists($touch) && time() - filemtime($touch) < 24 * 3600) {
2477a1a7c58SAndreas Gohr            return;
2487a1a7c58SAndreas Gohr        }
2497a1a7c58SAndreas Gohr
2507a1a7c58SAndreas Gohr        $event->stopPropagation();
2517a1a7c58SAndreas Gohr        $event->preventDefault();
2527a1a7c58SAndreas Gohr
2537a1a7c58SAndreas Gohr        // these are the tables to be pruned
2547a1a7c58SAndreas Gohr        $tables = [
2557a1a7c58SAndreas Gohr            'edits',
2567a1a7c58SAndreas Gohr            'history',
2577a1a7c58SAndreas Gohr            'iplocation',
2587a1a7c58SAndreas Gohr            'logins',
2597a1a7c58SAndreas Gohr            'media',
2607a1a7c58SAndreas Gohr            'outlinks',
2617a1a7c58SAndreas Gohr            'pageviews',
2627a1a7c58SAndreas Gohr            'referers',
2637a1a7c58SAndreas Gohr            'search',
2647a1a7c58SAndreas Gohr            'sessions',
2657a1a7c58SAndreas Gohr        ];
2667a1a7c58SAndreas Gohr
2677a1a7c58SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
2687a1a7c58SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
2697a1a7c58SAndreas Gohr        $db = $hlp->getDB();
2707a1a7c58SAndreas Gohr
2717a1a7c58SAndreas Gohr        $db->getPdo()->beginTransaction();
2727a1a7c58SAndreas Gohr        foreach ($tables as $table) {
2737a1a7c58SAndreas Gohr            echo "Plugin Statistics: pruning $table" . DOKU_LF;
2747a1a7c58SAndreas Gohr            $db->exec(
2757a1a7c58SAndreas Gohr                "DELETE FROM $table WHERE dt < datetime('now', '-$retention days')"
2767a1a7c58SAndreas Gohr            );
2777a1a7c58SAndreas Gohr        }
2787a1a7c58SAndreas Gohr        $db->getPdo()->commit();
2797a1a7c58SAndreas Gohr
2807a1a7c58SAndreas Gohr        echo "Plugin Statistics: Optimizing" . DOKU_LF;
2817a1a7c58SAndreas Gohr        $db->exec('VACUUM');
2827a1a7c58SAndreas Gohr
2837a1a7c58SAndreas Gohr        // touch the retention file to prevent multiple runs
2847a1a7c58SAndreas Gohr        io_saveFile($touch, dformat());
2857a1a7c58SAndreas Gohr    }
2867a1a7c58SAndreas Gohr
2877a1a7c58SAndreas Gohr    /**
288b5e880bdSAndreas Gohr     * Pre-Sanitize the action command
289b5e880bdSAndreas Gohr     *
290b5e880bdSAndreas Gohr     * Similar to act_clean in action.php but simplified and without
291b5e880bdSAndreas Gohr     * error messages
292b5e880bdSAndreas Gohr     */
293a5dadbc1SAndreas Gohr    protected function actClean($act)
294a8acb244SAndreas Gohr    {
295b5e880bdSAndreas Gohr        // check if the action was given as array key
296b5e880bdSAndreas Gohr        if (is_array($act)) {
297a8acb244SAndreas Gohr            [$act] = array_keys($act);
298b5e880bdSAndreas Gohr        }
299b5e880bdSAndreas Gohr
300b5e880bdSAndreas Gohr        //remove all bad chars
301b5e880bdSAndreas Gohr        $act = strtolower($act);
302b5e880bdSAndreas Gohr        $act = preg_replace('/[^a-z_]+/', '', $act);
303b5e880bdSAndreas Gohr
304b5e880bdSAndreas Gohr        return $act;
305b5e880bdSAndreas Gohr    }
306b5e880bdSAndreas Gohr}
307