1<?php 2/* 3 * User Manager 4 * 5 * Dokuwiki Admin Plugin 6 * 7 * This version of the user manager has been modified to only work with 8 * objectified version of auth system 9 * 10 * @author neolao <neolao@neolao.com> 11 * @author Chris Smith <chris@jalakai.co.uk> 12 */ 13// must be run within Dokuwiki 14if(!defined('DOKU_INC')) die(); 15 16if(!defined('DOKU_PLUGIN_IMAGES')) define('DOKU_PLUGIN_IMAGES',DOKU_BASE.'lib/plugins/usermanager/images/'); 17 18/** 19 * All DokuWiki plugins to extend the admin function 20 * need to inherit from this class 21 */ 22class admin_plugin_usermanager extends DokuWiki_Admin_Plugin { 23 24 protected $_auth = null; // auth object 25 protected $_user_total = 0; // number of registered users 26 protected $_filter = array(); // user selection filter(s) 27 protected $_start = 0; // index of first user to be displayed 28 protected $_last = 0; // index of the last user to be displayed 29 protected $_pagesize = 20; // number of users to list on one page 30 protected $_edit_user = ''; // set to user selected for editing 31 protected $_edit_userdata = array(); 32 protected $_disabled = ''; // if disabled set to explanatory string 33 protected $_import_failures = array(); 34 protected $_lastdisabled = false; // set to true if last user is unknown and last button is hence buggy 35 36 /** 37 * Constructor 38 */ 39 public function __construct(){ 40 /** @var DokuWiki_Auth_Plugin $auth */ 41 global $auth; 42 43 $this->setupLocale(); 44 45 if (!isset($auth)) { 46 $this->_disabled = $this->lang['noauth']; 47 } else if (!$auth->canDo('getUsers')) { 48 $this->_disabled = $this->lang['nosupport']; 49 } else { 50 51 // we're good to go 52 $this->_auth = & $auth; 53 54 } 55 56 // attempt to retrieve any import failures from the session 57 if (!empty($_SESSION['import_failures'])){ 58 $this->_import_failures = $_SESSION['import_failures']; 59 } 60 } 61 62 /** 63 * Return prompt for admin menu 64 * 65 * @param string $language 66 * @return string 67 */ 68 public function getMenuText($language) { 69 70 if (!is_null($this->_auth)) 71 return parent::getMenuText($language); 72 73 return $this->getLang('menu').' '.$this->_disabled; 74 } 75 76 /** 77 * return sort order for position in admin menu 78 * 79 * @return int 80 */ 81 public function getMenuSort() { 82 return 2; 83 } 84 85 /** 86 * @return int current start value for pageination 87 */ 88 public function getStart() { 89 return $this->_start; 90 } 91 92 /** 93 * @return int number of users per page 94 */ 95 public function getPagesize() { 96 return $this->_pagesize; 97 } 98 99 /** 100 * @param boolean $lastdisabled 101 */ 102 public function setLastdisabled($lastdisabled) { 103 $this->_lastdisabled = $lastdisabled; 104 } 105 106 /** 107 * Handle user request 108 * 109 * @return bool 110 */ 111 public function handle() { 112 global $INPUT; 113 if (is_null($this->_auth)) return false; 114 115 // extract the command and any specific parameters 116 // submit button name is of the form - fn[cmd][param(s)] 117 $fn = $INPUT->param('fn'); 118 119 if (is_array($fn)) { 120 $cmd = key($fn); 121 $param = is_array($fn[$cmd]) ? key($fn[$cmd]) : null; 122 } else { 123 $cmd = $fn; 124 $param = null; 125 } 126 127 if ($cmd != "search") { 128 $this->_start = $INPUT->int('start', 0); 129 $this->_filter = $this->_retrieveFilter(); 130 } 131 132 switch($cmd){ 133 case "add" : $this->_addUser(); break; 134 case "delete" : $this->_deleteUser(); break; 135 case "modify" : $this->_modifyUser(); break; 136 case "edit" : $this->_editUser($param); break; 137 case "search" : $this->_setFilter($param); 138 $this->_start = 0; 139 break; 140 case "export" : $this->_export(); break; 141 case "import" : $this->_import(); break; 142 case "importfails" : $this->_downloadImportFailures(); break; 143 } 144 145 $this->_user_total = $this->_auth->canDo('getUserCount') ? $this->_auth->getUserCount($this->_filter) : -1; 146 147 // page handling 148 switch($cmd){ 149 case 'start' : $this->_start = 0; break; 150 case 'prev' : $this->_start -= $this->_pagesize; break; 151 case 'next' : $this->_start += $this->_pagesize; break; 152 case 'last' : $this->_start = $this->_user_total; break; 153 } 154 $this->_validatePagination(); 155 return true; 156 } 157 158 /** 159 * Output appropriate html 160 * 161 * @return bool 162 */ 163 public function html() { 164 global $ID; 165 166 if(is_null($this->_auth)) { 167 print $this->lang['badauth']; 168 return false; 169 } 170 171 $user_list = $this->_auth->retrieveUsers($this->_start, $this->_pagesize, $this->_filter); 172 173 $page_buttons = $this->_pagination(); 174 $delete_disable = $this->_auth->canDo('delUser') ? '' : 'disabled="disabled"'; 175 176 $editable = $this->_auth->canDo('UserMod'); 177 $export_label = empty($this->_filter) ? $this->lang['export_all'] : $this->lang['export_filtered']; 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 ($this->_user_total > 0) { 186 ptln("<p>".sprintf($this->lang['summary'],$this->_start+1,$this->_last,$this->_user_total,$this->_auth->getUserCount())."</p>"); 187 } else { 188 if($this->_user_total < 0) { 189 $allUserTotal = 0; 190 } else { 191 $allUserTotal = $this->_auth->getUserCount(); 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><th>".$this->lang["user_groups"]."</th>"); 202 ptln(" </tr>"); 203 204 ptln(" <tr>"); 205 ptln(" <td class=\"rightalign\"><input type=\"image\" src=\"".DOKU_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(" <td><input type=\"text\" name=\"usergroups\" class=\"edit\" value=\"".$this->_htmlFilter('grps')."\" /></td>"); 210 ptln(" </tr>"); 211 ptln(" </thead>"); 212 213 if ($this->_user_total) { 214 ptln(" <tbody>"); 215 foreach ($user_list as $user => $userinfo) { 216 extract($userinfo); 217 /** 218 * @var string $name 219 * @var string $pass 220 * @var string $mail 221 * @var array $grps 222 */ 223 $groups = join(', ',$grps); 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><td>".hsc($groups)."</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[delete]\" id=\"usrmgr__del\" ".$delete_disable.">".$this->lang['delete_selected']."</button>"); 245 ptln(" </span>"); 246 ptln(" <span class=\"mediaright\">"); 247 ptln(" <button type=\"submit\" name=\"fn[start]\" ".$page_buttons['start'].">".$this->lang['start']."</button>"); 248 ptln(" <button type=\"submit\" name=\"fn[prev]\" ".$page_buttons['prev'].">".$this->lang['prev']."</button>"); 249 ptln(" <button type=\"submit\" name=\"fn[next]\" ".$page_buttons['next'].">".$this->lang['next']."</button>"); 250 ptln(" <button type=\"submit\" name=\"fn[last]\" ".$page_buttons['last'].">".$this->lang['last']."</button>"); 251 ptln(" </span>"); 252 if (!empty($this->_filter)) { 253 ptln(" <button type=\"submit\" name=\"fn[search][clear]\">".$this->lang['clear']."</button>"); 254 } 255 ptln(" <button type=\"submit\" name=\"fn[export]\">".$export_label."</button>"); 256 ptln(" <input type=\"hidden\" name=\"do\" value=\"admin\" />"); 257 ptln(" <input type=\"hidden\" name=\"page\" value=\"usermanager\" />"); 258 259 $this->_htmlFilterSettings(2); 260 261 ptln(" </td></tr>"); 262 ptln(" </tbody>"); 263 ptln(" </table>"); 264 ptln(" </div>"); 265 266 ptln("</form>"); 267 ptln("</div>"); 268 269 $style = $this->_edit_user ? " class=\"edit_user\"" : ""; 270 271 if ($this->_auth->canDo('addUser')) { 272 ptln("<div".$style.">"); 273 print $this->locale_xhtml('add'); 274 ptln(" <div class=\"level2\">"); 275 276 $this->_htmlUserForm('add',null,array(),4); 277 278 ptln(" </div>"); 279 ptln("</div>"); 280 } 281 282 if($this->_edit_user && $this->_auth->canDo('UserMod')){ 283 ptln("<div".$style." id=\"scroll__here\">"); 284 print $this->locale_xhtml('edit'); 285 ptln(" <div class=\"level2\">"); 286 287 $this->_htmlUserForm('modify',$this->_edit_user,$this->_edit_userdata,4); 288 289 ptln(" </div>"); 290 ptln("</div>"); 291 } 292 293 if ($this->_auth->canDo('addUser')) { 294 $this->_htmlImportForm(); 295 } 296 ptln("</div>"); 297 return true; 298 } 299 300 /** 301 * User Manager is only available if the auth backend supports it 302 * 303 * @inheritdoc 304 * @return bool 305 */ 306 public function isAccessibleByCurrentUser() 307 { 308 /** @var DokuWiki_Auth_Plugin $auth */ 309 global $auth; 310 if(!$auth || !$auth->canDo('getUsers') ) { 311 return false; 312 } 313 314 return parent::isAccessibleByCurrentUser(); 315 } 316 317 318 /** 319 * Display form to add or modify a user 320 * 321 * @param string $cmd 'add' or 'modify' 322 * @param string $user id of user 323 * @param array $userdata array with name, mail, pass and grps 324 * @param int $indent 325 */ 326 protected function _htmlUserForm($cmd,$user='',$userdata=array(),$indent=0) { 327 global $conf; 328 global $ID; 329 global $lang; 330 331 $name = $mail = $groups = ''; 332 $notes = array(); 333 334 if ($user) { 335 extract($userdata); 336 if (!empty($grps)) $groups = join(',',$grps); 337 } else { 338 $notes[] = sprintf($this->lang['note_group'],$conf['defaultgroup']); 339 } 340 341 ptln("<form action=\"".wl($ID)."\" method=\"post\">",$indent); 342 formSecurityToken(); 343 ptln(" <div class=\"table\">",$indent); 344 ptln(" <table class=\"inline\">",$indent); 345 ptln(" <thead>",$indent); 346 ptln(" <tr><th>".$this->lang["field"]."</th><th>".$this->lang["value"]."</th></tr>",$indent); 347 ptln(" </thead>",$indent); 348 ptln(" <tbody>",$indent); 349 350 $this->_htmlInputField($cmd."_userid", "userid", $this->lang["user_id"], $user, $this->_auth->canDo("modLogin"), true, $indent+6); 351 $this->_htmlInputField($cmd."_userpass", "userpass", $this->lang["user_pass"], "", $this->_auth->canDo("modPass"), false, $indent+6); 352 $this->_htmlInputField($cmd."_userpass2", "userpass2", $lang["passchk"], "", $this->_auth->canDo("modPass"), false, $indent+6); 353 $this->_htmlInputField($cmd."_username", "username", $this->lang["user_name"], $name, $this->_auth->canDo("modName"), true, $indent+6); 354 $this->_htmlInputField($cmd."_usermail", "usermail", $this->lang["user_mail"], $mail, $this->_auth->canDo("modMail"), true, $indent+6); 355 $this->_htmlInputField($cmd."_usergroups","usergroups",$this->lang["user_groups"],$groups,$this->_auth->canDo("modGroups"), false, $indent+6); 356 357 if ($this->_auth->canDo("modPass")) { 358 if ($cmd == 'add') { 359 $notes[] = $this->lang['note_pass']; 360 } 361 if ($user) { 362 $notes[] = $this->lang['note_notify']; 363 } 364 365 ptln("<tr><td><label for=\"".$cmd."_usernotify\" >".$this->lang["user_notify"].": </label></td><td><input type=\"checkbox\" id=\"".$cmd."_usernotify\" name=\"usernotify\" value=\"1\" /></td></tr>", $indent); 366 } 367 368 ptln(" </tbody>",$indent); 369 ptln(" <tbody>",$indent); 370 ptln(" <tr>",$indent); 371 ptln(" <td colspan=\"2\">",$indent); 372 ptln(" <input type=\"hidden\" name=\"do\" value=\"admin\" />",$indent); 373 ptln(" <input type=\"hidden\" name=\"page\" value=\"usermanager\" />",$indent); 374 375 // save current $user, we need this to access details if the name is changed 376 if ($user) 377 ptln(" <input type=\"hidden\" name=\"userid_old\" value=\"".hsc($user)."\" />",$indent); 378 379 $this->_htmlFilterSettings($indent+10); 380 381 ptln(" <button type=\"submit\" name=\"fn[".$cmd."]\">".$this->lang[$cmd]."</button>",$indent); 382 ptln(" </td>",$indent); 383 ptln(" </tr>",$indent); 384 ptln(" </tbody>",$indent); 385 ptln(" </table>",$indent); 386 387 if ($notes) { 388 ptln(" <ul class=\"notes\">"); 389 foreach ($notes as $note) { 390 ptln(" <li><span class=\"li\">".$note."</li>",$indent); 391 } 392 ptln(" </ul>"); 393 } 394 ptln(" </div>",$indent); 395 ptln("</form>",$indent); 396 } 397 398 /** 399 * Prints a inputfield 400 * 401 * @param string $id 402 * @param string $name 403 * @param string $label 404 * @param string $value 405 * @param bool $cando whether auth backend is capable to do this action 406 * @param bool $required is this field required? 407 * @param int $indent 408 */ 409 protected function _htmlInputField($id, $name, $label, $value, $cando, $required, $indent=0) { 410 $class = $cando ? '' : ' class="disabled"'; 411 echo str_pad('',$indent); 412 413 if($name == 'userpass' || $name == 'userpass2'){ 414 $fieldtype = 'password'; 415 $autocomp = 'autocomplete="off"'; 416 }elseif($name == 'usermail'){ 417 $fieldtype = 'email'; 418 $autocomp = ''; 419 }else{ 420 $fieldtype = 'text'; 421 $autocomp = ''; 422 } 423 $value = hsc($value); 424 425 echo "<tr $class>"; 426 echo "<td><label for=\"$id\" >$label: </label></td>"; 427 echo "<td>"; 428 if($cando){ 429 $req = ''; 430 if($required) $req = 'required="required"'; 431 echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" value=\"$value\" class=\"edit\" $autocomp $req />"; 432 }else{ 433 echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />"; 434 echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />"; 435 } 436 echo "</td>"; 437 echo "</tr>"; 438 } 439 440 /** 441 * Returns htmlescaped filter value 442 * 443 * @param string $key name of search field 444 * @return string html escaped value 445 */ 446 protected function _htmlFilter($key) { 447 if (empty($this->_filter)) return ''; 448 return (isset($this->_filter[$key]) ? hsc($this->_filter[$key]) : ''); 449 } 450 451 /** 452 * Print hidden inputs with the current filter values 453 * 454 * @param int $indent 455 */ 456 protected function _htmlFilterSettings($indent=0) { 457 458 ptln("<input type=\"hidden\" name=\"start\" value=\"".$this->_start."\" />",$indent); 459 460 foreach ($this->_filter as $key => $filter) { 461 ptln("<input type=\"hidden\" name=\"filter[".$key."]\" value=\"".hsc($filter)."\" />",$indent); 462 } 463 } 464 465 /** 466 * Print import form and summary of previous import 467 * 468 * @param int $indent 469 */ 470 protected function _htmlImportForm($indent=0) { 471 global $ID; 472 473 $failure_download_link = wl($ID,array('do'=>'admin','page'=>'usermanager','fn[importfails]'=>1)); 474 475 ptln('<div class="level2 import_users">',$indent); 476 print $this->locale_xhtml('import'); 477 ptln(' <form action="'.wl($ID).'" method="post" enctype="multipart/form-data">',$indent); 478 formSecurityToken(); 479 ptln(' <label>'.$this->lang['import_userlistcsv'].'<input type="file" name="import" /></label>',$indent); 480 ptln(' <button type="submit" name="fn[import]">'.$this->lang['import'].'</button>',$indent); 481 ptln(' <input type="hidden" name="do" value="admin" />',$indent); 482 ptln(' <input type="hidden" name="page" value="usermanager" />',$indent); 483 484 $this->_htmlFilterSettings($indent+4); 485 ptln(' </form>',$indent); 486 ptln('</div>'); 487 488 // list failures from the previous import 489 if ($this->_import_failures) { 490 $digits = strlen(count($this->_import_failures)); 491 ptln('<div class="level3 import_failures">',$indent); 492 ptln(' <h3>'.$this->lang['import_header'].'</h3>'); 493 ptln(' <table class="import_failures">',$indent); 494 ptln(' <thead>',$indent); 495 ptln(' <tr>',$indent); 496 ptln(' <th class="line">'.$this->lang['line'].'</th>',$indent); 497 ptln(' <th class="error">'.$this->lang['error'].'</th>',$indent); 498 ptln(' <th class="userid">'.$this->lang['user_id'].'</th>',$indent); 499 ptln(' <th class="username">'.$this->lang['user_name'].'</th>',$indent); 500 ptln(' <th class="usermail">'.$this->lang['user_mail'].'</th>',$indent); 501 ptln(' <th class="usergroups">'.$this->lang['user_groups'].'</th>',$indent); 502 ptln(' </tr>',$indent); 503 ptln(' </thead>',$indent); 504 ptln(' <tbody>',$indent); 505 foreach ($this->_import_failures as $line => $failure) { 506 ptln(' <tr>',$indent); 507 ptln(' <td class="lineno"> '.sprintf('%0'.$digits.'d',$line).' </td>',$indent); 508 ptln(' <td class="error">' .$failure['error'].' </td>', $indent); 509 ptln(' <td class="field userid"> '.hsc($failure['user'][0]).' </td>',$indent); 510 ptln(' <td class="field username"> '.hsc($failure['user'][2]).' </td>',$indent); 511 ptln(' <td class="field usermail"> '.hsc($failure['user'][3]).' </td>',$indent); 512 ptln(' <td class="field usergroups"> '.hsc($failure['user'][4]).' </td>',$indent); 513 ptln(' </tr>',$indent); 514 } 515 ptln(' </tbody>',$indent); 516 ptln(' </table>',$indent); 517 ptln(' <p><a href="'.$failure_download_link.'">'.$this->lang['import_downloadfailures'].'</a></p>'); 518 ptln('</div>'); 519 } 520 521 } 522 523 /** 524 * Add an user to auth backend 525 * 526 * @return bool whether succesful 527 */ 528 protected function _addUser(){ 529 global $INPUT; 530 if (!checkSecurityToken()) return false; 531 if (!$this->_auth->canDo('addUser')) return false; 532 533 list($user,$pass,$name,$mail,$grps,$passconfirm) = $this->_retrieveUser(); 534 if (empty($user)) return false; 535 536 if ($this->_auth->canDo('modPass')){ 537 if (empty($pass)){ 538 if($INPUT->has('usernotify')){ 539 $pass = auth_pwgen($user); 540 } else { 541 msg($this->lang['add_fail'], -1); 542 msg($this->lang['addUser_error_missing_pass'], -1); 543 return false; 544 } 545 } else { 546 if (!$this->_verifyPassword($pass,$passconfirm)) { 547 msg($this->lang['add_fail'], -1); 548 msg($this->lang['addUser_error_pass_not_identical'], -1); 549 return false; 550 } 551 } 552 } else { 553 if (!empty($pass)){ 554 msg($this->lang['add_fail'], -1); 555 msg($this->lang['addUser_error_modPass_disabled'], -1); 556 return false; 557 } 558 } 559 560 if ($this->_auth->canDo('modName')){ 561 if (empty($name)){ 562 msg($this->lang['add_fail'], -1); 563 msg($this->lang['addUser_error_name_missing'], -1); 564 return false; 565 } 566 } else { 567 if (!empty($name)){ 568 msg($this->lang['add_fail'], -1); 569 msg($this->lang['addUser_error_modName_disabled'], -1); 570 return false; 571 } 572 } 573 574 if ($this->_auth->canDo('modMail')){ 575 if (empty($mail)){ 576 msg($this->lang['add_fail'], -1); 577 msg($this->lang['addUser_error_mail_missing'], -1); 578 return false; 579 } 580 } else { 581 if (!empty($mail)){ 582 msg($this->lang['add_fail'], -1); 583 msg($this->lang['addUser_error_modMail_disabled'], -1); 584 return false; 585 } 586 } 587 588 if ($ok = $this->_auth->triggerUserMod('create', array($user,$pass,$name,$mail,$grps))) { 589 590 msg($this->lang['add_ok'], 1); 591 592 if ($INPUT->has('usernotify') && $pass) { 593 $this->_notifyUser($user,$pass); 594 } 595 } else { 596 msg($this->lang['add_fail'], -1); 597 msg($this->lang['addUser_error_create_event_failed'], -1); 598 } 599 600 return $ok; 601 } 602 603 /** 604 * Delete user from auth backend 605 * 606 * @return bool whether succesful 607 */ 608 protected function _deleteUser(){ 609 global $conf, $INPUT; 610 611 if (!checkSecurityToken()) return false; 612 if (!$this->_auth->canDo('delUser')) return false; 613 614 $selected = $INPUT->arr('delete'); 615 if (empty($selected)) return false; 616 $selected = array_keys($selected); 617 618 if(in_array($_SERVER['REMOTE_USER'], $selected)) { 619 msg("You can't delete yourself!", -1); 620 return false; 621 } 622 623 $count = $this->_auth->triggerUserMod('delete', array($selected)); 624 if ($count == count($selected)) { 625 $text = str_replace('%d', $count, $this->lang['delete_ok']); 626 msg("$text.", 1); 627 } else { 628 $part1 = str_replace('%d', $count, $this->lang['delete_ok']); 629 $part2 = str_replace('%d', (count($selected)-$count), $this->lang['delete_fail']); 630 msg("$part1, $part2",-1); 631 } 632 633 // invalidate all sessions 634 io_saveFile($conf['cachedir'].'/sessionpurge',time()); 635 636 return true; 637 } 638 639 /** 640 * Edit user (a user has been selected for editing) 641 * 642 * @param string $param id of the user 643 * @return bool whether succesful 644 */ 645 protected function _editUser($param) { 646 if (!checkSecurityToken()) return false; 647 if (!$this->_auth->canDo('UserMod')) return false; 648 $user = $this->_auth->cleanUser(preg_replace('/.*[:\/]/','',$param)); 649 $userdata = $this->_auth->getUserData($user); 650 651 // no user found? 652 if (!$userdata) { 653 msg($this->lang['edit_usermissing'],-1); 654 return false; 655 } 656 657 $this->_edit_user = $user; 658 $this->_edit_userdata = $userdata; 659 660 return true; 661 } 662 663 /** 664 * Modify user in the auth backend (modified user data has been recieved) 665 * 666 * @return bool whether succesful 667 */ 668 protected function _modifyUser(){ 669 global $conf, $INPUT; 670 671 if (!checkSecurityToken()) return false; 672 if (!$this->_auth->canDo('UserMod')) return false; 673 674 // get currently valid user data 675 $olduser = $this->_auth->cleanUser(preg_replace('/.*[:\/]/','',$INPUT->str('userid_old'))); 676 $oldinfo = $this->_auth->getUserData($olduser); 677 678 // get new user data subject to change 679 list($newuser,$newpass,$newname,$newmail,$newgrps,$passconfirm) = $this->_retrieveUser(); 680 if (empty($newuser)) return false; 681 682 $changes = array(); 683 if ($newuser != $olduser) { 684 685 if (!$this->_auth->canDo('modLogin')) { // sanity check, shouldn't be possible 686 msg($this->lang['update_fail'],-1); 687 return false; 688 } 689 690 // check if $newuser already exists 691 if ($this->_auth->getUserData($newuser)) { 692 msg(sprintf($this->lang['update_exists'],$newuser),-1); 693 $re_edit = true; 694 } else { 695 $changes['user'] = $newuser; 696 } 697 } 698 if ($this->_auth->canDo('modPass')) { 699 if ($newpass || $passconfirm) { 700 if ($this->_verifyPassword($newpass,$passconfirm)) { 701 $changes['pass'] = $newpass; 702 } else { 703 return false; 704 } 705 } else { 706 // no new password supplied, check if we need to generate one (or it stays unchanged) 707 if ($INPUT->has('usernotify')) { 708 $changes['pass'] = auth_pwgen($olduser); 709 } 710 } 711 } 712 713 if (!empty($newname) && $this->_auth->canDo('modName') && $newname != $oldinfo['name']) { 714 $changes['name'] = $newname; 715 } 716 if (!empty($newmail) && $this->_auth->canDo('modMail') && $newmail != $oldinfo['mail']) { 717 $changes['mail'] = $newmail; 718 } 719 if (!empty($newgrps) && $this->_auth->canDo('modGroups') && $newgrps != $oldinfo['grps']) { 720 $changes['grps'] = $newgrps; 721 } 722 723 if ($ok = $this->_auth->triggerUserMod('modify', array($olduser, $changes))) { 724 msg($this->lang['update_ok'],1); 725 726 if ($INPUT->has('usernotify') && !empty($changes['pass'])) { 727 $notify = empty($changes['user']) ? $olduser : $newuser; 728 $this->_notifyUser($notify,$changes['pass']); 729 } 730 731 // invalidate all sessions 732 io_saveFile($conf['cachedir'].'/sessionpurge',time()); 733 734 } else { 735 msg($this->lang['update_fail'],-1); 736 } 737 738 if (!empty($re_edit)) { 739 $this->_editUser($olduser); 740 } 741 742 return $ok; 743 } 744 745 /** 746 * Send password change notification email 747 * 748 * @param string $user id of user 749 * @param string $password plain text 750 * @param bool $status_alert whether status alert should be shown 751 * @return bool whether succesful 752 */ 753 protected function _notifyUser($user, $password, $status_alert=true) { 754 755 if ($sent = auth_sendPassword($user,$password)) { 756 if ($status_alert) { 757 msg($this->lang['notify_ok'], 1); 758 } 759 } else { 760 if ($status_alert) { 761 msg($this->lang['notify_fail'], -1); 762 } 763 } 764 765 return $sent; 766 } 767 768 /** 769 * Verify password meets minimum requirements 770 * :TODO: extend to support password strength 771 * 772 * @param string $password candidate string for new password 773 * @param string $confirm repeated password for confirmation 774 * @return bool true if meets requirements, false otherwise 775 */ 776 protected function _verifyPassword($password, $confirm) { 777 global $lang; 778 779 if (empty($password) && empty($confirm)) { 780 return false; 781 } 782 783 if ($password !== $confirm) { 784 msg($lang['regbadpass'], -1); 785 return false; 786 } 787 788 // :TODO: test password for required strength 789 790 // if we make it this far the password is good 791 return true; 792 } 793 794 /** 795 * Retrieve & clean user data from the form 796 * 797 * @param bool $clean whether the cleanUser method of the authentication backend is applied 798 * @return array (user, password, full name, email, array(groups)) 799 */ 800 protected function _retrieveUser($clean=true) { 801 /** @var DokuWiki_Auth_Plugin $auth */ 802 global $auth; 803 global $INPUT; 804 805 $user = array(); 806 $user[0] = ($clean) ? $auth->cleanUser($INPUT->str('userid')) : $INPUT->str('userid'); 807 $user[1] = $INPUT->str('userpass'); 808 $user[2] = $INPUT->str('username'); 809 $user[3] = $INPUT->str('usermail'); 810 $user[4] = explode(',',$INPUT->str('usergroups')); 811 $user[5] = $INPUT->str('userpass2'); // repeated password for confirmation 812 813 $user[4] = array_map('trim',$user[4]); 814 if($clean) $user[4] = array_map(array($auth,'cleanGroup'),$user[4]); 815 $user[4] = array_filter($user[4]); 816 $user[4] = array_unique($user[4]); 817 if(!count($user[4])) $user[4] = null; 818 819 return $user; 820 } 821 822 /** 823 * Set the filter with the current search terms or clear the filter 824 * 825 * @param string $op 'new' or 'clear' 826 */ 827 protected function _setFilter($op) { 828 829 $this->_filter = array(); 830 831 if ($op == 'new') { 832 list($user,/* $pass */,$name,$mail,$grps) = $this->_retrieveUser(false); 833 834 if (!empty($user)) $this->_filter['user'] = $user; 835 if (!empty($name)) $this->_filter['name'] = $name; 836 if (!empty($mail)) $this->_filter['mail'] = $mail; 837 if (!empty($grps)) $this->_filter['grps'] = join('|',$grps); 838 } 839 } 840 841 /** 842 * Get the current search terms 843 * 844 * @return array 845 */ 846 protected function _retrieveFilter() { 847 global $INPUT; 848 849 $t_filter = $INPUT->arr('filter'); 850 851 // messy, but this way we ensure we aren't getting any additional crap from malicious users 852 $filter = array(); 853 854 if (isset($t_filter['user'])) $filter['user'] = $t_filter['user']; 855 if (isset($t_filter['name'])) $filter['name'] = $t_filter['name']; 856 if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail']; 857 if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps']; 858 859 return $filter; 860 } 861 862 /** 863 * Validate and improve the pagination values 864 */ 865 protected function _validatePagination() { 866 867 if ($this->_start >= $this->_user_total) { 868 $this->_start = $this->_user_total - $this->_pagesize; 869 } 870 if ($this->_start < 0) $this->_start = 0; 871 872 $this->_last = min($this->_user_total, $this->_start + $this->_pagesize); 873 } 874 875 /** 876 * Return an array of strings to enable/disable pagination buttons 877 * 878 * @return array with enable/disable attributes 879 */ 880 protected function _pagination() { 881 882 $disabled = 'disabled="disabled"'; 883 884 $buttons = array(); 885 $buttons['start'] = $buttons['prev'] = ($this->_start == 0) ? $disabled : ''; 886 887 if ($this->_user_total == -1) { 888 $buttons['last'] = $disabled; 889 $buttons['next'] = ''; 890 } else { 891 $buttons['last'] = $buttons['next'] = (($this->_start + $this->_pagesize) >= $this->_user_total) ? $disabled : ''; 892 } 893 894 if ($this->_lastdisabled) { 895 $buttons['last'] = $disabled; 896 } 897 898 return $buttons; 899 } 900 901 /** 902 * Export a list of users in csv format using the current filter criteria 903 */ 904 protected function _export() { 905 // list of users for export - based on current filter criteria 906 $user_list = $this->_auth->retrieveUsers(0, 0, $this->_filter); 907 $column_headings = array( 908 $this->lang["user_id"], 909 $this->lang["user_name"], 910 $this->lang["user_mail"], 911 $this->lang["user_groups"] 912 ); 913 914 // ============================================================================================== 915 // GENERATE OUTPUT 916 // normal headers for downloading... 917 header('Content-type: text/csv;charset=utf-8'); 918 header('Content-Disposition: attachment; filename="wikiusers.csv"'); 919# // for debugging assistance, send as text plain to the browser 920# header('Content-type: text/plain;charset=utf-8'); 921 922 // output the csv 923 $fd = fopen('php://output','w'); 924 fputcsv($fd, $column_headings); 925 foreach ($user_list as $user => $info) { 926 $line = array($user, $info['name'], $info['mail'], join(',',$info['grps'])); 927 fputcsv($fd, $line); 928 } 929 fclose($fd); 930 if (defined('DOKU_UNITTEST')){ return; } 931 932 die; 933 } 934 935 /** 936 * Import a file of users in csv format 937 * 938 * csv file should have 4 columns, user_id, full name, email, groups (comma separated) 939 * 940 * @return bool whether successful 941 */ 942 protected function _import() { 943 // check we are allowed to add users 944 if (!checkSecurityToken()) return false; 945 if (!$this->_auth->canDo('addUser')) return false; 946 947 // check file uploaded ok. 948 if (empty($_FILES['import']['size']) || !empty($_FILES['import']['error']) && $this->_isUploadedFile($_FILES['import']['tmp_name'])) { 949 msg($this->lang['import_error_upload'],-1); 950 return false; 951 } 952 // retrieve users from the file 953 $this->_import_failures = array(); 954 $import_success_count = 0; 955 $import_fail_count = 0; 956 $line = 0; 957 $fd = fopen($_FILES['import']['tmp_name'],'r'); 958 if ($fd) { 959 while($csv = fgets($fd)){ 960 if (!utf8_check($csv)) { 961 $csv = utf8_encode($csv); 962 } 963 $raw = str_getcsv($csv); 964 $error = ''; // clean out any errors from the previous line 965 // data checks... 966 if (1 == ++$line) { 967 if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue; // skip headers 968 } 969 if (count($raw) < 4) { // need at least four fields 970 $import_fail_count++; 971 $error = sprintf($this->lang['import_error_fields'], count($raw)); 972 $this->_import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv); 973 continue; 974 } 975 array_splice($raw,1,0,auth_pwgen()); // splice in a generated password 976 $clean = $this->_cleanImportUser($raw, $error); 977 if ($clean && $this->_addImportUser($clean, $error)) { 978 $sent = $this->_notifyUser($clean[0],$clean[1],false); 979 if (!$sent){ 980 msg(sprintf($this->lang['import_notify_fail'],$clean[0],$clean[3]),-1); 981 } 982 $import_success_count++; 983 } else { 984 $import_fail_count++; 985 array_splice($raw, 1, 1); // remove the spliced in password 986 $this->_import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv); 987 } 988 } 989 msg(sprintf($this->lang['import_success_count'], ($import_success_count+$import_fail_count), $import_success_count),($import_success_count ? 1 : -1)); 990 if ($import_fail_count) { 991 msg(sprintf($this->lang['import_failure_count'], $import_fail_count),-1); 992 } 993 } else { 994 msg($this->lang['import_error_readfail'],-1); 995 } 996 997 // save import failures into the session 998 if (!headers_sent()) { 999 session_start(); 1000 $_SESSION['import_failures'] = $this->_import_failures; 1001 session_write_close(); 1002 } 1003 return true; 1004 } 1005 1006 /** 1007 * Returns cleaned user data 1008 * 1009 * @param array $candidate raw values of line from input file 1010 * @param string $error 1011 * @return array|false cleaned data or false 1012 */ 1013 protected function _cleanImportUser($candidate, & $error){ 1014 global $INPUT; 1015 1016 // kludgy .... 1017 $INPUT->set('userid', $candidate[0]); 1018 $INPUT->set('userpass', $candidate[1]); 1019 $INPUT->set('username', $candidate[2]); 1020 $INPUT->set('usermail', $candidate[3]); 1021 $INPUT->set('usergroups', $candidate[4]); 1022 1023 $cleaned = $this->_retrieveUser(); 1024 list($user,/* $pass */,$name,$mail,/* $grps */) = $cleaned; 1025 if (empty($user)) { 1026 $error = $this->lang['import_error_baduserid']; 1027 return false; 1028 } 1029 1030 // no need to check password, handled elsewhere 1031 1032 if (!($this->_auth->canDo('modName') xor empty($name))){ 1033 $error = $this->lang['import_error_badname']; 1034 return false; 1035 } 1036 1037 if ($this->_auth->canDo('modMail')) { 1038 if (empty($mail) || !mail_isvalid($mail)) { 1039 $error = $this->lang['import_error_badmail']; 1040 return false; 1041 } 1042 } else { 1043 if (!empty($mail)) { 1044 $error = $this->lang['import_error_badmail']; 1045 return false; 1046 } 1047 } 1048 1049 return $cleaned; 1050 } 1051 1052 /** 1053 * Adds imported user to auth backend 1054 * 1055 * Required a check of canDo('addUser') before 1056 * 1057 * @param array $user data of user 1058 * @param string &$error reference catched error message 1059 * @return bool whether successful 1060 */ 1061 protected function _addImportUser($user, & $error){ 1062 if (!$this->_auth->triggerUserMod('create', $user)) { 1063 $error = $this->lang['import_error_create']; 1064 return false; 1065 } 1066 1067 return true; 1068 } 1069 1070 /** 1071 * Downloads failures as csv file 1072 */ 1073 protected function _downloadImportFailures(){ 1074 1075 // ============================================================================================== 1076 // GENERATE OUTPUT 1077 // normal headers for downloading... 1078 header('Content-type: text/csv;charset=utf-8'); 1079 header('Content-Disposition: attachment; filename="importfails.csv"'); 1080# // for debugging assistance, send as text plain to the browser 1081# header('Content-type: text/plain;charset=utf-8'); 1082 1083 // output the csv 1084 $fd = fopen('php://output','w'); 1085 foreach ($this->_import_failures as $fail) { 1086 fputs($fd, $fail['orig']); 1087 } 1088 fclose($fd); 1089 die; 1090 } 1091 1092 /** 1093 * wrapper for is_uploaded_file to facilitate overriding by test suite 1094 * 1095 * @param string $file filename 1096 * @return bool 1097 */ 1098 protected function _isUploadedFile($file) { 1099 return is_uploaded_file($file); 1100 } 1101} 1102