xref: /plugin/lastseen/helper.php (revision 51e72b81450b695de7d55fc5789cf4f3555d8e81)
134b8413fStracker-user<?php
2e85c7321Stracker-user
3e85c7321Stracker-userif (!defined('DOKU_INC')) die();
4e85c7321Stracker-user
5e85c7321Stracker-useruse dokuwiki\Extension\Plugin;
6e85c7321Stracker-user
734b8413fStracker-user/**
834b8413fStracker-user * Last Seen plugin — storage helper.
934b8413fStracker-user *
1034b8413fStracker-user * Owns the on-disk format and path for the per-user last-seen timestamps,
1134b8413fStracker-user * shared between the action component (which records activity) and the admin
1234b8413fStracker-user * component (which displays it).
1334b8413fStracker-user */
1434b8413fStracker-user
15e85c7321Stracker-userclass helper_plugin_lastseen extends Plugin
1634b8413fStracker-user{
1734b8413fStracker-user    /**
1834b8413fStracker-user     * Absolute path to the storage file.
1934b8413fStracker-user     *
2034b8413fStracker-user     * Lives in the meta directory: it must survive cache clears (so not
2134b8413fStracker-user     * cachedir) and DokuWiki upgrades (so not inside the plugin folder).
2234b8413fStracker-user     * Format: a serialized [username => unix_timestamp] map.
2334b8413fStracker-user     *
2434b8413fStracker-user     * @return string
2534b8413fStracker-user     */
2634b8413fStracker-user    public function getStorePath()
2734b8413fStracker-user    {
2834b8413fStracker-user        global $conf;
2934b8413fStracker-user        return $conf['metadir'] . '/_lastseen.dat';
3034b8413fStracker-user    }
3134b8413fStracker-user
3234b8413fStracker-user    /**
3334b8413fStracker-user     * Read the full [username => timestamp] map.
3434b8413fStracker-user     *
3534b8413fStracker-user     * @return array empty array if there is no data yet or the file is corrupt
3634b8413fStracker-user     */
3734b8413fStracker-user    public function getAll()
3834b8413fStracker-user    {
3934b8413fStracker-user        $path = $this->getStorePath();
4034b8413fStracker-user        if (!file_exists($path)) {
4134b8413fStracker-user            return [];
4234b8413fStracker-user        }
4334b8413fStracker-user        $raw = io_readFile($path, false);
4434b8413fStracker-user        if ($raw === '') {
4534b8413fStracker-user            return [];
4634b8413fStracker-user        }
4734b8413fStracker-user        // allowed_classes => false: we only ever store scalars, and this
4834b8413fStracker-user        // blocks object-injection if the file is ever tampered with.
4934b8413fStracker-user        $data = unserialize($raw, ['allowed_classes' => false]);
5034b8413fStracker-user        return is_array($data) ? $data : [];
5134b8413fStracker-user    }
5234b8413fStracker-user
5334b8413fStracker-user    /**
5434b8413fStracker-user     * Last-seen timestamp for one user.
5534b8413fStracker-user     *
5634b8413fStracker-user     * @param string $user
5734b8413fStracker-user     * @return int|null unix timestamp, or null if never recorded
5834b8413fStracker-user     */
5934b8413fStracker-user    public function getTimestamp($user)
6034b8413fStracker-user    {
6134b8413fStracker-user        $all = $this->getAll();
6234b8413fStracker-user        return $all[$user] ?? null;
6334b8413fStracker-user    }
6434b8413fStracker-user
6534b8413fStracker-user    /**
6634b8413fStracker-user     * Record $user as seen "now" — throttled.
6734b8413fStracker-user     *
6834b8413fStracker-user     * The store is only rewritten if the user's existing timestamp is older
6934b8413fStracker-user     * than the configured interval. Under heavy browsing this turns hundreds
7034b8413fStracker-user     * of page views into at most one write per interval per user.
7134b8413fStracker-user     *
7234b8413fStracker-user     * @param string $user
7334b8413fStracker-user     * @return bool true if the store was updated, false if throttled/skipped
7434b8413fStracker-user     */
7534b8413fStracker-user    public function record($user)
7634b8413fStracker-user    {
7734b8413fStracker-user        if ($user === '' || $user === null) {
7834b8413fStracker-user            return false;
7934b8413fStracker-user        }
8034b8413fStracker-user
8134b8413fStracker-user        $now      = time();
8234b8413fStracker-user        $interval = (int) $this->getConf('update_interval');
8334b8413fStracker-user
8434b8413fStracker-user        // Fast path: read without locking. If the stored timestamp is still
8534b8413fStracker-user        // within the throttle window there is nothing to do — and this is
8634b8413fStracker-user        // what the vast majority of requests hit.
8734b8413fStracker-user        $current = $this->getTimestamp($user);
8834b8413fStracker-user        if ($current !== null && ($now - $current) < $interval) {
8934b8413fStracker-user            return false;
9034b8413fStracker-user        }
9134b8413fStracker-user
9234b8413fStracker-user        // Slow path: a write is due. Lock, re-read (a concurrent request may
9334b8413fStracker-user        // have just updated it), update, save atomically, unlock.
94*51e72b81Stracker-user        //
95*51e72b81Stracker-user        // We lock a *sentinel* path (.lock suffix) rather than $path itself.
96*51e72b81Stracker-user        // io_saveFile() calls io_lock()/io_unlock() on its own argument, so
97*51e72b81Stracker-user        // locking $path here would cause a double-lock on the same md5 key:
98*51e72b81Stracker-user        // the inner lock busy-waits the full 3-second stale-lock timeout on
99*51e72b81Stracker-user        // every write, then tears down the directory prematurely — eliminating
100*51e72b81Stracker-user        // the mutual-exclusion we intended while stalling the request.
10134b8413fStracker-user        $path = $this->getStorePath();
102*51e72b81Stracker-user        $lock = $path . '.lock';
103*51e72b81Stracker-user        io_lock($lock);
10434b8413fStracker-user
10534b8413fStracker-user        $all = $this->getAll();
10634b8413fStracker-user        if (isset($all[$user]) && ($now - $all[$user]) < $interval) {
10734b8413fStracker-user            // Lost the race — another request updated it while we waited.
108*51e72b81Stracker-user            io_unlock($lock);
10934b8413fStracker-user            return false;
11034b8413fStracker-user        }
11134b8413fStracker-user        $all[$user] = $now;
11234b8413fStracker-user        io_saveFile($path, serialize($all));
11334b8413fStracker-user
114*51e72b81Stracker-user        io_unlock($lock);
11534b8413fStracker-user        return true;
11634b8413fStracker-user    }
11734b8413fStracker-user}
118