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