xref: /plugin/statistics/Logger.php (revision 69fb56a24f2453b927cc4a932234a024a80bedd0)
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