[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

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

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