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