PluginProbe
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password / 2.4
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password v2.4
2.8.1 2.1 2.1.1 2.1.2 2.1.3 2.2 2.3 2.3.1 2.3.2 2.3.3 2.3.4 2.3.5 2.4 2.4.1 2.4.2 2.5 2.5.1 2.6 2.6.1 2.6.2 2.6.3 2.7 2.7.1 2.8 trunk All 48 releases
magic-login / includes / classes / LoginManager.php

LoginManager.php in Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password 2.4, at includes/classes/LoginManager.php

1,194 lines 34.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Login functionality
4 *
5 * @package MagicLogin
6 */
7
8 namespace MagicLogin;
9
10 use function MagicLogin\Utils\get_email_placeholders_by_user;
11 use function MagicLogin\Utils\get_user_by_log_input;
12 use function MagicLogin\Utils\get_wp_login_url;
13 use function MagicLogin\Utils\get_ttl_by_user;
14 use const MagicLogin\Constants\CRON_HOOK_NAME;
15 use const MagicLogin\Constants\TOKEN_USER_META;
16 use function MagicLogin\Utils\create_login_link;
17 use function MagicLogin\Utils\get_allowed_intervals;
18 use function MagicLogin\Utils\get_ttl_with_interval;
19 use function MagicLogin\Utils\get_user_default_redirect;
20 use function MagicLogin\Utils\get_user_tokens;
21 use \WP_Error as WP_Error;
22
23 // phpcs:disable WordPress.WP.I18n.MissingTranslatorsComment
24
25 /**
26 * Login Manager
27 */
28 class LoginManager {
29 /**
30 * Singleton instance
31 *
32 * @var self
33 */
34 private static $instance = null;
35
36 /**
37 * Return an instance of the current class
38 */
39 public static function setup() {
40 static $instance = false;
41
42 if ( ! $instance ) {
43 $instance = new self();
44 }
45
46 return $instance;
47 }
48
49 /**
50 * Constructor
51 */
52 public function __construct() {
53 add_action( 'login_form_magic_login', [ self::class, 'wp_login_action' ] );
54 add_action( 'login_form_login', [ self::class, 'maybe_redirect' ] );
55 add_action( 'init', [ self::class, 'handle_login_request' ], 1 );
56 add_action( CRON_HOOK_NAME, [ self::class, 'clear_expired_tokens' ] );
57 add_action( 'login_footer', [ self::class, 'print_login_button' ] );
58 add_action( 'login_head', [ self::class, 'login_css' ] );
59 add_action( 'wp_ajax_magic_login_ajax_request', [ self::class, 'ajax_request' ] );
60 add_action( 'wp_ajax_nopriv_magic_login_ajax_request', [ self::class, 'ajax_request' ] );
61 add_filter( 'wp_mail', [ self::class, 'maybe_add_auto_login_link' ], 999 );
62 add_filter( 'wp_mail', [ self::class, 'replace_magic_link_in_wp_mail' ], 999 );
63 }
64
65 /**
66 * Replace {{MAGIC_LINK}} placeholder with login link for all outgoing emails
67 *
68 * @param array $atts wp_mail args
69 *
70 * @return mixed
71 * @since 2.0.0
72 */
73 public static function replace_magic_link_in_wp_mail( $atts ) {
74 if ( ! is_array( $atts ) || empty( $atts['message'] ) ) {
75 return $atts;
76 }
77
78 // replace encoded placeholder
79 $atts['message'] = str_replace( '%7B%7BMAGIC_LINK%7D%7D', '{{MAGIC_LINK}}', $atts['message'] );
80
81 if ( false === strpos( $atts['message'], '{{MAGIC_LINK}}' ) ) {
82 return $atts;
83 }
84
85 $magic_link = '';
86
87 if ( self::is_single_recipient( $atts ) ) {
88 $to = $atts['to'];
89 $to = is_array( $to ) ? array_shift( $to ) : $to;
90 $user = get_user_by( 'email', $to );
91
92 if ( $user ) {
93
94 /**
95 * Filter magic_login_replace_magic_link_in_wp_mail
96 *
97 * @param bool $status false to exclude, default true
98 * @param array $atts wp_mail args
99 * @param \WP_User $user user object
100 *
101 * @since 2.0.0
102 */
103 $replace_magic_link = apply_filters( 'magic_login_replace_magic_link_in_wp_mail', true, $atts, $user );
104 if ( $replace_magic_link ) {
105 $magic_link = create_login_link( $user );
106 }
107 }
108 }
109
110 /**
111 * Filter magic login replace
112 *
113 * @param string $magic_link login link
114 * @param array $atts wp_mail args
115 *
116 * @since 2.0.0
117 */
118 $magic_link = apply_filters( 'magic_login_replace_magic_link_in_wp_mail_message', $magic_link, $atts );
119 $atts['message'] = str_replace( '{{MAGIC_LINK}}', $magic_link, $atts['message'] );
120
121 return $atts;
122 }
123
124
125 /**
126 * Ajax callback for login requests
127 *
128 * @return void
129 */
130 public static function ajax_request() {
131
132 if ( ! isset( $_POST['data'] ) ) {
133 wp_send_json_error(
134 [
135 'message' => esc_html__( 'Invalid request', 'magic-login' ),
136 'show_form' => true,
137 'show_registration_form' => false,
138 ]
139 );
140 }
141
142 parse_str( wp_unslash( $_POST['data'] ), $form_data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
143
144 $global_data = [ 'log', 'redirect_to', 'g-recaptcha-response', 'cf-turnstile-response' ];
145
146 // populate super global $_POST with form data
147 foreach ( $global_data as $key ) {
148 if ( isset( $form_data[ $key ] ) ) {
149 $_POST[ $key ] = $form_data[ $key ];
150 }
151 }
152
153 $args = []; // pass custom messages to backend
154 if ( ! empty( $form_data['messages'] ) ) {
155 foreach ( $form_data['messages'] as $key => $value ) {
156 $args[ $key . '_message' ] = wp_kses_post( $value );
157 }
158 }
159
160 $login_request = self::process_login_request( $args );
161 $settings = \MagicLogin\Utils\get_settings();
162 $error_messages = '';
163 $login_errors = $login_request['errors'];
164
165 if ( $login_request['show_registration_form'] ) {
166 $email = isset( $_POST['log'] ) && is_email( wp_unslash( $_POST['log'] ) ) ? sanitize_email( wp_unslash( $_POST['log'] ) ) : '';
167 if ( 'auto' === $settings['registration']['mode'] && $settings['registration']['fallback_email_field'] ) {
168 $shortcode = sprintf( '[magic_login_registration_form show_name="false" show_terms="false" email="%s"]', $email );
169 } else {
170 $shortcode = sprintf( '[magic_login_registration_form email="%s"]', $email );
171 }
172
173 $registration_form = do_shortcode( $shortcode );
174
175 wp_send_json_success(
176 [
177 'message' => $login_request['info'],
178 'show_form' => $login_request['show_form'],
179 'code_login' => $login_request['code_login'],
180 'phone_login' => $login_request['phone_login'],
181 'registration_form' => $registration_form,
182 ]
183 );
184 }
185
186 // error messages
187 if ( ! empty( $login_errors ) && is_wp_error( $login_errors ) && $login_errors->has_errors() ) {
188 foreach ( $login_errors->get_error_codes() as $code ) {
189 foreach ( $login_errors->get_error_messages( $code ) as $message ) {
190 $error_messages .= $message . "<br />\n";
191 }
192 }
193 }
194
195 if ( ! empty( $error_messages ) ) {
196 $error_message = sprintf( '<div id="login_error" class="magic_login_block_login_error">%s</div>', wp_kses_post( $error_messages ) );
197
198 wp_send_json_error(
199 [
200 'message' => $error_message,
201 'show_form' => $login_request['show_form'],
202 'code_login' => $login_request['code_login'],
203 'phone_login' => $login_request['phone_login'],
204 ]
205 );
206 }
207
208 if ( $login_request['code_login'] ) {
209 ob_start();
210 CodeLogin::code_form();
211 $code_login_form = ob_get_clean();
212 wp_send_json_success(
213 [
214 'info' => $login_request['info'],
215 'errors' => $login_request['errors'],
216 'message' => $login_request['info'],
217 'show_form' => $login_request['show_form'],
218 'code_login' => $login_request['code_login'],
219 'phone_login' => $login_request['phone_login'],
220 'code_form' => $code_login_form,
221 ]
222 );
223 }
224
225 if ( ! empty( $login_request['info'] ) ) {
226 wp_send_json_success(
227 [
228 'code_login' => $login_request['code_login'],
229 'phone_login' => $login_request['phone_login'],
230 'message' => $login_request['info'],
231 'show_form' => $login_request['show_form'],
232 ]
233 );
234 }
235 }
236
237
238 /**
239 * Maybe add login link to outgoing email
240 *
241 * @param array $atts wp_mail args
242 *
243 * @return mixed
244 * @since 1.6
245 */
246 public static function maybe_add_auto_login_link( $atts ) {
247 $settings = \MagicLogin\Utils\get_settings();
248
249 if ( ! $settings['auto_login_links'] ) {
250 return $atts;
251 }
252
253 $to = $atts['to'];
254
255 if ( ! self::is_single_recipient( $atts ) ) {
256 return $atts;
257 }
258
259 $to = is_array( $to ) ? array_shift( $to ) : $to;
260 $user = get_user_by( 'email', $to );
261
262 if ( ! $user ) {
263 return $atts;
264 }
265
266 if ( self::is_auto_login_link_excluded_mail( $atts ) ) {
267 return $atts;
268 }
269
270 /**
271 * Filter auto login link
272 *
273 * @param bool $status false to exclude, default true
274 * @param array $atts wp_mail args
275 * @param \WP_User $user user object
276 *
277 * @since 1.6.0
278 */
279 $add_login_link = apply_filters( 'magic_login_add_auto_login_link', true, $atts, $user );
280
281 if ( ! $add_login_link ) {
282 return $atts;
283 }
284
285 $atts['message'] = self::add_auto_login_link_to_message( $atts, $user );
286
287 return $atts;
288 }
289
290
291 /**
292 * Add auto login link to message
293 *
294 * @param array $args wp mail content
295 * @param \WP_User $user User Object
296 *
297 * @return string
298 * @since 1.6
299 */
300 public static function add_auto_login_link_to_message( $args, $user ) {
301 $ttl = get_ttl_by_user( $user->ID );
302 list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $ttl );
303 $selected_interval_str = strtolower( $selected_interval );
304 $allowed_intervals = get_allowed_intervals();
305 if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
306 $selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
307 }
308
309 $message = $args['message'];
310 $is_html = ! empty( $args['headers'] ) && false !== strpos( implode( '|', (array) $args['headers'] ), 'text/html' );
311
312 $link = create_login_link( $user );
313
314 if ( $is_html ) {
315 $login_message = '<br>';
316 /* translators: %s: The magic login link */
317 $login_message .= sprintf( __( '<a href="%s" target="_blank" rel="noopener">Click here to login</a>.', 'magic-login' ), $link );
318 } else {
319 $login_message = PHP_EOL;
320 /* translators: %s: The magic login link */
321 $login_message .= sprintf( __( 'Auto Login: %s', 'magic-login' ), $link );
322 }
323
324 if ( $token_ttl > 0 ) {
325 $login_message .= $is_html ? '<br>' : PHP_EOL;
326 /* translators: 1: TTL value (number) 2: Unit (minute(s), hour(s), days(s)) */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
327 $login_message .= sprintf( __( 'Login link will expire in %1$s %2$s.', 'magic-login' ), $token_ttl, $selected_interval_str );
328 }
329
330 /**
331 * Filter login message
332 *
333 * @param string $login_message Appended message for the login
334 * @param string $link Login URL
335 * @param \WP_User $user User Object
336 *
337 * @since 1.6
338 */
339 $login_message = apply_filters( 'magic_login_auto_login_link_message', $login_message, $link, $user );
340
341 $email_message = $message . $login_message;
342
343 /**
344 * Filter email message
345 *
346 * @param string $email_message Email message
347 * @param string $message Email content before appending login link
348 * @param string $login_message Login message
349 * @param array $args WP Mail args
350 * @param string $link Login URL
351 * @param \WP_User $user User Object
352 *
353 * @since 1.6
354 */
355 return apply_filters( 'magic_login_auto_login_link_email_message', $email_message, $message, $login_message, $args, $link, $user );
356 }
357
358
359 /**
360 * Check if auto login link is excluded for given mail
361 *
362 * @param array $args wp mail args
363 *
364 * @return bool
365 * @since 1.6
366 */
367 public static function is_auto_login_link_excluded_mail( $args ) {
368 $is_excluded = false;
369
370 /**
371 * Exclude some of the emails
372 * Copy emails as is, for covering in translated versions
373 *
374 * @link https://github.com/johnbillion/wp_mail
375 */
376 $excluded_subjects = apply_filters(
377 'magic_login_auto_login_excluded_subjects',
378 [
379 __( '[%s] New Admin Email Address' ),
380 __( '[%s] Network Admin Email Change Request' ),
381 __( '[%s] Admin Email Changed' ),
382 __( '[%s] Notice of Network Admin Email Change' ),
383 __( '[%s] Login Details' ),
384 __( '[%s] Password Reset' ),
385 __( '[%s] Password Changed' ),
386 __( '[%s] Email Change Request' ),
387 __( 'Your login confirmation code' ),
388 ]
389 );
390
391 // remove [%s] from subjects
392 $normalize_email_title = preg_replace( '#\[.*?\]#s', ' ', $args['subject'] ); // remove placeholders
393 foreach ( $excluded_subjects as $subject ) {
394 $subject = preg_replace( '#\[.*?\]#s', ' ', $subject ); // remove placeholders
395 if ( false !== strpos( $normalize_email_title, $subject ) ) {
396 $is_excluded = true;
397 break;
398 }
399 }
400
401 // no need to add for login email itself
402 if ( did_action( 'magic_login_send_login_link' ) ) {
403 $is_excluded = true;
404 }
405
406 /**
407 * Filter if auto login link is excluded for given mail
408 *
409 * @param bool $is_excluded whether the email is excluded or not
410 * @param array $args wp_mail args
411 *
412 * @since 1.6
413 */
414 return (bool) apply_filters( 'magic_login_auto_login_link_excluded', $is_excluded, $args );
415 }
416
417
418 /**
419 * Check if email has single recipient
420 *
421 * @param array $atts wp_mail args
422 *
423 * @return bool
424 * @since 2.0.0
425 * @since 2.4 Previously named `has_single_recipient`
426 */
427 public static function is_single_recipient( $atts ) {
428 $to = $atts['to'];
429
430 if ( empty( $to ) ) {
431 return false;
432 }
433
434 if ( is_array( $to ) && 1 !== count( $to ) ) {
435 return false;
436 }
437
438 $to = is_array( $to ) ? array_shift( $to ) : $to;
439
440 if ( is_string( $to ) && false !== strpos( $to, ',' ) ) {
441 return false;
442 }
443
444 /**
445 * Check bcc/cc
446 * Login links are personal, so we don't want to send them to other people.
447 */
448 if ( ! empty( $atts['headers'] ) ) {
449 $headers = $atts['headers'];
450
451 if ( is_string( $headers ) ) {
452 $headers = [ $headers ];
453 }
454
455 foreach ( $headers as $header ) {
456 if ( 1 === preg_match( '/(bcc|cc):/i', $header ) ) {
457 return false;
458 }
459 }
460 }
461
462 return true;
463 }
464
465
466 /**
467 * Add small tweaks to login form
468 */
469 public static function login_css() {
470 $settings = \MagicLogin\Utils\get_settings();
471
472 if ( ! $settings['add_login_button'] ) {
473 return;
474 }
475
476 ?>
477 <style>
478
479 form[name="validate_2fa_form"] #magic-login-button,
480 form[name="validate_2fa_form"] .magic-login-or-separator {
481 display: none;
482 }
483
484 form[name="validate_2fa_form"] .submit {
485 display: none;
486 }
487
488 .two-factor-email-resend input[type="submit"] {
489 width: 100%;
490 margin: auto;
491 display: block;
492 text-align: center;
493 padding: 0 36px;
494 min-height: 46px;
495 }
496
497
498 #loginform #wp-submit {
499 display: none;
500 }
501
502 .magic-login-normal-login {
503 width: 100%;
504 margin: auto;
505 padding-top: 10px;
506 display: block;
507 text-align: center;
508 clear: both;
509 }
510
511 .magic-login-normal-login .button,
512 #magic-login-button {
513 width: 100%;
514 float: none !important;
515 }
516
517 #magic-login-button {
518 padding: unset !important;
519 }
520
521 .continue-with-magic-login {
522 width: 100%;
523 margin: auto;
524 display: block;
525 text-align: center;
526 }
527
528 .continue-with-magic-login .button {
529 float: none;
530 }
531
532 .magic-login-or-separator {
533 display: block;
534 text-align: center;
535 position: relative;
536 margin: 10px auto;
537 width: 100%;
538 }
539
540 .magic-login-or-separator:before {
541 content: "<?php esc_html_e( 'or', 'magic-login' ); ?>";
542 background-color: #fff;
543 font-size: 13px;
544 color: #9b9b9b;
545 display: inline-block;
546 width: 62px;
547 position: relative;
548 z-index: 1;
549 }
550
551 .magic-login-or-separator:after {
552 content: "";
553 width: 100%;
554 position: absolute;
555 left: 0;
556 top: 50%;
557 height: 1px;
558 margin-top: -0.5px;
559 background-color: #d8d8d8;
560 }
561
562 .magic-login-captcha-wrapper {
563 text-align: center;
564 margin: 10px 0;
565 width: 100%;
566 }
567
568 /* Target reCAPTCHA v2 checkbox (not invisible) */
569 .magic-login-captcha-wrapper .g-recaptcha:not([data-size="invisible"]) {
570 transform: scale(0.90);
571 transform-origin: 0 0;
572 display: inline-block;
573 }
574
575 /* No scaling for invisible reCAPTCHA */
576 .magic-login-captcha-wrapper .g-recaptcha[data-size="invisible"] {
577 transform: none;
578 display: inline;
579 }
580
581 .magic-login-captcha-wrapper iframe,
582 .magic-login-captcha-wrapper .grecaptcha-badge {
583 width: 100% !important;
584 }
585
586 .magic-login-captcha-error {
587 color: #d63638;
588 }
589
590 #cf-turnstile-container {
591 width: 100%;
592 transform: scale(0.9);
593 transform-origin: top left;
594 }
595
596 </style>
597 <?php
598 }
599
600
601 /**
602 * Add login button to wp-login.php
603 */
604 public static function print_login_button() {
605 $settings = \MagicLogin\Utils\get_settings();
606
607 if ( ! $settings['add_login_button'] ) {
608 return;
609 }
610
611 $login_url = get_wp_login_url();
612
613 if ( isset( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
614 $login_url = esc_url_raw( add_query_arg( 'redirect_to', urlencode( $_GET['redirect_to'] ), $login_url ) ); // phpcs:ignore
615 }
616
617 ?>
618 <script type="text/javascript">
619 (function () {
620 let loginForm = document.getElementById('loginform');
621
622 if (loginForm) {
623 loginForm.insertAdjacentHTML(
624 'beforeend',
625 '<div class="magic-login-normal-login">' +
626 '<button type="submit" name="wp-submit" id="wp-login-submit" class="button button-primary button-hero magic-login-submit" value="<?php esc_attr_e( 'Log In', 'magic-login' ); // phpcs:ignore ?>"><?php esc_attr_e( 'Log In', 'magic-login' ); ?></button>' +
627 '</div>' +
628 '<span class="magic-login-or-separator"></span>' +
629 '<div id="continue-with-magic-login" class="continue-with-magic-login">' +
630 '<button type="button" value="<?php echo esc_url( $login_url ); ?>" class="button button-primary button-hero" id="magic-login-button">' +
631 '<?php esc_html_e( 'Send me the login link', 'magic-login' ); ?>' +
632 '</button>' +
633 '</div>'
634 );
635
636 document.getElementById('magic-login-button').onclick = function () {
637 let loginInput = document.getElementById('user_login');
638 if (loginInput != null && loginInput.value.length > 0) {
639 let frm = document.getElementById('loginform') || null;
640 if (frm) {
641 frm.action = "<?php echo esc_url_raw( $login_url ); ?>";
642 frm.submit();
643 }
644 } else {
645 location.href = "<?php echo esc_url_raw( $login_url ); ?>";
646 }
647 }
648 }
649 })();
650 </script>
651 <?php
652
653 }
654
655
656 /**
657 * Handle cleanup process for expired tokens
658 *
659 * @param int $user_id user id
660 * @since 2.4 Previously named cleanup_expired_tokens
661 */
662 public static function clear_expired_tokens( $user_id ) {
663 $ttl = get_ttl_by_user( $user_id );
664 $tokens = get_user_meta( $user_id, TOKEN_USER_META, true );
665 $tokens = is_string( $tokens ) ? array( $tokens ) : $tokens;
666 $live_tokens = array();
667
668 foreach ( $tokens as $token ) {
669 if ( empty( $token ) || ! isset( $token['time'] ) ) {
670 continue;
671 }
672
673 // not expired yet
674 if ( absint( $token['time'] ) + ( $ttl * MINUTE_IN_SECONDS ) > time() ) {
675 $live_tokens[] = $token;
676 }
677 }
678
679 update_user_meta( $user_id, TOKEN_USER_META, $live_tokens );
680 }
681
682 /**
683 * Handle login request
684 */
685 public static function handle_login_request() {
686 global $pagenow;
687
688 /**
689 * Since 1.2.2 $pagenow control has been deprecated
690 * in favor compatibility with 3rd party plugins
691 */
692 if ( 'wp-login.php' !== $pagenow && empty( $_GET['magic-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
693 return;
694 }
695
696 if ( empty( $_GET['user_id'] ) || empty( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
697 return;
698 }
699
700 do_action( 'magic_login_handle_login_request' );
701 // Use a generic error message to ensure user ids can't be sniffed
702 $user_id = (int) $_GET['user_id']; //phpcs:ignore WordPress.Security.NonceVerification.Recommended
703 $token = $_GET['token']; //phpcs:ignore
704 self::authenticate_user_token( $user_id, $token );
705 }
706
707 /**
708 * Authenticate the user with the token.
709 *
710 * @param int $user_id User ID.
711 * @param string $token Login token.
712 * @param bool $code_login Code login or not.
713 *
714 * @return void|string Returns error message or redirect info if $code_login is true.
715 * @since 2.4
716 */
717 public static function authenticate_user_token( $user_id, $token, $code_login = false ) {
718 // Load settings once.
719 $settings = \MagicLogin\Utils\get_settings();
720
721 // Prepare the default error message.
722 if ( is_user_logged_in() ) {
723 /* translators: 1: User login 2: Dashboard URL */
724 $error = sprintf(
725 __( 'Invalid magic login token, but you are logged in as \'%1$s\'. <a href="%2$s">Go to the dashboard instead</a>?', 'magic-login' ),
726 wp_get_current_user()->user_login,
727 admin_url()
728 );
729 } else {
730 /* translators: %s: Login URL */
731 $error = sprintf(
732 __( 'Invalid magic login token. <a href="%s">Try signing in instead</a>?', 'magic-login' ),
733 wp_login_url()
734 );
735 if ( $settings['is_default'] ) {
736 /* translators: %s: Magic Login URL */
737 $login_url = esc_url( add_query_arg( 'action', 'magic_login', wp_login_url() ) );
738 $error = sprintf(
739 __( 'Invalid magic login token. Please try to create <a href="%s">a new login link</a>?', 'magic-login' ),
740 $login_url
741 );
742 }
743 }
744
745 // Override error message if we're in code login mode.
746 if ( $code_login ) {
747 $error = esc_html__( 'Invalid login code.', 'magic-login' );
748 }
749
750 // Early return if the user does not exist.
751 $user = get_user_by( 'id', $user_id ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
752 if ( ! $user ) {
753 do_action( 'magic_login_invalid_user' );
754 $error = apply_filters( 'magic_login_error_message', $error, 'invalid_user' );
755 if ( $code_login ) {
756 return wp_kses_post( $error );
757 }
758 wp_die( wp_kses_post( $error ) );
759 }
760
761 // Validate token.
762 $token_validity = $settings['token_validity'];
763 $tokens = get_user_tokens( $user->ID, true );
764 $is_valid = false;
765 $current_token = null;
766
767 foreach ( $tokens as $i => $token_data ) {
768 if ( empty( $token_data ) || ! is_array( $token_data ) || ! isset( $token_data['token'] ) ) {
769 unset( $tokens[ $i ] );
770 continue;
771 }
772
773 // Verify the token using a secure comparison.
774 if ( hash_equals( $token_data['token'], hash_hmac( 'sha256', $token, wp_salt() ) ) ) { // phpcs:ignore
775 $is_valid = true;
776 $current_token = $token_data;
777 $token_usage_count = isset( $token_data['usage_count'] ) ? absint( $token_data['usage_count'] ) + 1 : 1;
778 $tokens[ $i ]['usage_count'] = $token_usage_count;
779
780 // Remove token if usage exceeds validity.
781 if ( 0 !== $token_validity && $token_validity <= $token_usage_count ) {
782 unset( $tokens[ $i ] );
783 }
784 break;
785 }
786 }
787
788 if ( ! $is_valid ) {
789 do_action( 'magic_login_invalid_token' );
790 $error = apply_filters( 'magic_login_invalid_token_error_message', $error );
791 $error = apply_filters( 'magic_login_error_message', $error, 'invalid_token' );
792 if ( $code_login ) {
793 return $error;
794 }
795 wp_die( wp_kses_post( $error ) );
796 }
797
798 // Proceed with login if token is valid.
799 if ( headers_sent() ) {
800 error_log( 'Magic Login: Headers already sent. Cannot set auth cookie.' ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
801 }
802
803 // Trigger actions before login.
804 do_action( 'magic_login_before_login', $user, $current_token );
805
806 // Update token metadata.
807 update_user_meta( $user->ID, TOKEN_USER_META, $tokens );
808
809 // Set authentication cookie.
810 wp_set_auth_cookie( $user->ID, true, is_ssl() );
811
812 // Trigger post-login actions.
813 do_action( 'magic_login_logged_in', $user, $current_token );
814 do_action( 'wp_login', $user->user_login, $user );
815
816 // Determine redirect URL.
817 $redirect_to = get_user_default_redirect( $user );
818 $requested_redirect_to = isset( $_REQUEST['redirect_to'] ) && is_string( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : ''; // phpcs:ignore
819
820 $redirect_to = apply_filters( 'login_redirect', $redirect_to, $requested_redirect_to, $user );
821 $login_redirect = apply_filters( 'magic_login_redirect', $redirect_to, $user );
822
823 // Return early if in code login mode.
824 if ( $code_login ) {
825 return [ 'redirect_to' => $login_redirect ];
826 }
827
828 // Perform final redirection.
829 wp_safe_redirect( $login_redirect );
830 exit;
831 }
832
833
834 /**
835 * Login form actions
836 */
837 public static function wp_login_action() {
838 $login_request = self::process_login_request();
839
840 login_header( esc_html__( 'Log in', 'magic-login' ), $login_request['info'], $login_request['errors'] );
841
842 if ( $login_request['code_login'] ) {
843 CodeLogin::code_form(); // phpcs:ignore
844 } elseif ( $login_request['show_form'] ) {
845 self::login_form();
846 }
847
848 login_footer();
849 exit;
850 }
851
852
853 /**
854 * Process login request
855 *
856 * @param array $args Custom messages - Added in 2.1
857 * eg: [
858 * 'info_message' => 'Custom message'
859 * 'error_message' => 'Custom message'
860 * 'success_message' => 'Custom message'
861 * ]
862 *
863 * @return array
864 */
865 public static function process_login_request( $args = [] ) {
866 $defaults = [
867 'info_message' => '',
868 ];
869 $args = wp_parse_args( $args, $defaults );
870
871 $response = [
872 'show_form' => true,
873 'errors' => new WP_Error(),
874 'info' => '',
875 'is_processed' => false,
876 'show_registration_form' => false,
877 'code_login' => false,
878 'phone_login' => false,
879 ];
880
881 // Default info message
882 $response['info'] = defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY
883 ? '<p class="message">' . __( 'Please enter your username. You will receive an email message to log in.', 'magic-login' ) . '</p>'
884 : '<p class="message">' . __( 'Please enter your username or email address. You will receive an email message to log in.', 'magic-login' ) . '</p>';
885
886 // Allow customization of info message
887 $response['info'] = apply_filters( 'magic_login_info_message', $response['info'], $args );
888
889 // Override info message if provided
890 if ( ! empty( $args['info_message'] ) ) {
891 $response['info'] = '<p class="message">' . esc_html( $args['info_message'] ) . '</p>';
892 }
893
894 // Handle form submission
895 if ( self::is_login_form_submission() ) {
896 $response = self::process_login_form_submission( $args );
897 }
898
899 // Handle magic link login request
900 if ( ! empty( $_GET['magic-registration'] ) ) { // phpcs:ignore
901 $response['show_registration_form'] = true;
902 }
903
904 /**
905 * Filter the result of the login process
906 *
907 * @hook magic_login_process_login_request_result
908 * @param array $response Result of the login process
909 * @param array $args Arguments passed to the function
910 * @since 2.4
911 */
912 return apply_filters( 'magic_login_process_login_request_result', $response, $args );
913 }
914
915 /**
916 * login form
917 */
918 public static function login_form() {
919 $user_login = '';
920
921 if ( isset( $_POST['log'] ) && is_string( $_POST['log'] ) ) {
922 $user_login = wp_unslash( $_POST['log'] ); // phpcs:ignore
923 }
924
925 ?>
926 <form
927 name="magicloginform"
928 id="magicloginform"
929 action="<?php echo esc_url( get_wp_login_url() ); ?>"
930 method="post"
931 autocomplete="off"
932 data-ajax-url="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>"
933 data-ajax-spinner="<?php echo esc_url( get_admin_url() . 'images/spinner.gif' ); ?>"
934 data-ajax-sending-msg="<?php esc_attr_e( 'Sending...', 'magic-login' ); ?>"
935 data-spam-protection-msg="<?php esc_attr_e( 'Please verify that you are not a robot.', 'magic-login' ); ?>"
936 >
937 <p>
938 <?php if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) : ?>
939 <label for="user_login"><?php esc_html_e( 'Username', 'magic-login' ); ?></label>
940 <?php else : ?>
941 <label for="user_login"><?php esc_html_e( 'Username or Email Address', 'magic-login' ); ?></label>
942 <?php endif; ?>
943 <input type="text" name="log" id="user_login" class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required />
944 </p>
945 <?php
946
947 /**
948 * Fires following the 'email' field in the login form.
949 *
950 * @since 1.0
951 */
952 do_action( 'magic_login_form' );
953
954 ?>
955 <p class="submit">
956 <input type="submit" name="wp-submit" id="wp-submit" style="float: none;width: 100%;" class="magic-login-submit button button-primary button-hero" value="<?php esc_attr_e( 'Send me the link', 'magic-login' ); ?>" />
957 <?php if ( isset( $_GET['redirect_to'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
958 <input type="hidden" name="redirect_to" value="<?php echo esc_url( $_GET['redirect_to'] ); // phpcs:ignore ?>">
959 <?php endif; ?>
960 <input type="hidden" name="testcookie" value="1" />
961 </p>
962 </form>
963 <?php
964 }
965
966
967 /**
968 * Redirect to magic login page once it used as default login method
969 */
970 public static function maybe_redirect() {
971 global $pagenow;
972
973 if ( 'wp-login.php' !== $pagenow ) {
974 return;
975 }
976
977 if ( ! empty( ( $_POST ) ) ) {
978 return;
979 }
980
981 if ( isset( $_REQUEST['interim-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
982 return;
983 }
984
985 /**
986 * short-circuit if want to interrupt redirect
987 */
988 if ( null !== apply_filters( 'magic_login_before_login_form_redirect', null ) ) {
989 return;
990 }
991
992 $settings = \MagicLogin\Utils\get_settings();
993
994 if ( true === $settings['is_default'] ) {
995 wp_safe_redirect( esc_url_raw( add_query_arg( 'action', 'magic_login' ) ) );
996 exit;
997 }
998 }
999
1000
1001 /**
1002 * Handle login submission POST request
1003 *
1004 * @param array $args Arguments
1005 *
1006 * @return array
1007 * @since 2.2
1008 */
1009 public static function process_login_form_submission( $args ) {
1010 $show_form = true;
1011 $show_registration_form = false;
1012 $code_login = false;
1013 $user_name = isset( $_POST['log'] ) ? sanitize_user( wp_unslash( $_POST['log'] ) ) : '';
1014 $user = get_user_by_log_input( $user_name );
1015 $is_processed = true;
1016 $phone_login = false;
1017 $info = '';
1018
1019 $settings = \MagicLogin\Utils\get_settings();
1020
1021 if ( false !== strpos( $settings['login_email'], '{{MAGIC_LOGIN_CODE}}' ) ) {
1022 $code_login = true;
1023 }
1024
1025 // Apply pre-process filter
1026 $result = apply_filters( 'magic_login_pre_process_login_request', null );
1027
1028 if ( is_wp_error( $result ) ) {
1029 return [
1030 'is_processed' => $is_processed,
1031 'errors' => $result,
1032 'info' => $info,
1033 'show_form' => $show_form,
1034 'show_registration_form' => $show_registration_form,
1035 'code_login' => $code_login,
1036 'phone_login' => $phone_login,
1037 ];
1038 }
1039
1040 // Apply pre-send link filter
1041 $send_link = apply_filters( 'magic_login_pre_send_login_link', null, $user );
1042 $errors = null;
1043
1044 if ( ! is_a( $user, '\WP_User' ) ) {
1045 $error_code = 'missing_user';
1046
1047 if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) {
1048 $error_message = esc_html__( 'There is no account with that username.', 'magic-login' );
1049 } else {
1050 $error_message = esc_html__( 'There is no account with that username or email address.', 'magic-login' );
1051 }
1052
1053 if ( $code_login && $phone_login ) {
1054 $error_message = esc_html__( 'Unable to process your request. Please try again.', 'magic-login' );
1055 }
1056
1057 if ( ! empty( $args['error_message'] ) ) {
1058 $error_message = $args['error_message'];
1059 }
1060
1061 /**
1062 * Filter the error message when user is missing
1063 *
1064 * @hook magic_login_missing_user_error_message
1065 * $error_message string
1066 * $error_code string
1067 * $args array
1068 * @since 2.4
1069 */
1070 $error_message = apply_filters( 'magic_login_missing_user_error_message', $error_message, $error_code, $args );
1071
1072 $errors = new WP_Error( $error_code, $error_message );
1073 $show_form = true;
1074 $code_login = false;
1075 $phone_login = false;
1076 } elseif ( null !== $send_link ) {
1077 $errors = $send_link;
1078 $show_form = false;
1079 $code_login = false;
1080 $phone_login = false;
1081 } else {
1082 global $magic_login_code_login_result, $magic_login_link;
1083 if ( ! empty( $magic_login_code_login_result ) ) {
1084 // after post request, if code login failed, show error message
1085 $errors = $magic_login_code_login_result;
1086 } else {
1087 $errors = self::send_login_link( $user, false, $code_login );
1088 }
1089 }
1090
1091 if ( ! is_wp_error( $errors ) ) {
1092 $show_form = false;
1093 $info = '<p class="message magic_login_block_login_success">' . __( 'Please check your inbox for the login link. If you did not receive a login email, check your spam folder too.', 'magic-login' ) . '</p>';
1094
1095 if ( $phone_login ) {
1096 $info = '<p class="message magic_login_block_login_success">' . __( 'Please check your phone for the login link.', 'magic-login' ) . '</p>';
1097 }
1098
1099 if ( ! empty( $args['success_message'] ) ) {
1100 $info = '<p class="message magic_login_block_login_success">' . $args['success_message'] . '</p>';
1101 }
1102
1103 if ( $code_login ) {
1104 $info = '<p class="message magic_login_block_login_success">' . __( 'Please enter the code sent to your email.', 'magic-login' ) . '</p>';
1105
1106 if ( ! empty( $args['code_success_message'] ) ) {
1107 $info = '<p class="message magic_login_block_login_success">' . $args['code_success_message'] . '</p>';
1108 }
1109 }
1110 }
1111
1112 return [
1113 'is_processed' => $is_processed,
1114 'errors' => $errors,
1115 'info' => $info,
1116 'show_form' => $show_form,
1117 'show_registration_form' => $show_registration_form,
1118 'code_login' => $code_login,
1119 'phone_login' => $phone_login,
1120 ];
1121 }
1122
1123
1124 /**
1125 * Send magic link to user
1126 *
1127 * @param object $user \WP_User object
1128 * @param mixed|string|bool $login_link use given link when it provided. @since 1.9
1129 * @param mixed|string|bool $code_login create login link with code. @since 2.4
1130 *
1131 * @return bool
1132 */
1133 public static function send_login_link( $user, $login_link = false, $code_login = false ) {
1134 global $magic_login_token;
1135
1136 if ( ! $login_link ) {
1137 $context = $code_login ? 'email_code' : 'email';
1138 $login_link = create_login_link( $user, $context );
1139 }
1140
1141 $settings = \MagicLogin\Utils\get_settings();
1142 $login_email = $settings['login_email'];
1143 $email_subject = $settings['email_subject'];
1144
1145 $placeholder_values = get_email_placeholders_by_user( $user );
1146 $placeholder_values['{{MAGIC_LINK}}'] = $login_link;
1147 $placeholder_values['{{MAGIC_LOGIN_CODE}}'] = $magic_login_token;
1148
1149 $login_email = str_replace( array_keys( $placeholder_values ), $placeholder_values, $login_email );
1150 $email_subject = str_replace( array_keys( $placeholder_values ), $placeholder_values, $email_subject );
1151
1152 $login_email = apply_filters( 'magic_login_email_content', $login_email, $placeholder_values );
1153 $email_subject = apply_filters( 'magic_login_email_subject', $email_subject, $placeholder_values );
1154
1155 $headers = apply_filters( 'magic_login_email_headers', array( 'Content-Type: text/html; charset=UTF-8' ) );
1156
1157 foreach ( (array) $headers as $header ) {
1158 if ( false !== stripos( $header, 'text/html' ) ) {
1159 // convert line breaks to br when content type is html but
1160 // input doesn't contain HTML tags (adding <br/> can ruin the templating)
1161 if ( strip_tags( $login_email, '<a>' ) === $login_email ) {
1162 $login_email = nl2br( $login_email );
1163 }
1164 break;
1165 }
1166 }
1167
1168 /**
1169 * Send the email only once at a run.
1170 * Eg: when having login block in a page, and shortcode at some other part of the page.
1171 * It will send the email twice due to the way we handle the request.
1172 */
1173 if ( did_action( 'magic_login_send_login_link' ) ) {
1174 return true;
1175 }
1176
1177 do_action( 'magic_login_send_login_link', $user );
1178
1179 return wp_mail( $user->user_email, $email_subject, $login_email, $headers );
1180 }
1181
1182 /**
1183 * Determines if the current request is a login form submission.
1184 *
1185 * @return bool
1186 * @since 2.4
1187 */
1188 private static function is_login_form_submission() {
1189 return isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_POST['log'] );
1190 }
1191
1192
1193 }
1194