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