134b8413fStracker-user<?php 234b8413fStracker-user/** 334b8413fStracker-user * Last Seen plugin — admin panel page. 434b8413fStracker-user * 534b8413fStracker-user * Lists every registered user with the time of their last authenticated 634b8413fStracker-user * activity. Appears in the Admin panel right after the User Manager. 734b8413fStracker-user */ 834b8413fStracker-user 934b8413fStracker-userclass admin_plugin_lastseen extends DokuWiki_Admin_Plugin 1034b8413fStracker-user{ 1134b8413fStracker-user /** Admin-only — last-seen data is mildly sensitive activity information. */ 1234b8413fStracker-user public function forAdminOnly() 1334b8413fStracker-user { 1434b8413fStracker-user return true; 1534b8413fStracker-user } 1634b8413fStracker-user 17*b9651948Stracker-user /** Position in the admin menu. */ 1834b8413fStracker-user public function getMenuSort() 1934b8413fStracker-user { 20*b9651948Stracker-user return 1000; 2134b8413fStracker-user } 2234b8413fStracker-user 2334b8413fStracker-user public function getMenuText($language) 2434b8413fStracker-user { 2534b8413fStracker-user return $this->getLang('menu'); 2634b8413fStracker-user } 2734b8413fStracker-user 2834b8413fStracker-user /** Read-only page — no form submissions to process. */ 2934b8413fStracker-user public function handle() 3034b8413fStracker-user { 3134b8413fStracker-user } 3234b8413fStracker-user 3334b8413fStracker-user /** 3434b8413fStracker-user * Render the admin page. 3534b8413fStracker-user */ 3634b8413fStracker-user public function html() 3734b8413fStracker-user { 3834b8413fStracker-user global $auth, $INPUT, $ID; 3934b8413fStracker-user 4034b8413fStracker-user echo '<h1>' . hsc($this->getLang('menu')) . '</h1>'; 4134b8413fStracker-user 4234b8413fStracker-user /** @var helper_plugin_lastseen $hlp */ 4334b8413fStracker-user $hlp = plugin_load('helper', 'lastseen'); 4434b8413fStracker-user if ($hlp === null) { 4534b8413fStracker-user echo '<div class="error">Helper component could not be loaded.</div>'; 4634b8413fStracker-user return; 4734b8413fStracker-user } 4834b8413fStracker-user 4934b8413fStracker-user // Some auth backends (certain LDAP/AD setups) cannot enumerate users. 5034b8413fStracker-user // authplain can; degrade gracefully for the rest. 5134b8413fStracker-user if (!$auth || !$auth->canDo('getUsers')) { 5234b8413fStracker-user echo '<div class="error">' . hsc($this->getLang('no_userlist')) . '</div>'; 5334b8413fStracker-user return; 5434b8413fStracker-user } 5534b8413fStracker-user 5634b8413fStracker-user // retrieveUsers(0, 0): start at 0, limit 0 == all users. 5734b8413fStracker-user // Returns [username => ['name' => ..., 'mail' => ..., 'grps' => []]]. 5834b8413fStracker-user $users = $auth->retrieveUsers(0, 0); 5934b8413fStracker-user $seen = $hlp->getAll(); 6034b8413fStracker-user 6134b8413fStracker-user // ---- sorting ------------------------------------------------- 6234b8413fStracker-user $sortable = ['login', 'name', 'grps', 'lastseen']; 6334b8413fStracker-user $sort = $INPUT->str('sort', 'lastseen'); 6434b8413fStracker-user if (!in_array($sort, $sortable, true)) { 6534b8413fStracker-user $sort = 'lastseen'; 6634b8413fStracker-user } 6734b8413fStracker-user $order = ($INPUT->str('order', 'desc') === 'asc') ? 'asc' : 'desc'; 6834b8413fStracker-user 6934b8413fStracker-user $rows = []; 7034b8413fStracker-user foreach ($users as $login => $info) { 7134b8413fStracker-user $rows[] = [ 7234b8413fStracker-user 'login' => $login, 7334b8413fStracker-user 'name' => $info['name'] ?? '', 7434b8413fStracker-user 'grps' => isset($info['grps']) ? implode(', ', (array) $info['grps']) : '', 7534b8413fStracker-user 'lastseen' => isset($seen[$login]) ? (int) $seen[$login] : 0, // 0 == never 7634b8413fStracker-user ]; 7734b8413fStracker-user } 7834b8413fStracker-user 7934b8413fStracker-user usort($rows, function ($a, $b) use ($sort, $order) { 8034b8413fStracker-user switch ($sort) { 8134b8413fStracker-user case 'login': 8234b8413fStracker-user $cmp = strcasecmp($a['login'], $b['login']); 8334b8413fStracker-user break; 8434b8413fStracker-user case 'name': 8534b8413fStracker-user $cmp = strcasecmp($a['name'], $b['name']); 8634b8413fStracker-user break; 8734b8413fStracker-user case 'grps': 8834b8413fStracker-user $cmp = strcasecmp($a['grps'], $b['grps']); 8934b8413fStracker-user break; 9034b8413fStracker-user case 'lastseen': 9134b8413fStracker-user default: 9234b8413fStracker-user $cmp = $a['lastseen'] <=> $b['lastseen']; 9334b8413fStracker-user break; 9434b8413fStracker-user } 9534b8413fStracker-user return ($order === 'asc') ? $cmp : -$cmp; 9634b8413fStracker-user }); 9734b8413fStracker-user 9834b8413fStracker-user $showNever = (bool) $this->getConf('show_never'); 9934b8413fStracker-user 10034b8413fStracker-user // ---- render -------------------------------------------------- 10134b8413fStracker-user echo '<p>' . hsc($this->getLang('intro')) . '</p>'; 10234b8413fStracker-user echo '<div class="table">'; 10334b8413fStracker-user echo '<table class="inline plugin_lastseen">'; 10434b8413fStracker-user echo '<thead><tr>'; 10534b8413fStracker-user $this->headerCell('login', $this->getLang('col_login'), $sort, $order, $ID); 10634b8413fStracker-user $this->headerCell('name', $this->getLang('col_name'), $sort, $order, $ID); 10734b8413fStracker-user $this->headerCell('grps', $this->getLang('col_grps'), $sort, $order, $ID); 10834b8413fStracker-user $this->headerCell('lastseen', $this->getLang('col_lastseen'), $sort, $order, $ID); 10934b8413fStracker-user echo '</tr></thead><tbody>'; 11034b8413fStracker-user 11134b8413fStracker-user $count = 0; 11234b8413fStracker-user foreach ($rows as $row) { 11334b8413fStracker-user if ($row['lastseen'] === 0 && !$showNever) { 11434b8413fStracker-user continue; 11534b8413fStracker-user } 11634b8413fStracker-user $count++; 11734b8413fStracker-user echo '<tr>'; 11834b8413fStracker-user echo '<td>' . hsc($row['login']) . '</td>'; 11934b8413fStracker-user echo '<td>' . hsc($row['name']) . '</td>'; 12034b8413fStracker-user echo '<td>' . hsc($row['grps']) . '</td>'; 12134b8413fStracker-user if ($row['lastseen'] === 0) { 12234b8413fStracker-user echo '<td class="lastseen_never">' . hsc($this->getLang('never')) . '</td>'; 12334b8413fStracker-user } else { 12434b8413fStracker-user echo '<td>' . hsc(dformat($row['lastseen'])) 12534b8413fStracker-user . ' <span class="lastseen_rel">(' 12634b8413fStracker-user . hsc($this->relativeTime($row['lastseen'])) . ')</span></td>'; 12734b8413fStracker-user } 12834b8413fStracker-user echo '</tr>'; 12934b8413fStracker-user } 13034b8413fStracker-user 13134b8413fStracker-user echo '</tbody></table></div>'; 13234b8413fStracker-user echo '<p class="lastseen_count">' . sprintf($this->getLang('total'), $count) . '</p>'; 13334b8413fStracker-user } 13434b8413fStracker-user 13534b8413fStracker-user /** 13634b8413fStracker-user * Emit a sortable column header. Clicking a header sorts by that column; 13734b8413fStracker-user * clicking the already-active column flips the direction. 13834b8413fStracker-user * 13934b8413fStracker-user * @param string $key column key 14034b8413fStracker-user * @param string $label visible header text 14134b8413fStracker-user * @param string $sort currently active sort column 14234b8413fStracker-user * @param string $order currently active order (asc|desc) 14334b8413fStracker-user * @param string $id current page id (for the link target) 14434b8413fStracker-user */ 14534b8413fStracker-user protected function headerCell($key, $label, $sort, $order, $id) 14634b8413fStracker-user { 14734b8413fStracker-user // If this column is already active, clicking flips the order; 14834b8413fStracker-user // otherwise a fresh column starts ascending. 14934b8413fStracker-user $newOrder = ($sort === $key && $order === 'asc') ? 'desc' : 'asc'; 15034b8413fStracker-user 15134b8413fStracker-user $arrow = ''; 15234b8413fStracker-user if ($sort === $key) { 15334b8413fStracker-user // ▲ U+25B2 / ▼ U+25BC as HTML entities (concatenated raw, not hsc'd) 15434b8413fStracker-user $arrow = ($order === 'asc') ? ' ▲' : ' ▼'; 15534b8413fStracker-user } 15634b8413fStracker-user 15760518ac5Stracker-user // wl() already returns an HTML-safe URL — its default separator is the 15860518ac5Stracker-user // pre-encoded "&". It must NOT be passed through hsc(): doing so 15960518ac5Stracker-user // double-encodes the ampersands ("&" -> "&amp;"), the browser 16060518ac5Stracker-user // then navigates to a URL containing a literal "&", and the query 16160518ac5Stracker-user // parameters arrive mis-named ("amp;sort" instead of "sort") — which 16260518ac5Stracker-user // silently breaks sorting. The label, being plain text, IS hsc()'d. 16334b8413fStracker-user $url = wl($id, [ 16434b8413fStracker-user 'do' => 'admin', 16534b8413fStracker-user 'page' => 'lastseen', 16634b8413fStracker-user 'sort' => $key, 16734b8413fStracker-user 'order' => $newOrder, 16834b8413fStracker-user ]); 16934b8413fStracker-user 17060518ac5Stracker-user echo '<th><a href="' . $url . '">' . hsc($label) . $arrow . '</a></th>'; 17134b8413fStracker-user } 17234b8413fStracker-user 17334b8413fStracker-user /** 17434b8413fStracker-user * Human-readable "time ago" string for a timestamp. 17534b8413fStracker-user * 17634b8413fStracker-user * @param int $timestamp 17734b8413fStracker-user * @return string 17834b8413fStracker-user */ 17934b8413fStracker-user protected function relativeTime($timestamp) 18034b8413fStracker-user { 18134b8413fStracker-user $diff = time() - $timestamp; 18234b8413fStracker-user if ($diff < 0) { 18334b8413fStracker-user $diff = 0; 18434b8413fStracker-user } 18534b8413fStracker-user 18634b8413fStracker-user if ($diff < 60) { 18734b8413fStracker-user return $this->getLang('rel_now'); 18834b8413fStracker-user } 18934b8413fStracker-user if ($diff < 3600) { 19034b8413fStracker-user return sprintf($this->getLang('rel_minutes'), (int) floor($diff / 60)); 19134b8413fStracker-user } 19234b8413fStracker-user if ($diff < 86400) { 19334b8413fStracker-user return sprintf($this->getLang('rel_hours'), (int) floor($diff / 3600)); 19434b8413fStracker-user } 19534b8413fStracker-user if ($diff < 86400 * 30) { 19634b8413fStracker-user return sprintf($this->getLang('rel_days'), (int) floor($diff / 86400)); 19734b8413fStracker-user } 19834b8413fStracker-user if ($diff < 86400 * 365) { 19934b8413fStracker-user return sprintf($this->getLang('rel_months'), (int) floor($diff / (86400 * 30))); 20034b8413fStracker-user } 20134b8413fStracker-user return sprintf($this->getLang('rel_years'), (int) floor($diff / (86400 * 365))); 20234b8413fStracker-user } 20334b8413fStracker-user} 204