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