PluginProbe
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password / 2.1
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password v2.1
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 / login.php

login.php in Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password 2.1, at includes/login.php

910 lines 25.2 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\Login;
9
10 use const MagicLogin\Constants\CRON_HOOK_NAME;
11 use const MagicLogin\Constants\TOKEN_USER_META;
12 use function MagicLogin\Utils\create_login_link;
13 use function MagicLogin\Utils\get_allowed_intervals;
14 use function MagicLogin\Utils\get_ttl_with_interval;
15 use function MagicLogin\Utils\get_user_default_redirect;
16 use function MagicLogin\Utils\get_user_tokens;
17 use \WP_Error as WP_Error;
18
19 // phpcs:disable WordPress.WP.I18n.MissingTranslatorsComment
20
21 /**
22 * Default setup routine
23 *
24 * @return void
25 */
26 function setup() {
27 add_action( 'login_form_magic_login', __NAMESPACE__ . '\\action_magic_login' );
28 add_action( 'login_form_login', __NAMESPACE__ . '\\maybe_redirect' );
29 add_action( 'init', __NAMESPACE__ . '\\handle_login_request' );
30 add_action( CRON_HOOK_NAME, __NAMESPACE__ . '\\cleanup_expired_tokens' );
31 add_action( 'login_footer', __NAMESPACE__ . '\\print_login_button' );
32 add_action( 'login_head', __NAMESPACE__ . '\\login_css' );
33 add_filter( 'wp_mail', __NAMESPACE__ . '\\maybe_add_auto_login_link', 999 );
34 add_action( 'wp_ajax_magic_login_ajax_request', __NAMESPACE__ . '\\ajax_request' );
35 add_action( 'wp_ajax_nopriv_magic_login_ajax_request', __NAMESPACE__ . '\\ajax_request' );
36 add_filter( 'wp_mail', __NAMESPACE__ . '\\replace_magic_link_in_wp_mail', 999 );
37 }
38
39
40 /**
41 * Process login request
42 *
43 * @param array $args Custom messages - Added in 2.1
44 * eg: [
45 * 'info_message' => 'Custom message'
46 * 'error_message' => 'Custom message'
47 * 'success_message' => 'Custom message'
48 * ]
49 *
50 * @return array
51 */
52 function process_login_request( $args = array() ) {
53 $show_form = true;
54 $errors = new WP_Error();
55 if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) {
56 $info = '<p class="message">' . __( 'Please enter your username. You will receive an email message to log in.', 'magic-login' ) . '</p>';
57 } else {
58 $info = '<p class="message">' . __( 'Please enter your username or email address. You will receive an email message to log in.', 'magic-login' ) . '</p>';
59 }
60
61 if ( ! empty( $args['info_message'] ) ) {
62 $info = '<p class="message">' . $args['info_message'] . '</p>';
63 }
64
65 $is_processed = false;
66
67 // process form request
68 if ( isset( $_SERVER['REQUEST_METHOD'] ) && 'POST' === $_SERVER['REQUEST_METHOD'] && ! empty( $_POST['log'] ) ) {
69 $user_name = sanitize_user( wp_unslash( $_POST['log'] ) );
70 $user = get_user_by( 'login', $user_name );
71
72 if ( ! defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) || false === MAGIC_LOGIN_USERNAME_ONLY ) {
73 if ( ! $user && strpos( $user_name, '@' ) ) {
74 $user = get_user_by( 'email', $user_name );
75 }
76 }
77
78 $is_processed = true;
79
80 /**
81 * Short circuit to prevent unwanted requests
82 */
83 $send_link = apply_filters( 'magic_login_pre_send_login_link', null, $user );
84
85 if ( ! is_a( $user, '\WP_User' ) ) {
86 $info = '';
87 if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) {
88 $errors = new WP_Error( 'missing_user', esc_html__( 'There is no account with that username.', 'magic-login' ) );
89 } else {
90 $errors = new WP_Error( 'missing_user', esc_html__( 'There is no account with that username or email address.', 'magic-login' ) );
91 }
92
93 if ( ! empty( $args['error_message'] ) ) {
94 $errors = new WP_Error( 'missing_user', $args['error_message'] );
95 }
96
97 $show_form = true;
98 } elseif ( null !== $send_link ) {
99 $info = '';
100 $errors = $send_link;
101 $show_form = false;
102 } else {
103 $errors = send_login_link( $user );
104 }
105
106 if ( ! is_wp_error( $errors ) ) {
107 $show_form = false;
108 $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>';
109
110 if ( ! empty( $args['success_message'] ) ) {
111 $info = '<p class="message magic_login_block_login_success">' . $args['success_message'] . '</p>';
112 }
113 }
114 }
115
116 return [
117 'show_form' => $show_form,
118 'errors' => $errors,
119 'info' => $info,
120 'is_processed' => $is_processed,
121 ];
122 }
123
124 /**
125 * Login form actions
126 */
127 function action_magic_login() {
128
129 $login_request = process_login_request();
130
131 login_header( esc_html__( 'Log in', 'magic-login' ), $login_request['info'], $login_request['errors'] );
132
133 if ( $login_request['show_form'] ) {
134 login_form();
135 }
136
137 login_footer();
138 exit;
139 }
140
141
142 /**
143 * Send magic link to user
144 *
145 * @param object $user \WP_User object
146 * @param mixed|string|bool $login_link use given link when it provided. @since 1.9
147 *
148 * @return bool
149 */
150 function send_login_link( $user, $login_link = false ) {
151 if ( is_multisite() ) {
152 $site_name = get_network()->site_name;
153 } else {
154 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
155 }
156
157 if ( ! $login_link ) {
158 $login_link = create_login_link( $user );
159 }
160
161 $settings = \MagicLogin\Utils\get_settings();
162 $login_email = $settings['login_email'];
163 $email_subject = $settings['email_subject'];
164
165 list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $settings['token_ttl'] );
166 $selected_interval_str = strtolower( $selected_interval );
167
168 $allowed_intervals = get_allowed_intervals();
169
170 if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
171 $selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
172 }
173
174 $placeholder_values = [
175 '{{SITEURL}}' => home_url(),
176 '{{USERNAME}}' => $user->user_login,
177 '{{FIRST_NAME}}' => $user->first_name,
178 '{{LAST_NAME}}' => $user->last_name,
179 '{{FULL_NAME}}' => $user->first_name . ' ' . $user->last_name,
180 '{{DISPLAY_NAME}}' => $user->display_name,
181 '{{USER_EMAIL}}' => $user->user_email,
182 '{{SITENAME}}' => $site_name,
183 '{{EXPIRES}}' => $settings['token_ttl'],
184 '{{EXPIRES_WITH_INTERVAL}}' => $token_ttl . ' ' . $selected_interval_str,
185 '{{MAGIC_LINK}}' => $login_link,
186 '{{TOKEN_VALIDITY_COUNT}}' => $settings['token_validity'],
187 ];
188
189 $login_email = str_replace( array_keys( $placeholder_values ), $placeholder_values, $login_email );
190 $email_subject = str_replace( array_keys( $placeholder_values ), $placeholder_values, $email_subject );
191
192 $login_email = apply_filters( 'magic_login_email_content', $login_email, $placeholder_values );
193 $email_subject = apply_filters( 'magic_login_email_subject', $email_subject, $placeholder_values );
194
195 $headers = apply_filters( 'magic_login_email_headers', array( 'Content-Type: text/html; charset=UTF-8' ) );
196
197 foreach ( (array) $headers as $header ) {
198 if ( false !== stripos( $header, 'text/html' ) ) {
199 // convert line breaks to br when content type is html but
200 // input doesn't contain HTML tags (adding <br/> can ruin the templating)
201 if ( strip_tags( $login_email, '<a>' ) === $login_email ) {
202 $login_email = nl2br( $login_email );
203 }
204 break;
205 }
206 }
207
208 /**
209 * Send the email only once at a run.
210 * Eg: when having login block in a page, and shortcode at some other part of the page.
211 * It will send the email twice due to the way we handle the request.
212 */
213 if ( did_action( 'magic_login_send_login_link' ) ) {
214 return true;
215 }
216
217 do_action( 'magic_login_send_login_link', $user );
218
219 return wp_mail( $user->user_email, $email_subject, $login_email, $headers );
220 }
221
222
223 /**
224 * login form
225 */
226 function login_form() {
227 $user_login = '';
228
229 if ( isset( $_POST['log'] ) && is_string( $_POST['log'] ) ) {
230 $user_login = wp_unslash( $_POST['log'] ); // phpcs:ignore
231 }
232 ?>
233 <form name="magicloginform" id="magicloginform" action="<?php echo esc_url( site_url( 'wp-login.php?action=magic_login', 'login_post' ) ); ?>" method="post" autocomplete="off">
234 <p>
235 <?php if ( defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) && MAGIC_LOGIN_USERNAME_ONLY ) : ?>
236 <label for="user_login"><?php esc_html_e( 'Username', 'magic-login' ); ?></label>
237 <?php else : ?>
238 <label for="user_login"><?php esc_html_e( 'Username or Email Address', 'magic-login' ); ?></label>
239 <?php endif; ?>
240 <input type="text" name="log" id="user_login" class="input" value="<?php echo esc_attr( $user_login ); ?>" size="20" autocapitalize="off" autocomplete="username" required />
241 </p>
242 <?php
243
244 /**
245 * Fires following the 'email' field in the login form.
246 *
247 * @since 1.0
248 */
249 do_action( 'magic_login_form' );
250
251 ?>
252 <p class="submit">
253 <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' ); ?>" />
254 <?php if ( isset( $_GET['redirect_to'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
255 <input type="hidden" name="redirect_to" value="<?php echo esc_url( $_GET['redirect_to'] ); // phpcs:ignore ?>">
256 <?php endif; ?>
257
258 <input type="hidden" name="testcookie" value="1" />
259 </p>
260 </form>
261 <?php
262 }
263
264 /**
265 * Redirect to magic login page once it used as default login method
266 */
267 function maybe_redirect() {
268 global $pagenow;
269
270 if ( 'wp-login.php' !== $pagenow ) {
271 return;
272 }
273
274 if ( ! empty( ( $_POST ) ) ) {
275 return;
276 }
277
278 if ( isset( $_REQUEST['interim-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
279 return;
280 }
281
282 /**
283 * short-circuit if want to interrupt redirect
284 */
285 if ( null !== apply_filters( 'magic_login_before_login_form_redirect', null ) ) {
286 return;
287 }
288
289 $settings = \MagicLogin\Utils\get_settings();
290
291 if ( true === $settings['is_default'] ) {
292 wp_safe_redirect( esc_url_raw( add_query_arg( 'action', 'magic_login' ) ) );
293 exit;
294 }
295 }
296
297 /**
298 * Handle login request
299 */
300 function handle_login_request() {
301 global $pagenow;
302
303 /**
304 * Since 1.2.2 $pagenow control has been deprecated
305 * in favor compatibility with 3rd party plugins
306 */
307 if ( 'wp-login.php' !== $pagenow && empty( $_GET['magic-login'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
308 return;
309 }
310
311 if ( empty( $_GET['user_id'] ) || empty( $_GET['token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
312 return;
313 }
314
315 do_action( 'magic_login_handle_login_request' );
316
317 if ( is_user_logged_in() ) {
318 /* translators: 1: User login 2: Dashboard URL */
319 $error = sprintf( __( 'Invalid magic login token, but you are logged in as \'%1$s\'. <a href="%2$s">Go to the dashboard instead</a>?', 'magic-login' ), wp_get_current_user()->user_login, admin_url() );
320 } else {
321 /* translators: %s: Login URL */
322 $error = sprintf( __( 'Invalid magic login token. <a href="%s">Try signing in instead</a>?', 'magic-login' ), wp_login_url() );
323 }
324
325 // Use a generic error message to ensure user ids can't be sniffed
326 $user = get_user_by( 'id', (int) $_GET['user_id'] ); //phpcs:ignore WordPress.Security.NonceVerification.Recommended
327 if ( ! $user ) {
328 do_action( 'magic_login_invalid_user' );
329 wp_die( $error ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
330 }
331
332 $settings = \MagicLogin\Utils\get_settings();
333 $token_validity = $settings['token_validity'];
334
335 $tokens = get_user_tokens( $user->ID, true );
336 $is_valid = false;
337 $current_token = null;
338 foreach ( $tokens as $i => $token_data ) {
339 if ( empty( $token_data ) || ! is_array( $token_data ) || ! isset( $token_data['token'] ) ) {
340 unset( $tokens[ $i ] );
341 continue;
342 }
343
344 if ( hash_equals( $token_data['token'], hash_hmac( 'sha256', $_GET['token'], wp_salt() ) ) ) { // phpcs:ignore
345 $is_valid = true;
346 $current_token = $token_data;
347 $token_usage_count = isset( $token_data['usage_count'] ) ? absint( $token_data['usage_count'] ) + 1 : 1;
348
349 $tokens[ $i ]['usage_count'] = $token_usage_count;
350 if ( 0 !== $token_validity && $token_validity <= $token_usage_count ) {
351 unset( $tokens[ $i ] );
352 }
353
354 break;
355 }
356 }
357
358 if ( ! $is_valid ) {
359 do_action( 'magic_login_invalid_token' );
360
361 /**
362 * Invalid token error message.
363 * Since 1.2
364 */
365 $error_message = apply_filters( 'magic_login_invalid_token_error_message', $error );
366 wp_die( wp_kses_post( $error_message ) );
367 }
368
369 /**
370 * Fires before setting up auth cookie
371 *
372 * @since 1.0
373 */
374 do_action( 'magic_login_before_login', $user, $current_token );
375
376 update_user_meta( $user->ID, TOKEN_USER_META, $tokens );
377 wp_set_auth_cookie( $user->ID, true, is_ssl() );
378
379 /**
380 * Fires after setting up auth cookie
381 *
382 * @since 1.0
383 * @since 1.5.1 array $current_token added
384 */
385 do_action( 'magic_login_logged_in', $user, $current_token );
386
387 /**
388 * Some plugins integrated with core's wp_login hook.
389 * So fire it here too.
390 *
391 * @since 1.3
392 */
393 do_action( 'wp_login', $user->user_login, $user );
394
395 $default_redirect = get_user_default_redirect( $user );
396 $login_redirect = apply_filters( 'magic_login_redirect', $default_redirect, $user );
397 wp_safe_redirect( $login_redirect );
398 exit;
399 }
400
401 /**
402 * Handle cleanup process for expired tokens
403 *
404 * @param int $user_id user id
405 */
406 function cleanup_expired_tokens( $user_id ) {
407 $settings = \MagicLogin\Utils\get_settings();
408 $ttl = absint( $settings['token_ttl'] );
409 $tokens = get_user_meta( $user_id, TOKEN_USER_META, true );
410 $tokens = is_string( $tokens ) ? array( $tokens ) : $tokens;
411 $live_tokens = array();
412
413 foreach ( $tokens as $token ) {
414 if ( empty( $token ) || ! isset( $token['time'] ) ) {
415 continue;
416 }
417
418 // not expired yet
419 if ( absint( $token['time'] ) + ( $ttl * MINUTE_IN_SECONDS ) > time() ) {
420 $live_tokens[] = $token;
421 }
422 }
423
424 update_user_meta( $user_id, TOKEN_USER_META, $live_tokens );
425 }
426
427 /**
428 * Add login button to wp-login.php
429 */
430 function print_login_button() {
431 $settings = \MagicLogin\Utils\get_settings();
432
433 if ( ! $settings['add_login_button'] ) {
434 return;
435 }
436
437 $login_url = site_url( 'wp-login.php?action=magic_login', 'login_post' );
438
439 if ( isset( $_GET['redirect_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
440 $login_url = esc_url( add_query_arg( 'redirect_to', $_GET['redirect_to'], $login_url ) ); // phpcs:ignore
441 }
442
443 ?>
444 <script type="text/javascript">
445 (function () {
446 let loginForm = document.getElementById('loginform');
447
448 if( loginForm ){
449 loginForm.insertAdjacentHTML(
450 'beforeend',
451 '<div class="magic-login-normal-login">' +
452 '<button type="submit" name="wp-submit" id="wp-login-submit" class="button button-primary button-hero" value="<?php echo esc_attr( __( 'Log In', 'magic-login' ) ); // phpcs:ignore ?>"><?php echo esc_attr( __( 'Log In', 'magic-login' ) ); ?></button>'+
453 '</div>'+
454 '<span class="magic-login-or-separator"></span>' +
455 '<div id="continue-with-magic-login" class="continue-with-magic-login">' +
456 '<button type="button" value="<?php echo esc_url( $login_url ); ?>" class="button button-primary button-hero" id="magic-login-button">' +
457 '<?php esc_html_e( 'Send me the login link', 'magic-login' ); ?>' +
458 '</button>'+
459 '</a>' +
460 '</div>'
461 );
462
463 document.getElementById('magic-login-button').onclick = function () {
464 let loginInput = document.getElementById('user_login');
465 if ( loginInput != null && loginInput.value.length > 0 ) {
466 let frm = document.getElementById('loginform') || null;
467 if ( frm ) {
468 frm.action = "<?php echo esc_url( $login_url ); ?>";
469 frm.submit();
470 }
471 }else{
472 location.href = "<?php echo esc_url( $login_url ); ?>";
473 }
474 }
475 }
476 })();
477 </script>
478 <?php
479
480 }
481
482 /**
483 * Add small tweaks to login form
484 */
485 function login_css() {
486 $settings = \MagicLogin\Utils\get_settings();
487
488 if ( ! $settings['add_login_button'] ) {
489 return;
490 }
491
492 ?>
493 <style>
494 form[name="validate_2fa_form"] .submit {
495 display: none;
496 }
497
498 .two-factor-email-resend input[type="submit"] {
499 width: 100%;
500 margin: auto;
501 display: block;
502 text-align: center;
503 padding: 0 36px;
504 min-height: 46px;
505 }
506
507
508 #loginform #wp-submit {
509 display: none;
510 }
511
512 .magic-login-normal-login{
513 width: 100%;
514 margin: auto;
515 padding-top:10px;
516 display: block;
517 text-align: center;
518 clear:both;
519 }
520
521 .magic-login-normal-login .button,
522 #magic-login-button {
523 width: 100%;
524 float: none!important;
525 }
526
527 #magic-login-button {
528 padding: unset !important;
529 }
530
531 .continue-with-magic-login {
532 width: 100%;
533 margin: auto;
534 display: block;
535 text-align: center;
536 }
537
538 .continue-with-magic-login .button {
539 float: none;
540 }
541
542 .magic-login-or-separator {
543 display: block;
544 text-align: center;
545 position: relative;
546 margin: 10px auto;
547 width: 100%;
548 }
549
550 .magic-login-or-separator:before {
551 content: "<?php esc_html_e( 'or', 'magic-login' ); ?>";
552 background-color: #fff;
553 font-size: 13px;
554 color: #9b9b9b;
555 display: inline-block;
556 width: 62px;
557 position: relative;
558 z-index: 1;
559 }
560
561 .magic-login-or-separator:after {
562 content: "";
563 width: 100%;
564 position: absolute;
565 left: 0;
566 top: 50%;
567 height: 1px;
568 margin-top: -0.5px;
569 background-color: #d8d8d8;
570 }
571
572 </style>
573 <?php
574 }
575
576 /**
577 * Maybe add login link to outgoing email
578 *
579 * @param array $atts wp_mail args
580 *
581 * @return mixed
582 * @since 1.6
583 */
584 function maybe_add_auto_login_link( $atts ) {
585 $settings = \MagicLogin\Utils\get_settings();
586
587 if ( ! $settings['auto_login_links'] ) {
588 return $atts;
589 }
590
591 $to = $atts['to'];
592
593 if ( ! has_single_recipient( $atts ) ) {
594 return $atts;
595 }
596
597 $user = get_user_by( 'email', $to );
598
599 if ( ! $user ) {
600 return $atts;
601 }
602
603 if ( is_auto_login_link_excluded_mail( $atts ) ) {
604 return $atts;
605 }
606
607 /**
608 * Filter auto login link
609 *
610 * @param bool $status false to exclude, default true
611 * @param array $atts wp_mail args
612 * @param \WP_User $user user object
613 *
614 * @since 1.6.0
615 */
616 $add_login_link = apply_filters( 'magic_login_add_auto_login_link', true, $atts, $user );
617
618 if ( ! $add_login_link ) {
619 return $atts;
620 }
621
622 $atts['message'] = add_auto_login_link_to_message( $atts, $user );
623
624 return $atts;
625 }
626
627 /**
628 * Add auto login link to message
629 *
630 * @param array $args wp mail content
631 * @param \WP_User $user User Object
632 *
633 * @return string
634 * @since 1.6
635 */
636 function add_auto_login_link_to_message( $args, $user ) {
637 $settings = \MagicLogin\Utils\get_settings();
638 list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $settings['token_ttl'] );
639 $selected_interval_str = strtolower( $selected_interval );
640 $allowed_intervals = get_allowed_intervals();
641 if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
642 $selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
643 }
644
645 $message = $args['message'];
646 $is_html = ! empty( $args['headers'] ) && false !== strpos( implode( '|', (array) $args['headers'] ), 'text/html' );
647
648 $link = create_login_link( $user );
649
650 if ( $is_html ) {
651 $login_message = '<br>';
652 /* translators: %s: The magic login link */
653 $login_message .= sprintf( __( '<a href="%s" target="_blank" rel="noopener">Click here to login</a>.', 'magic-login' ), $link );
654 } else {
655 $login_message = PHP_EOL;
656 /* translators: %s: The magic login link */
657 $login_message .= sprintf( __( 'Auto Login: %s', 'magic-login' ), $link );
658 }
659
660 if ( $token_ttl > 0 ) {
661 $login_message .= $is_html ? '<br>' : PHP_EOL;
662
663 /* translators: 1: TTL value (number) 2: Unit (minute(s), hour(s), days(s)) */ // phpcs:ignore Squiz.PHP.CommentedOutCode.Found
664 $login_message .= sprintf( __( 'Login link will expire in %1$s %2$s.', 'magic-login' ), $token_ttl, $selected_interval_str );
665 }
666
667 /**
668 * Filter login message
669 *
670 * @param string $login_message Appended message for the login
671 * @param string $link Login URL
672 * @param \WP_User $user User Object
673 *
674 * @since 1.6
675 */
676 $login_message = apply_filters( 'magic_login_auto_login_link_message', $login_message, $link, $user );
677
678 $email_message = $message . $login_message;
679
680 /**
681 * Filter email message
682 *
683 * @param string $email_message Email message
684 * @param string $message Email content before appending login link
685 * @param string $login_message Login message
686 * @param array $args WP Mail args
687 * @param string $link Login URL
688 * @param \WP_User $user User Object
689 *
690 * @since 1.6
691 */
692 return apply_filters( 'magic_login_auto_login_link_email_message', $email_message, $message, $login_message, $args, $link, $user );
693 }
694
695 /**
696 * Check if auto login link is excluded for given mail
697 *
698 * @param array $args wp mail args
699 *
700 * @return bool
701 * @since 1.6
702 */
703 function is_auto_login_link_excluded_mail( $args ) {
704 $is_excluded = false;
705
706 /**
707 * Exclude some of the emails
708 * Copy emails as is, for covering in translated versions
709 *
710 * @link https://github.com/johnbillion/wp_mail
711 */
712 $excluded_subjects = apply_filters(
713 'magic_login_auto_login_excluded_subjects',
714 [
715 __( '[%s] New Admin Email Address' ),
716 __( '[%s] Network Admin Email Change Request' ),
717 __( '[%s] Admin Email Changed' ),
718 __( '[%s] Notice of Network Admin Email Change' ),
719 __( '[%s] Login Details' ),
720 __( '[%s] Password Reset' ),
721 __( '[%s] Password Changed' ),
722 __( '[%s] Email Change Request' ),
723 ]
724 );
725
726 // remove [%s] from subjects
727 $normalize_email_title = preg_replace( '#\[.*?\]#s', ' ', $args['subject'] ); // remove placeholders
728 foreach ( $excluded_subjects as $subject ) {
729 $subject = preg_replace( '#\[.*?\]#s', ' ', $subject ); // remove placeholders
730 if ( false !== strpos( $normalize_email_title, $subject ) ) {
731 $is_excluded = true;
732 break;
733 }
734 }
735
736 // no need to add for login email itself
737 if ( did_action( 'magic_login_send_login_link' ) ) {
738 $is_excluded = true;
739 }
740
741 /**
742 * Filter if auto login link is excluded for given mail
743 *
744 * @param bool $is_excluded whether the email is excluded or not
745 * @param array $args wp_mail args
746 *
747 * @since 1.6
748 */
749 return (bool) apply_filters( 'magic_login_auto_login_link_excluded', $is_excluded, $args );
750 }
751
752 /**
753 * Ajax callback for login requests
754 *
755 * @return void
756 */
757 function ajax_request() {
758
759 if ( ! isset( $_POST['data'] ) ) {
760 wp_send_json_error(
761 [
762 'message' => esc_html__( 'Invalid request', 'magic-login' ),
763 'show_form' => true,
764 ]
765 );
766 }
767
768 parse_str( wp_unslash( $_POST['data'] ), $form_data ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
769
770 if ( $form_data['log'] ) {
771 $_POST['log'] = $form_data['log'];
772 }
773
774 if ( $form_data['redirect_to'] ) {
775 $_POST['redirect_to'] = $form_data['redirect_to'];
776 }
777
778 $args = []; // pass custom messages to backend
779 if ( ! empty( $form_data['messages'] ) ) {
780 foreach ( $form_data['messages'] as $key => $value ) {
781 $args[ $key . '_message' ] = wp_kses_post( $value );
782 }
783 }
784
785 $login_request = process_login_request( $args );
786
787 if ( ! empty( $login_request['info'] ) ) {
788 wp_send_json_success(
789 [
790 'message' => $login_request['info'],
791 'show_form' => $login_request['show_form'],
792 ]
793 );
794 }
795
796 $error_messages = '';
797 $login_errors = $login_request['errors'];
798 // error messages
799 if ( ! empty( $login_errors ) && is_wp_error( $login_errors ) && $login_errors->has_errors() ) {
800 foreach ( $login_errors->get_error_codes() as $code ) {
801 foreach ( $login_errors->get_error_messages( $code ) as $message ) {
802 $error_messages .= $message . "<br />\n";
803 }
804 }
805 }
806
807 $error_message = sprintf( '<div id="login_error" class="magic_login_block_login_error">%s</div>', wp_kses_post( $error_messages ) );
808
809 wp_send_json_error(
810 [
811 'message' => $error_message,
812 'show_form' => $login_request['show_form'],
813 ]
814 );
815 }
816
817 /**
818 * Replace {{MAGIC_LINK}} placeholder with login link for all outgoing emails
819 *
820 * @param array $atts wp_mail args
821 * @since 2.0.0
822 * @return mixed
823 */
824 function replace_magic_link_in_wp_mail( $atts ) {
825 if ( false === strpos( $atts['message'], '{{MAGIC_LINK}}' ) ) {
826 return $atts;
827 }
828
829 $magic_link = '';
830
831 if ( has_single_recipient( $atts ) ) {
832 $user = get_user_by( 'email', $atts['to'] );
833 if ( $user ) {
834
835 /**
836 * Filter magic_login_replace_magic_link_in_wp_mail
837 *
838 * @param bool $status false to exclude, default true
839 * @param array $atts wp_mail args
840 * @param \WP_User $user user object
841 *
842 * @since 2.0.0
843 */
844 $replace_magic_link = apply_filters( 'magic_login_replace_magic_link_in_wp_mail', true, $atts, $user );
845 if ( $replace_magic_link ) {
846 $magic_link = create_login_link( $user );
847 }
848 }
849 }
850
851 /**
852 * Filter magic login replace
853 *
854 * @param string $magic_link login link
855 * @param array $atts wp_mail args
856 *
857 * @since 2.0.0
858 */
859 $magic_link = apply_filters( 'magic_login_replace_magic_link_in_wp_mail_message', $magic_link, $atts );
860 $atts['message'] = str_replace( '{{MAGIC_LINK}}', $magic_link, $atts['message'] );
861
862 return $atts;
863 }
864
865 /**
866 * Check if email has single recipient
867 *
868 * @param array $atts wp_mail args
869 *
870 * @return bool
871 * @since 2.0.0
872 */
873 function has_single_recipient( $atts ) {
874 $to = $atts['to'];
875
876 if ( empty( $to ) ) {
877 return false;
878 }
879
880 if ( is_array( $to ) && 1 !== count( $to ) ) {
881 return false;
882 }
883
884 $to = is_array( $to ) ? array_shift( $to ) : $to;
885
886 if ( is_string( $to ) && false !== strpos( $to, ',' ) ) {
887 return false;
888 }
889
890 /**
891 * Check bcc/cc
892 * Login links are personal, so we don't want to send them to other people.
893 */
894 if ( ! empty( $atts['headers'] ) ) {
895 $headers = $atts['headers'];
896
897 if ( is_string( $headers ) ) {
898 $headers = [ $headers ];
899 }
900
901 foreach ( $headers as $header ) {
902 if ( 1 === preg_match( '/(bcc|cc):/i', $header ) ) {
903 return false;
904 }
905 }
906 }
907
908 return true;
909 }
910