xref: /plugin/usersettings/admin.php (revision 54f11439a68c5f07eddd4c7f20a09acd149bde82)
11ab40613Stracker-user<?php
21ab40613Stracker-user
31ab40613Stracker-user/**
41ab40613Stracker-user * User Settings plugin — admin component.
51ab40613Stracker-user *
61ab40613Stracker-user * Provides an admin-only overview of every user's preferences as a flat,
71ab40613Stracker-user * sortable table: one row per (user x setting), with the columns
81ab40613Stracker-user *   Display name | Setting | Value | Changed by | Changed at.
91ab40613Stracker-user * A row's value is the user's explicit choice, or the toggle's default
101ab40613Stracker-user * (marked as such) when they never set one. A filter narrows the table to a
111ab40613Stracker-user * single setting; the table never grows wider as more toggles are added.
121ab40613Stracker-user *
131ab40613Stracker-user * Clicking a display name opens a per-user edit form (Model A+: an admin may
141ab40613Stracker-user * change anyone's preferences). Such a change is stored with the admin as the
151ab40613Stracker-user * recorded actor, and the user can still change it back themselves later.
161ab40613Stracker-user */
171ab40613Stracker-user
181ab40613Stracker-user// must be run within DokuWiki
191ab40613Stracker-userif (!defined('DOKU_INC')) die();
201ab40613Stracker-user
2149b74e0aStracker-useruse dokuwiki\Extension\AdminPlugin;
22*54f11439Stracker-useruse dokuwiki\Utf8\PhpString;
2349b74e0aStracker-user
2449b74e0aStracker-userclass admin_plugin_usersettings extends AdminPlugin
251ab40613Stracker-user{
261ab40613Stracker-user    /** @var string[] sort key => row field used for that sort */
271ab40613Stracker-user    protected $sortFields = [
28*54f11439Stracker-user        'login'     => 'user',
291ab40613Stracker-user        'name'      => 'display_name',
30*54f11439Stracker-user        'mail'      => 'mail',
31*54f11439Stracker-user        'grps'      => 'grps',
321ab40613Stracker-user        'setting'   => 'setting_label',
331ab40613Stracker-user        'value'     => 'value_display',
341ab40613Stracker-user        'changedby' => 'changed_by_display',
351ab40613Stracker-user        'changedat' => 'changed_at',
361ab40613Stracker-user    ];
371ab40613Stracker-user
381ab40613Stracker-user    // ---------------------------------------------------------------------
391ab40613Stracker-user    //  Admin plugin metadata
401ab40613Stracker-user    // ---------------------------------------------------------------------
411ab40613Stracker-user
421ab40613Stracker-user    /** Only administrators may see other users' preferences. */
431ab40613Stracker-user    public function forAdminOnly()
441ab40613Stracker-user    {
451ab40613Stracker-user        return true;
461ab40613Stracker-user    }
471ab40613Stracker-user
481ab40613Stracker-user    /** Position in the admin menu. */
491ab40613Stracker-user    public function getMenuSort()
501ab40613Stracker-user    {
51437b34a9Stracker-user        return 1000;
521ab40613Stracker-user    }
531ab40613Stracker-user
541ab40613Stracker-user    /** Admin menu label — distinct from the user menu's "Preferences". */
551ab40613Stracker-user    public function getMenuText($language)
561ab40613Stracker-user    {
571ab40613Stracker-user        return $this->getLang('admin_menu');
581ab40613Stracker-user    }
591ab40613Stracker-user
601ab40613Stracker-user    // ---------------------------------------------------------------------
611ab40613Stracker-user    //  Component access
621ab40613Stracker-user    // ---------------------------------------------------------------------
631ab40613Stracker-user
641ab40613Stracker-user    /** @return helper_plugin_usersettings|null */
651ab40613Stracker-user    protected function getHelper()
661ab40613Stracker-user    {
671ab40613Stracker-user        return plugin_load('helper', 'usersettings');
681ab40613Stracker-user    }
691ab40613Stracker-user
701ab40613Stracker-user    /** @return action_plugin_usersettings|null */
711ab40613Stracker-user    protected function getActionPlugin()
721ab40613Stracker-user    {
731ab40613Stracker-user        return plugin_load('action', 'usersettings');
741ab40613Stracker-user    }
751ab40613Stracker-user
761ab40613Stracker-user    // ---------------------------------------------------------------------
771ab40613Stracker-user    //  Request handling
781ab40613Stracker-user    // ---------------------------------------------------------------------
791ab40613Stracker-user
801ab40613Stracker-user    /**
811ab40613Stracker-user     * Handle a submitted per-user edit form.
821ab40613Stracker-user     *
831ab40613Stracker-user     * Runs only for admins (DokuWiki's admin dispatcher enforces
841ab40613Stracker-user     * forAdminOnly() before this is called). Uses Post/Redirect/Get.
851ab40613Stracker-user     */
861ab40613Stracker-user    public function handle()
871ab40613Stracker-user    {
881ab40613Stracker-user        global $INPUT, $ID;
891ab40613Stracker-user
901ab40613Stracker-user        if (!$INPUT->post->bool('usersettings_adminsave')) {
911ab40613Stracker-user            return;
921ab40613Stracker-user        }
931ab40613Stracker-user        if (!checkSecurityToken()) {
941ab40613Stracker-user            return;
951ab40613Stracker-user        }
961ab40613Stracker-user
971ab40613Stracker-user        $this->processAdminSave();
981ab40613Stracker-user
991ab40613Stracker-user        // Post/Redirect/Get back to the overview
1001ab40613Stracker-user        send_redirect(wl($ID, ['do' => 'admin', 'page' => 'usersettings'], true, '&'));
1011ab40613Stracker-user    }
1021ab40613Stracker-user
1031ab40613Stracker-user    /**
1041ab40613Stracker-user     * Validate the target user and store the submitted preferences for them,
1051ab40613Stracker-user     * recording the acting admin as the actor. Kept redirect-free so it can
1061ab40613Stracker-user     * be tested directly.
1071ab40613Stracker-user     *
1081ab40613Stracker-user     * @return bool
1091ab40613Stracker-user     */
1101ab40613Stracker-user    public function processAdminSave()
1111ab40613Stracker-user    {
1121ab40613Stracker-user        global $INPUT, $auth;
1131ab40613Stracker-user
1141ab40613Stracker-user        $target = $INPUT->post->str('edituser');
1151ab40613Stracker-user        $admin  = $INPUT->server->str('REMOTE_USER');
1161ab40613Stracker-user
1171ab40613Stracker-user        $userData = ($auth !== null) ? $auth->getUserData($target) : false;
1181ab40613Stracker-user        if ($userData === false) {
1191ab40613Stracker-user            msg($this->getLang('badidentuser'), -1);
1201ab40613Stracker-user            return false;
1211ab40613Stracker-user        }
1221ab40613Stracker-user
1231ab40613Stracker-user        $action = $this->getActionPlugin();
1241ab40613Stracker-user        $ok = ($action !== null) && $action->saveSubmittedPreferences($target, $admin);
1251ab40613Stracker-user
12649b74e0aStracker-user        $name = (($userData['name'] ?? '') !== '') ? $userData['name'] : $target;
1271ab40613Stracker-user        msg(
1281ab40613Stracker-user            sprintf($this->getLang($ok ? 'adminsaved' : 'adminsavefail'), hsc($name)),
1291ab40613Stracker-user            $ok ? 1 : -1
1301ab40613Stracker-user        );
1311ab40613Stracker-user        return $ok;
1321ab40613Stracker-user    }
1331ab40613Stracker-user
1341ab40613Stracker-user    // ---------------------------------------------------------------------
1351ab40613Stracker-user    //  Output
1361ab40613Stracker-user    // ---------------------------------------------------------------------
1371ab40613Stracker-user
1381ab40613Stracker-user    /**
1391ab40613Stracker-user     * Render either the overview table or, when an edituser parameter is
1401ab40613Stracker-user     * present, the per-user edit form.
1411ab40613Stracker-user     */
1421ab40613Stracker-user    public function html()
1431ab40613Stracker-user    {
1441ab40613Stracker-user        global $INPUT;
1451ab40613Stracker-user
1461ab40613Stracker-user        $edituser = $INPUT->get->str('edituser');
1471ab40613Stracker-user        if ($edituser !== '') {
1481ab40613Stracker-user            echo $this->renderEditForm($edituser);
1491ab40613Stracker-user        } else {
1501ab40613Stracker-user            echo $this->renderTable();
1511ab40613Stracker-user        }
1521ab40613Stracker-user    }
1531ab40613Stracker-user
1541ab40613Stracker-user    // ---- overview table --------------------------------------------------
1551ab40613Stracker-user
1561ab40613Stracker-user    /**
1571ab40613Stracker-user     * Build the rows of the overview: one per (user x toggle).
1581ab40613Stracker-user     *
1591ab40613Stracker-user     * @param array $users   [username => userdata] as from $auth->retrieveUsers()
1601ab40613Stracker-user     * @param array $toggles registered toggle definitions
1611ab40613Stracker-user     * @return array list of row arrays
1621ab40613Stracker-user     */
1631ab40613Stracker-user    public function buildRows(array $users, array $toggles)
1641ab40613Stracker-user    {
1651ab40613Stracker-user        $helper = $this->getHelper();
1661ab40613Stracker-user        if ($helper === null) {
1671ab40613Stracker-user            return [];
1681ab40613Stracker-user        }
1691ab40613Stracker-user
1701ab40613Stracker-user        $rows = [];
1711ab40613Stracker-user        foreach ($users as $username => $userData) {
1721ab40613Stracker-user            $displayName = (!empty($userData['name'])) ? $userData['name'] : $username;
173*54f11439Stracker-user            $mail        = $userData['mail'] ?? '';
174*54f11439Stracker-user            $grps        = isset($userData['grps']) ? implode(', ', (array) $userData['grps']) : '';
1751ab40613Stracker-user            $stored      = $helper->loadUserData($username);
1761ab40613Stracker-user
1771ab40613Stracker-user            foreach ($toggles as $key => $def) {
1781ab40613Stracker-user                if (isset($stored[$key]) && array_key_exists('value', $stored[$key])) {
1791ab40613Stracker-user                    $value     = $stored[$key]['value'];
1801ab40613Stracker-user                    $changedBy = $stored[$key]['changed_by'] ?? '';
1811ab40613Stracker-user                    $changedAt = (int) ($stored[$key]['changed_at'] ?? 0);
1821ab40613Stracker-user                    $isDefault = false;
1831ab40613Stracker-user                } else {
1841ab40613Stracker-user                    $value     = $def['default'];
1851ab40613Stracker-user                    $changedBy = '';
1861ab40613Stracker-user                    $changedAt = 0;
1871ab40613Stracker-user                    $isDefault = true;
1881ab40613Stracker-user                }
1891ab40613Stracker-user
1901ab40613Stracker-user                $rows[] = [
1911ab40613Stracker-user                    'user'               => $username,
1921ab40613Stracker-user                    'display_name'       => $displayName,
193*54f11439Stracker-user                    'mail'               => $mail,
194*54f11439Stracker-user                    'grps'               => $grps,
1951ab40613Stracker-user                    'setting_key'        => $key,
1961ab40613Stracker-user                    'setting_label'      => $def['label'],
1971ab40613Stracker-user                    'value_display'      => $this->displayValue($def, $value),
1981ab40613Stracker-user                    'is_default'         => $isDefault,
1991ab40613Stracker-user                    'changed_by_display' => $isDefault ? '' : $this->resolveActor($changedBy, $users),
2001ab40613Stracker-user                    'changed_at'         => $changedAt,
2011ab40613Stracker-user                ];
2021ab40613Stracker-user            }
2031ab40613Stracker-user        }
2041ab40613Stracker-user        return $rows;
2051ab40613Stracker-user    }
2061ab40613Stracker-user
2071ab40613Stracker-user    /**
2081ab40613Stracker-user     * Sort overview rows by the given column and direction.
2091ab40613Stracker-user     *
2101ab40613Stracker-user     * @param array  $rows
2111ab40613Stracker-user     * @param string $sort one of the keys of $this->sortFields
2121ab40613Stracker-user     * @param string $dir  'asc' or 'desc'
2131ab40613Stracker-user     * @return array
2141ab40613Stracker-user     */
2151ab40613Stracker-user    public function sortRows(array $rows, $sort, $dir)
2161ab40613Stracker-user    {
2171ab40613Stracker-user        $field = $this->sortFields[$sort] ?? 'display_name';
2181ab40613Stracker-user
2191ab40613Stracker-user        usort($rows, function ($a, $b) use ($field) {
2201ab40613Stracker-user            if ($field === 'changed_at') {
2211ab40613Stracker-user                return $a[$field] <=> $b[$field];
2221ab40613Stracker-user            }
2231ab40613Stracker-user            return strcasecmp((string) $a[$field], (string) $b[$field]);
2241ab40613Stracker-user        });
2251ab40613Stracker-user
2261ab40613Stracker-user        if ($dir === 'desc') {
2271ab40613Stracker-user            $rows = array_reverse($rows);
2281ab40613Stracker-user        }
2291ab40613Stracker-user        return $rows;
2301ab40613Stracker-user    }
2311ab40613Stracker-user
2321ab40613Stracker-user    /**
2331ab40613Stracker-user     * Human-readable value of a toggle: On/Off for a checkbox, the option
2341ab40613Stracker-user     * label for a select.
2351ab40613Stracker-user     *
2361ab40613Stracker-user     * @param array $def
2371ab40613Stracker-user     * @param mixed $value
2381ab40613Stracker-user     * @return string
2391ab40613Stracker-user     */
2401ab40613Stracker-user    public function displayValue(array $def, $value)
2411ab40613Stracker-user    {
2421ab40613Stracker-user        if ($def['type'] === 'select') {
2431ab40613Stracker-user            if (isset($def['options'][$value])) {
2441ab40613Stracker-user                return (string) $def['options'][$value];
2451ab40613Stracker-user            }
2461ab40613Stracker-user            return (string) $value; // stored value no longer a defined option
2471ab40613Stracker-user        }
2481ab40613Stracker-user        return $this->getLang(empty($value) ? 'val_off' : 'val_on');
2491ab40613Stracker-user    }
2501ab40613Stracker-user
2511ab40613Stracker-user    /**
2521ab40613Stracker-user     * Resolve an actor username to a display name, falling back to the raw
2531ab40613Stracker-user     * username when the actor is not (or no longer) a known user.
2541ab40613Stracker-user     *
2551ab40613Stracker-user     * @param string $actor
2561ab40613Stracker-user     * @param array  $users [username => userdata]
2571ab40613Stracker-user     * @return string
2581ab40613Stracker-user     */
2591ab40613Stracker-user    protected function resolveActor($actor, array $users)
2601ab40613Stracker-user    {
2611ab40613Stracker-user        if ($actor === '') {
2621ab40613Stracker-user            return '';
2631ab40613Stracker-user        }
2641ab40613Stracker-user        if (isset($users[$actor]) && !empty($users[$actor]['name'])) {
2651ab40613Stracker-user            return $users[$actor]['name'];
2661ab40613Stracker-user        }
2671ab40613Stracker-user        return $actor;
2681ab40613Stracker-user    }
2691ab40613Stracker-user
2701ab40613Stracker-user    /**
271*54f11439Stracker-user     * Render the overview table: sortable headers, a per-column text-filter
272*54f11439Stracker-user     * row (plus the existing setting drop-down), the rows for the current
273*54f11439Stracker-user     * page, and a numbered pager.
2741ab40613Stracker-user     *
2751ab40613Stracker-user     * @return string
2761ab40613Stracker-user     */
2771ab40613Stracker-user    protected function renderTable()
2781ab40613Stracker-user    {
279*54f11439Stracker-user        global $INPUT, $auth, $ID;
2801ab40613Stracker-user
2811ab40613Stracker-user        $helper  = $this->getHelper();
2821ab40613Stracker-user        $toggles = $helper ? $helper->getRegisteredToggles() : [];
2831ab40613Stracker-user
2841ab40613Stracker-user        $html  = '<div class="plugin_usersettings_admin">';
2851ab40613Stracker-user        $html .= '<h1>' . hsc($this->getLang('admin_heading')) . '</h1>';
2861ab40613Stracker-user
2871ab40613Stracker-user        if (empty($toggles)) {
2881ab40613Stracker-user            return $html . '<p>' . hsc($this->getLang('notoggles')) . '</p></div>';
2891ab40613Stracker-user        }
2901ab40613Stracker-user
2911ab40613Stracker-user        $users = ($auth !== null) ? $auth->retrieveUsers(0, 0) : [];
2921ab40613Stracker-user        if (empty($users)) {
2931ab40613Stracker-user            return $html . '<p>' . hsc($this->getLang('nousers')) . '</p></div>';
2941ab40613Stracker-user        }
2951ab40613Stracker-user
296*54f11439Stracker-user        $showMail = (bool) $this->getConf('show_mail');
297*54f11439Stracker-user        $showGrps = (bool) $this->getConf('show_grps');
298*54f11439Stracker-user        $perPage  = (int) $this->getConf('entries_per_page');
299*54f11439Stracker-user
300*54f11439Stracker-user        // visible columns, in display order
301*54f11439Stracker-user        $cols = ['login', 'name'];
302*54f11439Stracker-user        if ($showMail) {
303*54f11439Stracker-user            $cols[] = 'mail';
304*54f11439Stracker-user        }
305*54f11439Stracker-user        if ($showGrps) {
306*54f11439Stracker-user            $cols[] = 'grps';
307*54f11439Stracker-user        }
308*54f11439Stracker-user        $cols = array_merge($cols, ['setting', 'value', 'changedby', 'changedat']);
309*54f11439Stracker-user
310*54f11439Stracker-user        // col => row field for the text-filterable columns ("Setting" has its
311*54f11439Stracker-user        // own drop-down; "Changed at" is not filterable)
312*54f11439Stracker-user        $filterMap = $this->filterFieldMap($cols);
313*54f11439Stracker-user
3141ab40613Stracker-user        // request parameters (sort links and the filter form are GET)
3151ab40613Stracker-user        $sort = $INPUT->get->str('sort', 'name');
3161ab40613Stracker-user        if (!isset($this->sortFields[$sort])) {
3171ab40613Stracker-user            $sort = 'name';
3181ab40613Stracker-user        }
319*54f11439Stracker-user        if (($sort === 'mail' && !$showMail) || ($sort === 'grps' && !$showGrps)) {
320*54f11439Stracker-user            $sort = 'name';
3211ab40613Stracker-user        }
322*54f11439Stracker-user        $dir = ($INPUT->get->str('dir') === 'desc') ? 'desc' : 'asc';
323*54f11439Stracker-user
324*54f11439Stracker-user        $setFilter = $INPUT->get->str('filter');
325*54f11439Stracker-user        if ($setFilter !== '' && !isset($toggles[$setFilter])) {
326*54f11439Stracker-user            $setFilter = '';
327*54f11439Stracker-user        }
328*54f11439Stracker-user        $qfilters = $this->activeFilters(array_keys($filterMap));
3291ab40613Stracker-user
3301ab40613Stracker-user        $html .= '<p>' . hsc($this->getLang('admin_intro')) . '</p>';
3311ab40613Stracker-user
332*54f11439Stracker-user        // build rows, narrow by the setting drop-down, then the text filters,
333*54f11439Stracker-user        // then sort — all before paging so the counts and page numbers match
3341ab40613Stracker-user        $rows = $this->buildRows($users, $toggles);
335*54f11439Stracker-user        if ($setFilter !== '') {
336*54f11439Stracker-user            $rows = array_values(array_filter($rows, static function ($r) use ($setFilter) {
337*54f11439Stracker-user                return $r['setting_key'] === $setFilter;
3381ab40613Stracker-user            }));
3391ab40613Stracker-user        }
340*54f11439Stracker-user        $rows  = $this->applyFilters($rows, $qfilters, $filterMap);
3411ab40613Stracker-user        $rows  = $this->sortRows($rows, $sort, $dir);
342*54f11439Stracker-user        $total = count($rows);
3431ab40613Stracker-user
344*54f11439Stracker-user        [$pageRows, $page, $totalPages, $from, $to] = $this->paginate($rows, $perPage);
3451ab40613Stracker-user
346*54f11439Stracker-user        $labels = [
347*54f11439Stracker-user            'login'     => $this->getLang('th_login'),
348*54f11439Stracker-user            'name'      => $this->getLang('th_name'),
349*54f11439Stracker-user            'mail'      => $this->getLang('th_mail'),
350*54f11439Stracker-user            'grps'      => $this->getLang('th_grps'),
351*54f11439Stracker-user            'setting'   => $this->getLang('th_setting'),
352*54f11439Stracker-user            'value'     => $this->getLang('th_value'),
353*54f11439Stracker-user            'changedby' => $this->getLang('th_changedby'),
354*54f11439Stracker-user            'changedat' => $this->getLang('th_changedat'),
355*54f11439Stracker-user        ];
3561ab40613Stracker-user
357*54f11439Stracker-user        // GET form so the filters combine with the sort links and bookmark
358*54f11439Stracker-user        // cleanly; the action URL's query string is dropped on submit, so the
359*54f11439Stracker-user        // standing parameters travel as hidden fields.
360*54f11439Stracker-user        $html .= '<form class="us-filter-form" method="get" action="' . DOKU_BASE . DOKU_SCRIPT . '">';
3611ab40613Stracker-user        $html .= '<input type="hidden" name="id" value="' . hsc($ID) . '" />';
3621ab40613Stracker-user        $html .= '<input type="hidden" name="do" value="admin" />';
3631ab40613Stracker-user        $html .= '<input type="hidden" name="page" value="usersettings" />';
3641ab40613Stracker-user        $html .= '<input type="hidden" name="sort" value="' . hsc($sort) . '" />';
3651ab40613Stracker-user        $html .= '<input type="hidden" name="dir" value="' . hsc($dir) . '" />';
366*54f11439Stracker-user        $html .= '<input type="hidden" name="pg" value="1" />'; // a new search lands on page 1
3671ab40613Stracker-user
368*54f11439Stracker-user        $html .= '<div class="table">';
369*54f11439Stracker-user        $html .= '<table class="inline plugin_usersettings_table">';
370*54f11439Stracker-user        $html .= '<thead><tr>';
371*54f11439Stracker-user        foreach ($cols as $c) {
372*54f11439Stracker-user            $html .= $this->sortHeader($labels[$c], $c, $sort, $dir, $setFilter, $qfilters);
3731ab40613Stracker-user        }
374*54f11439Stracker-user        $html .= '</tr>';
375*54f11439Stracker-user        $html .= $this->renderFilterRow($cols, $filterMap, $qfilters, $toggles, $setFilter, $sort, $dir);
376*54f11439Stracker-user        $html .= '</thead><tbody>';
377*54f11439Stracker-user
378*54f11439Stracker-user        if ($total === 0) {
379*54f11439Stracker-user            $html .= '<tr><td colspan="' . count($cols) . '" class="us-none">'
380*54f11439Stracker-user                   . hsc($this->getLang('none')) . '</td></tr>';
381*54f11439Stracker-user        } else {
382*54f11439Stracker-user            foreach ($pageRows as $row) {
383*54f11439Stracker-user                $rowClass = $row['is_default'] ? ' class="us-default-row"' : '';
384*54f11439Stracker-user                $html .= '<tr' . $rowClass . '>';
385*54f11439Stracker-user                foreach ($cols as $c) {
386*54f11439Stracker-user                    $html .= $this->bodyCell($c, $row);
387*54f11439Stracker-user                }
388*54f11439Stracker-user                $html .= '</tr>';
389*54f11439Stracker-user            }
390*54f11439Stracker-user        }
391*54f11439Stracker-user
392*54f11439Stracker-user        $html .= '</tbody></table></div>';
393*54f11439Stracker-user        $html .= '</form>';
394*54f11439Stracker-user
395*54f11439Stracker-user        $html .= $this->renderPager($page, $totalPages, $sort, $dir, $setFilter, $qfilters);
396*54f11439Stracker-user
397*54f11439Stracker-user        if ($total > 0) {
398*54f11439Stracker-user            $html .= '<p class="us-count">'
399*54f11439Stracker-user                   . hsc(sprintf($this->getLang('shown'), $from, $to, $total)) . '</p>';
400*54f11439Stracker-user        }
401*54f11439Stracker-user
402*54f11439Stracker-user        return $html . '</div>';
4031ab40613Stracker-user    }
4041ab40613Stracker-user
4051ab40613Stracker-user    /**
406*54f11439Stracker-user     * Render one body cell for a column.
407*54f11439Stracker-user     *
408*54f11439Stracker-user     * @param string $c   column key
409*54f11439Stracker-user     * @param array  $row
410*54f11439Stracker-user     * @return string
411*54f11439Stracker-user     */
412*54f11439Stracker-user    protected function bodyCell($c, array $row)
413*54f11439Stracker-user    {
414*54f11439Stracker-user        switch ($c) {
415*54f11439Stracker-user            case 'login':
416*54f11439Stracker-user                return '<td>' . hsc($row['user']) . '</td>';
417*54f11439Stracker-user            case 'name':
418*54f11439Stracker-user                $editUrl = $this->pageURL(['edituser' => $row['user']]);
419*54f11439Stracker-user                return '<td><a href="' . $editUrl . '">' . hsc($row['display_name']) . '</a></td>';
420*54f11439Stracker-user            case 'mail':
421*54f11439Stracker-user                return '<td>' . hsc($row['mail']) . '</td>';
422*54f11439Stracker-user            case 'grps':
423*54f11439Stracker-user                return '<td>' . hsc($row['grps']) . '</td>';
424*54f11439Stracker-user            case 'setting':
425*54f11439Stracker-user                return '<td>' . hsc($row['setting_label']) . '</td>';
426*54f11439Stracker-user            case 'value':
427*54f11439Stracker-user                return '<td>' . hsc($row['value_display']) . '</td>';
428*54f11439Stracker-user            case 'changedby':
429*54f11439Stracker-user                return '<td>' . ($row['is_default']
430*54f11439Stracker-user                    ? '<span class="us-default-mark">' . hsc($this->getLang('bydefault')) . '</span>'
431*54f11439Stracker-user                    : hsc($row['changed_by_display'])) . '</td>';
432*54f11439Stracker-user            case 'changedat':
433*54f11439Stracker-user                return '<td>' . ($row['changed_at'] > 0 ? hsc(dformat($row['changed_at'])) : '&mdash;') . '</td>';
434*54f11439Stracker-user            default:
435*54f11439Stracker-user                return '<td></td>';
436*54f11439Stracker-user        }
437*54f11439Stracker-user    }
438*54f11439Stracker-user
439*54f11439Stracker-user    // ---- filtering -------------------------------------------------------
440*54f11439Stracker-user
441*54f11439Stracker-user    /**
442*54f11439Stracker-user     * Map of text-filterable column => row field, limited to the visible
443*54f11439Stracker-user     * columns. "Setting" (drop-down) and "Changed at" are deliberately absent.
444*54f11439Stracker-user     *
445*54f11439Stracker-user     * @param string[] $cols visible columns
446*54f11439Stracker-user     * @return array
447*54f11439Stracker-user     */
448*54f11439Stracker-user    protected function filterFieldMap(array $cols)
449*54f11439Stracker-user    {
450*54f11439Stracker-user        $all = [
451*54f11439Stracker-user            'login'     => 'user',
452*54f11439Stracker-user            'name'      => 'display_name',
453*54f11439Stracker-user            'mail'      => 'mail',
454*54f11439Stracker-user            'grps'      => 'grps',
455*54f11439Stracker-user            'value'     => 'value_display',
456*54f11439Stracker-user            'changedby' => 'changed_by_display',
457*54f11439Stracker-user        ];
458*54f11439Stracker-user        $map = [];
459*54f11439Stracker-user        foreach ($cols as $c) {
460*54f11439Stracker-user            if (isset($all[$c])) {
461*54f11439Stracker-user                $map[$c] = $all[$c];
462*54f11439Stracker-user            }
463*54f11439Stracker-user        }
464*54f11439Stracker-user        return $map;
465*54f11439Stracker-user    }
466*54f11439Stracker-user
467*54f11439Stracker-user    /**
468*54f11439Stracker-user     * Read the active text filters from the request (the q[] array), keeping
469*54f11439Stracker-user     * only the given columns and dropping blanks.
470*54f11439Stracker-user     *
471*54f11439Stracker-user     * @param string[] $cols filterable column keys
472*54f11439Stracker-user     * @return array [column => trimmed term]
473*54f11439Stracker-user     */
474*54f11439Stracker-user    protected function activeFilters(array $cols)
475*54f11439Stracker-user    {
476*54f11439Stracker-user        global $INPUT;
477*54f11439Stracker-user        $raw = $INPUT->arr('q');
478*54f11439Stracker-user        $out = [];
479*54f11439Stracker-user        foreach ($cols as $c) {
480*54f11439Stracker-user            if (isset($raw[$c]) && is_string($raw[$c])) {
481*54f11439Stracker-user                $term = trim($raw[$c]);
482*54f11439Stracker-user                if ($term !== '') {
483*54f11439Stracker-user                    $out[$c] = $term;
484*54f11439Stracker-user                }
485*54f11439Stracker-user            }
486*54f11439Stracker-user        }
487*54f11439Stracker-user        return $out;
488*54f11439Stracker-user    }
489*54f11439Stracker-user
490*54f11439Stracker-user    /**
491*54f11439Stracker-user     * Keep only rows matching every active text filter (substring,
492*54f11439Stracker-user     * case-insensitive).
493*54f11439Stracker-user     *
494*54f11439Stracker-user     * @param array $rows
495*54f11439Stracker-user     * @param array $qfilters [column => term]
496*54f11439Stracker-user     * @param array $map      [column => row field]
497*54f11439Stracker-user     * @return array
498*54f11439Stracker-user     */
499*54f11439Stracker-user    protected function applyFilters(array $rows, array $qfilters, array $map)
500*54f11439Stracker-user    {
501*54f11439Stracker-user        if ($qfilters === []) {
502*54f11439Stracker-user            return $rows;
503*54f11439Stracker-user        }
504*54f11439Stracker-user        return array_values(array_filter($rows, function ($row) use ($qfilters, $map) {
505*54f11439Stracker-user            foreach ($qfilters as $col => $term) {
506*54f11439Stracker-user                $field = $map[$col] ?? null;
507*54f11439Stracker-user                if ($field === null) {
508*54f11439Stracker-user                    continue;
509*54f11439Stracker-user                }
510*54f11439Stracker-user                if (!$this->matches($row[$field] ?? '', $term)) {
511*54f11439Stracker-user                    return false;
512*54f11439Stracker-user                }
513*54f11439Stracker-user            }
514*54f11439Stracker-user            return true;
515*54f11439Stracker-user        }));
516*54f11439Stracker-user    }
517*54f11439Stracker-user
518*54f11439Stracker-user    /**
519*54f11439Stracker-user     * Case-insensitive UTF-8 substring test.
520*54f11439Stracker-user     *
521*54f11439Stracker-user     * @param string $haystack
522*54f11439Stracker-user     * @param string $needle
523*54f11439Stracker-user     * @return bool
524*54f11439Stracker-user     */
525*54f11439Stracker-user    protected function matches($haystack, $needle)
526*54f11439Stracker-user    {
527*54f11439Stracker-user        if ($needle === '') {
528*54f11439Stracker-user            return true;
529*54f11439Stracker-user        }
530*54f11439Stracker-user        $h = PhpString::strtolower((string) $haystack);
531*54f11439Stracker-user        $n = PhpString::strtolower((string) $needle);
532*54f11439Stracker-user        return PhpString::strpos($h, $n) !== false;
533*54f11439Stracker-user    }
534*54f11439Stracker-user
535*54f11439Stracker-user    // ---- pagination ------------------------------------------------------
536*54f11439Stracker-user
537*54f11439Stracker-user    /**
538*54f11439Stracker-user     * Slice the rows for the current page.
539*54f11439Stracker-user     *
540*54f11439Stracker-user     * @param array $rows    filtered + sorted rows
541*54f11439Stracker-user     * @param int   $perPage rows per page; <= 0 means "all on one page"
542*54f11439Stracker-user     * @return array [pageRows, page, totalPages, from, to] — from/to are 1-based
543*54f11439Stracker-user     *               row numbers of the slice (0 when there are no rows)
544*54f11439Stracker-user     */
545*54f11439Stracker-user    protected function paginate(array $rows, $perPage)
546*54f11439Stracker-user    {
547*54f11439Stracker-user        global $INPUT;
548*54f11439Stracker-user        $total = count($rows);
549*54f11439Stracker-user
550*54f11439Stracker-user        if ($perPage <= 0) {
551*54f11439Stracker-user            return [$rows, 1, 1, $total > 0 ? 1 : 0, $total];
552*54f11439Stracker-user        }
553*54f11439Stracker-user
554*54f11439Stracker-user        $totalPages = max(1, (int) ceil($total / $perPage));
555*54f11439Stracker-user        $page = $INPUT->int('pg', 1);
556*54f11439Stracker-user        if ($page < 1) {
557*54f11439Stracker-user            $page = 1;
558*54f11439Stracker-user        }
559*54f11439Stracker-user        if ($page > $totalPages) {
560*54f11439Stracker-user            $page = $totalPages;
561*54f11439Stracker-user        }
562*54f11439Stracker-user
563*54f11439Stracker-user        $offset = ($page - 1) * $perPage;
564*54f11439Stracker-user        $slice  = array_slice($rows, $offset, $perPage);
565*54f11439Stracker-user        $from   = $total > 0 ? $offset + 1 : 0;
566*54f11439Stracker-user        $to     = min($total, $offset + $perPage);
567*54f11439Stracker-user
568*54f11439Stracker-user        return [$slice, $page, $totalPages, $from, $to];
569*54f11439Stracker-user    }
570*54f11439Stracker-user
571*54f11439Stracker-user    // ---- link + header + filter-row + pager helpers ----------------------
572*54f11439Stracker-user
573*54f11439Stracker-user    /**
574*54f11439Stracker-user     * The standing query parameters every in-table link must carry: sort,
575*54f11439Stracker-user     * direction, the setting drop-down and the active text filters.
576*54f11439Stracker-user     *
577*54f11439Stracker-user     * @param string $sort
578*54f11439Stracker-user     * @param string $dir
579*54f11439Stracker-user     * @param string $setFilter
580*54f11439Stracker-user     * @param array  $qfilters
581*54f11439Stracker-user     * @return array
582*54f11439Stracker-user     */
583*54f11439Stracker-user    protected function standingParams($sort, $dir, $setFilter, array $qfilters)
584*54f11439Stracker-user    {
585*54f11439Stracker-user        $params = ['sort' => $sort, 'dir' => $dir];
586*54f11439Stracker-user        if ($setFilter !== '') {
587*54f11439Stracker-user            $params['filter'] = $setFilter;
588*54f11439Stracker-user        }
589*54f11439Stracker-user        if ($qfilters !== []) {
590*54f11439Stracker-user            $params['q'] = $qfilters;
591*54f11439Stracker-user        }
592*54f11439Stracker-user        return $params;
593*54f11439Stracker-user    }
594*54f11439Stracker-user
595*54f11439Stracker-user    /**
596*54f11439Stracker-user     * Build a URL back to this admin page with the given query parameters.
597*54f11439Stracker-user     *
598*54f11439Stracker-user     * @param array $params
599*54f11439Stracker-user     * @return string HTML-attribute-safe URL
600*54f11439Stracker-user     */
601*54f11439Stracker-user    protected function tableURL(array $params)
602*54f11439Stracker-user    {
603*54f11439Stracker-user        global $ID;
604*54f11439Stracker-user        $base = ['do' => 'admin', 'page' => 'usersettings'];
605*54f11439Stracker-user        return wl($ID, array_merge($base, $params), false, '&amp;');
606*54f11439Stracker-user    }
607*54f11439Stracker-user
608*54f11439Stracker-user    /**
609*54f11439Stracker-user     * Render one sortable column header. Clicking the active column flips the
610*54f11439Stracker-user     * direction; the drop-down filter and text filters are preserved and the
611*54f11439Stracker-user     * page resets to 1.
6121ab40613Stracker-user     *
6131ab40613Stracker-user     * @param string $label
6141ab40613Stracker-user     * @param string $col       sort key for this column
6151ab40613Stracker-user     * @param string $sort      currently active sort key
6161ab40613Stracker-user     * @param string $dir       currently active direction
617*54f11439Stracker-user     * @param string $setFilter currently selected setting key
618*54f11439Stracker-user     * @param array  $qfilters  active text filters
6191ab40613Stracker-user     * @return string
6201ab40613Stracker-user     */
621*54f11439Stracker-user    protected function sortHeader($label, $col, $sort, $dir, $setFilter, array $qfilters)
6221ab40613Stracker-user    {
6231ab40613Stracker-user        // clicking the active column flips direction; others start ascending
6241ab40613Stracker-user        $newDir = ($sort === $col && $dir === 'asc') ? 'desc' : 'asc';
6251ab40613Stracker-user        $arrow  = '';
6261ab40613Stracker-user        if ($sort === $col) {
6271ab40613Stracker-user            $arrow = ($dir === 'asc') ? " \u{25B2}" : " \u{25BC}";
6281ab40613Stracker-user        }
6291ab40613Stracker-user
630*54f11439Stracker-user        $url = $this->tableURL(array_merge(
631*54f11439Stracker-user            $this->standingParams($sort, $dir, $setFilter, $qfilters),
632*54f11439Stracker-user            ['sort' => $col, 'dir' => $newDir]
633*54f11439Stracker-user        ));
6341ab40613Stracker-user        return '<th><a href="' . $url . '">' . hsc($label) . $arrow . '</a></th>';
6351ab40613Stracker-user    }
6361ab40613Stracker-user
637*54f11439Stracker-user    /**
638*54f11439Stracker-user     * The per-column filter row: a text input under each text-filterable
639*54f11439Stracker-user     * column, the existing setting drop-down under "Setting", and the
640*54f11439Stracker-user     * Search/Clear controls under the (non-filterable) "Changed at" column.
641*54f11439Stracker-user     *
642*54f11439Stracker-user     * @param string[] $cols      visible columns in order
643*54f11439Stracker-user     * @param array    $filterMap col => row field for text-filterable columns
644*54f11439Stracker-user     * @param array    $qfilters  active text filters
645*54f11439Stracker-user     * @param array    $toggles   registered toggles (for the drop-down)
646*54f11439Stracker-user     * @param string   $setFilter currently selected setting key
647*54f11439Stracker-user     * @param string   $sort
648*54f11439Stracker-user     * @param string   $dir
649*54f11439Stracker-user     * @return string
650*54f11439Stracker-user     */
651*54f11439Stracker-user    protected function renderFilterRow(array $cols, array $filterMap, array $qfilters, array $toggles, $setFilter, $sort, $dir)
652*54f11439Stracker-user    {
653*54f11439Stracker-user        $html = '<tr class="us-filterrow">';
654*54f11439Stracker-user        foreach ($cols as $c) {
655*54f11439Stracker-user            if ($c === 'setting') {
656*54f11439Stracker-user                $html .= '<td><select name="filter" title="' . hsc($this->getLang('filter_label')) . '">';
657*54f11439Stracker-user                $html .= '<option value="">' . hsc($this->getLang('filter_all')) . '</option>';
658*54f11439Stracker-user                foreach ($toggles as $key => $def) {
659*54f11439Stracker-user                    $selected = ($setFilter === $key) ? ' selected="selected"' : '';
660*54f11439Stracker-user                    $html .= '<option value="' . hsc($key) . '"' . $selected . '>'
661*54f11439Stracker-user                           . hsc($def['label']) . '</option>';
662*54f11439Stracker-user                }
663*54f11439Stracker-user                $html .= '</select></td>';
664*54f11439Stracker-user            } elseif ($c === 'changedat') {
665*54f11439Stracker-user                $html .= '<td class="us-filteractions">';
666*54f11439Stracker-user                $html .= '<button type="submit" class="button">'
667*54f11439Stracker-user                       . hsc($this->getLang('filter_search')) . '</button>';
668*54f11439Stracker-user                if ($setFilter !== '' || $qfilters !== []) {
669*54f11439Stracker-user                    $clear = $this->tableURL(['sort' => $sort, 'dir' => $dir]);
670*54f11439Stracker-user                    $html .= ' <a class="us-clear" href="' . $clear . '">'
671*54f11439Stracker-user                           . hsc($this->getLang('filter_clear')) . '</a>';
672*54f11439Stracker-user                }
673*54f11439Stracker-user                $html .= '</td>';
674*54f11439Stracker-user            } elseif (isset($filterMap[$c])) {
675*54f11439Stracker-user                $val = isset($qfilters[$c]) ? hsc($qfilters[$c]) : '';
676*54f11439Stracker-user                $html .= '<td><input type="text" name="q[' . hsc($c) . ']" class="edit" value="'
677*54f11439Stracker-user                       . $val . '" /></td>';
678*54f11439Stracker-user            } else {
679*54f11439Stracker-user                $html .= '<td></td>';
680*54f11439Stracker-user            }
681*54f11439Stracker-user        }
682*54f11439Stracker-user        return $html . '</tr>';
683*54f11439Stracker-user    }
684*54f11439Stracker-user
685*54f11439Stracker-user    /**
686*54f11439Stracker-user     * Render the numbered pager: « prev  1 … 4 [5] 6 … 20  next ». Returns the
687*54f11439Stracker-user     * empty string when there is only one page.
688*54f11439Stracker-user     *
689*54f11439Stracker-user     * @param int    $page
690*54f11439Stracker-user     * @param int    $totalPages
691*54f11439Stracker-user     * @param string $sort
692*54f11439Stracker-user     * @param string $dir
693*54f11439Stracker-user     * @param string $setFilter
694*54f11439Stracker-user     * @param array  $qfilters
695*54f11439Stracker-user     * @return string
696*54f11439Stracker-user     */
697*54f11439Stracker-user    protected function renderPager($page, $totalPages, $sort, $dir, $setFilter, array $qfilters)
698*54f11439Stracker-user    {
699*54f11439Stracker-user        if ($totalPages <= 1) {
700*54f11439Stracker-user            return '';
701*54f11439Stracker-user        }
702*54f11439Stracker-user
703*54f11439Stracker-user        $html = '<nav class="us-pager" aria-label="' . hsc($this->getLang('pager_label')) . '">';
704*54f11439Stracker-user
705*54f11439Stracker-user        if ($page > 1) {
706*54f11439Stracker-user            $html .= $this->pagerLink($page - 1, $sort, $dir, $setFilter, $qfilters, '&#8249;', 'pager_prev');
707*54f11439Stracker-user        } else {
708*54f11439Stracker-user            $html .= '<span class="pager_btn pager_disabled">&#8249;</span>';
709*54f11439Stracker-user        }
710*54f11439Stracker-user
711*54f11439Stracker-user        foreach ($this->pageWindow($page, $totalPages) as $p) {
712*54f11439Stracker-user            if ($p === 0) {
713*54f11439Stracker-user                $html .= '<span class="pager_gap">&#8230;</span>';
714*54f11439Stracker-user            } elseif ($p === $page) {
715*54f11439Stracker-user                $html .= '<span class="pager_cur">' . $p . '</span>';
716*54f11439Stracker-user            } else {
717*54f11439Stracker-user                $html .= $this->pagerLink($p, $sort, $dir, $setFilter, $qfilters, (string) $p, '');
718*54f11439Stracker-user            }
719*54f11439Stracker-user        }
720*54f11439Stracker-user
721*54f11439Stracker-user        if ($page < $totalPages) {
722*54f11439Stracker-user            $html .= $this->pagerLink($page + 1, $sort, $dir, $setFilter, $qfilters, '&#8250;', 'pager_next');
723*54f11439Stracker-user        } else {
724*54f11439Stracker-user            $html .= '<span class="pager_btn pager_disabled">&#8250;</span>';
725*54f11439Stracker-user        }
726*54f11439Stracker-user
727*54f11439Stracker-user        return $html . '</nav>';
728*54f11439Stracker-user    }
729*54f11439Stracker-user
730*54f11439Stracker-user    /**
731*54f11439Stracker-user     * One pager link (number or arrow), preserving sort + filters.
732*54f11439Stracker-user     *
733*54f11439Stracker-user     * @param int    $p        target page
734*54f11439Stracker-user     * @param string $sort
735*54f11439Stracker-user     * @param string $dir
736*54f11439Stracker-user     * @param string $setFilter
737*54f11439Stracker-user     * @param array  $qfilters
738*54f11439Stracker-user     * @param string $text     already-safe link text (number or entity)
739*54f11439Stracker-user     * @param string $titleKey lang key for the title attribute, or '' for none
740*54f11439Stracker-user     * @return string
741*54f11439Stracker-user     */
742*54f11439Stracker-user    protected function pagerLink($p, $sort, $dir, $setFilter, array $qfilters, $text, $titleKey)
743*54f11439Stracker-user    {
744*54f11439Stracker-user        $url = $this->tableURL(array_merge(
745*54f11439Stracker-user            $this->standingParams($sort, $dir, $setFilter, $qfilters),
746*54f11439Stracker-user            ['pg' => $p]
747*54f11439Stracker-user        ));
748*54f11439Stracker-user        $title = ($titleKey !== '') ? ' title="' . hsc($this->getLang($titleKey)) . '"' : '';
749*54f11439Stracker-user        return '<a class="pager_btn" href="' . $url . '"' . $title . '>' . $text . '</a>';
750*54f11439Stracker-user    }
751*54f11439Stracker-user
752*54f11439Stracker-user    /**
753*54f11439Stracker-user     * Page numbers around the current page, 0 marking an elided gap; always
754*54f11439Stracker-user     * includes the first and last page.
755*54f11439Stracker-user     *
756*54f11439Stracker-user     * @param int $page
757*54f11439Stracker-user     * @param int $totalPages
758*54f11439Stracker-user     * @return int[]
759*54f11439Stracker-user     */
760*54f11439Stracker-user    protected function pageWindow($page, $totalPages)
761*54f11439Stracker-user    {
762*54f11439Stracker-user        $window = 2;
763*54f11439Stracker-user        $keep   = [];
764*54f11439Stracker-user        for ($i = 1; $i <= $totalPages; $i++) {
765*54f11439Stracker-user            if ($i === 1 || $i === $totalPages || ($i >= $page - $window && $i <= $page + $window)) {
766*54f11439Stracker-user                $keep[] = $i;
767*54f11439Stracker-user            }
768*54f11439Stracker-user        }
769*54f11439Stracker-user
770*54f11439Stracker-user        $out  = [];
771*54f11439Stracker-user        $prev = 0;
772*54f11439Stracker-user        foreach ($keep as $p) {
773*54f11439Stracker-user            if ($prev && ($p - $prev) > 1) {
774*54f11439Stracker-user                $out[] = 0; // gap marker
775*54f11439Stracker-user            }
776*54f11439Stracker-user            $out[] = $p;
777*54f11439Stracker-user            $prev  = $p;
778*54f11439Stracker-user        }
779*54f11439Stracker-user        return $out;
780*54f11439Stracker-user    }
781*54f11439Stracker-user
7821ab40613Stracker-user    // ---- per-user edit form ---------------------------------------------
7831ab40613Stracker-user
7841ab40613Stracker-user    /**
7851ab40613Stracker-user     * Render the edit form for one user's preferences.
7861ab40613Stracker-user     *
7871ab40613Stracker-user     * @param string $user
7881ab40613Stracker-user     * @return string
7891ab40613Stracker-user     */
7901ab40613Stracker-user    protected function renderEditForm($user)
7911ab40613Stracker-user    {
7921ab40613Stracker-user        global $auth, $ID;
7931ab40613Stracker-user
7948f16c88bStracker-user        $html = '<div class="plugin_usersettings_admin plugin_usersettings">';
7951ab40613Stracker-user
7961ab40613Stracker-user        $userData = ($auth !== null) ? $auth->getUserData($user) : false;
7971ab40613Stracker-user        if ($userData === false) {
7981ab40613Stracker-user            $html .= '<h1>' . hsc($this->getLang('admin_heading')) . '</h1>';
7991ab40613Stracker-user            $html .= '<p>' . hsc($this->getLang('badidentuser')) . '</p>';
8001ab40613Stracker-user            $html .= '<p><a href="' . $this->pageURL() . '">'
8011ab40613Stracker-user                   . hsc($this->getLang('edit_back')) . '</a></p>';
8021ab40613Stracker-user            return $html . '</div>';
8031ab40613Stracker-user        }
8041ab40613Stracker-user
80549b74e0aStracker-user        $displayName = (($userData['name'] ?? '') !== '') ? $userData['name'] : $user;
8061ab40613Stracker-user        $html .= '<h1>' . hsc(sprintf($this->getLang('edit_heading'), $displayName)) . '</h1>';
8071ab40613Stracker-user
8081ab40613Stracker-user        $helper  = $this->getHelper();
8091ab40613Stracker-user        $action  = $this->getActionPlugin();
8101ab40613Stracker-user        $toggles = $helper ? $helper->getRegisteredToggles() : [];
8111ab40613Stracker-user
8121ab40613Stracker-user        if (empty($toggles) || $action === null) {
8131ab40613Stracker-user            $html .= '<p>' . hsc($this->getLang('notoggles')) . '</p>';
8141ab40613Stracker-user            $html .= '<p><a href="' . $this->pageURL() . '">'
8151ab40613Stracker-user                   . hsc($this->getLang('edit_back')) . '</a></p>';
8161ab40613Stracker-user            return $html . '</div>';
8171ab40613Stracker-user        }
8181ab40613Stracker-user
8191ab40613Stracker-user        $formAction = wl($ID, ['do' => 'admin', 'page' => 'usersettings'], false, '&amp;');
8201ab40613Stracker-user        $html .= '<form method="post" action="' . $formAction . '" class="us-form">';
8211ab40613Stracker-user        $html .= formSecurityToken(false);
8221ab40613Stracker-user        $html .= '<input type="hidden" name="edituser" value="' . hsc($user) . '" />';
8231ab40613Stracker-user        $html .= '<input type="hidden" name="usersettings_adminsave" value="1" />';
8241ab40613Stracker-user
8251ab40613Stracker-user        foreach ($toggles as $key => $def) {
8261ab40613Stracker-user            $html .= $action->renderToggleRow($def, $helper->getPreference($key, $user));
8271ab40613Stracker-user        }
8281ab40613Stracker-user
8291ab40613Stracker-user        $html .= '<div class="us-actions">';
8301ab40613Stracker-user        $html .= '<button type="submit" class="button">'
8311ab40613Stracker-user               . hsc($this->getLang('save')) . '</button> ';
8321ab40613Stracker-user        $html .= '<a href="' . $this->pageURL() . '" class="us-back">'
8331ab40613Stracker-user               . hsc($this->getLang('edit_back')) . '</a>';
8341ab40613Stracker-user        $html .= '</div>';
8351ab40613Stracker-user        $html .= '</form>';
8361ab40613Stracker-user
8371ab40613Stracker-user        return $html . '</div>';
8381ab40613Stracker-user    }
8391ab40613Stracker-user
8401ab40613Stracker-user    // ---- helpers ---------------------------------------------------------
8411ab40613Stracker-user
8421ab40613Stracker-user    /**
8431ab40613Stracker-user     * Build a URL back to this admin page with the given extra parameters.
8441ab40613Stracker-user     *
8451ab40613Stracker-user     * @param array $params
8461ab40613Stracker-user     * @return string  HTML-attribute-safe URL
8471ab40613Stracker-user     */
8481ab40613Stracker-user    protected function pageURL(array $params = [])
8491ab40613Stracker-user    {
8501ab40613Stracker-user        global $ID;
8511ab40613Stracker-user        $base = ['do' => 'admin', 'page' => 'usersettings'];
8521ab40613Stracker-user        return wl($ID, array_merge($base, $params), false, '&amp;');
8531ab40613Stracker-user    }
8541ab40613Stracker-user}
855