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