xref: /plugin/statistics/action.php (revision 523da37276acfd8d5f4f6c331a0062602b449fec)
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', []);
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
49*523da372SAndreas Gohr        $session = $_SESSION[DOKU_COOKIE]['statistics'] ?? [];
50*523da372SAndreas 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;
7104928db4SAndreas Gohr        set_doku_pref('plgstats', $session['uid']);
7204928db4SAndreas Gohr    }
7304928db4SAndreas Gohr
7404928db4SAndreas Gohr    /**
7514d99ec0SAndreas Gohr     * @fixme call this in the webbug call
7614d99ec0SAndreas Gohr     */
77a8acb244SAndreas Gohr    public function putpixel()
78a8acb244SAndreas Gohr    {
79ed6e7cc1SAndreas Gohr (aider)        global $ID, $INPUT;
8087e0f0b1SAndreas Gohr        $url = DOKU_BASE . 'lib/plugins/statistics/dispatch.php?p=' . rawurlencode($ID) .
81ed6e7cc1SAndreas Gohr (aider)            '&amp;r=' . rawurlencode($INPUT->server->str('HTTP_REFERER')) . '&rnd=' . time();
8214d99ec0SAndreas Gohr
832257e39bSAndreas Gohr        echo '<noscript><img alt="" src="' . $url . '" width="1" height="1" /></noscript>';
8414d99ec0SAndreas Gohr    }
8558511ae8SAndreas Gohr
8658511ae8SAndreas Gohr    /**
875bccfe87SAndreas Gohr     * Log page edits actions
8858511ae8SAndreas Gohr     */
89a8acb244SAndreas Gohr    public function logedits(Event $event, $param)
90a8acb244SAndreas Gohr    {
9158511ae8SAndreas Gohr        if ($event->data[3]) return; // no revision
9258511ae8SAndreas Gohr
9358511ae8SAndreas Gohr        if (file_exists($event->data[0][0])) {
9458511ae8SAndreas Gohr            if ($event->data[0][1] == '') {
9558511ae8SAndreas Gohr                $type = 'D';
9658511ae8SAndreas Gohr            } else {
9758511ae8SAndreas Gohr                $type = 'E';
9858511ae8SAndreas Gohr            }
9958511ae8SAndreas Gohr        } else {
10058511ae8SAndreas Gohr            $type = 'C';
10158511ae8SAndreas Gohr        }
1021664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
10358511ae8SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
10402aa9b73SAndreas Gohr        $hlp->getLogger()->logEdit($event->data[1] . ':' . $event->data[2], $type);
10558511ae8SAndreas Gohr    }
1065bccfe87SAndreas Gohr
1075bccfe87SAndreas Gohr    /**
1085bccfe87SAndreas Gohr     * Log internal search
1095bccfe87SAndreas Gohr     */
110a8acb244SAndreas Gohr    public function logsearch(Event $event, $param)
111a8acb244SAndreas Gohr    {
1121664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1135bccfe87SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
11402aa9b73SAndreas Gohr        $hlp->getLogger()->logSearch($event->data['query'], $event->data['highlight']);
1155bccfe87SAndreas Gohr    }
116b5e880bdSAndreas Gohr
117b5e880bdSAndreas Gohr    /**
118b5e880bdSAndreas Gohr     * Log login/logouts
119b5e880bdSAndreas Gohr     */
120a8acb244SAndreas Gohr    public function loglogins(Event $event, $param)
121a8acb244SAndreas Gohr    {
122ed6e7cc1SAndreas Gohr (aider)        global $INPUT;
123ed6e7cc1SAndreas Gohr (aider)
124b5e880bdSAndreas Gohr        $type = '';
125a5dadbc1SAndreas Gohr        $act = $this->actClean($event->data);
126af93d154SAndreas Gohr        $user = $INPUT->server->str('REMOTE_USER');
127b5e880bdSAndreas Gohr        if ($act == 'logout') {
128af93d154SAndreas Gohr            // logout
129b5e880bdSAndreas Gohr            $type = 'o';
130ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->server->str('REMOTE_USER') && $act == 'login') {
131ed6e7cc1SAndreas Gohr (aider)            if ($INPUT->str('r')) {
132af93d154SAndreas Gohr                // permanent login
133b5e880bdSAndreas Gohr                $type = 'p';
134b5e880bdSAndreas Gohr            } else {
135af93d154SAndreas Gohr                // normal login
136b5e880bdSAndreas Gohr                $type = 'l';
137b5e880bdSAndreas Gohr            }
138ed6e7cc1SAndreas Gohr (aider)        } elseif ($INPUT->str('u') && !$INPUT->str('http_credentials') && !$INPUT->server->str('REMOTE_USER')) {
139af93d154SAndreas Gohr            // failed attempt
140af93d154SAndreas Gohr            $user = $INPUT->str('u');
141b5e880bdSAndreas Gohr            $type = 'f';
142b5e880bdSAndreas Gohr        }
143b5e880bdSAndreas Gohr        if (!$type) return;
144b5e880bdSAndreas Gohr
1451664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
146b5e880bdSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
147af93d154SAndreas Gohr        $hlp->getLogger()->logLogin($type, $user);
14814d99ec0SAndreas Gohr    }
14914d99ec0SAndreas Gohr
150535aeea1SAndreas Gohr    /**
151535aeea1SAndreas Gohr     * Log user creations
152535aeea1SAndreas Gohr     */
153a8acb244SAndreas Gohr    public function logregistration(Event $event, $param)
154a8acb244SAndreas Gohr    {
155535aeea1SAndreas Gohr        if ($event->data['type'] == 'create') {
1561664ba1dSAndreas Gohr            /** @var helper_plugin_statistics $hlp */
157535aeea1SAndreas Gohr            $hlp = plugin_load('helper', 'statistics');
158211caa5dSAndreas Gohr            $hlp->getLogger()->logLogin('C', $event->data['params'][0]);
159535aeea1SAndreas Gohr        }
160535aeea1SAndreas Gohr    }
161b5e880bdSAndreas Gohr
162b5e880bdSAndreas Gohr    /**
1631664ba1dSAndreas Gohr     * Log media access
1641664ba1dSAndreas Gohr     */
165a8acb244SAndreas Gohr    public function logmedia(Event $event, $param)
166a8acb244SAndreas Gohr    {
1671664ba1dSAndreas Gohr        if ($event->data['status'] < 200) return;
1681664ba1dSAndreas Gohr        if ($event->data['status'] >= 400) return;
1691664ba1dSAndreas Gohr        if (preg_match('/^\w+:\/\//', $event->data['media'])) return;
1701664ba1dSAndreas Gohr
1711664ba1dSAndreas Gohr        // no size for redirect/not modified
1721664ba1dSAndreas Gohr        if ($event->data['status'] >= 300) {
1731664ba1dSAndreas Gohr            $size = 0;
1741664ba1dSAndreas Gohr        } else {
17517978b38SAndreas Gohr            $size = @filesize($event->data['file']);
1761664ba1dSAndreas Gohr        }
1771664ba1dSAndreas Gohr
1781664ba1dSAndreas Gohr        /** @var helper_plugin_statistics $hlp */
1791664ba1dSAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
180211caa5dSAndreas Gohr        $hlp->getLogger()->logMedia(
1811664ba1dSAndreas Gohr            $event->data['media'],
1821664ba1dSAndreas Gohr            $event->data['mime'],
1831664ba1dSAndreas Gohr            !$event->data['download'],
1841664ba1dSAndreas Gohr            $size
1851664ba1dSAndreas Gohr        );
1861664ba1dSAndreas Gohr    }
1871664ba1dSAndreas Gohr
1881664ba1dSAndreas Gohr    /**
189cae4a1c5SAndreas Gohr     * Log the daily page and media counts for the history
190cae4a1c5SAndreas Gohr     */
191a8acb244SAndreas Gohr    public function loghistory(Event $event, $param)
192a8acb244SAndreas Gohr    {
193cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: started' . DOKU_LF;
194cae4a1c5SAndreas Gohr
195cae4a1c5SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
196cae4a1c5SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
197a9509e05SAndreas Gohr (aider)        $db = $hlp->getDB();
198cae4a1c5SAndreas Gohr
199cae4a1c5SAndreas Gohr        // check if a history was gathered already today
200a9509e05SAndreas Gohr (aider)        $result = $db->queryAll(
20102aa9b73SAndreas Gohr            "SELECT info FROM history WHERE date(dt) = date('now')"
202a9509e05SAndreas Gohr (aider)        );
203cae4a1c5SAndreas Gohr
204cae4a1c5SAndreas Gohr        $page_ran = false;
205cae4a1c5SAndreas Gohr        $media_ran = false;
206cae4a1c5SAndreas Gohr        foreach ($result as $row) {
207cae4a1c5SAndreas Gohr            if ($row['info'] == 'page_count') $page_ran = true;
208cae4a1c5SAndreas Gohr            if ($row['info'] == 'media_count') $media_ran = true;
209cae4a1c5SAndreas Gohr        }
210cae4a1c5SAndreas Gohr
211cae4a1c5SAndreas Gohr        if ($page_ran && $media_ran) {
212cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: nothing to do - finished' . DOKU_LF;
213cae4a1c5SAndreas Gohr            return;
214cae4a1c5SAndreas Gohr        }
215cae4a1c5SAndreas Gohr
216cae4a1c5SAndreas Gohr        $event->stopPropagation();
217cae4a1c5SAndreas Gohr        $event->preventDefault();
218cae4a1c5SAndreas Gohr
219cae4a1c5SAndreas Gohr        if ($page_ran) {
220cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging media' . DOKU_LF;
221211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryMedia();
222cae4a1c5SAndreas Gohr        } else {
223cae4a1c5SAndreas Gohr            echo 'Plugin Statistics: logging pages' . DOKU_LF;
224211caa5dSAndreas Gohr            $hlp->getLogger()->logHistoryPages();
225cae4a1c5SAndreas Gohr        }
226cae4a1c5SAndreas Gohr        echo 'Plugin Statistics: finished' . DOKU_LF;
227cae4a1c5SAndreas Gohr    }
228cae4a1c5SAndreas Gohr
229cae4a1c5SAndreas Gohr    /**
2307a1a7c58SAndreas Gohr     * Prune old data
2317a1a7c58SAndreas Gohr     *
2327a1a7c58SAndreas Gohr     * This is run once a day and removes all data older than the configured
2337a1a7c58SAndreas Gohr     * retention time.
2347a1a7c58SAndreas Gohr     */
2357a1a7c58SAndreas Gohr    public function retention(Event $event, $param)
2367a1a7c58SAndreas Gohr    {
2377a1a7c58SAndreas Gohr        $retention = (int)$this->getConf('retention');
2387a1a7c58SAndreas Gohr        if ($retention <= 0) return;
2397a1a7c58SAndreas Gohr        // pruning is only done once a day
2407a1a7c58SAndreas Gohr        $touch = getCacheName('statistics_retention', '.statistics-retention');
2417a1a7c58SAndreas Gohr        if (file_exists($touch) && time() - filemtime($touch) < 24 * 3600) {
2427a1a7c58SAndreas Gohr            return;
2437a1a7c58SAndreas Gohr        }
2447a1a7c58SAndreas Gohr
2457a1a7c58SAndreas Gohr        $event->stopPropagation();
2467a1a7c58SAndreas Gohr        $event->preventDefault();
2477a1a7c58SAndreas Gohr
2487a1a7c58SAndreas Gohr        // these are the tables to be pruned
2497a1a7c58SAndreas Gohr        $tables = [
2507a1a7c58SAndreas Gohr            'edits',
2517a1a7c58SAndreas Gohr            'history',
2527a1a7c58SAndreas Gohr            'iplocation',
2537a1a7c58SAndreas Gohr            'logins',
2547a1a7c58SAndreas Gohr            'media',
2557a1a7c58SAndreas Gohr            'outlinks',
2567a1a7c58SAndreas Gohr            'pageviews',
2577a1a7c58SAndreas Gohr            'referers',
2587a1a7c58SAndreas Gohr            'search',
2597a1a7c58SAndreas Gohr            'sessions',
2607a1a7c58SAndreas Gohr        ];
2617a1a7c58SAndreas Gohr
2627a1a7c58SAndreas Gohr        /** @var helper_plugin_statistics $hlp */
2637a1a7c58SAndreas Gohr        $hlp = plugin_load('helper', 'statistics');
2647a1a7c58SAndreas Gohr        $db = $hlp->getDB();
2657a1a7c58SAndreas Gohr
2667a1a7c58SAndreas Gohr        $db->getPdo()->beginTransaction();
2677a1a7c58SAndreas Gohr        foreach ($tables as $table) {
2687a1a7c58SAndreas Gohr            echo "Plugin Statistics: pruning $table" . DOKU_LF;
2697a1a7c58SAndreas Gohr            $db->exec(
2707a1a7c58SAndreas Gohr                "DELETE FROM $table WHERE dt < datetime('now', '-$retention days')"
2717a1a7c58SAndreas Gohr            );
2727a1a7c58SAndreas Gohr        }
2737a1a7c58SAndreas Gohr        $db->getPdo()->commit();
2747a1a7c58SAndreas Gohr
2757a1a7c58SAndreas Gohr        echo "Plugin Statistics: Optimizing" . DOKU_LF;
2767a1a7c58SAndreas Gohr        $db->exec('VACUUM');
2777a1a7c58SAndreas Gohr
2787a1a7c58SAndreas Gohr        // touch the retention file to prevent multiple runs
2797a1a7c58SAndreas Gohr        io_saveFile($touch, dformat());
2807a1a7c58SAndreas Gohr    }
2817a1a7c58SAndreas Gohr
2827a1a7c58SAndreas Gohr    /**
283b5e880bdSAndreas Gohr     * Pre-Sanitize the action command
284b5e880bdSAndreas Gohr     *
285b5e880bdSAndreas Gohr     * Similar to act_clean in action.php but simplified and without
286b5e880bdSAndreas Gohr     * error messages
287b5e880bdSAndreas Gohr     */
288a5dadbc1SAndreas Gohr    protected function actClean($act)
289a8acb244SAndreas Gohr    {
290b5e880bdSAndreas Gohr        // check if the action was given as array key
291b5e880bdSAndreas Gohr        if (is_array($act)) {
292a8acb244SAndreas Gohr            [$act] = array_keys($act);
293b5e880bdSAndreas Gohr        }
294b5e880bdSAndreas Gohr
295b5e880bdSAndreas Gohr        //remove all bad chars
296b5e880bdSAndreas Gohr        $act = strtolower($act);
297b5e880bdSAndreas Gohr        $act = preg_replace('/[^a-z_]+/', '', $act);
298b5e880bdSAndreas Gohr
299b5e880bdSAndreas Gohr        return $act;
300b5e880bdSAndreas Gohr    }
301b5e880bdSAndreas Gohr}
302