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