<?php
/**
* This is the login screen. It also handles actions related to logging in/out, registering, changing password and closing account.
*
* This file is part of the evoCore framework - {@link http://evocore.net/}
* See also {@link https://github.com/b2evolution/b2evolution}.
*
* @license GNU GPL v2 - {@link http://b2evolution.net/about/gnu-gpl-license}
*
* @copyright (c)2003-2020 by Francois Planque - {@link http://fplanque.com/}
* Parts of this file are copyright (c)2004-2006 by Daniel HAHLER - {@link http://thequod.de/contact}.
*
* @package htsrv
*/
/**
* Includes:
*/
require_once dirname(__FILE__).'/../conf/_config.php';
/**
* @global boolean Is this a login page?
*/
$is_login_page = true;
require_once $inc_path.'_main.inc.php';
// Check and redirect if current URL must be used as https instead of http:
check_https_url( 'login' );
$login = param( $dummy_fields['login'], 'string', '' );
param_action( 'req_login' );
param( 'mode', 'string', '' );
param( 'inskin', 'boolean', false );
if( $inskin )
{
param( 'blog', 'integer', NULL );
}
// gets used by header_redirect();
param( 'redirect_to', 'url', $ReqURI );
// Used to ABORT login
param( 'return_to', 'url', $ReqURI );
// Used to redirect if user is already logged in:
param( 'forward_to', 'url', NULL );
if( $action == 'req_login' && is_logged_in() && $forward_to !== NULL )
{ // Redirect to a requested URL if user is not logged-in:
header_redirect( $forward_to );
}
switch( $action )
{
case 'logout':
// Log the current user out:
logout(); // logout $Session and set $current_User = NULL
// TODO: to give the user feedback through $Messages, we would need to start a new $Session here and append $Messages to it.
// Redirect to $baseurl on logout if redirect URI is not set. Temporarily fix until we remove actions from redirect URIs
if( $redirect_to == $ReqURI )
{
$redirect_to = $baseurl;
}
header_redirect(); // defaults to redirect_to param and exits
/* exited */
break;
case 'closeaccount':
// Close current user account and log out:
global $Session, $Messages, $UserSettings;
$Session->assert_received_crumb( 'closeaccountform' );
// Check if User has provided a reason for closing his account:
$reasons = trim( $Settings->get( 'account_close_reasons' ) );
param( 'account_close_type', 'string', '' );
if( ! empty( $reasons ) && empty( $account_close_type ) )
{ // Don't submit a form without a selected reason
$Messages->add( T_( 'Please quickly select a reason for closing your account.' ) );
// Set this session var only to repopulate other reason textarea input
$Session->set( 'account_close_reason', param( 'account_close_reason', 'text', '' ) );
// Redirect to show the errors:
header_redirect(); // Will EXIT
// We have EXITed already at this point!!
}
if( check_user_perm( 'users', 'edit', false ) )
{ // Admins cannot close own accounts
$Messages->add( T_( 'Since you are an Admin with User management privileges, you cannot close your own account!' ) );
// Redirect to show the errors:
header_redirect(); // Will EXIT
// We have EXITed already at this point!!
}
if( is_logged_in() && $current_User->update_status_from_Request( true, 'closed' ) )
{ // user account was closed successful
// Send notification email about closed account to users with edit users permission
$email_template_params = array(
'login' => $current_User->login,
'email' => $current_User->email,
'reason' => trim( param( 'account_close_type', 'string', '' ).' '.param( 'account_close_reason', 'text', '' ) ),
'user_ID' => $current_User->ID,
'days_count' => $current_User->get_days_count_close()
);
send_admin_notification( NT_('User account closed'), 'account_closed', $email_template_params );
// Set this session var only to know when display a good-bye message:
$Session->set( 'account_closing_success', true );
}
else
{ // db update was unsuccessful
$Messages->add_to_group( 'Unexpected error. Unable to close your account.', 'error', T_('Close account').':' );
}
header_redirect();
/* exited */
break;
case 'resetpassword':
// Send password reset request by email:
// Stop a request from the blocked IP addresses or Domains
antispam_block_request();
global $servertimenow;
$login_required = true; // Do not display "Without login.." link on the form
if( empty( $login ) )
{ // Don't allow empty request
param_error( $dummy_fields['login'], T_('You must enter your username or your email address so we know where to send the password reset email.'), '' );
// Set this var to know after redirection if error was here
$lostpassword_error = true;
$action = 'lostpassword';
break;
}
// Check if a password reset email was already requested recently and block too frequent requests:
$request_ts_login = $Session->get( 'core.changepwd.request_ts_login' );
if( $request_ts_login != NULL )
{
list( $last_request_ts, $last_request_login ) = preg_split( '~_~', $request_ts_login );
if( ( $login == $last_request_login ) && ( ( $servertimenow - $pwdchange_request_delay ) < $last_request_ts ) )
{ // the same request was sent from the same session in the last $pwdchange_request_delay seconds ( 5 minutes by default )
$Messages->add( sprintf( T_('We have already sent you a password reset email at %s. Please allow %d minutes for delivery before requesting a new one.' ), date( locale_datetimefmt(), $last_request_ts ), $pwdchange_request_delay / 60 ) );
// Go back to login page:
$action = 'req_login';
break;
}
}
$UserCache = & get_UserCache();
$UserCache->clear();
if( is_email( $login ) )
{ // User gave an email address, get matching User accounts for this email address and also get first activated one as recipient for the reset email:
$only_activated = false;
// load all not closed users with this email address
$login = utf8_strtolower( $login );
$UserCache->load_where( 'user_email = "'.$login.'" && user_status <> "closed"' );
$not_activated_Ids = array();
while( ( $iterator_User = & $UserCache->get_next() ) != NULL )
{ // Iterate through UserCache
if( $iterator_User->check_status( 'is_validated' ) )
{
$only_activated = true;
}
else
{ // strore not activated user Ids for further use
$not_activated_Ids[] = $iterator_User->ID;
}
}
// if we have activated users then remove every not activated from the cache
if( $only_activated && ( !empty( $not_activated_Ids ) ) )
{
foreach( $not_activated_Ids as $not_activated_Id )
{
$UserCache->remove_by_ID( $not_activated_Id );
}
}
$UserCache->rewind();
$forgetful_User = & $UserCache->get_next();
$UserCache->rewind();
}
else
{ // User gave a login, get hat User by login:
$forgetful_User = & $UserCache->get_by_login( $login );
}
if( ! $forgetful_User )
{ // User does not exist
// pretend that the email is sent to make it harder for attackers to guess user logins
$Messages->add( T_('If you correctly entered your login or email address, a link to reset your password has been sent to your registered email address.' ), 'success' );
// Go back to login page:
$action = 'req_login';
break;
}
locale_temp_switch( $forgetful_User->locale );
if( $demo_mode )
{
$Messages->add( T_('You cannot reset passwords in demo mode.'), 'error' );
// Go back to login page:
$action = 'req_login';
break;
}
if( empty( $forgetful_User->email ) )
{
$Messages->add( T_('Your user account has no associated email address; therefore we cannot reset your password.')
.' '.T_('Please try contacting the admin.'), 'error' );
}
else
{
// Generate a Random key to include in the password reset link that will be sent by email:
$request_id = generate_random_key(22); // 22 to make it not too long for URL but unique/safe enough
// Count how many users match the reset request ( It can be more than 1 in case an email address was provided instead of a unique login )
$user_ids = $UserCache->get_ID_array();
$user_count = count( $user_ids );
// Set blog param for email link:
$blog_param = '';
if( !empty( $blog ) )
{
$blog_param = '&inskin=true&blog='.$blog;
}
// Other params for the email:
$subject = sprintf( T_( 'Password reset request for %s' ), $login );
$email_template_params = array(
'user_count' => $user_count,
'request_id' => $request_id,
'blog_param' => $blog_param,
);
// Send email to User by using a text/html template:
// In case several Users have the same email address, the email will allow the recipient to choose which account he wants to reset:
if( ! send_mail_to_User( $forgetful_User->ID, $subject, 'account_password_reset', $email_template_params, true ) )
{
$Messages->add( T_('Sorry, the email with the link to reset your password could not be sent.')
.'<br />'.get_send_mail_error(), 'error' );
}
else
{
// Prevent too many identical password recovery emails:
$Session->set( 'core.changepwd.request_ts_login', $servertimenow.'_'.$login, $pwdchange_request_delay + 60 ); // Session var expires 60 seconds after the allowed delay.
// Secret key that will be included into the reset email in order to validate that it gets to the owner of the email address:
$Session->set( 'core.changepwd.request_id', $request_id, 86400 * 2 ); // Session var expires in two days (or when password changed)
// Target of the request (can be a login or an email address):
$Session->set( 'core.changepwd.request_for', $login, 86400 * 2 ); // Session var expires in two days (or when password changed)
$Session->dbsave(); // save immediately
$Messages->add( T_('If you correctly entered your login or email address, a link to reset your password has been sent to your registered email address.' ), 'success' );
syslog_insert( 'User requested password reset', 'info', 'user', $forgetful_User->ID );
}
}
locale_restore_previous();
$action = 'req_login';
break;
case 'changepwd':
// User clicked "Reset password NOW" link from an password reset email:
param( 'reqID', 'string', '' );
$UserCache = & get_UserCache();
$forgetful_User = & $UserCache->get_by_login( $login );
// Validate params against session vars:
if( ! validate_pwd_reset_session( $reqID, $forgetful_User ) )
{
$Messages->add( T_('Invalid password change request! Remember you must use the same session (by means of your session cookie) as when you have requested the action. Please try again...'), 'error' );
locale_restore_previous();
$action = 'lostpassword';
$login_required = true; // Do not display "Without login.." link on the form
break;
}
// Link User to Session and Log in:
$Session->set_user_ID( $forgetful_User->ID );
$current_User = & $forgetful_User;
// Add Message to change the password:
$Messages->add( T_( 'Please choose a new password now...' ), 'note' );
// Redirect to the user's change password tab
$changepwd_url = NULL;
if( !empty( $blog ) )
{ // blog is set, redirect to in-skin change password form
$BlogCache = & get_BlogCache();
$Collection = $Blog = $BlogCache->get_by_ID( $blog );
if( $Blog )
{
$changepwd_url = $Blog->get( 'pwdchangeurl', array( 'url_suffix' => 'reqID='.$reqID, 'glue' => '&' ) );
}
}
locale_restore_previous();
if( empty( $changepwd_url ) )
{ // Display standard(non-skin) form to change password
$action = 'changepwd';
}
else
{ // redirect will save $Messages into Session:
header_redirect( $changepwd_url ); // display user's change password tab
/* exited */
}
break;
case 'updatepwd':
// User is updating his password (submit action of the above reset password form):
param( 'reqID', 'string', '' );
if( ! is_logged_in() )
{ // Don't allow not logged in user here, because it must be logged in on the action 'changepwd' above
$Messages->add( T_('Invalid password change request! Please try again...'), 'error' );
$action = 'lostpassword';
$login_required = true; // Do not display "Without login.." link on the form
break;
}
$forgetful_User = & $current_User;
// Validate params against session vars:
if( ! validate_pwd_reset_session( $reqID, $forgetful_User ) )
{
$Messages->add( T_('Invalid password change request! Remember you must use the same session (by means of your session cookie) as when you have requested the action. Please try again...'), 'error' );
locale_restore_previous();
$action = 'lostpassword';
$login_required = true; // Do not display "Without login.." link on the form
break;
}
$result = $forgetful_User->update_from_request();
if( $result !== true )
{ // Some errors exist on form submit, Display the form again to change them
$action = 'changepwd';
break;
}
// Clean up session variables:
$Session->delete( 'core.changepwd.request_ts_login' );
$Session->delete( 'core.changepwd.request_id' );
$Session->delete( 'core.changepwd.request_for' );
$Session->dbsave(); // save immediately
locale_restore_previous();
// redirect Will save $Messages into Session:
header_redirect( $baseurl ); // display user's change password tab
/* exited */
break;
case 'activateacc_ez':
// User clicked 'Activate NOW' or 'Reactivate NOW' from an account activation email with EASY activation process (first email or reminder):
// Stop a request from the blocked IP addresses or Domains
antispam_block_request();
global $UserSettings, $Session, $baseurl;
// get user id and reminder key
$userID = param( 'userID', 'integer', '' );
$reminder_key = param( 'reminderKey', 'string', '' );
$UserCache = & get_UserCache();
$User = $UserCache->get_by_ID( $userID );
$last_reminder_key = $UserSettings->get( 'last_activation_reminder_key', $userID );
if( !$User->check_status( 'can_be_validated' ) )
{
if( $User->check_status( 'is_validated' ) )
{ // Already activated, e.g. clicked on an obsolete email link:
$Messages->add( T_('Your account has already been activated.'), 'note' );
if( is_logged_in() )
{ // Redirect to base url if user is already logged in:
header_redirect( $baseurl );
/* exited */
}
else
{ // Display a login form if user is not logged in yet:
$action = 'req_login';
break;
}
}
elseif( $User->check_status( 'is_closed' ) )
{ // Account was closed, don't let to activate the account
$Messages->add( T_('Your account is closed. You cannot activate it.'), 'error' );
// redirect to base url
header_redirect( $baseurl );
/* exited */
}
}
elseif( empty( $last_reminder_key ) || ( $last_reminder_key != $reminder_key ) )
{ // the reminder key in db is empty or not equal with the received one
$Messages->add( T_('Invalid account activation request!'), 'error' );
$action = 'req_activate_email';
break;
}
// log in with user
$Session->set_user_ID( $userID );
// activate user account
$User->activate_from_Request();
$Messages->add( T_('Your account is now activated.'), 'success' );
header_redirect( htmlspecialchars_decode( redirect_after_account_activation() ) );
/* exited */
break;
case 'activateacc_sec':
// User clicked 'Activate NOW' or 'Reactivate NOW' from an account activation email with SECURE activation process (first email or reminder):
// fp> NOTE: I am not sure secure process works allows reminders.
// Stop a request from the blocked IP addresses or Domains
antispam_block_request();
param( 'reqID', 'string', '' );
param( 'sessID', 'integer', '' );
if( check_user_status( 'is_validated' ) )
{ // Already validated, e.g. clicked on an obsolete email link:
$Messages->add( T_('Your account has already been activated.'), 'note' );
// no break: cleanup & redirect below
}
else
{
// Check valid format:
if( empty($reqID) )
{ // This was not requested
$Messages->add( T_('Invalid account activation request!'), 'error' );
$action = 'req_activate_email';
break;
}
// Check valid session (format only, meant as help for the user):
if( $sessID != $Session->ID )
{ // Another session ID than for requesting account validation link used!
$Messages->add( T_('You have to use the same session (by means of your session cookie) as when you have requested the action. Please try again...'), 'error' );
$action = 'req_activate_email';
break;
}
// Validate provided reqID against the one stored in the user's session
$request_ids = $Session->get( 'core.activateacc.request_ids' );
if( ( ! is_array($request_ids) || ! in_array( $reqID, $request_ids ) )
&& ! ( isset($current_User) && $current_User->grp_ID == 1 && $reqID == 1 /* admin users can validate themselves by a button click */ ) )
{
$Messages->add( T_('Invalid account activation request!'), 'error' );
$action = 'req_activate_email';
$login_required = true; // Do not display "Without login.." link on the form
break;
}
if( ! is_logged_in() )
{ // this can happen, if a new user registers and clicks on the "validate by email" link, without logging in first
// Note: we reuse $reqID and $sessID in the form to come back here.
$Messages->add( T_('Please log in to activate your account.'), 'error' );
break;
}
// activate user account
$current_User->activate_from_Request();
$Messages->add( T_( 'Your account is now activated.' ), 'success' );
}
// redirect Will save $Messages into Session:
header_redirect( htmlspecialchars_decode( redirect_after_account_activation() ) );
/* exited */
break;
} // switch( $action ) (1st)
/* For actions that other delegate to from the switch above: */
switch( $action )
{
case 'req_activate_email':
// User wants to request a new activation link by email (initial form and action):
if( ! is_logged_in() )
{
$Messages->add( T_('You have to be logged in to request an account validation link.'), 'error' );
$action = '';
break;
}
if( check_user_status( 'is_validated' ) )
{ // Activation not required (check this after login, so it does not get "announced")
$action = '';
break;
}
param( 'req_activate_email_submit', 'integer', 0 ); // has the form been submitted
$email = utf8_strtolower( param( $dummy_fields['email'], 'string', $current_User->email ) ); // the email address is editable
if( $req_activate_email_submit )
{ // Form has been submitted
param_check_email( $dummy_fields['email'], true );
// check if user email was changed
$email_changed = ( $current_User->get( 'email' ) != $email );
// check if we really needs to send a new validation email
if( !$email_changed )
{ // the email was not changed
$last_activation_email_date = $UserSettings->get( 'last_activation_email', $current_User->ID );
if( ! empty( $last_activation_email_date ) )
{ // at least one validation email was sent
// convert date to timestamp
$last_activation_email_ts = mysql2timestamp( $last_activation_email_date );
$activate_requests_limit = $Settings->get( 'activate_requests_limit' );
if( $servertimenow - $last_activation_email_ts < $activate_requests_limit )
{ // a validation email was sent to the same email address less then the x seconds, where x is the "Activation requests limit" value
// get difference between local time and server time
$time_difference = $Settings->get('time_difference');
// get last activation email local date and time
$last_email_date = date( locale_datetimefmt(), $last_activation_email_ts + $time_difference );
$Messages->add( sprintf( T_( "We have already sent you an activation message to %s at %s. Please allow %d minutes for delivery before requesting a new one." ), $email, $last_email_date, $activate_requests_limit / 60 ) );
}
}
}
// Call plugin event to allow catching input in general and validating own things from DisplayRegisterFormFieldset event:
$Plugins->trigger_event( 'ValidateAccountFormSent' );
if( $Messages->has_errors() )
{
break;
}
if( $email_changed )
{ // Update user's email:
$current_User->set_email( $email );
if( !$current_User->dbupdate() )
{ // email address couldn't be updated
$Messages->add( T_('Could not update your email address.'), 'error' );
break;
}
}
$inskin_blog = $inskin ? $blog : NULL;
if( $current_User->send_validate_email( $redirect_to, $inskin_blog, $email_changed ) )
{
$Messages->add( sprintf( /* TRANS: %s gets replaced by the user's email address */ T_('An email has been sent to your email address (%s). Please click on the link therein to activate your account.'), $current_User->dget('email') ), 'success' );
}
elseif( $demo_mode )
{
$Messages->add( 'Sorry, could not send email. Sending email in demo mode is disabled.', 'error' );
}
else
{
$Messages->add( T_('Sorry, the email with the link to activate your account could not be sent.')
.'<br />'.get_send_mail_error(), 'error' );
}
}
else
{ // Form not yet submitted:
// Add a note, if we have already sent validation links:
$request_ids = $Session->get( 'core.activateacc.request_ids' );
if( is_array($request_ids) && count($request_ids) )
{
$Messages->add( sprintf( T_('We have already sent you %d email(s) with an activation link.'), count($request_ids) ), 'note' );
}
if( empty($current_User->email) )
{ // add (error) note to be displayed in the form
$Messages->add( T_('Your user account has no associated email address; therefore we cannot activate it. Please provide your email address below.'), 'error' );
}
}
break;
}
if( strlen( $redirect_to ) )
{ // Make it relative to the form's target, in case it has been set absolute (and can be made relative).
$redirect_to = url_rel_to_same_host( $redirect_to, get_htsrv_url( 'login' ) );
}
if( preg_match( '#/login.php([&?].*)?$#', $redirect_to ) )
{ // avoid "endless loops"
$redirect_to = $baseurl;
}
// Remove login and pwd parameters from URL, so that they do not trigger the login screen again:
$redirect_to = preg_replace( '~(?<=\?|&) (login|pwd) = [^&]+ ~x', '', $redirect_to );
$Debuglog->add( 'redirect_to: '.$redirect_to );
if( strlen( $return_to ) )
{ // Make it relative to the form's target, in case it has been set absolute (and can be made relative).
$return_to = url_rel_to_same_host( $return_to, get_htsrv_url( 'login' ) );
}
if( preg_match( '#/login.php([&?].*)?$#', $return_to ) )
{ // avoid "endless loops"
$redirect_to = $baseurl;
}
// Remove login and pwd parameters from URL, so that they do not trigger the login screen again:
$return_to = preg_replace( '~(?<=\?|&) (login|pwd) = [^&]+ ~x', '', $return_to );
$Debuglog->add( 'return_to: '.$return_to );
/*
* Display in-skin login if it's supported
*/
if( $inskin && use_in_skin_login() )
{ // in-skin display:
$BlogCache = & get_BlogCache();
$Collection = $Blog = $BlogCache->get_by_ID( $blog, false, false );
if( ! empty( $Blog ) )
{
if( !empty( $login_error ) )
{
$Messages->add( $login_error );
}
if( empty( $redirect_to ) )
{
$redirect_to = $Blog->gen_blogurl();
}
// check if action was req_activate_email
if( ( $action == 'req_activate_email' ) && !empty( $current_User ) )
{ // redirect to inskin activate account page
$redirect = url_add_param( $Blog->gen_blogurl(), 'disp=activateinfo', '&' );
if( $Messages->has_errors() )
{ // Redirect to a form for requesting an activation again if some errors exist
$redirect = url_add_param( $redirect, 'force_request=1', '&' );
}
}
elseif( $action == 'lostpassword' )
{ // redirect to inskin lost password page
$redirect = $Blog->get( 'lostpasswordurl', array( 'glue' => '&' ) );
if( ! empty( $lostpassword_error ) )
{ // Set this param to know after redirection if error was here
$redirect = url_add_param( $redirect, 'field_error=1', '&' );
}
}
else
{ // redirect to inskin login page
$redirect = $Blog->get( 'loginurl', array( 'glue' => '&' ) );
}
$redirect = url_add_param( $redirect, 'redirect_to='.rawurlencode( $redirect_to ), '&' );
header_redirect( $redirect );
// already exited here
exit(0);
}
}
/**
* Display one of the basic login management screens:
*/
switch( $action )
{
case 'lostpassword':
// Lost password:
$page_title = T_('Lost your password?');
$hidden_params = array( 'redirect_to' => url_rel_to_same_host( $redirect_to, get_htsrv_url( 'login' ) ) );
$wrap_width = '480px';
// Use the links in the form title
$use_form_links = true;
// Include page header:
require $adminskins_path.'login/_html_header.inc.php';
// Lost password form
$params = array(
'form_title_lostpass' => $page_title,
'login_form_inskin' => false,
'login_page_class' => 'evo_panel__login',
'login_page_before' => '<div class="evo_panel__lostpass">',
'login_page_after' => '</div>',
'form_class_login' => 'evo_form__login evo_form__lostpass',
'lostpass_form_params' => $login_form_params,
'lostpass_form_footer' => false,
'abort_link_text' => '<button type="button" class="close" aria-label="Close"><span aria-hidden="true">×</span></button>',
);
require skin_fallback_path( '_lostpassword.disp.php', 6 );
// Include page footer:
require $adminskins_path.'login/_html_footer.inc.php';
break;
case 'changepwd':
// Display form to reset password: (after 'lostpassword' form has been submitted and email has been received+clicked)
require $adminskins_path.'login/_reset_pwd_form.main.php';
break;
case 'req_activate_email':
// Send activation link by email (initial form and action)
// Display validation form:
require $adminskins_path.'login/_validate_form.main.php';
break;
default:
// Display login form:
if( $Settings->get( 'http_auth_require' ) && ! isset( $_SERVER['PHP_AUTH_USER'] ) )
{ // Require HTTP authentication:
header( 'WWW-Authenticate: Basic realm="b2evolution"' );
header( 'HTTP/1.0 401 Unauthorized' );
}
require $adminskins_path.'login/_login_form.main.php';
}
exit(0);
?>