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