Textpattern | PHP Cross Reference | Content Management Systems |
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 <head> 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
Body
title
Description
Body
title
Description
Body
title
Body
title