[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> auth.php (source)

   1  <?php
   2  
   3  /**
   4   * Authentication library
   5   *
   6   * Including this file will automatically try to login
   7   * a user by calling auth_login()
   8   *
   9   * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
  10   * @author     Andreas Gohr <andi@splitbrain.org>
  11   */
  12  
  13  use dokuwiki\Ip;
  14  use dokuwiki\ErrorHandler;
  15  use dokuwiki\JWT;
  16  use dokuwiki\Logger;
  17  use dokuwiki\MailUtils;
  18  use dokuwiki\Utf8\PhpString;
  19  use dokuwiki\Extension\AuthPlugin;
  20  use dokuwiki\Extension\Event;
  21  use dokuwiki\Extension\PluginController;
  22  use dokuwiki\PassHash;
  23  use dokuwiki\Subscriptions\RegistrationSubscriptionSender;
  24  use phpseclib3\Crypt\AES;
  25  use phpseclib3\Crypt\Common\SymmetricKey;
  26  use phpseclib3\Exception\BadDecryptionException;
  27  
  28  /**
  29   * Initialize the auth system.
  30   *
  31   * This function is automatically called at the end of init.php
  32   *
  33   * This used to be the main() of the auth.php
  34   *
  35   * @todo backend loading maybe should be handled by the class autoloader
  36   * @todo maybe split into multiple functions at the XXX marked positions
  37   * @triggers AUTH_LOGIN_CHECK
  38   * @return bool
  39   */
  40  function auth_setup()
  41  {
  42      global $conf;
  43      /* @var AuthPlugin $auth */
  44      global $auth;
  45      /* @var Input $INPUT */
  46      global $INPUT;
  47      global $AUTH_ACL;
  48      global $lang;
  49      /* @var PluginController $plugin_controller */
  50      global $plugin_controller;
  51      $AUTH_ACL = [];
  52  
  53      // unset REMOTE_USER if empty
  54      if ($INPUT->server->str('REMOTE_USER') === '') {
  55          $INPUT->server->remove('REMOTE_USER');
  56      }
  57  
  58      if (!$conf['useacl']) return false;
  59  
  60      // try to load auth backend from plugins
  61      foreach ($plugin_controller->getList('auth') as $plugin) {
  62          if ($conf['authtype'] === $plugin) {
  63              $auth = $plugin_controller->load('auth', $plugin);
  64              break;
  65          }
  66      }
  67  
  68      if (!$auth instanceof AuthPlugin) {
  69          msg($lang['authtempfail'], -1);
  70          return false;
  71      }
  72  
  73      if ($auth->success == false) {
  74          // degrade to unauthenticated user
  75          $auth = null;
  76          auth_logoff();
  77          msg($lang['authtempfail'], -1);
  78          return false;
  79      }
  80  
  81      // do the login either by cookie or provided credentials XXX
  82      $INPUT->set('http_credentials', false);
  83      if (!$conf['rememberme']) $INPUT->set('r', false);
  84  
  85      // Populate Basic Auth user/password from Authorization header
  86      // Note: with FastCGI, data is in REDIRECT_HTTP_AUTHORIZATION instead of HTTP_AUTHORIZATION
  87      $header = $INPUT->server->str('HTTP_AUTHORIZATION') ?: $INPUT->server->str('REDIRECT_HTTP_AUTHORIZATION');
  88      if (preg_match('~^Basic ([a-z\d/+]*={0,2})$~i', $header, $matches)) {
  89          $userpass = explode(':', base64_decode($matches[1]));
  90          [$_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']] = $userpass;
  91      }
  92  
  93      // if no credentials were given try to use HTTP auth (for SSO)
  94      if (!$INPUT->str('u') && empty($_COOKIE[DOKU_COOKIE]) && !empty($INPUT->server->str('PHP_AUTH_USER'))) {
  95          $INPUT->set('u', $INPUT->server->str('PHP_AUTH_USER'));
  96          $INPUT->set('p', $INPUT->server->str('PHP_AUTH_PW'));
  97          $INPUT->set('http_credentials', true);
  98      }
  99  
 100      // apply cleaning (auth specific user names, remove control chars)
 101      if (true === $auth->success) {
 102          $INPUT->set('u', $auth->cleanUser(stripctl($INPUT->str('u'))));
 103          $INPUT->set('p', stripctl($INPUT->str('p')));
 104      }
 105  
 106      if (!auth_tokenlogin()) {
 107          $ok = null;
 108  
 109          if ($auth->canDo('external')) {
 110              $ok = $auth->trustExternal($INPUT->str('u'), $INPUT->str('p'), $INPUT->bool('r'));
 111          }
 112  
 113          if ($ok === null) {
 114              // external trust mechanism not in place, or returns no result,
 115              // then attempt auth_login
 116              $evdata = [
 117                  'user' => $INPUT->str('u'),
 118                  'password' => $INPUT->str('p'),
 119                  'sticky' => $INPUT->bool('r'),
 120                  'silent' => $INPUT->bool('http_credentials')
 121              ];
 122              Event::createAndTrigger('AUTH_LOGIN_CHECK', $evdata, 'auth_login_wrapper');
 123          }
 124      }
 125  
 126      //load ACL into a global array XXX
 127      $AUTH_ACL = auth_loadACL();
 128  
 129      return true;
 130  }
 131  
 132  /**
 133   * Loads the ACL setup and handle user wildcards
 134   *
 135   * @author Andreas Gohr <andi@splitbrain.org>
 136   *
 137   * @return array
 138   */
 139  function auth_loadACL()
 140  {
 141      global $config_cascade;
 142      global $USERINFO;
 143      /* @var Input $INPUT */
 144      global $INPUT;
 145  
 146      if (!is_readable($config_cascade['acl']['default'])) return [];
 147  
 148      $acl = file($config_cascade['acl']['default']);
 149  
 150      $out = [];
 151      foreach ($acl as $line) {
 152          $line = trim($line);
 153          if (empty($line) || ($line[0] == '#')) continue; // skip blank lines & comments
 154          [$id, $rest] = preg_split('/[ \t]+/', $line, 2);
 155  
 156          // substitute user wildcard first (its 1:1)
 157          if (strstr($line, '%USER%')) {
 158              // if user is not logged in, this ACL line is meaningless - skip it
 159              if (!$INPUT->server->has('REMOTE_USER')) continue;
 160  
 161              $id   = str_replace('%USER%', cleanID($INPUT->server->str('REMOTE_USER')), $id);
 162              $rest = str_replace('%USER%', auth_nameencode($INPUT->server->str('REMOTE_USER')), $rest);
 163          }
 164  
 165          // substitute group wildcard (its 1:m)
 166          if (strstr($line, '%GROUP%')) {
 167              // if user is not logged in, grps is empty, no output will be added (i.e. skipped)
 168              if (isset($USERINFO['grps'])) {
 169                  foreach ((array) $USERINFO['grps'] as $grp) {
 170                      $nid   = str_replace('%GROUP%', cleanID($grp), $id);
 171                      $nrest = str_replace('%GROUP%', '@' . auth_nameencode($grp), $rest);
 172                      $out[] = "$nid\t$nrest";
 173                  }
 174              }
 175          } else {
 176              $out[] = "$id\t$rest";
 177          }
 178      }
 179  
 180      return $out;
 181  }
 182  
 183  /**
 184   * Try a token login
 185   *
 186   * @return bool true if token login succeeded
 187   */
 188  function auth_tokenlogin()
 189  {
 190      global $USERINFO;
 191      global $INPUT;
 192      /** @var DokuWiki_Auth_Plugin $auth */
 193      global $auth;
 194      if (!$auth) return false;
 195  
 196      $headers = [];
 197  
 198      // try to get the headers from Apache
 199      if (function_exists('getallheaders')) {
 200          $headers = getallheaders();
 201          if (is_array($headers)) {
 202              $headers = array_change_key_case($headers);
 203          }
 204      }
 205  
 206      // get the headers from $_SERVER
 207      if (!$headers) {
 208          foreach ($_SERVER as $key => $value) {
 209              if (str_starts_with($key, 'HTTP_')) {
 210                  // underscores in $_SERVER keys stand for dashes in the header name
 211                  $headers[strtolower(strtr(substr($key, 5), '_', '-'))] = $value;
 212              }
 213          }
 214      }
 215  
 216      // check authorization header
 217      if (isset($headers['authorization'])) {
 218          [$type, $token] = sexplode(' ', $headers['authorization'], 2);
 219          if ($type !== 'Bearer') $token = ''; // not the token we want
 220      }
 221  
 222      // check x-dokuwiki-token header
 223      if (isset($headers['x-dokuwiki-token'])) {
 224          $token = $headers['x-dokuwiki-token'];
 225      }
 226  
 227      if (empty($token)) return false;
 228  
 229      // check token
 230      try {
 231          $authtoken = JWT::validate($token);
 232      } catch (Exception $e) {
 233          Logger::debug('Token login failed: ' . $e->getMessage(), null, $e->getFile(), $e->getLine());
 234          msg(hsc($e->getMessage()), -1);
 235          return false;
 236      }
 237  
 238      // fetch user info from backend
 239      $user = $authtoken->getUser();
 240      $USERINFO = $auth->getUserData($user);
 241      if (!$USERINFO) return false;
 242  
 243      // the code is correct, set up user
 244      $INPUT->server->set('REMOTE_USER', $user);
 245      $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
 246      $_SESSION[DOKU_COOKIE]['auth']['pass'] = 'nope';
 247      $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
 248      $_SESSION[DOKU_COOKIE]['auth']['token'] = $token;
 249  
 250      return true;
 251  }
 252  
 253  /**
 254   * Event hook callback for AUTH_LOGIN_CHECK
 255   *
 256   * @param array $evdata
 257   * @return bool
 258   * @throws Exception
 259   */
 260  function auth_login_wrapper($evdata)
 261  {
 262      return auth_login(
 263          $evdata['user'],
 264          $evdata['password'],
 265          $evdata['sticky'],
 266          $evdata['silent']
 267      );
 268  }
 269  
 270  /**
 271   * This tries to login the user based on the sent auth credentials
 272   *
 273   * The authentication works like this: if a username was given
 274   * a new login is assumed and user/password are checked. If they
 275   * are correct the password is encrypted with blowfish and stored
 276   * together with the username in a cookie - the same info is stored
 277   * in the session, too. Additonally a browserID is stored in the
 278   * session.
 279   *
 280   * If no username was given the cookie is checked: if the username,
 281   * crypted password and browserID match between session and cookie
 282   * no further testing is done and the user is accepted
 283   *
 284   * If a cookie was found but no session info was availabe the
 285   * blowfish encrypted password from the cookie is decrypted and
 286   * together with username rechecked by calling this function again.
 287   *
 288   * On a successful login $_SERVER[REMOTE_USER] and $USERINFO
 289   * are set.
 290   *
 291   * @param string $user Username
 292   * @param string $pass Cleartext Password
 293   * @param bool $sticky Cookie should not expire
 294   * @param bool $silent Don't show error on bad auth
 295   * @return bool true on successful auth
 296   * @throws Exception
 297   *
 298   * @author  Andreas Gohr <andi@splitbrain.org>
 299   */
 300  function auth_login($user, $pass, $sticky = false, $silent = false)
 301  {
 302      global $USERINFO;
 303      global $conf;
 304      global $lang;
 305      /* @var AuthPlugin $auth */
 306      global $auth;
 307      /* @var Input $INPUT */
 308      global $INPUT;
 309  
 310      if (!$auth instanceof AuthPlugin) return false;
 311  
 312      if (!empty($user)) {
 313          //usual login
 314          if (!empty($pass)) usleep(random_int(0, 250)); // add a random delay to prevent timing attacks #4491
 315          if (!empty($pass) && $auth->checkPass($user, $pass)) {
 316              // make logininfo globally available
 317              $INPUT->server->set('REMOTE_USER', $user);
 318              $secret                 = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
 319              auth_setCookie($user, auth_encrypt($pass, $secret), $sticky);
 320              return true;
 321          }
 322          //invalid credentials - log off
 323          if (!$silent) {
 324              http_status(403, 'Login failed');
 325              msg($lang['badlogin'], -1);
 326          }
 327          auth_logoff();
 328          return false;
 329      }
 330      // read cookie information
 331      [$user, $sticky, $pass] = auth_getCookie();
 332      if ($user && $pass) {
 333          // we got a cookie - see if we can trust it
 334  
 335          // get session info
 336          if (isset($_SESSION[DOKU_COOKIE])) {
 337              $session = $_SESSION[DOKU_COOKIE]['auth'] ?? [];
 338              if (
 339                  isset($session['user']) &&
 340                  isset($session['pass']) &&
 341                  $auth->useSessionCache($user) &&
 342                  ($session['time'] >= time() - $conf['auth_security_timeout']) &&
 343                  ($session['user'] === $user) &&
 344                  ($session['pass'] === sha1($pass)) && //still crypted
 345                  ($session['buid'] === auth_browseruid())
 346              ) {
 347                  // he has session, cookie and browser right - let him in
 348                  $INPUT->server->set('REMOTE_USER', $user);
 349                  $USERINFO = $session['info']; //FIXME move all references to session
 350                  return true;
 351              }
 352          }
 353          // no we don't trust it yet - recheck pass but silent
 354          $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
 355          $pass   = auth_decrypt($pass, $secret);
 356          return auth_login($user, $pass, $sticky, true);
 357      }
 358      //just to be sure
 359      auth_logoff(true);
 360      return false;
 361  }
 362  
 363  /**
 364   * Builds a pseudo UID from browser and IP data
 365   *
 366   * This is neither unique nor unfakable - still it adds some
 367   * security. Using the first part of the IP makes sure
 368   * proxy farms like AOLs are still okay.
 369   *
 370   * @author  Andreas Gohr <andi@splitbrain.org>
 371   *
 372   * @return  string  a SHA256 sum of various browser headers
 373   */
 374  function auth_browseruid()
 375  {
 376      /* @var Input $INPUT */
 377      global $INPUT;
 378  
 379      $ip = clientIP(true);
 380      // convert IP string to packed binary representation
 381      $pip = inet_pton($ip);
 382  
 383      $uid = implode("\n", [
 384          $INPUT->server->str('HTTP_USER_AGENT'),
 385          $INPUT->server->str('HTTP_ACCEPT_LANGUAGE'),
 386          substr($pip, 0, strlen($pip) / 2), // use half of the IP address (works for both IPv4 and IPv6)
 387      ]);
 388      return hash('sha256', $uid);
 389  }
 390  
 391  /**
 392   * Creates a random key to encrypt the password in cookies
 393   *
 394   * This function tries to read the password for encrypting
 395   * cookies from $conf['metadir'].'/_htcookiesalt'
 396   * if no such file is found a random key is created and
 397   * and stored in this file.
 398   *
 399   * @param bool $addsession if true, the sessionid is added to the salt
 400   * @param bool $secure if security is more important than keeping the old value
 401   * @return  string
 402   * @throws Exception
 403   *
 404   * @author  Andreas Gohr <andi@splitbrain.org>
 405   */
 406  function auth_cookiesalt($addsession = false, $secure = false)
 407  {
 408      if (defined('SIMPLE_TEST')) {
 409          return 'test';
 410      }
 411      global $conf;
 412      $file = $conf['metadir'] . '/_htcookiesalt';
 413      if ($secure || !file_exists($file)) {
 414          $file = $conf['metadir'] . '/_htcookiesalt2';
 415      }
 416      $salt = io_readFile($file);
 417      if (empty($salt)) {
 418          $salt = bin2hex(auth_randombytes(64));
 419          io_saveFile($file, $salt);
 420      }
 421      if ($addsession) {
 422          $salt .= session_id();
 423      }
 424      return $salt;
 425  }
 426  
 427  /**
 428   * Return cryptographically secure random bytes.
 429   *
 430   * @param int $length number of bytes
 431   * @return string cryptographically secure random bytes
 432   * @throws Exception
 433   *
 434   * @author Niklas Keller <me@kelunik.com>
 435   */
 436  function auth_randombytes($length)
 437  {
 438      return random_bytes($length);
 439  }
 440  
 441  /**
 442   * Cryptographically secure random number generator.
 443   *
 444   * @param int $min
 445   * @param int $max
 446   * @return int
 447   * @throws Exception
 448   *
 449   * @author Niklas Keller <me@kelunik.com>
 450   */
 451  function auth_random($min, $max)
 452  {
 453      return random_int($min, $max);
 454  }
 455  
 456  /**
 457   * Encrypt data using the given secret using AES
 458   *
 459   * The mode is CBC with a random initialization vector, the key is derived
 460   * using pbkdf2.
 461   *
 462   * @param string $data The data that shall be encrypted
 463   * @param string $secret The secret/password that shall be used
 464   * @return string The ciphertext
 465   * @throws Exception
 466   */
 467  function auth_encrypt($data, $secret)
 468  {
 469      $iv     = auth_randombytes(16);
 470      $cipher = new AES('cbc');
 471      $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
 472      $cipher->setIV($iv);
 473  
 474      /*
 475      this uses the encrypted IV as IV as suggested in
 476      http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
 477      for unique but necessarily random IVs. The resulting ciphertext is
 478      compatible to ciphertext that was created using a "normal" IV.
 479      */
 480      return $cipher->encrypt($iv . $data);
 481  }
 482  
 483  /**
 484   * Decrypt the given AES ciphertext
 485   *
 486   * The mode is CBC, the key is derived using pbkdf2
 487   *
 488   * @param string $ciphertext The encrypted data
 489   * @param string $secret     The secret/password that shall be used
 490   * @return string|null The decrypted data
 491   */
 492  function auth_decrypt($ciphertext, $secret)
 493  {
 494      $iv     = substr($ciphertext, 0, 16);
 495      $cipher = new AES('cbc');
 496      $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
 497      $cipher->setIV($iv);
 498  
 499      try {
 500          return $cipher->decrypt(substr($ciphertext, 16));
 501      } catch (BadDecryptionException $e) {
 502          ErrorHandler::logException($e);
 503          return null;
 504      }
 505  }
 506  
 507  /**
 508   * Log out the current user
 509   *
 510   * This clears all authentication data and thus log the user
 511   * off. It also clears session data.
 512   *
 513   * @author  Andreas Gohr <andi@splitbrain.org>
 514   *
 515   * @param bool $keepbc - when true, the breadcrumb data is not cleared
 516   */
 517  function auth_logoff($keepbc = false)
 518  {
 519      global $conf;
 520      global $USERINFO;
 521      /* @var AuthPlugin $auth */
 522      global $auth;
 523      /* @var Input $INPUT */
 524      global $INPUT;
 525  
 526      // make sure the session is writable (it usually is)
 527      @session_start();
 528  
 529      if (isset($_SESSION[DOKU_COOKIE]['auth']['user']))
 530          unset($_SESSION[DOKU_COOKIE]['auth']['user']);
 531      if (isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
 532          unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
 533      if (isset($_SESSION[DOKU_COOKIE]['auth']['info']))
 534          unset($_SESSION[DOKU_COOKIE]['auth']['info']);
 535      if (!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
 536          unset($_SESSION[DOKU_COOKIE]['bc']);
 537      $INPUT->server->remove('REMOTE_USER');
 538      $USERINFO = null; //FIXME
 539  
 540      $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
 541      setcookie(DOKU_COOKIE, '', [
 542          'expires' => time() - 600000,
 543          'path' => $cookieDir,
 544          'secure' => ($conf['securecookie'] && Ip::isSsl()),
 545          'httponly' => true,
 546          'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
 547      ]);
 548  
 549      if ($auth instanceof AuthPlugin) {
 550          $auth->logOff();
 551      }
 552  }
 553  
 554  /**
 555   * Check if a user is a manager
 556   *
 557   * Should usually be called without any parameters to check the current
 558   * user.
 559   *
 560   * The info is available through $INFO['ismanager'], too
 561   *
 562   * @param string $user Username
 563   * @param array $groups List of groups the user is in
 564   * @param bool $adminonly when true checks if user is admin
 565   * @param bool $recache set to true to refresh the cache
 566   * @return bool
 567   * @see    auth_isadmin
 568   *
 569   * @author Andreas Gohr <andi@splitbrain.org>
 570   */
 571  function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache = false)
 572  {
 573      global $conf;
 574      global $USERINFO;
 575      /* @var AuthPlugin $auth */
 576      global $auth;
 577      /* @var Input $INPUT */
 578      global $INPUT;
 579  
 580  
 581      if (!$auth instanceof AuthPlugin) return false;
 582      if (is_null($user)) {
 583          if (!$INPUT->server->has('REMOTE_USER')) {
 584              return false;
 585          }
 586          $user = $INPUT->server->str('REMOTE_USER');
 587      }
 588      if (is_null($groups)) {
 589          // checking the logged in user, or another one?
 590          if ($USERINFO && $user === $INPUT->server->str('REMOTE_USER')) {
 591              $groups =  (array) $USERINFO['grps'];
 592          } else {
 593              $groups = $auth->getUserData($user);
 594              $groups = $groups ? $groups['grps'] : [];
 595          }
 596      }
 597  
 598      // prefer cached result
 599      static $cache = [];
 600      $cachekey = serialize([$user, $adminonly, $groups]);
 601      if (!isset($cache[$cachekey]) || $recache) {
 602          // check superuser match
 603          $ok = auth_isMember($conf['superuser'], $user, $groups);
 604  
 605          // check managers
 606          if (!$ok && !$adminonly) {
 607              $ok = auth_isMember($conf['manager'], $user, $groups);
 608          }
 609  
 610          $cache[$cachekey] = $ok;
 611      }
 612  
 613      return $cache[$cachekey];
 614  }
 615  
 616  /**
 617   * Check if a user is admin
 618   *
 619   * Alias to auth_ismanager with adminonly=true
 620   *
 621   * The info is available through $INFO['isadmin'], too
 622   *
 623   * @param string $user Username
 624   * @param array $groups List of groups the user is in
 625   * @param bool $recache set to true to refresh the cache
 626   * @return bool
 627   * @author Andreas Gohr <andi@splitbrain.org>
 628   * @see auth_ismanager()
 629   *
 630   */
 631  function auth_isadmin($user = null, $groups = null, $recache = false)
 632  {
 633      return auth_ismanager($user, $groups, true, $recache);
 634  }
 635  
 636  /**
 637   * Match a user and his groups against a comma separated list of
 638   * users and groups to determine membership status
 639   *
 640   * Note: all input should NOT be nameencoded.
 641   *
 642   * @param string $memberlist commaseparated list of allowed users and groups
 643   * @param string $user       user to match against
 644   * @param array  $groups     groups the user is member of
 645   * @return bool       true for membership acknowledged
 646   */
 647  function auth_isMember($memberlist, $user, array $groups)
 648  {
 649      /* @var AuthPlugin $auth */
 650      global $auth;
 651      if (!$auth instanceof AuthPlugin) return false;
 652  
 653      // numeric names may arrive as integers when they were used as array keys
 654      // (PHP coerces numeric keys); cast to string so the strict comparisons
 655      // below match again without weakening them
 656      $user   = (string) $user;
 657      $groups = array_map(strval(...), $groups);
 658  
 659      // clean user and groups
 660      if (!$auth->isCaseSensitive()) {
 661          $user   = PhpString::strtolower($user);
 662          $groups = array_map(PhpString::strtolower(...), $groups);
 663      }
 664      $user   = $auth->cleanUser($user);
 665      $groups = array_map($auth->cleanGroup(...), $groups);
 666  
 667      // extract the memberlist
 668      $members = explode(',', $memberlist);
 669      $members = array_map(trim(...), $members);
 670      $members = array_unique($members);
 671      $members = array_filter($members);
 672  
 673      // compare cleaned values
 674      foreach ($members as $member) {
 675          if ($member == '@ALL') return true;
 676          if (!$auth->isCaseSensitive()) $member = PhpString::strtolower($member);
 677          if ($member[0] == '@') {
 678              $member = $auth->cleanGroup(substr($member, 1));
 679              if (in_array($member, $groups, true)) return true;
 680          } else {
 681              $member = $auth->cleanUser($member);
 682              if ($member === $user) return true;
 683          }
 684      }
 685  
 686      // still here? not a member!
 687      return false;
 688  }
 689  
 690  /**
 691   * Convinience function for auth_aclcheck()
 692   *
 693   * This checks the permissions for the current user
 694   *
 695   * @author  Andreas Gohr <andi@splitbrain.org>
 696   *
 697   * @param  string  $id  page ID (needs to be resolved and cleaned)
 698   * @return int          permission level
 699   */
 700  function auth_quickaclcheck($id)
 701  {
 702      global $conf;
 703      global $USERINFO;
 704      /* @var Input $INPUT */
 705      global $INPUT;
 706      # if no ACL is used always return upload rights
 707      if (!$conf['useacl']) return AUTH_UPLOAD;
 708      return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : []);
 709  }
 710  
 711  /**
 712   * Build the ACL path for a media file.
 713   *
 714   * Media files do not have per-file ACLs; permissions are always evaluated against the namespace
 715   * they live in. This returns the namespace wildcard path (e.g. "wiki:*" or "*" for root-namespace
 716   * media) suitable for passing to auth_quickaclcheck() or auth_aclcheck().
 717   *
 718   * @param string $id media ID (needs to be resolved and cleaned)
 719   * @return string the ACL path to check
 720   */
 721  function mediaAclPath($id)
 722  {
 723      return ltrim(getNS($id) . ':*', ':');
 724  }
 725  
 726  /**
 727   * Returns the maximum rights a user has for the given ID or its namespace
 728   *
 729   * @author  Andreas Gohr <andi@splitbrain.org>
 730   *
 731   * @triggers AUTH_ACL_CHECK
 732   * @param  string       $id     page ID (needs to be resolved and cleaned)
 733   * @param  string       $user   Username
 734   * @param  array|null   $groups Array of groups the user is in
 735   * @return int             permission level
 736   */
 737  function auth_aclcheck($id, $user, $groups)
 738  {
 739      $data = [
 740          'id'     => $id ?? '',
 741          'user'   => $user,
 742          'groups' => $groups
 743      ];
 744  
 745      return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
 746  }
 747  
 748  /**
 749   * default ACL check method
 750   *
 751   * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
 752   *
 753   * @author  Andreas Gohr <andi@splitbrain.org>
 754   *
 755   * @param  array $data event data
 756   * @return int   permission level
 757   */
 758  function auth_aclcheck_cb($data)
 759  {
 760      $id     =& $data['id'];
 761      $user   =& $data['user'];
 762      $groups =& $data['groups'];
 763  
 764      global $conf;
 765      global $AUTH_ACL;
 766      /* @var AuthPlugin $auth */
 767      global $auth;
 768  
 769      // if no ACL is used always return upload rights
 770      if (!$conf['useacl']) return AUTH_UPLOAD;
 771      if (!$auth instanceof AuthPlugin) return AUTH_NONE;
 772      if (!is_array($AUTH_ACL)) return AUTH_NONE;
 773  
 774      //make sure groups is an array
 775      if (!is_array($groups)) $groups = [];
 776  
 777      //if user is superuser or in superusergroup return 255 (acl_admin)
 778      if (auth_isadmin($user, $groups)) {
 779          return AUTH_ADMIN;
 780      }
 781  
 782      if (!$auth->isCaseSensitive()) {
 783          $user   = PhpString::strtolower($user);
 784          $groups = array_map(PhpString::strtolower(...), $groups);
 785      }
 786      $user   = auth_nameencode($auth->cleanUser($user));
 787      $groups = array_map($auth->cleanGroup(...), $groups);
 788  
 789      //prepend groups with @ and nameencode
 790      foreach ($groups as &$group) {
 791          $group = '@' . auth_nameencode($group);
 792      }
 793  
 794      $ns   = getNS($id);
 795      $perm = -1;
 796  
 797      //add ALL group
 798      $groups[] = '@ALL';
 799  
 800      //add User
 801      if ($user) $groups[] = $user;
 802  
 803      //check exact match first
 804      $matches = preg_grep('/^' . preg_quote($id, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
 805      if (count($matches)) {
 806          foreach ($matches as $match) {
 807              $match = preg_replace('/#.*$/', '', $match); //ignore comments
 808              $acl   = preg_split('/[ \t]+/', $match);
 809              if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
 810                  $acl[1] = PhpString::strtolower($acl[1]);
 811              }
 812              if (!in_array($acl[1], $groups, true)) {
 813                  continue;
 814              }
 815              if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
 816              if ($acl[2] > $perm) {
 817                  $perm = $acl[2];
 818              }
 819          }
 820          if ($perm > -1) {
 821              //we had a match - return it
 822              return (int) $perm;
 823          }
 824      }
 825  
 826      //still here? do the namespace checks
 827      if ($ns) {
 828          $path = $ns . ':*';
 829      } else {
 830          $path = '*'; //root document
 831      }
 832  
 833      do {
 834          $matches = preg_grep('/^' . preg_quote($path, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
 835          if (count($matches)) {
 836              foreach ($matches as $match) {
 837                  $match = preg_replace('/#.*$/', '', $match); //ignore comments
 838                  $acl   = preg_split('/[ \t]+/', $match);
 839                  if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
 840                      $acl[1] = PhpString::strtolower($acl[1]);
 841                  }
 842                  if (!in_array($acl[1], $groups, true)) {
 843                      continue;
 844                  }
 845                  if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
 846                  if ($acl[2] > $perm) {
 847                      $perm = $acl[2];
 848                  }
 849              }
 850              //we had a match - return it
 851              if ($perm != -1) {
 852                  return (int) $perm;
 853              }
 854          }
 855          //get next higher namespace
 856          $ns = getNS($ns);
 857  
 858          if ($path != '*') {
 859              $path = $ns . ':*';
 860              if ($path == ':*') $path = '*';
 861          } else {
 862              //we did this already
 863              //looks like there is something wrong with the ACL
 864              //break here
 865              msg('No ACL setup yet! Denying access to everyone.');
 866              return AUTH_NONE;
 867          }
 868      } while (1); //this should never loop endless
 869      return AUTH_NONE;
 870  }
 871  
 872  /**
 873   * Encode ASCII special chars
 874   *
 875   * Some auth backends allow special chars in their user and groupnames
 876   * The special chars are encoded with this function. Only ASCII chars
 877   * are encoded UTF-8 multibyte are left as is (different from usual
 878   * urlencoding!).
 879   *
 880   * Decoding can be done with rawurldecode
 881   *
 882   * @author Andreas Gohr <gohr@cosmocode.de>
 883   * @see rawurldecode()
 884   *
 885   * @param string $name
 886   * @param bool $skip_group
 887   * @return string
 888   */
 889  function auth_nameencode($name, $skip_group = false)
 890  {
 891      global $cache_authname;
 892      $cache =& $cache_authname;
 893      $name  = (string) $name;
 894  
 895      // never encode wildcard FS#1955
 896      if ($name == '%USER%') return $name;
 897      if ($name == '%GROUP%') return $name;
 898  
 899      if (!isset($cache[$name][$skip_group])) {
 900          if ($skip_group && $name[0] == '@') {
 901              $cache[$name][$skip_group] = '@' . preg_replace_callback(
 902                  '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
 903                  auth_nameencode_callback(...),
 904                  substr($name, 1)
 905              );
 906          } else {
 907              $cache[$name][$skip_group] = preg_replace_callback(
 908                  '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
 909                  auth_nameencode_callback(...),
 910                  $name
 911              );
 912          }
 913      }
 914  
 915      return $cache[$name][$skip_group];
 916  }
 917  
 918  /**
 919   * callback encodes the matches
 920   *
 921   * @param array $matches first complete match, next matching subpatterms
 922   * @return string
 923   */
 924  function auth_nameencode_callback($matches)
 925  {
 926      return '%' . dechex(ord(substr($matches[1], -1)));
 927  }
 928  
 929  /**
 930   * Create a pronouncable password
 931   *
 932   * The $foruser variable might be used by plugins to run additional password
 933   * policy checks, but is not used by the default implementation
 934   *
 935   * @param string $foruser username for which the password is generated
 936   * @return string  pronouncable password
 937   * @throws Exception
 938   *
 939   * @link     http://www.phpbuilder.com/annotate/message.php3?id=1014451
 940   * @triggers AUTH_PASSWORD_GENERATE
 941   *
 942   * @author   Andreas Gohr <andi@splitbrain.org>
 943   */
 944  function auth_pwgen($foruser = '')
 945  {
 946      $data = [
 947          'password' => '',
 948          'foruser'  => $foruser
 949      ];
 950  
 951      $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
 952      if ($evt->advise_before(true)) {
 953          $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
 954          $v = 'aeiou'; //vowels
 955          $a = $c . $v; //both
 956          $s = '!$%&?+*~#-_:.;,'; // specials
 957  
 958          //use thre syllables...
 959          for ($i = 0; $i < 3; $i++) {
 960              $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
 961              $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
 962              $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
 963          }
 964          //... and add a nice number and special
 965          $data['password'] .= $s[auth_random(0, strlen($s) - 1)] . auth_random(10, 99);
 966      }
 967      $evt->advise_after();
 968  
 969      return $data['password'];
 970  }
 971  
 972  /**
 973   * Sends a password to the given user
 974   *
 975   * @author  Andreas Gohr <andi@splitbrain.org>
 976   *
 977   * @param string $user Login name of the user
 978   * @param string $password The new password in clear text
 979   * @return bool  true on success
 980   */
 981  function auth_sendPassword($user, $password)
 982  {
 983      global $lang;
 984      /* @var AuthPlugin $auth */
 985      global $auth;
 986      if (!$auth instanceof AuthPlugin) return false;
 987  
 988      $user     = $auth->cleanUser($user);
 989      $userinfo = $auth->getUserData($user, false);
 990  
 991      if (!$userinfo['mail']) return false;
 992  
 993      $text = rawLocale('password');
 994      $trep = [
 995          'FULLNAME' => $userinfo['name'],
 996          'LOGIN'    => $user,
 997          'PASSWORD' => $password
 998      ];
 999  
1000      $mail = new Mailer();
1001      $mail->to($mail->getCleanName($userinfo['name']) . ' <' . $userinfo['mail'] . '>');
1002      $mail->subject($lang['regpwmail']);
1003      $mail->setBody($text, $trep);
1004      return $mail->send();
1005  }
1006  
1007  /**
1008   * Register a new user
1009   *
1010   * This registers a new user - Data is read directly from $_POST
1011   *
1012   * @return bool  true on success, false on any error
1013   * @throws Exception
1014   *
1015   * @author  Andreas Gohr <andi@splitbrain.org>
1016   */
1017  function register()
1018  {
1019      global $lang;
1020      global $conf;
1021      /* @var AuthPlugin $auth */
1022      global $auth;
1023      global $INPUT;
1024  
1025      if (!$INPUT->post->bool('save')) return false;
1026      if (!actionOK('register')) return false;
1027  
1028      // gather input
1029      $login    = trim($auth->cleanUser($INPUT->post->str('login')));
1030      $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
1031      $email    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
1032      $pass     = $INPUT->post->str('pass');
1033      $passchk  = $INPUT->post->str('passchk');
1034  
1035      if (empty($login) || empty($fullname) || empty($email)) {
1036          msg($lang['regmissing'], -1);
1037          return false;
1038      }
1039  
1040      if ($conf['autopasswd']) {
1041          $pass = auth_pwgen($login); // automatically generate password
1042      } elseif (empty($pass) || empty($passchk)) {
1043          msg($lang['regmissing'], -1); // complain about missing passwords
1044          return false;
1045      } elseif ($pass != $passchk) {
1046          msg($lang['regbadpass'], -1); // complain about misspelled passwords
1047          return false;
1048      }
1049  
1050      //check mail
1051      if (!MailUtils::isValid($email)) {
1052          msg($lang['regbadmail'], -1);
1053          return false;
1054      }
1055  
1056      //okay try to create the user
1057      if (!$auth->triggerUserMod('create', [$login, $pass, $fullname, $email])) {
1058          msg($lang['regfail'], -1);
1059          return false;
1060      }
1061  
1062      // send notification about the new user
1063      $subscription = new RegistrationSubscriptionSender();
1064      $subscription->sendRegister($login, $fullname, $email);
1065  
1066      // are we done?
1067      if (!$conf['autopasswd']) {
1068          msg($lang['regsuccess2'], 1);
1069          return true;
1070      }
1071  
1072      // autogenerated password? then send password to user
1073      if (auth_sendPassword($login, $pass)) {
1074          msg($lang['regsuccess'], 1);
1075          return true;
1076      }
1077      msg($lang['regmailfail'], -1);
1078      return false;
1079  }
1080  
1081  /**
1082   * Update user profile
1083   *
1084   * @throws Exception
1085   *
1086   * @author    Christopher Smith <chris@jalakai.co.uk>
1087   */
1088  function updateprofile()
1089  {
1090      global $conf;
1091      global $lang;
1092      /* @var AuthPlugin $auth */
1093      global $auth;
1094      /* @var Input $INPUT */
1095      global $INPUT;
1096  
1097      if (!$INPUT->post->bool('save')) return false;
1098      if (!checkSecurityToken()) return false;
1099  
1100      if (!actionOK('profile')) {
1101          msg($lang['profna'], -1);
1102          return false;
1103      }
1104  
1105      $changes         = [];
1106      $changes['pass'] = $INPUT->post->str('newpass');
1107      $changes['name'] = $INPUT->post->str('fullname');
1108      $changes['mail'] = $INPUT->post->str('email');
1109  
1110      // check misspelled passwords
1111      if ($changes['pass'] != $INPUT->post->str('passchk')) {
1112          msg($lang['regbadpass'], -1);
1113          return false;
1114      }
1115  
1116      // clean fullname and email
1117      $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
1118      $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
1119  
1120      // no empty name and email (except the backend doesn't support them)
1121      if (
1122          (empty($changes['name']) && $auth->canDo('modName')) ||
1123          (empty($changes['mail']) && $auth->canDo('modMail'))
1124      ) {
1125          msg($lang['profnoempty'], -1);
1126          return false;
1127      }
1128      if (!MailUtils::isValid($changes['mail']) && $auth->canDo('modMail')) {
1129          msg($lang['regbadmail'], -1);
1130          return false;
1131      }
1132  
1133      $changes = array_filter($changes);
1134  
1135      // check for unavailable capabilities
1136      if (!$auth->canDo('modName')) unset($changes['name']);
1137      if (!$auth->canDo('modMail')) unset($changes['mail']);
1138      if (!$auth->canDo('modPass')) unset($changes['pass']);
1139  
1140      // anything to do?
1141      if ($changes === []) {
1142          msg($lang['profnochange'], -1);
1143          return false;
1144      }
1145  
1146      if ($conf['profileconfirm']) {
1147          if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1148              msg($lang['badpassconfirm'], -1);
1149              return false;
1150          }
1151      }
1152  
1153      if (!$auth->triggerUserMod('modify', [$INPUT->server->str('REMOTE_USER'), &$changes])) {
1154          msg($lang['proffail'], -1);
1155          return false;
1156      }
1157  
1158      if (array_key_exists('pass', $changes) && $changes['pass']) {
1159          // update cookie and session with the changed data
1160          [/* user */, $sticky, /* pass */] = auth_getCookie();
1161          $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
1162          auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
1163      } else {
1164          // make sure the session is writable
1165          @session_start();
1166          // invalidate session cache
1167          $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
1168          session_write_close();
1169      }
1170  
1171      return true;
1172  }
1173  
1174  /**
1175   * Delete the current logged-in user
1176   *
1177   * @return bool true on success, false on any error
1178   */
1179  function auth_deleteprofile()
1180  {
1181      global $conf;
1182      global $lang;
1183      /* @var AuthPlugin $auth */
1184      global $auth;
1185      /* @var Input $INPUT */
1186      global $INPUT;
1187  
1188      if (!$INPUT->post->bool('delete')) return false;
1189      if (!checkSecurityToken()) return false;
1190  
1191      // action prevented or auth module disallows
1192      if (!actionOK('profile_delete') || !$auth->canDo('delUser')) {
1193          msg($lang['profnodelete'], -1);
1194          return false;
1195      }
1196  
1197      if (!$INPUT->post->bool('confirm_delete')) {
1198          msg($lang['profconfdeletemissing'], -1);
1199          return false;
1200      }
1201  
1202      if ($conf['profileconfirm']) {
1203          if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1204              msg($lang['badpassconfirm'], -1);
1205              return false;
1206          }
1207      }
1208  
1209      $deleted = [];
1210      $deleted[] = $INPUT->server->str('REMOTE_USER');
1211      if ($auth->triggerUserMod('delete', [$deleted])) {
1212          // force and immediate logout including removing the sticky cookie
1213          auth_logoff();
1214          return true;
1215      }
1216  
1217      return false;
1218  }
1219  
1220  /**
1221   * Send a  new password
1222   *
1223   * This function handles both phases of the password reset:
1224   *
1225   *   - handling the first request of password reset
1226   *   - validating the password reset auth token
1227   *
1228   * @return bool true on success, false on any error
1229   * @throws Exception
1230   *
1231   * @author Andreas Gohr <andi@splitbrain.org>
1232   * @author Benoit Chesneau <benoit@bchesneau.info>
1233   * @author Chris Smith <chris@jalakai.co.uk>
1234   */
1235  function act_resendpwd()
1236  {
1237      global $lang;
1238      global $conf;
1239      /* @var AuthPlugin $auth */
1240      global $auth;
1241      /* @var Input $INPUT */
1242      global $INPUT;
1243  
1244      if (!actionOK('resendpwd')) {
1245          msg($lang['resendna'], -1);
1246          return false;
1247      }
1248  
1249      $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
1250  
1251      if ($token) {
1252          // we're in token phase - get user info from token
1253  
1254          $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
1255          if (!file_exists($tfile)) {
1256              msg($lang['resendpwdbadauth'], -1);
1257              $INPUT->remove('pwauth');
1258              return false;
1259          }
1260          // token is only valid for 3 days
1261          if ((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
1262              msg($lang['resendpwdbadauth'], -1);
1263              $INPUT->remove('pwauth');
1264              @unlink($tfile);
1265              return false;
1266          }
1267  
1268          $user     = io_readfile($tfile);
1269          $userinfo = $auth->getUserData($user, false);
1270          if (!$userinfo['mail']) {
1271              msg($lang['resendpwdnouser'], -1);
1272              return false;
1273          }
1274  
1275          if (!$conf['autopasswd']) { // we let the user choose a password
1276              $pass = $INPUT->str('pass');
1277  
1278              // password given correctly?
1279              if (!$pass) return false;
1280              if ($pass != $INPUT->str('passchk')) {
1281                  msg($lang['regbadpass'], -1);
1282                  return false;
1283              }
1284  
1285              // change it
1286              if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
1287                  msg($lang['proffail'], -1);
1288                  return false;
1289              }
1290          } else { // autogenerate the password and send by mail
1291              $pass = auth_pwgen($user);
1292              if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
1293                  msg($lang['proffail'], -1);
1294                  return false;
1295              }
1296  
1297              if (auth_sendPassword($user, $pass)) {
1298                  msg($lang['resendpwdsuccess'], 1);
1299              } else {
1300                  msg($lang['regmailfail'], -1);
1301              }
1302          }
1303  
1304          @unlink($tfile);
1305          return true;
1306      }
1307      // we're in request phase
1308      if (!$INPUT->post->bool('save')) return false;
1309      if (!$INPUT->post->str('login')) {
1310          msg($lang['resendpwdmissing'], -1);
1311          return false;
1312      }
1313      $user = trim($auth->cleanUser($INPUT->post->str('login')));
1314      $userinfo = $auth->getUserData($user, false);
1315      if (!$userinfo['mail']) {
1316          msg($lang['resendpwdnouser'], -1);
1317          return false;
1318      }
1319      // generate auth token
1320      $token = md5(auth_randombytes(16)); // random secret
1321      $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
1322      $url   = wl('', ['do' => 'resendpwd', 'pwauth' => $token], true, '&');
1323      io_saveFile($tfile, $user);
1324      $text = rawLocale('pwconfirm');
1325      $trep = ['FULLNAME' => $userinfo['name'], 'LOGIN'    => $user, 'CONFIRM'  => $url];
1326      $mail = new Mailer();
1327      $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>');
1328      $mail->subject($lang['regpwmail']);
1329      $mail->setBody($text, $trep);
1330      if ($mail->send()) {
1331          msg($lang['resendpwdconfirm'], 1);
1332      } else {
1333          msg($lang['regmailfail'], -1);
1334      }
1335      return true;
1336  }
1337  
1338  /**
1339   * Encrypts a password using the given method and salt
1340   *
1341   * If the selected method needs a salt and none was given, a random one
1342   * is chosen.
1343   *
1344   * You can pass null as the password to create an unusable hash.
1345   *
1346   * @author  Andreas Gohr <andi@splitbrain.org>
1347   *
1348   * @param string $clear The clear text password
1349   * @param string $method The hashing method
1350   * @param string $salt A salt, null for random
1351   * @return  string  The crypted password
1352   */
1353  function auth_cryptPassword($clear, $method = '', $salt = null)
1354  {
1355      global $conf;
1356  
1357      if ($clear === null) {
1358          return DOKU_UNUSABLE_PASSWORD;
1359      }
1360  
1361      if (empty($method)) $method = $conf['passcrypt'];
1362  
1363      $pass = new PassHash();
1364      $call = 'hash_' . $method;
1365  
1366      if (!method_exists($pass, $call)) {
1367          msg("Unsupported crypt method $method", -1);
1368          return false;
1369      }
1370  
1371      return $pass->$call($clear, $salt);
1372  }
1373  
1374  /**
1375   * Verifies a cleartext password against a crypted hash
1376   *
1377   * @param string $clear The clear text password
1378   * @param string $crypt The hash to compare with
1379   * @return bool true if both match
1380   * @throws Exception
1381   *
1382   * @author Andreas Gohr <andi@splitbrain.org>
1383   */
1384  function auth_verifyPassword($clear, $crypt)
1385  {
1386      if ($crypt === DOKU_UNUSABLE_PASSWORD) {
1387          return false;
1388      }
1389  
1390      $pass = new PassHash();
1391      return $pass->verify_hash($clear, $crypt);
1392  }
1393  
1394  /**
1395   * Set the authentication cookie and add user identification data to the session
1396   *
1397   * @param string  $user       username
1398   * @param string  $pass       encrypted password
1399   * @param bool    $sticky     whether or not the cookie will last beyond the session
1400   * @return bool
1401   */
1402  function auth_setCookie($user, $pass, $sticky)
1403  {
1404      global $conf;
1405      /* @var AuthPlugin $auth */
1406      global $auth;
1407      global $USERINFO;
1408  
1409      if (!$auth instanceof AuthPlugin) return false;
1410      $USERINFO = $auth->getUserData($user);
1411  
1412      // set cookie
1413      $cookie    = base64_encode($user) . '|' . ((int) $sticky) . '|' . base64_encode($pass);
1414      $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1415      $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1416      setcookie(DOKU_COOKIE, $cookie, [
1417          'expires' => $time,
1418          'path' => $cookieDir,
1419          'secure' => ($conf['securecookie'] && Ip::isSsl()),
1420          'httponly' => true,
1421          'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
1422      ]);
1423  
1424      // set session
1425      $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1426      $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1427      $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1428      $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1429      $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1430  
1431      return true;
1432  }
1433  
1434  /**
1435   * Returns the user, (encrypted) password and sticky bit from cookie
1436   *
1437   * @returns array
1438   */
1439  function auth_getCookie()
1440  {
1441      if (!isset($_COOKIE[DOKU_COOKIE])) {
1442          return [null, null, null];
1443      }
1444      [$user, $sticky, $pass] = sexplode('|', $_COOKIE[DOKU_COOKIE], 3, '');
1445      $sticky = (bool) $sticky;
1446      $pass   = base64_decode($pass);
1447      $user   = base64_decode($user);
1448      return [$user, $sticky, $pass];
1449  }
1450  
1451  //Setup VIM: ex: et ts=2 :