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) '&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