xref: /plugin/statistics/Logger.php (revision 02aa9b73ad7fe7ccc600c393ae1cf861c37c9024)
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;
94a163f50SAndreas Gohruse dokuwiki\ErrorHandler;
10762f4807SAndreas Gohruse dokuwiki\HTTP\DokuHTTPClient;
11762f4807SAndreas Gohruse dokuwiki\plugin\sqlite\SQLiteDB;
12762f4807SAndreas Gohruse dokuwiki\Utf8\Clean;
13762f4807SAndreas Gohruse helper_plugin_popularity;
14762f4807SAndreas Gohruse helper_plugin_statistics;
15762f4807SAndreas Gohr
16762f4807SAndreas Gohrclass Logger
17762f4807SAndreas Gohr{
18762f4807SAndreas Gohr    /** @var helper_plugin_statistics The statistics helper plugin instance */
19762f4807SAndreas Gohr    protected helper_plugin_statistics $hlp;
20762f4807SAndreas Gohr
21762f4807SAndreas Gohr    /** @var SQLiteDB The SQLite database instance */
22762f4807SAndreas Gohr    protected SQLiteDB $db;
23762f4807SAndreas Gohr
24762f4807SAndreas Gohr    /** @var string The full user agent string */
25762f4807SAndreas Gohr    protected string $uaAgent;
26762f4807SAndreas Gohr
27762f4807SAndreas Gohr    /** @var string The type of user agent (browser, robot, feedreader) */
28762f4807SAndreas Gohr    protected string $uaType = 'browser';
29762f4807SAndreas Gohr
30762f4807SAndreas Gohr    /** @var string The browser/client name */
31762f4807SAndreas Gohr    protected string $uaName;
32762f4807SAndreas Gohr
33762f4807SAndreas Gohr    /** @var string The browser/client version */
34762f4807SAndreas Gohr    protected string $uaVersion;
35762f4807SAndreas Gohr
36762f4807SAndreas Gohr    /** @var string The operating system/platform */
37762f4807SAndreas Gohr    protected string $uaPlatform;
38762f4807SAndreas Gohr
394a163f50SAndreas Gohr    /** @var string|null The user name, if available */
404a163f50SAndreas Gohr    protected ?string $user = null;
414a163f50SAndreas Gohr
42762f4807SAndreas Gohr    /** @var string The unique user identifier */
43762f4807SAndreas Gohr    protected string $uid;
44762f4807SAndreas Gohr
454a163f50SAndreas Gohr    /** @var string The session identifier */
464a163f50SAndreas Gohr    protected string $session;
474a163f50SAndreas Gohr
484a163f50SAndreas Gohr    /** @var int|null The ID of the main access log entry if any */
494a163f50SAndreas Gohr    protected ?int $hit = null;
504a163f50SAndreas Gohr
51c7cad24dSAndreas Gohr    /** @var DokuHTTPClient|null The HTTP client instance for testing */
52c7cad24dSAndreas Gohr    protected ?DokuHTTPClient $httpClient = null;
53c7cad24dSAndreas Gohr
544a163f50SAndreas Gohr    // region lifecycle
55762f4807SAndreas Gohr
56762f4807SAndreas Gohr    /**
57762f4807SAndreas Gohr     * Constructor
58762f4807SAndreas Gohr     *
59762f4807SAndreas Gohr     * Parses browser info and set internal vars
60762f4807SAndreas Gohr     */
61c7cad24dSAndreas Gohr    public function __construct(helper_plugin_statistics $hlp, ?DokuHTTPClient $httpClient = null)
62762f4807SAndreas Gohr    {
63762f4807SAndreas Gohr        global $INPUT;
64762f4807SAndreas Gohr
65762f4807SAndreas Gohr        $this->hlp = $hlp;
66762f4807SAndreas Gohr        $this->db = $this->hlp->getDB();
67c7cad24dSAndreas Gohr        $this->httpClient = $httpClient;
68762f4807SAndreas Gohr
694a163f50SAndreas Gohr        // FIXME if we already have a session, we should not re-parse the user agent
70762f4807SAndreas Gohr
714a163f50SAndreas Gohr        $ua = trim($INPUT->server->str('HTTP_USER_AGENT'));
72762f4807SAndreas Gohr        AbstractDeviceParser::setVersionTruncation(AbstractDeviceParser::VERSION_TRUNCATION_MAJOR);
73762f4807SAndreas Gohr        $dd = new DeviceDetector($ua); // FIXME we could use client hints, but need to add headers
74762f4807SAndreas Gohr        $dd->discardBotInformation();
75762f4807SAndreas Gohr        $dd->parse();
76762f4807SAndreas Gohr
7700f786d8SAndreas Gohr        if ($dd->isFeedReader()) {
7800f786d8SAndreas Gohr            $this->uaType = 'feedreader';
7900f786d8SAndreas Gohr        } elseif ($dd->isBot()) {
80762f4807SAndreas Gohr            $this->uaType = 'robot';
81762f4807SAndreas Gohr            // for now ignore bots
82c5d2f052SAndreas Gohr            throw new IgnoreException('Bot detected, not logging');
83762f4807SAndreas Gohr        }
84762f4807SAndreas Gohr
85762f4807SAndreas Gohr        $this->uaAgent = $ua;
8605786d83SAndreas Gohr        $this->uaName = Browser::getBrowserFamily($dd->getClient('name')) ?: 'Unknown';
8700f786d8SAndreas Gohr        $this->uaVersion = $dd->getClient('version') ?: '0';
8805786d83SAndreas Gohr        $this->uaPlatform = OperatingSystem::getOsFamily($dd->getOs('name')) ?: 'Unknown';
89762f4807SAndreas Gohr        $this->uid = $this->getUID();
904a163f50SAndreas Gohr        $this->session = $this->getSession();
914a163f50SAndreas Gohr        $this->user = $INPUT->server->str('REMOTE_USER') ?: null;
92762f4807SAndreas 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    {
101762f4807SAndreas Gohr        $this->hlp->getDB()->getPdo()->beginTransaction();
1024a163f50SAndreas Gohr
1034a163f50SAndreas Gohr        $this->logUser();
1044a163f50SAndreas Gohr        $this->logGroups();
1054a163f50SAndreas Gohr        $this->logDomain();
1064a163f50SAndreas Gohr        $this->logSession();
107762f4807SAndreas Gohr    }
108762f4807SAndreas Gohr
109762f4807SAndreas Gohr    /**
110762f4807SAndreas Gohr     * Should be called after logging
111762f4807SAndreas Gohr     *
112762f4807SAndreas Gohr     * This commits the transaction started in begin()
113762f4807SAndreas Gohr     */
114762f4807SAndreas Gohr    public function end(): void
115762f4807SAndreas Gohr    {
116762f4807SAndreas Gohr        $this->hlp->getDB()->getPdo()->commit();
117762f4807SAndreas Gohr    }
118762f4807SAndreas Gohr
1194a163f50SAndreas Gohr    // endregion
1204a163f50SAndreas Gohr    // region data gathering
1214a163f50SAndreas Gohr
122762f4807SAndreas Gohr    /**
123762f4807SAndreas Gohr     * Get the unique user ID
124762f4807SAndreas Gohr     *
125762f4807SAndreas Gohr     * @return string The unique user identifier
126762f4807SAndreas Gohr     */
127762f4807SAndreas Gohr    protected function getUID(): string
128762f4807SAndreas Gohr    {
129762f4807SAndreas Gohr        global $INPUT;
130762f4807SAndreas Gohr
131762f4807SAndreas Gohr        $uid = $INPUT->str('uid');
132762f4807SAndreas Gohr        if (!$uid) $uid = get_doku_pref('plgstats', false);
133762f4807SAndreas Gohr        if (!$uid) $uid = session_id();
134b188870fSAndreas Gohr        set_doku_pref('plgstats', $uid);
135762f4807SAndreas Gohr        return $uid;
136762f4807SAndreas Gohr    }
137762f4807SAndreas Gohr
138762f4807SAndreas Gohr    /**
139762f4807SAndreas Gohr     * Return the user's session ID
140762f4807SAndreas Gohr     *
141762f4807SAndreas Gohr     * This is usually our own managed session, not a PHP session (only in fallback)
142762f4807SAndreas Gohr     *
143762f4807SAndreas Gohr     * @return string The session identifier
144762f4807SAndreas Gohr     */
145762f4807SAndreas Gohr    protected function getSession(): string
146762f4807SAndreas Gohr    {
147762f4807SAndreas Gohr        global $INPUT;
148762f4807SAndreas Gohr
149*02aa9b73SAndreas Gohr
150*02aa9b73SAndreas Gohr
151*02aa9b73SAndreas Gohr
1524a163f50SAndreas Gohr        // FIXME session setting needs work. It should be reset on user change, maybe we do rely on the PHP session?
1534a163f50SAndreas Gohr        // We also want to store the user agent in the session table, so this needs also change the session ID
154762f4807SAndreas Gohr        $ses = $INPUT->str('ses');
155762f4807SAndreas Gohr        if (!$ses) $ses = get_doku_pref('plgstatsses', false);
156762f4807SAndreas Gohr        if (!$ses) $ses = session_id();
157b188870fSAndreas Gohr        set_doku_pref('plgstatsses', $ses);
158762f4807SAndreas Gohr        return $ses;
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
1924a163f50SAndreas Gohr                         end = CURRENT_TIMESTAMP
1934a163f50SAndreas Gohr                   WHERE excluded.session = sessions.session
1944a163f50SAndreas Gohr             ',
1954a163f50SAndreas Gohr            $this->session,
1964a163f50SAndreas Gohr            $this->uid,
1974a163f50SAndreas Gohr            $this->user,
1984a163f50SAndreas Gohr            $this->uaAgent,
1994a163f50SAndreas Gohr            $this->uaName,
2004a163f50SAndreas Gohr            $this->uaType,
2014a163f50SAndreas Gohr            $this->uaVersion,
2024a163f50SAndreas Gohr            $this->uaPlatform
203762f4807SAndreas Gohr        );
204762f4807SAndreas Gohr    }
205762f4807SAndreas Gohr
206762f4807SAndreas Gohr    /**
2074a163f50SAndreas Gohr     * Log all groups for the user
208762f4807SAndreas Gohr     *
2094a163f50SAndreas Gohr     * @todo maybe this should be done only once per session?
210762f4807SAndreas Gohr     */
2114a163f50SAndreas Gohr    protected function logGroups(): void
212762f4807SAndreas Gohr    {
2134a163f50SAndreas Gohr        global $USERINFO;
214762f4807SAndreas Gohr
2154a163f50SAndreas Gohr        if (!$this->user) return;
2164a163f50SAndreas Gohr        if (!isset($USERINFO['grps'])) return;
2174a163f50SAndreas Gohr        if (!is_array($USERINFO['grps'])) return;
2184a163f50SAndreas Gohr        $groups = $USERINFO['grps'];
219fced2f86SAnna Dabrowska
2204a163f50SAndreas Gohr        $this->db->exec('DELETE FROM groups WHERE user = ?', $this->user);
221762f4807SAndreas Gohr
222*02aa9b73SAndreas Gohr        $placeholders = implode(',', array_fill(0, count($groups), '(?, ?)'));
223762f4807SAndreas Gohr        $params = [];
2244a163f50SAndreas Gohr        $sql = "INSERT INTO groups (`user`, `group`) VALUES $placeholders";
225762f4807SAndreas Gohr        foreach ($groups as $group) {
2264a163f50SAndreas Gohr            $params[] = $this->user;
227762f4807SAndreas Gohr            $params[] = $group;
228762f4807SAndreas Gohr        }
229762f4807SAndreas Gohr        $this->db->exec($sql, $params);
230762f4807SAndreas Gohr    }
231762f4807SAndreas Gohr
232762f4807SAndreas Gohr    /**
2334a163f50SAndreas Gohr     * Log email domain
2340b8b7abaSAnna Dabrowska     *
2354a163f50SAndreas Gohr     * @todo maybe this should be done only once per session?
2360b8b7abaSAnna Dabrowska     */
2374a163f50SAndreas Gohr    protected function logDomain(): void
2380b8b7abaSAnna Dabrowska    {
2394a163f50SAndreas Gohr        global $USERINFO;
2404a163f50SAndreas Gohr        if (!$this->user) return;
2414a163f50SAndreas Gohr        if (!isset($USERINFO['mail'])) return;
2424a163f50SAndreas Gohr        $mail = $USERINFO['mail'];
2430b8b7abaSAnna Dabrowska
2440b8b7abaSAnna Dabrowska        $pos = strrpos($mail, '@');
2450b8b7abaSAnna Dabrowska        if (!$pos) return;
2460b8b7abaSAnna Dabrowska        $domain = substr($mail, $pos + 1);
2470b8b7abaSAnna Dabrowska        if (empty($domain)) return;
2480b8b7abaSAnna Dabrowska
2494a163f50SAndreas Gohr        $sql = 'UPDATE users SET domain = ? WHERE user = ?';
2504a163f50SAndreas Gohr        $this->db->exec($sql, [$domain, $this->user]);
2510b8b7abaSAnna Dabrowska    }
2520b8b7abaSAnna Dabrowska
2534a163f50SAndreas Gohr    // endregion
2544a163f50SAndreas Gohr    // region internal loggers called by the dispatchers
2554a163f50SAndreas Gohr
2560b8b7abaSAnna Dabrowska    /**
2574a163f50SAndreas Gohr     * Log the given referer URL
258762f4807SAndreas Gohr     *
2594a163f50SAndreas Gohr     * @param $referer
2604a163f50SAndreas Gohr     * @return int|null The referer ID or null if no referer was given
261762f4807SAndreas Gohr     */
2624a163f50SAndreas Gohr    public function logReferer($referer): ?int
263762f4807SAndreas Gohr    {
2644a163f50SAndreas Gohr        if (!$referer) return null;
265762f4807SAndreas Gohr
2664a163f50SAndreas Gohr        // FIXME we could check against a blacklist here
267762f4807SAndreas Gohr
2684a163f50SAndreas Gohr        $se = new SearchEngines($referer);
2694a163f50SAndreas Gohr        $type = $se->isSearchEngine() ? 'search' : 'external';
270762f4807SAndreas Gohr
271*02aa9b73SAndreas Gohr        $sql = 'INSERT OR IGNORE INTO referers (url, type, dt) VALUES (?, ?, CURRENT_TIMESTAMP)';
272*02aa9b73SAndreas Gohr        return $this->db->exec($sql, [$referer, $type]); // returns ID even if the insert was ignored
273762f4807SAndreas Gohr    }
274762f4807SAndreas Gohr
275762f4807SAndreas Gohr    /**
276762f4807SAndreas Gohr     * Resolve IP to country/city and store in database
277762f4807SAndreas Gohr     *
2784a163f50SAndreas Gohr     * @return string The IP address as stored
279762f4807SAndreas Gohr     */
2804a163f50SAndreas Gohr    public function logIp(): string
281762f4807SAndreas Gohr    {
2824a163f50SAndreas Gohr        $ip = clientIP(true);
2834a163f50SAndreas Gohr        $hash = $ip; // @todo we could anonymize here
2844a163f50SAndreas Gohr
285762f4807SAndreas Gohr        // check if IP already known and up-to-date
286762f4807SAndreas Gohr        $result = $this->db->queryValue(
287762f4807SAndreas Gohr            "SELECT ip
288762f4807SAndreas Gohr             FROM   iplocation
289762f4807SAndreas Gohr             WHERE  ip = ?
290762f4807SAndreas Gohr               AND  lastupd > date('now', '-30 days')",
2914a163f50SAndreas Gohr            $hash
292762f4807SAndreas Gohr        );
2934a163f50SAndreas Gohr        if ($result) return $hash; // already known and up-to-date
294762f4807SAndreas Gohr
295c7cad24dSAndreas Gohr        $http = $this->httpClient ?: new DokuHTTPClient();
2964a163f50SAndreas Gohr        $http->timeout = 7;
297762f4807SAndreas Gohr        $json = $http->get('http://ip-api.com/json/' . $ip); // yes, it's HTTP only
298762f4807SAndreas Gohr
2994a163f50SAndreas Gohr        if (!$json) {
3004a163f50SAndreas Gohr            \dokuwiki\Logger::error('Statistics Plugin - Failed talk to ip-api.com.');
3014a163f50SAndreas Gohr            return $hash;
3024a163f50SAndreas Gohr        }
303762f4807SAndreas Gohr        try {
304762f4807SAndreas Gohr            $data = json_decode($json, true, 512, JSON_THROW_ON_ERROR);
305762f4807SAndreas Gohr        } catch (\JsonException $e) {
3064a163f50SAndreas Gohr            \dokuwiki\Logger::error('Statistics Plugin - Failed to decode JSON from ip-api.com.', $e);
3074a163f50SAndreas Gohr            return $hash;
308762f4807SAndreas Gohr        }
309*02aa9b73SAndreas Gohr        if (!isset($data['status'])) {
310*02aa9b73SAndreas Gohr            \dokuwiki\Logger::error('Statistics Plugin - Invalid ip-api.com result' . $ip, $data);
3114a163f50SAndreas Gohr            return $hash;
312*02aa9b73SAndreas Gohr        };
313*02aa9b73SAndreas Gohr
314*02aa9b73SAndreas Gohr        // we do not check for 'success' status here. when the API can't resolve the IP we still log it
315*02aa9b73SAndreas Gohr        // without location data, so we won't re-query it in the next 30 days.
316762f4807SAndreas Gohr
3174a163f50SAndreas Gohr        $host = gethostbyaddr($ip); // @todo if we anonymize the IP, we should not do this
318762f4807SAndreas Gohr        $this->db->exec(
319762f4807SAndreas Gohr            'INSERT OR REPLACE INTO iplocation (
320762f4807SAndreas Gohr                    ip, country, code, city, host, lastupd
321762f4807SAndreas Gohr                 ) VALUES (
322762f4807SAndreas Gohr                    ?, ?, ?, ?, ?, CURRENT_TIMESTAMP
323762f4807SAndreas Gohr                 )',
3244a163f50SAndreas Gohr            $hash,
325*02aa9b73SAndreas Gohr            $data['country'] ?? '',
326*02aa9b73SAndreas Gohr            $data['countryCode'] ?? '',
327*02aa9b73SAndreas Gohr            $data['city'] ?? '',
3282adee4c6SAndreas Gohr            $host
329762f4807SAndreas Gohr        );
3304a163f50SAndreas Gohr
3314a163f50SAndreas Gohr        return $hash;
3324a163f50SAndreas Gohr    }
3334a163f50SAndreas Gohr
3344a163f50SAndreas Gohr    // endregion
3354a163f50SAndreas Gohr    // region log dispatchers
3364a163f50SAndreas Gohr
3374a163f50SAndreas Gohr    public function logPageView(): void
3384a163f50SAndreas Gohr    {
3394a163f50SAndreas Gohr        global $INPUT;
3404a163f50SAndreas Gohr
3414a163f50SAndreas Gohr        if (!$INPUT->str('p')) return;
3424a163f50SAndreas Gohr
3434a163f50SAndreas Gohr
3444a163f50SAndreas Gohr        $referer = $INPUT->filter('trim')->str('r');
3454a163f50SAndreas Gohr        $ip = $this->logIp(); // resolve the IP address
3464a163f50SAndreas Gohr
3474a163f50SAndreas Gohr        $data = [
3484a163f50SAndreas Gohr            'page' => $INPUT->filter('cleanID')->str('p'),
3494a163f50SAndreas Gohr            'ip' => $ip,
3504a163f50SAndreas Gohr            'ref_id' => $this->logReferer($referer),
3514a163f50SAndreas Gohr            'sx' => $INPUT->int('sx'),
3524a163f50SAndreas Gohr            'sy' => $INPUT->int('sy'),
3534a163f50SAndreas Gohr            'vx' => $INPUT->int('vx'),
3544a163f50SAndreas Gohr            'vy' => $INPUT->int('vy'),
3554a163f50SAndreas Gohr            'session' => $this->session,
3564a163f50SAndreas Gohr        ];
3574a163f50SAndreas Gohr
3584a163f50SAndreas Gohr        $this->db->exec('
3594a163f50SAndreas Gohr        INSERT INTO pageviews (
3604a163f50SAndreas Gohr            dt, page, ip, ref_id, screen_x, screen_y, view_x, view_y, session
3614a163f50SAndreas Gohr        ) VALUES (
3624a163f50SAndreas Gohr            CURRENT_TIMESTAMP, :page, :ip, :ref_id, :sx, :sy, :vx, :vy, :session
3634a163f50SAndreas Gohr        )
3644a163f50SAndreas Gohr        ',
3654a163f50SAndreas Gohr            $data
3664a163f50SAndreas Gohr        );
367762f4807SAndreas Gohr    }
368762f4807SAndreas Gohr
369762f4807SAndreas Gohr    /**
370762f4807SAndreas Gohr     * Log a click on an external link
371762f4807SAndreas Gohr     *
372762f4807SAndreas Gohr     * Called from log.php
373762f4807SAndreas Gohr     */
374762f4807SAndreas Gohr    public function logOutgoing(): void
375762f4807SAndreas Gohr    {
376762f4807SAndreas Gohr        global $INPUT;
377762f4807SAndreas Gohr
378762f4807SAndreas Gohr        if (!$INPUT->str('ol')) return;
379762f4807SAndreas Gohr
3804a163f50SAndreas Gohr        $link = $INPUT->filter('trim')->str('ol');
3814a163f50SAndreas Gohr        $session = $this->session;
3824a163f50SAndreas Gohr        $page = $INPUT->filter('cleanID')->str('p');
383762f4807SAndreas Gohr
384762f4807SAndreas Gohr        $this->db->exec(
385762f4807SAndreas Gohr            'INSERT INTO outlinks (
3864a163f50SAndreas Gohr                dt, session, page, link
387762f4807SAndreas Gohr             ) VALUES (
388762f4807SAndreas Gohr                CURRENT_TIMESTAMP, ?, ?, ?, ?
389762f4807SAndreas Gohr             )',
3902adee4c6SAndreas Gohr            $session,
3912adee4c6SAndreas Gohr            $page,
3922adee4c6SAndreas Gohr            $link
393762f4807SAndreas Gohr        );
394762f4807SAndreas Gohr    }
395762f4807SAndreas Gohr
396762f4807SAndreas Gohr    /**
397762f4807SAndreas Gohr     * Log access to a media file
398762f4807SAndreas Gohr     *
399762f4807SAndreas Gohr     * Called from action.php
400762f4807SAndreas Gohr     *
401762f4807SAndreas Gohr     * @param string $media The media ID
402762f4807SAndreas Gohr     * @param string $mime The media's mime type
403762f4807SAndreas Gohr     * @param bool $inline Is this displayed inline?
404762f4807SAndreas Gohr     * @param int $size Size of the media file
405762f4807SAndreas Gohr     */
406762f4807SAndreas Gohr    public function logMedia(string $media, string $mime, bool $inline, int $size): void
407762f4807SAndreas Gohr    {
408762f4807SAndreas Gohr        [$mime1, $mime2] = explode('/', strtolower($mime));
409762f4807SAndreas Gohr        $inline = $inline ? 1 : 0;
410762f4807SAndreas Gohr
411762f4807SAndreas Gohr
4124a163f50SAndreas Gohr        $data = [
4134a163f50SAndreas Gohr            'media' => cleanID($media),
4144a163f50SAndreas Gohr            'ip' => $this->logIp(), // resolve the IP address
4154a163f50SAndreas Gohr            'session' => $this->session,
4164a163f50SAndreas Gohr            'size' => $size,
4174a163f50SAndreas Gohr            'mime1' => $mime1,
4184a163f50SAndreas Gohr            'mime2' => $mime2,
4194a163f50SAndreas Gohr            'inline' => $inline,
4204a163f50SAndreas Gohr        ];
4214a163f50SAndreas Gohr
4224a163f50SAndreas Gohr        $this->db->exec('
4234a163f50SAndreas Gohr                INSERT INTO media ( dt, media, ip, session, size, mime1, mime2, inline )
4244a163f50SAndreas Gohr                     VALUES (CURRENT_TIMESTAMP, :media, :ip, :session, :size, :mime1, :mime2, :inline)
4254a163f50SAndreas Gohr            ',
4264a163f50SAndreas Gohr            $data
427762f4807SAndreas Gohr        );
428762f4807SAndreas Gohr    }
429762f4807SAndreas Gohr
430762f4807SAndreas Gohr    /**
431762f4807SAndreas Gohr     * Log page edits
432762f4807SAndreas Gohr     *
4334a163f50SAndreas Gohr     * called from action.php
4344a163f50SAndreas Gohr     *
435762f4807SAndreas Gohr     * @param string $page The page that was edited
436762f4807SAndreas Gohr     * @param string $type The type of edit (create, edit, etc.)
437762f4807SAndreas Gohr     */
438762f4807SAndreas Gohr    public function logEdit(string $page, string $type): void
439762f4807SAndreas Gohr    {
4404a163f50SAndreas Gohr        $data = [
4414a163f50SAndreas Gohr            'page' => cleanID($page),
4424a163f50SAndreas Gohr            'type' => $type,
4434a163f50SAndreas Gohr            'ip' => $this->logIp(), // resolve the IP address
4444a163f50SAndreas Gohr            'session' => $this->session
4454a163f50SAndreas Gohr        ];
446762f4807SAndreas Gohr
4470b8b7abaSAnna Dabrowska        $editId = $this->db->exec(
448762f4807SAndreas Gohr            'INSERT INTO edits (
4494a163f50SAndreas Gohr                dt, page, type, ip, session
450762f4807SAndreas Gohr             ) VALUES (
4514a163f50SAndreas Gohr                CURRENT_TIMESTAMP, :page, :type, :ip, :session
452762f4807SAndreas Gohr             )',
4534a163f50SAndreas Gohr            $data
454762f4807SAndreas Gohr        );
455762f4807SAndreas Gohr    }
456762f4807SAndreas Gohr
457762f4807SAndreas Gohr    /**
458762f4807SAndreas Gohr     * Log login/logoffs and user creations
459762f4807SAndreas Gohr     *
460762f4807SAndreas Gohr     * @param string $type The type of login event (login, logout, create)
461762f4807SAndreas Gohr     * @param string $user The username (optional, will use current user if empty)
4624a163f50SAndreas Gohr     * @fixme this is still broken, I need to figure out the session handling first
463762f4807SAndreas Gohr     */
464762f4807SAndreas Gohr    public function logLogin(string $type, string $user = ''): void
465762f4807SAndreas Gohr    {
466762f4807SAndreas Gohr        global $INPUT;
467762f4807SAndreas Gohr
468762f4807SAndreas Gohr        if (!$user) $user = $INPUT->server->str('REMOTE_USER');
469762f4807SAndreas Gohr
470762f4807SAndreas Gohr        $ip = clientIP(true);
4714a163f50SAndreas Gohr        $session = $this->session;
472762f4807SAndreas Gohr
473762f4807SAndreas Gohr        $this->db->exec(
474762f4807SAndreas Gohr            'INSERT INTO logins (
4754a163f50SAndreas Gohr                dt, type, ip, session
476762f4807SAndreas Gohr             ) VALUES (
477762f4807SAndreas Gohr                CURRENT_TIMESTAMP, ?, ?, ?, ?, ?
478762f4807SAndreas Gohr             )',
4792adee4c6SAndreas Gohr            $type,
4802adee4c6SAndreas Gohr            $ip,
4812adee4c6SAndreas Gohr            $user,
4822adee4c6SAndreas Gohr            $session,
4832adee4c6SAndreas Gohr            $this->uid
484762f4807SAndreas Gohr        );
485762f4807SAndreas Gohr    }
486762f4807SAndreas Gohr
487762f4807SAndreas Gohr    /**
488*02aa9b73SAndreas Gohr     * Log search data to the search related tables
489*02aa9b73SAndreas Gohr     *
490*02aa9b73SAndreas Gohr     * @param string $query The search query
491*02aa9b73SAndreas Gohr     * @param string[] $words The query split into words
492*02aa9b73SAndreas Gohr     */
493*02aa9b73SAndreas Gohr    public function logSearch(string $query, array $words): void
494*02aa9b73SAndreas Gohr    {
495*02aa9b73SAndreas Gohr        if(!$query) return;
496*02aa9b73SAndreas Gohr
497*02aa9b73SAndreas Gohr        $sid = $this->db->exec(
498*02aa9b73SAndreas Gohr            'INSERT INTO search (dt, ip, session, query) VALUES (CURRENT_TIMESTAMP, ?, ? , ?)',
499*02aa9b73SAndreas Gohr            $this->logIp(), // resolve the IP address
500*02aa9b73SAndreas Gohr            $this->session,
501*02aa9b73SAndreas Gohr            $query,
502*02aa9b73SAndreas Gohr        );
503*02aa9b73SAndreas Gohr
504*02aa9b73SAndreas Gohr        foreach ($words as $word) {
505*02aa9b73SAndreas Gohr            if (!$word) continue;
506*02aa9b73SAndreas Gohr            $this->db->exec(
507*02aa9b73SAndreas Gohr                'INSERT INTO searchwords (sid, word) VALUES (?, ?)',
508*02aa9b73SAndreas Gohr                $sid,
509*02aa9b73SAndreas Gohr                $word
510*02aa9b73SAndreas Gohr            );
511*02aa9b73SAndreas Gohr        }
512*02aa9b73SAndreas Gohr    }
513*02aa9b73SAndreas Gohr
514*02aa9b73SAndreas Gohr    /**
515762f4807SAndreas Gohr     * Log the current page count and size as today's history entry
516762f4807SAndreas Gohr     */
517762f4807SAndreas Gohr    public function logHistoryPages(): void
518762f4807SAndreas Gohr    {
519762f4807SAndreas Gohr        global $conf;
520762f4807SAndreas Gohr
521762f4807SAndreas Gohr        // use the popularity plugin's search method to find the wanted data
522762f4807SAndreas Gohr        /** @var helper_plugin_popularity $pop */
523762f4807SAndreas Gohr        $pop = plugin_load('helper', 'popularity');
524b188870fSAndreas Gohr        $list = $this->initEmptySearchList();
525762f4807SAndreas Gohr        search($list, $conf['datadir'], [$pop, 'searchCountCallback'], ['all' => false], '');
526762f4807SAndreas Gohr        $page_count = $list['file_count'];
527762f4807SAndreas Gohr        $page_size = $list['file_size'];
528762f4807SAndreas Gohr
529762f4807SAndreas Gohr        $this->db->exec(
530762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
531762f4807SAndreas Gohr                info, value, dt
532762f4807SAndreas Gohr             ) VALUES (
533483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
534762f4807SAndreas Gohr             )',
5352adee4c6SAndreas Gohr            'page_count',
5362adee4c6SAndreas Gohr            $page_count
537762f4807SAndreas Gohr        );
538762f4807SAndreas Gohr        $this->db->exec(
539762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
540762f4807SAndreas Gohr                info, value, dt
541762f4807SAndreas Gohr             ) VALUES (
542483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
543762f4807SAndreas Gohr             )',
5442adee4c6SAndreas Gohr            'page_size',
5452adee4c6SAndreas Gohr            $page_size
546762f4807SAndreas Gohr        );
547762f4807SAndreas Gohr    }
548762f4807SAndreas Gohr
549762f4807SAndreas Gohr    /**
550762f4807SAndreas Gohr     * Log the current media count and size as today's history entry
551762f4807SAndreas Gohr     */
552762f4807SAndreas Gohr    public function logHistoryMedia(): void
553762f4807SAndreas Gohr    {
554762f4807SAndreas Gohr        global $conf;
555762f4807SAndreas Gohr
556762f4807SAndreas Gohr        // use the popularity plugin's search method to find the wanted data
557762f4807SAndreas Gohr        /** @var helper_plugin_popularity $pop */
558762f4807SAndreas Gohr        $pop = plugin_load('helper', 'popularity');
559b188870fSAndreas Gohr        $list = $this->initEmptySearchList();
560762f4807SAndreas Gohr        search($list, $conf['mediadir'], [$pop, 'searchCountCallback'], ['all' => true], '');
561762f4807SAndreas Gohr        $media_count = $list['file_count'];
562762f4807SAndreas Gohr        $media_size = $list['file_size'];
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            'media_count',
5712adee4c6SAndreas Gohr            $media_count
572762f4807SAndreas Gohr        );
573762f4807SAndreas Gohr        $this->db->exec(
574762f4807SAndreas Gohr            'INSERT OR REPLACE INTO history (
575762f4807SAndreas Gohr                info, value, dt
576762f4807SAndreas Gohr             ) VALUES (
577483101d3SAndreas Gohr                ?, ?, CURRENT_TIMESTAMP
578762f4807SAndreas Gohr             )',
5792adee4c6SAndreas Gohr            'media_size',
5802adee4c6SAndreas Gohr            $media_size
581762f4807SAndreas Gohr        );
582762f4807SAndreas Gohr    }
583b188870fSAndreas Gohr
5844a163f50SAndreas Gohr    // endregion
5854a163f50SAndreas Gohr
586b188870fSAndreas Gohr    /**
587b188870fSAndreas Gohr     * @todo can be dropped in favor of helper_plugin_popularity::initEmptySearchList() once it's public
588b188870fSAndreas Gohr     * @return array
589b188870fSAndreas Gohr     */
590b188870fSAndreas Gohr    protected function initEmptySearchList()
591b188870fSAndreas Gohr    {
592b188870fSAndreas Gohr        return array_fill_keys([
593b188870fSAndreas Gohr            'file_count',
594b188870fSAndreas Gohr            'file_size',
595b188870fSAndreas Gohr            'file_max',
596b188870fSAndreas Gohr            'file_min',
597b188870fSAndreas Gohr            'dir_count',
598b188870fSAndreas Gohr            'dir_nest',
599b188870fSAndreas Gohr            'file_oldest'
600b188870fSAndreas Gohr        ], 0);
601b188870fSAndreas Gohr    }
602762f4807SAndreas Gohr}
603