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