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