xref: /plugin/statistics/Logger.php (revision 7a1a7c58b8012671597983163b5701969480e69a)
1762f4807SAndreas Gohr<?php
2762f4807SAndreas Gohr
3762f4807SAndreas Gohrnamespace dokuwiki\plugin\statistics;
4762f4807SAndreas Gohr
5762f4807SAndreas Gohruse DeviceDetector\DeviceDetector;
6762f4807SAndreas Gohruse DeviceDetector\Parser\Client\Browser;
7762f4807SAndreas Gohruse DeviceDetector\Parser\Device\AbstractDeviceParser;
8762f4807SAndreas Gohruse DeviceDetector\Parser\OperatingSystem;
941d1fffcSAndreas Gohruse dokuwiki\Input\Input;
10762f4807SAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB;
11762f4807SAndreas Gohruse helper_plugin_popularity;
12762f4807SAndreas Gohruse helper_plugin_statistics;
13762f4807SAndreas Gohr
14762f4807SAndreas Gohrclass Logger
15762f4807SAndreas Gohr{
16762f4807SAndreas Gohr    /** @var helper_plugin_statistics The statistics helper plugin instance */
17762f4807SAndreas Gohr    protected helper_plugin_statistics $hlp;
18762f4807SAndreas Gohr
19762f4807SAndreas Gohr    /** @var SQLiteDB The SQLite database instance */
20762f4807SAndreas Gohr    protected SQLiteDB $db;
21762f4807SAndreas Gohr
22762f4807SAndreas Gohr    /** @var string The full user agent string */
23762f4807SAndreas Gohr    protected string $uaAgent;
24762f4807SAndreas Gohr
25762f4807SAndreas Gohr    /** @var string The type of user agent (browser, robot, feedreader) */
26762f4807SAndreas Gohr    protected string $uaType = 'browser';
27762f4807SAndreas Gohr
28762f4807SAndreas Gohr    /** @var string The browser/client name */
29762f4807SAndreas Gohr    protected string $uaName;
30762f4807SAndreas Gohr
31762f4807SAndreas Gohr    /** @var string The browser/client version */
32762f4807SAndreas Gohr    protected string $uaVersion;
33762f4807SAndreas Gohr
34762f4807SAndreas Gohr    /** @var string The operating system/platform */
35762f4807SAndreas Gohr    protected string $uaPlatform;
36762f4807SAndreas Gohr
374a163f50SAndreas Gohr    /** @var string|null The user name, if available */
384a163f50SAndreas Gohr    protected ?string $user = null;
394a163f50SAndreas Gohr
40762f4807SAndreas Gohr    /** @var string The unique user identifier */
41762f4807SAndreas Gohr    protected string $uid;
42762f4807SAndreas Gohr
434a163f50SAndreas Gohr    /** @var string The session identifier */
444a163f50SAndreas Gohr    protected string $session;
454a163f50SAndreas Gohr
464a163f50SAndreas Gohr    /** @var int|null The ID of the main access log entry if any */
474a163f50SAndreas Gohr    protected ?int $hit = null;
484a163f50SAndreas Gohr
494a163f50SAndreas Gohr    // region lifecycle
50762f4807SAndreas Gohr
51762f4807SAndreas Gohr    /**
52762f4807SAndreas Gohr     * Constructor
53762f4807SAndreas Gohr     *
54762f4807SAndreas Gohr     * Parses browser info and set internal vars
55762f4807SAndreas Gohr     */
56ba6b3b10SAndreas Gohr    public function __construct(helper_plugin_statistics $hlp)
57762f4807SAndreas Gohr    {
5841d1fffcSAndreas Gohr        /** @var Input $INPUT */
59762f4807SAndreas Gohr        global $INPUT;
60762f4807SAndreas Gohr
61762f4807SAndreas Gohr        $this->hlp = $hlp;
62762f4807SAndreas Gohr        $this->db = $this->hlp->getDB();
63762f4807SAndreas Gohr
644a163f50SAndreas Gohr        // FIXME if we already have a session, we should not re-parse the user agent
65762f4807SAndreas Gohr
664a163f50SAndreas Gohr        $ua = trim($INPUT->server->str('HTTP_USER_AGENT'));
67762f4807SAndreas Gohr        AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_MAJOR);
68762f4807SAndreas Gohr        $dd = new DeviceDetector($ua); // FIXME we could use client hints, but need to add headers
69762f4807SAndreas Gohr        $dd->discardBotInformation();
70762f4807SAndreas Gohr        $dd->parse();
71762f4807SAndreas Gohr
7200f786d8SAndreas Gohr        if ($dd->isFeedReader()) {
7300f786d8SAndreas Gohr            $this->uaType = 'feedreader';
7400f786d8SAndreas Gohr        } elseif ($dd->isBot()) {
75762f4807SAndreas Gohr            $this->uaType = 'robot';
76762f4807SAndreas Gohr            // for now ignore bots
77c5d2f052SAndreas Gohr            throw new IgnoreException('Bot detected, not logging');
78762f4807SAndreas Gohr        }
79762f4807SAndreas Gohr
80762f4807SAndreas Gohr        $this->uaAgent = $ua;
8105786d83SAndreas Gohr        $this->uaName = Browser::getBrowserFamily($dd->getClient('name')) ?: 'Unknown';
8200f786d8SAndreas Gohr        $this->uaVersion = $dd->getClient('version') ?: '0';
8305786d83SAndreas Gohr        $this->uaPlatform = OperatingSystem::getOsFamily($dd->getOs('name')) ?: 'Unknown';
84762f4807SAndreas Gohr        $this->uid = $this->getUID();
854a163f50SAndreas Gohr        $this->session = $this->getSession();
86d550a4adSAndreas Gohr
87d550a4adSAndreas Gohr        if (!$this->hlp->getConf('nousers')) {
8841d1fffcSAndreas Gohr            $this->user = $INPUT->server->str('REMOTE_USER', null, true);
89762f4807SAndreas Gohr        }
90d550a4adSAndreas Gohr    }
91762f4807SAndreas Gohr
92762f4807SAndreas Gohr    /**
93762f4807SAndreas Gohr     * Should be called before logging
94762f4807SAndreas Gohr     *
954a163f50SAndreas Gohr     * This starts a transaction, so all logging is done in one go. It also logs the user and session data.
96762f4807SAndreas Gohr     */
97762f4807SAndreas Gohr    public function begin(): void
98762f4807SAndreas Gohr    {
99762f4807SAndreas Gohr        $this->hlp->getDB()->getPdo()->beginTransaction();
1004a163f50SAndreas Gohr
1014a163f50SAndreas Gohr        $this->logUser();
1024a163f50SAndreas Gohr        $this->logGroups();
1034a163f50SAndreas Gohr        $this->logDomain();
1044a163f50SAndreas Gohr        $this->logSession();
105762f4807SAndreas Gohr    }
106762f4807SAndreas Gohr
107762f4807SAndreas Gohr    /**
108762f4807SAndreas Gohr     * Should be called after logging
109762f4807SAndreas Gohr     *
110762f4807SAndreas Gohr     * This commits the transaction started in begin()
111762f4807SAndreas Gohr     */
112762f4807SAndreas Gohr    public function end(): void
113762f4807SAndreas Gohr    {
114762f4807SAndreas Gohr        $this->hlp->getDB()->getPdo()->commit();
115762f4807SAndreas Gohr    }
116762f4807SAndreas Gohr
1174a163f50SAndreas Gohr    // endregion
1184a163f50SAndreas Gohr    // region data gathering
1194a163f50SAndreas Gohr
120762f4807SAndreas Gohr    /**
121762f4807SAndreas Gohr     * Get the unique user ID
122762f4807SAndreas Gohr     *
12304928db4SAndreas Gohr     * The user ID is stored in the user preferences and should stay there forever.
124762f4807SAndreas Gohr     * @return string The unique user identifier
125762f4807SAndreas Gohr     */
126762f4807SAndreas Gohr    protected function getUID(): string
127762f4807SAndreas Gohr    {
12804928db4SAndreas Gohr        if (!isset($_SESSION[DOKU_COOKIE]['statistics']['uid'])) {
12904928db4SAndreas Gohr            // when there is no session UID set, we assume this was deliberate and we simply abort all logging
13004928db4SAndreas Gohr            // @todo we may later make UID generation optional
13104928db4SAndreas Gohr            throw new IgnoreException('No user ID found');
13204928db4SAndreas Gohr        }
133762f4807SAndreas Gohr
13404928db4SAndreas Gohr        return $_SESSION[DOKU_COOKIE]['statistics']['uid'];
135762f4807SAndreas Gohr    }
136762f4807SAndreas Gohr
137762f4807SAndreas Gohr    /**
138762f4807SAndreas Gohr     * Return the user's session ID
139762f4807SAndreas Gohr     *
140762f4807SAndreas Gohr     * @return string The session identifier
141762f4807SAndreas Gohr     */
142762f4807SAndreas Gohr    protected function getSession(): string
143762f4807SAndreas Gohr    {
14404928db4SAndreas Gohr        if (!isset($_SESSION[DOKU_COOKIE]['statistics']['id'])) {
14504928db4SAndreas Gohr            // when there is no session ID set, we assume this was deliberate and we simply abort all logging
14604928db4SAndreas Gohr            throw new IgnoreException('No session ID found');
14704928db4SAndreas Gohr        }
148762f4807SAndreas Gohr
14904928db4SAndreas Gohr        return $_SESSION[DOKU_COOKIE]['statistics']['id'];
150762f4807SAndreas Gohr    }
151762f4807SAndreas Gohr
1524a163f50SAndreas Gohr    // endregion
1534a163f50SAndreas Gohr    // region automatic logging
154762f4807SAndreas Gohr
1554a163f50SAndreas Gohr    /**
1564a163f50SAndreas Gohr     * Log the user was seen
1574a163f50SAndreas Gohr     */
1584a163f50SAndreas Gohr    protected function logUser(): void
1594a163f50SAndreas Gohr    {
1604a163f50SAndreas Gohr        if (!$this->user) return;
161762f4807SAndreas Gohr
162762f4807SAndreas Gohr        $this->db->exec(
1634a163f50SAndreas Gohr            'INSERT INTO users (user, dt)
1644a163f50SAndreas Gohr                  VALUES (?, CURRENT_TIMESTAMP)
1654a163f50SAndreas Gohr            ON CONFLICT (user) DO UPDATE SET
1664a163f50SAndreas Gohr                         dt = CURRENT_TIMESTAMP
1674a163f50SAndreas Gohr                   WHERE excluded.user = users.user
1684a163f50SAndreas Gohr            ',
1694a163f50SAndreas Gohr            $this->user
1704a163f50SAndreas Gohr        );
1714a163f50SAndreas Gohr    }
1724a163f50SAndreas Gohr
1734a163f50SAndreas Gohr    /**
1744a163f50SAndreas Gohr     * Log the session and user agent information
1754a163f50SAndreas Gohr     */
1764a163f50SAndreas Gohr    protected function logSession(): void
1774a163f50SAndreas Gohr    {
1784a163f50SAndreas Gohr        $this->db->exec(
1794a163f50SAndreas Gohr            'INSERT INTO sessions (session, dt, end, uid, user, ua, ua_info, ua_type, ua_ver, os)
1804a163f50SAndreas Gohr                  VALUES (?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?)
1814a163f50SAndreas Gohr             ON CONFLICT (session) DO UPDATE SET
18241d1fffcSAndreas Gohr                         end = CURRENT_TIMESTAMP,
18341d1fffcSAndreas Gohr                         user = excluded.user,
18441d1fffcSAndreas Gohr                         uid = excluded.uid
1854a163f50SAndreas Gohr                   WHERE excluded.session = sessions.session
1864a163f50SAndreas Gohr             ',
1874a163f50SAndreas Gohr            $this->session,
1884a163f50SAndreas Gohr            $this->uid,
1894a163f50SAndreas Gohr            $this->user,
1904a163f50SAndreas Gohr            $this->uaAgent,
1914a163f50SAndreas Gohr            $this->uaName,
1924a163f50SAndreas Gohr            $this->uaType,
1934a163f50SAndreas Gohr            $this->uaVersion,
1944a163f50SAndreas Gohr            $this->uaPlatform
195762f4807SAndreas Gohr        );
196762f4807SAndreas Gohr    }
197762f4807SAndreas Gohr
198762f4807SAndreas Gohr    /**
1994a163f50SAndreas Gohr     * Log all groups for the user
200762f4807SAndreas Gohr     *
2014a163f50SAndreas Gohr     * @todo maybe this should be done only once per session?
202762f4807SAndreas Gohr     */
2034a163f50SAndreas Gohr    protected function logGroups(): void
204762f4807SAndreas Gohr    {
2054a163f50SAndreas Gohr        global $USERINFO;
206762f4807SAndreas Gohr
2074a163f50SAndreas Gohr        if (!$this->user) return;
2084a163f50SAndreas Gohr        if (!isset($USERINFO['grps'])) return;
2094a163f50SAndreas Gohr        if (!is_array($USERINFO['grps'])) return;
2104a163f50SAndreas Gohr        $groups = $USERINFO['grps'];
211fced2f86SAnna Dabrowska
2124a163f50SAndreas Gohr        $this->db->exec('DELETE FROM groups WHERE user = ?', $this->user);
213762f4807SAndreas Gohr
214bd514593SAndreas Gohr        if ($groups === []) {
21541d1fffcSAndreas Gohr            return;
21641d1fffcSAndreas Gohr        }
21741d1fffcSAndreas Gohr
21802aa9b73SAndreas Gohr        $placeholders = implode(',', array_fill(0, count($groups), '(?, ?)'));
219762f4807SAndreas Gohr        $params = [];
2204a163f50SAndreas Gohr        $sql = "INSERT INTO groups (`user`, `group`) VALUES $placeholders";
221762f4807SAndreas Gohr        foreach ($groups as $group) {
2224a163f50SAndreas Gohr            $params[] = $this->user;
223762f4807SAndreas Gohr            $params[] = $group;
224762f4807SAndreas Gohr        }
225762f4807SAndreas Gohr        $this->db->exec($sql, $params);
226762f4807SAndreas Gohr    }
227762f4807SAndreas Gohr
228762f4807SAndreas Gohr    /**
2294a163f50SAndreas Gohr     * Log email domain
2300b8b7abaSAnna Dabrowska     *
2314a163f50SAndreas Gohr     * @todo maybe this should be done only once per session?
2320b8b7abaSAnna Dabrowska     */
2334a163f50SAndreas Gohr    protected function logDomain(): void
2340b8b7abaSAnna Dabrowska    {
2354a163f50SAndreas Gohr        global $USERINFO;
2364a163f50SAndreas Gohr        if (!$this->user) return;
2374a163f50SAndreas Gohr        if (!isset($USERINFO['mail'])) return;
2384a163f50SAndreas Gohr        $mail = $USERINFO['mail'];
2390b8b7abaSAnna Dabrowska
2400b8b7abaSAnna Dabrowska        $pos = strrpos($mail, '@');
2410b8b7abaSAnna Dabrowska        if (!$pos) return;
2420b8b7abaSAnna Dabrowska        $domain = substr($mail, $pos + 1);
2430b8b7abaSAnna Dabrowska        if (empty($domain)) return;
2440b8b7abaSAnna Dabrowska
2454a163f50SAndreas Gohr        $sql = 'UPDATE users SET domain = ? WHERE user = ?';
2464a163f50SAndreas Gohr        $this->db->exec($sql, [$domain, $this->user]);
2470b8b7abaSAnna Dabrowska    }
2480b8b7abaSAnna Dabrowska
2494a163f50SAndreas Gohr    // endregion
2504a163f50SAndreas Gohr    // region internal loggers called by the dispatchers
2514a163f50SAndreas Gohr
2520b8b7abaSAnna Dabrowska    /**
2534a163f50SAndreas Gohr     * Log the given referer URL
254762f4807SAndreas Gohr     *
2552a30f557SAndreas Gohr     * Note: we DO log empty referers. These are external accesses that did not provide a referer URL.
2562a30f557SAndreas Gohr     * We do not log referers that are our own pages though.
2572a30f557SAndreas Gohr     *
2582a30f557SAndreas Gohr     * engine set -> a search engine referer
2592a30f557SAndreas Gohr     * no engine set, url empty -> a direct access (bookmark, direct link, etc.)
2602a30f557SAndreas Gohr     * no engine set, url not empty -> a referer from another page (not a wiki page)
2612a30f557SAndreas Gohr     * null returned -> referer was a wiki page
2622a30f557SAndreas Gohr     *
2634a163f50SAndreas Gohr     * @param $referer
2642a30f557SAndreas Gohr     * @return int|null The referer ID or null if no referer was logged
2652a30f557SAndreas Gohr     * @todo we could check against a blacklist here
266762f4807SAndreas Gohr     */
2674a163f50SAndreas Gohr    public function logReferer($referer): ?int
268762f4807SAndreas Gohr    {
2692a30f557SAndreas Gohr        $referer = trim($referer);
270762f4807SAndreas Gohr
271569a5066SAndreas Gohr        // do not log our own pages as referers (empty referer is OK though)
272569a5066SAndreas Gohr        if (!empty($referer)) {
273569a5066SAndreas Gohr            $selfre = '^' . preg_quote(DOKU_URL, '/');
2742a30f557SAndreas Gohr            if (preg_match("/$selfre/", $referer)) {
2752a30f557SAndreas Gohr                return null;
2762a30f557SAndreas Gohr            }
277569a5066SAndreas Gohr        }
278762f4807SAndreas Gohr
2792a30f557SAndreas Gohr        // is it a search engine?
2804a163f50SAndreas Gohr        $se = new SearchEngines($referer);
28141d1fffcSAndreas Gohr        $engine = $se->getEngine();
282762f4807SAndreas Gohr
28341d1fffcSAndreas Gohr        $sql = 'INSERT OR IGNORE INTO referers (url, engine, dt) VALUES (?, ?, CURRENT_TIMESTAMP)';
284569a5066SAndreas Gohr        $this->db->exec($sql, [$referer, $engine]);
285569a5066SAndreas Gohr        return (int)$this->db->queryValue('SELECT id FROM referers WHERE url = ?', $referer);
286762f4807SAndreas Gohr    }
287762f4807SAndreas Gohr
288762f4807SAndreas Gohr    /**
289762f4807SAndreas Gohr     * Resolve IP to country/city and store in database
290762f4807SAndreas Gohr     *
2914a163f50SAndreas Gohr     * @return string The IP address as stored
292762f4807SAndreas Gohr     */
2934a163f50SAndreas Gohr    public function logIp(): string
294762f4807SAndreas Gohr    {
2954a163f50SAndreas Gohr        $ip = clientIP(true);
29669fb56a2SAndreas Gohr
29769fb56a2SAndreas Gohr        // anonymize the IP address for storage?
29869fb56a2SAndreas Gohr        if ($this->hlp->getConf('anonips')) {
29969fb56a2SAndreas Gohr            $hash = md5($ip . strrev($ip)); // we use the reversed IP as salt to avoid common rainbow tables
30069fb56a2SAndreas Gohr            $host = '';
30169fb56a2SAndreas Gohr        } else {
30269fb56a2SAndreas Gohr            $hash = $ip;
30369fb56a2SAndreas Gohr            $host = gethostbyaddr($ip);
30469fb56a2SAndreas Gohr        }
3054a163f50SAndreas Gohr
306ba6b3b10SAndreas Gohr        if ($this->hlp->getConf('nolocation')) {
307ba6b3b10SAndreas Gohr            // if we don't resolve location data, we just return the IP address
308ba6b3b10SAndreas Gohr            return $hash;
309ba6b3b10SAndreas Gohr        }
310ba6b3b10SAndreas Gohr
311762f4807SAndreas Gohr        // check if IP already known and up-to-date
312762f4807SAndreas Gohr        $result = $this->db->queryValue(
313762f4807SAndreas Gohr            "SELECT ip
314762f4807SAndreas Gohr             FROM   iplocation
315762f4807SAndreas Gohr             WHERE  ip = ?
316*7a1a7c58SAndreas Gohr               AND  dt > date('now', '-30 days')",
3174a163f50SAndreas Gohr            $hash
318762f4807SAndreas Gohr        );
3194a163f50SAndreas Gohr        if ($result) return $hash; // already known and up-to-date
320762f4807SAndreas Gohr
321762f4807SAndreas Gohr
322ba6b3b10SAndreas Gohr        // resolve the IP address to location data
323762f4807SAndreas Gohr        try {
324ba6b3b10SAndreas Gohr            $data = $this->hlp->resolveIP($ip);
325ba6b3b10SAndreas Gohr        } catch (IpResolverException $e) {
326ba6b3b10SAndreas Gohr            \dokuwiki\Logger::error('Statistics Plugin: ' . $e->getMessage(), $e->details);
327ba6b3b10SAndreas Gohr            $data = [];
328762f4807SAndreas Gohr        }
329762f4807SAndreas Gohr
330762f4807SAndreas Gohr        $this->db->exec(
331762f4807SAndreas Gohr            'INSERT OR REPLACE INTO iplocation (
332*7a1a7c58SAndreas Gohr                    ip, country, code, city, host, dt
333762f4807SAndreas Gohr                 ) VALUES (
334762f4807SAndreas Gohr                    ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
335762f4807SAndreas Gohr                 )',
3364a163f50SAndreas Gohr            $hash,
33702aa9b73SAndreas Gohr            $data['country'] ?? '',
33802aa9b73SAndreas Gohr            $data['countryCode'] ?? '',
33902aa9b73SAndreas Gohr            $data['city'] ?? '',
3402adee4c6SAndreas Gohr            $host
341762f4807SAndreas Gohr        );
3424a163f50SAndreas Gohr
3434a163f50SAndreas Gohr        return $hash;
3444a163f50SAndreas Gohr    }
3454a163f50SAndreas Gohr
3464a163f50SAndreas Gohr    // endregion
3474a163f50SAndreas Gohr    // region log dispatchers
3484a163f50SAndreas Gohr
3494a163f50SAndreas Gohr    public function logPageView(): void
3504a163f50SAndreas Gohr    {
3514a163f50SAndreas Gohr        global $INPUT;
3524a163f50SAndreas Gohr
3534a163f50SAndreas Gohr        if (!$INPUT->str('p')) return;
3544a163f50SAndreas Gohr
3554a163f50SAndreas Gohr
3564a163f50SAndreas Gohr        $referer = $INPUT->filter('trim')->str('r');
3574a163f50SAndreas Gohr        $ip = $this->logIp(); // resolve the IP address
3584a163f50SAndreas Gohr
3594a163f50SAndreas Gohr        $data = [
3604a163f50SAndreas Gohr            'page' => $INPUT->filter('cleanID')->str('p'),
3614a163f50SAndreas Gohr            'ip' => $ip,
3624a163f50SAndreas Gohr            'ref_id' => $this->logReferer($referer),
3634a163f50SAndreas Gohr            'sx' => $INPUT->int('sx'),
3644a163f50SAndreas Gohr            'sy' => $INPUT->int('sy'),
3654a163f50SAndreas Gohr            'vx' => $INPUT->int('vx'),
3664a163f50SAndreas Gohr            'vy' => $INPUT->int('vy'),
3674a163f50SAndreas Gohr            'session' => $this->session,
3684a163f50SAndreas Gohr        ];
3694a163f50SAndreas Gohr
370bd514593SAndreas Gohr        $this->db->exec(
371bd514593SAndreas Gohr            '
3724a163f50SAndreas Gohr        INSERT INTO pageviews (
3734a163f50SAndreas Gohr            dt, page, ip, ref_id, screen_x, screen_y, view_x, view_y, session
3744a163f50SAndreas Gohr        ) VALUES (
3754a163f50SAndreas Gohr            CURRENT_TIMESTAMP, :page, :ip, :ref_id, :sx, :sy, :vx, :vy, :session
3764a163f50SAndreas Gohr        )
3774a163f50SAndreas Gohr        ',
3784a163f50SAndreas Gohr            $data
3794a163f50SAndreas Gohr        );
380762f4807SAndreas Gohr    }
381762f4807SAndreas Gohr
382762f4807SAndreas Gohr    /**
383762f4807SAndreas Gohr     * Log a click on an external link
384762f4807SAndreas Gohr     *
38587e0f0b1SAndreas Gohr     * Called from dispatch.php
386762f4807SAndreas Gohr     */
387762f4807SAndreas Gohr    public function logOutgoing(): void
388762f4807SAndreas Gohr    {
389762f4807SAndreas Gohr        global $INPUT;
390762f4807SAndreas Gohr
391762f4807SAndreas Gohr        if (!$INPUT->str('ol')) return;
392762f4807SAndreas Gohr
3934a163f50SAndreas Gohr        $link = $INPUT->filter('trim')->str('ol');
3944a163f50SAndreas Gohr        $session = $this->session;
3954a163f50SAndreas Gohr        $page = $INPUT->filter('cleanID')->str('p');
396762f4807SAndreas Gohr
397762f4807SAndreas Gohr        $this->db->exec(
398762f4807SAndreas Gohr            'INSERT INTO outlinks (
3994a163f50SAndreas Gohr                dt, session, page, link
400762f4807SAndreas Gohr             ) VALUES (
40141d1fffcSAndreas Gohr                CURRENT_TIMESTAMP, ?, ?, ?
402762f4807SAndreas Gohr             )',
4032adee4c6SAndreas Gohr            $session,
4042adee4c6SAndreas Gohr            $page,
4052adee4c6SAndreas Gohr            $link
406762f4807SAndreas Gohr        );
407762f4807SAndreas Gohr    }
408762f4807SAndreas Gohr
409762f4807SAndreas Gohr    /**
410762f4807SAndreas Gohr     * Log access to a media file
411762f4807SAndreas Gohr     *
412762f4807SAndreas Gohr     * Called from action.php
413762f4807SAndreas Gohr     *
414762f4807SAndreas Gohr     * @param string $media The media ID
415762f4807SAndreas Gohr     * @param string $mime The media's mime type
416762f4807SAndreas Gohr     * @param bool $inline Is this displayed inline?
417762f4807SAndreas Gohr     * @param int $size Size of the media file
418762f4807SAndreas Gohr     */
419762f4807SAndreas Gohr    public function logMedia(string $media, string $mime, bool $inline, int $size): void
420762f4807SAndreas Gohr    {
421762f4807SAndreas Gohr        [$mime1, $mime2] = explode('/', strtolower($mime));
422762f4807SAndreas Gohr        $inline = $inline ? 1 : 0;
423762f4807SAndreas Gohr
424762f4807SAndreas Gohr
4254a163f50SAndreas Gohr        $data = [
4264a163f50SAndreas Gohr            'media' => cleanID($media),
4274a163f50SAndreas Gohr            'ip' => $this->logIp(), // resolve the IP address
4284a163f50SAndreas Gohr            'session' => $this->session,
4294a163f50SAndreas Gohr            'size' => $size,
4304a163f50SAndreas Gohr            'mime1' => $mime1,
4314a163f50SAndreas Gohr            'mime2' => $mime2,
4324a163f50SAndreas Gohr            'inline' => $inline,
4334a163f50SAndreas Gohr        ];
4344a163f50SAndreas Gohr
435bd514593SAndreas Gohr        $this->db->exec(
436bd514593SAndreas Gohr            '
4374a163f50SAndreas Gohr                INSERT INTO media ( dt, media, ip, session, size, mime1, mime2, inline )
4384a163f50SAndreas Gohr                     VALUES (CURRENT_TIMESTAMP, :media, :ip, :session, :size, :mime1, :mime2, :inline)
4394a163f50SAndreas Gohr            ',
4404a163f50SAndreas Gohr            $data
441762f4807SAndreas Gohr        );
442762f4807SAndreas Gohr    }
443762f4807SAndreas Gohr
444762f4807SAndreas Gohr    /**
445762f4807SAndreas Gohr     * Log page edits
446762f4807SAndreas Gohr     *
4474a163f50SAndreas Gohr     * called from action.php
4484a163f50SAndreas Gohr     *
449762f4807SAndreas Gohr     * @param string $page The page that was edited
450762f4807SAndreas Gohr     * @param string $type The type of edit (create, edit, etc.)
451762f4807SAndreas Gohr     */
452762f4807SAndreas Gohr    public function logEdit(string $page, string $type): void
453762f4807SAndreas Gohr    {
4544a163f50SAndreas Gohr        $data = [
4554a163f50SAndreas Gohr            'page' => cleanID($page),
4564a163f50SAndreas Gohr            'type' => $type,
4574a163f50SAndreas Gohr            'ip' => $this->logIp(), // resolve the IP address
4584a163f50SAndreas Gohr            'session' => $this->session
4594a163f50SAndreas Gohr        ];
460762f4807SAndreas Gohr
46141d1fffcSAndreas Gohr        $this->db->exec(
462762f4807SAndreas Gohr            'INSERT INTO edits (
4634a163f50SAndreas Gohr                dt, page, type, ip, session
464762f4807SAndreas Gohr             ) VALUES (
4654a163f50SAndreas Gohr                CURRENT_TIMESTAMP, :page, :type, :ip, :session
466762f4807SAndreas Gohr             )',
4674a163f50SAndreas Gohr            $data
468762f4807SAndreas Gohr        );
469762f4807SAndreas Gohr    }
470762f4807SAndreas Gohr
471762f4807SAndreas Gohr    /**
472762f4807SAndreas Gohr     * Log login/logoffs and user creations
473762f4807SAndreas Gohr     *
474af93d154SAndreas Gohr     * @param string $type The type of login event (login, logout, create, failed)
475af93d154SAndreas Gohr     * @param string $user The username
476762f4807SAndreas Gohr     */
477762f4807SAndreas Gohr    public function logLogin(string $type, string $user = ''): void
478762f4807SAndreas Gohr    {
479762f4807SAndreas Gohr        global $INPUT;
480762f4807SAndreas Gohr
481762f4807SAndreas Gohr        if (!$user) $user = $INPUT->server->str('REMOTE_USER');
482762f4807SAndreas Gohr
483762f4807SAndreas Gohr        $ip = clientIP(true);
484762f4807SAndreas Gohr
485762f4807SAndreas Gohr        $this->db->exec(
486762f4807SAndreas Gohr            'INSERT INTO logins (
487af93d154SAndreas Gohr                dt, ip, user, type
488762f4807SAndreas Gohr             ) VALUES (
489af93d154SAndreas Gohr                CURRENT_TIMESTAMP, ?, ?, ?
490762f4807SAndreas Gohr             )',
4912adee4c6SAndreas Gohr            $ip,
4922adee4c6SAndreas Gohr            $user,
493af93d154SAndreas Gohr            $type
494762f4807SAndreas Gohr        );
495762f4807SAndreas Gohr    }
496762f4807SAndreas Gohr
497762f4807SAndreas Gohr    /**
49802aa9b73SAndreas Gohr     * Log search data to the search related tables
49902aa9b73SAndreas Gohr     *
50002aa9b73SAndreas Gohr     * @param string $query The search query
50102aa9b73SAndreas Gohr     * @param string[] $words The query split into words
50202aa9b73SAndreas Gohr     */
50302aa9b73SAndreas Gohr    public function logSearch(string $query, array $words): void
50402aa9b73SAndreas Gohr    {
50502aa9b73SAndreas Gohr        if (!$query) return;
50602aa9b73SAndreas Gohr
50702aa9b73SAndreas Gohr        $sid = $this->db->exec(
50802aa9b73SAndreas Gohr            'INSERT INTO search (dt, ip, session, query) VALUES (CURRENT_TIMESTAMP, ?, ? , ?)',
50902aa9b73SAndreas Gohr            $this->logIp(), // resolve the IP address
51002aa9b73SAndreas Gohr            $this->session,
51102aa9b73SAndreas Gohr            $query,
51202aa9b73SAndreas Gohr        );
51302aa9b73SAndreas Gohr
51402aa9b73SAndreas Gohr        foreach ($words as $word) {
51502aa9b73SAndreas Gohr            if (!$word) continue;
51602aa9b73SAndreas Gohr            $this->db->exec(
51702aa9b73SAndreas Gohr                'INSERT INTO searchwords (sid, word) VALUES (?, ?)',
51802aa9b73SAndreas Gohr                $sid,
51902aa9b73SAndreas Gohr                $word
52002aa9b73SAndreas Gohr            );
52102aa9b73SAndreas Gohr        }
52202aa9b73SAndreas Gohr    }
52302aa9b73SAndreas Gohr
52402aa9b73SAndreas Gohr    /**
525762f4807SAndreas Gohr     * Log the current page count and size as today's history entry
526762f4807SAndreas Gohr     */
527762f4807SAndreas Gohr    public function logHistoryPages(): void
528762f4807SAndreas Gohr    {
529762f4807SAndreas Gohr        global $conf;
530762f4807SAndreas Gohr
531762f4807SAndreas Gohr        // use the popularity plugin's search method to find the wanted data
532762f4807SAndreas Gohr        /** @var helper_plugin_popularity $pop */
533762f4807SAndreas Gohr        $pop = plugin_load('helper', 'popularity');
534b188870fSAndreas Gohr        $list = $this->initEmptySearchList();
535762f4807SAndreas Gohr        search($list, $conf['datadir'], [$pop, 'searchCountCallback'], ['all' => false], '');
536762f4807SAndreas Gohr        $page_count = $list['file_count'];
537762f4807SAndreas Gohr        $page_size = $list['file_size'];
538762f4807SAndreas Gohr
539762f4807SAndreas Gohr        $this->db->exec(
540762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
541762f4807SAndreas Gohr                info, value, dt
542762f4807SAndreas Gohr             ) VALUES (
543483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
544762f4807SAndreas Gohr             )',
5452adee4c6SAndreas Gohr            'page_count',
5462adee4c6SAndreas Gohr            $page_count
547762f4807SAndreas Gohr        );
548762f4807SAndreas Gohr        $this->db->exec(
549762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
550762f4807SAndreas Gohr                info, value, dt
551762f4807SAndreas Gohr             ) VALUES (
552483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
553762f4807SAndreas Gohr             )',
5542adee4c6SAndreas Gohr            'page_size',
5552adee4c6SAndreas Gohr            $page_size
556762f4807SAndreas Gohr        );
557762f4807SAndreas Gohr    }
558762f4807SAndreas Gohr
559762f4807SAndreas Gohr    /**
560762f4807SAndreas Gohr     * Log the current media count and size as today's history entry
561762f4807SAndreas Gohr     */
562762f4807SAndreas Gohr    public function logHistoryMedia(): void
563762f4807SAndreas Gohr    {
564762f4807SAndreas Gohr        global $conf;
565762f4807SAndreas Gohr
566762f4807SAndreas Gohr        // use the popularity plugin's search method to find the wanted data
567762f4807SAndreas Gohr        /** @var helper_plugin_popularity $pop */
568762f4807SAndreas Gohr        $pop = plugin_load('helper', 'popularity');
569b188870fSAndreas Gohr        $list = $this->initEmptySearchList();
570762f4807SAndreas Gohr        search($list, $conf['mediadir'], [$pop, 'searchCountCallback'], ['all' => true], '');
571762f4807SAndreas Gohr        $media_count = $list['file_count'];
572762f4807SAndreas Gohr        $media_size = $list['file_size'];
573762f4807SAndreas Gohr
574762f4807SAndreas Gohr        $this->db->exec(
575762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
576762f4807SAndreas Gohr                info, value, dt
577762f4807SAndreas Gohr             ) VALUES (
578483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
579762f4807SAndreas Gohr             )',
5802adee4c6SAndreas Gohr            'media_count',
5812adee4c6SAndreas Gohr            $media_count
582762f4807SAndreas Gohr        );
583762f4807SAndreas Gohr        $this->db->exec(
584762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
585762f4807SAndreas Gohr                info, value, dt
586762f4807SAndreas Gohr             ) VALUES (
587483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
588762f4807SAndreas Gohr             )',
5892adee4c6SAndreas Gohr            'media_size',
5902adee4c6SAndreas Gohr            $media_size
591762f4807SAndreas Gohr        );
592762f4807SAndreas Gohr    }
593b188870fSAndreas Gohr
5944a163f50SAndreas Gohr    // endregion
5954a163f50SAndreas Gohr
596b188870fSAndreas Gohr    /**
597b188870fSAndreas Gohr     * @todo can be dropped in favor of helper_plugin_popularity::initEmptySearchList() once it's public
598b188870fSAndreas Gohr     * @return array
599b188870fSAndreas Gohr     */
600b188870fSAndreas Gohr    protected function initEmptySearchList()
601b188870fSAndreas Gohr    {
602b188870fSAndreas Gohr        return array_fill_keys([
603b188870fSAndreas Gohr            'file_count',
604b188870fSAndreas Gohr            'file_size',
605b188870fSAndreas Gohr            'file_max',
606b188870fSAndreas Gohr            'file_min',
607b188870fSAndreas Gohr            'dir_count',
608b188870fSAndreas Gohr            'dir_nest',
609b188870fSAndreas Gohr            'file_oldest'
610b188870fSAndreas Gohr        ], 0);
611b188870fSAndreas Gohr    }
612762f4807SAndreas Gohr}
613