row field used for that sort */ protected $sortFields = [ 'login' => 'user', 'name' => 'display_name', 'mail' => 'mail', 'grps' => 'grps', 'setting' => 'setting_label', 'value' => 'value_display', 'changedby' => 'changed_by_display', 'changedat' => 'changed_at', ]; // --------------------------------------------------------------------- // Admin plugin metadata // --------------------------------------------------------------------- /** Only administrators may see other users' preferences. */ public function forAdminOnly() { return true; } /** Position in the admin menu. */ public function getMenuSort() { return 1000; } /** Admin menu label — distinct from the user menu's "Preferences". */ public function getMenuText($language) { return $this->getLang('admin_menu'); } // --------------------------------------------------------------------- // Component access // --------------------------------------------------------------------- /** @return helper_plugin_usersettings|null */ protected function getHelper() { return plugin_load('helper', 'usersettings'); } /** @return action_plugin_usersettings|null */ protected function getActionPlugin() { return plugin_load('action', 'usersettings'); } // --------------------------------------------------------------------- // Request handling // --------------------------------------------------------------------- /** * Handle a submitted per-user edit form. * * Runs only for admins (DokuWiki's admin dispatcher enforces * forAdminOnly() before this is called). Uses Post/Redirect/Get. */ public function handle() { global $INPUT, $ID; if (!$INPUT->post->bool('usersettings_adminsave')) { return; } if (!checkSecurityToken()) { return; } $this->processAdminSave(); // Post/Redirect/Get back to the overview send_redirect(wl($ID, ['do' => 'admin', 'page' => 'usersettings'], true, '&')); } /** * Validate the target user and store the submitted preferences for them, * recording the acting admin as the actor. Kept redirect-free so it can * be tested directly. * * @return bool */ public function processAdminSave() { global $INPUT, $auth; $target = $INPUT->post->str('edituser'); $admin = $INPUT->server->str('REMOTE_USER'); $userData = ($auth !== null) ? $auth->getUserData($target) : false; if ($userData === false) { msg($this->getLang('badidentuser'), -1); return false; } $action = $this->getActionPlugin(); $ok = ($action !== null) && $action->saveSubmittedPreferences($target, $admin); $name = (($userData['name'] ?? '') !== '') ? $userData['name'] : $target; msg( sprintf($this->getLang($ok ? 'adminsaved' : 'adminsavefail'), hsc($name)), $ok ? 1 : -1 ); return $ok; } // --------------------------------------------------------------------- // Output // --------------------------------------------------------------------- /** * Render either the overview table or, when an edituser parameter is * present, the per-user edit form. */ public function html() { global $INPUT; $edituser = $INPUT->get->str('edituser'); if ($edituser !== '') { echo $this->renderEditForm($edituser); } else { echo $this->renderTable(); } } // ---- overview table -------------------------------------------------- /** * Build the rows of the overview: one per (user x toggle). * * @param array $users [username => userdata] as from $auth->retrieveUsers() * @param array $toggles registered toggle definitions * @return array list of row arrays */ public function buildRows(array $users, array $toggles) { $helper = $this->getHelper(); if ($helper === null) { return []; } $rows = []; foreach ($users as $username => $userData) { $displayName = (!empty($userData['name'])) ? $userData['name'] : $username; $mail = $userData['mail'] ?? ''; $grps = isset($userData['grps']) ? implode(', ', (array) $userData['grps']) : ''; $stored = $helper->loadUserData($username); foreach ($toggles as $key => $def) { if (isset($stored[$key]) && array_key_exists('value', $stored[$key])) { $value = $stored[$key]['value']; $changedBy = $stored[$key]['changed_by'] ?? ''; $changedAt = (int) ($stored[$key]['changed_at'] ?? 0); $isDefault = false; } else { $value = $def['default']; $changedBy = ''; $changedAt = 0; $isDefault = true; } $rows[] = [ 'user' => $username, 'display_name' => $displayName, 'mail' => $mail, 'grps' => $grps, 'setting_key' => $key, 'setting_label' => $def['label'], 'value_display' => $this->displayValue($def, $value), 'is_default' => $isDefault, 'changed_by_display' => $isDefault ? '' : $this->resolveActor($changedBy, $users), 'changed_at' => $changedAt, ]; } } return $rows; } /** * Sort overview rows by the given column and direction. * * @param array $rows * @param string $sort one of the keys of $this->sortFields * @param string $dir 'asc' or 'desc' * @return array */ public function sortRows(array $rows, $sort, $dir) { $field = $this->sortFields[$sort] ?? 'display_name'; usort($rows, function ($a, $b) use ($field) { if ($field === 'changed_at') { return $a[$field] <=> $b[$field]; } return strcasecmp((string) $a[$field], (string) $b[$field]); }); if ($dir === 'desc') { $rows = array_reverse($rows); } return $rows; } /** * Human-readable value of a toggle: On/Off for a checkbox, the option * label for a select. * * @param array $def * @param mixed $value * @return string */ public function displayValue(array $def, $value) { if ($def['type'] === 'select') { if (isset($def['options'][$value])) { return (string) $def['options'][$value]; } return (string) $value; // stored value no longer a defined option } return $this->getLang(empty($value) ? 'val_off' : 'val_on'); } /** * Resolve an actor username to a display name, falling back to the raw * username when the actor is not (or no longer) a known user. * * @param string $actor * @param array $users [username => userdata] * @return string */ protected function resolveActor($actor, array $users) { if ($actor === '') { return ''; } if (isset($users[$actor]) && !empty($users[$actor]['name'])) { return $users[$actor]['name']; } return $actor; } /** * Render the overview table: sortable headers, a per-column text-filter * row (plus the existing setting drop-down), the rows for the current * page, and a numbered pager. * * @return string */ protected function renderTable() { global $INPUT, $auth, $ID; $helper = $this->getHelper(); $toggles = $helper ? $helper->getRegisteredToggles() : []; $html = '
' . hsc($this->getLang('notoggles')) . '
' . hsc($this->getLang('nousers')) . '
'; } $showMail = (bool) $this->getConf('show_mail'); $showGrps = (bool) $this->getConf('show_grps'); $perPage = (int) $this->getConf('entries_per_page'); // visible columns, in display order $cols = ['login', 'name']; if ($showMail) { $cols[] = 'mail'; } if ($showGrps) { $cols[] = 'grps'; } $cols = array_merge($cols, ['setting', 'value', 'changedby', 'changedat']); // col => row field for the text-filterable columns ("Setting" has its // own drop-down; "Changed at" is not filterable) $filterMap = $this->filterFieldMap($cols); // request parameters (sort links and the filter form are GET) $sort = $INPUT->get->str('sort', 'name'); if (!isset($this->sortFields[$sort])) { $sort = 'name'; } if (($sort === 'mail' && !$showMail) || ($sort === 'grps' && !$showGrps)) { $sort = 'name'; } $dir = ($INPUT->get->str('dir') === 'desc') ? 'desc' : 'asc'; $setFilter = $INPUT->get->str('filter'); if ($setFilter !== '' && !isset($toggles[$setFilter])) { $setFilter = ''; } $qfilters = $this->activeFilters(array_keys($filterMap)); $html .= '' . hsc($this->getLang('admin_intro')) . '
'; // build rows, narrow by the setting drop-down, then the text filters, // then sort — all before paging so the counts and page numbers match $rows = $this->buildRows($users, $toggles); if ($setFilter !== '') { $rows = array_values(array_filter($rows, static function ($r) use ($setFilter) { return $r['setting_key'] === $setFilter; })); } $rows = $this->applyFilters($rows, $qfilters, $filterMap); $rows = $this->sortRows($rows, $sort, $dir); $total = count($rows); [$pageRows, $page, $totalPages, $from, $to] = $this->paginate($rows, $perPage); $labels = [ 'login' => $this->getLang('th_login'), 'name' => $this->getLang('th_name'), 'mail' => $this->getLang('th_mail'), 'grps' => $this->getLang('th_grps'), 'setting' => $this->getLang('th_setting'), 'value' => $this->getLang('th_value'), 'changedby' => $this->getLang('th_changedby'), 'changedat' => $this->getLang('th_changedat'), ]; // GET form so the filters combine with the sort links and bookmark // cleanly; the action URL's query string is dropped on submit, so the // standing parameters travel as hidden fields. $html .= ''; $html .= $this->renderPager($page, $totalPages, $sort, $dir, $setFilter, $qfilters); if ($total > 0) { $html .= '' . hsc(sprintf($this->getLang('shown'), $from, $to, $total)) . '
'; } return $html . ''; } /** * Render one body cell for a column. * * @param string $c column key * @param array $row * @return string */ protected function bodyCell($c, array $row) { switch ($c) { case 'login': return '' . hsc($this->getLang('badidentuser')) . '
'; $html .= ''; return $html . '' . hsc($this->getLang('notoggles')) . '
'; $html .= ''; return $html . ''; } $formAction = wl($ID, ['do' => 'admin', 'page' => 'usersettings'], false, '&'); $html .= ''; return $html . ''; } // ---- helpers --------------------------------------------------------- /** * Build a URL back to this admin page with the given extra parameters. * * @param array $params * @return string HTML-attribute-safe URL */ protected function pageURL(array $params = []) { global $ID; $base = ['do' => 'admin', 'page' => 'usersettings']; return wl($ID, array_merge($base, $params), false, '&'); } }