[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/lib/plugins/authplain/ -> auth.php (source)

   1  <?php
   2  use dokuwiki\Utf8\Sort;
   3  
   4  /**
   5   * Plaintext authentication backend
   6   *
   7   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
   8   * @author     Andreas Gohr <andi@splitbrain.org>
   9   * @author     Chris Smith <chris@jalakai.co.uk>
  10   * @author     Jan Schumann <js@schumann-it.com>
  11   */
  12  class auth_plugin_authplain extends DokuWiki_Auth_Plugin
  13  {
  14      /** @var array user cache */
  15      protected $users = null;
  16  
  17      /** @var array filter pattern */
  18      protected $pattern = array();
  19  
  20      /** @var bool safe version of preg_split */
  21      protected $pregsplit_safe = false;
  22  
  23      /**
  24       * Constructor
  25       *
  26       * Carry out sanity checks to ensure the object is
  27       * able to operate. Set capabilities.
  28       *
  29       * @author  Christopher Smith <chris@jalakai.co.uk>
  30       */
  31      public function __construct()
  32      {
  33          parent::__construct();
  34          global $config_cascade;
  35  
  36          if (!@is_readable($config_cascade['plainauth.users']['default'])) {
  37              $this->success = false;
  38          } else {
  39              if (@is_writable($config_cascade['plainauth.users']['default'])) {
  40                  $this->cando['addUser']   = true;
  41                  $this->cando['delUser']   = true;
  42                  $this->cando['modLogin']  = true;
  43                  $this->cando['modPass']   = true;
  44                  $this->cando['modName']   = true;
  45                  $this->cando['modMail']   = true;
  46                  $this->cando['modGroups'] = true;
  47              }
  48              $this->cando['getUsers']     = true;
  49              $this->cando['getUserCount'] = true;
  50              $this->cando['getGroups']    = true;
  51          }
  52  
  53          $this->pregsplit_safe = version_compare(PCRE_VERSION, '6.7', '>=');
  54      }
  55  
  56      /**
  57       * Check user+password
  58       *
  59       * Checks if the given user exists and the given
  60       * plaintext password is correct
  61       *
  62       * @author  Andreas Gohr <andi@splitbrain.org>
  63       * @param string $user
  64       * @param string $pass
  65       * @return  bool
  66       */
  67      public function checkPass($user, $pass)
  68      {
  69          $userinfo = $this->getUserData($user);
  70          if ($userinfo === false) return false;
  71  
  72          return auth_verifyPassword($pass, $this->users[$user]['pass']);
  73      }
  74  
  75      /**
  76       * Return user info
  77       *
  78       * Returns info about the given user needs to contain
  79       * at least these fields:
  80       *
  81       * name string  full name of the user
  82       * mail string  email addres of the user
  83       * grps array   list of groups the user is in
  84       *
  85       * @author  Andreas Gohr <andi@splitbrain.org>
  86       * @param string $user
  87       * @param bool $requireGroups  (optional) ignored by this plugin, grps info always supplied
  88       * @return array|false
  89       */
  90      public function getUserData($user, $requireGroups = true)
  91      {
  92          if ($this->users === null) $this->loadUserData();
  93          return isset($this->users[$user]) ? $this->users[$user] : false;
  94      }
  95  
  96      /**
  97       * Creates a string suitable for saving as a line
  98       * in the file database
  99       * (delimiters escaped, etc.)
 100       *
 101       * @param string $user
 102       * @param string $pass
 103       * @param string $name
 104       * @param string $mail
 105       * @param array  $grps list of groups the user is in
 106       * @return string
 107       */
 108      protected function createUserLine($user, $pass, $name, $mail, $grps)
 109      {
 110          $groups   = join(',', $grps);
 111          $userline = array($user, $pass, $name, $mail, $groups);
 112          $userline = str_replace('\\', '\\\\', $userline); // escape \ as \\
 113          $userline = str_replace(':', '\\:', $userline); // escape : as \:
 114          $userline = join(':', $userline)."\n";
 115          return $userline;
 116      }
 117  
 118      /**
 119       * Create a new User
 120       *
 121       * Returns false if the user already exists, null when an error
 122       * occurred and true if everything went well.
 123       *
 124       * The new user will be added to the default group by this
 125       * function if grps are not specified (default behaviour).
 126       *
 127       * @author  Andreas Gohr <andi@splitbrain.org>
 128       * @author  Chris Smith <chris@jalakai.co.uk>
 129       *
 130       * @param string $user
 131       * @param string $pwd
 132       * @param string $name
 133       * @param string $mail
 134       * @param array  $grps
 135       * @return bool|null|string
 136       */
 137      public function createUser($user, $pwd, $name, $mail, $grps = null)
 138      {
 139          global $conf;
 140          global $config_cascade;
 141  
 142          // user mustn't already exist
 143          if ($this->getUserData($user) !== false) {
 144              msg($this->getLang('userexists'), -1);
 145              return false;
 146          }
 147  
 148          $pass = auth_cryptPassword($pwd);
 149  
 150          // set default group if no groups specified
 151          if (!is_array($grps)) $grps = array($conf['defaultgroup']);
 152  
 153          // prepare user line
 154          $userline = $this->createUserLine($user, $pass, $name, $mail, $grps);
 155  
 156          if (!io_saveFile($config_cascade['plainauth.users']['default'], $userline, true)) {
 157              msg($this->getLang('writefail'), -1);
 158              return null;
 159          }
 160  
 161          $this->users[$user] = compact('pass', 'name', 'mail', 'grps');
 162          return $pwd;
 163      }
 164  
 165      /**
 166       * Modify user data
 167       *
 168       * @author  Chris Smith <chris@jalakai.co.uk>
 169       * @param   string $user      nick of the user to be changed
 170       * @param   array  $changes   array of field/value pairs to be changed (password will be clear text)
 171       * @return  bool
 172       */
 173      public function modifyUser($user, $changes)
 174      {
 175          global $ACT;
 176          global $config_cascade;
 177  
 178          // sanity checks, user must already exist and there must be something to change
 179          if (($userinfo = $this->getUserData($user)) === false) {
 180              msg($this->getLang('usernotexists'), -1);
 181              return false;
 182          }
 183  
 184          // don't modify protected users
 185          if (!empty($userinfo['protected'])) {
 186              msg(sprintf($this->getLang('protected'), hsc($user)), -1);
 187              return false;
 188          }
 189  
 190          if (!is_array($changes) || !count($changes)) return true;
 191  
 192          // update userinfo with new data, remembering to encrypt any password
 193          $newuser = $user;
 194          foreach ($changes as $field => $value) {
 195              if ($field == 'user') {
 196                  $newuser = $value;
 197                  continue;
 198              }
 199              if ($field == 'pass') $value = auth_cryptPassword($value);
 200              $userinfo[$field] = $value;
 201          }
 202  
 203          $userline = $this->createUserLine(
 204              $newuser,
 205              $userinfo['pass'],
 206              $userinfo['name'],
 207              $userinfo['mail'],
 208              $userinfo['grps']
 209          );
 210  
 211          if (!io_replaceInFile($config_cascade['plainauth.users']['default'], '/^'.$user.':/', $userline, true)) {
 212              msg('There was an error modifying your user data. You may need to register again.', -1);
 213              // FIXME, io functions should be fail-safe so existing data isn't lost
 214              $ACT = 'register';
 215              return false;
 216          }
 217  
 218          if(isset($this->users[$user])) unset($this->users[$user]);
 219          $this->users[$newuser] = $userinfo;
 220          return true;
 221      }
 222  
 223      /**
 224       * Remove one or more users from the list of registered users
 225       *
 226       * @author  Christopher Smith <chris@jalakai.co.uk>
 227       * @param   array  $users   array of users to be deleted
 228       * @return  int             the number of users deleted
 229       */
 230      public function deleteUsers($users)
 231      {
 232          global $config_cascade;
 233  
 234          if (!is_array($users) || empty($users)) return 0;
 235  
 236          if ($this->users === null) $this->loadUserData();
 237  
 238          $deleted = array();
 239          foreach ($users as $user) {
 240              // don't delete protected users
 241              if (!empty($this->users[$user]['protected'])) {
 242                  msg(sprintf($this->getLang('protected'), hsc($user)), -1);
 243                  continue;
 244              }
 245              if (isset($this->users[$user])) $deleted[] = preg_quote($user, '/');
 246          }
 247  
 248          if (empty($deleted)) return 0;
 249  
 250          $pattern = '/^('.join('|', $deleted).'):/';
 251          if (!io_deleteFromFile($config_cascade['plainauth.users']['default'], $pattern, true)) {
 252              msg($this->getLang('writefail'), -1);
 253              return 0;
 254          }
 255  
 256          // reload the user list and count the difference
 257          $count = count($this->users);
 258          $this->loadUserData();
 259          $count -= count($this->users);
 260          return $count;
 261      }
 262  
 263      /**
 264       * Return a count of the number of user which meet $filter criteria
 265       *
 266       * @author  Chris Smith <chris@jalakai.co.uk>
 267       *
 268       * @param array $filter
 269       * @return int
 270       */
 271      public function getUserCount($filter = array())
 272      {
 273  
 274          if ($this->users === null) $this->loadUserData();
 275  
 276          if (!count($filter)) return count($this->users);
 277  
 278          $count = 0;
 279          $this->constructPattern($filter);
 280  
 281          foreach ($this->users as $user => $info) {
 282              $count += $this->filter($user, $info);
 283          }
 284  
 285          return $count;
 286      }
 287  
 288      /**
 289       * Bulk retrieval of user data
 290       *
 291       * @author  Chris Smith <chris@jalakai.co.uk>
 292       *
 293       * @param   int   $start index of first user to be returned
 294       * @param   int   $limit max number of users to be returned
 295       * @param   array $filter array of field/pattern pairs
 296       * @return  array userinfo (refer getUserData for internal userinfo details)
 297       */
 298      public function retrieveUsers($start = 0, $limit = 0, $filter = array())
 299      {
 300  
 301          if ($this->users === null) $this->loadUserData();
 302  
 303          Sort::ksort($this->users);
 304  
 305          $i     = 0;
 306          $count = 0;
 307          $out   = array();
 308          $this->constructPattern($filter);
 309  
 310          foreach ($this->users as $user => $info) {
 311              if ($this->filter($user, $info)) {
 312                  if ($i >= $start) {
 313                      $out[$user] = $info;
 314                      $count++;
 315                      if (($limit > 0) && ($count >= $limit)) break;
 316                  }
 317                  $i++;
 318              }
 319          }
 320  
 321          return $out;
 322      }
 323  
 324      /**
 325       * Retrieves groups.
 326       * Loads complete user data into memory before searching for groups.
 327       *
 328       * @param   int   $start index of first group to be returned
 329       * @param   int   $limit max number of groups to be returned
 330       * @return  array
 331       */
 332      public function retrieveGroups($start = 0, $limit = 0)
 333      {
 334          $groups = [];
 335  
 336          if ($this->users === null) $this->loadUserData();
 337          foreach($this->users as $user => $info) {
 338              $groups = array_merge($groups, array_diff($info['grps'], $groups));
 339          }
 340          Sort::ksort($groups);
 341  
 342          if($limit > 0) {
 343              return array_splice($groups, $start, $limit);
 344          }
 345          return array_splice($groups, $start);
 346      }
 347  
 348      /**
 349       * Only valid pageid's (no namespaces) for usernames
 350       *
 351       * @param string $user
 352       * @return string
 353       */
 354      public function cleanUser($user)
 355      {
 356          global $conf;
 357          return cleanID(str_replace(':', $conf['sepchar'], $user));
 358      }
 359  
 360      /**
 361       * Only valid pageid's (no namespaces) for groupnames
 362       *
 363       * @param string $group
 364       * @return string
 365       */
 366      public function cleanGroup($group)
 367      {
 368          global $conf;
 369          return cleanID(str_replace(':', $conf['sepchar'], $group));
 370      }
 371  
 372      /**
 373       * Load all user data
 374       *
 375       * loads the user file into a datastructure
 376       *
 377       * @author  Andreas Gohr <andi@splitbrain.org>
 378       */
 379      protected function loadUserData()
 380      {
 381          global $config_cascade;
 382  
 383          $this->users = $this->readUserFile($config_cascade['plainauth.users']['default']);
 384  
 385          // support protected users
 386          if (!empty($config_cascade['plainauth.users']['protected'])) {
 387              $protected = $this->readUserFile($config_cascade['plainauth.users']['protected']);
 388              foreach (array_keys($protected) as $key) {
 389                  $protected[$key]['protected'] = true;
 390              }
 391              $this->users = array_merge($this->users, $protected);
 392          }
 393      }
 394  
 395      /**
 396       * Read user data from given file
 397       *
 398       * ignores non existing files
 399       *
 400       * @param string $file the file to load data from
 401       * @return array
 402       */
 403      protected function readUserFile($file)
 404      {
 405          $users = array();
 406          if (!file_exists($file)) return $users;
 407  
 408          $lines = file($file);
 409          foreach ($lines as $line) {
 410              $line = preg_replace('/#.*$/', '', $line); //ignore comments
 411              $line = trim($line);
 412              if (empty($line)) continue;
 413  
 414              $row = $this->splitUserData($line);
 415              $row = str_replace('\\:', ':', $row);
 416              $row = str_replace('\\\\', '\\', $row);
 417  
 418              $groups = array_values(array_filter(explode(",", $row[4])));
 419  
 420              $users[$row[0]]['pass'] = $row[1];
 421              $users[$row[0]]['name'] = urldecode($row[2]);
 422              $users[$row[0]]['mail'] = $row[3];
 423              $users[$row[0]]['grps'] = $groups;
 424          }
 425          return $users;
 426      }
 427  
 428      /**
 429       * Get the user line split into it's parts
 430       *
 431       * @param string $line
 432       * @return string[]
 433       */
 434      protected function splitUserData($line)
 435      {
 436          // due to a bug in PCRE 6.6, preg_split will fail with the regex we use here
 437          // refer github issues 877 & 885
 438          if ($this->pregsplit_safe) {
 439              return preg_split('/(?<![^\\\\]\\\\)\:/', $line, 5);       // allow for : escaped as \:
 440          }
 441  
 442          $row = array();
 443          $piece = '';
 444          $len = strlen($line);
 445          for ($i=0; $i<$len; $i++) {
 446              if ($line[$i]=='\\') {
 447                  $piece .= $line[$i];
 448                  $i++;
 449                  if ($i>=$len) break;
 450              } elseif ($line[$i]==':') {
 451                  $row[] = $piece;
 452                  $piece = '';
 453                  continue;
 454              }
 455              $piece .= $line[$i];
 456          }
 457          $row[] = $piece;
 458  
 459          return $row;
 460      }
 461  
 462      /**
 463       * return true if $user + $info match $filter criteria, false otherwise
 464       *
 465       * @author   Chris Smith <chris@jalakai.co.uk>
 466       *
 467       * @param string $user User login
 468       * @param array  $info User's userinfo array
 469       * @return bool
 470       */
 471      protected function filter($user, $info)
 472      {
 473          foreach ($this->pattern as $item => $pattern) {
 474              if ($item == 'user') {
 475                  if (!preg_match($pattern, $user)) return false;
 476              } elseif ($item == 'grps') {
 477                  if (!count(preg_grep($pattern, $info['grps']))) return false;
 478              } else {
 479                  if (!preg_match($pattern, $info[$item])) return false;
 480              }
 481          }
 482          return true;
 483      }
 484  
 485      /**
 486       * construct a filter pattern
 487       *
 488       * @param array $filter
 489       */
 490      protected function constructPattern($filter)
 491      {
 492          $this->pattern = array();
 493          foreach ($filter as $item => $pattern) {
 494              $this->pattern[$item] = '/'.str_replace('/', '\/', $pattern).'/i'; // allow regex characters
 495          }
 496      }
 497  }