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