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