xref: /plugin/lastseen/helper.php (revision 34b8413f371c8ced24cf179370bf6a8907b81994)
1*34b8413fStracker-user<?php
2*34b8413fStracker-user/**
3*34b8413fStracker-user * Last Seen plugin — storage helper.
4*34b8413fStracker-user *
5*34b8413fStracker-user * Owns the on-disk format and path for the per-user last-seen timestamps,
6*34b8413fStracker-user * shared between the action component (which records activity) and the admin
7*34b8413fStracker-user * component (which displays it).
8*34b8413fStracker-user */
9*34b8413fStracker-user
10*34b8413fStracker-userclass helper_plugin_lastseen extends DokuWiki_Plugin
11*34b8413fStracker-user{
12*34b8413fStracker-user    /**
13*34b8413fStracker-user     * Absolute path to the storage file.
14*34b8413fStracker-user     *
15*34b8413fStracker-user     * Lives in the meta directory: it must survive cache clears (so not
16*34b8413fStracker-user     * cachedir) and DokuWiki upgrades (so not inside the plugin folder).
17*34b8413fStracker-user     * Format: a serialized [username => unix_timestamp] map.
18*34b8413fStracker-user     *
19*34b8413fStracker-user     * @return string
20*34b8413fStracker-user     */
21*34b8413fStracker-user    public function getStorePath()
22*34b8413fStracker-user    {
23*34b8413fStracker-user        global $conf;
24*34b8413fStracker-user        return $conf['metadir'] . '/_lastseen.dat';
25*34b8413fStracker-user    }
26*34b8413fStracker-user
27*34b8413fStracker-user    /**
28*34b8413fStracker-user     * Read the full [username => timestamp] map.
29*34b8413fStracker-user     *
30*34b8413fStracker-user     * @return array empty array if there is no data yet or the file is corrupt
31*34b8413fStracker-user     */
32*34b8413fStracker-user    public function getAll()
33*34b8413fStracker-user    {
34*34b8413fStracker-user        $path = $this->getStorePath();
35*34b8413fStracker-user        if (!file_exists($path)) {
36*34b8413fStracker-user            return [];
37*34b8413fStracker-user        }
38*34b8413fStracker-user        $raw = io_readFile($path, false);
39*34b8413fStracker-user        if ($raw === '') {
40*34b8413fStracker-user            return [];
41*34b8413fStracker-user        }
42*34b8413fStracker-user        // allowed_classes => false: we only ever store scalars, and this
43*34b8413fStracker-user        // blocks object-injection if the file is ever tampered with.
44*34b8413fStracker-user        $data = unserialize($raw, ['allowed_classes' => false]);
45*34b8413fStracker-user        return is_array($data) ? $data : [];
46*34b8413fStracker-user    }
47*34b8413fStracker-user
48*34b8413fStracker-user    /**
49*34b8413fStracker-user     * Last-seen timestamp for one user.
50*34b8413fStracker-user     *
51*34b8413fStracker-user     * @param string $user
52*34b8413fStracker-user     * @return int|null unix timestamp, or null if never recorded
53*34b8413fStracker-user     */
54*34b8413fStracker-user    public function getTimestamp($user)
55*34b8413fStracker-user    {
56*34b8413fStracker-user        $all = $this->getAll();
57*34b8413fStracker-user        return $all[$user] ?? null;
58*34b8413fStracker-user    }
59*34b8413fStracker-user
60*34b8413fStracker-user    /**
61*34b8413fStracker-user     * Record $user as seen "now" — throttled.
62*34b8413fStracker-user     *
63*34b8413fStracker-user     * The store is only rewritten if the user's existing timestamp is older
64*34b8413fStracker-user     * than the configured interval. Under heavy browsing this turns hundreds
65*34b8413fStracker-user     * of page views into at most one write per interval per user.
66*34b8413fStracker-user     *
67*34b8413fStracker-user     * @param string $user
68*34b8413fStracker-user     * @return bool true if the store was updated, false if throttled/skipped
69*34b8413fStracker-user     */
70*34b8413fStracker-user    public function record($user)
71*34b8413fStracker-user    {
72*34b8413fStracker-user        if ($user === '' || $user === null) {
73*34b8413fStracker-user            return false;
74*34b8413fStracker-user        }
75*34b8413fStracker-user
76*34b8413fStracker-user        $now      = time();
77*34b8413fStracker-user        $interval = (int) $this->getConf('update_interval');
78*34b8413fStracker-user
79*34b8413fStracker-user        // Fast path: read without locking. If the stored timestamp is still
80*34b8413fStracker-user        // within the throttle window there is nothing to do — and this is
81*34b8413fStracker-user        // what the vast majority of requests hit.
82*34b8413fStracker-user        $current = $this->getTimestamp($user);
83*34b8413fStracker-user        if ($current !== null && ($now - $current) < $interval) {
84*34b8413fStracker-user            return false;
85*34b8413fStracker-user        }
86*34b8413fStracker-user
87*34b8413fStracker-user        // Slow path: a write is due. Lock, re-read (a concurrent request may
88*34b8413fStracker-user        // have just updated it), update, save atomically, unlock.
89*34b8413fStracker-user        $path = $this->getStorePath();
90*34b8413fStracker-user        io_lock($path);
91*34b8413fStracker-user
92*34b8413fStracker-user        $all = $this->getAll();
93*34b8413fStracker-user        if (isset($all[$user]) && ($now - $all[$user]) < $interval) {
94*34b8413fStracker-user            // Lost the race — another request updated it while we waited.
95*34b8413fStracker-user            io_unlock($path);
96*34b8413fStracker-user            return false;
97*34b8413fStracker-user        }
98*34b8413fStracker-user        $all[$user] = $now;
99*34b8413fStracker-user        io_saveFile($path, serialize($all));
100*34b8413fStracker-user
101*34b8413fStracker-user        io_unlock($path);
102*34b8413fStracker-user        return true;
103*34b8413fStracker-user    }
104*34b8413fStracker-user}
105