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'] ); } }