xref: /plugin/twofactor/admin.php (revision d0a31016dd6ef844c1c5cd5481e6dc6c2e3315df)
1<?php
2/*
3 *  Twofactor Manager
4 *
5 *  Dokuwiki Admin Plugin
6 *  Special thanks to the useradmin extension as a starting point for this class
7 *
8 *  @author  Mike Wilmes <mwilmes@avc.edu>
9 */
10// must be run within Dokuwiki
11if (!defined('DOKU_INC')) die();
12
13if (!defined('DOKU_TWOFACTOR_PLUGIN_IMAGES')) define('DOKU_TWOFACTOR_PLUGIN_IMAGES',
14    DOKU_BASE . 'lib/plugins/twofactor/images/');
15
16/**
17 * All DokuWiki plugins to extend the admin function
18 * need to inherit from this class
19 */
20class admin_plugin_twofactor extends DokuWiki_Admin_Plugin
21{
22    protected $_user_list = array();     // list of users with attributes
23    protected $_filter = array();   // user selection filter(s)
24    protected $_start = 0;          // index of first user to be displayed
25    protected $_last = 0;           // index of the last user to be displayed
26    protected $_pagesize = 20;      // number of users to list on one page
27    protected $_disabled = '';      // if disabled set to explanatory string
28    protected $_lastdisabled = false; // set to true if last user is unknown and last button is hence buggy
29
30    /**
31     * Constructor
32     */
33    public function __construct()
34    {
35        global $auth;
36
37        $this->setupLocale();
38
39        if (!isset($auth)) {
40            $this->_disabled = $this->lang['noauth'];
41        }
42
43        $requireAttribute = $this->getConf("enable") === 1;
44        $this->attribute = $requireAttribute ? $this->loadHelper('attribute',
45            'TwoFactor depends on the Attribute plugin, but the Attribute plugin is not installed!') : null;
46
47        $available = Twofactor_Auth_Module::_listModules();
48        $allmodules = Twofactor_Auth_Module::_loadModules($available);
49        $failed = array_diff($available, array_keys($allmodules));
50        if (count($failed) > 0) {
51            msg('At least one loaded module did not have a properly named class.' . ' ' . implode(', ', $failed), -1);
52        }
53        $this->modules = &$allmodules;
54        $this->_getUsers();
55    }
56
57    protected function _getUsers()
58    {
59        if ($this->getConf("enable") === 1) {
60            if (!is_null($this->attribute)) {
61                $attr = $this->attribute;
62                $this->_user_list = $this->attribute->enumerateUsers('twofactor');
63            } else {
64                msg($this->lang['no_purpose'], -1);
65            }
66        }
67    }
68
69    /**
70     * Return prompt for admin menu
71     *
72     * @param string $language
73     * @return string
74     */
75    public function getMenuText($language)
76    {
77        global $INFO;
78        if (!$INFO['isadmin']) {
79            return parent::getMenuText($language);
80        }
81
82        return $this->getLang('menu') . ' ' . $this->_disabled;
83    }
84
85    /**
86     * return sort order for position in admin menu
87     *
88     * @return int
89     */
90    public function getMenuSort()
91    {
92        return 2;
93    }
94
95    /**
96     * @return int current start value for pageination
97     */
98    public function getStart()
99    {
100        return $this->_start;
101    }
102
103    /**
104     * @return int number of users per page
105     */
106    public function getPagesize()
107    {
108        return $this->_pagesize;
109    }
110
111    /**
112     * @param boolean $lastdisabled
113     */
114    public function setLastdisabled($lastdisabled)
115    {
116        $this->_lastdisabled = $lastdisabled;
117    }
118
119    /**
120     * Handle user request
121     *
122     * @return bool
123     */
124    public function handle()
125    {
126        global $INPUT, $INFO;
127        if (!$INFO['isadmin']) return false;
128        if ($this->_disabled) {
129            // If disabled, don't process anything.
130            return true;
131        }
132
133        // extract the command and any specific parameters
134        // submit button name is of the form - fn[cmd][param(s)]
135        $fn = $INPUT->param('fn');
136
137        if (is_array($fn)) {
138            $cmd = key($fn);
139            $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null;
140        } else {
141            $cmd = $fn;
142            $param = null;
143        }
144
145        if ($cmd != "search") {
146            $this->_start = $INPUT->int('start', 0);
147            $this->_filter = $this->_retrieveFilter();
148        }
149
150        switch ($cmd) {
151            case "reset"  :
152                $this->_resetUser();
153                break;
154            case "search" :
155                $this->_setFilter($param);
156                $this->_start = 0;
157                break;
158        }
159
160        $this->_user_total = count($this->_user_list) > 0 ? $this->_getUserCount($this->_filter) : -1;
161
162        // page handling
163        switch ($cmd) {
164            case 'start' :
165                $this->_start = 0;
166                break;
167            case 'prev'  :
168                $this->_start -= $this->_pagesize;
169                break;
170            case 'next'  :
171                $this->_start += $this->_pagesize;
172                break;
173            case 'last'  :
174                $this->_start = $this->_user_total;
175                break;
176        }
177        $this->_validatePagination();
178        return true;
179    }
180
181    /**
182     * Output appropriate html
183     *
184     * @return bool
185     */
186    public function html()
187    {
188        global $ID, $INFO;
189
190        if (!$INFO['isadmin']) {
191            print $this->lang['badauth'];
192            return false;
193        }
194
195        if ($this->disabled) {
196            msg($this->_disabled, -1);
197            return true;
198        }
199
200        $user_list = $this->_retrieveUsers($this->_start, $this->_pagesize, $this->_filter);
201
202        $page_buttons = $this->_pagination();
203
204        print $this->locale_xhtml('intro');
205        print $this->locale_xhtml('list');
206
207        ptln("<div id=\"user__manager\">");
208        ptln("<div class=\"level2\">");
209
210        if (count($this->_user_list) > 0) {
211            ptln("<p>" . sprintf($this->lang['summary'], $this->_start + 1, $this->_last,
212                    $this->_getUserCount($this->_filter), count($this->_user_list)) . "</p>");
213        } else {
214            if (count($this->_user_list) < 0) {
215                $allUserTotal = 0;
216            } else {
217                $allUserTotal = count($this->_user_list);
218            }
219            ptln("<p>" . sprintf($this->lang['nonefound'], $allUserTotal) . "</p>");
220        }
221        ptln("<form action=\"" . wl($ID) . "\" method=\"post\">");
222        formSecurityToken();
223        ptln("  <div class=\"table\">");
224        ptln("  <table class=\"inline\">");
225        ptln("    <thead>");
226        ptln("      <tr>");
227        ptln("        <th>&#160;</th><th>" . $this->lang["user_id"] . "</th><th>" . $this->lang["user_name"] . "</th><th>" . $this->lang["user_mail"] . "</th>");
228        ptln("      </tr>");
229
230        ptln("      <tr>");
231        ptln("        <td class=\"rightalign\"><input type=\"image\" src=\"" . DOKU_TWOFACTOR_PLUGIN_IMAGES . "search.png\" name=\"fn[search][new]\" title=\"" . $this->lang['search_prompt'] . "\" alt=\"" . $this->lang['search'] . "\" class=\"button\" /></td>");
232        ptln("        <td><input type=\"text\" name=\"userid\" class=\"edit\" value=\"" . $this->_htmlFilter('user') . "\" /></td>");
233        ptln("        <td><input type=\"text\" name=\"username\" class=\"edit\" value=\"" . $this->_htmlFilter('name') . "\" /></td>");
234        ptln("        <td><input type=\"text\" name=\"usermail\" class=\"edit\" value=\"" . $this->_htmlFilter('mail') . "\" /></td>");
235        ptln("      </tr>");
236        ptln("    </thead>");
237
238        if ($this->_user_total) {
239            ptln("    <tbody>");
240            foreach ($user_list as $user => $userinfo) {
241                extract($userinfo);
242                /**
243                 * @var string $name
244                 * @var string $pass
245                 * @var string $mail
246                 * @var array $grps
247                 */
248                ptln("    <tr class=\"user_info\">");
249                ptln("      <td class=\"centeralign\"><input type=\"checkbox\" name=\"delete[" . hsc($user) . "]\" " . $delete_disable . " /></td>");
250                if ($editable) {
251                    ptln("    <td><a href=\"" . wl($ID, array(
252                            'fn[edit][' . $user . ']' => 1,
253                            'do' => 'admin',
254                            'page' => 'usermanager',
255                            'sectok' => getSecurityToken(),
256                        )) .
257                        "\" title=\"" . $this->lang['edit_prompt'] . "\">" . hsc($user) . "</a></td>");
258                } else {
259                    ptln("    <td>" . hsc($user) . "</td>");
260                }
261                ptln("      <td>" . hsc($name) . "</td><td>" . hsc($mail) . "</td>");
262                ptln("    </tr>");
263            }
264            ptln("    </tbody>");
265        }
266
267        ptln("    <tbody>");
268        ptln("      <tr><td colspan=\"5\" class=\"centeralign\">");
269        ptln("        <span class=\"medialeft\">");
270        ptln("          <button type=\"submit\" name=\"fn[reset]\" id=\"usrmgr__reset\" >" . $this->lang['reset_selected'] . "</button>");
271        ptln("        ");
272        if (!empty($this->_filter)) {
273            ptln("    <button type=\"submit\" name=\"fn[search][clear]\">" . $this->lang['clear'] . "</button>");
274        }
275        ptln("        <input type=\"hidden\" name=\"do\"    value=\"admin\" />");
276        ptln("        <input type=\"hidden\" name=\"page\"  value=\"twofactor\" />");
277
278        $this->_htmlFilterSettings(2);
279        ptln("        </span>");
280        ptln("        <span class=\"mediaright\">");
281        ptln("          <button type=\"submit\" name=\"fn[start]\" " . $page_buttons['start'] . ">" . $this->lang['start'] . "</button>");
282        ptln("          <button type=\"submit\" name=\"fn[prev]\" " . $page_buttons['prev'] . ">" . $this->lang['prev'] . "</button>");
283        ptln("          <button type=\"submit\" name=\"fn[next]\" " . $page_buttons['next'] . ">" . $this->lang['next'] . "</button>");
284        ptln("          <button type=\"submit\" name=\"fn[last]\" " . $page_buttons['last'] . ">" . $this->lang['last'] . "</button>");
285        ptln("        </span>");
286
287        ptln("      </td></tr>");
288        ptln("    </tbody>");
289        ptln("  </table>");
290        ptln("  </div>");
291
292        ptln("</form>");
293        ptln("</div>");
294
295        ptln("</div>");
296        return true;
297    }
298
299    /**
300     * Prints a inputfield
301     *
302     * @param string $id
303     * @param string $name
304     * @param string $label
305     * @param string $value
306     * @param bool $cando whether auth backend is capable to do this action
307     * @param int $indent
308     */
309    protected function _htmlInputField($id, $name, $label, $value, $cando, $indent = 0)
310    {
311        $class = $cando ? '' : ' class="disabled"';
312        echo str_pad('', $indent);
313
314        if ($name == 'userpass' || $name == 'userpass2') {
315            $fieldtype = 'password';
316            $autocomp = 'autocomplete="off"';
317        } elseif ($name == 'usermail') {
318            $fieldtype = 'email';
319            $autocomp = '';
320        } else {
321            $fieldtype = 'text';
322            $autocomp = '';
323        }
324        $value = hsc($value);
325
326        echo "<tr $class>";
327        echo "<td><label for=\"$id\" >$label: </label></td>";
328        echo "<td>";
329        if ($cando) {
330            echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" value=\"$value\" class=\"edit\" $autocomp />";
331        } else {
332            echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />";
333            echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />";
334        }
335        echo "</td>";
336        echo "</tr>";
337    }
338
339    /**
340     * Returns htmlescaped filter value
341     *
342     * @param string $key name of search field
343     * @return string html escaped value
344     */
345    protected function _htmlFilter($key)
346    {
347        if (empty($this->_filter)) return '';
348        return (isset($this->_filter[$key]) ? hsc($this->_filter[$key]) : '');
349    }
350
351    /**
352     * Print hidden inputs with the current filter values
353     *
354     * @param int $indent
355     */
356    protected function _htmlFilterSettings($indent = 0)
357    {
358
359        ptln("<input type=\"hidden\" name=\"start\" value=\"" . $this->_start . "\" />", $indent);
360
361        foreach ($this->_filter as $key => $filter) {
362            ptln("<input type=\"hidden\" name=\"filter[" . $key . "]\" value=\"" . hsc($filter) . "\" />", $indent);
363        }
364    }
365
366    /**
367     * Reset user (a user has been selected to remove two factor authentication)
368     *
369     * @param string $param id of the user
370     * @return bool whether succesful
371     */
372    protected function _resetUser()
373    {
374        global $INPUT;
375        if (!checkSecurityToken()) return false;
376
377        $selected = $INPUT->arr('delete');
378        if (empty($selected)) return false;
379        $selected = array_keys($selected);
380
381        if (in_array($_SERVER['REMOTE_USER'], $selected)) {
382            msg($this->lang['reset_not_self'], -1);
383            return false;
384        }
385
386        $count = 0;
387        foreach ($selected as $user) {
388            // All users here have a attribute namespace file. Purge them.
389            $purged = $this->attribute->purge('twofactor', $user);
390            foreach ($this->modules as $mod) {
391                $purged |= $this->attribute->purge($mod->moduleName, $user);
392            }
393            $count += $purged ? 1 : 0;
394        }
395
396        if ($count == count($selected)) {
397            $text = str_replace('%d', $count, $this->lang['reset_ok']);
398            msg("$text.", 1);
399        } else {
400            $part1 = str_replace('%d', $count, $this->lang['reset_ok']);
401            $part2 = str_replace('%d', (count($selected) - $count), $this->lang['reset_fail']);
402            // Output results.
403            msg("$part1, $part2", -1);
404        }
405
406        // Now refresh the user list.
407        $this->_getUsers();
408
409        return true;
410    }
411
412    protected function _retrieveFilteredUsers($filter = array())
413    {
414        global $auth;
415        $users = array();
416        $noUsers = is_null($auth) || !$auth->canDo('getUsers');
417        foreach ($this->_user_list as $user) {
418            if ($noUsers) {
419                $userdata = array('user' => $user, 'name' => $user, 'mail' => null);
420            } else {
421                $userdata = $auth->getUserData($user);
422                if (!is_array($userdata)) {
423                    $userdata = array('user' => $user, 'name' => null, 'mail' => null);
424                }
425            }
426            $include = true;
427            foreach ($filter as $key => $value) {
428                $include &= strstr($userdata[$key], $value);
429            }
430            if ($include) {
431                $users[$user] = $userdata;
432            }
433        }
434        return $users;
435    }
436
437    protected function _getUserCount($filter)
438    {
439        return count($this->_retrieveFilteredUsers($filter));
440    }
441
442    protected function _retrieveUsers($start, $pagesize, $filter)
443    {
444        $users = $this->_retrieveFilteredUsers($filter);
445        return $users;
446    }
447
448    /**
449     * Retrieve & clean user data from the form
450     *
451     * @param bool $clean whether the cleanUser method of the authentication backend is applied
452     * @return array (user, password, full name, email, array(groups))
453     */
454    protected function _retrieveUser($clean = true)
455    {
456        /** @var DokuWiki_Auth_Plugin $auth */
457        global $auth;
458        global $INPUT;
459
460        $user = array();
461        $user[] = $INPUT->str('userid');
462        $user[] = $INPUT->str('username');
463        $user[] = $INPUT->str('usermail');
464
465        return $user;
466    }
467
468    /**
469     * Set the filter with the current search terms or clear the filter
470     *
471     * @param string $op 'new' or 'clear'
472     */
473    protected function _setFilter($op)
474    {
475
476        $this->_filter = array();
477
478        if ($op == 'new') {
479            list($user, $name, $mail) = $this->_retrieveUser();
480
481            if (!empty($user)) $this->_filter['user'] = $user;
482            if (!empty($name)) $this->_filter['name'] = $name;
483            if (!empty($mail)) $this->_filter['mail'] = $mail;
484        }
485    }
486
487    /**
488     * Get the current search terms
489     *
490     * @return array
491     */
492    protected function _retrieveFilter()
493    {
494        global $INPUT;
495
496        $t_filter = $INPUT->arr('filter');
497
498        // messy, but this way we ensure we aren't getting any additional crap from malicious users
499        $filter = array();
500
501        if (isset($t_filter['user'])) $filter['user'] = $t_filter['user'];
502        if (isset($t_filter['name'])) $filter['name'] = $t_filter['name'];
503        if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail'];
504
505        return $filter;
506    }
507
508    /**
509     * Validate and improve the pagination values
510     */
511    protected function _validatePagination()
512    {
513
514        if ($this->_start >= $this->_user_total) {
515            $this->_start = $this->_user_total - $this->_pagesize;
516        }
517        if ($this->_start < 0) $this->_start = 0;
518
519        $this->_last = min($this->_user_total, $this->_start + $this->_pagesize);
520    }
521
522    /**
523     * Return an array of strings to enable/disable pagination buttons
524     *
525     * @return array with enable/disable attributes
526     */
527    protected function _pagination()
528    {
529
530        $disabled = 'disabled="disabled"';
531
532        $buttons = array();
533        $buttons['start'] = $buttons['prev'] = ($this->_start == 0) ? $disabled : '';
534
535        if ($this->_user_total == -1) {
536            $buttons['last'] = $disabled;
537            $buttons['next'] = '';
538        } else {
539            $buttons['last'] = $buttons['next'] = (($this->_start + $this->_pagesize) >= $this->_user_total) ? $disabled : '';
540        }
541
542        if ($this->_lastdisabled) {
543            $buttons['last'] = $disabled;
544        }
545
546        return $buttons;
547    }
548
549}
550