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 / utils.php

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

699 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Common utilities and functions
4 *
5 * @package MagicLogin
6 */
7
8 namespace MagicLogin\Utils;
9
10 use MagicLogin\Encryption;
11 use const MagicLogin\Constants\CRON_HOOK_NAME;
12 use const MagicLogin\Constants\SETTING_OPTION;
13 use const MagicLogin\Constants\TOKEN_USER_META;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Create token
21 *
22 * @param object $user \WP_User object
23 * @param string $context Context (email|email_code|sms|sms_code) @since 2.4
24 *
25 * @return string
26 */
27 function create_user_token( $user, $context = 'email' ) {
28 $settings = get_settings(); // phpcs:ignore
29 $tokens = get_user_meta( $user->ID, TOKEN_USER_META, true );
30 $tokens = is_string( $tokens ) ? array( $tokens ) : $tokens;
31 $new_token = sha1( wp_generate_password() );
32
33 switch ( $context ) {
34 case 'sms':
35 // helps to keep url link short due to 300 char limit for most of the SMS providers
36 $new_token = substr( $new_token, 0, 12 );
37 break;
38 case 'sms_code':
39 // 6-digit PIN for SMS
40 $new_token = wp_rand( 100000, 999999 );
41 break;
42 case 'email_code':
43 $new_token = strtoupper( substr( str_shuffle( 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' ), 0, 10 ) );
44 break;
45 }
46
47 /**
48 * Filter the token
49 *
50 * @hook magic_login_create_user_token
51 *
52 * @param {string} $new_token New token
53 * @param {int} $user->ID User ID
54 * @param {string} $context Context
55 *
56 * @return {string} New value
57 * @since 2.4
58 */
59 $new_token = apply_filters( 'magic_login_create_user_token', $new_token, $user->ID, $context );
60
61 $hashed_token = hash_hmac( 'sha256', $new_token, wp_salt() );
62
63 $ip = sha1( get_client_ip() );
64 if ( defined( 'WP_CLI' ) && WP_CLI ) {
65 $ip = 'cli';
66 }
67
68 $tokens[] = [
69 'token' => $hashed_token,
70 'time' => time(),
71 'ip_hash' => $ip,
72 ];
73
74 update_user_meta( $user->ID, TOKEN_USER_META, $tokens );
75
76 if ( absint( $settings['token_ttl'] ) > 0 ) { // eternal token
77 wp_schedule_single_event( time() + ( $settings['token_ttl'] * MINUTE_IN_SECONDS ), CRON_HOOK_NAME, array( $user->ID ) );
78 }
79
80 return $new_token;
81 }
82
83
84 /**
85 * Create login link for given user
86 *
87 * @param object $user WP_User object
88 * @param string $context Context (email|email_code|sms|sms_code) @since 2.4
89 * @param string $redirect_to Redirect URL
90 *
91 * @return mixed|string
92 */
93 function create_login_link( $user, $context = 'email', $redirect_to = null ) {
94 global $magic_login_token;
95 $token = create_user_token( $user, $context );
96 $magic_login_token = $token;
97
98 $query_args = array(
99 'user_id' => $user->ID,
100 'token' => $token,
101 'magic-login' => 1,
102 );
103
104 if ( ! empty( $_POST['redirect_to'] ) ) {
105 $query_args['redirect_to'] = urlencode( wp_unslash( $_POST['redirect_to'] ) ); // phpcs:ignore
106 }
107
108 if ( ! empty( $redirect_to ) ) {
109 $query_args['redirect_to'] = urlencode( $redirect_to );
110 }
111
112 $login_url = esc_url_raw( add_query_arg( $query_args, wp_login_url() ) );
113
114 /**
115 * Filter the login URL
116 *
117 * @hook magic_login_create_login_link
118 *
119 * @param {string} $login_url Login URL
120 * @param {object} $user WP_User object
121 * @param {string} $context Context
122 * @param {string} $redirect_to Redirect URL
123 *
124 * @return {string} New value
125 * @since 2.4
126 */
127 $login_url = apply_filters( 'magic_login_create_login_link', $login_url, $user, $context, $redirect_to );
128
129 return $login_url;
130 }
131
132 /**
133 * Get client raw ip
134 * this should be hashed
135 *
136 * @return mixed
137 */
138 function get_client_ip() {
139 /**
140 * `HTTP_X_FORWARDED_FOR` removed in 1.5
141 * Filters the ip address
142 *
143 * @hook magic_login_client_ip
144 *
145 * @param {string} REMOTE_ADDR
146 *
147 * @return {string} New value.
148 * @since 1.5
149 */
150 return apply_filters( 'magic_login_client_ip', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore
151 }
152
153 /**
154 * Get settings with defaults
155 *
156 * @return array
157 * @since 1.0
158 */
159 function get_settings() {
160 $defaults = [
161 'is_default' => false,
162 'add_login_button' => true,
163 'token_ttl' => 5,
164 'token_validity' => 1,
165 'token_interval' => 'MINUTE',
166 'enable_brute_force_protection' => false,
167 'brute_force_bantime' => 60, // in minutes
168 'brute_force_login_attempt' => 10,
169 'brute_force_login_time' => 5, // in minutes
170 'enable_login_throttling' => false,
171 'login_throttling_limit' => 10,
172 'login_throttling_time' => 15, // in minutes
173 'enable_ip_check' => false,
174 'enable_domain_restriction' => false,
175 'allowed_domains' => '',
176 'login_email' => get_default_login_email_text(),
177 'enable_login_redirection' => false,
178 'default_redirection_url' => '',
179 'enforce_redirection_rules' => true,
180 'enable_wp_login_redirection' => false,
181 'enable_role_based_redirection' => false,
182 'role_based_redirection_rules' => [],
183 'email_subject' => __( 'Log in to {{SITENAME}}', 'magic-login' ),
184 'auto_login_links' => false,
185 'enable_ajax' => false,
186 'enable_woo_integration' => false,
187 'woo_position' => 'before',
188 'enable_woo_customer_login' => false,
189 'woo_customer_login_position' => 'before',
190 'enable_edd_checkout' => false,
191 'edd_checkout_position' => 'edd_before_purchase_form',
192 'enable_edd_login' => false,
193 'edd_login_position' => 'before',
194 'registration' => [
195 'enable' => false,
196 'mode' => 'auto', // or standard|shortcode
197 'fallback_email_field' => true, // show email field on auto registration mode as a fallback
198 'show_name_field' => true,
199 'require_name_field' => true,
200 'show_terms_field' => false,
201 'require_terms_field' => false,
202 'terms' => '',
203 'send_email' => true,
204 'email_subject' => esc_html__( 'Welcome to {{SITENAME}}', 'magic-login' ),
205 'email_content' => get_default_registration_email_text(),
206 'enable_domain_restriction' => false,
207 'allowed_domains' => '',
208 'role' => '',
209 ],
210 'spam_protection' => [
211 'service' => 'recaptcha',
212 'enable_login' => false,
213 'enable_registration' => false,
214 ],
215 'recaptcha' => [
216 'type' => 'v3', // which version to use
217 'v2_checkbox' => [
218 'site_key' => '',
219 'secret_key' => '',
220 ],
221 'v2_invisible' => [
222 'site_key' => '',
223 'secret_key' => '',
224 ],
225 'v3' => [
226 'site_key' => '',
227 'secret_key' => '',
228 ],
229 ],
230 'cf_turnstile' => [
231 'site_key' => '',
232 'secret_key' => '',
233 ],
234 'enable_rest_api' => false,
235 'sms' => [
236 'enable' => false,
237 'provider' => 'twilio',
238 'twilio' => [
239 'account_sid' => '',
240 'auth_token' => '',
241 'from' => '',
242 ],
243 /**
244 * phone_only: Only send SMS if the user enters their phone number (instead of email) when logging in.
245 * sms_and_email: If the user has a phone number linked to their account, send both SMS and email.
246 * sms_or_email: Always send SMS if the user has a phone number, but do not send email notifications.
247 */
248 'sms_sending_strategy' => 'phone_only',
249 'wp_registration' => false,
250 'wp_require_phone' => false,
251 'magic_registration' => false,
252 'magic_registration_require_phone' => false,
253 'login_message' => esc_html__( 'Your login code is here: {{MAGIC_LOGIN_CODE}} Your code will expire in {{EXPIRES_WITH_INTERVAL}}', 'magic-login' ),
254 'send_registration_message' => false,
255 'registration_message' => esc_html__( 'Welcome {{FULL_NAME}}! 🎉 Your account on {{SITENAME}} has been created. You can login here: {{MAGIC_LINK}}.', 'magic-login' ),
256 ],
257 ];
258
259 if ( MAGIC_LOGIN_IS_NETWORK ) {
260 $settings = get_site_option( SETTING_OPTION, [] );
261 } else {
262 $settings = get_option( SETTING_OPTION, [] );
263 }
264
265 // Merge settings with defaults, ensuring new additions and nested arrays are included
266 $settings = array_replace_recursive( $defaults, $settings );
267
268 return $settings;
269 }
270
271 /**
272 * Default login email message
273 *
274 * @return mixed|string|void
275 */
276 function get_default_login_email_text() {
277 /* translators: Do not translate USERNAME, SITENAME,EXPIRES, MAGIC_LINK, SITENAME, SITEUR, EXPIRES_WITH_INTERVAL: those are placeholders. */
278 $email_text = __(
279 'Hi {{USERNAME}},
280
281 Click and confirm that you want to log in to {{SITENAME}}. This link will expire in {{EXPIRES_WITH_INTERVAL}} and can only be used once:
282
283 <a href="{{MAGIC_LINK}}" target="_blank" rel="noreferrer noopener">Log In</a>
284
285 Need the link? {{MAGIC_LINK}}
286
287
288 You can safely ignore and delete this email if you do not want to log in.
289
290 Regards,
291 All at {{SITENAME}}
292 {{SITEURL}}',
293 'magic-login'
294 );
295
296 return $email_text;
297 }
298
299 /**
300 * Is plugin activated network wide?
301 *
302 * @param string $plugin_file file path
303 *
304 * @return bool
305 * @since 1.0
306 */
307 function is_network_wide( $plugin_file ) {
308 if ( ! is_multisite() ) {
309 return false;
310 }
311
312 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
313 require_once ABSPATH . '/wp-admin/includes/plugin.php';
314 }
315
316 return is_plugin_active_for_network( plugin_basename( $plugin_file ) );
317 }
318
319
320 /**
321 * Get login link
322 *
323 * @return mixed|string
324 */
325 function get_magic_login_url() {
326 _deprecated_function( __FUNCTION__, '2.3.4', __NAMESPACE__ . '\get_wp_login_url' );
327
328 return get_wp_login_url();
329 }
330
331 /**
332 * Get login link
333 *
334 * @return mixed|string
335 */
336 function get_wp_login_url() {
337 $url = site_url( 'wp-login.php?action=magic_login', 'login_post' );
338 /**
339 * Filter the login URL for magic login
340 *
341 * @hook magic_login_get_wp_login_url
342 * @since 2.3.4
343 */
344 $url = apply_filters( 'magic_login_get_wp_login_url', $url );
345
346 return esc_url_raw( $url );
347 }
348
349 /**
350 * Get user tokens
351 *
352 * @param int $user_id User ID
353 * @param bool $clear_expired flag for clean-up expired tokens
354 *
355 * @return array|mixed
356 */
357 function get_user_tokens( $user_id, $clear_expired = false ) {
358 $tokens = get_user_meta( $user_id, TOKEN_USER_META, true );
359 $tokens = is_array( $tokens ) ? $tokens : [];
360
361 /**
362 * Filter user tokens
363 *
364 * @hook magic_login_user_tokens
365 *
366 * @param {array} $tokens User tokens.
367 * @param {int} $user_id User ID.
368 * @param {boolean} $clear_expired Whether to clear expired tokens or not.
369 *
370 * @return {array} New value
371 * @since 2.1
372 */
373 $tokens = (array) apply_filters( 'magic_login_user_tokens', $tokens, $user_id, $clear_expired );
374
375 if ( $clear_expired ) {
376 $ttl = get_ttl_by_user( $user_id );
377
378 if ( 0 === $ttl ) { // means token lives forever till used
379 return $tokens;
380 }
381
382 foreach ( $tokens as $index => $token_data ) {
383 if ( empty( $token_data ) || ! isset( $token_data['time'] ) ) {
384 unset( $tokens[ $index ] );
385 continue;
386 }
387
388 if ( time() > absint( $token_data['time'] ) + ( $ttl * MINUTE_IN_SECONDS ) ) {
389 unset( $tokens[ $index ] );
390 }
391 }
392 update_user_meta( $user_id, TOKEN_USER_META, $tokens );
393 }
394
395 return $tokens;
396 }
397
398 /**
399 * Get default redirect url for given user
400 *
401 * @param \WP_User $user User object
402 *
403 * @return string|void
404 */
405 function get_user_default_redirect( $user ) {
406 if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) {
407 $redirect_to = user_admin_url();
408 } elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) {
409 $redirect_to = get_dashboard_url( $user->ID );
410 } elseif ( ! $user->has_cap( 'edit_posts' ) ) {
411 $redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url();
412 } else {
413 $redirect_to = admin_url();
414 }
415
416 return $redirect_to;
417 }
418
419 /**
420 * Delete all token meta
421 */
422 function delete_all_tokens() {
423 global $wpdb;
424
425 return $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
426 $wpdb->usermeta,
427 [
428 'meta_key' => TOKEN_USER_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
429 ]
430 );
431 }
432
433
434 /**
435 * Allowed intervals for TTL.
436 *
437 * @return array
438 * @since 1.2
439 */
440 function get_allowed_intervals() {
441 return [
442 'MINUTE' => esc_html__( 'Minute(s)', 'magic-login' ),
443 'HOUR' => esc_html__( 'Hour(s)', 'magic-login' ),
444 'DAY' => esc_html__( 'Day(s)', 'magic-login' ),
445 ];
446 }
447
448 /**
449 * Convert minutes to possible time format
450 *
451 * @param int $timeout_in_minutes TTL in minutes
452 *
453 * @return array
454 * @since 1.2
455 */
456 function get_ttl_with_interval( $timeout_in_minutes ) {
457 $ttl = $timeout_in_minutes;
458 $interval = 'MINUTE';
459
460 if ( $ttl > 0 ) {
461 if ( 0 === (int) ( $ttl % 1440 ) ) {
462 $ttl = $ttl / 1440;
463 $interval = 'DAY';
464 } elseif ( 0 === (int) ( $ttl % 60 ) ) {
465 $ttl = $ttl / 60;
466 $interval = 'HOUR';
467 }
468 }
469
470 return array(
471 $ttl,
472 $interval,
473 );
474 }
475
476
477 /**
478 * Get the documentation url
479 *
480 * @param string $path The path of documentation
481 * @param string $fragment URL Fragment
482 *
483 * @return string final URL
484 */
485 function get_doc_url( $path = null, $fragment = '' ) {
486 $doc_base = 'https://handyplugins.co/';
487 $utm_parameters = '?utm_source=wp_admin&utm_medium=plugin&utm_campaign=settings_page';
488
489 if ( ! empty( $path ) ) {
490 $doc_base .= ltrim( $path, '/' );
491 }
492
493 $doc_url = trailingslashit( $doc_base ) . $utm_parameters;
494
495 if ( ! empty( $fragment ) ) {
496 $doc_url .= '#' . $fragment;
497 }
498
499 return $doc_url;
500 }
501
502 /**
503 * Check whether current screen is magic login settings page or not
504 *
505 * @return bool
506 * @since 1.2.1
507 */
508 function is_magic_login_settings_screen() {
509 $current_screen = get_current_screen();
510
511 if ( ! is_a( $current_screen, '\WP_Screen' ) ) {
512 return false;
513 }
514
515 if ( false !== strpos( $current_screen->base, 'magic-login' ) ) {
516 return true;
517 }
518
519 return false;
520 }
521
522 /**
523 * Mask given string
524 *
525 * @param string $input_string String
526 * @param int $unmask_length The length of unmask
527 *
528 * @return string
529 * @since 2.2
530 */
531 function mask_string( $input_string, $unmask_length ) {
532 $output_string = substr( $input_string, 0, $unmask_length );
533
534 if ( strlen( $input_string ) > $unmask_length ) {
535 $output_string .= str_repeat( '*', strlen( $input_string ) - $unmask_length );
536 }
537
538 return $output_string;
539 }
540
541
542 /**
543 * Check if the given value is masked
544 *
545 * @param string $value The value to check
546 * @param int $mask_length The length of the mask
547 *
548 * @return bool
549 * @since 2.2
550 */
551 function is_masked_value( $value, $mask_length = 3 ) {
552 // Get the last characters of the string
553 $last_chars = substr( $value, - $mask_length );
554
555 // Check if the last characters are asterisks
556 return str_repeat( '*', $mask_length ) === $last_chars;
557 }
558
559 /**
560 * Get email placeholders by user
561 *
562 * @param \WP_User $user User object
563 *
564 * @return array
565 * @since 2.2
566 */
567 function get_email_placeholders_by_user( $user ) {
568 if ( is_multisite() ) {
569 $site_name = get_network()->site_name;
570 } else {
571 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
572 }
573
574 $settings = \MagicLogin\Utils\get_settings();
575 $ttl = get_ttl_by_user( $user->ID );
576
577 list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $ttl );
578 $selected_interval_str = strtolower( $selected_interval );
579 $allowed_intervals = get_allowed_intervals();
580
581 if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
582 $selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
583 }
584
585 $placeholders = [
586 '{{SITEURL}}' => home_url(),
587 '{{USERNAME}}' => $user->user_login,
588 '{{FIRST_NAME}}' => $user->first_name,
589 '{{LAST_NAME}}' => $user->last_name,
590 '{{FULL_NAME}}' => $user->first_name . ' ' . $user->last_name,
591 '{{DISPLAY_NAME}}' => $user->display_name,
592 '{{USER_EMAIL}}' => $user->user_email,
593 '{{SITENAME}}' => $site_name,
594 '{{EXPIRES}}' => $ttl,
595 '{{EXPIRES_WITH_INTERVAL}}' => $token_ttl . ' ' . $selected_interval_str,
596 '{{TOKEN_VALIDITY_COUNT}}' => $settings['token_validity'],
597 ];
598
599 return $placeholders;
600 }
601
602 /**
603 * Get decrypted value
604 *
605 * @param string $value encrypted value
606 *
607 * @return bool|mixed|string
608 * @since 2.2
609 */
610 function get_decrypted_value( $value ) {
611 $encryption = new Encryption();
612 $decrypted_value = $encryption->decrypt( $value );
613
614 if ( false !== $decrypted_value ) {
615 return $decrypted_value;
616 }
617
618 return $value;
619 }
620
621 /**
622 * Get the token TTL by user
623 *
624 * @param int $user_id User ID
625 *
626 * @return int TTL in minutes
627 * @since 2.2
628 */
629 function get_ttl_by_user( $user_id ) {
630 $settings = \MagicLogin\Utils\get_settings();
631 $ttl = $settings['token_ttl'];
632
633 /**
634 * Filter the token TTL by user
635 *
636 * @hook magic_login_token_ttl_by_user
637 *
638 * @param {int} $ttl TTL in minutes
639 * @param {int} $user_id User ID
640 *
641 * @return {int} New value
642 * @since 2.2
643 */
644 return apply_filters( 'magic_login_token_ttl_by_user', $ttl, $user_id );
645 }
646
647
648 /**
649 * Default registration email message
650 *
651 * @return mixed|string|void
652 * @since 2.2
653 */
654 function get_default_registration_email_text() {
655 $email_text = __(
656 'Hi there,
657 <br><br>
658 Thank you for signing up to {{SITENAME}}! We are excited to have you on board.
659 <br>
660 To get started, simply use the magic link below to log in:
661 <br><br>
662 <a href="{{MAGIC_LINK}}" target="_blank" rel="noreferrer noopener">Click here to log in</a>
663 <br><br>
664 If the button above does not work, you can also copy and paste the following URL into your browser:
665 <br>
666 {{MAGIC_LINK}}
667 <br><br>
668 We hope you enjoy your experience with us. If you have any questions or need assistance, feel free to reach out.
669 <br><br>
670 Regards,<br>
671 All at {{SITENAME}}<br>
672 {{SITEURL}}',
673 'magic-login'
674 );
675
676 return $email_text;
677 }
678
679
680 /**
681 * Get user by log input
682 *
683 * @param string $input Input. It can be username, email or phone number
684 *
685 * @return false|mixed|\WP_User|null
686 * @since 2.4
687 */
688 function get_user_by_log_input( $input ) {
689 $user = get_user_by( 'login', $input );
690
691 if ( ! defined( 'MAGIC_LOGIN_USERNAME_ONLY' ) || false === MAGIC_LOGIN_USERNAME_ONLY ) {
692 if ( ! $user && strpos( $input, '@' ) ) {
693 $user = get_user_by( 'email', $input );
694 }
695 }
696
697 return $user;
698 }
699