1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Form\Form;
5use dokuwiki\plugin\twofactor\Manager;
6use dokuwiki\plugin\twofactor\MenuItem;
7
8/**
9 * DokuWiki Plugin twofactor (Action Component)
10 *
11 * This handles the 2fa profile screen where users can set their 2fa preferences and configure the
12 * providers they want to use.
13 *
14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
15 */
16class action_plugin_twofactor_profile extends ActionPlugin
17{
18    /** @inheritDoc */
19    public function register(Doku_Event_Handler $controller)
20    {
21        // Adds our twofactor profile to the user menu.
22        $controller->register_hook('MENU_ITEMS_ASSEMBLY', 'AFTER', $this, 'handleUserMenuAssembly');
23
24        $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handlePreProcess');
25        $controller->register_hook('TPL_ACT_UNKNOWN', 'BEFORE', $this, 'handleUnknownAction');
26    }
27
28    /**
29     * Add 2fa Menu Item
30     *
31     * @param Doku_Event $event
32     */
33    public function handleUserMenuAssembly(Doku_Event $event)
34    {
35        if (!(Manager::getInstance())->isReady()) return;
36
37        global $INPUT;
38        // If this is not the user menu, then get out.
39        if ($event->data['view'] != 'user') return;
40        if (!$INPUT->server->has('REMOTE_USER')) return;
41
42        // Create the new menu item
43        $menuitem = new MenuItem($this->getLang('btn_twofactor_profile'));
44
45        // Find index of existing Profile menu item.
46        for ($index = 0; $index > count($event->data['items']); $index++) {
47            if ($event->data['items'][$index]->getType() === 'profile') {
48                break;
49            }
50        }
51        array_splice($event->data['items'], $index + 1, 0, [$menuitem]);
52    }
53
54    /**
55     * Check permissions to call the 2fa profile
56     *
57     * @param Doku_Event $event
58     */
59    public function handlePreProcess(Doku_Event $event)
60    {
61        if ($event->data != 'twofactor_profile') return;
62        if (!(Manager::getInstance())->isReady()) return;
63
64        // We will be handling this action's permissions here.
65        $event->preventDefault();
66        $event->stopPropagation();
67
68        // If not logged into the main auth plugin then send there.
69        global $INPUT;
70        global $ID;
71
72        if (!$INPUT->server->has('REMOTE_USER')) {
73            $event->result = false;
74            send_redirect(wl($ID, array('do' => 'login'), true, '&'));
75            return;
76        }
77
78        if (strtolower($INPUT->server->str('REQUEST_METHOD')) == 'post') {
79            if ($this->handleProfile()) {
80                // we might have changed something important, make sure the whole workflow restarts
81                send_redirect(wl($ID, ['do' => 'twofactor_profile'], true, '&'));
82            }
83
84        }
85    }
86
87    /**
88     * Output the forms
89     *
90     * @param Doku_Event $event
91     */
92    public function handleUnknownAction(Doku_Event $event)
93    {
94        if ($event->data != 'twofactor_profile') return;
95        if (!(Manager::getInstance())->isReady()) return;
96        $event->preventDefault();
97        $event->stopPropagation();
98        global $INPUT;
99
100        echo '<div class="plugin_twofactor_profile">';
101        echo $this->locale_xhtml('profile');
102
103        if ($INPUT->has('twofactor_setup')) {
104            $this->printProviderSetup();
105        } else {
106            if (!$this->printOptOutForm()) {
107                $this->printConfiguredProviders();
108
109                $this->printProviderSetupSelect();
110            }
111        }
112        echo '</div>';
113    }
114
115    /**
116     * Handle POSTs for provider forms
117     *
118     * @return bool should a redirect be made?
119     */
120    protected function handleProfile()
121    {
122        global $INPUT;
123        if (!checkSecurityToken()) return true;
124        $manager = Manager::getInstance();
125
126        if ($INPUT->has('twofactor_optout') && $this->getConf('optinout') === 'optout') {
127            $manager->userOptOutState($INPUT->bool('optout'));
128            return true;
129        }
130
131        if (!$INPUT->has('provider')) return true;
132        $providers = $manager->getAllProviders();
133        if (!isset($providers[$INPUT->str('provider')])) return true;
134        $provider = $providers[$INPUT->str('provider')];
135
136        if ($INPUT->has('twofactor_delete')) {
137            $provider->reset();
138            $manager->getUserDefaultProvider(); // resets the default to the next available
139            return true;
140        }
141
142        if ($INPUT->has('twofactor_default')) {
143            $manager->setUserDefaultProvider($provider);
144            return true;
145        }
146
147        if (!$provider->isConfigured()) {
148            $provider->handleProfileForm();
149            return $provider->isConfigured(); // redirect only if configuration finished
150        }
151
152        return true;
153    }
154
155    /**
156     * Print the opt-out form (if available)
157     *
158     * @return bool true if the user currently opted out
159     */
160    protected function printOptOutForm()
161    {
162        $manager = Manager::getInstance();
163        $optedout = false;
164        $setting = $this->getConf('optinout');
165
166        echo '<section class="state">';
167        echo $this->locale_xhtml($setting);
168
169        // optout form
170        if ($setting == 'optout') {
171            $form = new Form(['method' => 'post']);
172            $cb = $form->addCheckbox('optout', $this->getLang('optout'));
173            if ($manager->userOptOutState()) {
174                $cb->attr('checked', 'checked');
175            }
176            $form->addButton('twofactor_optout', $this->getLang('btn_confirm'));
177            echo $form->toHTML();
178
179            // when user opted out, don't show the rest of the form
180            if ($manager->userOptOutState()) {
181                $optedout = true;
182            }
183        }
184
185        echo '</section>';
186        return $optedout;
187    }
188
189    /**
190     * Print the form where a user can select their default provider
191     *
192     * @return void
193     */
194    protected function printConfiguredProviders()
195    {
196        $manager = Manager::getInstance();
197
198        $userproviders = $manager->getUserProviders();
199        $default = $manager->getUserDefaultProvider();
200        if (!$userproviders) return;
201
202        $form = new Form(['method' => 'POST']);
203        $form->addFieldsetOpen($this->getLang('providers'));
204        foreach ($userproviders as $provider) {
205            $el = $form->addRadioButton('provider', $provider->getLabel())->val($provider->getProviderID());
206            if ($provider->getProviderID() === $default->getProviderID()) {
207                $el->attr('checked', 'checked');
208                $el->getLabel()->val($provider->getLabel() . ' ' . $this->getLang('default'));
209            }
210        }
211
212        $form->addTagOpen('div')->addClass('buttons');
213        $form->addButton('twofactor_default', $this->getLang('btn_default'))->attr('submit');
214        $form->addButton('twofactor_delete', $this->getLang('btn_remove'))
215             ->addClass('twofactor_delconfirm')->attr('submit');
216        $form->addTagClose('div');
217
218        $form->addFieldsetClose();
219        echo $form->toHTML();
220    }
221
222    /**
223     * List providers available for adding
224     *
225     * @return void
226     */
227    protected function printProviderSetupSelect()
228    {
229        $manager = Manager::getInstance();
230        $available = $manager->getUserProviders(false);
231        if (!$available) return;
232
233        $options = [];
234        foreach ($available as $provider) {
235            $options[$provider->getProviderID()] = $provider->getLabel();
236        }
237
238        $form = new Form(['method' => 'post']);
239        $form->setHiddenField('do', 'twofactor_profile');
240        $form->setHiddenField('init', '1');
241        $form->addFieldsetOpen($this->getLang('newprovider'));
242        $form->addDropdown('provider', $options, $this->getLang('provider'));
243
244        $form->addTagOpen('div')->addClass('buttons');
245        $form->addButton('twofactor_setup', $this->getLang('btn_setup'))->attr('type', 'submit');
246        $form->addTagClose('div');
247
248        $form->addFieldsetClose();
249        echo $form->toHTML();
250    }
251
252    /**
253     * Display the setup form for a provider
254     *
255     * @return void
256     */
257    protected function printProviderSetup()
258    {
259        global $lang;
260        global $INPUT;
261
262        $providerID = $INPUT->str('provider');
263        $providers = (Manager::getInstance())->getUserProviders(false);
264        if (!isset($providers[$providerID])) return;
265        $provider = $providers[$providerID];
266
267        $form = new Form(['method' => 'POST', 'class' => 'provider-' . $providerID]);
268        $form->setHiddenField('do', 'twofactor_profile');
269        $form->setHiddenField('twofactor_setup', '1');
270        $form->setHiddenField('provider', $provider->getProviderID());
271
272        $form->addFieldsetOpen($provider->getLabel());
273        $provider->renderProfileForm($form);
274
275        $form->addTagOpen('div')->addClass('buttons');
276        $form->addButton('twofactor_submit', $this->getLang('btn_confirm'))->attr('type', 'submit');
277        $form->addButton('twofactor_delete', $lang['btn_cancel'])->attr('type', 'submit');
278        $form->addTagClose('div');
279
280        $form->addFieldsetClose();
281        echo $form->toHTML();
282    }
283
284}
285
286
287