1762f4807SAndreas Gohr<?php 2762f4807SAndreas Gohr 3762f4807SAndreas Gohrnamespace dokuwiki\plugin\statistics; 4762f4807SAndreas Gohr 51c4e3694SAndreas Gohruse DeviceDetector\ClientHints; 6762f4807SAndreas Gohruse DeviceDetector\DeviceDetector; 7*523da372SAndreas Gohruse DeviceDetector\Parser\AbstractParser; 8762f4807SAndreas Gohruse DeviceDetector\Parser\Device\AbstractDeviceParser; 9762f4807SAndreas Gohruse DeviceDetector\Parser\OperatingSystem; 1041d1fffcSAndreas Gohruse dokuwiki\Input\Input; 11762f4807SAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB; 12762f4807SAndreas Gohruse helper_plugin_popularity; 13762f4807SAndreas Gohruse helper_plugin_statistics; 14762f4807SAndreas Gohr 15762f4807SAndreas Gohrclass Logger 16762f4807SAndreas Gohr{ 17762f4807SAndreas Gohr /** @var helper_plugin_statistics The statistics helper plugin instance */ 18762f4807SAndreas Gohr protected helper_plugin_statistics $hlp; 19762f4807SAndreas Gohr 20762f4807SAndreas Gohr /** @var SQLiteDB The SQLite database instance */ 21762f4807SAndreas Gohr protected SQLiteDB $db; 22762f4807SAndreas Gohr 23762f4807SAndreas Gohr /** @var string The full user agent string */ 24762f4807SAndreas Gohr protected string $uaAgent; 25762f4807SAndreas Gohr 26762f4807SAndreas Gohr /** @var string The type of user agent (browser, robot, feedreader) */ 27762f4807SAndreas Gohr protected string $uaType = 'browser'; 28762f4807SAndreas Gohr 29762f4807SAndreas Gohr /** @var string The browser/client name */ 30762f4807SAndreas Gohr protected string $uaName; 31762f4807SAndreas Gohr 32762f4807SAndreas Gohr /** @var string The browser/client version */ 33762f4807SAndreas Gohr protected string $uaVersion; 34762f4807SAndreas Gohr 35762f4807SAndreas Gohr /** @var string The operating system/platform */ 36762f4807SAndreas Gohr protected string $uaPlatform; 37762f4807SAndreas Gohr 384a163f50SAndreas Gohr /** @var string|null The user name, if available */ 394a163f50SAndreas Gohr protected ?string $user = null; 404a163f50SAndreas Gohr 41762f4807SAndreas Gohr /** @var string The unique user identifier */ 42762f4807SAndreas Gohr protected string $uid; 43762f4807SAndreas Gohr 444a163f50SAndreas Gohr /** @var string The session identifier */ 454a163f50SAndreas Gohr protected string $session; 464a163f50SAndreas Gohr 474a163f50SAndreas Gohr /** @var int|null The ID of the main access log entry if any */ 484a163f50SAndreas Gohr protected ?int $hit = null; 494a163f50SAndreas Gohr 504a163f50SAndreas Gohr // region lifecycle 51762f4807SAndreas Gohr 52762f4807SAndreas Gohr /** 53762f4807SAndreas Gohr * Constructor 54762f4807SAndreas Gohr * 55762f4807SAndreas Gohr * Parses browser info and set internal vars 56*523da372SAndreas Gohr * @throws IgnoreException 57762f4807SAndreas Gohr */ 58ba6b3b10SAndreas Gohr public function __construct(helper_plugin_statistics $hlp) 59762f4807SAndreas Gohr { 6041d1fffcSAndreas Gohr /** @var Input $INPUT */ 61762f4807SAndreas Gohr global $INPUT; 62762f4807SAndreas Gohr 63762f4807SAndreas Gohr $this->hlp = $hlp; 64762f4807SAndreas Gohr $this->db = $this->hlp->getDB(); 65762f4807SAndreas Gohr 664a163f50SAndreas Gohr // FIXME if we already have a session, we should not re-parse the user agent 67762f4807SAndreas Gohr 684a163f50SAndreas Gohr $ua = trim($INPUT->server->str('HTTP_USER_AGENT')); 69*523da372SAndreas Gohr AbstractDeviceParser::setVersionTruncation(AbstractParser::VERSION_TRUNCATION_MAJOR); 701c4e3694SAndreas Gohr $dd = new DeviceDetector($ua, ClientHints::factory($_SERVER)); 71762f4807SAndreas Gohr $dd->discardBotInformation(); 72762f4807SAndreas Gohr $dd->parse(); 73762f4807SAndreas Gohr 7400f786d8SAndreas Gohr if ($dd->isFeedReader()) { 7500f786d8SAndreas Gohr $this->uaType = 'feedreader'; 7600f786d8SAndreas Gohr } elseif ($dd->isBot()) { 77762f4807SAndreas Gohr $this->uaType = 'robot'; 78762f4807SAndreas Gohr // for now ignore bots 79c5d2f052SAndreas Gohr throw new IgnoreException('Bot detected, not logging'); 80762f4807SAndreas Gohr } 81762f4807SAndreas Gohr 82762f4807SAndreas Gohr $this->uaAgent = $ua; 83823a6144SAndreas Gohr $this->uaName = $dd->getClient('name') ?: 'Unknown'; 8400f786d8SAndreas Gohr $this->uaVersion = $dd->getClient('version') ?: '0'; 8505786d83SAndreas Gohr $this->uaPlatform = OperatingSystem::getOsFamily($dd->getOs('name')) ?: 'Unknown'; 86762f4807SAndreas Gohr $this->uid = $this->getUID(); 874a163f50SAndreas Gohr $this->session = $this->getSession(); 88d550a4adSAndreas Gohr 89d550a4adSAndreas Gohr if (!$this->hlp->getConf('nousers')) { 9041d1fffcSAndreas Gohr $this->user = $INPUT->server->str('REMOTE_USER', null, true); 91762f4807SAndreas Gohr } 92d550a4adSAndreas Gohr } 93762f4807SAndreas Gohr 94762f4807SAndreas Gohr /** 95762f4807SAndreas Gohr * Should be called before logging 96762f4807SAndreas Gohr * 974a163f50SAndreas Gohr * This starts a transaction, so all logging is done in one go. It also logs the user and session data. 98762f4807SAndreas Gohr */ 99762f4807SAndreas Gohr public function begin(): void 100762f4807SAndreas Gohr { 101*523da372SAndreas Gohr $this->db->getPdo()->beginTransaction(); 1024a163f50SAndreas Gohr 1034a163f50SAndreas Gohr $this->logUser(); 1044a163f50SAndreas Gohr $this->logGroups(); 1054a163f50SAndreas Gohr $this->logDomain(); 1064a163f50SAndreas Gohr $this->logSession(); 1079aec20efSAndreas Gohr $this->logCampaign(); 108762f4807SAndreas Gohr } 109762f4807SAndreas Gohr 110762f4807SAndreas Gohr /** 111762f4807SAndreas Gohr * Should be called after logging 112762f4807SAndreas Gohr * 113762f4807SAndreas Gohr * This commits the transaction started in begin() 114762f4807SAndreas Gohr */ 115762f4807SAndreas Gohr public function end(): void 116762f4807SAndreas Gohr { 117*523da372SAndreas Gohr $this->db->getPdo()->commit(); 118762f4807SAndreas Gohr } 119762f4807SAndreas Gohr 1204a163f50SAndreas Gohr // endregion 1214a163f50SAndreas Gohr // region data gathering 1224a163f50SAndreas Gohr 123762f4807SAndreas Gohr /** 124762f4807SAndreas Gohr * Get the unique user ID 125762f4807SAndreas Gohr * 12604928db4SAndreas Gohr * The user ID is stored in the user preferences and should stay there forever. 127762f4807SAndreas Gohr * @return string The unique user identifier 128*523da372SAndreas Gohr * @throws IgnoreException 129762f4807SAndreas Gohr */ 130762f4807SAndreas Gohr protected function getUID(): string 131762f4807SAndreas Gohr { 13204928db4SAndreas Gohr if (!isset($_SESSION[DOKU_COOKIE]['statistics']['uid'])) { 13304928db4SAndreas Gohr // when there is no session UID set, we assume this was deliberate and we simply abort all logging 13404928db4SAndreas Gohr // @todo we may later make UID generation optional 13504928db4SAndreas Gohr throw new IgnoreException('No user ID found'); 13604928db4SAndreas Gohr } 137762f4807SAndreas Gohr 13804928db4SAndreas Gohr return $_SESSION[DOKU_COOKIE]['statistics']['uid']; 139762f4807SAndreas Gohr } 140762f4807SAndreas Gohr 141762f4807SAndreas Gohr /** 142762f4807SAndreas Gohr * Return the user's session ID 143762f4807SAndreas Gohr * 144762f4807SAndreas Gohr * @return string The session identifier 145*523da372SAndreas Gohr * @throws IgnoreException 146762f4807SAndreas Gohr */ 147762f4807SAndreas Gohr protected function getSession(): string 148762f4807SAndreas Gohr { 14904928db4SAndreas Gohr if (!isset($_SESSION[DOKU_COOKIE]['statistics']['id'])) { 15004928db4SAndreas Gohr // when there is no session ID set, we assume this was deliberate and we simply abort all logging 15104928db4SAndreas Gohr throw new IgnoreException('No session ID found'); 15204928db4SAndreas Gohr } 153762f4807SAndreas Gohr 15404928db4SAndreas Gohr return $_SESSION[DOKU_COOKIE]['statistics']['id']; 155762f4807SAndreas Gohr } 156762f4807SAndreas Gohr 1574a163f50SAndreas Gohr // endregion 1584a163f50SAndreas Gohr // region automatic logging 159762f4807SAndreas Gohr 1604a163f50SAndreas Gohr /** 1614a163f50SAndreas Gohr * Log the user was seen 1624a163f50SAndreas Gohr */ 1634a163f50SAndreas Gohr protected function logUser(): void 1644a163f50SAndreas Gohr { 1654a163f50SAndreas Gohr if (!$this->user) return; 166762f4807SAndreas Gohr 167762f4807SAndreas Gohr $this->db->exec( 1684a163f50SAndreas Gohr 'INSERT INTO users (user, dt) 1694a163f50SAndreas Gohr VALUES (?, CURRENT_TIMESTAMP) 1704a163f50SAndreas Gohr ON CONFLICT (user) DO UPDATE SET 1714a163f50SAndreas Gohr dt = CURRENT_TIMESTAMP 1724a163f50SAndreas Gohr WHERE excluded.user = users.user 1734a163f50SAndreas Gohr ', 1744a163f50SAndreas Gohr $this->user 1754a163f50SAndreas Gohr ); 1764a163f50SAndreas Gohr } 1774a163f50SAndreas Gohr 1784a163f50SAndreas Gohr /** 1794a163f50SAndreas Gohr * Log the session and user agent information 1804a163f50SAndreas Gohr */ 1814a163f50SAndreas Gohr protected function logSession(): void 1824a163f50SAndreas Gohr { 1834a163f50SAndreas Gohr $this->db->exec( 1844a163f50SAndreas Gohr 'INSERT INTO sessions (session, dt, end, uid, user, ua, ua_info, ua_type, ua_ver, os) 1854a163f50SAndreas Gohr VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?) 1864a163f50SAndreas Gohr ON CONFLICT (session) DO UPDATE SET 18741d1fffcSAndreas Gohr end = CURRENT_TIMESTAMP, 18841d1fffcSAndreas Gohr user = excluded.user, 18941d1fffcSAndreas Gohr uid = excluded.uid 1904a163f50SAndreas Gohr WHERE excluded.session = sessions.session 1914a163f50SAndreas Gohr ', 1924a163f50SAndreas Gohr $this->session, 1934a163f50SAndreas Gohr $this->uid, 1944a163f50SAndreas Gohr $this->user, 1954a163f50SAndreas Gohr $this->uaAgent, 1964a163f50SAndreas Gohr $this->uaName, 1974a163f50SAndreas Gohr $this->uaType, 1984a163f50SAndreas Gohr $this->uaVersion, 1994a163f50SAndreas Gohr $this->uaPlatform 200762f4807SAndreas Gohr ); 201762f4807SAndreas Gohr } 202762f4807SAndreas Gohr 203762f4807SAndreas Gohr /** 2049aec20efSAndreas Gohr * Log UTM campaign data 2059aec20efSAndreas Gohr * 2069aec20efSAndreas Gohr * @return void 2079aec20efSAndreas Gohr */ 2089aec20efSAndreas Gohr protected function logCampaign(): void 2099aec20efSAndreas Gohr { 2109aec20efSAndreas Gohr global $INPUT; 2119aec20efSAndreas Gohr 2129aec20efSAndreas Gohr $campaign = $INPUT->filter('trim')->str('utm_campaign', null, true); 2139aec20efSAndreas Gohr $source = $INPUT->filter('trim')->str('utm_source', null, true); 2149aec20efSAndreas Gohr $medium = $INPUT->filter('trim')->str('utm_medium', null, true); 2159aec20efSAndreas Gohr 21659a64057SAndreas Gohr if (!$campaign && !$source && !$medium) return; 2179aec20efSAndreas Gohr 2189aec20efSAndreas Gohr $this->db->exec( 2199aec20efSAndreas Gohr 'INSERT OR IGNORE INTO campaigns (session, campaign, source, medium) 2209aec20efSAndreas Gohr VALUES (?, ?, ?, ?)', 2219aec20efSAndreas Gohr $this->session, 2229aec20efSAndreas Gohr $campaign, 2239aec20efSAndreas Gohr $source, 2249aec20efSAndreas Gohr $medium 2259aec20efSAndreas Gohr ); 2269aec20efSAndreas Gohr } 2279aec20efSAndreas Gohr 2289aec20efSAndreas Gohr /** 2294a163f50SAndreas Gohr * Log all groups for the user 230762f4807SAndreas Gohr * 2314a163f50SAndreas Gohr * @todo maybe this should be done only once per session? 232762f4807SAndreas Gohr */ 2334a163f50SAndreas Gohr protected function logGroups(): void 234762f4807SAndreas Gohr { 2354a163f50SAndreas Gohr global $USERINFO; 236762f4807SAndreas Gohr 2374a163f50SAndreas Gohr if (!$this->user) return; 2384a163f50SAndreas Gohr if (!isset($USERINFO['grps'])) return; 2394a163f50SAndreas Gohr if (!is_array($USERINFO['grps'])) return; 2404a163f50SAndreas Gohr $groups = $USERINFO['grps']; 241fced2f86SAnna Dabrowska 2424a163f50SAndreas Gohr $this->db->exec('DELETE FROM groups WHERE user = ?', $this->user); 243762f4807SAndreas Gohr 244bd514593SAndreas Gohr if ($groups === []) { 24541d1fffcSAndreas Gohr return; 24641d1fffcSAndreas Gohr } 24741d1fffcSAndreas Gohr 24802aa9b73SAndreas Gohr $placeholders = implode(',', array_fill(0, count($groups), '(?, ?)')); 249762f4807SAndreas Gohr $params = []; 2504a163f50SAndreas Gohr $sql = "INSERT INTO groups (`user`, `group`) VALUES $placeholders"; 251762f4807SAndreas Gohr foreach ($groups as $group) { 2524a163f50SAndreas Gohr $params[] = $this->user; 253762f4807SAndreas Gohr $params[] = $group; 254762f4807SAndreas Gohr } 255762f4807SAndreas Gohr $this->db->exec($sql, $params); 256762f4807SAndreas Gohr } 257762f4807SAndreas Gohr 258762f4807SAndreas Gohr /** 2594a163f50SAndreas Gohr * Log email domain 2600b8b7abaSAnna Dabrowska * 2614a163f50SAndreas Gohr * @todo maybe this should be done only once per session? 2620b8b7abaSAnna Dabrowska */ 2634a163f50SAndreas Gohr protected function logDomain(): void 2640b8b7abaSAnna Dabrowska { 2654a163f50SAndreas Gohr global $USERINFO; 2664a163f50SAndreas Gohr if (!$this->user) return; 2674a163f50SAndreas Gohr if (!isset($USERINFO['mail'])) return; 2684a163f50SAndreas Gohr $mail = $USERINFO['mail']; 2690b8b7abaSAnna Dabrowska 2700b8b7abaSAnna Dabrowska $pos = strrpos($mail, '@'); 2710b8b7abaSAnna Dabrowska if (!$pos) return; 2720b8b7abaSAnna Dabrowska $domain = substr($mail, $pos + 1); 2730b8b7abaSAnna Dabrowska if (empty($domain)) return; 2740b8b7abaSAnna Dabrowska 2754a163f50SAndreas Gohr $sql = 'UPDATE users SET domain = ? WHERE user = ?'; 2764a163f50SAndreas Gohr $this->db->exec($sql, [$domain, $this->user]); 2770b8b7abaSAnna Dabrowska } 2780b8b7abaSAnna Dabrowska 2794a163f50SAndreas Gohr // endregion 2804a163f50SAndreas Gohr // region internal loggers called by the dispatchers 2814a163f50SAndreas Gohr 2820b8b7abaSAnna Dabrowska /** 2834a163f50SAndreas Gohr * Log the given referer URL 284762f4807SAndreas Gohr * 2852a30f557SAndreas Gohr * Note: we DO log empty referers. These are external accesses that did not provide a referer URL. 2862a30f557SAndreas Gohr * We do not log referers that are our own pages though. 2872a30f557SAndreas Gohr * 2882a30f557SAndreas Gohr * engine set -> a search engine referer 2892a30f557SAndreas Gohr * no engine set, url empty -> a direct access (bookmark, direct link, etc.) 2902a30f557SAndreas Gohr * no engine set, url not empty -> a referer from another page (not a wiki page) 2912a30f557SAndreas Gohr * null returned -> referer was a wiki page 2922a30f557SAndreas Gohr * 2934a163f50SAndreas Gohr * @param $referer 2942a30f557SAndreas Gohr * @return int|null The referer ID or null if no referer was logged 2952a30f557SAndreas Gohr * @todo we could check against a blacklist here 296762f4807SAndreas Gohr */ 2974a163f50SAndreas Gohr public function logReferer($referer): ?int 298762f4807SAndreas Gohr { 2992a30f557SAndreas Gohr $referer = trim($referer); 300762f4807SAndreas Gohr 301569a5066SAndreas Gohr // do not log our own pages as referers (empty referer is OK though) 302569a5066SAndreas Gohr if (!empty($referer)) { 303569a5066SAndreas Gohr $selfre = '^' . preg_quote(DOKU_URL, '/'); 3042a30f557SAndreas Gohr if (preg_match("/$selfre/", $referer)) { 3052a30f557SAndreas Gohr return null; 3062a30f557SAndreas Gohr } 307569a5066SAndreas Gohr } 308762f4807SAndreas Gohr 3092a30f557SAndreas Gohr // is it a search engine? 3104a163f50SAndreas Gohr $se = new SearchEngines($referer); 31141d1fffcSAndreas Gohr $engine = $se->getEngine(); 312762f4807SAndreas Gohr 31341d1fffcSAndreas Gohr $sql = 'INSERT OR IGNORE INTO referers (url, engine, dt) VALUES (?, ?, CURRENT_TIMESTAMP)'; 314569a5066SAndreas Gohr $this->db->exec($sql, [$referer, $engine]); 315569a5066SAndreas Gohr return (int)$this->db->queryValue('SELECT id FROM referers WHERE url = ?', $referer); 316762f4807SAndreas Gohr } 317762f4807SAndreas Gohr 318762f4807SAndreas Gohr /** 319762f4807SAndreas Gohr * Resolve IP to country/city and store in database 320762f4807SAndreas Gohr * 3214a163f50SAndreas Gohr * @return string The IP address as stored 322762f4807SAndreas Gohr */ 3234a163f50SAndreas Gohr public function logIp(): string 324762f4807SAndreas Gohr { 3254a163f50SAndreas Gohr $ip = clientIP(true); 32669fb56a2SAndreas Gohr 32769fb56a2SAndreas Gohr // anonymize the IP address for storage? 32869fb56a2SAndreas Gohr if ($this->hlp->getConf('anonips')) { 32969fb56a2SAndreas Gohr $hash = md5($ip . strrev($ip)); // we use the reversed IP as salt to avoid common rainbow tables 33069fb56a2SAndreas Gohr $host = ''; 33169fb56a2SAndreas Gohr } else { 33269fb56a2SAndreas Gohr $hash = $ip; 33369fb56a2SAndreas Gohr $host = gethostbyaddr($ip); 33469fb56a2SAndreas Gohr } 3354a163f50SAndreas Gohr 336ba6b3b10SAndreas Gohr if ($this->hlp->getConf('nolocation')) { 337ba6b3b10SAndreas Gohr // if we don't resolve location data, we just return the IP address 338ba6b3b10SAndreas Gohr return $hash; 339ba6b3b10SAndreas Gohr } 340ba6b3b10SAndreas Gohr 341762f4807SAndreas Gohr // check if IP already known and up-to-date 342762f4807SAndreas Gohr $result = $this->db->queryValue( 343762f4807SAndreas Gohr "SELECT ip 344762f4807SAndreas Gohr FROM iplocation 345762f4807SAndreas Gohr WHERE ip = ? 3467a1a7c58SAndreas Gohr AND dt > date('now', '-30 days')", 3474a163f50SAndreas Gohr $hash 348762f4807SAndreas Gohr ); 3494a163f50SAndreas Gohr if ($result) return $hash; // already known and up-to-date 350762f4807SAndreas Gohr 351762f4807SAndreas Gohr 352ba6b3b10SAndreas Gohr // resolve the IP address to location data 353762f4807SAndreas Gohr try { 354ba6b3b10SAndreas Gohr $data = $this->hlp->resolveIP($ip); 355ba6b3b10SAndreas Gohr } catch (IpResolverException $e) { 356ba6b3b10SAndreas Gohr \dokuwiki\Logger::error('Statistics Plugin: ' . $e->getMessage(), $e->details); 357ba6b3b10SAndreas Gohr $data = []; 358762f4807SAndreas Gohr } 359762f4807SAndreas Gohr 360762f4807SAndreas Gohr $this->db->exec( 361762f4807SAndreas Gohr 'INSERT OR REPLACE INTO iplocation ( 3627a1a7c58SAndreas Gohr ip, country, code, city, host, dt 363762f4807SAndreas Gohr ) VALUES ( 364762f4807SAndreas Gohr ?, ?, ?, ?, ?, CURRENT_TIMESTAMP 365762f4807SAndreas Gohr )', 3664a163f50SAndreas Gohr $hash, 36702aa9b73SAndreas Gohr $data['country'] ?? '', 36802aa9b73SAndreas Gohr $data['countryCode'] ?? '', 36902aa9b73SAndreas Gohr $data['city'] ?? '', 3702adee4c6SAndreas Gohr $host 371762f4807SAndreas Gohr ); 3724a163f50SAndreas Gohr 3734a163f50SAndreas Gohr return $hash; 3744a163f50SAndreas Gohr } 3754a163f50SAndreas Gohr 3764a163f50SAndreas Gohr // endregion 3774a163f50SAndreas Gohr // region log dispatchers 3784a163f50SAndreas Gohr 3794a163f50SAndreas Gohr public function logPageView(): void 3804a163f50SAndreas Gohr { 3814a163f50SAndreas Gohr global $INPUT; 3824a163f50SAndreas Gohr 3834a163f50SAndreas Gohr if (!$INPUT->str('p')) return; 3844a163f50SAndreas Gohr 3854a163f50SAndreas Gohr 3864a163f50SAndreas Gohr $referer = $INPUT->filter('trim')->str('r'); 3874a163f50SAndreas Gohr $ip = $this->logIp(); // resolve the IP address 3884a163f50SAndreas Gohr 3894a163f50SAndreas Gohr $data = [ 3904a163f50SAndreas Gohr 'page' => $INPUT->filter('cleanID')->str('p'), 3914a163f50SAndreas Gohr 'ip' => $ip, 3924a163f50SAndreas Gohr 'ref_id' => $this->logReferer($referer), 3934a163f50SAndreas Gohr 'sx' => $INPUT->int('sx'), 3944a163f50SAndreas Gohr 'sy' => $INPUT->int('sy'), 3954a163f50SAndreas Gohr 'vx' => $INPUT->int('vx'), 3964a163f50SAndreas Gohr 'vy' => $INPUT->int('vy'), 3974a163f50SAndreas Gohr 'session' => $this->session, 3984a163f50SAndreas Gohr ]; 3994a163f50SAndreas Gohr 400bd514593SAndreas Gohr $this->db->exec( 401bd514593SAndreas Gohr ' 4024a163f50SAndreas Gohr INSERT INTO pageviews ( 4034a163f50SAndreas Gohr dt, page, ip, ref_id, screen_x, screen_y, view_x, view_y, session 4044a163f50SAndreas Gohr ) VALUES ( 4054a163f50SAndreas Gohr CURRENT_TIMESTAMP, :page, :ip, :ref_id, :sx, :sy, :vx, :vy, :session 4064a163f50SAndreas Gohr ) 4074a163f50SAndreas Gohr ', 4084a163f50SAndreas Gohr $data 4094a163f50SAndreas Gohr ); 410762f4807SAndreas Gohr } 411762f4807SAndreas Gohr 412762f4807SAndreas Gohr /** 413762f4807SAndreas Gohr * Log a click on an external link 414762f4807SAndreas Gohr * 41587e0f0b1SAndreas Gohr * Called from dispatch.php 416762f4807SAndreas Gohr */ 417762f4807SAndreas Gohr public function logOutgoing(): void 418762f4807SAndreas Gohr { 419762f4807SAndreas Gohr global $INPUT; 420762f4807SAndreas Gohr 421762f4807SAndreas Gohr if (!$INPUT->str('ol')) return; 422762f4807SAndreas Gohr 4234a163f50SAndreas Gohr $link = $INPUT->filter('trim')->str('ol'); 4244a163f50SAndreas Gohr $session = $this->session; 4254a163f50SAndreas Gohr $page = $INPUT->filter('cleanID')->str('p'); 426762f4807SAndreas Gohr 427762f4807SAndreas Gohr $this->db->exec( 428762f4807SAndreas Gohr 'INSERT INTO outlinks ( 4294a163f50SAndreas Gohr dt, session, page, link 430762f4807SAndreas Gohr ) VALUES ( 43141d1fffcSAndreas Gohr CURRENT_TIMESTAMP, ?, ?, ? 432762f4807SAndreas Gohr )', 4332adee4c6SAndreas Gohr $session, 4342adee4c6SAndreas Gohr $page, 4352adee4c6SAndreas Gohr $link 436762f4807SAndreas Gohr ); 437762f4807SAndreas Gohr } 438762f4807SAndreas Gohr 439762f4807SAndreas Gohr /** 440762f4807SAndreas Gohr * Log access to a media file 441762f4807SAndreas Gohr * 442762f4807SAndreas Gohr * Called from action.php 443762f4807SAndreas Gohr * 444762f4807SAndreas Gohr * @param string $media The media ID 445762f4807SAndreas Gohr * @param string $mime The media's mime type 446762f4807SAndreas Gohr * @param bool $inline Is this displayed inline? 447762f4807SAndreas Gohr * @param int $size Size of the media file 448762f4807SAndreas Gohr */ 449762f4807SAndreas Gohr public function logMedia(string $media, string $mime, bool $inline, int $size): void 450762f4807SAndreas Gohr { 451762f4807SAndreas Gohr [$mime1, $mime2] = explode('/', strtolower($mime)); 452762f4807SAndreas Gohr $inline = $inline ? 1 : 0; 453762f4807SAndreas Gohr 454762f4807SAndreas Gohr 4554a163f50SAndreas Gohr $data = [ 4564a163f50SAndreas Gohr 'media' => cleanID($media), 4574a163f50SAndreas Gohr 'ip' => $this->logIp(), // resolve the IP address 4584a163f50SAndreas Gohr 'session' => $this->session, 4594a163f50SAndreas Gohr 'size' => $size, 4604a163f50SAndreas Gohr 'mime1' => $mime1, 4614a163f50SAndreas Gohr 'mime2' => $mime2, 4624a163f50SAndreas Gohr 'inline' => $inline, 4634a163f50SAndreas Gohr ]; 4644a163f50SAndreas Gohr 465bd514593SAndreas Gohr $this->db->exec( 466bd514593SAndreas Gohr ' 4674a163f50SAndreas Gohr INSERT INTO media ( dt, media, ip, session, size, mime1, mime2, inline ) 4684a163f50SAndreas Gohr VALUES (CURRENT_TIMESTAMP, :media, :ip, :session, :size, :mime1, :mime2, :inline) 4694a163f50SAndreas Gohr ', 4704a163f50SAndreas Gohr $data 471762f4807SAndreas Gohr ); 472762f4807SAndreas Gohr } 473762f4807SAndreas Gohr 474762f4807SAndreas Gohr /** 475762f4807SAndreas Gohr * Log page edits 476762f4807SAndreas Gohr * 4774a163f50SAndreas Gohr * called from action.php 4784a163f50SAndreas Gohr * 479762f4807SAndreas Gohr * @param string $page The page that was edited 480762f4807SAndreas Gohr * @param string $type The type of edit (create, edit, etc.) 481762f4807SAndreas Gohr */ 482762f4807SAndreas Gohr public function logEdit(string $page, string $type): void 483762f4807SAndreas Gohr { 4844a163f50SAndreas Gohr $data = [ 4854a163f50SAndreas Gohr 'page' => cleanID($page), 4864a163f50SAndreas Gohr 'type' => $type, 4874a163f50SAndreas Gohr 'ip' => $this->logIp(), // resolve the IP address 4884a163f50SAndreas Gohr 'session' => $this->session 4894a163f50SAndreas Gohr ]; 490762f4807SAndreas Gohr 49141d1fffcSAndreas Gohr $this->db->exec( 492762f4807SAndreas Gohr 'INSERT INTO edits ( 4934a163f50SAndreas Gohr dt, page, type, ip, session 494762f4807SAndreas Gohr ) VALUES ( 4954a163f50SAndreas Gohr CURRENT_TIMESTAMP, :page, :type, :ip, :session 496762f4807SAndreas Gohr )', 4974a163f50SAndreas Gohr $data 498762f4807SAndreas Gohr ); 499762f4807SAndreas Gohr } 500762f4807SAndreas Gohr 501762f4807SAndreas Gohr /** 502762f4807SAndreas Gohr * Log login/logoffs and user creations 503762f4807SAndreas Gohr * 504af93d154SAndreas Gohr * @param string $type The type of login event (login, logout, create, failed) 505af93d154SAndreas Gohr * @param string $user The username 506762f4807SAndreas Gohr */ 507762f4807SAndreas Gohr public function logLogin(string $type, string $user = ''): void 508762f4807SAndreas Gohr { 509762f4807SAndreas Gohr global $INPUT; 510762f4807SAndreas Gohr 511762f4807SAndreas Gohr if (!$user) $user = $INPUT->server->str('REMOTE_USER'); 512762f4807SAndreas Gohr 513762f4807SAndreas Gohr $ip = clientIP(true); 514762f4807SAndreas Gohr 515762f4807SAndreas Gohr $this->db->exec( 516762f4807SAndreas Gohr 'INSERT INTO logins ( 517af93d154SAndreas Gohr dt, ip, user, type 518762f4807SAndreas Gohr ) VALUES ( 519af93d154SAndreas Gohr CURRENT_TIMESTAMP, ?, ?, ? 520762f4807SAndreas Gohr )', 5212adee4c6SAndreas Gohr $ip, 5222adee4c6SAndreas Gohr $user, 523af93d154SAndreas Gohr $type 524762f4807SAndreas Gohr ); 525762f4807SAndreas Gohr } 526762f4807SAndreas Gohr 527762f4807SAndreas Gohr /** 52802aa9b73SAndreas Gohr * Log search data to the search related tables 52902aa9b73SAndreas Gohr * 53002aa9b73SAndreas Gohr * @param string $query The search query 53102aa9b73SAndreas Gohr * @param string[] $words The query split into words 53202aa9b73SAndreas Gohr */ 53302aa9b73SAndreas Gohr public function logSearch(string $query, array $words): void 53402aa9b73SAndreas Gohr { 53502aa9b73SAndreas Gohr if (!$query) return; 53602aa9b73SAndreas Gohr 53702aa9b73SAndreas Gohr $sid = $this->db->exec( 53802aa9b73SAndreas Gohr 'INSERT INTO search (dt, ip, session, query) VALUES (CURRENT_TIMESTAMP, ?, ? , ?)', 53902aa9b73SAndreas Gohr $this->logIp(), // resolve the IP address 54002aa9b73SAndreas Gohr $this->session, 54102aa9b73SAndreas Gohr $query, 54202aa9b73SAndreas Gohr ); 54302aa9b73SAndreas Gohr 54402aa9b73SAndreas Gohr foreach ($words as $word) { 54502aa9b73SAndreas Gohr if (!$word) continue; 54602aa9b73SAndreas Gohr $this->db->exec( 54702aa9b73SAndreas Gohr 'INSERT INTO searchwords (sid, word) VALUES (?, ?)', 54802aa9b73SAndreas Gohr $sid, 54902aa9b73SAndreas Gohr $word 55002aa9b73SAndreas Gohr ); 55102aa9b73SAndreas Gohr } 55202aa9b73SAndreas Gohr } 55302aa9b73SAndreas Gohr 55402aa9b73SAndreas Gohr /** 555762f4807SAndreas Gohr * Log the current page count and size as today's history entry 556762f4807SAndreas Gohr */ 557762f4807SAndreas Gohr public function logHistoryPages(): void 558762f4807SAndreas Gohr { 559762f4807SAndreas Gohr global $conf; 560762f4807SAndreas Gohr 561762f4807SAndreas Gohr // use the popularity plugin's search method to find the wanted data 562762f4807SAndreas Gohr /** @var helper_plugin_popularity $pop */ 563762f4807SAndreas Gohr $pop = plugin_load('helper', 'popularity'); 564b188870fSAndreas Gohr $list = $this->initEmptySearchList(); 565762f4807SAndreas Gohr search($list, $conf['datadir'], [$pop, 'searchCountCallback'], ['all' => false], ''); 566762f4807SAndreas Gohr $page_count = $list['file_count']; 567762f4807SAndreas Gohr $page_size = $list['file_size']; 568762f4807SAndreas Gohr 569762f4807SAndreas Gohr $this->db->exec( 570762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 571762f4807SAndreas Gohr info, value, dt 572762f4807SAndreas Gohr ) VALUES ( 573483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 574762f4807SAndreas Gohr )', 5752adee4c6SAndreas Gohr 'page_count', 5762adee4c6SAndreas Gohr $page_count 577762f4807SAndreas Gohr ); 578762f4807SAndreas Gohr $this->db->exec( 579762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 580762f4807SAndreas Gohr info, value, dt 581762f4807SAndreas Gohr ) VALUES ( 582483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 583762f4807SAndreas Gohr )', 5842adee4c6SAndreas Gohr 'page_size', 5852adee4c6SAndreas Gohr $page_size 586762f4807SAndreas Gohr ); 587762f4807SAndreas Gohr } 588762f4807SAndreas Gohr 589762f4807SAndreas Gohr /** 590762f4807SAndreas Gohr * Log the current media count and size as today's history entry 591762f4807SAndreas Gohr */ 592762f4807SAndreas Gohr public function logHistoryMedia(): void 593762f4807SAndreas Gohr { 594762f4807SAndreas Gohr global $conf; 595762f4807SAndreas Gohr 596762f4807SAndreas Gohr // use the popularity plugin's search method to find the wanted data 597762f4807SAndreas Gohr /** @var helper_plugin_popularity $pop */ 598762f4807SAndreas Gohr $pop = plugin_load('helper', 'popularity'); 599b188870fSAndreas Gohr $list = $this->initEmptySearchList(); 600762f4807SAndreas Gohr search($list, $conf['mediadir'], [$pop, 'searchCountCallback'], ['all' => true], ''); 601762f4807SAndreas Gohr $media_count = $list['file_count']; 602762f4807SAndreas Gohr $media_size = $list['file_size']; 603762f4807SAndreas Gohr 604762f4807SAndreas Gohr $this->db->exec( 605762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 606762f4807SAndreas Gohr info, value, dt 607762f4807SAndreas Gohr ) VALUES ( 608483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 609762f4807SAndreas Gohr )', 6102adee4c6SAndreas Gohr 'media_count', 6112adee4c6SAndreas Gohr $media_count 612762f4807SAndreas Gohr ); 613762f4807SAndreas Gohr $this->db->exec( 614762f4807SAndreas Gohr 'INSERT OR REPLACE INTO history ( 615762f4807SAndreas Gohr info, value, dt 616762f4807SAndreas Gohr ) VALUES ( 617483101d3SAndreas Gohr ?, ?, CURRENT_TIMESTAMP 618762f4807SAndreas Gohr )', 6192adee4c6SAndreas Gohr 'media_size', 6202adee4c6SAndreas Gohr $media_size 621762f4807SAndreas Gohr ); 622762f4807SAndreas Gohr } 623b188870fSAndreas Gohr 6244a163f50SAndreas Gohr // endregion 6254a163f50SAndreas Gohr 626b188870fSAndreas Gohr /** 627b188870fSAndreas Gohr * @todo can be dropped in favor of helper_plugin_popularity::initEmptySearchList() once it's public 628b188870fSAndreas Gohr * @return array 629b188870fSAndreas Gohr */ 630b188870fSAndreas Gohr protected function initEmptySearchList() 631b188870fSAndreas Gohr { 632b188870fSAndreas Gohr return array_fill_keys([ 633b188870fSAndreas Gohr 'file_count', 634b188870fSAndreas Gohr 'file_size', 635b188870fSAndreas Gohr 'file_max', 636b188870fSAndreas Gohr 'file_min', 637b188870fSAndreas Gohr 'dir_count', 638b188870fSAndreas Gohr 'dir_nest', 639b188870fSAndreas Gohr 'file_oldest' 640b188870fSAndreas Gohr ], 0); 641b188870fSAndreas Gohr } 642762f4807SAndreas Gohr} 643