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