1*1ab40613Stracker-user<?php 2*1ab40613Stracker-user 3*1ab40613Stracker-user/** 4*1ab40613Stracker-user * User Settings plugin — action component. 5*1ab40613Stracker-user * 6*1ab40613Stracker-user * Provides three things: 7*1ab40613Stracker-user * 8*1ab40613Stracker-user * 1. A "Preferences" item in the user menu, placed just before "Update 9*1ab40613Stracker-user * Profile" (via the MENU_ITEMS_ASSEMBLY event — template-independent). 10*1ab40613Stracker-user * 11*1ab40613Stracker-user * 2. A custom action, do=usersettings, claimed in ACTION_ACT_PREPROCESS and 12*1ab40613Stracker-user * rendered in TPL_ACT_UNKNOWN. This is the documented way for a plugin to 13*1ab40613Stracker-user * own a do= value: preventing the preprocess default makes DokuWiki route 14*1ab40613Stracker-user * the action through dokuwiki\Action\Plugin, which fires TPL_ACT_UNKNOWN. 15*1ab40613Stracker-user * 16*1ab40613Stracker-user * 3. The settings page itself: a plain HTML form of every registered toggle, 17*1ab40613Stracker-user * with Post/Redirect/Get handling that saves through the helper. 18*1ab40613Stracker-user */ 19*1ab40613Stracker-user 20*1ab40613Stracker-user// must be run within DokuWiki 21*1ab40613Stracker-userif (!defined('DOKU_INC')) die(); 22*1ab40613Stracker-user 23*1ab40613Stracker-userclass action_plugin_usersettings extends DokuWiki_Action_Plugin 24*1ab40613Stracker-user{ 25*1ab40613Stracker-user /** the do= value this plugin owns */ 26*1ab40613Stracker-user const ACTION = 'usersettings'; 27*1ab40613Stracker-user 28*1ab40613Stracker-user /** 29*1ab40613Stracker-user * Register event handlers. 30*1ab40613Stracker-user */ 31*1ab40613Stracker-user public function register(Doku_Event_Handler $controller) 32*1ab40613Stracker-user { 33*1ab40613Stracker-user $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handleMenuAssembly'); 34*1ab40613Stracker-user $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreprocess'); 35*1ab40613Stracker-user $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleUnknown'); 36*1ab40613Stracker-user } 37*1ab40613Stracker-user 38*1ab40613Stracker-user /** 39*1ab40613Stracker-user * Load the storage/registration helper. 40*1ab40613Stracker-user * 41*1ab40613Stracker-user * @return helper_plugin_usersettings|null 42*1ab40613Stracker-user */ 43*1ab40613Stracker-user protected function getHelper() 44*1ab40613Stracker-user { 45*1ab40613Stracker-user /** @var helper_plugin_usersettings|null $helper */ 46*1ab40613Stracker-user $helper = plugin_load('helper', 'usersettings'); 47*1ab40613Stracker-user return $helper; 48*1ab40613Stracker-user } 49*1ab40613Stracker-user 50*1ab40613Stracker-user // --------------------------------------------------------------------- 51*1ab40613Stracker-user // 1. The user-menu item 52*1ab40613Stracker-user // --------------------------------------------------------------------- 53*1ab40613Stracker-user 54*1ab40613Stracker-user /** 55*1ab40613Stracker-user * Insert the "Preferences" item into the user menu, just before the 56*1ab40613Stracker-user * "Update Profile" item. 57*1ab40613Stracker-user * 58*1ab40613Stracker-user * @param Doku_Event $event MENU_ITEMS_ASSEMBLY 59*1ab40613Stracker-user * @param mixed $param 60*1ab40613Stracker-user */ 61*1ab40613Stracker-user public function handleMenuAssembly(Doku_Event $event, $param) 62*1ab40613Stracker-user { 63*1ab40613Stracker-user if (!is_array($event->data) || ($event->data['view'] ?? '') !== 'user') { 64*1ab40613Stracker-user return; 65*1ab40613Stracker-user } 66*1ab40613Stracker-user 67*1ab40613Stracker-user try { 68*1ab40613Stracker-user $item = new \dokuwiki\plugin\usersettings\MenuItem(); 69*1ab40613Stracker-user } catch (\RuntimeException $e) { 70*1ab40613Stracker-user // anonymous visitor, or the action is disabled — no menu item 71*1ab40613Stracker-user return; 72*1ab40613Stracker-user } 73*1ab40613Stracker-user 74*1ab40613Stracker-user if (!isset($event->data['items']) || !is_array($event->data['items'])) { 75*1ab40613Stracker-user return; 76*1ab40613Stracker-user } 77*1ab40613Stracker-user $items =& $event->data['items']; 78*1ab40613Stracker-user 79*1ab40613Stracker-user // find the Profile item; default to appending if it is not present 80*1ab40613Stracker-user $pos = count($items); 81*1ab40613Stracker-user foreach ($items as $i => $existing) { 82*1ab40613Stracker-user if ($existing instanceof \dokuwiki\Menu\Item\Profile) { 83*1ab40613Stracker-user $pos = $i; 84*1ab40613Stracker-user break; 85*1ab40613Stracker-user } 86*1ab40613Stracker-user } 87*1ab40613Stracker-user array_splice($items, $pos, 0, [$item]); 88*1ab40613Stracker-user } 89*1ab40613Stracker-user 90*1ab40613Stracker-user // --------------------------------------------------------------------- 91*1ab40613Stracker-user // 2. Claiming the custom action + handling the save 92*1ab40613Stracker-user // --------------------------------------------------------------------- 93*1ab40613Stracker-user 94*1ab40613Stracker-user /** 95*1ab40613Stracker-user * Claim do=usersettings and, on a form submission, save and redirect. 96*1ab40613Stracker-user * 97*1ab40613Stracker-user * @param Doku_Event $event ACTION_ACT_PREPROCESS 98*1ab40613Stracker-user * @param mixed $param 99*1ab40613Stracker-user */ 100*1ab40613Stracker-user public function handlePreprocess(Doku_Event $event, $param) 101*1ab40613Stracker-user { 102*1ab40613Stracker-user if ($event->data !== self::ACTION) { 103*1ab40613Stracker-user return; 104*1ab40613Stracker-user } 105*1ab40613Stracker-user 106*1ab40613Stracker-user // Preventing the default makes DokuWiki keep the action and route it 107*1ab40613Stracker-user // through dokuwiki\Action\Plugin, which will fire TPL_ACT_UNKNOWN. 108*1ab40613Stracker-user $event->preventDefault(); 109*1ab40613Stracker-user $event->stopPropagation(); 110*1ab40613Stracker-user 111*1ab40613Stracker-user global $INPUT, $ID; 112*1ab40613Stracker-user 113*1ab40613Stracker-user $user = $INPUT->server->str('REMOTE_USER'); 114*1ab40613Stracker-user if ($user === '') { 115*1ab40613Stracker-user return; // anonymous — the rendered page shows a login notice 116*1ab40613Stracker-user } 117*1ab40613Stracker-user 118*1ab40613Stracker-user // not a save submission — nothing to do, the page will just render 119*1ab40613Stracker-user if (!$INPUT->post->bool('usersettings_save')) { 120*1ab40613Stracker-user return; 121*1ab40613Stracker-user } 122*1ab40613Stracker-user 123*1ab40613Stracker-user // CSRF protection; checkSecurityToken() shows its own error on failure 124*1ab40613Stracker-user if (!checkSecurityToken()) { 125*1ab40613Stracker-user return; 126*1ab40613Stracker-user } 127*1ab40613Stracker-user 128*1ab40613Stracker-user $ok = $this->saveSubmittedPreferences($user); 129*1ab40613Stracker-user msg($this->getLang($ok ? 'saved' : 'savefail'), $ok ? 1 : -1); 130*1ab40613Stracker-user 131*1ab40613Stracker-user // Post/Redirect/Get: a refresh must not re-submit the form 132*1ab40613Stracker-user send_redirect(wl($ID, ['do' => self::ACTION], true, '&')); 133*1ab40613Stracker-user } 134*1ab40613Stracker-user 135*1ab40613Stracker-user /** 136*1ab40613Stracker-user * Read the submitted toggle values for every registered toggle and store 137*1ab40613Stracker-user * them for the given user. 138*1ab40613Stracker-user * 139*1ab40613Stracker-user * Kept separate from handlePreprocess() so it carries no redirect and can 140*1ab40613Stracker-user * be exercised directly by tests. Checkboxes that are unchecked do not 141*1ab40613Stracker-user * appear in the POST data, so every registered toggle is read explicitly 142*1ab40613Stracker-user * rather than iterating whatever was submitted. 143*1ab40613Stracker-user * 144*1ab40613Stracker-user * @param string $user whose preferences are being written 145*1ab40613Stracker-user * @param string|null $actor who is making the change; defaults to $user 146*1ab40613Stracker-user * (the admin component passes the admin here) 147*1ab40613Stracker-user * @return bool 148*1ab40613Stracker-user */ 149*1ab40613Stracker-user public function saveSubmittedPreferences($user, $actor = null) 150*1ab40613Stracker-user { 151*1ab40613Stracker-user global $INPUT; 152*1ab40613Stracker-user 153*1ab40613Stracker-user if ($actor === null) { 154*1ab40613Stracker-user $actor = $user; 155*1ab40613Stracker-user } 156*1ab40613Stracker-user 157*1ab40613Stracker-user $helper = $this->getHelper(); 158*1ab40613Stracker-user if ($helper === null) { 159*1ab40613Stracker-user return false; 160*1ab40613Stracker-user } 161*1ab40613Stracker-user 162*1ab40613Stracker-user $values = []; 163*1ab40613Stracker-user foreach ($helper->getRegisteredToggles() as $key => $def) { 164*1ab40613Stracker-user if ($def['type'] === 'checkbox') { 165*1ab40613Stracker-user $values[$key] = $INPUT->post->bool($key) ? 1 : 0; 166*1ab40613Stracker-user } else { 167*1ab40613Stracker-user $values[$key] = $INPUT->post->str($key); 168*1ab40613Stracker-user } 169*1ab40613Stracker-user } 170*1ab40613Stracker-user 171*1ab40613Stracker-user return $helper->setPreferences($values, $user, $actor); 172*1ab40613Stracker-user } 173*1ab40613Stracker-user 174*1ab40613Stracker-user // --------------------------------------------------------------------- 175*1ab40613Stracker-user // 3. Rendering the settings page 176*1ab40613Stracker-user // --------------------------------------------------------------------- 177*1ab40613Stracker-user 178*1ab40613Stracker-user /** 179*1ab40613Stracker-user * Render the settings page for do=usersettings. 180*1ab40613Stracker-user * 181*1ab40613Stracker-user * @param Doku_Event $event TPL_ACT_UNKNOWN 182*1ab40613Stracker-user * @param mixed $param 183*1ab40613Stracker-user */ 184*1ab40613Stracker-user public function handleUnknown(Doku_Event $event, $param) 185*1ab40613Stracker-user { 186*1ab40613Stracker-user if ($event->data !== self::ACTION) { 187*1ab40613Stracker-user return; 188*1ab40613Stracker-user } 189*1ab40613Stracker-user $event->preventDefault(); 190*1ab40613Stracker-user $event->stopPropagation(); 191*1ab40613Stracker-user 192*1ab40613Stracker-user echo $this->renderSettingsPage(); 193*1ab40613Stracker-user } 194*1ab40613Stracker-user 195*1ab40613Stracker-user /** 196*1ab40613Stracker-user * Build the HTML of the settings page. 197*1ab40613Stracker-user * 198*1ab40613Stracker-user * @return string 199*1ab40613Stracker-user */ 200*1ab40613Stracker-user public function renderSettingsPage() 201*1ab40613Stracker-user { 202*1ab40613Stracker-user global $INPUT, $ID; 203*1ab40613Stracker-user 204*1ab40613Stracker-user $user = $INPUT->server->str('REMOTE_USER'); 205*1ab40613Stracker-user 206*1ab40613Stracker-user $html = '<div class="plugin_usersettings">'; 207*1ab40613Stracker-user $html .= '<h1>' . hsc($this->getLang('heading')) . '</h1>'; 208*1ab40613Stracker-user 209*1ab40613Stracker-user if ($user === '') { 210*1ab40613Stracker-user $html .= '<p>' . hsc($this->getLang('nologin')) . '</p>'; 211*1ab40613Stracker-user return $html . '</div>'; 212*1ab40613Stracker-user } 213*1ab40613Stracker-user 214*1ab40613Stracker-user $helper = $this->getHelper(); 215*1ab40613Stracker-user $toggles = $helper ? $helper->getRegisteredToggles() : []; 216*1ab40613Stracker-user 217*1ab40613Stracker-user if (empty($toggles)) { 218*1ab40613Stracker-user $html .= '<p>' . hsc($this->getLang('notoggles')) . '</p>'; 219*1ab40613Stracker-user return $html . '</div>'; 220*1ab40613Stracker-user } 221*1ab40613Stracker-user 222*1ab40613Stracker-user $html .= '<p class="us-intro">' . hsc($this->getLang('intro')) . '</p>'; 223*1ab40613Stracker-user 224*1ab40613Stracker-user $action = wl($ID, ['do' => self::ACTION], false, '&'); 225*1ab40613Stracker-user $html .= '<form method="post" action="' . $action . '" class="us-form">'; 226*1ab40613Stracker-user $html .= formSecurityToken(false); 227*1ab40613Stracker-user 228*1ab40613Stracker-user foreach ($toggles as $key => $def) { 229*1ab40613Stracker-user $html .= $this->renderToggleRow($def, $helper->getPreference($key, $user)); 230*1ab40613Stracker-user } 231*1ab40613Stracker-user 232*1ab40613Stracker-user $html .= '<div class="us-actions">'; 233*1ab40613Stracker-user $html .= '<button type="submit" name="usersettings_save" value="1" class="button">' 234*1ab40613Stracker-user . hsc($this->getLang('save')) . '</button>'; 235*1ab40613Stracker-user $html .= '</div>'; 236*1ab40613Stracker-user $html .= '</form>'; 237*1ab40613Stracker-user 238*1ab40613Stracker-user return $html . '</div>'; 239*1ab40613Stracker-user } 240*1ab40613Stracker-user 241*1ab40613Stracker-user /** 242*1ab40613Stracker-user * Render one toggle as a form row. Public so the admin component can 243*1ab40613Stracker-user * reuse it for its per-user edit form. 244*1ab40613Stracker-user * 245*1ab40613Stracker-user * @param array $def a normalised toggle definition 246*1ab40613Stracker-user * @param mixed $value the user's effective value for this toggle 247*1ab40613Stracker-user * @return string 248*1ab40613Stracker-user */ 249*1ab40613Stracker-user public function renderToggleRow(array $def, $value) 250*1ab40613Stracker-user { 251*1ab40613Stracker-user $key = hsc($def['key']); 252*1ab40613Stracker-user 253*1ab40613Stracker-user if ($def['type'] === 'select') { 254*1ab40613Stracker-user $id = 'us__' . $key; 255*1ab40613Stracker-user $html = '<div class="us-row us-row-select">'; 256*1ab40613Stracker-user $html .= '<label class="us-label" for="' . $id . '">'; 257*1ab40613Stracker-user $html .= '<span class="us-name">' . hsc($def['label']) . '</span>'; 258*1ab40613Stracker-user $html .= '<select name="' . $key . '" id="' . $id . '">'; 259*1ab40613Stracker-user foreach ($def['options'] as $optValue => $optLabel) { 260*1ab40613Stracker-user $selected = ((string) $optValue === (string) $value) ? ' selected="selected"' : ''; 261*1ab40613Stracker-user $html .= '<option value="' . hsc((string) $optValue) . '"' . $selected . '>' 262*1ab40613Stracker-user . hsc((string) $optLabel) . '</option>'; 263*1ab40613Stracker-user } 264*1ab40613Stracker-user $html .= '</select>'; 265*1ab40613Stracker-user $html .= '</label>'; 266*1ab40613Stracker-user } else { 267*1ab40613Stracker-user $checked = empty($value) ? '' : ' checked="checked"'; 268*1ab40613Stracker-user $html = '<div class="us-row us-row-checkbox">'; 269*1ab40613Stracker-user $html .= '<label class="us-label">'; 270*1ab40613Stracker-user $html .= '<input type="checkbox" name="' . $key . '" value="1"' . $checked . ' />'; 271*1ab40613Stracker-user $html .= '<span class="us-name">' . hsc($def['label']) . '</span>'; 272*1ab40613Stracker-user $html .= '</label>'; 273*1ab40613Stracker-user } 274*1ab40613Stracker-user 275*1ab40613Stracker-user if ($def['desc'] !== '') { 276*1ab40613Stracker-user $html .= '<div class="us-desc">' . hsc($def['desc']) . '</div>'; 277*1ab40613Stracker-user } 278*1ab40613Stracker-user 279*1ab40613Stracker-user return $html . '</div>'; 280*1ab40613Stracker-user } 281*1ab40613Stracker-user} 282