xref: /plugin/usersettings/action.php (revision 1ab406139ae52af26e6f9eaacaf1646e58521679)
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, '&amp;');
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