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