getLang('menu'); } /** Read-only page — no form submissions to process. */ public function handle() { } /** * Render the admin page. */ public function html() { global $auth, $INPUT, $ID; echo '

' . hsc($this->getLang('menu')) . '

'; /** @var helper_plugin_lastseen $hlp */ $hlp = plugin_load('helper', 'lastseen'); if ($hlp === null) { echo '
Helper component could not be loaded.
'; return; } // Some auth backends (certain LDAP/AD setups) cannot enumerate users. // authplain can; degrade gracefully for the rest. if (!$auth || !$auth->canDo('getUsers')) { echo '
' . hsc($this->getLang('no_userlist')) . '
'; return; } // retrieveUsers(0, 0): start at 0, limit 0 == all users. // Returns [username => ['name' => ..., 'mail' => ..., 'grps' => []]]. $users = $auth->retrieveUsers(0, 0); $seen = $hlp->getAll(); // ---- sorting ------------------------------------------------- $sortable = ['login', 'name', 'grps', 'lastseen']; $sort = $INPUT->str('sort', 'lastseen'); if (!in_array($sort, $sortable, true)) { $sort = 'lastseen'; } $order = ($INPUT->str('order', 'desc') === 'asc') ? 'asc' : 'desc'; $rows = []; foreach ($users as $login => $info) { $rows[] = [ 'login' => $login, 'name' => $info['name'] ?? '', 'grps' => isset($info['grps']) ? implode(', ', (array) $info['grps']) : '', 'lastseen' => isset($seen[$login]) ? (int) $seen[$login] : 0, // 0 == never ]; } usort($rows, function ($a, $b) use ($sort, $order) { switch ($sort) { case 'login': $cmp = strcasecmp($a['login'], $b['login']); break; case 'name': $cmp = strcasecmp($a['name'], $b['name']); break; case 'grps': $cmp = strcasecmp($a['grps'], $b['grps']); break; case 'lastseen': default: $cmp = $a['lastseen'] <=> $b['lastseen']; break; } return ($order === 'asc') ? $cmp : -$cmp; }); $showNever = (bool) $this->getConf('show_never'); // ---- render -------------------------------------------------- echo '

' . hsc($this->getLang('intro')) . '

'; echo '
'; echo ''; echo ''; $this->headerCell('login', $this->getLang('col_login'), $sort, $order, $ID); $this->headerCell('name', $this->getLang('col_name'), $sort, $order, $ID); $this->headerCell('grps', $this->getLang('col_grps'), $sort, $order, $ID); $this->headerCell('lastseen', $this->getLang('col_lastseen'), $sort, $order, $ID); echo ''; $count = 0; foreach ($rows as $row) { if ($row['lastseen'] === 0 && !$showNever) { continue; } $count++; echo ''; echo ''; echo ''; echo ''; if ($row['lastseen'] === 0) { echo ''; } else { echo ''; } echo ''; } echo '
' . hsc($row['login']) . '' . hsc($row['name']) . '' . hsc($row['grps']) . '' . hsc($this->getLang('never')) . '' . hsc(dformat($row['lastseen'])) . ' (' . hsc($this->relativeTime($row['lastseen'])) . ')
'; echo '

' . sprintf($this->getLang('total'), $count) . '

'; } /** * Emit a sortable column header. Clicking a header sorts by that column; * clicking the already-active column flips the direction. * * @param string $key column key * @param string $label visible header text * @param string $sort currently active sort column * @param string $order currently active order (asc|desc) * @param string $id current page id (for the link target) */ protected function headerCell($key, $label, $sort, $order, $id) { // If this column is already active, clicking flips the order; // otherwise a fresh column starts ascending. $newOrder = ($sort === $key && $order === 'asc') ? 'desc' : 'asc'; $arrow = ''; if ($sort === $key) { // ▲ U+25B2 / ▼ U+25BC as HTML entities (concatenated raw, not hsc'd) $arrow = ($order === 'asc') ? ' ▲' : ' ▼'; } // wl() already returns an HTML-safe URL — its default separator is the // pre-encoded "&". It must NOT be passed through hsc(): doing so // double-encodes the ampersands ("&" -> "&amp;"), the browser // then navigates to a URL containing a literal "&", and the query // parameters arrive mis-named ("amp;sort" instead of "sort") — which // silently breaks sorting. The label, being plain text, IS hsc()'d. $url = wl($id, [ 'do' => 'admin', 'page' => 'lastseen', 'sort' => $key, 'order' => $newOrder, ]); echo '' . hsc($label) . $arrow . ''; } /** * Human-readable "time ago" string for a timestamp. * * @param int $timestamp * @return string */ protected function relativeTime($timestamp) { $diff = time() - $timestamp; if ($diff < 0) { $diff = 0; } if ($diff < 60) { return $this->getLang('rel_now'); } if ($diff < 3600) { return sprintf($this->getLang('rel_minutes'), (int) floor($diff / 60)); } if ($diff < 86400) { return sprintf($this->getLang('rel_hours'), (int) floor($diff / 3600)); } if ($diff < 86400 * 30) { return sprintf($this->getLang('rel_days'), (int) floor($diff / 86400)); } if ($diff < 86400 * 365) { return sprintf($this->getLang('rel_months'), (int) floor($diff / (86400 * 30))); } return sprintf($this->getLang('rel_years'), (int) floor($diff / (86400 * 365))); } }