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