esc_html__( 'Invalid request', 'magic-login' ),
'show_form' => true,
'show_registration_form' => false,
]
);
}
parse_str( wp_unslash( $_POST['data'] ), $form_data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$global_data = [ 'log', 'redirect_to', 'g-recaptcha-response', 'cf-turnstile-response' ];
// populate super global $_POST with form data
foreach ( $global_data as $key ) {
if ( isset( $form_data[ $key ] ) ) {
$_POST[ $key ] = $form_data[ $key ];
}
}
$args = []; // pass custom messages to backend
if ( ! empty( $form_data['messages'] ) ) {
foreach ( $form_data['messages'] as $key => $value ) {
$args[ $key . '_message' ] = wp_kses_post( $value );
}
}
$login_request = self::process_login_request( $args );
$settings = \MagicLogin\Utils\get_settings();
$error_messages = '';
$login_errors = $login_request['errors'];
if ( $login_request['show_registration_form'] ) {
$email = isset( $_POST['log'] ) && is_email( wp_unslash( $_POST['log'] ) ) ? sanitize_email( wp_unslash( $_POST['log'] ) ) : '';
if ( 'auto' === $settings['registration']['mode'] && $settings['registration']['fallback_email_field'] ) {
$shortcode = sprintf( '[magic_login_registration_form show_name="false" show_terms="false" email="%s"]', $email );
} else {
$shortcode = sprintf( '[magic_login_registration_form email="%s"]', $email );
}
$registration_form = do_shortcode( $shortcode );
wp_send_json_success(
[
'message' => $login_request['info'],
'show_form' => $login_request['show_form'],
'code_login' => $login_request['code_login'],
'phone_login' => $login_request['phone_login'],
'registration_form' => $registration_form,
]
);
}
// error messages
if ( ! empty( $login_errors ) && is_wp_error( $login_errors ) && $login_errors->has_errors() ) {
foreach ( $login_errors->get_error_codes() as $code ) {
foreach ( $login_errors->get_error_messages( $code ) as $message ) {
$error_messages .= $message . "
\n";
}
}
}
if ( ! empty( $error_messages ) ) {
$error_message = sprintf( '
%s
', wp_kses_post( $error_messages ) );
wp_send_json_error(
[
'message' => $error_message,
'show_form' => $login_request['show_form'],
'code_login' => $login_request['code_login'],
'phone_login' => $login_request['phone_login'],
]
);
}
if ( $login_request['code_login'] ) {
ob_start();
CodeLogin::code_form();
$code_login_form = ob_get_clean();
wp_send_json_success(
[
'info' => $login_request['info'],
'errors' => $login_request['errors'],
'message' => $login_request['info'],
'show_form' => $login_request['show_form'],
'code_login' => $login_request['code_login'],
'phone_login' => $login_request['phone_login'],
'code_form' => $code_login_form,
]
);
}
if ( ! empty( $login_request['info'] ) ) {
wp_send_json_success(
[
'code_login' => $login_request['code_login'],
'phone_login' => $login_request['phone_login'],
'message' => $login_request['info'],
'show_form' => $login_request['show_form'],
]
);
}
}
/**
* Maybe add login link to outgoing email
*
* @param array $atts wp_mail args
*
* @return mixed
* @since 1.6
*/
public static function maybe_add_auto_login_link( $atts ) {
$settings = \MagicLogin\Utils\get_settings();
if ( ! $settings['auto_login_links'] ) {
return $atts;
}
$to = $atts['to'];
if ( ! self::is_single_recipient( $atts ) ) {
return $atts;
}
$to = is_array( $to ) ? array_shift( $to ) : $to;
$user = get_user_by( 'email', $to );
if ( ! $user ) {
return $atts;
}
if ( self::is_auto_login_link_excluded_mail( $atts ) ) {
return $atts;
}
/**
* Filter auto login link
*
* @param bool $status false to exclude, default true
* @param array $atts wp_mail args
* @param \WP_User $user user object
*
* @since 1.6.0
*/
$add_login_link = apply_filters( 'magic_login_add_auto_login_link', true, $atts, $user );
if ( ! $add_login_link ) {
return $atts;
}
$atts['message'] = self::add_auto_login_link_to_message( $atts, $user );
return $atts;
}
/**
* Add auto login link to message
*
* @param array $args wp mail content
* @param \WP_User $user User Object
*
* @return string
* @since 1.6
*/
public static function add_auto_login_link_to_message( $args, $user ) {
$ttl = get_ttl_by_user( $user->ID );
list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $ttl );
$selected_interval_str = strtolower( $selected_interval );
$allowed_intervals = get_allowed_intervals();
if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
$selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
}
$message = $args['message'];
$is_html = ! empty( $args['headers'] ) && false !== strpos( implode( '|', (array) $args['headers'] ), 'text/html' );
$link = create_login_link( $user );
if ( $is_html ) {
$login_message = '
';
/* translators: %s: The magic login link */
$login_message .= sprintf( __( 'Click here to login.', 'magic-login' ), $link );
} else {
$login_message = PHP_EOL;
/* translators: %s: The magic login link */
$login_message .= sprintf( __( 'Auto Login: %s', 'magic-login' ), $link );
}
if ( $token_ttl > 0 ) {
$login_message .= $is_html ? '
' : PHP_EOL;
/* translators: 1: TTL value (number) 2: Unit (minute(s), hour(s), days(s)) */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
$login_message .= sprintf( __( 'Login link will expire in %1$s %2$s.', 'magic-login' ), $token_ttl, $selected_interval_str );
}
/**
* Filter login message
*
* @param string $login_message Appended message for the login
* @param string $link Login URL
* @param \WP_User $user User Object
*
* @since 1.6
*/
$login_message = apply_filters( 'magic_login_auto_login_link_message', $login_message, $link, $user );
$email_message = $message . $login_message;
/**
* Filter email message
*
* @param string $email_message Email message
* @param string $message Email content before appending login link
* @param string $login_message Login message
* @param array $args WP Mail args
* @param string $link Login URL
* @param \WP_User $user User Object
*
* @since 1.6
*/
return apply_filters( 'magic_login_auto_login_link_email_message', $email_message, $message, $login_message, $args, $link, $user );
}
/**
* Check if auto login link is excluded for given mail
*
* @param array $args wp mail args
*
* @return bool
* @since 1.6
*/
public static function is_auto_login_link_excluded_mail( $args ) {
$is_excluded = false;
/**
* Exclude some of the emails
* Copy emails as is, for covering in translated versions
*
* @link https://github.com/johnbillion/wp_mail
*/
$excluded_subjects = apply_filters(
'magic_login_auto_login_excluded_subjects',
[
__( '[%s] New Admin Email Address' ),
__( '[%s] Network Admin Email Change Request' ),
__( '[%s] Admin Email Changed' ),
__( '[%s] Notice of Network Admin Email Change' ),
__( '[%s] Login Details' ),
__( '[%s] Password Reset' ),
__( '[%s] Password Changed' ),
__( '[%s] Email Change Request' ),
__( 'Your login confirmation code' ),
]
);
// remove [%s] from subjects
$normalize_email_title = preg_replace( '#\[.*?\]#s', ' ', $args['subject'] ); // remove placeholders
foreach ( $excluded_subjects as $subject ) {
$subject = preg_replace( '#\[.*?\]#s', ' ', $subject ); // remove placeholders
if ( false !== strpos( $normalize_email_title, $subject ) ) {
$is_excluded = true;
break;
}
}
// no need to add for login email itself
if ( did_action( 'magic_login_send_login_link' ) ) {
$is_excluded = true;
}
/**
* Filter if auto login link is excluded for given mail
*
* @param bool $is_excluded whether the email is excluded or not
* @param array $args wp_mail args
*
* @since 1.6
*/
return (bool) apply_filters( 'magic_login_auto_login_link_excluded', $is_excluded, $args );
}
/**
* Check if email has single recipient
*
* @param array $atts wp_mail args
*
* @return bool
* @since 2.0.0
* @since 2.4 Previously named `has_single_recipient`
*/
public static function is_single_recipient( $atts ) {
$to = $atts['to'];
if ( empty( $to ) ) {
return false;
}
if ( is_array( $to ) && 1 !== count( $to ) ) {
return false;
}
$to = is_array( $to ) ? array_shift( $to ) : $to;
if ( is_string( $to ) && false !== strpos( $to, ',' ) ) {
return false;
}
/**
* Check bcc/cc
* Login links are personal, so we don't want to send them to other people.
*/
if ( ! empty( $atts['headers'] ) ) {
$headers = $atts['headers'];
if ( is_string( $headers ) ) {
$headers = [ $headers ];
}
foreach ( $headers as $header ) {
if ( 1 === preg_match( '/(bcc|cc):/i', $header ) ) {
return false;
}
}
}
return true;
}
/**
* Add small tweaks to login form
*/
public static function login_css() {
$settings = \MagicLogin\Utils\get_settings();
if ( ! $settings['add_login_button'] ) {
return;
}
?>
time() ) {
$live_tokens[] = $token;
}
}
update_user_meta( $user_id, TOKEN_USER_META, $live_tokens );
}
/**
* Handle login request
*/
public static function handle_login_request() {
global $pagenow;
/**
* Since 1.2.2 $pagenow control has been deprecated
* in favor compatibility with 3rd party plugins
*/
if ( 'wp-login.php' !== $pagenow && empty( $_GET['magic-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
if ( empty( $_GET['user_id'] ) || empty( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
return;
}
do_action( 'magic_login_handle_login_request' );
// Use a generic error message to ensure user ids can't be sniffed
$user_id = (int) $_GET['user_id']; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
$token = $_GET['token']; //phpcs:ignore
self::authenticate_user_token( $user_id, $token );
}
/**
* Authenticate the user with the token.
*
* @param int $user_id User ID.
* @param string $token Login token.
* @param bool $code_login Code login or not.
*
* @return void|string Returns error message or redirect info if $code_login is true.
* @since 2.4
*/
public static function authenticate_user_token( $user_id, $token, $code_login = false ) {
// Load settings once.
$settings = \MagicLogin\Utils\get_settings();
// Prepare the default error message.
if ( is_user_logged_in() ) {
/* translators: 1: User login 2: Dashboard URL */
$error = sprintf(
__( 'Invalid magic login token, but you are logged in as \'%1$s\'. Go to the dashboard instead?', 'magic-login' ),
wp_get_current_user()->user_login,
admin_url()
);
} else {
/* translators: %s: Login URL */
$error = sprintf(
__( 'Invalid magic login token. Try signing in instead?', 'magic-login' ),
wp_login_url()
);
if ( $settings['is_default'] ) {
/* translators: %s: Magic Login URL */
$login_url = esc_url( add_query_arg( 'action', 'magic_login', wp_login_url() ) );
$error = sprintf(
__( 'Invalid magic login token. Please try to create a new login link?', 'magic-login' ),
$login_url
);
}
}
// Override error message if we're in code login mode.
if ( $code_login ) {
$error = esc_html__( 'Invalid login code.', 'magic-login' );
}
// Early return if the user does not exist.
$user = get_user_by( 'id', $user_id ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! $user ) {
do_action( 'magic_login_invalid_user' );
$error = apply_filters( 'magic_login_error_message', $error, 'invalid_user' );
if ( $code_login ) {
return wp_kses_post( $error );
}
wp_die( wp_kses_post( $error ) );
}
// Validate token.
$token_validity = $settings['token_validity'];
$tokens = get_user_tokens( $user->ID, true );
$is_valid = false;
$current_token = null;
foreach ( $tokens as $i => $token_data ) {
if ( empty( $token_data ) || ! is_array( $token_data ) || ! isset( $token_data['token'] ) ) {
unset( $tokens[ $i ] );
continue;
}
// Verify the token using a secure comparison.
if ( hash_equals( $token_data['token'], hash_hmac( 'sha256', $token, wp_salt() ) ) ) { // phpcs:ignore
$is_valid = true;
$current_token = $token_data;
$token_usage_count = isset( $token_data['usage_count'] ) ? absint( $token_data['usage_count'] ) + 1 : 1;
$tokens[ $i ]['usage_count'] = $token_usage_count;
// Remove token if usage exceeds validity.
if ( 0 !== $token_validity && $token_validity <= $token_usage_count ) {
unset( $tokens[ $i ] );
}
break;
}
}
if ( ! $is_valid ) {
do_action( 'magic_login_invalid_token' );
$error = apply_filters( 'magic_login_invalid_token_error_message', $error );
$error = apply_filters( 'magic_login_error_message', $error, 'invalid_token' );
if ( $code_login ) {
return $error;
}
wp_die( wp_kses_post( $error ) );
}
// Proceed with login if token is valid.
if ( headers_sent() ) {
error_log( 'Magic Login: Headers already sent. Cannot set auth cookie.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
}
// Trigger actions before login.
do_action( 'magic_login_before_login', $user, $current_token );
// Update token metadata.
update_user_meta( $user->ID, TOKEN_USER_META, $tokens );
// Set authentication cookie.
wp_set_auth_cookie( $user->ID, true, is_ssl() );
// Trigger post-login actions.
do_action( 'magic_login_logged_in', $user, $current_token );
do_action( 'wp_login', $user->user_login, $user );
// Determine redirect URL.
$redirect_to = get_user_default_redirect( $user );
$requested_redirect_to = isset( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; // phpcs:ignore
$redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $user );
$login_redirect = apply_filters( 'magic_login_redirect', $redirect_to, $user );
// Return early if in code login mode.
if ( $code_login ) {
return [ 'redirect_to' => $login_redirect ];
}
// Perform final redirection.
wp_safe_redirect( $login_redirect );
exit;
}
/**
* Login form actions
*/
public static function wp_login_action() {
$login_request = self::process_login_request();
login_header( esc_html__( 'Log in', 'magic-login' ), $login_request['info'], $login_request['errors'] );
if ( $login_request['code_login'] ) {
CodeLogin::code_form(); // phpcs:ignore
} elseif ( $login_request['show_form'] ) {
self::login_form();
}
login_footer();
exit;
}
/**
* Process login request
*
* @param array $args Custom messages - Added in 2.1
* eg: [
* 'info_message' => 'Custom message'
* 'error_message' => 'Custom message'
* 'success_message' => 'Custom message'
* ]
*
* @return array
*/
public static function process_login_request( $args = [] ) {
$defaults = [
'info_message' => '',
];
$args = wp_parse_args( $args, $defaults );
$response = [
'show_form' => true,
'errors' => new WP_Error(),
'info' => '',
'is_processed' => false,
'show_registration_form' => false,
'code_login' => false,
'phone_login' => false,
];
// Default info message
$response['info'] = defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY
? '' . __( 'Please enter your username. You will receive an email message to log in.', 'magic-login' ) . '
'
: '' . __( 'Please enter your username or email address. You will receive an email message to log in.', 'magic-login' ) . '
';
// Allow customization of info message
$response['info'] = apply_filters( 'magic_login_info_message', $response['info'], $args );
// Override info message if provided
if ( ! empty( $args['info_message'] ) ) {
$response['info'] = '' . esc_html( $args['info_message'] ) . '
';
}
// Handle form submission
if ( self::is_login_form_submission() ) {
$response = self::process_login_form_submission( $args );
}
// Handle magic link login request
if ( ! empty( $_GET['magic-registration'] ) ) { // phpcs:ignore
$response['show_registration_form'] = true;
}
/**
* Filter the result of the login process
*
* @hook magic_login_process_login_request_result
* @param array $response Result of the login process
* @param array $args Arguments passed to the function
* @since 2.4
*/
return apply_filters( 'magic_login_process_login_request_result', $response, $args );
}
/**
* login form
*/
public static function login_form() {
$user_login = '';
if ( isset( $_POST['log'] ) && is_string( $_POST['log'] ) ) {
$user_login = wp_unslash( $_POST['log'] ); // phpcs:ignore
}
?>
$is_processed,
'errors' => $result,
'info' => $info,
'show_form' => $show_form,
'show_registration_form' => $show_registration_form,
'code_login' => $code_login,
'phone_login' => $phone_login,
];
}
// Apply pre-send link filter
$send_link = apply_filters( 'magic_login_pre_send_login_link', null, $user );
$errors = null;
if ( ! is_a( $user, '\WP_User' ) ) {
$error_code = 'missing_user';
if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) {
$error_message = esc_html__( 'There is no account with that username.', 'magic-login' );
} else {
$error_message = esc_html__( 'There is no account with that username or email address.', 'magic-login' );
}
if ( $code_login && $phone_login ) {
$error_message = esc_html__( 'Unable to process your request. Please try again.', 'magic-login' );
}
if ( ! empty( $args['error_message'] ) ) {
$error_message = $args['error_message'];
}
/**
* Filter the error message when user is missing
*
* @hook magic_login_missing_user_error_message
* $error_message string
* $error_code string
* $args array
* @since 2.4
*/
$error_message = apply_filters( 'magic_login_missing_user_error_message', $error_message, $error_code, $args );
$errors = new WP_Error( $error_code, $error_message );
$show_form = true;
$code_login = false;
$phone_login = false;
} elseif ( null !== $send_link ) {
$errors = $send_link;
$show_form = false;
$code_login = false;
$phone_login = false;
} else {
global $magic_login_code_login_result, $magic_login_link;
if ( ! empty( $magic_login_code_login_result ) ) {
// after post request, if code login failed, show error message
$errors = $magic_login_code_login_result;
} else {
$errors = self::send_login_link( $user, false, $code_login );
}
}
if ( ! is_wp_error( $errors ) ) {
$show_form = false;
$info = '' . __( 'Please check your inbox for the login link. If you did not receive a login email, check your spam folder too.', 'magic-login' ) . '
';
if ( $phone_login ) {
$info = '' . __( 'Please check your phone for the login link.', 'magic-login' ) . '
';
}
if ( ! empty( $args['success_message'] ) ) {
$info = '' . $args['success_message'] . '
';
}
if ( $code_login ) {
$info = '' . __( 'Please enter the code sent to your email.', 'magic-login' ) . '
';
if ( ! empty( $args['code_success_message'] ) ) {
$info = '' . $args['code_success_message'] . '
';
}
}
}
return [
'is_processed' => $is_processed,
'errors' => $errors,
'info' => $info,
'show_form' => $show_form,
'show_registration_form' => $show_registration_form,
'code_login' => $code_login,
'phone_login' => $phone_login,
];
}
/**
* Send magic link to user
*
* @param object $user \WP_User object
* @param mixed|string|bool $login_link use given link when it provided. @since 1.9
* @param mixed|string|bool $code_login create login link with code. @since 2.4
*
* @return bool
*/
public static function send_login_link( $user, $login_link = false, $code_login = false ) {
global $magic_login_token;
if ( ! $login_link ) {
$context = $code_login ? 'email_code' : 'email';
$login_link = create_login_link( $user, $context );
}
$settings = \MagicLogin\Utils\get_settings();
$login_email = $settings['login_email'];
$email_subject = $settings['email_subject'];
$placeholder_values = get_email_placeholders_by_user( $user );
$placeholder_values['{{MAGIC_LINK}}'] = $login_link;
$placeholder_values['{{MAGIC_LOGIN_CODE}}'] = $magic_login_token;
$login_email = str_replace( array_keys( $placeholder_values ), $placeholder_values, $login_email );
$email_subject = str_replace( array_keys( $placeholder_values ), $placeholder_values, $email_subject );
$login_email = apply_filters( 'magic_login_email_content', $login_email, $placeholder_values );
$email_subject = apply_filters( 'magic_login_email_subject', $email_subject, $placeholder_values );
$headers = apply_filters( 'magic_login_email_headers', array( 'Content-Type: text/html; charset=UTF-8' ) );
foreach ( (array) $headers as $header ) {
if ( false !== stripos( $header, 'text/html' ) ) {
// convert line breaks to br when content type is html but
// input doesn't contain HTML tags (adding
can ruin the templating)
if ( strip_tags( $login_email, '' ) === $login_email ) {
$login_email = nl2br( $login_email );
}
break;
}
}
/**
* Send the email only once at a run.
* Eg: when having login block in a page, and shortcode at some other part of the page.
* It will send the email twice due to the way we handle the request.
*/
if ( did_action( 'magic_login_send_login_link' ) ) {
return true;
}
do_action( 'magic_login_send_login_link', $user );
return wp_mail( $user->user_email, $email_subject, $login_email, $headers );
}
/**
* Determines if the current request is a login form submission.
*
* @return bool
* @since 2.4
*/
private static function is_login_form_submission() {
return isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_POST['log'] );
}
}