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-21', 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> </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