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