xref: /plugin/lastseen/admin.php (revision 60518ac5e15ad2146a77203238dce0e34485568e)
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
1734b8413fStracker-user    /** Position in the admin menu — directly after User Manager (sort 2). */
1834b8413fStracker-user    public function getMenuSort()
1934b8413fStracker-user    {
2034b8413fStracker-user        return 3;
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') ? ' &#9650;' : ' &#9660;';
15534b8413fStracker-user        }
15634b8413fStracker-user
157*60518ac5Stracker-user        // wl() already returns an HTML-safe URL — its default separator is the
158*60518ac5Stracker-user        // pre-encoded "&amp;". It must NOT be passed through hsc(): doing so
159*60518ac5Stracker-user        // double-encodes the ampersands ("&amp;" -> "&amp;amp;"), the browser
160*60518ac5Stracker-user        // then navigates to a URL containing a literal "&amp;", and the query
161*60518ac5Stracker-user        // parameters arrive mis-named ("amp;sort" instead of "sort") — which
162*60518ac5Stracker-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
170*60518ac5Stracker-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