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