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