[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/plugins/usermanager/ -> admin.php (source)

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