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