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