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