xref: /plugin/usersettings/action.php (revision cc98f4d1e5f0c930e495939174b11819e96464f3)
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
231ab40613Stracker-userclass action_plugin_usersettings extends DokuWiki_Action_Plugin
241ab40613Stracker-user{
251ab40613Stracker-user    /** the do= value this plugin owns */
261ab40613Stracker-user    const ACTION = 'usersettings';
271ab40613Stracker-user
281ab40613Stracker-user    /**
291ab40613Stracker-user     * Register event handlers.
301ab40613Stracker-user     */
311ab40613Stracker-user    public function register(Doku_Event_Handler $controller)
321ab40613Stracker-user    {
331ab40613Stracker-user        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handleMenuAssembly');
341ab40613Stracker-user        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreprocess');
351ab40613Stracker-user        $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleUnknown');
36*cc98f4d1Stracker-user
37*cc98f4d1Stracker-user        // Register the built-in interface language toggle.
38*cc98f4d1Stracker-user        $controller->register_hook(
39*cc98f4d1Stracker-user            helper_plugin_usersettings::REGISTER_EVENT,
40*cc98f4d1Stracker-user            'BEFORE',
41*cc98f4d1Stracker-user            $this,
42*cc98f4d1Stracker-user            'registerLangToggle'
43*cc98f4d1Stracker-user        );
44*cc98f4d1Stracker-user
45*cc98f4d1Stracker-user        // Apply the user's language choice as early as possible so that all
46*cc98f4d1Stracker-user        // DokuWiki rendering — including TPL_ hooks further down the chain —
47*cc98f4d1Stracker-user        // uses the right language strings.  ACTION_ACT_PREPROCESS fires before
48*cc98f4d1Stracker-user        // any output is produced and before template rendering begins.
49*cc98f4d1Stracker-user        $controller->register_hook(
50*cc98f4d1Stracker-user            'ACTION_ACT_PREPROCESS',
51*cc98f4d1Stracker-user            'BEFORE',
52*cc98f4d1Stracker-user            $this,
53*cc98f4d1Stracker-user            'applyUserLang',
54*cc98f4d1Stracker-user            null,
55*cc98f4d1Stracker-user            // run at priority -10 so we fire before handlePreprocess (0) and
56*cc98f4d1Stracker-user            // before anything else that might read $conf['lang']
57*cc98f4d1Stracker-user            -10
58*cc98f4d1Stracker-user        );
591ab40613Stracker-user    }
601ab40613Stracker-user
611ab40613Stracker-user    /**
621ab40613Stracker-user     * Load the storage/registration helper.
631ab40613Stracker-user     *
641ab40613Stracker-user     * @return helper_plugin_usersettings|null
651ab40613Stracker-user     */
661ab40613Stracker-user    protected function getHelper()
671ab40613Stracker-user    {
681ab40613Stracker-user        /** @var helper_plugin_usersettings|null $helper */
691ab40613Stracker-user        $helper = plugin_load('helper', 'usersettings');
701ab40613Stracker-user        return $helper;
711ab40613Stracker-user    }
721ab40613Stracker-user
731ab40613Stracker-user    // ---------------------------------------------------------------------
741ab40613Stracker-user    //  1. The user-menu item
751ab40613Stracker-user    // ---------------------------------------------------------------------
761ab40613Stracker-user
771ab40613Stracker-user    /**
781ab40613Stracker-user     * Insert the "Preferences" item into the user menu, just before the
791ab40613Stracker-user     * "Update Profile" item.
801ab40613Stracker-user     *
811ab40613Stracker-user     * @param Doku_Event $event MENU_ITEMS_ASSEMBLY
821ab40613Stracker-user     * @param mixed       $param
831ab40613Stracker-user     */
841ab40613Stracker-user    public function handleMenuAssembly(Doku_Event $event, $param)
851ab40613Stracker-user    {
861ab40613Stracker-user        if (!is_array($event->data) || ($event->data['view'] ?? '') !== 'user') {
871ab40613Stracker-user            return;
881ab40613Stracker-user        }
891ab40613Stracker-user
901ab40613Stracker-user        try {
911ab40613Stracker-user            $item = new \dokuwiki\plugin\usersettings\MenuItem();
921ab40613Stracker-user        } catch (\RuntimeException $e) {
931ab40613Stracker-user            // anonymous visitor, or the action is disabled — no menu item
941ab40613Stracker-user            return;
951ab40613Stracker-user        }
961ab40613Stracker-user
971ab40613Stracker-user        if (!isset($event->data['items']) || !is_array($event->data['items'])) {
981ab40613Stracker-user            return;
991ab40613Stracker-user        }
1001ab40613Stracker-user        $items =& $event->data['items'];
1011ab40613Stracker-user
1021ab40613Stracker-user        // find the Profile item; default to appending if it is not present
1031ab40613Stracker-user        $pos = count($items);
1041ab40613Stracker-user        foreach ($items as $i => $existing) {
1051ab40613Stracker-user            if ($existing instanceof \dokuwiki\Menu\Item\Profile) {
1061ab40613Stracker-user                $pos = $i;
1071ab40613Stracker-user                break;
1081ab40613Stracker-user            }
1091ab40613Stracker-user        }
1101ab40613Stracker-user        array_splice($items, $pos, 0, [$item]);
1111ab40613Stracker-user    }
1121ab40613Stracker-user
1131ab40613Stracker-user    // ---------------------------------------------------------------------
1141ab40613Stracker-user    //  2. Claiming the custom action + handling the save
1151ab40613Stracker-user    // ---------------------------------------------------------------------
1161ab40613Stracker-user
1171ab40613Stracker-user    /**
1181ab40613Stracker-user     * Claim do=usersettings and, on a form submission, save and redirect.
1191ab40613Stracker-user     *
1201ab40613Stracker-user     * @param Doku_Event $event ACTION_ACT_PREPROCESS
1211ab40613Stracker-user     * @param mixed       $param
1221ab40613Stracker-user     */
1231ab40613Stracker-user    public function handlePreprocess(Doku_Event $event, $param)
1241ab40613Stracker-user    {
1251ab40613Stracker-user        if ($event->data !== self::ACTION) {
1261ab40613Stracker-user            return;
1271ab40613Stracker-user        }
1281ab40613Stracker-user
1291ab40613Stracker-user        // Preventing the default makes DokuWiki keep the action and route it
1301ab40613Stracker-user        // through dokuwiki\Action\Plugin, which will fire TPL_ACT_UNKNOWN.
1311ab40613Stracker-user        $event->preventDefault();
1321ab40613Stracker-user        $event->stopPropagation();
1331ab40613Stracker-user
1341ab40613Stracker-user        global $INPUT, $ID;
1351ab40613Stracker-user
1361ab40613Stracker-user        $user = $INPUT->server->str('REMOTE_USER');
1371ab40613Stracker-user        if ($user === '') {
1381ab40613Stracker-user            return; // anonymous — the rendered page shows a login notice
1391ab40613Stracker-user        }
1401ab40613Stracker-user
1411ab40613Stracker-user        // not a save submission — nothing to do, the page will just render
1421ab40613Stracker-user        if (!$INPUT->post->bool('usersettings_save')) {
1431ab40613Stracker-user            return;
1441ab40613Stracker-user        }
1451ab40613Stracker-user
1461ab40613Stracker-user        // CSRF protection; checkSecurityToken() shows its own error on failure
1471ab40613Stracker-user        if (!checkSecurityToken()) {
1481ab40613Stracker-user            return;
1491ab40613Stracker-user        }
1501ab40613Stracker-user
1511ab40613Stracker-user        $ok = $this->saveSubmittedPreferences($user);
1521ab40613Stracker-user        msg($this->getLang($ok ? 'saved' : 'savefail'), $ok ? 1 : -1);
1531ab40613Stracker-user
1541ab40613Stracker-user        // Post/Redirect/Get: a refresh must not re-submit the form
1551ab40613Stracker-user        send_redirect(wl($ID, ['do' => self::ACTION], true, '&'));
1561ab40613Stracker-user    }
1571ab40613Stracker-user
1581ab40613Stracker-user    /**
1591ab40613Stracker-user     * Read the submitted toggle values for every registered toggle and store
1601ab40613Stracker-user     * them for the given user.
1611ab40613Stracker-user     *
1621ab40613Stracker-user     * Kept separate from handlePreprocess() so it carries no redirect and can
1631ab40613Stracker-user     * be exercised directly by tests. Checkboxes that are unchecked do not
1641ab40613Stracker-user     * appear in the POST data, so every registered toggle is read explicitly
1651ab40613Stracker-user     * rather than iterating whatever was submitted.
1661ab40613Stracker-user     *
1671ab40613Stracker-user     * @param string      $user   whose preferences are being written
1681ab40613Stracker-user     * @param string|null $actor  who is making the change; defaults to $user
1691ab40613Stracker-user     *                            (the admin component passes the admin here)
1701ab40613Stracker-user     * @return bool
1711ab40613Stracker-user     */
1721ab40613Stracker-user    public function saveSubmittedPreferences($user, $actor = null)
1731ab40613Stracker-user    {
1741ab40613Stracker-user        global $INPUT;
1751ab40613Stracker-user
1761ab40613Stracker-user        if ($actor === null) {
1771ab40613Stracker-user            $actor = $user;
1781ab40613Stracker-user        }
1791ab40613Stracker-user
1801ab40613Stracker-user        $helper = $this->getHelper();
1811ab40613Stracker-user        if ($helper === null) {
1821ab40613Stracker-user            return false;
1831ab40613Stracker-user        }
1841ab40613Stracker-user
1851ab40613Stracker-user        $values = [];
1861ab40613Stracker-user        foreach ($helper->getRegisteredToggles() as $key => $def) {
1871ab40613Stracker-user            if ($def['type'] === 'checkbox') {
1881ab40613Stracker-user                $values[$key] = $INPUT->post->bool($key) ? 1 : 0;
1891ab40613Stracker-user            } else {
1901ab40613Stracker-user                $values[$key] = $INPUT->post->str($key);
1911ab40613Stracker-user            }
1921ab40613Stracker-user        }
1931ab40613Stracker-user
1941ab40613Stracker-user        return $helper->setPreferences($values, $user, $actor);
1951ab40613Stracker-user    }
1961ab40613Stracker-user
1971ab40613Stracker-user    // ---------------------------------------------------------------------
1981ab40613Stracker-user    //  3. Rendering the settings page
1991ab40613Stracker-user    // ---------------------------------------------------------------------
2001ab40613Stracker-user
2011ab40613Stracker-user    /**
2021ab40613Stracker-user     * Render the settings page for do=usersettings.
2031ab40613Stracker-user     *
2041ab40613Stracker-user     * @param Doku_Event $event TPL_ACT_UNKNOWN
2051ab40613Stracker-user     * @param mixed       $param
2061ab40613Stracker-user     */
2071ab40613Stracker-user    public function handleUnknown(Doku_Event $event, $param)
2081ab40613Stracker-user    {
2091ab40613Stracker-user        if ($event->data !== self::ACTION) {
2101ab40613Stracker-user            return;
2111ab40613Stracker-user        }
2121ab40613Stracker-user        $event->preventDefault();
2131ab40613Stracker-user        $event->stopPropagation();
2141ab40613Stracker-user
2151ab40613Stracker-user        echo $this->renderSettingsPage();
2161ab40613Stracker-user    }
2171ab40613Stracker-user
2181ab40613Stracker-user    /**
2191ab40613Stracker-user     * Build the HTML of the settings page.
2201ab40613Stracker-user     *
2211ab40613Stracker-user     * @return string
2221ab40613Stracker-user     */
2231ab40613Stracker-user    public function renderSettingsPage()
2241ab40613Stracker-user    {
2251ab40613Stracker-user        global $INPUT, $ID;
2261ab40613Stracker-user
2271ab40613Stracker-user        $user = $INPUT->server->str('REMOTE_USER');
2281ab40613Stracker-user
2291ab40613Stracker-user        $html  = '<div class="plugin_usersettings">';
2301ab40613Stracker-user        $html .= '<h1>' . hsc($this->getLang('heading')) . '</h1>';
2311ab40613Stracker-user
2321ab40613Stracker-user        if ($user === '') {
2331ab40613Stracker-user            $html .= '<p>' . hsc($this->getLang('nologin')) . '</p>';
2341ab40613Stracker-user            return $html . '</div>';
2351ab40613Stracker-user        }
2361ab40613Stracker-user
2371ab40613Stracker-user        $helper  = $this->getHelper();
2381ab40613Stracker-user        $toggles = $helper ? $helper->getRegisteredToggles() : [];
2391ab40613Stracker-user
2401ab40613Stracker-user        if (empty($toggles)) {
2411ab40613Stracker-user            $html .= '<p>' . hsc($this->getLang('notoggles')) . '</p>';
2421ab40613Stracker-user            return $html . '</div>';
2431ab40613Stracker-user        }
2441ab40613Stracker-user
2451ab40613Stracker-user        $html .= '<p class="us-intro">' . hsc($this->getLang('intro')) . '</p>';
2461ab40613Stracker-user
2471ab40613Stracker-user        $action = wl($ID, ['do' => self::ACTION], false, '&amp;');
2481ab40613Stracker-user        $html  .= '<form method="post" action="' . $action . '" class="us-form">';
2491ab40613Stracker-user        $html  .= formSecurityToken(false);
2501ab40613Stracker-user
2511ab40613Stracker-user        foreach ($toggles as $key => $def) {
2521ab40613Stracker-user            $html .= $this->renderToggleRow($def, $helper->getPreference($key, $user));
2531ab40613Stracker-user        }
2541ab40613Stracker-user
2551ab40613Stracker-user        $html .= '<div class="us-actions">';
2561ab40613Stracker-user        $html .= '<button type="submit" name="usersettings_save" value="1" class="button">'
2571ab40613Stracker-user               . hsc($this->getLang('save')) . '</button>';
2581ab40613Stracker-user        $html .= '</div>';
2591ab40613Stracker-user        $html .= '</form>';
2601ab40613Stracker-user
2611ab40613Stracker-user        return $html . '</div>';
2621ab40613Stracker-user    }
2631ab40613Stracker-user
264*cc98f4d1Stracker-user    // ---------------------------------------------------------------------
265*cc98f4d1Stracker-user    //  Built-in: interface language toggle
266*cc98f4d1Stracker-user    // ---------------------------------------------------------------------
267*cc98f4d1Stracker-user
268*cc98f4d1Stracker-user    /**
269*cc98f4d1Stracker-user     * Contribute the "Interface language" select to the usersettings registry.
270*cc98f4d1Stracker-user     *
271*cc98f4d1Stracker-user     * The option list is built by scanning DOKU_INC/inc/lang/ for sub-
272*cc98f4d1Stracker-user     * directories that contain a lang.php file — the same source the
273*cc98f4d1Stracker-user     * Configuration Manager uses for its own language drop-down.  The scan
274*cc98f4d1Stracker-user     * result is sorted alphabetically by language code; the site default is
275*cc98f4d1Stracker-user     * used as the toggle's default value so the toggle appears pre-selected
276*cc98f4d1Stracker-user     * correctly for users who have never changed it.
277*cc98f4d1Stracker-user     *
278*cc98f4d1Stracker-user     * @param Doku_Event $event PLUGIN_USERSETTINGS_REGISTER
279*cc98f4d1Stracker-user     * @param mixed       $param
280*cc98f4d1Stracker-user     */
281*cc98f4d1Stracker-user    public function registerLangToggle(Doku_Event $event, $param)
282*cc98f4d1Stracker-user    {
283*cc98f4d1Stracker-user        global $conf;
284*cc98f4d1Stracker-user
285*cc98f4d1Stracker-user        $options = $this->getAvailableLanguages();
286*cc98f4d1Stracker-user        if (empty($options)) {
287*cc98f4d1Stracker-user            return; // nothing to register if we cannot list languages
288*cc98f4d1Stracker-user        }
289*cc98f4d1Stracker-user
290*cc98f4d1Stracker-user        $siteDefault = $conf['lang'] ?? 'en';
291*cc98f4d1Stracker-user        if (!array_key_exists($siteDefault, $options)) {
292*cc98f4d1Stracker-user            $siteDefault = array_key_first($options);
293*cc98f4d1Stracker-user        }
294*cc98f4d1Stracker-user
295*cc98f4d1Stracker-user        $event->data[] = [
296*cc98f4d1Stracker-user            'key'     => 'lang',
297*cc98f4d1Stracker-user            'label'   => $this->getLang('lang_label'),
298*cc98f4d1Stracker-user            'desc'    => $this->getLang('lang_desc'),
299*cc98f4d1Stracker-user            'type'    => 'select',
300*cc98f4d1Stracker-user            'options' => $options,
301*cc98f4d1Stracker-user            'default' => $siteDefault,
302*cc98f4d1Stracker-user            'plugin'  => 'usersettings',
303*cc98f4d1Stracker-user        ];
304*cc98f4d1Stracker-user    }
305*cc98f4d1Stracker-user
306*cc98f4d1Stracker-user    /**
307*cc98f4d1Stracker-user     * Build the [code => display name] map of all installed DokuWiki interface
308*cc98f4d1Stracker-user     * languages by scanning inc/lang/.  The display name is the language code
309*cc98f4d1Stracker-user     * itself (e.g. "en", "de", "fr") — consistent with how the Configuration
310*cc98f4d1Stracker-user     * Manager presents the option.
311*cc98f4d1Stracker-user     *
312*cc98f4d1Stracker-user     * @return array  [langCode => langCode]  sorted by language code
313*cc98f4d1Stracker-user     */
314*cc98f4d1Stracker-user    protected function getAvailableLanguages()
315*cc98f4d1Stracker-user    {
316*cc98f4d1Stracker-user        $pattern = DOKU_INC . 'inc/lang/*/lang.php';
317*cc98f4d1Stracker-user        $files   = glob($pattern);
318*cc98f4d1Stracker-user        if ($files === false || empty($files)) {
319*cc98f4d1Stracker-user            return [];
320*cc98f4d1Stracker-user        }
321*cc98f4d1Stracker-user
322*cc98f4d1Stracker-user        $langs = [];
323*cc98f4d1Stracker-user        foreach ($files as $file) {
324*cc98f4d1Stracker-user            $code = basename(dirname($file)); // e.g. "de" from ".../inc/lang/de/lang.php"
325*cc98f4d1Stracker-user            if ($code === '' || $code === '.' || $code === '..') {
326*cc98f4d1Stracker-user                continue;
327*cc98f4d1Stracker-user            }
328*cc98f4d1Stracker-user            $langs[$code] = $code;
329*cc98f4d1Stracker-user        }
330*cc98f4d1Stracker-user
331*cc98f4d1Stracker-user        ksort($langs, SORT_STRING);
332*cc98f4d1Stracker-user        return $langs;
333*cc98f4d1Stracker-user    }
334*cc98f4d1Stracker-user
335*cc98f4d1Stracker-user    /**
336*cc98f4d1Stracker-user     * Apply the logged-in user's preferred interface language, overriding the
337*cc98f4d1Stracker-user     * site-wide $conf['lang'] before any rendering takes place.
338*cc98f4d1Stracker-user     *
339*cc98f4d1Stracker-user     * DokuWiki loads language strings lazily (via getLang() / $lang global
340*cc98f4d1Stracker-user     * reloads triggered by calls to init_lang()), so changing $conf['lang']
341*cc98f4d1Stracker-user     * here — in the earliest ACTION_ACT_PREPROCESS handler — is sufficient
342*cc98f4d1Stracker-user     * to affect all subsequent output.
343*cc98f4d1Stracker-user     *
344*cc98f4d1Stracker-user     * No-op for anonymous visitors or when the user has not chosen a language
345*cc98f4d1Stracker-user     * that differs from the site default.
346*cc98f4d1Stracker-user     *
347*cc98f4d1Stracker-user     * @param Doku_Event $event ACTION_ACT_PREPROCESS
348*cc98f4d1Stracker-user     * @param mixed       $param
349*cc98f4d1Stracker-user     */
350*cc98f4d1Stracker-user    public function applyUserLang(Doku_Event $event, $param)
351*cc98f4d1Stracker-user    {
352*cc98f4d1Stracker-user        global $conf, $INPUT;
353*cc98f4d1Stracker-user
354*cc98f4d1Stracker-user        $user = $INPUT->server->str('REMOTE_USER');
355*cc98f4d1Stracker-user        if ($user === '') {
356*cc98f4d1Stracker-user            return; // anonymous — use the site default
357*cc98f4d1Stracker-user        }
358*cc98f4d1Stracker-user
359*cc98f4d1Stracker-user        $helper = $this->getHelper();
360*cc98f4d1Stracker-user        if ($helper === null) {
361*cc98f4d1Stracker-user            return;
362*cc98f4d1Stracker-user        }
363*cc98f4d1Stracker-user
364*cc98f4d1Stracker-user        $preferred = $helper->getPreference('lang', $user);
365*cc98f4d1Stracker-user        if ($preferred === null || $preferred === '' || $preferred === $conf['lang']) {
366*cc98f4d1Stracker-user            return; // no preference stored or already correct
367*cc98f4d1Stracker-user        }
368*cc98f4d1Stracker-user
369*cc98f4d1Stracker-user        // Validate: only apply if the directory actually exists to avoid a
370*cc98f4d1Stracker-user        // broken page when someone stores a stale language code.
371*cc98f4d1Stracker-user        $langDir = DOKU_INC . 'inc/lang/' . $preferred;
372*cc98f4d1Stracker-user        if (!is_dir($langDir)) {
373*cc98f4d1Stracker-user            return;
374*cc98f4d1Stracker-user        }
375*cc98f4d1Stracker-user
376*cc98f4d1Stracker-user        $conf['lang'] = $preferred;
377*cc98f4d1Stracker-user
378*cc98f4d1Stracker-user        // Re-initialise the global $lang array so immediately-following
379*cc98f4d1Stracker-user        // getLang() calls within this request pick up the new language.
380*cc98f4d1Stracker-user        init_lang($preferred);
381*cc98f4d1Stracker-user    }
382*cc98f4d1Stracker-user
383*cc98f4d1Stracker-user    // ---------------------------------------------------------------------
384*cc98f4d1Stracker-user    //  Form rendering (shared between action and admin)
385*cc98f4d1Stracker-user    // ---------------------------------------------------------------------
386*cc98f4d1Stracker-user
3871ab40613Stracker-user    /**
3881ab40613Stracker-user     * Render one toggle as a form row. Public so the admin component can
3891ab40613Stracker-user     * reuse it for its per-user edit form.
3901ab40613Stracker-user     *
3911ab40613Stracker-user     * @param array $def    a normalised toggle definition
3921ab40613Stracker-user     * @param mixed $value  the user's effective value for this toggle
3931ab40613Stracker-user     * @return string
3941ab40613Stracker-user     */
3951ab40613Stracker-user    public function renderToggleRow(array $def, $value)
3961ab40613Stracker-user    {
3971ab40613Stracker-user        $key = hsc($def['key']);
3981ab40613Stracker-user
3991ab40613Stracker-user        if ($def['type'] === 'select') {
4001ab40613Stracker-user            $id   = 'us__' . $key;
4011ab40613Stracker-user            $html = '<div class="us-row us-row-select">';
4021ab40613Stracker-user            $html .= '<label class="us-label" for="' . $id . '">';
4031ab40613Stracker-user            $html .= '<span class="us-name">' . hsc($def['label']) . '</span>';
4041ab40613Stracker-user            $html .= '<select name="' . $key . '" id="' . $id . '">';
4051ab40613Stracker-user            foreach ($def['options'] as $optValue => $optLabel) {
4061ab40613Stracker-user                $selected = ((string) $optValue === (string) $value) ? ' selected="selected"' : '';
4071ab40613Stracker-user                $html .= '<option value="' . hsc((string) $optValue) . '"' . $selected . '>'
4081ab40613Stracker-user                       . hsc((string) $optLabel) . '</option>';
4091ab40613Stracker-user            }
4101ab40613Stracker-user            $html .= '</select>';
4111ab40613Stracker-user            $html .= '</label>';
4121ab40613Stracker-user        } else {
4131ab40613Stracker-user            $checked = empty($value) ? '' : ' checked="checked"';
4141ab40613Stracker-user            $html  = '<div class="us-row us-row-checkbox">';
4151ab40613Stracker-user            $html .= '<label class="us-label">';
4161ab40613Stracker-user            $html .= '<input type="checkbox" name="' . $key . '" value="1"' . $checked . ' />';
4171ab40613Stracker-user            $html .= '<span class="us-name">' . hsc($def['label']) . '</span>';
4181ab40613Stracker-user            $html .= '</label>';
4191ab40613Stracker-user        }
4201ab40613Stracker-user
4211ab40613Stracker-user        if ($def['desc'] !== '') {
4221ab40613Stracker-user            $html .= '<div class="us-desc">' . hsc($def['desc']) . '</div>';
4231ab40613Stracker-user        }
4241ab40613Stracker-user
4251ab40613Stracker-user        return $html . '</div>';
4261ab40613Stracker-user    }
4271ab40613Stracker-user}
428