134b8413fStracker-user<?php 2e85c7321Stracker-user 3e85c7321Stracker-userif (!defined('DOKU_INC')) die(); 4e85c7321Stracker-user 5e85c7321Stracker-useruse dokuwiki\Extension\AdminPlugin; 6e85c7321Stracker-user 734b8413fStracker-user/** 834b8413fStracker-user * Last Seen plugin — admin panel page. 934b8413fStracker-user * 1034b8413fStracker-user * Lists every registered user with the time of their last authenticated 1134b8413fStracker-user * activity. Appears in the Admin panel right after the User Manager. 1234b8413fStracker-user */ 1334b8413fStracker-user 14e85c7321Stracker-userclass admin_plugin_lastseen extends AdminPlugin 1534b8413fStracker-user{ 16e85c7321Stracker-user /** 17e85c7321Stracker-user * Admin-only — last-seen data is mildly sensitive activity information. 18e85c7321Stracker-user * 19e85c7321Stracker-user * @return bool 20e85c7321Stracker-user */ 2134b8413fStracker-user public function forAdminOnly() 2234b8413fStracker-user { 2334b8413fStracker-user return true; 2434b8413fStracker-user } 2534b8413fStracker-user 26e85c7321Stracker-user /** 27e85c7321Stracker-user * Position in the admin menu. 28e85c7321Stracker-user * 29e85c7321Stracker-user * @return int 30e85c7321Stracker-user */ 3134b8413fStracker-user public function getMenuSort() 3234b8413fStracker-user { 33b9651948Stracker-user return 1000; 3434b8413fStracker-user } 3534b8413fStracker-user 36e85c7321Stracker-user /** 37e85c7321Stracker-user * @param string $language 38e85c7321Stracker-user * @return string 39e85c7321Stracker-user */ 4034b8413fStracker-user public function getMenuText($language) 4134b8413fStracker-user { 4234b8413fStracker-user return $this->getLang('menu'); 4334b8413fStracker-user } 4434b8413fStracker-user 45e85c7321Stracker-user /** 46e85c7321Stracker-user * Read-only page — no form submissions to process. 47e85c7321Stracker-user * 48e85c7321Stracker-user * @return void 49e85c7321Stracker-user */ 5034b8413fStracker-user public function handle() 5134b8413fStracker-user { 5234b8413fStracker-user } 5334b8413fStracker-user 5434b8413fStracker-user /** 5534b8413fStracker-user * Render the admin page. 5634b8413fStracker-user */ 5734b8413fStracker-user public function html() 5834b8413fStracker-user { 5934b8413fStracker-user global $auth, $INPUT, $ID; 6034b8413fStracker-user 6134b8413fStracker-user echo '<h1>' . hsc($this->getLang('menu')) . '</h1>'; 6234b8413fStracker-user 6334b8413fStracker-user /** @var helper_plugin_lastseen $hlp */ 6434b8413fStracker-user $hlp = plugin_load('helper', 'lastseen'); 6534b8413fStracker-user if ($hlp === null) { 66*51e72b81Stracker-user echo '<div class="error">' . hsc($this->getLang('helper_missing')) . '</div>'; 6734b8413fStracker-user return; 6834b8413fStracker-user } 6934b8413fStracker-user 7034b8413fStracker-user // Some auth backends (certain LDAP/AD setups) cannot enumerate users. 7134b8413fStracker-user // authplain can; degrade gracefully for the rest. 7234b8413fStracker-user if (!$auth || !$auth->canDo('getUsers')) { 7334b8413fStracker-user echo '<div class="error">' . hsc($this->getLang('no_userlist')) . '</div>'; 7434b8413fStracker-user return; 7534b8413fStracker-user } 7634b8413fStracker-user 7734b8413fStracker-user // retrieveUsers(0, 0): start at 0, limit 0 == all users. 7834b8413fStracker-user // Returns [username => ['name' => ..., 'mail' => ..., 'grps' => []]]. 7934b8413fStracker-user $users = $auth->retrieveUsers(0, 0); 8034b8413fStracker-user $seen = $hlp->getAll(); 8134b8413fStracker-user 8234b8413fStracker-user // ---- sorting ------------------------------------------------- 8334b8413fStracker-user $sortable = ['login', 'name', 'grps', 'lastseen']; 8434b8413fStracker-user $sort = $INPUT->str('sort', 'lastseen'); 8534b8413fStracker-user if (!in_array($sort, $sortable, true)) { 8634b8413fStracker-user $sort = 'lastseen'; 8734b8413fStracker-user } 8834b8413fStracker-user $order = ($INPUT->str('order', 'desc') === 'asc') ? 'asc' : 'desc'; 8934b8413fStracker-user 9034b8413fStracker-user $rows = []; 9134b8413fStracker-user foreach ($users as $login => $info) { 9234b8413fStracker-user $rows[] = [ 9334b8413fStracker-user 'login' => $login, 9434b8413fStracker-user 'name' => $info['name'] ?? '', 9534b8413fStracker-user 'grps' => isset($info['grps']) ? implode(', ', (array) $info['grps']) : '', 9634b8413fStracker-user 'lastseen' => isset($seen[$login]) ? (int) $seen[$login] : 0, // 0 == never 9734b8413fStracker-user ]; 9834b8413fStracker-user } 9934b8413fStracker-user 10034b8413fStracker-user usort($rows, function ($a, $b) use ($sort, $order) { 10134b8413fStracker-user switch ($sort) { 10234b8413fStracker-user case 'login': 10334b8413fStracker-user $cmp = strcasecmp($a['login'], $b['login']); 10434b8413fStracker-user break; 10534b8413fStracker-user case 'name': 10634b8413fStracker-user $cmp = strcasecmp($a['name'], $b['name']); 10734b8413fStracker-user break; 10834b8413fStracker-user case 'grps': 10934b8413fStracker-user $cmp = strcasecmp($a['grps'], $b['grps']); 11034b8413fStracker-user break; 11134b8413fStracker-user case 'lastseen': 11234b8413fStracker-user default: 11334b8413fStracker-user $cmp = $a['lastseen'] <=> $b['lastseen']; 11434b8413fStracker-user break; 11534b8413fStracker-user } 11634b8413fStracker-user return ($order === 'asc') ? $cmp : -$cmp; 11734b8413fStracker-user }); 11834b8413fStracker-user 11934b8413fStracker-user $showNever = (bool) $this->getConf('show_never'); 12034b8413fStracker-user 12134b8413fStracker-user // ---- render -------------------------------------------------- 12234b8413fStracker-user echo '<p>' . hsc($this->getLang('intro')) . '</p>'; 12334b8413fStracker-user echo '<div class="table">'; 12434b8413fStracker-user echo '<table class="inline plugin_lastseen">'; 12534b8413fStracker-user echo '<thead><tr>'; 12634b8413fStracker-user $this->headerCell('login', $this->getLang('col_login'), $sort, $order, $ID); 12734b8413fStracker-user $this->headerCell('name', $this->getLang('col_name'), $sort, $order, $ID); 12834b8413fStracker-user $this->headerCell('grps', $this->getLang('col_grps'), $sort, $order, $ID); 12934b8413fStracker-user $this->headerCell('lastseen', $this->getLang('col_lastseen'), $sort, $order, $ID); 13034b8413fStracker-user echo '</tr></thead><tbody>'; 13134b8413fStracker-user 13234b8413fStracker-user $count = 0; 13334b8413fStracker-user foreach ($rows as $row) { 13434b8413fStracker-user if ($row['lastseen'] === 0 && !$showNever) { 13534b8413fStracker-user continue; 13634b8413fStracker-user } 13734b8413fStracker-user $count++; 13834b8413fStracker-user echo '<tr>'; 13934b8413fStracker-user echo '<td>' . hsc($row['login']) . '</td>'; 14034b8413fStracker-user echo '<td>' . hsc($row['name']) . '</td>'; 14134b8413fStracker-user echo '<td>' . hsc($row['grps']) . '</td>'; 14234b8413fStracker-user if ($row['lastseen'] === 0) { 14334b8413fStracker-user echo '<td class="lastseen_never">' . hsc($this->getLang('never')) . '</td>'; 14434b8413fStracker-user } else { 14534b8413fStracker-user echo '<td>' . hsc(dformat($row['lastseen'])) 14634b8413fStracker-user . ' <span class="lastseen_rel">(' 14734b8413fStracker-user . hsc($this->relativeTime($row['lastseen'])) . ')</span></td>'; 14834b8413fStracker-user } 14934b8413fStracker-user echo '</tr>'; 15034b8413fStracker-user } 15134b8413fStracker-user 15234b8413fStracker-user echo '</tbody></table></div>'; 153e85c7321Stracker-user $totalKey = ($count === 1) ? 'total_one' : 'total'; 154e85c7321Stracker-user echo '<p class="lastseen_count">' . sprintf($this->getLang($totalKey), $count) . '</p>'; 15534b8413fStracker-user } 15634b8413fStracker-user 15734b8413fStracker-user /** 15834b8413fStracker-user * Emit a sortable column header. Clicking a header sorts by that column; 15934b8413fStracker-user * clicking the already-active column flips the direction. 16034b8413fStracker-user * 16134b8413fStracker-user * @param string $key column key 16234b8413fStracker-user * @param string $label visible header text 16334b8413fStracker-user * @param string $sort currently active sort column 16434b8413fStracker-user * @param string $order currently active order (asc|desc) 16534b8413fStracker-user * @param string $id current page id (for the link target) 16634b8413fStracker-user */ 16734b8413fStracker-user protected function headerCell($key, $label, $sort, $order, $id) 16834b8413fStracker-user { 16934b8413fStracker-user // If this column is already active, clicking flips the order; 17034b8413fStracker-user // otherwise a fresh column starts ascending. 17134b8413fStracker-user $newOrder = ($sort === $key && $order === 'asc') ? 'desc' : 'asc'; 17234b8413fStracker-user 17334b8413fStracker-user $arrow = ''; 17434b8413fStracker-user if ($sort === $key) { 17534b8413fStracker-user // ▲ U+25B2 / ▼ U+25BC as HTML entities (concatenated raw, not hsc'd) 17634b8413fStracker-user $arrow = ($order === 'asc') ? ' ▲' : ' ▼'; 17734b8413fStracker-user } 17834b8413fStracker-user 17960518ac5Stracker-user // wl() already returns an HTML-safe URL — its default separator is the 18060518ac5Stracker-user // pre-encoded "&". It must NOT be passed through hsc(): doing so 18160518ac5Stracker-user // double-encodes the ampersands ("&" -> "&amp;"), the browser 18260518ac5Stracker-user // then navigates to a URL containing a literal "&", and the query 18360518ac5Stracker-user // parameters arrive mis-named ("amp;sort" instead of "sort") — which 18460518ac5Stracker-user // silently breaks sorting. The label, being plain text, IS hsc()'d. 18534b8413fStracker-user $url = wl($id, [ 18634b8413fStracker-user 'do' => 'admin', 18734b8413fStracker-user 'page' => 'lastseen', 18834b8413fStracker-user 'sort' => $key, 18934b8413fStracker-user 'order' => $newOrder, 19034b8413fStracker-user ]); 19134b8413fStracker-user 19260518ac5Stracker-user echo '<th><a href="' . $url . '">' . hsc($label) . $arrow . '</a></th>'; 19334b8413fStracker-user } 19434b8413fStracker-user 19534b8413fStracker-user /** 19634b8413fStracker-user * Human-readable "time ago" string for a timestamp. 19734b8413fStracker-user * 19834b8413fStracker-user * @param int $timestamp 19934b8413fStracker-user * @return string 20034b8413fStracker-user */ 20134b8413fStracker-user protected function relativeTime($timestamp) 20234b8413fStracker-user { 20334b8413fStracker-user $diff = time() - $timestamp; 20434b8413fStracker-user if ($diff < 0) { 20534b8413fStracker-user $diff = 0; 20634b8413fStracker-user } 20734b8413fStracker-user 20834b8413fStracker-user if ($diff < 60) { 20934b8413fStracker-user return $this->getLang('rel_now'); 21034b8413fStracker-user } 21134b8413fStracker-user if ($diff < 3600) { 212*51e72b81Stracker-user $n = (int) floor($diff / 60); 213*51e72b81Stracker-user return sprintf($this->getLang($n === 1 ? 'rel_minute' : 'rel_minutes'), $n); 21434b8413fStracker-user } 21534b8413fStracker-user if ($diff < 86400) { 216*51e72b81Stracker-user $n = (int) floor($diff / 3600); 217*51e72b81Stracker-user return sprintf($this->getLang($n === 1 ? 'rel_hour' : 'rel_hours'), $n); 21834b8413fStracker-user } 21934b8413fStracker-user if ($diff < 86400 * 30) { 220*51e72b81Stracker-user $n = (int) floor($diff / 86400); 221*51e72b81Stracker-user return sprintf($this->getLang($n === 1 ? 'rel_day' : 'rel_days'), $n); 22234b8413fStracker-user } 22334b8413fStracker-user if ($diff < 86400 * 365) { 224*51e72b81Stracker-user $n = (int) floor($diff / (86400 * 30)); 225*51e72b81Stracker-user return sprintf($this->getLang($n === 1 ? 'rel_month' : 'rel_months'), $n); 22634b8413fStracker-user } 227*51e72b81Stracker-user $n = (int) floor($diff / (86400 * 365)); 228*51e72b81Stracker-user return sprintf($this->getLang($n === 1 ? 'rel_year' : 'rel_years'), $n); 22934b8413fStracker-user } 23034b8413fStracker-user} 231