1*34b8413fStracker-user<?php 2*34b8413fStracker-user/** 3*34b8413fStracker-user * Last Seen plugin — admin panel page. 4*34b8413fStracker-user * 5*34b8413fStracker-user * Lists every registered user with the time of their last authenticated 6*34b8413fStracker-user * activity. Appears in the Admin panel right after the User Manager. 7*34b8413fStracker-user */ 8*34b8413fStracker-user 9*34b8413fStracker-userclass admin_plugin_lastseen extends DokuWiki_Admin_Plugin 10*34b8413fStracker-user{ 11*34b8413fStracker-user /** Admin-only — last-seen data is mildly sensitive activity information. */ 12*34b8413fStracker-user public function forAdminOnly() 13*34b8413fStracker-user { 14*34b8413fStracker-user return true; 15*34b8413fStracker-user } 16*34b8413fStracker-user 17*34b8413fStracker-user /** Position in the admin menu — directly after User Manager (sort 2). */ 18*34b8413fStracker-user public function getMenuSort() 19*34b8413fStracker-user { 20*34b8413fStracker-user return 3; 21*34b8413fStracker-user } 22*34b8413fStracker-user 23*34b8413fStracker-user public function getMenuText($language) 24*34b8413fStracker-user { 25*34b8413fStracker-user return $this->getLang('menu'); 26*34b8413fStracker-user } 27*34b8413fStracker-user 28*34b8413fStracker-user /** Read-only page — no form submissions to process. */ 29*34b8413fStracker-user public function handle() 30*34b8413fStracker-user { 31*34b8413fStracker-user } 32*34b8413fStracker-user 33*34b8413fStracker-user /** 34*34b8413fStracker-user * Render the admin page. 35*34b8413fStracker-user */ 36*34b8413fStracker-user public function html() 37*34b8413fStracker-user { 38*34b8413fStracker-user global $auth, $INPUT, $ID; 39*34b8413fStracker-user 40*34b8413fStracker-user echo '<h1>' . hsc($this->getLang('menu')) . '</h1>'; 41*34b8413fStracker-user 42*34b8413fStracker-user /** @var helper_plugin_lastseen $hlp */ 43*34b8413fStracker-user $hlp = plugin_load('helper', 'lastseen'); 44*34b8413fStracker-user if ($hlp === null) { 45*34b8413fStracker-user echo '<div class="error">Helper component could not be loaded.</div>'; 46*34b8413fStracker-user return; 47*34b8413fStracker-user } 48*34b8413fStracker-user 49*34b8413fStracker-user // Some auth backends (certain LDAP/AD setups) cannot enumerate users. 50*34b8413fStracker-user // authplain can; degrade gracefully for the rest. 51*34b8413fStracker-user if (!$auth || !$auth->canDo('getUsers')) { 52*34b8413fStracker-user echo '<div class="error">' . hsc($this->getLang('no_userlist')) . '</div>'; 53*34b8413fStracker-user return; 54*34b8413fStracker-user } 55*34b8413fStracker-user 56*34b8413fStracker-user // retrieveUsers(0, 0): start at 0, limit 0 == all users. 57*34b8413fStracker-user // Returns [username => ['name' => ..., 'mail' => ..., 'grps' => []]]. 58*34b8413fStracker-user $users = $auth->retrieveUsers(0, 0); 59*34b8413fStracker-user $seen = $hlp->getAll(); 60*34b8413fStracker-user 61*34b8413fStracker-user // ---- sorting ------------------------------------------------- 62*34b8413fStracker-user $sortable = ['login', 'name', 'grps', 'lastseen']; 63*34b8413fStracker-user $sort = $INPUT->str('sort', 'lastseen'); 64*34b8413fStracker-user if (!in_array($sort, $sortable, true)) { 65*34b8413fStracker-user $sort = 'lastseen'; 66*34b8413fStracker-user } 67*34b8413fStracker-user $order = ($INPUT->str('order', 'desc') === 'asc') ? 'asc' : 'desc'; 68*34b8413fStracker-user 69*34b8413fStracker-user $rows = []; 70*34b8413fStracker-user foreach ($users as $login => $info) { 71*34b8413fStracker-user $rows[] = [ 72*34b8413fStracker-user 'login' => $login, 73*34b8413fStracker-user 'name' => $info['name'] ?? '', 74*34b8413fStracker-user 'grps' => isset($info['grps']) ? implode(', ', (array) $info['grps']) : '', 75*34b8413fStracker-user 'lastseen' => isset($seen[$login]) ? (int) $seen[$login] : 0, // 0 == never 76*34b8413fStracker-user ]; 77*34b8413fStracker-user } 78*34b8413fStracker-user 79*34b8413fStracker-user usort($rows, function ($a, $b) use ($sort, $order) { 80*34b8413fStracker-user switch ($sort) { 81*34b8413fStracker-user case 'login': 82*34b8413fStracker-user $cmp = strcasecmp($a['login'], $b['login']); 83*34b8413fStracker-user break; 84*34b8413fStracker-user case 'name': 85*34b8413fStracker-user $cmp = strcasecmp($a['name'], $b['name']); 86*34b8413fStracker-user break; 87*34b8413fStracker-user case 'grps': 88*34b8413fStracker-user $cmp = strcasecmp($a['grps'], $b['grps']); 89*34b8413fStracker-user break; 90*34b8413fStracker-user case 'lastseen': 91*34b8413fStracker-user default: 92*34b8413fStracker-user $cmp = $a['lastseen'] <=> $b['lastseen']; 93*34b8413fStracker-user break; 94*34b8413fStracker-user } 95*34b8413fStracker-user return ($order === 'asc') ? $cmp : -$cmp; 96*34b8413fStracker-user }); 97*34b8413fStracker-user 98*34b8413fStracker-user $showNever = (bool) $this->getConf('show_never'); 99*34b8413fStracker-user 100*34b8413fStracker-user // ---- render -------------------------------------------------- 101*34b8413fStracker-user echo '<p>' . hsc($this->getLang('intro')) . '</p>'; 102*34b8413fStracker-user echo '<div class="table">'; 103*34b8413fStracker-user echo '<table class="inline plugin_lastseen">'; 104*34b8413fStracker-user echo '<thead><tr>'; 105*34b8413fStracker-user $this->headerCell('login', $this->getLang('col_login'), $sort, $order, $ID); 106*34b8413fStracker-user $this->headerCell('name', $this->getLang('col_name'), $sort, $order, $ID); 107*34b8413fStracker-user $this->headerCell('grps', $this->getLang('col_grps'), $sort, $order, $ID); 108*34b8413fStracker-user $this->headerCell('lastseen', $this->getLang('col_lastseen'), $sort, $order, $ID); 109*34b8413fStracker-user echo '</tr></thead><tbody>'; 110*34b8413fStracker-user 111*34b8413fStracker-user $count = 0; 112*34b8413fStracker-user foreach ($rows as $row) { 113*34b8413fStracker-user if ($row['lastseen'] === 0 && !$showNever) { 114*34b8413fStracker-user continue; 115*34b8413fStracker-user } 116*34b8413fStracker-user $count++; 117*34b8413fStracker-user echo '<tr>'; 118*34b8413fStracker-user echo '<td>' . hsc($row['login']) . '</td>'; 119*34b8413fStracker-user echo '<td>' . hsc($row['name']) . '</td>'; 120*34b8413fStracker-user echo '<td>' . hsc($row['grps']) . '</td>'; 121*34b8413fStracker-user if ($row['lastseen'] === 0) { 122*34b8413fStracker-user echo '<td class="lastseen_never">' . hsc($this->getLang('never')) . '</td>'; 123*34b8413fStracker-user } else { 124*34b8413fStracker-user echo '<td>' . hsc(dformat($row['lastseen'])) 125*34b8413fStracker-user . ' <span class="lastseen_rel">(' 126*34b8413fStracker-user . hsc($this->relativeTime($row['lastseen'])) . ')</span></td>'; 127*34b8413fStracker-user } 128*34b8413fStracker-user echo '</tr>'; 129*34b8413fStracker-user } 130*34b8413fStracker-user 131*34b8413fStracker-user echo '</tbody></table></div>'; 132*34b8413fStracker-user echo '<p class="lastseen_count">' . sprintf($this->getLang('total'), $count) . '</p>'; 133*34b8413fStracker-user } 134*34b8413fStracker-user 135*34b8413fStracker-user /** 136*34b8413fStracker-user * Emit a sortable column header. Clicking a header sorts by that column; 137*34b8413fStracker-user * clicking the already-active column flips the direction. 138*34b8413fStracker-user * 139*34b8413fStracker-user * @param string $key column key 140*34b8413fStracker-user * @param string $label visible header text 141*34b8413fStracker-user * @param string $sort currently active sort column 142*34b8413fStracker-user * @param string $order currently active order (asc|desc) 143*34b8413fStracker-user * @param string $id current page id (for the link target) 144*34b8413fStracker-user */ 145*34b8413fStracker-user protected function headerCell($key, $label, $sort, $order, $id) 146*34b8413fStracker-user { 147*34b8413fStracker-user // If this column is already active, clicking flips the order; 148*34b8413fStracker-user // otherwise a fresh column starts ascending. 149*34b8413fStracker-user $newOrder = ($sort === $key && $order === 'asc') ? 'desc' : 'asc'; 150*34b8413fStracker-user 151*34b8413fStracker-user $arrow = ''; 152*34b8413fStracker-user if ($sort === $key) { 153*34b8413fStracker-user // ▲ U+25B2 / ▼ U+25BC as HTML entities (concatenated raw, not hsc'd) 154*34b8413fStracker-user $arrow = ($order === 'asc') ? ' ▲' : ' ▼'; 155*34b8413fStracker-user } 156*34b8413fStracker-user 157*34b8413fStracker-user $url = wl($id, [ 158*34b8413fStracker-user 'do' => 'admin', 159*34b8413fStracker-user 'page' => 'lastseen', 160*34b8413fStracker-user 'sort' => $key, 161*34b8413fStracker-user 'order' => $newOrder, 162*34b8413fStracker-user ]); 163*34b8413fStracker-user 164*34b8413fStracker-user echo '<th><a href="' . hsc($url) . '">' . hsc($label) . $arrow . '</a></th>'; 165*34b8413fStracker-user } 166*34b8413fStracker-user 167*34b8413fStracker-user /** 168*34b8413fStracker-user * Human-readable "time ago" string for a timestamp. 169*34b8413fStracker-user * 170*34b8413fStracker-user * @param int $timestamp 171*34b8413fStracker-user * @return string 172*34b8413fStracker-user */ 173*34b8413fStracker-user protected function relativeTime($timestamp) 174*34b8413fStracker-user { 175*34b8413fStracker-user $diff = time() - $timestamp; 176*34b8413fStracker-user if ($diff < 0) { 177*34b8413fStracker-user $diff = 0; 178*34b8413fStracker-user } 179*34b8413fStracker-user 180*34b8413fStracker-user if ($diff < 60) { 181*34b8413fStracker-user return $this->getLang('rel_now'); 182*34b8413fStracker-user } 183*34b8413fStracker-user if ($diff < 3600) { 184*34b8413fStracker-user return sprintf($this->getLang('rel_minutes'), (int) floor($diff / 60)); 185*34b8413fStracker-user } 186*34b8413fStracker-user if ($diff < 86400) { 187*34b8413fStracker-user return sprintf($this->getLang('rel_hours'), (int) floor($diff / 3600)); 188*34b8413fStracker-user } 189*34b8413fStracker-user if ($diff < 86400 * 30) { 190*34b8413fStracker-user return sprintf($this->getLang('rel_days'), (int) floor($diff / 86400)); 191*34b8413fStracker-user } 192*34b8413fStracker-user if ($diff < 86400 * 365) { 193*34b8413fStracker-user return sprintf($this->getLang('rel_months'), (int) floor($diff / (86400 * 30))); 194*34b8413fStracker-user } 195*34b8413fStracker-user return sprintf($this->getLang('rel_years'), (int) floor($diff / (86400 * 365))); 196*34b8413fStracker-user } 197*34b8413fStracker-user} 198