[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

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

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