11ab40613Stracker-user<?php 21ab40613Stracker-user 31ab40613Stracker-user/** 41ab40613Stracker-user * User Settings plugin — action component. 51ab40613Stracker-user * 61ab40613Stracker-user * Provides three things: 71ab40613Stracker-user * 81ab40613Stracker-user * 1. A "Preferences" item in the user menu, placed just before "Update 91ab40613Stracker-user * Profile" (via the MENU_ITEMS_ASSEMBLY event — template-independent). 101ab40613Stracker-user * 111ab40613Stracker-user * 2. A custom action, do=usersettings, claimed in ACTION_ACT_PREPROCESS and 121ab40613Stracker-user * rendered in TPL_ACT_UNKNOWN. This is the documented way for a plugin to 131ab40613Stracker-user * own a do= value: preventing the preprocess default makes DokuWiki route 141ab40613Stracker-user * the action through dokuwiki\Action\Plugin, which fires TPL_ACT_UNKNOWN. 151ab40613Stracker-user * 161ab40613Stracker-user * 3. The settings page itself: a plain HTML form of every registered toggle, 171ab40613Stracker-user * with Post/Redirect/Get handling that saves through the helper. 181ab40613Stracker-user */ 191ab40613Stracker-user 201ab40613Stracker-user// must be run within DokuWiki 211ab40613Stracker-userif (!defined('DOKU_INC')) die(); 221ab40613Stracker-user 2349b74e0aStracker-useruse dokuwiki\Extension\ActionPlugin; 2449b74e0aStracker-useruse dokuwiki\Extension\EventHandler; 2549b74e0aStracker-useruse dokuwiki\Extension\Event; 2649b74e0aStracker-user 2749b74e0aStracker-userclass action_plugin_usersettings extends ActionPlugin 281ab40613Stracker-user{ 291ab40613Stracker-user /** the do= value this plugin owns */ 301ab40613Stracker-user const ACTION = 'usersettings'; 311ab40613Stracker-user 321ab40613Stracker-user /** 331ab40613Stracker-user * Register event handlers. 341ab40613Stracker-user */ 3549b74e0aStracker-user public function register(EventHandler $controller) 361ab40613Stracker-user { 371ab40613Stracker-user $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handleMenuAssembly'); 381ab40613Stracker-user $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreprocess'); 391ab40613Stracker-user $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleUnknown'); 40cc98f4d1Stracker-user 41cc98f4d1Stracker-user // Register the built-in interface language toggle. 42cc98f4d1Stracker-user $controller->register_hook( 43cc98f4d1Stracker-user helper_plugin_usersettings::REGISTER_EVENT, 44cc98f4d1Stracker-user 'BEFORE', 45cc98f4d1Stracker-user $this, 46cc98f4d1Stracker-user 'registerLangToggle' 47cc98f4d1Stracker-user ); 48cc98f4d1Stracker-user 49cc98f4d1Stracker-user // Apply the user's language choice as early as possible so that all 50cc98f4d1Stracker-user // DokuWiki rendering — including TPL_ hooks further down the chain — 51cc98f4d1Stracker-user // uses the right language strings. ACTION_ACT_PREPROCESS fires before 52cc98f4d1Stracker-user // any output is produced and before template rendering begins. 53cc98f4d1Stracker-user $controller->register_hook( 54cc98f4d1Stracker-user 'ACTION_ACT_PREPROCESS', 55cc98f4d1Stracker-user 'BEFORE', 56cc98f4d1Stracker-user $this, 57cc98f4d1Stracker-user 'applyUserLang', 58cc98f4d1Stracker-user null, 59cc98f4d1Stracker-user // run at priority -10 so we fire before handlePreprocess (0) and 60cc98f4d1Stracker-user // before anything else that might read $conf['lang'] 61cc98f4d1Stracker-user -10 62cc98f4d1Stracker-user ); 63*26676c97Stracker-user 64*26676c97Stracker-user // lib/exe/js.php is its own request and never fires 65*26676c97Stracker-user // ACTION_ACT_PREPROCESS, so applyUserLang() cannot reach it. Without 66*26676c97Stracker-user // this hook the JavaScript language bundle (LANG, LANG.plugins.*) 67*26676c97Stracker-user // always ships in the SITE-default language, ignoring the user's 68*26676c97Stracker-user // choice. JS_SCRIPT_LIST is the one event js.php fires before it builds 69*26676c97Stracker-user // its cache key and loads JS strings. 70*26676c97Stracker-user $controller->register_hook( 71*26676c97Stracker-user 'JS_SCRIPT_LIST', 72*26676c97Stracker-user 'BEFORE', 73*26676c97Stracker-user $this, 74*26676c97Stracker-user 'applyUserLangToScripts' 75*26676c97Stracker-user ); 761ab40613Stracker-user } 771ab40613Stracker-user 781ab40613Stracker-user /** 791ab40613Stracker-user * Load the storage/registration helper. 801ab40613Stracker-user * 811ab40613Stracker-user * @return helper_plugin_usersettings|null 821ab40613Stracker-user */ 831ab40613Stracker-user protected function getHelper() 841ab40613Stracker-user { 851ab40613Stracker-user /** @var helper_plugin_usersettings|null $helper */ 861ab40613Stracker-user $helper = plugin_load('helper', 'usersettings'); 871ab40613Stracker-user return $helper; 881ab40613Stracker-user } 891ab40613Stracker-user 901ab40613Stracker-user // --------------------------------------------------------------------- 911ab40613Stracker-user // 1. The user-menu item 921ab40613Stracker-user // --------------------------------------------------------------------- 931ab40613Stracker-user 941ab40613Stracker-user /** 951ab40613Stracker-user * Insert the "Preferences" item into the user menu, just before the 961ab40613Stracker-user * "Update Profile" item. 971ab40613Stracker-user * 9849b74e0aStracker-user * @param Event $event MENU_ITEMS_ASSEMBLY 991ab40613Stracker-user * @param mixed $param 1001ab40613Stracker-user */ 10149b74e0aStracker-user public function handleMenuAssembly(Event $event, $param) 1021ab40613Stracker-user { 1031ab40613Stracker-user if (!is_array($event->data) || ($event->data['view'] ?? '') !== 'user') { 1041ab40613Stracker-user return; 1051ab40613Stracker-user } 1061ab40613Stracker-user 1071ab40613Stracker-user try { 1081ab40613Stracker-user $item = new \dokuwiki\plugin\usersettings\MenuItem(); 1091ab40613Stracker-user } catch (\RuntimeException $e) { 1101ab40613Stracker-user // anonymous visitor, or the action is disabled — no menu item 1111ab40613Stracker-user return; 1121ab40613Stracker-user } 1131ab40613Stracker-user 1141ab40613Stracker-user if (!isset($event->data['items']) || !is_array($event->data['items'])) { 1151ab40613Stracker-user return; 1161ab40613Stracker-user } 1171ab40613Stracker-user $items =& $event->data['items']; 1181ab40613Stracker-user 1191ab40613Stracker-user // find the Profile item; default to appending if it is not present 1201ab40613Stracker-user $pos = count($items); 1211ab40613Stracker-user foreach ($items as $i => $existing) { 1221ab40613Stracker-user if ($existing instanceof \dokuwiki\Menu\Item\Profile) { 1231ab40613Stracker-user $pos = $i; 1241ab40613Stracker-user break; 1251ab40613Stracker-user } 1261ab40613Stracker-user } 1271ab40613Stracker-user array_splice($items, $pos, 0, [$item]); 1281ab40613Stracker-user } 1291ab40613Stracker-user 1301ab40613Stracker-user // --------------------------------------------------------------------- 1311ab40613Stracker-user // 2. Claiming the custom action + handling the save 1321ab40613Stracker-user // --------------------------------------------------------------------- 1331ab40613Stracker-user 1341ab40613Stracker-user /** 1351ab40613Stracker-user * Claim do=usersettings and, on a form submission, save and redirect. 1361ab40613Stracker-user * 13749b74e0aStracker-user * @param Event $event ACTION_ACT_PREPROCESS 1381ab40613Stracker-user * @param mixed $param 1391ab40613Stracker-user */ 14049b74e0aStracker-user public function handlePreprocess(Event $event, $param) 1411ab40613Stracker-user { 1421ab40613Stracker-user if ($event->data !== self::ACTION) { 1431ab40613Stracker-user return; 1441ab40613Stracker-user } 1451ab40613Stracker-user 1461ab40613Stracker-user // Preventing the default makes DokuWiki keep the action and route it 1471ab40613Stracker-user // through dokuwiki\Action\Plugin, which will fire TPL_ACT_UNKNOWN. 1481ab40613Stracker-user $event->preventDefault(); 1491ab40613Stracker-user $event->stopPropagation(); 1501ab40613Stracker-user 1511ab40613Stracker-user global $INPUT, $ID; 1521ab40613Stracker-user 1531ab40613Stracker-user $user = $INPUT->server->str('REMOTE_USER'); 1541ab40613Stracker-user if ($user === '') { 1551ab40613Stracker-user return; // anonymous — the rendered page shows a login notice 1561ab40613Stracker-user } 1571ab40613Stracker-user 1581ab40613Stracker-user // not a save submission — nothing to do, the page will just render 1591ab40613Stracker-user if (!$INPUT->post->bool('usersettings_save')) { 1601ab40613Stracker-user return; 1611ab40613Stracker-user } 1621ab40613Stracker-user 1631ab40613Stracker-user // CSRF protection; checkSecurityToken() shows its own error on failure 1641ab40613Stracker-user if (!checkSecurityToken()) { 1651ab40613Stracker-user return; 1661ab40613Stracker-user } 1671ab40613Stracker-user 1681ab40613Stracker-user $ok = $this->saveSubmittedPreferences($user); 1691ab40613Stracker-user msg($this->getLang($ok ? 'saved' : 'savefail'), $ok ? 1 : -1); 1701ab40613Stracker-user 1711ab40613Stracker-user // Post/Redirect/Get: a refresh must not re-submit the form 1721ab40613Stracker-user send_redirect(wl($ID, ['do' => self::ACTION], true, '&')); 1731ab40613Stracker-user } 1741ab40613Stracker-user 1751ab40613Stracker-user /** 1761ab40613Stracker-user * Read the submitted toggle values for every registered toggle and store 1771ab40613Stracker-user * them for the given user. 1781ab40613Stracker-user * 1791ab40613Stracker-user * Kept separate from handlePreprocess() so it carries no redirect and can 1801ab40613Stracker-user * be exercised directly by tests. Checkboxes that are unchecked do not 1811ab40613Stracker-user * appear in the POST data, so every registered toggle is read explicitly 1821ab40613Stracker-user * rather than iterating whatever was submitted. 1831ab40613Stracker-user * 1841ab40613Stracker-user * @param string $user whose preferences are being written 1851ab40613Stracker-user * @param string|null $actor who is making the change; defaults to $user 1861ab40613Stracker-user * (the admin component passes the admin here) 1871ab40613Stracker-user * @return bool 1881ab40613Stracker-user */ 1891ab40613Stracker-user public function saveSubmittedPreferences($user, $actor = null) 1901ab40613Stracker-user { 1911ab40613Stracker-user global $INPUT; 1921ab40613Stracker-user 1931ab40613Stracker-user if ($actor === null) { 1941ab40613Stracker-user $actor = $user; 1951ab40613Stracker-user } 1961ab40613Stracker-user 1971ab40613Stracker-user $helper = $this->getHelper(); 1981ab40613Stracker-user if ($helper === null) { 1991ab40613Stracker-user return false; 2001ab40613Stracker-user } 2011ab40613Stracker-user 2021ab40613Stracker-user $values = []; 2031ab40613Stracker-user foreach ($helper->getRegisteredToggles() as $key => $def) { 2041ab40613Stracker-user if ($def['type'] === 'checkbox') { 2051ab40613Stracker-user $values[$key] = $INPUT->post->bool($key) ? 1 : 0; 2061ab40613Stracker-user } else { 2071ab40613Stracker-user $values[$key] = $INPUT->post->str($key); 2081ab40613Stracker-user } 2091ab40613Stracker-user } 2101ab40613Stracker-user 2111ab40613Stracker-user return $helper->setPreferences($values, $user, $actor); 2121ab40613Stracker-user } 2131ab40613Stracker-user 2141ab40613Stracker-user // --------------------------------------------------------------------- 2151ab40613Stracker-user // 3. Rendering the settings page 2161ab40613Stracker-user // --------------------------------------------------------------------- 2171ab40613Stracker-user 2181ab40613Stracker-user /** 2191ab40613Stracker-user * Render the settings page for do=usersettings. 2201ab40613Stracker-user * 22149b74e0aStracker-user * @param Event $event TPL_ACT_UNKNOWN 2221ab40613Stracker-user * @param mixed $param 2231ab40613Stracker-user */ 22449b74e0aStracker-user public function handleUnknown(Event $event, $param) 2251ab40613Stracker-user { 2261ab40613Stracker-user if ($event->data !== self::ACTION) { 2271ab40613Stracker-user return; 2281ab40613Stracker-user } 2291ab40613Stracker-user $event->preventDefault(); 2301ab40613Stracker-user $event->stopPropagation(); 2311ab40613Stracker-user 2321ab40613Stracker-user echo $this->renderSettingsPage(); 2331ab40613Stracker-user } 2341ab40613Stracker-user 2351ab40613Stracker-user /** 2361ab40613Stracker-user * Build the HTML of the settings page. 2371ab40613Stracker-user * 2381ab40613Stracker-user * @return string 2391ab40613Stracker-user */ 2401ab40613Stracker-user public function renderSettingsPage() 2411ab40613Stracker-user { 2421ab40613Stracker-user global $INPUT, $ID; 2431ab40613Stracker-user 2441ab40613Stracker-user $user = $INPUT->server->str('REMOTE_USER'); 2451ab40613Stracker-user 2461ab40613Stracker-user $html = '<div class="plugin_usersettings">'; 2471ab40613Stracker-user $html .= '<h1>' . hsc($this->getLang('heading')) . '</h1>'; 2481ab40613Stracker-user 2491ab40613Stracker-user if ($user === '') { 2501ab40613Stracker-user $html .= '<p>' . hsc($this->getLang('nologin')) . '</p>'; 2511ab40613Stracker-user return $html . '</div>'; 2521ab40613Stracker-user } 2531ab40613Stracker-user 2541ab40613Stracker-user $helper = $this->getHelper(); 2551ab40613Stracker-user $toggles = $helper ? $helper->getRegisteredToggles() : []; 2561ab40613Stracker-user 2571ab40613Stracker-user if (empty($toggles)) { 2581ab40613Stracker-user $html .= '<p>' . hsc($this->getLang('notoggles')) . '</p>'; 2591ab40613Stracker-user return $html . '</div>'; 2601ab40613Stracker-user } 2611ab40613Stracker-user 2621ab40613Stracker-user $html .= '<p class="us-intro">' . hsc($this->getLang('intro')) . '</p>'; 2631ab40613Stracker-user 2641ab40613Stracker-user $action = wl($ID, ['do' => self::ACTION], false, '&'); 2651ab40613Stracker-user $html .= '<form method="post" action="' . $action . '" class="us-form">'; 2661ab40613Stracker-user $html .= formSecurityToken(false); 2671ab40613Stracker-user 2681ab40613Stracker-user foreach ($toggles as $key => $def) { 2691ab40613Stracker-user $html .= $this->renderToggleRow($def, $helper->getPreference($key, $user)); 2701ab40613Stracker-user } 2711ab40613Stracker-user 2721ab40613Stracker-user $html .= '<div class="us-actions">'; 2731ab40613Stracker-user $html .= '<button type="submit" name="usersettings_save" value="1" class="button">' 2741ab40613Stracker-user . hsc($this->getLang('save')) . '</button>'; 2751ab40613Stracker-user $html .= '</div>'; 2761ab40613Stracker-user $html .= '</form>'; 2771ab40613Stracker-user 2781ab40613Stracker-user return $html . '</div>'; 2791ab40613Stracker-user } 2801ab40613Stracker-user 281cc98f4d1Stracker-user // --------------------------------------------------------------------- 282cc98f4d1Stracker-user // Built-in: interface language toggle 283cc98f4d1Stracker-user // --------------------------------------------------------------------- 284cc98f4d1Stracker-user 285cc98f4d1Stracker-user /** 286cc98f4d1Stracker-user * Contribute the "Interface language" select to the usersettings registry. 287cc98f4d1Stracker-user * 288cc98f4d1Stracker-user * The option list is built by scanning DOKU_INC/inc/lang/ for sub- 289cc98f4d1Stracker-user * directories that contain a lang.php file — the same source the 290cc98f4d1Stracker-user * Configuration Manager uses for its own language drop-down. The scan 291cc98f4d1Stracker-user * result is sorted alphabetically by language code; the site default is 292cc98f4d1Stracker-user * used as the toggle's default value so the toggle appears pre-selected 293cc98f4d1Stracker-user * correctly for users who have never changed it. 294cc98f4d1Stracker-user * 29549b74e0aStracker-user * @param Event $event PLUGIN_USERSETTINGS_REGISTER 296cc98f4d1Stracker-user * @param mixed $param 297cc98f4d1Stracker-user */ 29849b74e0aStracker-user public function registerLangToggle(Event $event, $param) 299cc98f4d1Stracker-user { 300cc98f4d1Stracker-user global $conf; 301cc98f4d1Stracker-user 302cc98f4d1Stracker-user $options = $this->getAvailableLanguages(); 303cc98f4d1Stracker-user if (empty($options)) { 304cc98f4d1Stracker-user return; // nothing to register if we cannot list languages 305cc98f4d1Stracker-user } 306cc98f4d1Stracker-user 307cc98f4d1Stracker-user $siteDefault = $conf['lang'] ?? 'en'; 308cc98f4d1Stracker-user if (!array_key_exists($siteDefault, $options)) { 309cc98f4d1Stracker-user $siteDefault = array_key_first($options); 310cc98f4d1Stracker-user } 311cc98f4d1Stracker-user 312cc98f4d1Stracker-user $event->data[] = [ 313cc98f4d1Stracker-user 'key' => 'lang', 314cc98f4d1Stracker-user 'label' => $this->getLang('lang_label'), 315cc98f4d1Stracker-user 'desc' => $this->getLang('lang_desc'), 316cc98f4d1Stracker-user 'type' => 'select', 317cc98f4d1Stracker-user 'options' => $options, 318cc98f4d1Stracker-user 'default' => $siteDefault, 319cc98f4d1Stracker-user 'plugin' => 'usersettings', 320cc98f4d1Stracker-user ]; 321cc98f4d1Stracker-user } 322cc98f4d1Stracker-user 323cc98f4d1Stracker-user /** 324cc98f4d1Stracker-user * Build the [code => display name] map of all installed DokuWiki interface 32549b74e0aStracker-user * languages by scanning inc/lang/. The display name is the language's own 32649b74e0aStracker-user * native name (endonym), falling back to the bare code for any language not 32749b74e0aStracker-user * in the built-in map. 328cc98f4d1Stracker-user * 32949b74e0aStracker-user * @return array [langCode => endonym] sorted by language code 330cc98f4d1Stracker-user */ 331cc98f4d1Stracker-user protected function getAvailableLanguages() 332cc98f4d1Stracker-user { 333cc98f4d1Stracker-user $pattern = DOKU_INC . 'inc/lang/*/lang.php'; 334cc98f4d1Stracker-user $files = glob($pattern); 335cc98f4d1Stracker-user if ($files === false || empty($files)) { 336cc98f4d1Stracker-user return []; 337cc98f4d1Stracker-user } 338cc98f4d1Stracker-user 339cc98f4d1Stracker-user $langs = []; 340cc98f4d1Stracker-user foreach ($files as $file) { 341cc98f4d1Stracker-user $code = basename(dirname($file)); // e.g. "de" from ".../inc/lang/de/lang.php" 342cc98f4d1Stracker-user if ($code === '' || $code === '.' || $code === '..') { 343cc98f4d1Stracker-user continue; 344cc98f4d1Stracker-user } 34549b74e0aStracker-user $langs[$code] = $this->languageName($code); 346cc98f4d1Stracker-user } 347cc98f4d1Stracker-user 348cc98f4d1Stracker-user ksort($langs, SORT_STRING); 349cc98f4d1Stracker-user return $langs; 350cc98f4d1Stracker-user } 351cc98f4d1Stracker-user 352cc98f4d1Stracker-user /** 35349b74e0aStracker-user * Return the native name (endonym) for a language code. 35449b74e0aStracker-user * Falls back to the bare code for languages not in the built-in map. 35549b74e0aStracker-user * 35649b74e0aStracker-user * @param string $code ISO language code as used by DokuWiki 35749b74e0aStracker-user * @return string 35849b74e0aStracker-user */ 35949b74e0aStracker-user protected function languageName($code) 36049b74e0aStracker-user { 36149b74e0aStracker-user $names = [ 36249b74e0aStracker-user 'af' => 'Afrikaans', 36349b74e0aStracker-user 'ar' => 'العربية', 36449b74e0aStracker-user 'az' => 'Azərbaycan', 36549b74e0aStracker-user 'be' => 'Беларуская', 36649b74e0aStracker-user 'bg' => 'Български', 36749b74e0aStracker-user 'bn' => 'বাংলা', 36849b74e0aStracker-user 'br' => 'Brezhoneg', 36949b74e0aStracker-user 'ca' => 'Català', 37049b74e0aStracker-user 'ca-valencia' => 'Català (Valencià)', 37149b74e0aStracker-user 'ckb' => 'کوردی سۆرانی', 37249b74e0aStracker-user 'cs' => 'Čeština', 37349b74e0aStracker-user 'cy' => 'Cymraeg', 37449b74e0aStracker-user 'da' => 'Dansk', 37549b74e0aStracker-user 'de' => 'Deutsch', 37649b74e0aStracker-user 'de-informal' => 'Deutsch (informell)', 37749b74e0aStracker-user 'el' => 'Ελληνικά', 37849b74e0aStracker-user 'en' => 'English', 37949b74e0aStracker-user 'eo' => 'Esperanto', 38049b74e0aStracker-user 'es' => 'Español', 38149b74e0aStracker-user 'et' => 'Eesti', 38249b74e0aStracker-user 'eu' => 'Euskara', 38349b74e0aStracker-user 'fa' => 'فارسی', 38449b74e0aStracker-user 'fi' => 'Suomi', 38549b74e0aStracker-user 'fo' => 'Føroyskt', 38649b74e0aStracker-user 'fr' => 'Français', 38749b74e0aStracker-user 'fy' => 'Frysk', 38849b74e0aStracker-user 'gl' => 'Galego', 38949b74e0aStracker-user 'he' => 'עברית', 39049b74e0aStracker-user 'hi' => 'हिन्दी', 39149b74e0aStracker-user 'hr' => 'Hrvatski', 39249b74e0aStracker-user 'hu' => 'Magyar', 39349b74e0aStracker-user 'hu-formal' => 'Magyar (magázó)', 39449b74e0aStracker-user 'hy' => 'Հայերեն', 39549b74e0aStracker-user 'ia' => 'Interlingua', 39649b74e0aStracker-user 'id' => 'Bahasa Indonesia', 39749b74e0aStracker-user 'id-ni' => 'Bahasa Indonesia (NTT)', 39849b74e0aStracker-user 'is' => 'Íslenska', 39949b74e0aStracker-user 'it' => 'Italiano', 40049b74e0aStracker-user 'ja' => '日本語', 40149b74e0aStracker-user 'ka' => 'ქართული', 40249b74e0aStracker-user 'kk' => 'Қазақша', 40349b74e0aStracker-user 'km' => 'ភាសាខ្មែរ', 40449b74e0aStracker-user 'kn' => 'ಕನ್ನಡ', 40549b74e0aStracker-user 'ko' => '한국어', 40649b74e0aStracker-user 'ku' => 'Kurdî', 40749b74e0aStracker-user 'la' => 'Latina', 40849b74e0aStracker-user 'lb' => 'Lëtzebuergesch', 40949b74e0aStracker-user 'lt' => 'Lietuvių', 41049b74e0aStracker-user 'lv' => 'Latviešu', 41149b74e0aStracker-user 'mg' => 'Malagasy', 41249b74e0aStracker-user 'mk' => 'Македонски', 41349b74e0aStracker-user 'ml' => 'മലയാളം', 41449b74e0aStracker-user 'mr' => 'मराठी', 41549b74e0aStracker-user 'ms' => 'Bahasa Melayu', 41649b74e0aStracker-user 'my' => 'မြန်မာ', 41749b74e0aStracker-user 'nan' => '閩南語', 41849b74e0aStracker-user 'nb' => 'Norsk bokmål', 41949b74e0aStracker-user 'ne' => 'नेपाली', 42049b74e0aStracker-user 'nl' => 'Nederlands', 42149b74e0aStracker-user 'nn' => 'Nynorsk', 42249b74e0aStracker-user 'no' => 'Norsk', 42349b74e0aStracker-user 'oc' => 'Occitan', 42449b74e0aStracker-user 'pl' => 'Polski', 42549b74e0aStracker-user 'pt' => 'Português', 42649b74e0aStracker-user 'pt-br' => 'Português brasileiro', 42749b74e0aStracker-user 'ro' => 'Română', 42849b74e0aStracker-user 'ru' => 'Русский', 42949b74e0aStracker-user 'si' => 'සිංහල', 43049b74e0aStracker-user 'sk' => 'Slovenčina', 43149b74e0aStracker-user 'sl' => 'Slovenščina', 43249b74e0aStracker-user 'sq' => 'Shqip', 43349b74e0aStracker-user 'sr' => 'Српски', 43449b74e0aStracker-user 'sv' => 'Svenska', 43549b74e0aStracker-user 'sw' => 'Kiswahili', 43649b74e0aStracker-user 'ta' => 'தமிழ்', 43749b74e0aStracker-user 'te' => 'తెలుగు', 43849b74e0aStracker-user 'th' => 'ภาษาไทย', 43949b74e0aStracker-user 'tr' => 'Türkçe', 44049b74e0aStracker-user 'uk' => 'Українська', 44149b74e0aStracker-user 'ur' => 'اردو', 44249b74e0aStracker-user 'uz' => 'Oʻzbekcha', 44349b74e0aStracker-user 'vi' => 'Tiếng Việt', 44449b74e0aStracker-user 'zh' => '中文 (简体)', 44549b74e0aStracker-user 'zh-tw' => '中文 (繁體)', 44649b74e0aStracker-user ]; 44749b74e0aStracker-user 44849b74e0aStracker-user return $names[$code] ?? $code; 44949b74e0aStracker-user } 45049b74e0aStracker-user 45149b74e0aStracker-user /** 452cc98f4d1Stracker-user * Apply the logged-in user's preferred interface language, overriding the 453cc98f4d1Stracker-user * site-wide $conf['lang'] before any rendering takes place. 454cc98f4d1Stracker-user * 455cc98f4d1Stracker-user * DokuWiki loads language strings lazily (via getLang() / $lang global 456cc98f4d1Stracker-user * reloads triggered by calls to init_lang()), so changing $conf['lang'] 457cc98f4d1Stracker-user * here — in the earliest ACTION_ACT_PREPROCESS handler — is sufficient 45849b74e0aStracker-user * to affect all subsequent output, including this plugin's own chrome. 45949b74e0aStracker-user * 46049b74e0aStracker-user * We read the stored record directly via getRecord() rather than routing 46149b74e0aStracker-user * through getPreference(). getPreference() fires PLUGIN_USERSETTINGS_REGISTER 46249b74e0aStracker-user * (and the full inc/lang/ glob) on every logged-in request before the 46349b74e0aStracker-user * language is known. Reading the raw record avoids that overhead and, 46449b74e0aStracker-user * crucially, means the toggle registry is built *after* $conf['lang'] has 46549b74e0aStracker-user * been updated — so toggle labels resolve in the user's chosen language. 466cc98f4d1Stracker-user * 467cc98f4d1Stracker-user * No-op for anonymous visitors or when the user has not chosen a language 468cc98f4d1Stracker-user * that differs from the site default. 469cc98f4d1Stracker-user * 47049b74e0aStracker-user * @param Event $event ACTION_ACT_PREPROCESS 471cc98f4d1Stracker-user * @param mixed $param 472cc98f4d1Stracker-user */ 47349b74e0aStracker-user public function applyUserLang(Event $event, $param) 474cc98f4d1Stracker-user { 475*26676c97Stracker-user global $conf; 476cc98f4d1Stracker-user 477*26676c97Stracker-user $preferred = $this->resolvePreferredLang(); 478*26676c97Stracker-user if ($preferred === null) { 479*26676c97Stracker-user return; // anonymous, no/invalid preference, or already correct 480cc98f4d1Stracker-user } 481cc98f4d1Stracker-user 482cc98f4d1Stracker-user $conf['lang'] = $preferred; 483cc98f4d1Stracker-user 484cc98f4d1Stracker-user // Re-initialise the global $lang array so immediately-following 485cc98f4d1Stracker-user // getLang() calls within this request pick up the new language. 486cc98f4d1Stracker-user init_lang($preferred); 487cc98f4d1Stracker-user } 488cc98f4d1Stracker-user 489*26676c97Stracker-user /** 490*26676c97Stracker-user * Apply the user's interface language to the on-the-fly JavaScript bundle 491*26676c97Stracker-user * (lib/exe/js.php). 492*26676c97Stracker-user * 493*26676c97Stracker-user * js.php is its own request and never fires ACTION_ACT_PREPROCESS, so 494*26676c97Stracker-user * applyUserLang() does not reach it; without this, LANG.plugins.* and the 495*26676c97Stracker-user * rest of the JS strings always ship in the SITE-default language. Two 496*26676c97Stracker-user * things must happen here, both before js.php proceeds: 497*26676c97Stracker-user * 498*26676c97Stracker-user * 1. Switch $conf['lang'] (+ init_lang) so js_pluginstrings() and friends 499*26676c97Stracker-user * read the user's language. 500*26676c97Stracker-user * 2. Repoint the language-specific datepicker entry already present in 501*26676c97Stracker-user * the file list. js.php keys its output cache on 502*26676c97Stracker-user * md5(serialize($files)); that datepicker path is the only 503*26676c97Stracker-user * language-dependent member, so without rewriting it two users with 504*26676c97Stracker-user * different languages would collide on a single cached bundle. js.php 505*26676c97Stracker-user * skips non-existent files, so this is safe even for a language that 506*26676c97Stracker-user * ships no datepicker translation. 507*26676c97Stracker-user * 508*26676c97Stracker-user * @param Event $event JS_SCRIPT_LIST ($event->data is the file list, by ref) 509*26676c97Stracker-user * @param mixed $param 510*26676c97Stracker-user */ 511*26676c97Stracker-user public function applyUserLangToScripts(Event $event, $param) 512*26676c97Stracker-user { 513*26676c97Stracker-user global $conf; 514*26676c97Stracker-user 515*26676c97Stracker-user $preferred = $this->resolvePreferredLang(); 516*26676c97Stracker-user if ($preferred === null) { 517*26676c97Stracker-user return; 518*26676c97Stracker-user } 519*26676c97Stracker-user 520*26676c97Stracker-user $old = $conf['lang']; 521*26676c97Stracker-user 522*26676c97Stracker-user if (is_array($event->data)) { 523*26676c97Stracker-user $needle = 'inc/lang/' . $old . '/jquery.ui.datepicker.js'; 524*26676c97Stracker-user $replacement = 'inc/lang/' . $preferred . '/jquery.ui.datepicker.js'; 525*26676c97Stracker-user foreach ($event->data as $i => $file) { 526*26676c97Stracker-user if (is_string($file) && strpos($file, $needle) !== false) { 527*26676c97Stracker-user $event->data[$i] = str_replace($needle, $replacement, $file); 528*26676c97Stracker-user } 529*26676c97Stracker-user } 530*26676c97Stracker-user } 531*26676c97Stracker-user 532*26676c97Stracker-user $conf['lang'] = $preferred; 533*26676c97Stracker-user init_lang($preferred); 534*26676c97Stracker-user } 535*26676c97Stracker-user 536*26676c97Stracker-user /** 537*26676c97Stracker-user * Resolve the logged-in user's stored, validated interface-language 538*26676c97Stracker-user * preference, or null when there is none to apply. 539*26676c97Stracker-user * 540*26676c97Stracker-user * Returns null for anonymous visitors, when no preference is stored, when 541*26676c97Stracker-user * the stored value is malformed or names a missing inc/lang/ directory, or 542*26676c97Stracker-user * when it already matches the active $conf['lang']. Reads the raw stored 543*26676c97Stracker-user * record via getRecord() so it does NOT fire PLUGIN_USERSETTINGS_REGISTER 544*26676c97Stracker-user * (which would glob inc/lang/ on every request). 545*26676c97Stracker-user * 546*26676c97Stracker-user * @return string|null validated language code, or null for "leave as-is" 547*26676c97Stracker-user */ 548*26676c97Stracker-user protected function resolvePreferredLang() 549*26676c97Stracker-user { 550*26676c97Stracker-user global $conf, $INPUT; 551*26676c97Stracker-user 552*26676c97Stracker-user $user = $INPUT->server->str('REMOTE_USER'); 553*26676c97Stracker-user if ($user === '') { 554*26676c97Stracker-user return null; // anonymous — use the site default 555*26676c97Stracker-user } 556*26676c97Stracker-user 557*26676c97Stracker-user $helper = $this->getHelper(); 558*26676c97Stracker-user if ($helper === null) { 559*26676c97Stracker-user return null; 560*26676c97Stracker-user } 561*26676c97Stracker-user 562*26676c97Stracker-user // Read the raw stored record — does not fire PLUGIN_USERSETTINGS_REGISTER. 563*26676c97Stracker-user $record = $helper->getRecord('lang', $user); 564*26676c97Stracker-user $preferred = (is_array($record) && isset($record['value'])) ? (string) $record['value'] : null; 565*26676c97Stracker-user 566*26676c97Stracker-user if ($preferred === null || $preferred === '' || $preferred === $conf['lang']) { 567*26676c97Stracker-user return null; // no preference stored or already correct 568*26676c97Stracker-user } 569*26676c97Stracker-user 570*26676c97Stracker-user // Defence-in-depth: language codes are lowercase [a-z0-9-] only. 571*26676c97Stracker-user if (!preg_match('/^[a-z0-9-]+$/', $preferred)) { 572*26676c97Stracker-user return null; 573*26676c97Stracker-user } 574*26676c97Stracker-user 575*26676c97Stracker-user // Validate: only apply if the directory actually exists to avoid a 576*26676c97Stracker-user // broken page when someone stores a stale language code. 577*26676c97Stracker-user if (!is_dir(DOKU_INC . 'inc/lang/' . $preferred)) { 578*26676c97Stracker-user return null; 579*26676c97Stracker-user } 580*26676c97Stracker-user 581*26676c97Stracker-user return $preferred; 582*26676c97Stracker-user } 583*26676c97Stracker-user 584cc98f4d1Stracker-user // --------------------------------------------------------------------- 585cc98f4d1Stracker-user // Form rendering (shared between action and admin) 586cc98f4d1Stracker-user // --------------------------------------------------------------------- 587cc98f4d1Stracker-user 5881ab40613Stracker-user /** 5891ab40613Stracker-user * Render one toggle as a form row. Public so the admin component can 5901ab40613Stracker-user * reuse it for its per-user edit form. 5911ab40613Stracker-user * 5921ab40613Stracker-user * @param array $def a normalised toggle definition 5931ab40613Stracker-user * @param mixed $value the user's effective value for this toggle 5941ab40613Stracker-user * @return string 5951ab40613Stracker-user */ 5961ab40613Stracker-user public function renderToggleRow(array $def, $value) 5971ab40613Stracker-user { 5981ab40613Stracker-user $key = hsc($def['key']); 5991ab40613Stracker-user 6001ab40613Stracker-user if ($def['type'] === 'select') { 6011ab40613Stracker-user $id = 'us__' . $key; 6021ab40613Stracker-user $html = '<div class="us-row us-row-select">'; 6031ab40613Stracker-user $html .= '<label class="us-label" for="' . $id . '">'; 6041ab40613Stracker-user $html .= '<span class="us-name">' . hsc($def['label']) . '</span>'; 6051ab40613Stracker-user $html .= '<select name="' . $key . '" id="' . $id . '">'; 6061ab40613Stracker-user foreach ($def['options'] as $optValue => $optLabel) { 6071ab40613Stracker-user $selected = ((string) $optValue === (string) $value) ? ' selected="selected"' : ''; 6081ab40613Stracker-user $html .= '<option value="' . hsc((string) $optValue) . '"' . $selected . '>' 6091ab40613Stracker-user . hsc((string) $optLabel) . '</option>'; 6101ab40613Stracker-user } 6111ab40613Stracker-user $html .= '</select>'; 6121ab40613Stracker-user $html .= '</label>'; 6131ab40613Stracker-user } else { 6141ab40613Stracker-user $checked = empty($value) ? '' : ' checked="checked"'; 6151ab40613Stracker-user $html = '<div class="us-row us-row-checkbox">'; 6161ab40613Stracker-user $html .= '<label class="us-label">'; 6171ab40613Stracker-user $html .= '<input type="checkbox" name="' . $key . '" value="1"' . $checked . ' />'; 6181ab40613Stracker-user $html .= '<span class="us-name">' . hsc($def['label']) . '</span>'; 6191ab40613Stracker-user $html .= '</label>'; 6201ab40613Stracker-user } 6211ab40613Stracker-user 6221ab40613Stracker-user if ($def['desc'] !== '') { 6231ab40613Stracker-user $html .= '<div class="us-desc">' . hsc($def['desc']) . '</div>'; 6241ab40613Stracker-user } 6251ab40613Stracker-user 6261ab40613Stracker-user return $html . '</div>'; 6271ab40613Stracker-user } 6281ab40613Stracker-user} 629