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