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