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