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