xref: /plugin/lastseen/admin.php (revision 34b8413f371c8ced24cf179370bf6a8907b81994)
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') ? ' &#9650;' : ' &#9660;';
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