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