[ Index ] |
PHP Cross Reference of DokuWiki |
[Summary view] [Print] [Text view]
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 */ 18 class 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 * User Manager is only available if the auth backend supports it 353 * 354 * @inheritdoc 355 * @return bool 356 */ 357 public function isAccessibleByCurrentUser() 358 { 359 /** @var DokuWiki_Auth_Plugin $auth */ 360 global $auth; 361 if(!$auth || !$auth->canDo('getUsers') ) { 362 return false; 363 } 364 365 return parent::isAccessibleByCurrentUser(); 366 } 367 368 369 /** 370 * Display form to add or modify a user 371 * 372 * @param string $cmd 'add' or 'modify' 373 * @param string $user id of user 374 * @param array $userdata array with name, mail, pass and grps 375 * @param int $indent 376 */ 377 protected function htmlUserForm($cmd, $user = '', $userdata = array(), $indent = 0) 378 { 379 global $conf; 380 global $ID; 381 global $lang; 382 383 $name = $mail = $groups = ''; 384 $notes = array(); 385 386 if ($user) { 387 extract($userdata); 388 if (!empty($grps)) $groups = join(',', $grps); 389 } else { 390 $notes[] = sprintf($this->lang['note_group'], $conf['defaultgroup']); 391 } 392 393 ptln("<form action=\"".wl($ID)."\" method=\"post\">", $indent); 394 formSecurityToken(); 395 ptln(" <div class=\"table\">", $indent); 396 ptln(" <table class=\"inline\">", $indent); 397 ptln(" <thead>", $indent); 398 ptln(" <tr><th>".$this->lang["field"]."</th><th>".$this->lang["value"]."</th></tr>", $indent); 399 ptln(" </thead>", $indent); 400 ptln(" <tbody>", $indent); 401 402 $this->htmlInputField( 403 $cmd . "_userid", 404 "userid", 405 $this->lang["user_id"], 406 $user, 407 $this->auth->canDo("modLogin"), 408 true, 409 $indent + 6 410 ); 411 $this->htmlInputField( 412 $cmd . "_userpass", 413 "userpass", 414 $this->lang["user_pass"], 415 "", 416 $this->auth->canDo("modPass"), 417 false, 418 $indent + 6 419 ); 420 $this->htmlInputField( 421 $cmd . "_userpass2", 422 "userpass2", 423 $lang["passchk"], 424 "", 425 $this->auth->canDo("modPass"), 426 false, 427 $indent + 6 428 ); 429 $this->htmlInputField( 430 $cmd . "_username", 431 "username", 432 $this->lang["user_name"], 433 $name, 434 $this->auth->canDo("modName"), 435 true, 436 $indent + 6 437 ); 438 $this->htmlInputField( 439 $cmd . "_usermail", 440 "usermail", 441 $this->lang["user_mail"], 442 $mail, 443 $this->auth->canDo("modMail"), 444 true, 445 $indent + 6 446 ); 447 $this->htmlInputField( 448 $cmd . "_usergroups", 449 "usergroups", 450 $this->lang["user_groups"], 451 $groups, 452 $this->auth->canDo("modGroups"), 453 false, 454 $indent + 6 455 ); 456 457 if ($this->auth->canDo("modPass")) { 458 if ($cmd == 'add') { 459 $notes[] = $this->lang['note_pass']; 460 } 461 if ($user) { 462 $notes[] = $this->lang['note_notify']; 463 } 464 465 ptln("<tr><td><label for=\"".$cmd."_usernotify\" >". 466 $this->lang["user_notify"].": </label></td> 467 <td><input type=\"checkbox\" id=\"".$cmd."_usernotify\" name=\"usernotify\" value=\"1\" /> 468 </td></tr>", $indent); 469 } 470 471 ptln(" </tbody>", $indent); 472 ptln(" <tbody>", $indent); 473 ptln(" <tr>", $indent); 474 ptln(" <td colspan=\"2\">", $indent); 475 ptln(" <input type=\"hidden\" name=\"do\" value=\"admin\" />", $indent); 476 ptln(" <input type=\"hidden\" name=\"page\" value=\"usermanager\" />", $indent); 477 478 // save current $user, we need this to access details if the name is changed 479 if ($user) 480 ptln(" <input type=\"hidden\" name=\"userid_old\" value=\"".hsc($user)."\" />", $indent); 481 482 $this->htmlFilterSettings($indent+10); 483 484 ptln(" <button type=\"submit\" name=\"fn[".$cmd."]\">".$this->lang[$cmd]."</button>", $indent); 485 ptln(" </td>", $indent); 486 ptln(" </tr>", $indent); 487 ptln(" </tbody>", $indent); 488 ptln(" </table>", $indent); 489 490 if ($notes) { 491 ptln(" <ul class=\"notes\">"); 492 foreach ($notes as $note) { 493 ptln(" <li><span class=\"li\">".$note."</li>", $indent); 494 } 495 ptln(" </ul>"); 496 } 497 ptln(" </div>", $indent); 498 ptln("</form>", $indent); 499 } 500 501 /** 502 * Prints a inputfield 503 * 504 * @param string $id 505 * @param string $name 506 * @param string $label 507 * @param string $value 508 * @param bool $cando whether auth backend is capable to do this action 509 * @param bool $required is this field required? 510 * @param int $indent 511 */ 512 protected function htmlInputField($id, $name, $label, $value, $cando, $required, $indent = 0) 513 { 514 $class = $cando ? '' : ' class="disabled"'; 515 echo str_pad('', $indent); 516 517 if ($name == 'userpass' || $name == 'userpass2') { 518 $fieldtype = 'password'; 519 $autocomp = 'autocomplete="off"'; 520 } elseif ($name == 'usermail') { 521 $fieldtype = 'email'; 522 $autocomp = ''; 523 } else { 524 $fieldtype = 'text'; 525 $autocomp = ''; 526 } 527 $value = hsc($value); 528 529 echo "<tr $class>"; 530 echo "<td><label for=\"$id\" >$label: </label></td>"; 531 echo "<td>"; 532 if ($cando) { 533 $req = ''; 534 if ($required) $req = 'required="required"'; 535 echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" 536 value=\"$value\" class=\"edit\" $autocomp $req />"; 537 } else { 538 echo "<input type=\"hidden\" name=\"$name\" value=\"$value\" />"; 539 echo "<input type=\"$fieldtype\" id=\"$id\" name=\"$name\" 540 value=\"$value\" class=\"edit disabled\" disabled=\"disabled\" />"; 541 } 542 echo "</td>"; 543 echo "</tr>"; 544 } 545 546 /** 547 * Returns htmlescaped filter value 548 * 549 * @param string $key name of search field 550 * @return string html escaped value 551 */ 552 protected function htmlFilter($key) 553 { 554 if (empty($this->filter)) return ''; 555 return (isset($this->filter[$key]) ? hsc($this->filter[$key]) : ''); 556 } 557 558 /** 559 * Print hidden inputs with the current filter values 560 * 561 * @param int $indent 562 */ 563 protected function htmlFilterSettings($indent = 0) 564 { 565 566 ptln("<input type=\"hidden\" name=\"start\" value=\"".$this->start."\" />", $indent); 567 568 foreach ($this->filter as $key => $filter) { 569 ptln("<input type=\"hidden\" name=\"filter[".$key."]\" value=\"".hsc($filter)."\" />", $indent); 570 } 571 } 572 573 /** 574 * Print import form and summary of previous import 575 * 576 * @param int $indent 577 */ 578 protected function htmlImportForm($indent = 0) 579 { 580 global $ID; 581 582 $failure_download_link = wl($ID, array('do'=>'admin','page'=>'usermanager','fn[importfails]'=>1)); 583 584 ptln('<div class="level2 import_users">', $indent); 585 print $this->locale_xhtml('import'); 586 ptln(' <form action="'.wl($ID).'" method="post" enctype="multipart/form-data">', $indent); 587 formSecurityToken(); 588 ptln(' <label>'.$this->lang['import_userlistcsv'].'<input type="file" name="import" /></label>', $indent); 589 ptln(' <button type="submit" name="fn[import]">'.$this->lang['import'].'</button>', $indent); 590 ptln(' <input type="hidden" name="do" value="admin" />', $indent); 591 ptln(' <input type="hidden" name="page" value="usermanager" />', $indent); 592 593 $this->htmlFilterSettings($indent+4); 594 ptln(' </form>', $indent); 595 ptln('</div>'); 596 597 // list failures from the previous import 598 if ($this->import_failures) { 599 $digits = strlen(count($this->import_failures)); 600 ptln('<div class="level3 import_failures">', $indent); 601 ptln(' <h3>'.$this->lang['import_header'].'</h3>'); 602 ptln(' <table class="import_failures">', $indent); 603 ptln(' <thead>', $indent); 604 ptln(' <tr>', $indent); 605 ptln(' <th class="line">'.$this->lang['line'].'</th>', $indent); 606 ptln(' <th class="error">'.$this->lang['error'].'</th>', $indent); 607 ptln(' <th class="userid">'.$this->lang['user_id'].'</th>', $indent); 608 ptln(' <th class="username">'.$this->lang['user_name'].'</th>', $indent); 609 ptln(' <th class="usermail">'.$this->lang['user_mail'].'</th>', $indent); 610 ptln(' <th class="usergroups">'.$this->lang['user_groups'].'</th>', $indent); 611 ptln(' </tr>', $indent); 612 ptln(' </thead>', $indent); 613 ptln(' <tbody>', $indent); 614 foreach ($this->import_failures as $line => $failure) { 615 ptln(' <tr>', $indent); 616 ptln(' <td class="lineno"> '.sprintf('%0'.$digits.'d', $line).' </td>', $indent); 617 ptln(' <td class="error">' .$failure['error'].' </td>', $indent); 618 ptln(' <td class="field userid"> '.hsc($failure['user'][0]).' </td>', $indent); 619 ptln(' <td class="field username"> '.hsc($failure['user'][2]).' </td>', $indent); 620 ptln(' <td class="field usermail"> '.hsc($failure['user'][3]).' </td>', $indent); 621 ptln(' <td class="field usergroups"> '.hsc($failure['user'][4]).' </td>', $indent); 622 ptln(' </tr>', $indent); 623 } 624 ptln(' </tbody>', $indent); 625 ptln(' </table>', $indent); 626 ptln(' <p><a href="'.$failure_download_link.'">'.$this->lang['import_downloadfailures'].'</a></p>'); 627 ptln('</div>'); 628 } 629 } 630 631 /** 632 * Add an user to auth backend 633 * 634 * @return bool whether succesful 635 */ 636 protected function addUser() 637 { 638 global $INPUT; 639 if (!checkSecurityToken()) return false; 640 if (!$this->auth->canDo('addUser')) return false; 641 642 list($user,$pass,$name,$mail,$grps,$passconfirm) = $this->retrieveUser(); 643 if (empty($user)) return false; 644 645 if ($this->auth->canDo('modPass')) { 646 if (empty($pass)) { 647 if ($INPUT->has('usernotify')) { 648 $pass = auth_pwgen($user); 649 } else { 650 msg($this->lang['add_fail'], -1); 651 msg($this->lang['addUser_error_missing_pass'], -1); 652 return false; 653 } 654 } else { 655 if (!$this->verifyPassword($pass, $passconfirm)) { 656 msg($this->lang['add_fail'], -1); 657 msg($this->lang['addUser_error_pass_not_identical'], -1); 658 return false; 659 } 660 } 661 } else { 662 if (!empty($pass)) { 663 msg($this->lang['add_fail'], -1); 664 msg($this->lang['addUser_error_modPass_disabled'], -1); 665 return false; 666 } 667 } 668 669 if ($this->auth->canDo('modName')) { 670 if (empty($name)) { 671 msg($this->lang['add_fail'], -1); 672 msg($this->lang['addUser_error_name_missing'], -1); 673 return false; 674 } 675 } else { 676 if (!empty($name)) { 677 msg($this->lang['add_fail'], -1); 678 msg($this->lang['addUser_error_modName_disabled'], -1); 679 return false; 680 } 681 } 682 683 if ($this->auth->canDo('modMail')) { 684 if (empty($mail)) { 685 msg($this->lang['add_fail'], -1); 686 msg($this->lang['addUser_error_mail_missing'], -1); 687 return false; 688 } 689 } else { 690 if (!empty($mail)) { 691 msg($this->lang['add_fail'], -1); 692 msg($this->lang['addUser_error_modMail_disabled'], -1); 693 return false; 694 } 695 } 696 697 if ($ok = $this->auth->triggerUserMod('create', array($user, $pass, $name, $mail, $grps))) { 698 msg($this->lang['add_ok'], 1); 699 700 if ($INPUT->has('usernotify') && $pass) { 701 $this->notifyUser($user, $pass); 702 } 703 } else { 704 msg($this->lang['add_fail'], -1); 705 msg($this->lang['addUser_error_create_event_failed'], -1); 706 } 707 708 return $ok; 709 } 710 711 /** 712 * Delete user from auth backend 713 * 714 * @return bool whether succesful 715 */ 716 protected function deleteUser() 717 { 718 global $conf, $INPUT; 719 720 if (!checkSecurityToken()) return false; 721 if (!$this->auth->canDo('delUser')) return false; 722 723 $selected = $INPUT->arr('delete'); 724 if (empty($selected)) return false; 725 $selected = array_keys($selected); 726 727 if (in_array($_SERVER['REMOTE_USER'], $selected)) { 728 msg("You can't delete yourself!", -1); 729 return false; 730 } 731 732 $count = $this->auth->triggerUserMod('delete', array($selected)); 733 if ($count == count($selected)) { 734 $text = str_replace('%d', $count, $this->lang['delete_ok']); 735 msg("$text.", 1); 736 } else { 737 $part1 = str_replace('%d', $count, $this->lang['delete_ok']); 738 $part2 = str_replace('%d', (count($selected)-$count), $this->lang['delete_fail']); 739 msg("$part1, $part2", -1); 740 } 741 742 // invalidate all sessions 743 io_saveFile($conf['cachedir'].'/sessionpurge', time()); 744 745 return true; 746 } 747 748 /** 749 * Edit user (a user has been selected for editing) 750 * 751 * @param string $param id of the user 752 * @return bool whether succesful 753 */ 754 protected function editUser($param) 755 { 756 if (!checkSecurityToken()) return false; 757 if (!$this->auth->canDo('UserMod')) return false; 758 $user = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $param)); 759 $userdata = $this->auth->getUserData($user); 760 761 // no user found? 762 if (!$userdata) { 763 msg($this->lang['edit_usermissing'], -1); 764 return false; 765 } 766 767 $this->edit_user = $user; 768 $this->edit_userdata = $userdata; 769 770 return true; 771 } 772 773 /** 774 * Modify user in the auth backend (modified user data has been recieved) 775 * 776 * @return bool whether succesful 777 */ 778 protected function modifyUser() 779 { 780 global $conf, $INPUT; 781 782 if (!checkSecurityToken()) return false; 783 if (!$this->auth->canDo('UserMod')) return false; 784 785 // get currently valid user data 786 $olduser = $this->auth->cleanUser(preg_replace('/.*[:\/]/', '', $INPUT->str('userid_old'))); 787 $oldinfo = $this->auth->getUserData($olduser); 788 789 // get new user data subject to change 790 list($newuser,$newpass,$newname,$newmail,$newgrps,$passconfirm) = $this->retrieveUser(); 791 if (empty($newuser)) return false; 792 793 $changes = array(); 794 if ($newuser != $olduser) { 795 if (!$this->auth->canDo('modLogin')) { // sanity check, shouldn't be possible 796 msg($this->lang['update_fail'], -1); 797 return false; 798 } 799 800 // check if $newuser already exists 801 if ($this->auth->getUserData($newuser)) { 802 msg(sprintf($this->lang['update_exists'], $newuser), -1); 803 $re_edit = true; 804 } else { 805 $changes['user'] = $newuser; 806 } 807 } 808 if ($this->auth->canDo('modPass')) { 809 if ($newpass || $passconfirm) { 810 if ($this->verifyPassword($newpass, $passconfirm)) { 811 $changes['pass'] = $newpass; 812 } else { 813 return false; 814 } 815 } else { 816 // no new password supplied, check if we need to generate one (or it stays unchanged) 817 if ($INPUT->has('usernotify')) { 818 $changes['pass'] = auth_pwgen($olduser); 819 } 820 } 821 } 822 823 if (!empty($newname) && $this->auth->canDo('modName') && $newname != $oldinfo['name']) { 824 $changes['name'] = $newname; 825 } 826 if (!empty($newmail) && $this->auth->canDo('modMail') && $newmail != $oldinfo['mail']) { 827 $changes['mail'] = $newmail; 828 } 829 if (!empty($newgrps) && $this->auth->canDo('modGroups') && $newgrps != $oldinfo['grps']) { 830 $changes['grps'] = $newgrps; 831 } 832 833 if ($ok = $this->auth->triggerUserMod('modify', array($olduser, $changes))) { 834 msg($this->lang['update_ok'], 1); 835 836 if ($INPUT->has('usernotify') && !empty($changes['pass'])) { 837 $notify = empty($changes['user']) ? $olduser : $newuser; 838 $this->notifyUser($notify, $changes['pass']); 839 } 840 841 // invalidate all sessions 842 io_saveFile($conf['cachedir'].'/sessionpurge', time()); 843 } else { 844 msg($this->lang['update_fail'], -1); 845 } 846 847 if (!empty($re_edit)) { 848 $this->editUser($olduser); 849 } 850 851 return $ok; 852 } 853 854 /** 855 * Send password change notification email 856 * 857 * @param string $user id of user 858 * @param string $password plain text 859 * @param bool $status_alert whether status alert should be shown 860 * @return bool whether succesful 861 */ 862 protected function notifyUser($user, $password, $status_alert = true) 863 { 864 865 if ($sent = auth_sendPassword($user, $password)) { 866 if ($status_alert) { 867 msg($this->lang['notify_ok'], 1); 868 } 869 } else { 870 if ($status_alert) { 871 msg($this->lang['notify_fail'], -1); 872 } 873 } 874 875 return $sent; 876 } 877 878 /** 879 * Verify password meets minimum requirements 880 * :TODO: extend to support password strength 881 * 882 * @param string $password candidate string for new password 883 * @param string $confirm repeated password for confirmation 884 * @return bool true if meets requirements, false otherwise 885 */ 886 protected function verifyPassword($password, $confirm) 887 { 888 global $lang; 889 890 if (empty($password) && empty($confirm)) { 891 return false; 892 } 893 894 if ($password !== $confirm) { 895 msg($lang['regbadpass'], -1); 896 return false; 897 } 898 899 // :TODO: test password for required strength 900 901 // if we make it this far the password is good 902 return true; 903 } 904 905 /** 906 * Retrieve & clean user data from the form 907 * 908 * @param bool $clean whether the cleanUser method of the authentication backend is applied 909 * @return array (user, password, full name, email, array(groups)) 910 */ 911 protected function retrieveUser($clean = true) 912 { 913 /** @var DokuWiki_Auth_Plugin $auth */ 914 global $auth; 915 global $INPUT; 916 917 $user = array(); 918 $user[0] = ($clean) ? $auth->cleanUser($INPUT->str('userid')) : $INPUT->str('userid'); 919 $user[1] = $INPUT->str('userpass'); 920 $user[2] = $INPUT->str('username'); 921 $user[3] = $INPUT->str('usermail'); 922 $user[4] = explode(',', $INPUT->str('usergroups')); 923 $user[5] = $INPUT->str('userpass2'); // repeated password for confirmation 924 925 $user[4] = array_map('trim', $user[4]); 926 if ($clean) $user[4] = array_map(array($auth,'cleanGroup'), $user[4]); 927 $user[4] = array_filter($user[4]); 928 $user[4] = array_unique($user[4]); 929 if (!count($user[4])) $user[4] = null; 930 931 return $user; 932 } 933 934 /** 935 * Set the filter with the current search terms or clear the filter 936 * 937 * @param string $op 'new' or 'clear' 938 */ 939 protected function setFilter($op) 940 { 941 942 $this->filter = array(); 943 944 if ($op == 'new') { 945 list($user,/* $pass */,$name,$mail,$grps) = $this->retrieveUser(false); 946 947 if (!empty($user)) $this->filter['user'] = $user; 948 if (!empty($name)) $this->filter['name'] = $name; 949 if (!empty($mail)) $this->filter['mail'] = $mail; 950 if (!empty($grps)) $this->filter['grps'] = join('|', $grps); 951 } 952 } 953 954 /** 955 * Get the current search terms 956 * 957 * @return array 958 */ 959 protected function retrieveFilter() 960 { 961 global $INPUT; 962 963 $t_filter = $INPUT->arr('filter'); 964 965 // messy, but this way we ensure we aren't getting any additional crap from malicious users 966 $filter = array(); 967 968 if (isset($t_filter['user'])) $filter['user'] = $t_filter['user']; 969 if (isset($t_filter['name'])) $filter['name'] = $t_filter['name']; 970 if (isset($t_filter['mail'])) $filter['mail'] = $t_filter['mail']; 971 if (isset($t_filter['grps'])) $filter['grps'] = $t_filter['grps']; 972 973 return $filter; 974 } 975 976 /** 977 * Validate and improve the pagination values 978 */ 979 protected function validatePagination() 980 { 981 982 if ($this->start >= $this->users_total) { 983 $this->start = $this->users_total - $this->pagesize; 984 } 985 if ($this->start < 0) $this->start = 0; 986 987 $this->last = min($this->users_total, $this->start + $this->pagesize); 988 } 989 990 /** 991 * Return an array of strings to enable/disable pagination buttons 992 * 993 * @return array with enable/disable attributes 994 */ 995 protected function pagination() 996 { 997 998 $disabled = 'disabled="disabled"'; 999 1000 $buttons = array(); 1001 $buttons['start'] = $buttons['prev'] = ($this->start == 0) ? $disabled : ''; 1002 1003 if ($this->users_total == -1) { 1004 $buttons['last'] = $disabled; 1005 $buttons['next'] = ''; 1006 } else { 1007 $buttons['last'] = $buttons['next'] = 1008 (($this->start + $this->pagesize) >= $this->users_total) ? $disabled : ''; 1009 } 1010 1011 if ($this->lastdisabled) { 1012 $buttons['last'] = $disabled; 1013 } 1014 1015 return $buttons; 1016 } 1017 1018 /** 1019 * Export a list of users in csv format using the current filter criteria 1020 */ 1021 protected function exportCSV() 1022 { 1023 // list of users for export - based on current filter criteria 1024 $user_list = $this->auth->retrieveUsers(0, 0, $this->filter); 1025 $column_headings = array( 1026 $this->lang["user_id"], 1027 $this->lang["user_name"], 1028 $this->lang["user_mail"], 1029 $this->lang["user_groups"] 1030 ); 1031 1032 // ============================================================================================== 1033 // GENERATE OUTPUT 1034 // normal headers for downloading... 1035 header('Content-type: text/csv;charset=utf-8'); 1036 header('Content-Disposition: attachment; filename="wikiusers.csv"'); 1037 # // for debugging assistance, send as text plain to the browser 1038 # header('Content-type: text/plain;charset=utf-8'); 1039 1040 // output the csv 1041 $fd = fopen('php://output', 'w'); 1042 fputcsv($fd, $column_headings); 1043 foreach ($user_list as $user => $info) { 1044 $line = array($user, $info['name'], $info['mail'], join(',', $info['grps'])); 1045 fputcsv($fd, $line); 1046 } 1047 fclose($fd); 1048 if (defined('DOKU_UNITTEST')) { 1049 return; 1050 } 1051 1052 die; 1053 } 1054 1055 /** 1056 * Import a file of users in csv format 1057 * 1058 * csv file should have 4 columns, user_id, full name, email, groups (comma separated) 1059 * 1060 * @return bool whether successful 1061 */ 1062 protected function importCSV() 1063 { 1064 // check we are allowed to add users 1065 if (!checkSecurityToken()) return false; 1066 if (!$this->auth->canDo('addUser')) return false; 1067 1068 // check file uploaded ok. 1069 if (empty($_FILES['import']['size']) || 1070 !empty($_FILES['import']['error']) && $this->isUploadedFile($_FILES['import']['tmp_name']) 1071 ) { 1072 msg($this->lang['import_error_upload'], -1); 1073 return false; 1074 } 1075 // retrieve users from the file 1076 $this->import_failures = array(); 1077 $import_success_count = 0; 1078 $import_fail_count = 0; 1079 $line = 0; 1080 $fd = fopen($_FILES['import']['tmp_name'], 'r'); 1081 if ($fd) { 1082 while ($csv = fgets($fd)) { 1083 if (!\dokuwiki\Utf8\Clean::isUtf8($csv)) { 1084 $csv = utf8_encode($csv); 1085 } 1086 $raw = str_getcsv($csv); 1087 $error = ''; // clean out any errors from the previous line 1088 // data checks... 1089 if (1 == ++$line) { 1090 if ($raw[0] == 'user_id' || $raw[0] == $this->lang['user_id']) continue; // skip headers 1091 } 1092 if (count($raw) < 4) { // need at least four fields 1093 $import_fail_count++; 1094 $error = sprintf($this->lang['import_error_fields'], count($raw)); 1095 $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv); 1096 continue; 1097 } 1098 array_splice($raw, 1, 0, auth_pwgen()); // splice in a generated password 1099 $clean = $this->cleanImportUser($raw, $error); 1100 if ($clean && $this->importUser($clean, $error)) { 1101 $sent = $this->notifyUser($clean[0], $clean[1], false); 1102 if (!$sent) { 1103 msg(sprintf($this->lang['import_notify_fail'], $clean[0], $clean[3]), -1); 1104 } 1105 $import_success_count++; 1106 } else { 1107 $import_fail_count++; 1108 array_splice($raw, 1, 1); // remove the spliced in password 1109 $this->import_failures[$line] = array('error' => $error, 'user' => $raw, 'orig' => $csv); 1110 } 1111 } 1112 msg( 1113 sprintf( 1114 $this->lang['import_success_count'], 1115 ($import_success_count + $import_fail_count), 1116 $import_success_count 1117 ), 1118 ($import_success_count ? 1 : -1) 1119 ); 1120 if ($import_fail_count) { 1121 msg(sprintf($this->lang['import_failure_count'], $import_fail_count), -1); 1122 } 1123 } else { 1124 msg($this->lang['import_error_readfail'], -1); 1125 } 1126 1127 // save import failures into the session 1128 if (!headers_sent()) { 1129 session_start(); 1130 $_SESSION['import_failures'] = $this->import_failures; 1131 session_write_close(); 1132 } 1133 return true; 1134 } 1135 1136 /** 1137 * Returns cleaned user data 1138 * 1139 * @param array $candidate raw values of line from input file 1140 * @param string $error 1141 * @return array|false cleaned data or false 1142 */ 1143 protected function cleanImportUser($candidate, & $error) 1144 { 1145 global $INPUT; 1146 1147 // FIXME kludgy .... 1148 $INPUT->set('userid', $candidate[0]); 1149 $INPUT->set('userpass', $candidate[1]); 1150 $INPUT->set('username', $candidate[2]); 1151 $INPUT->set('usermail', $candidate[3]); 1152 $INPUT->set('usergroups', $candidate[4]); 1153 1154 $cleaned = $this->retrieveUser(); 1155 list($user,/* $pass */,$name,$mail,/* $grps */) = $cleaned; 1156 if (empty($user)) { 1157 $error = $this->lang['import_error_baduserid']; 1158 return false; 1159 } 1160 1161 // no need to check password, handled elsewhere 1162 1163 if (!($this->auth->canDo('modName') xor empty($name))) { 1164 $error = $this->lang['import_error_badname']; 1165 return false; 1166 } 1167 1168 if ($this->auth->canDo('modMail')) { 1169 if (empty($mail) || !mail_isvalid($mail)) { 1170 $error = $this->lang['import_error_badmail']; 1171 return false; 1172 } 1173 } else { 1174 if (!empty($mail)) { 1175 $error = $this->lang['import_error_badmail']; 1176 return false; 1177 } 1178 } 1179 1180 return $cleaned; 1181 } 1182 1183 /** 1184 * Adds imported user to auth backend 1185 * 1186 * Required a check of canDo('addUser') before 1187 * 1188 * @param array $user data of user 1189 * @param string &$error reference catched error message 1190 * @return bool whether successful 1191 */ 1192 protected function importUser($user, &$error) 1193 { 1194 if (!$this->auth->triggerUserMod('create', $user)) { 1195 $error = $this->lang['import_error_create']; 1196 return false; 1197 } 1198 1199 return true; 1200 } 1201 1202 /** 1203 * Downloads failures as csv file 1204 */ 1205 protected function downloadImportFailures() 1206 { 1207 1208 // ============================================================================================== 1209 // GENERATE OUTPUT 1210 // normal headers for downloading... 1211 header('Content-type: text/csv;charset=utf-8'); 1212 header('Content-Disposition: attachment; filename="importfails.csv"'); 1213 # // for debugging assistance, send as text plain to the browser 1214 # header('Content-type: text/plain;charset=utf-8'); 1215 1216 // output the csv 1217 $fd = fopen('php://output', 'w'); 1218 foreach ($this->import_failures as $fail) { 1219 fputs($fd, $fail['orig']); 1220 } 1221 fclose($fd); 1222 die; 1223 } 1224 1225 /** 1226 * wrapper for is_uploaded_file to facilitate overriding by test suite 1227 * 1228 * @param string $file filename 1229 * @return bool 1230 */ 1231 protected function isUploadedFile($file) 1232 { 1233 return is_uploaded_file($file); 1234 } 1235 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body