Textpattern PHP Cross Reference Content Management Systems

Source: /textpattern/include/txp_auth.php - 414 lines - 15046 bytes - Summary - Text - Print

Description: Login panel.

   1  <?php
   2  
   3  /*
   4   * Textpattern Content Management System
   5   * https://textpattern.com/
   6   *
   7   * Copyright (C) 2020 The Textpattern Development Team
   8   *
   9   * This file is part of Textpattern.
  10   *
  11   * Textpattern is free software; you can redistribute it and/or
  12   * modify it under the terms of the GNU General Public License
  13   * as published by the Free Software Foundation, version 2.
  14   *
  15   * Textpattern is distributed in the hope that it will be useful,
  16   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18   * GNU General Public License for more details.
  19   *
  20   * You should have received a copy of the GNU General Public License
  21   * along with Textpattern. If not, see <https://www.gnu.org/licenses/>.
  22   */
  23  
  24  /**
  25   * Login panel.
  26   *
  27   * @package Admin\Auth
  28   */
  29  
  30  if (!defined('txpinterface')) {
  31      die('txpinterface is undefined.');
  32  }
  33  
  34  /**
  35   * Renders a login panel if necessary.
  36   *
  37   * If the current visitor isn't authenticated,
  38   * terminates the script and instead renders
  39   * a login page.
  40   *
  41   * @access private
  42   */
  43  
  44  function doAuth()
  45  {
  46      global $txp_user;
  47  
  48      $txp_user = null;
  49  
  50      $message = doTxpValidate();
  51  
  52      if (!$txp_user) {
  53          if (trim(gps('app_mode')) == 'async') {
  54              echo 'if (confirm("'.escape_js(gTxt('login_to_textpattern')).'"))'.n.
  55                  '{window.location.assign("index.php")}';
  56              exit();
  57          } else {
  58              set_cookie('txp_test_cookie', '1', array('expires' => 0));
  59              doLoginForm($message);
  60          }
  61      }
  62  
  63      ob_start();
  64  }
  65  
  66  /**
  67   * Renders and outputs a login form.
  68   *
  69   * This function outputs a full HTML document,
  70   * including &lt;head&gt; and footer.
  71   *
  72   * @param string|array $message The activity message
  73   */
  74  
  75  function doLoginForm($message)
  76  {
  77      global $textarray_script, $event, $step;
  78  
  79      include txpath.'/lib/txplib_head.php';
  80  
  81      $event = 'login';
  82  
  83      $stay = (cs('txp_login') && !gps('logout') ? 1 : 0);
  84      $lang = sanitizeForUrl(gps('lang'));
  85      $reset = gps('reset');
  86      $confirm = gps('confirm');
  87      $activate = gps('activate');
  88  
  89      if (gps('logout')) {
  90          $step = 'logout';
  91      } elseif ($reset) {
  92          $step = 'reset';
  93      } elseif ($activate) {
  94          $step = 'activate';
  95      } elseif ($confirm) {
  96          $step = 'confirm';
  97      }
  98  
  99      $name = join(',', array_slice(explode(',', cs('txp_login')), 0, -1));
 100      $out = array();
 101  
 102      // Override language strings if indicated.
 103      $txpLang = Txp::get('\Textpattern\L10n\Lang');
 104      $installed = $txpLang->installed();
 105  
 106      $lang = in_array($lang, $installed) ? $lang : LANG;
 107      $langList = $txpLang->languageList();
 108      $txpLang->swapStrings($lang, 'admin');
 109  
 110      if ($reset) {
 111          $pageTitle = gTxt('password_reset');
 112          $out[] = hed(gTxt('password_reset'), 1, array('id' => 'txp-login-heading')).
 113              inputLabel(
 114                  'login_name',
 115                  fInput('text',
 116                      array(
 117                          'name'         => 'p_userid',
 118                          'autocomplete' => 'username',
 119                          'autofocus'    => true,
 120                      ), $name, '', '', '', INPUT_REGULAR, '', 'login_name', false, true),
 121                  'name', '', array('class' => 'txp-form-field login-name')
 122              ).
 123              graf(
 124                  fInput('submit', '', gTxt('password_reset_button'), 'publish')
 125              ).
 126              graf(
 127                  href(gTxt('back_to_login'), 'index.php?lang='.$lang), array('class' => 'login-return')
 128              ).
 129              hInput('lang', $lang).
 130              hInput('p_reset', 1);
 131      } elseif ($confirm || $activate) {
 132          $pageTitle = ($confirm) ? gTxt('change_password') : gTxt('set_password');
 133          $label = ($confirm) ? 'change_password' : 'set_password';
 134          $class = ($confirm) ? 'change-password' : 'set-password';
 135          $out[] = hed($pageTitle, 1, array('id' => 'txp-'.$class.'-heading')).
 136              inputLabel(
 137                  $label,
 138                  fInput('password',
 139                      array(
 140                          'name'         => 'p_password',
 141                          'autocomplete' => 'new-password',
 142                          'autofocus'    => true,
 143                      ), '', 'txp-maskable', '', '', INPUT_REGULAR, '', $label, false, true).
 144                  n.tag(
 145                      checkbox('unmask', 1, false, 0, 'show_password').
 146                      n.tag(gTxt('show_password'), 'label', array('for' => 'show_password')),
 147                      'div', array('class' => 'show-password')),
 148                  'new_password', '', array('class' => 'txp-form-field '.$class)
 149              ).
 150              graf(
 151                  fInput('submit', '', gTxt('password_confirm_button'), 'publish')
 152              ).
 153              ($confirm ? graf(
 154                  href(gTxt('back_to_login'), 'index.php?lang='.$lang), array('class' => 'login-return')
 155              ) : '').
 156              hInput('hash', gps('confirm').gps('activate')).
 157              hInput('lang', $lang).
 158              hInput(($confirm ? 'p_alter' : 'p_set'), 1);
 159      } else {
 160          $pageTitle = gTxt('login');
 161          $out[] = hed(gTxt('login_to_textpattern'), 1, array('id' => 'txp-login-heading')).
 162              (count($langList) > 1
 163                  ? graf(
 164                      tag(gTxt('language'), 'label', array('for' => 'lang')).
 165                      $txpLang->languageSelect('lang', $lang)
 166                      , array('class' => 'login-language txp-reduced-ui')
 167                  ) : hInput('lang', $lang)).
 168              inputLabel(
 169                  'login_name',
 170                  fInput('text',
 171                      array(
 172                          'name'         => 'p_userid',
 173                          'autocomplete' => 'username',
 174                          'autofocus'    => true,
 175                      ), $name, '', '', '', INPUT_REGULAR, '', 'login_name', false, true),
 176                  'name', '', array('class' => 'txp-form-field login-name')
 177              ).
 178              inputLabel(
 179                  'login_password',
 180                  fInput('password',
 181                      array(
 182                          'name'         => 'p_password',
 183                          'autocomplete' => 'current-password',
 184                      ), '', '', '', '', INPUT_REGULAR, '', 'login_password', false, true),
 185                  'password', '', array('class' => 'txp-form-field login-password')
 186              ).
 187              graf(
 188                  checkbox('stay', 1, $stay, '', 'login_stay').n.
 189                  tag(gTxt('stay_logged_in'), 'label', array('for' => 'login_stay')).
 190                  popHelp(array('remember_login', $lang)), array('class' => 'login-stay')
 191              ).
 192              graf(
 193                  fInput('submit', '', gTxt('log_in_button'), 'publish')
 194              ).
 195              graf(
 196                  href(gTxt('password_forgotten'), '?reset=1&lang='.$lang), array('class' => 'login-forgot')
 197              ).
 198              graf(
 199                  href(htmlspecialchars(get_pref('sitename')), hu, array(
 200                      'title'  => gTxt('tab_view_site'),
 201                  )), array('class' => 'login-view-site')
 202              );
 203  
 204          if (gps('event')) {
 205              $out[] = eInput(gps('event'));
 206          }
 207      }
 208  
 209      pagetop($pageTitle, $message);
 210  
 211      echo form(
 212          join('', $out), '', '', 'post', 'txp-login', '', 'login_form').
 213      script_js('textpattern.textarray = '.json_encode($textarray_script, TEXTPATTERN_JSON)).
 214      n.'</main><!-- /txp-body -->'.n.'</body>'.n.'</html>';
 215  
 216      exit(0);
 217  }
 218  
 219  /**
 220   * Validates the sent login form and creates a session.
 221   *
 222   * During the reset request procedure, it is conceivable to verify the
 223   * token as soon as it's presented in the URL, but that would:
 224   *  a) require refactoring code similarities in both p_confirm and p_alter branches
 225   *  b) require some way (e.g. an Exception) to signal back to doLoginForm() that
 226   *     the token is bogus so the 'change your password' form is not displayed.
 227   *  c) leak information about the validity of a token, thus allowing rapid brute-force
 228   *     attempts.
 229   *
 230   * The inconvenience of a real user following an expired token and being told so
 231   * after they've set a password is a small price to pay for the improved security
 232   * and reduction of attack surface that validating after submission affords.
 233   *
 234   * @todo  Could the checks be done via a (reusable) Validator()?
 235   *
 236   * @return string A localised feedback message
 237   * @see    doLoginForm()
 238   */
 239  
 240  function doTxpValidate()
 241  {
 242      global $logout, $txp_user;
 243  
 244      $p_userid   = ps('p_userid');
 245      $p_password = ps('p_password');
 246      $p_reset    = ps('p_reset');
 247      $p_alter    = ps('p_alter');
 248      $p_set      = ps('p_set');
 249      $stay       = ps('stay');
 250      $p_confirm  = gps('confirm');
 251      $logout     = gps('logout');
 252      $lang       = sanitizeForUrl(gps('lang'));
 253      $message    = '';
 254      $pub_path   = preg_replace('|//$|', '/', rhu.'/');
 255      $cookie_domain = (defined('cookie_domain')) ? cookie_domain : '';
 256  
 257      if (cs('txp_login') && strpos(cs('txp_login'), ',')) {
 258          $txp_login = explode(',', cs('txp_login'));
 259          $c_hash = end($txp_login);
 260          $c_userid = join(',', array_slice($txp_login, 0, -1));
 261      } else {
 262          $c_hash   = '';
 263          $c_userid = '';
 264      }
 265  
 266      // Override language strings if indicated.
 267      $txpLang = Txp::get('\Textpattern\L10n\Lang');
 268      $installed = $txpLang->installed();
 269      $lang = in_array($lang, $installed) ? $lang : LANG;
 270      $txpLang->swapStrings($lang, 'admin, common');
 271  
 272      if ($c_userid && strlen($c_hash) === 32) {
 273          // Cookie exists.
 274          // @todo Improve security by using a better nonce/salt mechanism. md5 and uniqid are bad.
 275          $r = safe_row(
 276              "name, nonce",
 277              'txp_users',
 278              "name = '".doSlash($c_userid)."' AND last_access > DATE_SUB(NOW(), INTERVAL 30 DAY)"
 279          );
 280  
 281          if ($r && $r['nonce'] && $r['nonce'] === md5($c_userid.pack('H*', $c_hash))) {
 282              // Cookie is good.
 283              if ($logout) {
 284                  $txp_user = $c_userid;
 285                  bouncer('logout', array('logout' => true));
 286                  $txp_user = null;
 287                  set_cookie('txp_login');
 288                  set_cookie('txp_login_public', '', array('path' => $pub_path, 'domain' => $cookie_domain));
 289                  // Destroy nonce.
 290                  safe_update(
 291                      'txp_users',
 292                      "nonce = '".doSlash(md5(uniqid(mt_rand(), true)))."'",
 293                      "name = '".doSlash($c_userid)."'"
 294                  );
 295              } else {
 296                  // Create $txp_user.
 297                  $txp_user = $r['name'];
 298              }
 299  
 300              return $message;
 301          } else {
 302              txp_status_header('401 Your session has expired');
 303              set_cookie('txp_login', $c_userid, array('expires' => time() + 3600 * 24 * 365));
 304              set_cookie('txp_login_public', '', array('path' => $pub_path, 'domain' => $cookie_domain));
 305              $message = array(gTxt('bad_cookie'), E_ERROR);
 306          }
 307      } elseif ($p_userid && $p_password) {
 308          // Incoming login vars.
 309          $name = txp_validate($p_userid, $p_password);
 310  
 311          if ($name !== false) {
 312              $c_hash = md5(uniqid(mt_rand(), true));
 313              $nonce  = md5($name.pack('H*', $c_hash));
 314  
 315              safe_update(
 316                  'txp_users',
 317                  "nonce = '".doSlash($nonce)."'",
 318                  "name = '".doSlash($name)."'"
 319              );
 320  
 321              set_cookie(
 322                  'txp_login',
 323                  $name.','.$c_hash,
 324                  array(
 325                      'expires' => $stay ? time() + 3600 * 24 * 365 : 0,
 326                      'httponly' => LOGIN_COOKIE_HTTP_ONLY
 327                  )
 328              );
 329  
 330              set_cookie(
 331                  'txp_login_public',
 332                  substr(md5($nonce), -10).$name,
 333                  array(
 334                      'expires' => $stay ? time() + 3600 * 24 * 30 : 0,
 335                      'path' => $pub_path,
 336                      'domain' => $cookie_domain
 337                  )
 338              );
 339  
 340              // Login is good, create $txp_user.
 341              $txp_user = $name;
 342              Txp::get('\Textpattern\DB\Core')->checkPrefsIntegrity();
 343  
 344              // Set admin language to the one set in the login screen.
 345              if ($lang) {
 346                  set_pref('language_ui', $lang, 'admin', PREF_HIDDEN, 'text_input', 0, PREF_PRIVATE);
 347              }
 348  
 349              script_js(<<<EOS
 350  $(document).ready(function ()
 351  {
 352      cookieEnabled = checkCookies();
 353  });
 354  EOS
 355              , false);
 356  
 357              return '';
 358          } else {
 359              sleep(3);
 360              txp_status_header('401 Could not log in with that username/password');
 361              $message = array(gTxt('could_not_log_in'), E_ERROR);
 362          }
 363      } elseif ($p_reset) {
 364          // Reset request.
 365          sleep(3);
 366  
 367          $message = ($p_userid) ? send_reset_confirmation_request($p_userid) : '';
 368      } elseif ($p_alter || $p_set) {
 369          // Password change/set confirmation.
 370          sleep(3);
 371          global $sitename;
 372  
 373          $pass = ps('p_password');
 374          $type = ($p_alter) ? 'password_reset' : 'account_activation';
 375  
 376          if (trim($pass) === '') {
 377              $message = array(gTxt('password_required'), E_ERROR);
 378          } else {
 379              $hash = gps('hash');
 380              $selector = substr($hash, SALT_LENGTH);
 381  
 382              $tokenInfo = safe_row("reference_id, token, expires", 'txp_token', "selector = '".doSlash($selector)."' AND type='$type'");
 383  
 384              if ($tokenInfo) {
 385                  if (strtotime($tokenInfo['expires']) <= time()) {
 386                      $message = array(gTxt('token_expired'), E_ERROR);
 387                  } else {
 388                      $uid = assert_int($tokenInfo['reference_id']);
 389                      $row = safe_row("name, email, nonce, pass AS old_pass", 'txp_users', "user_id = '$uid'");
 390  
 391                      if ($row && $row['nonce'] && ($hash === bin2hex(pack('H*', substr(hash(HASHING_ALGORITHM, $row['nonce'].$selector.$row['old_pass']), 0, SALT_LENGTH))).$selector)) {
 392                          if (change_user_password($row['name'], $pass)) {
 393                              $body = gTxt('salutation', array('{name}' => $row['name'])).
 394                                  n.n.($p_alter ? gTxt('password_change_confirmation') : gTxt('password_set_confirmation').n.n.gTxt('log_in_at').' '.ahu.'index.php?lang='.$lang);
 395                              $message = ($p_alter) ? gTxt('password_changed') : gTxt('password_set');
 396                              txpMail($row['email'], "[$sitename] ".$message, $body);
 397  
 398                              // Invalidate all tokens in the wild for this user.
 399                              safe_delete("txp_token", "reference_id = '$uid' AND type IN ('password_reset', 'account_activation')");
 400                          }
 401                      } else {
 402                          $message = array(gTxt('invalid_token'), E_ERROR);
 403                      }
 404                  }
 405              } else {
 406                  $message = array(gTxt('invalid_token'), E_ERROR);
 407              }
 408          }
 409      }
 410  
 411      $txp_user = '';
 412  
 413      return $message;
 414  }

title

Description

title

Description

title

Description

title

title

Body