1762f4807SAndreas Gohr<?php 2762f4807SAndreas Gohr 3762f4807SAndreas Gohrnamespace dokuwiki\plugin\statistics; 4762f4807SAndreas Gohr 541d1fffcSAndreas Gohr/** 641d1fffcSAndreas Gohr * Exception thrown when logging should be ignored 741d1fffcSAndreas Gohr */ 841d1fffcSAndreas Gohrclass IgnoreException extends \RuntimeException 941d1fffcSAndreas Gohr{ 1041d1fffcSAndreas Gohr} 1141d1fffcSAndreas Gohr 12762f4807SAndreas Gohruse DeviceDetector\DeviceDetector; 13762f4807SAndreas Gohruse DeviceDetector\Parser\Client\Browser; 14762f4807SAndreas Gohruse DeviceDetector\Parser\Device\AbstractDeviceParser; 15762f4807SAndreas Gohruse DeviceDetector\Parser\OperatingSystem; 16762f4807SAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient; 1741d1fffcSAndreas Gohruse dokuwiki\Input\Input; 18762f4807SAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB; 19762f4807SAndreas Gohruse helper_plugin_popularity; 20762f4807SAndreas Gohruse helper_plugin_statistics; 21762f4807SAndreas Gohr 22762f4807SAndreas Gohrclass Logger 23762f4807SAndreas Gohr{ 24762f4807SAndreas Gohr /** @var helper_plugin_statistics The statistics helper plugin instance */ 25762f4807SAndreas Gohr protected helper_plugin_statistics $hlp; 26762f4807SAndreas Gohr 27762f4807SAndreas Gohr /** @var SQLiteDB The SQLite database instance */ 28762f4807SAndreas Gohr protected SQLiteDB $db; 29762f4807SAndreas Gohr 30762f4807SAndreas Gohr /** @var string The full user agent string */ 31762f4807SAndreas Gohr protected string $uaAgent; 32762f4807SAndreas Gohr 33762f4807SAndreas Gohr /** @var string The type of user agent (browser, robot, feedreader) */ 34762f4807SAndreas Gohr protected string $uaType = 'browser'; 35762f4807SAndreas Gohr 36762f4807SAndreas Gohr /** @var string The browser/client name */ 37762f4807SAndreas Gohr protected string $uaName; 38762f4807SAndreas Gohr 39762f4807SAndreas Gohr /** @var string The browser/client version */ 40762f4807SAndreas Gohr protected string $uaVersion; 41762f4807SAndreas Gohr 42762f4807SAndreas Gohr /** @var string The operating system/platform */ 43762f4807SAndreas Gohr protected string $uaPlatform; 44762f4807SAndreas Gohr 454a163f50SAndreas Gohr /** @var string|null The user name, if available */ 464a163f50SAndreas Gohr protected ?string $user = null; 474a163f50SAndreas Gohr 48762f4807SAndreas Gohr /** @var string The unique user identifier */ 49762f4807SAndreas Gohr protected string $uid; 50762f4807SAndreas Gohr 514a163f50SAndreas Gohr /** @var string The session identifier */ 524a163f50SAndreas Gohr protected string $session; 534a163f50SAndreas Gohr 544a163f50SAndreas Gohr /** @var int|null The ID of the main access log entry if any */ 554a163f50SAndreas Gohr protected ?int $hit = null; 564a163f50SAndreas Gohr 57c7cad24dSAndreas Gohr /** @var DokuHTTPClient|null The HTTP client instance for testing */ 58c7cad24dSAndreas Gohr protected ?DokuHTTPClient $httpClient = null; 59c7cad24dSAndreas Gohr 604a163f50SAndreas Gohr // region lifecycle 61762f4807SAndreas Gohr 62762f4807SAndreas Gohr /** 63762f4807SAndreas Gohr * Constructor 64762f4807SAndreas Gohr * 65762f4807SAndreas Gohr * Parses browser info and set internal vars 66762f4807SAndreas Gohr */ 67c7cad24dSAndreas Gohr public function __construct(helper_plugin_statistics $hlp, ?DokuHTTPClient $httpClient = null) 68762f4807SAndreas Gohr { 6941d1fffcSAndreas Gohr /** @var Input $INPUT */ 70762f4807SAndreas Gohr global $INPUT; 71762f4807SAndreas Gohr 72762f4807SAndreas Gohr $this->hlp = $hlp; 73762f4807SAndreas Gohr $this->db = $this->hlp->getDB(); 74c7cad24dSAndreas Gohr $this->httpClient = $httpClient; 75762f4807SAndreas Gohr 764a163f50SAndreas Gohr // FIXME if we already have a session, we should not re-parse the user agent 77762f4807SAndreas Gohr 784a163f50SAndreas Gohr $ua = trim($INPUT->server->str('HTTP_USER_AGENT')); 79762f4807SAndreas Gohr AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_MAJOR); 80762f4807SAndreas Gohr $dd = new DeviceDetector($ua); // FIXME we could use client hints, but need to add headers 81762f4807SAndreas Gohr $dd->discardBotInformation(); 82762f4807SAndreas Gohr $dd->parse(); 83762f4807SAndreas Gohr 8400f786d8SAndreas Gohr if ($dd->isFeedReader()) { 8500f786d8SAndreas Gohr $this->uaType = 'feedreader'; 8600f786d8SAndreas Gohr } elseif ($dd->isBot()) { 87762f4807SAndreas Gohr $this->uaType = 'robot'; 88762f4807SAndreas Gohr // for now ignore bots 89c5d2f052SAndreas Gohr throw new IgnoreException('Bot detected, not logging'); 90762f4807SAndreas Gohr } 91762f4807SAndreas Gohr 92762f4807SAndreas Gohr $this->uaAgent = $ua; 9305786d83SAndreas Gohr $this->uaName = Browser::getBrowserFamily($dd->getClient('name')) ?: 'Unknown'; 9400f786d8SAndreas Gohr $this->uaVersion = $dd->getClient('version') ?: '0'; 9505786d83SAndreas Gohr $this->uaPlatform = OperatingSystem::getOsFamily($dd->getOs('name')) ?: 'Unknown'; 96762f4807SAndreas Gohr $this->uid = $this->getUID(); 974a163f50SAndreas Gohr $this->session = $this->getSession(); 9841d1fffcSAndreas Gohr $this->user = $INPUT->server->str('REMOTE_USER', null, true); 99762f4807SAndreas Gohr } 100762f4807SAndreas Gohr 101762f4807SAndreas Gohr /** 102762f4807SAndreas Gohr * Should be called before logging 103762f4807SAndreas Gohr * 1044a163f50SAndreas Gohr * This starts a transaction, so all logging is done in one go. It also logs the user and session data. 105762f4807SAndreas Gohr */ 106762f4807SAndreas Gohr public function begin(): void 107762f4807SAndreas Gohr { 108762f4807SAndreas Gohr $this->hlp->getDB()->getPdo()->beginTransaction(); 1094a163f50SAndreas Gohr 1104a163f50SAndreas Gohr $this->logUser(); 1114a163f50SAndreas Gohr $this->logGroups(); 1124a163f50SAndreas Gohr $this->logDomain(); 1134a163f50SAndreas Gohr $this->logSession(); 114762f4807SAndreas Gohr } 115762f4807SAndreas Gohr 116762f4807SAndreas Gohr /** 117762f4807SAndreas Gohr * Should be called after logging 118762f4807SAndreas Gohr * 119762f4807SAndreas Gohr * This commits the transaction started in begin() 120762f4807SAndreas Gohr */ 121762f4807SAndreas Gohr public function end(): void 122762f4807SAndreas Gohr { 123762f4807SAndreas Gohr $this->hlp->getDB()->getPdo()->commit(); 124762f4807SAndreas Gohr } 125762f4807SAndreas Gohr 1264a163f50SAndreas Gohr // endregion 1274a163f50SAndreas Gohr // region data gathering 1284a163f50SAndreas Gohr 129762f4807SAndreas Gohr /** 130762f4807SAndreas Gohr * Get the unique user ID 131762f4807SAndreas Gohr * 13204928db4SAndreas Gohr * The user ID is stored in the user preferences and should stay there forever. 133762f4807SAndreas Gohr * @return string The unique user identifier 134762f4807SAndreas Gohr */ 135762f4807SAndreas Gohr protected function getUID(): string 136762f4807SAndreas Gohr { 13704928db4SAndreas Gohr if (!isset($_SESSION[DOKU_COOKIE]['statistics']['uid'])) { 13804928db4SAndreas Gohr // when there is no session UID set, we assume this was deliberate and we simply abort all logging 13904928db4SAndreas Gohr // @todo we may later make UID generation optional 14004928db4SAndreas Gohr throw new IgnoreException('No user ID found'); 14104928db4SAndreas Gohr } 142762f4807SAndreas Gohr 14304928db4SAndreas Gohr return $_SESSION[DOKU_COOKIE]['statistics']['uid']; 144762f4807SAndreas Gohr } 145762f4807SAndreas Gohr 146762f4807SAndreas Gohr /** 147762f4807SAndreas Gohr * Return the user's session ID 148762f4807SAndreas Gohr * 149762f4807SAndreas Gohr * @return string The session identifier 150762f4807SAndreas Gohr */ 151762f4807SAndreas Gohr protected function getSession(): string 152762f4807SAndreas Gohr { 15304928db4SAndreas Gohr if (!isset($_SESSION[DOKU_COOKIE]['statistics']['id'])) { 15404928db4SAndreas Gohr // when there is no session ID set, we assume this was deliberate and we simply abort all logging 15504928db4SAndreas Gohr throw new IgnoreException('No session ID found'); 15604928db4SAndreas Gohr } 157762f4807SAndreas Gohr 15804928db4SAndreas Gohr return $_SESSION[DOKU_COOKIE]['statistics']['id']; 159762f4807SAndreas Gohr } 160762f4807SAndreas Gohr 1614a163f50SAndreas Gohr // endregion 1624a163f50SAndreas Gohr // region automatic logging 163762f4807SAndreas Gohr 1644a163f50SAndreas Gohr /** 1654a163f50SAndreas Gohr * Log the user was seen 1664a163f50SAndreas Gohr */ 1674a163f50SAndreas Gohr protected function logUser(): void 1684a163f50SAndreas Gohr { 1694a163f50SAndreas Gohr if (!$this->user) return; 170762f4807SAndreas Gohr 171762f4807SAndreas Gohr $this->db->exec( 1724a163f50SAndreas Gohr 'INSERT INTO users (user, dt) 1734a163f50SAndreas Gohr VALUES (?, CURRENT_TIMESTAMP) 1744a163f50SAndreas Gohr ON CONFLICT (user) DO UPDATE SET 1754a163f50SAndreas Gohr dt = CURRENT_TIMESTAMP 1764a163f50SAndreas Gohr WHERE excluded.user = users.user 1774a163f50SAndreas Gohr ', 1784a163f50SAndreas Gohr $this->user 1794a163f50SAndreas Gohr ); 1804a163f50SAndreas Gohr 1814a163f50SAndreas Gohr } 1824a163f50SAndreas Gohr 1834a163f50SAndreas Gohr /** 1844a163f50SAndreas Gohr * Log the session and user agent information 1854a163f50SAndreas Gohr */ 1864a163f50SAndreas Gohr protected function logSession(): void 1874a163f50SAndreas Gohr { 1884a163f50SAndreas Gohr $this->db->exec( 1894a163f50SAndreas Gohr 'INSERT INTO sessions (session, dt, end, uid, user, ua, ua_info, ua_type, ua_ver, os) 1904a163f50SAndreas Gohr VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?) 1914a163f50SAndreas Gohr ON CONFLICT (session) DO UPDATE SET 19241d1fffcSAndreas Gohr end = CURRENT_TIMESTAMP, 19341d1fffcSAndreas Gohr user = excluded.user, 19441d1fffcSAndreas Gohr uid = excluded.uid 1954a163f50SAndreas Gohr WHERE excluded.session = sessions.session 1964a163f50SAndreas Gohr ', 1974a163f50SAndreas Gohr $this->session, 1984a163f50SAndreas Gohr $this->uid, 1994a163f50SAndreas Gohr $this->user, 2004a163f50SAndreas Gohr $this->uaAgent, 2014a163f50SAndreas Gohr $this->uaName, 2024a163f50SAndreas Gohr $this->uaType, 2034a163f50SAndreas Gohr $this->uaVersion, 2044a163f50SAndreas Gohr $this->uaPlatform 205762f4807SAndreas Gohr ); 206762f4807SAndreas Gohr } 207762f4807SAndreas Gohr 208762f4807SAndreas Gohr /** 2094a163f50SAndreas Gohr * Log all groups for the user 210762f4807SAndreas Gohr * 2114a163f50SAndreas Gohr * @todo maybe this should be done only once per session? 212762f4807SAndreas Gohr */ 2134a163f50SAndreas Gohr protected function logGroups(): void 214762f4807SAndreas Gohr { 2154a163f50SAndreas Gohr global $USERINFO; 216762f4807SAndreas Gohr 2174a163f50SAndreas Gohr if (!$this->user) return; 2184a163f50SAndreas Gohr if (!isset($USERINFO['grps'])) return; 2194a163f50SAndreas Gohr if (!is_array($USERINFO['grps'])) return; 2204a163f50SAndreas Gohr $groups = $USERINFO['grps']; 221fced2f86SAnna Dabrowska 2224a163f50SAndreas Gohr $this->db->exec('DELETE FROM groups WHERE user = ?', $this->user); 223762f4807SAndreas Gohr 22441d1fffcSAndreas Gohr if (empty($groups)) { 22541d1fffcSAndreas Gohr return; 22641d1fffcSAndreas Gohr } 22741d1fffcSAndreas Gohr 22802aa9b73SAndreas Gohr $placeholders = implode(',', array_fill(0, count($groups), '(?, ?)')); 229762f4807SAndreas Gohr $params = []; 2304a163f50SAndreas Gohr $sql = "INSERT INTO groups (`user`, `group`) VALUES $placeholders"; 231762f4807SAndreas Gohr foreach ($groups as $group) { 2324a163f50SAndreas Gohr $params[] = $this->user; 233762f4807SAndreas Gohr $params[] = $group; 234762f4807SAndreas Gohr } 235762f4807SAndreas Gohr $this->db->exec($sql, $params); 236762f4807SAndreas Gohr } 237762f4807SAndreas Gohr 238762f4807SAndreas Gohr /** 2394a163f50SAndreas Gohr * Log email domain 2400b8b7abaSAnna Dabrowska * 2414a163f50SAndreas Gohr * @todo maybe this should be done only once per session? 2420b8b7abaSAnna Dabrowska */ 2434a163f50SAndreas Gohr protected function logDomain(): void 2440b8b7abaSAnna Dabrowska { 2454a163f50SAndreas Gohr global $USERINFO; 2464a163f50SAndreas Gohr if (!$this->user) return; 2474a163f50SAndreas Gohr if (!isset($USERINFO['mail'])) return; 2484a163f50SAndreas Gohr $mail = $USERINFO['mail']; 2490b8b7abaSAnna Dabrowska 2500b8b7abaSAnna Dabrowska $pos = strrpos($mail, '@'); 2510b8b7abaSAnna Dabrowska if (!$pos) return; 2520b8b7abaSAnna Dabrowska $domain = substr($mail, $pos + 1); 2530b8b7abaSAnna Dabrowska if (empty($domain)) return; 2540b8b7abaSAnna Dabrowska 2554a163f50SAndreas Gohr $sql = 'UPDATE users SET domain = ? WHERE user = ?'; 2564a163f50SAndreas Gohr $this->db->exec($sql, [$domain, $this->user]); 2570b8b7abaSAnna Dabrowska } 2580b8b7abaSAnna Dabrowska 2594a163f50SAndreas Gohr // endregion 2604a163f50SAndreas Gohr // region internal loggers called by the dispatchers 2614a163f50SAndreas Gohr 2620b8b7abaSAnna Dabrowska /** 2634a163f50SAndreas Gohr * Log the given referer URL 264762f4807SAndreas Gohr * 2652a30f557SAndreas Gohr * Note: we DO log empty referers. These are external accesses that did not provide a referer URL. 2662a30f557SAndreas Gohr * We do not log referers that are our own pages though. 2672a30f557SAndreas Gohr * 2682a30f557SAndreas Gohr * engine set -> a search engine referer 2692a30f557SAndreas Gohr * no engine set, url empty -> a direct access (bookmark, direct link, etc.) 2702a30f557SAndreas Gohr * no engine set, url not empty -> a referer from another page (not a wiki page) 2712a30f557SAndreas Gohr * null returned -> referer was a wiki page 2722a30f557SAndreas Gohr * 2734a163f50SAndreas Gohr * @param $referer 2742a30f557SAndreas Gohr * @return int|null The referer ID or null if no referer was logged 2752a30f557SAndreas Gohr * @todo we could check against a blacklist here 276762f4807SAndreas Gohr */ 2774a163f50SAndreas Gohr public function logReferer($referer): ?int 278762f4807SAndreas Gohr { 2792a30f557SAndreas Gohr $referer = trim($referer); 280762f4807SAndreas Gohr 281569a5066SAndreas Gohr // do not log our own pages as referers (empty referer is OK though) 282569a5066SAndreas Gohr if (!empty($referer)) { 283569a5066SAndreas Gohr $selfre = '^' . preg_quote(DOKU_URL, '/'); 2842a30f557SAndreas Gohr if (preg_match("/$selfre/", $referer)) { 2852a30f557SAndreas Gohr return null; 2862a30f557SAndreas Gohr } 287569a5066SAndreas Gohr } 288762f4807SAndreas Gohr 2892a30f557SAndreas Gohr // is it a search engine? 2904a163f50SAndreas Gohr $se = new SearchEngines($referer); 29141d1fffcSAndreas Gohr $engine = $se->getEngine(); 292762f4807SAndreas Gohr 29341d1fffcSAndreas Gohr $sql = 'INSERT OR IGNORE INTO referers (url, engine, dt) VALUES (?, ?, CURRENT_TIMESTAMP)'; 294569a5066SAndreas Gohr $this->db->exec($sql, [$referer, $engine]); 295569a5066SAndreas Gohr return (int)$this->db->queryValue('SELECT id FROM referers WHERE url = ?', $referer); 296762f4807SAndreas Gohr } 297762f4807SAndreas Gohr 298762f4807SAndreas Gohr /** 299762f4807SAndreas Gohr * Resolve IP to country/city and store in database 300762f4807SAndreas Gohr * 3014a163f50SAndreas Gohr * @return string The IP address as stored 302762f4807SAndreas Gohr */ 3034a163f50SAndreas Gohr public function logIp(): string 304762f4807SAndreas Gohr { 3054a163f50SAndreas Gohr $ip = clientIP(true); 306*69fb56a2SAndreas Gohr 307*69fb56a2SAndreas Gohr // anonymize the IP address for storage? 308*69fb56a2SAndreas Gohr if ($this->hlp->getConf('anonips')) { 309*69fb56a2SAndreas Gohr $hash = md5($ip . strrev($ip)); // we use the reversed IP as salt to avoid common rainbow tables 310*69fb56a2SAndreas Gohr $host = ''; 311*69fb56a2SAndreas Gohr } else { 312*69fb56a2SAndreas Gohr $hash = $ip; 313*69fb56a2SAndreas Gohr $host = gethostbyaddr($ip); 314*69fb56a2SAndreas Gohr } 3154a163f50SAndreas Gohr 316762f4807SAndreas Gohr // check if IP already known and up-to-date 317762f4807SAndreas Gohr $result = $this->db->queryValue( 318762f4807SAndreas Gohr "SELECT ip 319762f4807SAndreas Gohr FROM iplocation 320762f4807SAndreas Gohr WHERE ip = ? 321762f4807SAndreas Gohr AND lastupd > date('now', '-30 days')", 3224a163f50SAndreas Gohr $hash 323762f4807SAndreas Gohr ); 3244a163f50SAndreas Gohr if ($result) return $hash; // already known and up-to-date 325762f4807SAndreas Gohr 326c7cad24dSAndreas Gohr $http = $this->httpClient ?: new DokuHTTPClient(); 3274a163f50SAndreas Gohr $http->timeout = 7; 328762f4807SAndreas Gohr $json = $http->get('http://ip-api.com/json/' . $ip); // yes, it's HTTP only 329762f4807SAndreas Gohr 3304a163f50SAndreas Gohr if (!$json) { 3314a163f50SAndreas Gohr \dokuwiki\Logger::error('Statistics Plugin - Failed talk to ip-api.com.'); 3324a163f50SAndreas Gohr return $hash; 3334a163f50SAndreas Gohr } 334762f4807SAndreas Gohr try { 335762f4807SAndreas Gohr $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR); 336762f4807SAndreas Gohr } catch (\JsonException $e) { 3374a163f50SAndreas Gohr \dokuwiki\Logger::error('Statistics Plugin - Failed to decode JSON from ip-api.com.', $e); 3384a163f50SAndreas Gohr return $hash; 339762f4807SAndreas Gohr } 34002aa9b73SAndreas Gohr if (!isset($data['status'])) { 34102aa9b73SAndreas Gohr \dokuwiki\Logger::error('Statistics Plugin - Invalid ip-api.com result' . $ip, $data); 3424a163f50SAndreas Gohr return $hash; 34341d1fffcSAndreas Gohr } 34402aa9b73SAndreas Gohr 34502aa9b73SAndreas Gohr // we do not check for 'success' status here. when the API can't resolve the IP we still log it 34602aa9b73SAndreas Gohr // without location data, so we won't re-query it in the next 30 days. 347762f4807SAndreas Gohr 348762f4807SAndreas Gohr $this->db->exec( 349762f4807SAndreas Gohr 'INSERT OR REPLACE INTO iplocation ( 350762f4807SAndreas Gohr ip, country, code, city, host, lastupd 351762f4807SAndreas Gohr ) VALUES ( 352762f4807SAndreas Gohr ?, ?, ?, ?, ?, CURRENT_TIMESTAMP 353762f4807SAndreas Gohr )', 3544a163f50SAndreas Gohr $hash, 35502aa9b73SAndreas Gohr $data['country'] ?? '', 35602aa9b73SAndreas Gohr $data['countryCode'] ?? '', 35702aa9b73SAndreas Gohr $data['city'] ?? '', 3582adee4c6SAndreas Gohr $host 359762f4807SAndreas Gohr ); 3604a163f50SAndreas Gohr 3614a163f50SAndreas Gohr return $hash; 3624a163f50SAndreas Gohr } 3634a163f50SAndreas Gohr 3644a163f50SAndreas Gohr // endregion 3654a163f50SAndreas Gohr // region log dispatchers 3664a163f50SAndreas Gohr 3674a163f50SAndreas Gohr public function logPageView(): void 3684a163f50SAndreas Gohr { 3694a163f50SAndreas Gohr global $INPUT; 3704a163f50SAndreas Gohr 3714a163f50SAndreas Gohr if (!$INPUT->str('p')) return; 3724a163f50SAndreas Gohr 3734a163f50SAndreas Gohr 3744a163f50SAndreas Gohr $referer = $INPUT->filter('trim')->str('r'); 3754a163f50SAndreas Gohr $ip = $this->logIp(); // resolve the IP address 3764a163f50SAndreas Gohr 3774a163f50SAndreas Gohr $data = [ 3784a163f50SAndreas Gohr 'page' => $INPUT->filter('cleanID')->str('p'), 3794a163f50SAndreas Gohr 'ip' => $ip, 3804a163f50SAndreas Gohr 'ref_id' => $this->logReferer($referer), 3814a163f50SAndreas Gohr 'sx' => $INPUT->int('sx'), 3824a163f50SAndreas Gohr 'sy' => $INPUT->int('sy'), 3834a163f50SAndreas Gohr 'vx' => $INPUT->int('vx'), 3844a163f50SAndreas Gohr 'vy' => $INPUT->int('vy'), 3854a163f50SAndreas Gohr 'session' => $this->session, 3864a163f50SAndreas Gohr ]; 3874a163f50SAndreas Gohr 3884a163f50SAndreas Gohr $this->db->exec(' 3894a163f50SAndreas Gohr INSERT INTO pageviews ( 3904a163f50SAndreas Gohr dt, page, ip, ref_id, screen_x, screen_y, view_x, view_y, session 3914a163f50SAndreas Gohr ) VALUES ( 3924a163f50SAndreas Gohr CURRENT_TIMESTAMP, :page, :ip, :ref_id, :sx, :sy, :vx, :vy, :session 3934a163f50SAndreas Gohr ) 3944a163f50SAndreas Gohr ', 3954a163f50SAndreas Gohr $data 3964a163f50SAndreas Gohr ); 397762f4807SAndreas Gohr } 398762f4807SAndreas Gohr 399762f4807SAndreas Gohr /** 400762f4807SAndreas Gohr * Log a click on an external link 401762f4807SAndreas Gohr * 402762f4807SAndreas Gohr * Called from log.php 403762f4807SAndreas Gohr */ 404762f4807SAndreas Gohr public function logOutgoing(): void 405762f4807SAndreas Gohr { 406762f4807SAndreas Gohr global $INPUT; 407762f4807SAndreas Gohr 408762f4807SAndreas Gohr if (!$INPUT->str('ol')) return; 409762f4807SAndreas Gohr 4104a163f50SAndreas Gohr $link = $INPUT->filter('trim')->str('ol'); 4114a163f50SAndreas Gohr $session = $this->session; 4124a163f50SAndreas Gohr $page = $INPUT->filter('cleanID')->str('p'); 413762f4807SAndreas Gohr 414762f4807SAndreas Gohr $this->db->exec( 415762f4807SAndreas Gohr 'INSERT INTO outlinks ( 4164a163f50SAndreas Gohr dt, session, page, link 417762f4807SAndreas Gohr ) VALUES ( 41841d1fffcSAndreas Gohr CURRENT_TIMESTAMP, ?, ?, ? 419762f4807SAndreas Gohr )', 4202adee4c6SAndreas Gohr $session, 4212adee4c6SAndreas Gohr $page, 4222adee4c6SAndreas Gohr $link 423762f4807SAndreas Gohr ); 424762f4807SAndreas Gohr } 425762f4807SAndreas Gohr 426762f4807SAndreas Gohr /** 427762f4807SAndreas Gohr * Log access to a media file 428762f4807SAndreas Gohr * 429762f4807SAndreas Gohr * Called from action.php 430762f4807SAndreas Gohr * 431762f4807SAndreas Gohr * @param string $media The media ID 432762f4807SAndreas Gohr * @param string $mime The media's mime type 433762f4807SAndreas Gohr * @param bool $inline Is this displayed inline? 434762f4807SAndreas Gohr * @param int $size Size of the media file 435762f4807SAndreas Gohr */ 436762f4807SAndreas Gohr public function logMedia(string $media, string $mime, bool $inline, int $size): void 437762f4807SAndreas Gohr { 438762f4807SAndreas Gohr [$mime1, $mime2] = explode('/', strtolower($mime)); 439762f4807SAndreas Gohr $inline = $inline ? 1 : 0; 440762f4807SAndreas Gohr 441762f4807SAndreas Gohr 4424a163f50SAndreas Gohr $data = [ 4434a163f50SAndreas Gohr 'media' => cleanID($media), 4444a163f50SAndreas Gohr 'ip' => $this->logIp(), // resolve the IP address 4454a163f50SAndreas Gohr 'session' => $this->session, 4464a163f50SAndreas Gohr 'size' => $size, 4474a163f50SAndreas Gohr 'mime1' => $mime1, 4484a163f50SAndreas Gohr 'mime2' => $mime2, 4494a163f50SAndreas Gohr 'inline' => $inline, 4504a163f50SAndreas Gohr ]; 4514a163f50SAndreas Gohr 4524a163f50SAndreas Gohr $this->db->exec(' 4534a163f50SAndreas Gohr INSERT INTO media ( dt, media, ip, session, size, mime1, mime2, inline ) 4544a163f50SAndreas Gohr VALUES (CURRENT_TIMESTAMP, :media, :ip, :session, :size, :mime1, :mime2, :inline) 4554a163f50SAndreas Gohr ', 4564a163f50SAndreas Gohr $data 457762f4807SAndreas Gohr ); 458762f4807SAndreas Gohr } 459762f4807SAndreas Gohr 460762f4807SAndreas Gohr /** 461762f4807SAndreas Gohr * Log page edits 462762f4807SAndreas Gohr * 4634a163f50SAndreas Gohr * called from action.php 4644a163f50SAndreas Gohr * 465762f4807SAndreas Gohr * @param string $page The page that was edited 466762f4807SAndreas Gohr * @param string $type The type of edit (create, edit, etc.) 467762f4807SAndreas Gohr */ 468762f4807SAndreas Gohr public function logEdit(string $page, string $type): void 469762f4807SAndreas Gohr { 4704a163f50SAndreas Gohr $data = [ 4714a163f50SAndreas Gohr 'page' => cleanID($page), 4724a163f50SAndreas Gohr 'type' => $type, 4734a163f50SAndreas Gohr 'ip' => $this->logIp(), // resolve the IP address 4744a163f50SAndreas Gohr 'session' => $this->session 4754a163f50SAndreas Gohr ]; 476762f4807SAndreas Gohr 47741d1fffcSAndreas Gohr $this->db->exec( 478762f4807SAndreas Gohr 'INSERT INTO edits ( 4794a163f50SAndreas Gohr dt, page, type, ip, session 480762f4807SAndreas Gohr ) VALUES ( 4814a163f50SAndreas Gohr CURRENT_TIMESTAMP, :page, :type, :ip, :session 482762f4807SAndreas Gohr )', 4834a163f50SAndreas Gohr $data 484762f4807SAndreas Gohr ); 485762f4807SAndreas Gohr } 486762f4807SAndreas Gohr 487762f4807SAndreas Gohr /** 488762f4807SAndreas Gohr * Log login/logoffs and user creations 489762f4807SAndreas Gohr * 490af93d154SAndreas Gohr * @param string $type The type of login event (login, logout, create, failed) 491af93d154SAndreas Gohr * @param string $user The username 492762f4807SAndreas Gohr */ 493762f4807SAndreas Gohr public function logLogin(string $type, string $user = ''): void 494762f4807SAndreas Gohr { 495762f4807SAndreas Gohr global $INPUT; 496762f4807SAndreas Gohr 497762f4807SAndreas Gohr if (!$user) $user = $INPUT->server->str('REMOTE_USER'); 498762f4807SAndreas Gohr 499762f4807SAndreas Gohr $ip = clientIP(true); 500762f4807SAndreas Gohr 501762f4807SAndreas Gohr $this->db->exec( 502762f4807SAndreas Gohr 'INSERT INTO logins ( 503af93d154SAndreas Gohr dt, ip, user, type 504762f4807SAndreas Gohr ) VALUES ( 505af93d154SAndreas Gohr CURRENT_TIMESTAMP, ?, ?, ? 506762f4807SAndreas Gohr )', 5072adee4c6SAndreas Gohr $ip, 5082adee4c6SAndreas Gohr $user, 509af93d154SAndreas Gohr $type 510762f4807SAndreas Gohr ); 511762f4807SAndreas Gohr } 512762f4807SAndreas Gohr 513762f4807SAndreas Gohr /** 51402aa9b73SAndreas Gohr * Log search data to the search related tables 51502aa9b73SAndreas Gohr * 51602aa9b73SAndreas Gohr * @param string $query The search query 51702aa9b73SAndreas Gohr * @param string[] $words The query split into words 51802aa9b73SAndreas Gohr */ 51902aa9b73SAndreas Gohr public function logSearch(string $query, array $words): void 52002aa9b73SAndreas Gohr { 52102aa9b73SAndreas Gohr if (!$query) return; 52202aa9b73SAndreas Gohr 52302aa9b73SAndreas Gohr $sid = $this->db->exec( 52402aa9b73SAndreas Gohr 'INSERT INTO search (dt, ip, session, query) VALUES (CURRENT_TIMESTAMP, ?, ? , ?)', 52502aa9b73SAndreas Gohr $this->logIp(), // resolve the IP address 52602aa9b73SAndreas Gohr $this->session, 52702aa9b73SAndreas Gohr $query, 52802aa9b73SAndreas Gohr ); 52902aa9b73SAndreas Gohr 53002aa9b73SAndreas Gohr foreach ($words as $word) { 53102aa9b73SAndreas Gohr if (!$word) continue; 53202aa9b73SAndreas Gohr $this->db->exec( 53302aa9b73SAndreas Gohr 'INSERT INTO searchwords (sid, word) VALUES (?, ?)', 53402aa9b73SAndreas Gohr $sid, 53502aa9b73SAndreas Gohr $word 53602aa9b73SAndreas Gohr ); 53702aa9b73SAndreas Gohr } 53802aa9b73SAndreas Gohr } 53902aa9b73SAndreas Gohr 54002aa9b73SAndreas Gohr /** 541762f4807SAndreas Gohr * Log the current page count and size as today's history entry 542762f4807SAndreas Gohr */ 543762f4807SAndreas Gohr public function logHistoryPages(): void 544762f4807SAndreas Gohr { 545762f4807SAndreas Gohr global $conf; 546762f4807SAndreas Gohr 547762f4807SAndreas Gohr // use the popularity plugin's search method to find the wanted data 548762f4807SAndreas Gohr /** @var helper_plugin_popularity $pop */ 549762f4807SAndreas Gohr $pop = plugin_load('helper', 'popularity'); 550b188870fSAndreas Gohr $list = $this->initEmptySearchList(); 551762f4807SAndreas Gohr search($list, $conf['datadir'], [$pop, 'searchCountCallback'], ['all' => false], ''); 552762f4807SAndreas Gohr $page_count = $list['file_count']; 553762f4807SAndreas Gohr $page_size = $list['file_size']; 554762f4807SAndreas Gohr 555762f4807SAndreas Gohr $this->db->exec( 556762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 557762f4807SAndreas Gohr info, value, dt 558762f4807SAndreas Gohr ) VALUES ( 559483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 560762f4807SAndreas Gohr )', 5612adee4c6SAndreas Gohr 'page_count', 5622adee4c6SAndreas Gohr $page_count 563762f4807SAndreas Gohr ); 564762f4807SAndreas Gohr $this->db->exec( 565762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 566762f4807SAndreas Gohr info, value, dt 567762f4807SAndreas Gohr ) VALUES ( 568483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 569762f4807SAndreas Gohr )', 5702adee4c6SAndreas Gohr 'page_size', 5712adee4c6SAndreas Gohr $page_size 572762f4807SAndreas Gohr ); 573762f4807SAndreas Gohr } 574762f4807SAndreas Gohr 575762f4807SAndreas Gohr /** 576762f4807SAndreas Gohr * Log the current media count and size as today's history entry 577762f4807SAndreas Gohr */ 578762f4807SAndreas Gohr public function logHistoryMedia(): void 579762f4807SAndreas Gohr { 580762f4807SAndreas Gohr global $conf; 581762f4807SAndreas Gohr 582762f4807SAndreas Gohr // use the popularity plugin's search method to find the wanted data 583762f4807SAndreas Gohr /** @var helper_plugin_popularity $pop */ 584762f4807SAndreas Gohr $pop = plugin_load('helper', 'popularity'); 585b188870fSAndreas Gohr $list = $this->initEmptySearchList(); 586762f4807SAndreas Gohr search($list, $conf['mediadir'], [$pop, 'searchCountCallback'], ['all' => true], ''); 587762f4807SAndreas Gohr $media_count = $list['file_count']; 588762f4807SAndreas Gohr $media_size = $list['file_size']; 589762f4807SAndreas Gohr 590762f4807SAndreas Gohr $this->db->exec( 591762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 592762f4807SAndreas Gohr info, value, dt 593762f4807SAndreas Gohr ) VALUES ( 594483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 595762f4807SAndreas Gohr )', 5962adee4c6SAndreas Gohr 'media_count', 5972adee4c6SAndreas Gohr $media_count 598762f4807SAndreas Gohr ); 599762f4807SAndreas Gohr $this->db->exec( 600762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 601762f4807SAndreas Gohr info, value, dt 602762f4807SAndreas Gohr ) VALUES ( 603483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 604762f4807SAndreas Gohr )', 6052adee4c6SAndreas Gohr 'media_size', 6062adee4c6SAndreas Gohr $media_size 607762f4807SAndreas Gohr ); 608762f4807SAndreas Gohr } 609b188870fSAndreas Gohr 6104a163f50SAndreas Gohr // endregion 6114a163f50SAndreas Gohr 612b188870fSAndreas Gohr /** 613b188870fSAndreas Gohr * @todo can be dropped in favor of helper_plugin_popularity::initEmptySearchList() once it's public 614b188870fSAndreas Gohr * @return array 615b188870fSAndreas Gohr */ 616b188870fSAndreas Gohr protected function initEmptySearchList() 617b188870fSAndreas Gohr { 618b188870fSAndreas Gohr return array_fill_keys([ 619b188870fSAndreas Gohr 'file_count', 620b188870fSAndreas Gohr 'file_size', 621b188870fSAndreas Gohr 'file_max', 622b188870fSAndreas Gohr 'file_min', 623b188870fSAndreas Gohr 'dir_count', 624b188870fSAndreas Gohr 'dir_nest', 625b188870fSAndreas Gohr 'file_oldest' 626b188870fSAndreas Gohr ], 0); 627b188870fSAndreas Gohr } 628762f4807SAndreas Gohr} 629