xref: /plugin/lastseen/admin.php (revision e85c7321f150460107d896943a23fbe3faffced1)
134b8413fStracker-user<?php
2*e85c7321Stracker-user
3*e85c7321Stracker-userif (!defined('DOKU_INC')) die();
4*e85c7321Stracker-user
5*e85c7321Stracker-useruse dokuwiki\Extension\AdminPlugin;
6*e85c7321Stracker-user
734b8413fStracker-user/**
834b8413fStracker-user * Last Seen plugin — admin panel page.
934b8413fStracker-user *
1034b8413fStracker-user * Lists every registered user with the time of their last authenticated
1134b8413fStracker-user * activity. Appears in the Admin panel right after the User Manager.
1234b8413fStracker-user */
1334b8413fStracker-user
14*e85c7321Stracker-userclass admin_plugin_lastseen extends AdminPlugin
1534b8413fStracker-user{
16*e85c7321Stracker-user    /**
17*e85c7321Stracker-user     * Admin-only — last-seen data is mildly sensitive activity information.
18*e85c7321Stracker-user     *
19*e85c7321Stracker-user     * @return bool
20*e85c7321Stracker-user     */
2134b8413fStracker-user    public function forAdminOnly()
2234b8413fStracker-user    {
2334b8413fStracker-user        return true;
2434b8413fStracker-user    }
2534b8413fStracker-user
26*e85c7321Stracker-user    /**
27*e85c7321Stracker-user     * Position in the admin menu.
28*e85c7321Stracker-user     *
29*e85c7321Stracker-user     * @return int
30*e85c7321Stracker-user     */
3134b8413fStracker-user    public function getMenuSort()
3234b8413fStracker-user    {
33b9651948Stracker-user        return 1000;
3434b8413fStracker-user    }
3534b8413fStracker-user
36*e85c7321Stracker-user    /**
37*e85c7321Stracker-user     * @param string $language
38*e85c7321Stracker-user     * @return string
39*e85c7321Stracker-user     */
4034b8413fStracker-user    public function getMenuText($language)
4134b8413fStracker-user    {
4234b8413fStracker-user        return $this->getLang('menu');
4334b8413fStracker-user    }
4434b8413fStracker-user
45*e85c7321Stracker-user    /**
46*e85c7321Stracker-user     * Read-only page — no form submissions to process.
47*e85c7321Stracker-user     *
48*e85c7321Stracker-user     * @return void
49*e85c7321Stracker-user     */
5034b8413fStracker-user    public function handle()
5134b8413fStracker-user    {
5234b8413fStracker-user    }
5334b8413fStracker-user
5434b8413fStracker-user    /**
5534b8413fStracker-user     * Render the admin page.
5634b8413fStracker-user     */
5734b8413fStracker-user    public function html()
5834b8413fStracker-user    {
5934b8413fStracker-user        global $auth, $INPUT, $ID;
6034b8413fStracker-user
6134b8413fStracker-user        echo '<h1>' . hsc($this->getLang('menu')) . '</h1>';
6234b8413fStracker-user
6334b8413fStracker-user        /** @var helper_plugin_lastseen $hlp */
6434b8413fStracker-user        $hlp = plugin_load('helper', 'lastseen');
6534b8413fStracker-user        if ($hlp === null) {
6634b8413fStracker-user            echo '<div class="error">Helper component could not be loaded.</div>';
6734b8413fStracker-user            return;
6834b8413fStracker-user        }
6934b8413fStracker-user
7034b8413fStracker-user        // Some auth backends (certain LDAP/AD setups) cannot enumerate users.
7134b8413fStracker-user        // authplain can; degrade gracefully for the rest.
7234b8413fStracker-user        if (!$auth || !$auth->canDo('getUsers')) {
7334b8413fStracker-user            echo '<div class="error">' . hsc($this->getLang('no_userlist')) . '</div>';
7434b8413fStracker-user            return;
7534b8413fStracker-user        }
7634b8413fStracker-user
7734b8413fStracker-user        // retrieveUsers(0, 0): start at 0, limit 0 == all users.
7834b8413fStracker-user        // Returns [username => ['name' => ..., 'mail' => ..., 'grps' => []]].
7934b8413fStracker-user        $users = $auth->retrieveUsers(0, 0);
8034b8413fStracker-user        $seen  = $hlp->getAll();
8134b8413fStracker-user
8234b8413fStracker-user        // ---- sorting -------------------------------------------------
8334b8413fStracker-user        $sortable = ['login', 'name', 'grps', 'lastseen'];
8434b8413fStracker-user        $sort  = $INPUT->str('sort', 'lastseen');
8534b8413fStracker-user        if (!in_array($sort, $sortable, true)) {
8634b8413fStracker-user            $sort = 'lastseen';
8734b8413fStracker-user        }
8834b8413fStracker-user        $order = ($INPUT->str('order', 'desc') === 'asc') ? 'asc' : 'desc';
8934b8413fStracker-user
9034b8413fStracker-user        $rows = [];
9134b8413fStracker-user        foreach ($users as $login => $info) {
9234b8413fStracker-user            $rows[] = [
9334b8413fStracker-user                'login'    => $login,
9434b8413fStracker-user                'name'     => $info['name'] ?? '',
9534b8413fStracker-user                'grps'     => isset($info['grps']) ? implode(', ', (array) $info['grps']) : '',
9634b8413fStracker-user                'lastseen' => isset($seen[$login]) ? (int) $seen[$login] : 0, // 0 == never
9734b8413fStracker-user            ];
9834b8413fStracker-user        }
9934b8413fStracker-user
10034b8413fStracker-user        usort($rows, function ($a, $b) use ($sort, $order) {
10134b8413fStracker-user            switch ($sort) {
10234b8413fStracker-user                case 'login':
10334b8413fStracker-user                    $cmp = strcasecmp($a['login'], $b['login']);
10434b8413fStracker-user                    break;
10534b8413fStracker-user                case 'name':
10634b8413fStracker-user                    $cmp = strcasecmp($a['name'], $b['name']);
10734b8413fStracker-user                    break;
10834b8413fStracker-user                case 'grps':
10934b8413fStracker-user                    $cmp = strcasecmp($a['grps'], $b['grps']);
11034b8413fStracker-user                    break;
11134b8413fStracker-user                case 'lastseen':
11234b8413fStracker-user                default:
11334b8413fStracker-user                    $cmp = $a['lastseen'] <=> $b['lastseen'];
11434b8413fStracker-user                    break;
11534b8413fStracker-user            }
11634b8413fStracker-user            return ($order === 'asc') ? $cmp : -$cmp;
11734b8413fStracker-user        });
11834b8413fStracker-user
11934b8413fStracker-user        $showNever = (bool) $this->getConf('show_never');
12034b8413fStracker-user
12134b8413fStracker-user        // ---- render --------------------------------------------------
12234b8413fStracker-user        echo '<p>' . hsc($this->getLang('intro')) . '</p>';
12334b8413fStracker-user        echo '<div class="table">';
12434b8413fStracker-user        echo '<table class="inline plugin_lastseen">';
12534b8413fStracker-user        echo '<thead><tr>';
12634b8413fStracker-user        $this->headerCell('login',    $this->getLang('col_login'),    $sort, $order, $ID);
12734b8413fStracker-user        $this->headerCell('name',     $this->getLang('col_name'),     $sort, $order, $ID);
12834b8413fStracker-user        $this->headerCell('grps',     $this->getLang('col_grps'),     $sort, $order, $ID);
12934b8413fStracker-user        $this->headerCell('lastseen', $this->getLang('col_lastseen'), $sort, $order, $ID);
13034b8413fStracker-user        echo '</tr></thead><tbody>';
13134b8413fStracker-user
13234b8413fStracker-user        $count = 0;
13334b8413fStracker-user        foreach ($rows as $row) {
13434b8413fStracker-user            if ($row['lastseen'] === 0 && !$showNever) {
13534b8413fStracker-user                continue;
13634b8413fStracker-user            }
13734b8413fStracker-user            $count++;
13834b8413fStracker-user            echo '<tr>';
13934b8413fStracker-user            echo '<td>' . hsc($row['login']) . '</td>';
14034b8413fStracker-user            echo '<td>' . hsc($row['name']) . '</td>';
14134b8413fStracker-user            echo '<td>' . hsc($row['grps']) . '</td>';
14234b8413fStracker-user            if ($row['lastseen'] === 0) {
14334b8413fStracker-user                echo '<td class="lastseen_never">' . hsc($this->getLang('never')) . '</td>';
14434b8413fStracker-user            } else {
14534b8413fStracker-user                echo '<td>' . hsc(dformat($row['lastseen']))
14634b8413fStracker-user                    . ' <span class="lastseen_rel">('
14734b8413fStracker-user                    . hsc($this->relativeTime($row['lastseen'])) . ')</span></td>';
14834b8413fStracker-user            }
14934b8413fStracker-user            echo '</tr>';
15034b8413fStracker-user        }
15134b8413fStracker-user
15234b8413fStracker-user        echo '</tbody></table></div>';
153*e85c7321Stracker-user        $totalKey = ($count === 1) ? 'total_one' : 'total';
154*e85c7321Stracker-user        echo '<p class="lastseen_count">' . sprintf($this->getLang($totalKey), $count) . '</p>';
15534b8413fStracker-user    }
15634b8413fStracker-user
15734b8413fStracker-user    /**
15834b8413fStracker-user     * Emit a sortable column header. Clicking a header sorts by that column;
15934b8413fStracker-user     * clicking the already-active column flips the direction.
16034b8413fStracker-user     *
16134b8413fStracker-user     * @param string $key   column key
16234b8413fStracker-user     * @param string $label visible header text
16334b8413fStracker-user     * @param string $sort  currently active sort column
16434b8413fStracker-user     * @param string $order currently active order (asc|desc)
16534b8413fStracker-user     * @param string $id    current page id (for the link target)
16634b8413fStracker-user     */
16734b8413fStracker-user    protected function headerCell($key, $label, $sort, $order, $id)
16834b8413fStracker-user    {
16934b8413fStracker-user        // If this column is already active, clicking flips the order;
17034b8413fStracker-user        // otherwise a fresh column starts ascending.
17134b8413fStracker-user        $newOrder = ($sort === $key && $order === 'asc') ? 'desc' : 'asc';
17234b8413fStracker-user
17334b8413fStracker-user        $arrow = '';
17434b8413fStracker-user        if ($sort === $key) {
17534b8413fStracker-user            // ▲ U+25B2 / ▼ U+25BC as HTML entities (concatenated raw, not hsc'd)
17634b8413fStracker-user            $arrow = ($order === 'asc') ? ' &#9650;' : ' &#9660;';
17734b8413fStracker-user        }
17834b8413fStracker-user
17960518ac5Stracker-user        // wl() already returns an HTML-safe URL — its default separator is the
18060518ac5Stracker-user        // pre-encoded "&amp;". It must NOT be passed through hsc(): doing so
18160518ac5Stracker-user        // double-encodes the ampersands ("&amp;" -> "&amp;amp;"), the browser
18260518ac5Stracker-user        // then navigates to a URL containing a literal "&amp;", and the query
18360518ac5Stracker-user        // parameters arrive mis-named ("amp;sort" instead of "sort") — which
18460518ac5Stracker-user        // silently breaks sorting. The label, being plain text, IS hsc()'d.
18534b8413fStracker-user        $url = wl($id, [
18634b8413fStracker-user            'do'    => 'admin',
18734b8413fStracker-user            'page'  => 'lastseen',
18834b8413fStracker-user            'sort'  => $key,
18934b8413fStracker-user            'order' => $newOrder,
19034b8413fStracker-user        ]);
19134b8413fStracker-user
19260518ac5Stracker-user        echo '<th><a href="' . $url . '">' . hsc($label) . $arrow . '</a></th>';
19334b8413fStracker-user    }
19434b8413fStracker-user
19534b8413fStracker-user    /**
19634b8413fStracker-user     * Human-readable "time ago" string for a timestamp.
19734b8413fStracker-user     *
19834b8413fStracker-user     * @param int $timestamp
19934b8413fStracker-user     * @return string
20034b8413fStracker-user     */
20134b8413fStracker-user    protected function relativeTime($timestamp)
20234b8413fStracker-user    {
20334b8413fStracker-user        $diff = time() - $timestamp;
20434b8413fStracker-user        if ($diff < 0) {
20534b8413fStracker-user            $diff = 0;
20634b8413fStracker-user        }
20734b8413fStracker-user
20834b8413fStracker-user        if ($diff < 60) {
20934b8413fStracker-user            return $this->getLang('rel_now');
21034b8413fStracker-user        }
21134b8413fStracker-user        if ($diff < 3600) {
21234b8413fStracker-user            return sprintf($this->getLang('rel_minutes'), (int) floor($diff / 60));
21334b8413fStracker-user        }
21434b8413fStracker-user        if ($diff < 86400) {
21534b8413fStracker-user            return sprintf($this->getLang('rel_hours'), (int) floor($diff / 3600));
21634b8413fStracker-user        }
21734b8413fStracker-user        if ($diff < 86400 * 30) {
21834b8413fStracker-user            return sprintf($this->getLang('rel_days'), (int) floor($diff / 86400));
21934b8413fStracker-user        }
22034b8413fStracker-user        if ($diff < 86400 * 365) {
22134b8413fStracker-user            return sprintf($this->getLang('rel_months'), (int) floor($diff / (86400 * 30)));
22234b8413fStracker-user        }
22334b8413fStracker-user        return sprintf($this->getLang('rel_years'), (int) floor($diff / (86400 * 365)));
22434b8413fStracker-user    }
22534b8413fStracker-user}
226