[ 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          } else {
 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      } else {
 331          // read cookie information
 332          [$user, $sticky, $pass] = auth_getCookie();
 333          if ($user && $pass) {
 334              // we got a cookie - see if we can trust it
 335  
 336              // get session info
 337              if (isset($_SESSION[DOKU_COOKIE])) {
 338                  $session = $_SESSION[DOKU_COOKIE]['auth'] ?? [];
 339                  if (
 340                      isset($session['user']) &&
 341                      isset($session['pass']) &&
 342                      $auth->useSessionCache($user) &&
 343                      ($session['time'] >= time() - $conf['auth_security_timeout']) &&
 344                      ($session['user'] === $user) &&
 345                      ($session['pass'] === sha1($pass)) && //still crypted
 346                      ($session['buid'] === auth_browseruid())
 347                  ) {
 348                      // he has session, cookie and browser right - let him in
 349                      $INPUT->server->set('REMOTE_USER', $user);
 350                      $USERINFO = $session['info']; //FIXME move all references to session
 351                      return true;
 352                  }
 353              }
 354              // no we don't trust it yet - recheck pass but silent
 355              $secret = auth_cookiesalt(!$sticky, true); //bind non-sticky to session
 356              $pass   = auth_decrypt($pass, $secret);
 357              return auth_login($user, $pass, $sticky, true);
 358          }
 359      }
 360      //just to be sure
 361      auth_logoff(true);
 362      return false;
 363  }
 364  
 365  /**
 366   * Builds a pseudo UID from browser and IP data
 367   *
 368   * This is neither unique nor unfakable - still it adds some
 369   * security. Using the first part of the IP makes sure
 370   * proxy farms like AOLs are still okay.
 371   *
 372   * @author  Andreas Gohr <andi@splitbrain.org>
 373   *
 374   * @return  string  a SHA256 sum of various browser headers
 375   */
 376  function auth_browseruid()
 377  {
 378      /* @var Input $INPUT */
 379      global $INPUT;
 380  
 381      $ip = clientIP(true);
 382      // convert IP string to packed binary representation
 383      $pip = inet_pton($ip);
 384  
 385      $uid = implode("\n", [
 386          $INPUT->server->str('HTTP_USER_AGENT'),
 387          $INPUT->server->str('HTTP_ACCEPT_LANGUAGE'),
 388          substr($pip, 0, strlen($pip) / 2), // use half of the IP address (works for both IPv4 and IPv6)
 389      ]);
 390      return hash('sha256', $uid);
 391  }
 392  
 393  /**
 394   * Creates a random key to encrypt the password in cookies
 395   *
 396   * This function tries to read the password for encrypting
 397   * cookies from $conf['metadir'].'/_htcookiesalt'
 398   * if no such file is found a random key is created and
 399   * and stored in this file.
 400   *
 401   * @param bool $addsession if true, the sessionid is added to the salt
 402   * @param bool $secure if security is more important than keeping the old value
 403   * @return  string
 404   * @throws Exception
 405   *
 406   * @author  Andreas Gohr <andi@splitbrain.org>
 407   */
 408  function auth_cookiesalt($addsession = false, $secure = false)
 409  {
 410      if (defined('SIMPLE_TEST')) {
 411          return 'test';
 412      }
 413      global $conf;
 414      $file = $conf['metadir'] . '/_htcookiesalt';
 415      if ($secure || !file_exists($file)) {
 416          $file = $conf['metadir'] . '/_htcookiesalt2';
 417      }
 418      $salt = io_readFile($file);
 419      if (empty($salt)) {
 420          $salt = bin2hex(auth_randombytes(64));
 421          io_saveFile($file, $salt);
 422      }
 423      if ($addsession) {
 424          $salt .= session_id();
 425      }
 426      return $salt;
 427  }
 428  
 429  /**
 430   * Return cryptographically secure random bytes.
 431   *
 432   * @param int $length number of bytes
 433   * @return string cryptographically secure random bytes
 434   * @throws Exception
 435   *
 436   * @author Niklas Keller <me@kelunik.com>
 437   */
 438  function auth_randombytes($length)
 439  {
 440      return random_bytes($length);
 441  }
 442  
 443  /**
 444   * Cryptographically secure random number generator.
 445   *
 446   * @param int $min
 447   * @param int $max
 448   * @return int
 449   * @throws Exception
 450   *
 451   * @author Niklas Keller <me@kelunik.com>
 452   */
 453  function auth_random($min, $max)
 454  {
 455      return random_int($min, $max);
 456  }
 457  
 458  /**
 459   * Encrypt data using the given secret using AES
 460   *
 461   * The mode is CBC with a random initialization vector, the key is derived
 462   * using pbkdf2.
 463   *
 464   * @param string $data The data that shall be encrypted
 465   * @param string $secret The secret/password that shall be used
 466   * @return string The ciphertext
 467   * @throws Exception
 468   */
 469  function auth_encrypt($data, $secret)
 470  {
 471      $iv     = auth_randombytes(16);
 472      $cipher = new AES('cbc');
 473      $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
 474      $cipher->setIV($iv);
 475  
 476      /*
 477      this uses the encrypted IV as IV as suggested in
 478      http://csrc.nist.gov/publications/nistpubs/800-38a/sp800-38a.pdf, Appendix C
 479      for unique but necessarily random IVs. The resulting ciphertext is
 480      compatible to ciphertext that was created using a "normal" IV.
 481      */
 482      return $cipher->encrypt($iv . $data);
 483  }
 484  
 485  /**
 486   * Decrypt the given AES ciphertext
 487   *
 488   * The mode is CBC, the key is derived using pbkdf2
 489   *
 490   * @param string $ciphertext The encrypted data
 491   * @param string $secret     The secret/password that shall be used
 492   * @return string|null The decrypted data
 493   */
 494  function auth_decrypt($ciphertext, $secret)
 495  {
 496      $iv     = substr($ciphertext, 0, 16);
 497      $cipher = new AES('cbc');
 498      $cipher->setPassword($secret, 'pbkdf2', 'sha1', 'phpseclib');
 499      $cipher->setIV($iv);
 500  
 501      try {
 502          return $cipher->decrypt(substr($ciphertext, 16));
 503      } catch (BadDecryptionException $e) {
 504          ErrorHandler::logException($e);
 505          return null;
 506      }
 507  }
 508  
 509  /**
 510   * Log out the current user
 511   *
 512   * This clears all authentication data and thus log the user
 513   * off. It also clears session data.
 514   *
 515   * @author  Andreas Gohr <andi@splitbrain.org>
 516   *
 517   * @param bool $keepbc - when true, the breadcrumb data is not cleared
 518   */
 519  function auth_logoff($keepbc = false)
 520  {
 521      global $conf;
 522      global $USERINFO;
 523      /* @var AuthPlugin $auth */
 524      global $auth;
 525      /* @var Input $INPUT */
 526      global $INPUT;
 527  
 528      // make sure the session is writable (it usually is)
 529      @session_start();
 530  
 531      if (isset($_SESSION[DOKU_COOKIE]['auth']['user']))
 532          unset($_SESSION[DOKU_COOKIE]['auth']['user']);
 533      if (isset($_SESSION[DOKU_COOKIE]['auth']['pass']))
 534          unset($_SESSION[DOKU_COOKIE]['auth']['pass']);
 535      if (isset($_SESSION[DOKU_COOKIE]['auth']['info']))
 536          unset($_SESSION[DOKU_COOKIE]['auth']['info']);
 537      if (!$keepbc && isset($_SESSION[DOKU_COOKIE]['bc']))
 538          unset($_SESSION[DOKU_COOKIE]['bc']);
 539      $INPUT->server->remove('REMOTE_USER');
 540      $USERINFO = null; //FIXME
 541  
 542      $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
 543      setcookie(DOKU_COOKIE, '', [
 544          'expires' => time() - 600000,
 545          'path' => $cookieDir,
 546          'secure' => ($conf['securecookie'] && Ip::isSsl()),
 547          'httponly' => true,
 548          'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
 549      ]);
 550  
 551      if ($auth instanceof AuthPlugin) {
 552          $auth->logOff();
 553      }
 554  }
 555  
 556  /**
 557   * Check if a user is a manager
 558   *
 559   * Should usually be called without any parameters to check the current
 560   * user.
 561   *
 562   * The info is available through $INFO['ismanager'], too
 563   *
 564   * @param string $user Username
 565   * @param array $groups List of groups the user is in
 566   * @param bool $adminonly when true checks if user is admin
 567   * @param bool $recache set to true to refresh the cache
 568   * @return bool
 569   * @see    auth_isadmin
 570   *
 571   * @author Andreas Gohr <andi@splitbrain.org>
 572   */
 573  function auth_ismanager($user = null, $groups = null, $adminonly = false, $recache = false)
 574  {
 575      global $conf;
 576      global $USERINFO;
 577      /* @var AuthPlugin $auth */
 578      global $auth;
 579      /* @var Input $INPUT */
 580      global $INPUT;
 581  
 582  
 583      if (!$auth instanceof AuthPlugin) return false;
 584      if (is_null($user)) {
 585          if (!$INPUT->server->has('REMOTE_USER')) {
 586              return false;
 587          } else {
 588              $user = $INPUT->server->str('REMOTE_USER');
 589          }
 590      }
 591      if (is_null($groups)) {
 592          // checking the logged in user, or another one?
 593          if ($USERINFO && $user === $INPUT->server->str('REMOTE_USER')) {
 594              $groups =  (array) $USERINFO['grps'];
 595          } else {
 596              $groups = $auth->getUserData($user);
 597              $groups = $groups ? $groups['grps'] : [];
 598          }
 599      }
 600  
 601      // prefer cached result
 602      static $cache = [];
 603      $cachekey = serialize([$user, $adminonly, $groups]);
 604      if (!isset($cache[$cachekey]) || $recache) {
 605          // check superuser match
 606          $ok = auth_isMember($conf['superuser'], $user, $groups);
 607  
 608          // check managers
 609          if (!$ok && !$adminonly) {
 610              $ok = auth_isMember($conf['manager'], $user, $groups);
 611          }
 612  
 613          $cache[$cachekey] = $ok;
 614      }
 615  
 616      return $cache[$cachekey];
 617  }
 618  
 619  /**
 620   * Check if a user is admin
 621   *
 622   * Alias to auth_ismanager with adminonly=true
 623   *
 624   * The info is available through $INFO['isadmin'], too
 625   *
 626   * @param string $user Username
 627   * @param array $groups List of groups the user is in
 628   * @param bool $recache set to true to refresh the cache
 629   * @return bool
 630   * @author Andreas Gohr <andi@splitbrain.org>
 631   * @see auth_ismanager()
 632   *
 633   */
 634  function auth_isadmin($user = null, $groups = null, $recache = false)
 635  {
 636      return auth_ismanager($user, $groups, true, $recache);
 637  }
 638  
 639  /**
 640   * Match a user and his groups against a comma separated list of
 641   * users and groups to determine membership status
 642   *
 643   * Note: all input should NOT be nameencoded.
 644   *
 645   * @param string $memberlist commaseparated list of allowed users and groups
 646   * @param string $user       user to match against
 647   * @param array  $groups     groups the user is member of
 648   * @return bool       true for membership acknowledged
 649   */
 650  function auth_isMember($memberlist, $user, array $groups)
 651  {
 652      /* @var AuthPlugin $auth */
 653      global $auth;
 654      if (!$auth instanceof AuthPlugin) return false;
 655  
 656      // clean user and groups
 657      if (!$auth->isCaseSensitive()) {
 658          $user   = PhpString::strtolower($user);
 659          $groups = array_map(PhpString::strtolower(...), $groups);
 660      }
 661      $user   = $auth->cleanUser($user);
 662      $groups = array_map($auth->cleanGroup(...), $groups);
 663  
 664      // extract the memberlist
 665      $members = explode(',', $memberlist);
 666      $members = array_map(trim(...), $members);
 667      $members = array_unique($members);
 668      $members = array_filter($members);
 669  
 670      // compare cleaned values
 671      foreach ($members as $member) {
 672          if ($member == '@ALL') return true;
 673          if (!$auth->isCaseSensitive()) $member = PhpString::strtolower($member);
 674          if ($member[0] == '@') {
 675              $member = $auth->cleanGroup(substr($member, 1));
 676              if (in_array($member, $groups, true)) return true;
 677          } else {
 678              $member = $auth->cleanUser($member);
 679              if ($member === $user) return true;
 680          }
 681      }
 682  
 683      // still here? not a member!
 684      return false;
 685  }
 686  
 687  /**
 688   * Convinience function for auth_aclcheck()
 689   *
 690   * This checks the permissions for the current user
 691   *
 692   * @author  Andreas Gohr <andi@splitbrain.org>
 693   *
 694   * @param  string  $id  page ID (needs to be resolved and cleaned)
 695   * @return int          permission level
 696   */
 697  function auth_quickaclcheck($id)
 698  {
 699      global $conf;
 700      global $USERINFO;
 701      /* @var Input $INPUT */
 702      global $INPUT;
 703      # if no ACL is used always return upload rights
 704      if (!$conf['useacl']) return AUTH_UPLOAD;
 705      return auth_aclcheck($id, $INPUT->server->str('REMOTE_USER'), is_array($USERINFO) ? $USERINFO['grps'] : []);
 706  }
 707  
 708  /**
 709   * Build the ACL path for a media file.
 710   *
 711   * Media files do not have per-file ACLs; permissions are always evaluated against the namespace
 712   * they live in. This returns the namespace wildcard path (e.g. "wiki:*" or "*" for root-namespace
 713   * media) suitable for passing to auth_quickaclcheck() or auth_aclcheck().
 714   *
 715   * @param string $id media ID (needs to be resolved and cleaned)
 716   * @return string the ACL path to check
 717   */
 718  function mediaAclPath($id)
 719  {
 720      return ltrim(getNS($id) . ':*', ':');
 721  }
 722  
 723  /**
 724   * Returns the maximum rights a user has for the given ID or its namespace
 725   *
 726   * @author  Andreas Gohr <andi@splitbrain.org>
 727   *
 728   * @triggers AUTH_ACL_CHECK
 729   * @param  string       $id     page ID (needs to be resolved and cleaned)
 730   * @param  string       $user   Username
 731   * @param  array|null   $groups Array of groups the user is in
 732   * @return int             permission level
 733   */
 734  function auth_aclcheck($id, $user, $groups)
 735  {
 736      $data = [
 737          'id'     => $id ?? '',
 738          'user'   => $user,
 739          'groups' => $groups
 740      ];
 741  
 742      return Event::createAndTrigger('AUTH_ACL_CHECK', $data, 'auth_aclcheck_cb');
 743  }
 744  
 745  /**
 746   * default ACL check method
 747   *
 748   * DO NOT CALL DIRECTLY, use auth_aclcheck() instead
 749   *
 750   * @author  Andreas Gohr <andi@splitbrain.org>
 751   *
 752   * @param  array $data event data
 753   * @return int   permission level
 754   */
 755  function auth_aclcheck_cb($data)
 756  {
 757      $id     =& $data['id'];
 758      $user   =& $data['user'];
 759      $groups =& $data['groups'];
 760  
 761      global $conf;
 762      global $AUTH_ACL;
 763      /* @var AuthPlugin $auth */
 764      global $auth;
 765  
 766      // if no ACL is used always return upload rights
 767      if (!$conf['useacl']) return AUTH_UPLOAD;
 768      if (!$auth instanceof AuthPlugin) return AUTH_NONE;
 769      if (!is_array($AUTH_ACL)) return AUTH_NONE;
 770  
 771      //make sure groups is an array
 772      if (!is_array($groups)) $groups = [];
 773  
 774      //if user is superuser or in superusergroup return 255 (acl_admin)
 775      if (auth_isadmin($user, $groups)) {
 776          return AUTH_ADMIN;
 777      }
 778  
 779      if (!$auth->isCaseSensitive()) {
 780          $user   = PhpString::strtolower($user);
 781          $groups = array_map(PhpString::strtolower(...), $groups);
 782      }
 783      $user   = auth_nameencode($auth->cleanUser($user));
 784      $groups = array_map($auth->cleanGroup(...), $groups);
 785  
 786      //prepend groups with @ and nameencode
 787      foreach ($groups as &$group) {
 788          $group = '@' . auth_nameencode($group);
 789      }
 790  
 791      $ns   = getNS($id);
 792      $perm = -1;
 793  
 794      //add ALL group
 795      $groups[] = '@ALL';
 796  
 797      //add User
 798      if ($user) $groups[] = $user;
 799  
 800      //check exact match first
 801      $matches = preg_grep('/^' . preg_quote($id, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
 802      if (count($matches)) {
 803          foreach ($matches as $match) {
 804              $match = preg_replace('/#.*$/', '', $match); //ignore comments
 805              $acl   = preg_split('/[ \t]+/', $match);
 806              if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
 807                  $acl[1] = PhpString::strtolower($acl[1]);
 808              }
 809              if (!in_array($acl[1], $groups, true)) {
 810                  continue;
 811              }
 812              if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
 813              if ($acl[2] > $perm) {
 814                  $perm = $acl[2];
 815              }
 816          }
 817          if ($perm > -1) {
 818              //we had a match - return it
 819              return (int) $perm;
 820          }
 821      }
 822  
 823      //still here? do the namespace checks
 824      if ($ns) {
 825          $path = $ns . ':*';
 826      } else {
 827          $path = '*'; //root document
 828      }
 829  
 830      do {
 831          $matches = preg_grep('/^' . preg_quote($path, '/') . '[ \t]+([^ \t]+)[ \t]+/', $AUTH_ACL);
 832          if (count($matches)) {
 833              foreach ($matches as $match) {
 834                  $match = preg_replace('/#.*$/', '', $match); //ignore comments
 835                  $acl   = preg_split('/[ \t]+/', $match);
 836                  if (!$auth->isCaseSensitive() && $acl[1] !== '@ALL') {
 837                      $acl[1] = PhpString::strtolower($acl[1]);
 838                  }
 839                  if (!in_array($acl[1], $groups, true)) {
 840                      continue;
 841                  }
 842                  if ($acl[2] > AUTH_DELETE) $acl[2] = AUTH_DELETE; //no admins in the ACL!
 843                  if ($acl[2] > $perm) {
 844                      $perm = $acl[2];
 845                  }
 846              }
 847              //we had a match - return it
 848              if ($perm != -1) {
 849                  return (int) $perm;
 850              }
 851          }
 852          //get next higher namespace
 853          $ns = getNS($ns);
 854  
 855          if ($path != '*') {
 856              $path = $ns . ':*';
 857              if ($path == ':*') $path = '*';
 858          } else {
 859              //we did this already
 860              //looks like there is something wrong with the ACL
 861              //break here
 862              msg('No ACL setup yet! Denying access to everyone.');
 863              return AUTH_NONE;
 864          }
 865      } while (1); //this should never loop endless
 866      return AUTH_NONE;
 867  }
 868  
 869  /**
 870   * Encode ASCII special chars
 871   *
 872   * Some auth backends allow special chars in their user and groupnames
 873   * The special chars are encoded with this function. Only ASCII chars
 874   * are encoded UTF-8 multibyte are left as is (different from usual
 875   * urlencoding!).
 876   *
 877   * Decoding can be done with rawurldecode
 878   *
 879   * @author Andreas Gohr <gohr@cosmocode.de>
 880   * @see rawurldecode()
 881   *
 882   * @param string $name
 883   * @param bool $skip_group
 884   * @return string
 885   */
 886  function auth_nameencode($name, $skip_group = false)
 887  {
 888      global $cache_authname;
 889      $cache =& $cache_authname;
 890      $name  = (string) $name;
 891  
 892      // never encode wildcard FS#1955
 893      if ($name == '%USER%') return $name;
 894      if ($name == '%GROUP%') return $name;
 895  
 896      if (!isset($cache[$name][$skip_group])) {
 897          if ($skip_group && $name[0] == '@') {
 898              $cache[$name][$skip_group] = '@' . preg_replace_callback(
 899                  '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
 900                  auth_nameencode_callback(...),
 901                  substr($name, 1)
 902              );
 903          } else {
 904              $cache[$name][$skip_group] = preg_replace_callback(
 905                  '/([\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f])/',
 906                  auth_nameencode_callback(...),
 907                  $name
 908              );
 909          }
 910      }
 911  
 912      return $cache[$name][$skip_group];
 913  }
 914  
 915  /**
 916   * callback encodes the matches
 917   *
 918   * @param array $matches first complete match, next matching subpatterms
 919   * @return string
 920   */
 921  function auth_nameencode_callback($matches)
 922  {
 923      return '%' . dechex(ord(substr($matches[1], -1)));
 924  }
 925  
 926  /**
 927   * Create a pronouncable password
 928   *
 929   * The $foruser variable might be used by plugins to run additional password
 930   * policy checks, but is not used by the default implementation
 931   *
 932   * @param string $foruser username for which the password is generated
 933   * @return string  pronouncable password
 934   * @throws Exception
 935   *
 936   * @link     http://www.phpbuilder.com/annotate/message.php3?id=1014451
 937   * @triggers AUTH_PASSWORD_GENERATE
 938   *
 939   * @author   Andreas Gohr <andi@splitbrain.org>
 940   */
 941  function auth_pwgen($foruser = '')
 942  {
 943      $data = [
 944          'password' => '',
 945          'foruser'  => $foruser
 946      ];
 947  
 948      $evt = new Event('AUTH_PASSWORD_GENERATE', $data);
 949      if ($evt->advise_before(true)) {
 950          $c = 'bcdfghjklmnprstvwz'; //consonants except hard to speak ones
 951          $v = 'aeiou'; //vowels
 952          $a = $c . $v; //both
 953          $s = '!$%&?+*~#-_:.;,'; // specials
 954  
 955          //use thre syllables...
 956          for ($i = 0; $i < 3; $i++) {
 957              $data['password'] .= $c[auth_random(0, strlen($c) - 1)];
 958              $data['password'] .= $v[auth_random(0, strlen($v) - 1)];
 959              $data['password'] .= $a[auth_random(0, strlen($a) - 1)];
 960          }
 961          //... and add a nice number and special
 962          $data['password'] .= $s[auth_random(0, strlen($s) - 1)] . auth_random(10, 99);
 963      }
 964      $evt->advise_after();
 965  
 966      return $data['password'];
 967  }
 968  
 969  /**
 970   * Sends a password to the given user
 971   *
 972   * @author  Andreas Gohr <andi@splitbrain.org>
 973   *
 974   * @param string $user Login name of the user
 975   * @param string $password The new password in clear text
 976   * @return bool  true on success
 977   */
 978  function auth_sendPassword($user, $password)
 979  {
 980      global $lang;
 981      /* @var AuthPlugin $auth */
 982      global $auth;
 983      if (!$auth instanceof AuthPlugin) return false;
 984  
 985      $user     = $auth->cleanUser($user);
 986      $userinfo = $auth->getUserData($user, false);
 987  
 988      if (!$userinfo['mail']) return false;
 989  
 990      $text = rawLocale('password');
 991      $trep = [
 992          'FULLNAME' => $userinfo['name'],
 993          'LOGIN'    => $user,
 994          'PASSWORD' => $password
 995      ];
 996  
 997      $mail = new Mailer();
 998      $mail->to($mail->getCleanName($userinfo['name']) . ' <' . $userinfo['mail'] . '>');
 999      $mail->subject($lang['regpwmail']);
1000      $mail->setBody($text, $trep);
1001      return $mail->send();
1002  }
1003  
1004  /**
1005   * Register a new user
1006   *
1007   * This registers a new user - Data is read directly from $_POST
1008   *
1009   * @return bool  true on success, false on any error
1010   * @throws Exception
1011   *
1012   * @author  Andreas Gohr <andi@splitbrain.org>
1013   */
1014  function register()
1015  {
1016      global $lang;
1017      global $conf;
1018      /* @var AuthPlugin $auth */
1019      global $auth;
1020      global $INPUT;
1021  
1022      if (!$INPUT->post->bool('save')) return false;
1023      if (!actionOK('register')) return false;
1024  
1025      // gather input
1026      $login    = trim($auth->cleanUser($INPUT->post->str('login')));
1027      $fullname = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('fullname')));
1028      $email    = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $INPUT->post->str('email')));
1029      $pass     = $INPUT->post->str('pass');
1030      $passchk  = $INPUT->post->str('passchk');
1031  
1032      if (empty($login) || empty($fullname) || empty($email)) {
1033          msg($lang['regmissing'], -1);
1034          return false;
1035      }
1036  
1037      if ($conf['autopasswd']) {
1038          $pass = auth_pwgen($login); // automatically generate password
1039      } elseif (empty($pass) || empty($passchk)) {
1040          msg($lang['regmissing'], -1); // complain about missing passwords
1041          return false;
1042      } elseif ($pass != $passchk) {
1043          msg($lang['regbadpass'], -1); // complain about misspelled passwords
1044          return false;
1045      }
1046  
1047      //check mail
1048      if (!MailUtils::isValid($email)) {
1049          msg($lang['regbadmail'], -1);
1050          return false;
1051      }
1052  
1053      //okay try to create the user
1054      if (!$auth->triggerUserMod('create', [$login, $pass, $fullname, $email])) {
1055          msg($lang['regfail'], -1);
1056          return false;
1057      }
1058  
1059      // send notification about the new user
1060      $subscription = new RegistrationSubscriptionSender();
1061      $subscription->sendRegister($login, $fullname, $email);
1062  
1063      // are we done?
1064      if (!$conf['autopasswd']) {
1065          msg($lang['regsuccess2'], 1);
1066          return true;
1067      }
1068  
1069      // autogenerated password? then send password to user
1070      if (auth_sendPassword($login, $pass)) {
1071          msg($lang['regsuccess'], 1);
1072          return true;
1073      } else {
1074          msg($lang['regmailfail'], -1);
1075          return false;
1076      }
1077  }
1078  
1079  /**
1080   * Update user profile
1081   *
1082   * @throws Exception
1083   *
1084   * @author    Christopher Smith <chris@jalakai.co.uk>
1085   */
1086  function updateprofile()
1087  {
1088      global $conf;
1089      global $lang;
1090      /* @var AuthPlugin $auth */
1091      global $auth;
1092      /* @var Input $INPUT */
1093      global $INPUT;
1094  
1095      if (!$INPUT->post->bool('save')) return false;
1096      if (!checkSecurityToken()) return false;
1097  
1098      if (!actionOK('profile')) {
1099          msg($lang['profna'], -1);
1100          return false;
1101      }
1102  
1103      $changes         = [];
1104      $changes['pass'] = $INPUT->post->str('newpass');
1105      $changes['name'] = $INPUT->post->str('fullname');
1106      $changes['mail'] = $INPUT->post->str('email');
1107  
1108      // check misspelled passwords
1109      if ($changes['pass'] != $INPUT->post->str('passchk')) {
1110          msg($lang['regbadpass'], -1);
1111          return false;
1112      }
1113  
1114      // clean fullname and email
1115      $changes['name'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['name']));
1116      $changes['mail'] = trim(preg_replace('/[\x00-\x1f:<>&%,;]+/', '', $changes['mail']));
1117  
1118      // no empty name and email (except the backend doesn't support them)
1119      if (
1120          (empty($changes['name']) && $auth->canDo('modName')) ||
1121          (empty($changes['mail']) && $auth->canDo('modMail'))
1122      ) {
1123          msg($lang['profnoempty'], -1);
1124          return false;
1125      }
1126      if (!MailUtils::isValid($changes['mail']) && $auth->canDo('modMail')) {
1127          msg($lang['regbadmail'], -1);
1128          return false;
1129      }
1130  
1131      $changes = array_filter($changes);
1132  
1133      // check for unavailable capabilities
1134      if (!$auth->canDo('modName')) unset($changes['name']);
1135      if (!$auth->canDo('modMail')) unset($changes['mail']);
1136      if (!$auth->canDo('modPass')) unset($changes['pass']);
1137  
1138      // anything to do?
1139      if ($changes === []) {
1140          msg($lang['profnochange'], -1);
1141          return false;
1142      }
1143  
1144      if ($conf['profileconfirm']) {
1145          if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1146              msg($lang['badpassconfirm'], -1);
1147              return false;
1148          }
1149      }
1150  
1151      if (!$auth->triggerUserMod('modify', [$INPUT->server->str('REMOTE_USER'), &$changes])) {
1152          msg($lang['proffail'], -1);
1153          return false;
1154      }
1155  
1156      if (array_key_exists('pass', $changes) && $changes['pass']) {
1157          // update cookie and session with the changed data
1158          [/* user */, $sticky, /* pass */] = auth_getCookie();
1159          $pass = auth_encrypt($changes['pass'], auth_cookiesalt(!$sticky, true));
1160          auth_setCookie($INPUT->server->str('REMOTE_USER'), $pass, (bool) $sticky);
1161      } else {
1162          // make sure the session is writable
1163          @session_start();
1164          // invalidate session cache
1165          $_SESSION[DOKU_COOKIE]['auth']['time'] = 0;
1166          session_write_close();
1167      }
1168  
1169      return true;
1170  }
1171  
1172  /**
1173   * Delete the current logged-in user
1174   *
1175   * @return bool true on success, false on any error
1176   */
1177  function auth_deleteprofile()
1178  {
1179      global $conf;
1180      global $lang;
1181      /* @var AuthPlugin $auth */
1182      global $auth;
1183      /* @var Input $INPUT */
1184      global $INPUT;
1185  
1186      if (!$INPUT->post->bool('delete')) return false;
1187      if (!checkSecurityToken()) return false;
1188  
1189      // action prevented or auth module disallows
1190      if (!actionOK('profile_delete') || !$auth->canDo('delUser')) {
1191          msg($lang['profnodelete'], -1);
1192          return false;
1193      }
1194  
1195      if (!$INPUT->post->bool('confirm_delete')) {
1196          msg($lang['profconfdeletemissing'], -1);
1197          return false;
1198      }
1199  
1200      if ($conf['profileconfirm']) {
1201          if (!$auth->checkPass($INPUT->server->str('REMOTE_USER'), $INPUT->post->str('oldpass'))) {
1202              msg($lang['badpassconfirm'], -1);
1203              return false;
1204          }
1205      }
1206  
1207      $deleted = [];
1208      $deleted[] = $INPUT->server->str('REMOTE_USER');
1209      if ($auth->triggerUserMod('delete', [$deleted])) {
1210          // force and immediate logout including removing the sticky cookie
1211          auth_logoff();
1212          return true;
1213      }
1214  
1215      return false;
1216  }
1217  
1218  /**
1219   * Send a  new password
1220   *
1221   * This function handles both phases of the password reset:
1222   *
1223   *   - handling the first request of password reset
1224   *   - validating the password reset auth token
1225   *
1226   * @return bool true on success, false on any error
1227   * @throws Exception
1228   *
1229   * @author Andreas Gohr <andi@splitbrain.org>
1230   * @author Benoit Chesneau <benoit@bchesneau.info>
1231   * @author Chris Smith <chris@jalakai.co.uk>
1232   */
1233  function act_resendpwd()
1234  {
1235      global $lang;
1236      global $conf;
1237      /* @var AuthPlugin $auth */
1238      global $auth;
1239      /* @var Input $INPUT */
1240      global $INPUT;
1241  
1242      if (!actionOK('resendpwd')) {
1243          msg($lang['resendna'], -1);
1244          return false;
1245      }
1246  
1247      $token = preg_replace('/[^a-f0-9]+/', '', $INPUT->str('pwauth'));
1248  
1249      if ($token) {
1250          // we're in token phase - get user info from token
1251  
1252          $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
1253          if (!file_exists($tfile)) {
1254              msg($lang['resendpwdbadauth'], -1);
1255              $INPUT->remove('pwauth');
1256              return false;
1257          }
1258          // token is only valid for 3 days
1259          if ((time() - filemtime($tfile)) > (3 * 60 * 60 * 24)) {
1260              msg($lang['resendpwdbadauth'], -1);
1261              $INPUT->remove('pwauth');
1262              @unlink($tfile);
1263              return false;
1264          }
1265  
1266          $user     = io_readfile($tfile);
1267          $userinfo = $auth->getUserData($user, false);
1268          if (!$userinfo['mail']) {
1269              msg($lang['resendpwdnouser'], -1);
1270              return false;
1271          }
1272  
1273          if (!$conf['autopasswd']) { // we let the user choose a password
1274              $pass = $INPUT->str('pass');
1275  
1276              // password given correctly?
1277              if (!$pass) return false;
1278              if ($pass != $INPUT->str('passchk')) {
1279                  msg($lang['regbadpass'], -1);
1280                  return false;
1281              }
1282  
1283              // change it
1284              if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
1285                  msg($lang['proffail'], -1);
1286                  return false;
1287              }
1288          } else { // autogenerate the password and send by mail
1289              $pass = auth_pwgen($user);
1290              if (!$auth->triggerUserMod('modify', [$user, ['pass' => $pass]])) {
1291                  msg($lang['proffail'], -1);
1292                  return false;
1293              }
1294  
1295              if (auth_sendPassword($user, $pass)) {
1296                  msg($lang['resendpwdsuccess'], 1);
1297              } else {
1298                  msg($lang['regmailfail'], -1);
1299              }
1300          }
1301  
1302          @unlink($tfile);
1303          return true;
1304      } else {
1305          // we're in request phase
1306  
1307          if (!$INPUT->post->bool('save')) return false;
1308  
1309          if (!$INPUT->post->str('login')) {
1310              msg($lang['resendpwdmissing'], -1);
1311              return false;
1312          } else {
1313              $user = trim($auth->cleanUser($INPUT->post->str('login')));
1314          }
1315  
1316          $userinfo = $auth->getUserData($user, false);
1317          if (!$userinfo['mail']) {
1318              msg($lang['resendpwdnouser'], -1);
1319              return false;
1320          }
1321  
1322          // generate auth token
1323          $token = md5(auth_randombytes(16)); // random secret
1324          $tfile = $conf['cachedir'] . '/' . $token[0] . '/' . $token . '.pwauth';
1325          $url   = wl('', ['do' => 'resendpwd', 'pwauth' => $token], true, '&');
1326  
1327          io_saveFile($tfile, $user);
1328  
1329          $text = rawLocale('pwconfirm');
1330          $trep = ['FULLNAME' => $userinfo['name'], 'LOGIN'    => $user, 'CONFIRM'  => $url];
1331  
1332          $mail = new Mailer();
1333          $mail->to($userinfo['name'] . ' <' . $userinfo['mail'] . '>');
1334          $mail->subject($lang['regpwmail']);
1335          $mail->setBody($text, $trep);
1336          if ($mail->send()) {
1337              msg($lang['resendpwdconfirm'], 1);
1338          } else {
1339              msg($lang['regmailfail'], -1);
1340          }
1341          return true;
1342      }
1343      // never reached
1344  }
1345  
1346  /**
1347   * Encrypts a password using the given method and salt
1348   *
1349   * If the selected method needs a salt and none was given, a random one
1350   * is chosen.
1351   *
1352   * You can pass null as the password to create an unusable hash.
1353   *
1354   * @author  Andreas Gohr <andi@splitbrain.org>
1355   *
1356   * @param string $clear The clear text password
1357   * @param string $method The hashing method
1358   * @param string $salt A salt, null for random
1359   * @return  string  The crypted password
1360   */
1361  function auth_cryptPassword($clear, $method = '', $salt = null)
1362  {
1363      global $conf;
1364  
1365      if ($clear === null) {
1366          return DOKU_UNUSABLE_PASSWORD;
1367      }
1368  
1369      if (empty($method)) $method = $conf['passcrypt'];
1370  
1371      $pass = new PassHash();
1372      $call = 'hash_' . $method;
1373  
1374      if (!method_exists($pass, $call)) {
1375          msg("Unsupported crypt method $method", -1);
1376          return false;
1377      }
1378  
1379      return $pass->$call($clear, $salt);
1380  }
1381  
1382  /**
1383   * Verifies a cleartext password against a crypted hash
1384   *
1385   * @param string $clear The clear text password
1386   * @param string $crypt The hash to compare with
1387   * @return bool true if both match
1388   * @throws Exception
1389   *
1390   * @author Andreas Gohr <andi@splitbrain.org>
1391   */
1392  function auth_verifyPassword($clear, $crypt)
1393  {
1394      if ($crypt === DOKU_UNUSABLE_PASSWORD) {
1395          return false;
1396      }
1397  
1398      $pass = new PassHash();
1399      return $pass->verify_hash($clear, $crypt);
1400  }
1401  
1402  /**
1403   * Set the authentication cookie and add user identification data to the session
1404   *
1405   * @param string  $user       username
1406   * @param string  $pass       encrypted password
1407   * @param bool    $sticky     whether or not the cookie will last beyond the session
1408   * @return bool
1409   */
1410  function auth_setCookie($user, $pass, $sticky)
1411  {
1412      global $conf;
1413      /* @var AuthPlugin $auth */
1414      global $auth;
1415      global $USERINFO;
1416  
1417      if (!$auth instanceof AuthPlugin) return false;
1418      $USERINFO = $auth->getUserData($user);
1419  
1420      // set cookie
1421      $cookie    = base64_encode($user) . '|' . ((int) $sticky) . '|' . base64_encode($pass);
1422      $cookieDir = empty($conf['cookiedir']) ? DOKU_REL : $conf['cookiedir'];
1423      $time      = $sticky ? (time() + 60 * 60 * 24 * 365) : 0; //one year
1424      setcookie(DOKU_COOKIE, $cookie, [
1425          'expires' => $time,
1426          'path' => $cookieDir,
1427          'secure' => ($conf['securecookie'] && Ip::isSsl()),
1428          'httponly' => true,
1429          'samesite' => $conf['samesitecookie'] ?: null, // null means browser default
1430      ]);
1431  
1432      // set session
1433      $_SESSION[DOKU_COOKIE]['auth']['user'] = $user;
1434      $_SESSION[DOKU_COOKIE]['auth']['pass'] = sha1($pass);
1435      $_SESSION[DOKU_COOKIE]['auth']['buid'] = auth_browseruid();
1436      $_SESSION[DOKU_COOKIE]['auth']['info'] = $USERINFO;
1437      $_SESSION[DOKU_COOKIE]['auth']['time'] = time();
1438  
1439      return true;
1440  }
1441  
1442  /**
1443   * Returns the user, (encrypted) password and sticky bit from cookie
1444   *
1445   * @returns array
1446   */
1447  function auth_getCookie()
1448  {
1449      if (!isset($_COOKIE[DOKU_COOKIE])) {
1450          return [null, null, null];
1451      }
1452      [$user, $sticky, $pass] = sexplode('|', $_COOKIE[DOKU_COOKIE], 3, '');
1453      $sticky = (bool) $sticky;
1454      $pass   = base64_decode($pass);
1455      $user   = base64_decode($user);
1456      return [$user, $sticky, $pass];
1457  }
1458  
1459  //Setup VIM: ex: et ts=2 :