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