PluginProbe
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password / 2.2
Magic Login – Magic Link & Passwordless Authentication for WordPress – Login Without Password v2.2
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.2, at includes/utils.php

571 lines 13.8 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 *
24 * @return string
25 */
26 function create_user_token( $user ) {
27 $settings = get_settings(); // phpcs:ignore
28 $tokens = get_user_meta( $user->ID, TOKEN_USER_META, true );
29 $tokens = is_string( $tokens ) ? array( $tokens ) : $tokens;
30 $new_token = sha1( wp_generate_password() );
31 $hashed_token = hash_hmac( 'sha256', $new_token, wp_salt() );
32
33 $ip = sha1( get_client_ip() );
34 if ( defined( 'WP_CLI' ) && WP_CLI ) {
35 $ip = 'cli';
36 }
37
38 $tokens[] = [
39 'token' => $hashed_token,
40 'time' => time(),
41 'ip_hash' => $ip,
42 ];
43
44 update_user_meta( $user->ID, TOKEN_USER_META, $tokens );
45
46 if ( absint( $settings['token_ttl'] ) > 0 ) { // eternal token
47 wp_schedule_single_event( time() + ( $settings['token_ttl'] * MINUTE_IN_SECONDS ), CRON_HOOK_NAME, array( $user->ID ) );
48 }
49
50 return $new_token;
51 }
52
53
54 /**
55 * Create login link for given user
56 *
57 * @param object $user WP_User object
58 *
59 * @return mixed|string
60 */
61 function create_login_link( $user ) {
62 $token = create_user_token( $user );
63
64 $query_args = array(
65 'user_id' => $user->ID,
66 'token' => $token,
67 'magic-login' => 1,
68 );
69
70 if ( ! empty( $_POST['redirect_to'] ) ) {
71 $query_args['redirect_to'] = urlencode( wp_unslash( $_POST['redirect_to'] ) ); // phpcs:ignore
72 }
73
74 $login_url = esc_url_raw( add_query_arg( $query_args, wp_login_url() ) );
75
76 return $login_url;
77 }
78
79 /**
80 * Get client raw ip
81 * this should be hashed
82 *
83 * @return mixed
84 */
85 function get_client_ip() {
86 /**
87 * `HTTP_X_FORWARDED_FOR` removed in 1.5
88 * Filters the ip address
89 *
90 * @hook magic_login_client_ip
91 *
92 * @param {string} REMOTE_ADDR
93 *
94 * @return {string} New value.
95 * @since 1.5
96 */
97 return apply_filters( 'magic_login_client_ip', $_SERVER['REMOTE_ADDR'] ); // phpcs:ignore
98 }
99 /**
100 * Get settings with defaults
101 *
102 * @return array
103 * @since 1.0
104 */
105 function get_settings() {
106 $defaults = [
107 'is_default' => false,
108 'add_login_button' => true,
109 'token_ttl' => 5,
110 'token_validity' => 1,
111 'token_interval' => 'MINUTE',
112 'enable_brute_force_protection' => false,
113 'brute_force_bantime' => 60, // in minutes
114 'brute_force_login_attempt' => 10,
115 'brute_force_login_time' => 5, // in minutes
116 'enable_login_throttling' => false,
117 'login_throttling_limit' => 10,
118 'login_throttling_time' => 15, // in minutes
119 'enable_ip_check' => false,
120 'enable_domain_restriction' => false,
121 'allowed_domains' => '',
122 'login_email' => get_default_login_email_text(),
123 'enable_login_redirection' => false,
124 'default_redirection_url' => '',
125 'enforce_redirection_rules' => true,
126 'enable_wp_login_redirection' => false,
127 'enable_role_based_redirection' => false,
128 'role_based_redirection_rules' => [],
129 'email_subject' => __( 'Log in to {{SITENAME}}', 'magic-login' ),
130 'auto_login_links' => false,
131 'enable_ajax' => false,
132 'enable_woo_integration' => false,
133 'woo_position' => 'before',
134 'registration' => [
135 'enable' => false,
136 'mode' => 'auto', // or standard|shortcode
137 'fallback_email_field' => true, // show email field on auto registration mode as a fallback
138 'show_name_field' => true,
139 'require_name_field' => true,
140 'show_terms_field' => false,
141 'require_terms_field' => false,
142 'terms' => '',
143 'send_email' => true,
144 'email_subject' => esc_html__( 'Welcome to {{SITENAME}}', 'magic-login' ),
145 'email_content' => get_default_registration_email_text(),
146 ],
147 'spam_protection' => [
148 'service' => 'recaptcha',
149 'enable_login' => false,
150 'enable_registration' => false,
151 ],
152 'recaptcha' => [
153 'type' => 'v3',
154 'v2_checkbox' => [
155 'site_key' => '',
156 'secret_key' => '',
157 ],
158 'v2_invisible' => [
159 'site_key' => '',
160 'secret_key' => '',
161 ],
162 'v3' => [
163 'site_key' => '',
164 'secret_key' => '',
165 ],
166 ],
167 'cf_turnstile' => [
168 'site_key' => '',
169 'secret_key' => '',
170 ],
171 ];
172
173 if ( MAGIC_LOGIN_IS_NETWORK ) {
174 $settings = get_site_option( SETTING_OPTION, [] );
175 } else {
176 $settings = get_option( SETTING_OPTION, [] );
177 }
178
179 // Merge settings with defaults, ensuring new additions and nested arrays are included
180 $settings = array_replace_recursive( $defaults, $settings );
181
182 return $settings;
183 }
184
185 /**
186 * Default login email message
187 *
188 * @return mixed|string|void
189 */
190 function get_default_login_email_text() {
191 /* translators: Do not translate USERNAME, SITENAME,EXPIRES, MAGIC_LINK, SITENAME, SITEUR, EXPIRES_WITH_INTERVAL: those are placeholders. */
192 $email_text = __(
193 'Hi {{USERNAME}},
194
195 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:
196
197 <a href="{{MAGIC_LINK}}" target="_blank" rel="noreferrer noopener">Log In</a>
198
199 Need the link? {{MAGIC_LINK}}
200
201
202 You can safely ignore and delete this email if you do not want to log in.
203
204 Regards,
205 All at {{SITENAME}}
206 {{SITEURL}}',
207 'magic-login'
208 );
209
210 return $email_text;
211 }
212
213 /**
214 * Is plugin activated network wide?
215 *
216 * @param string $plugin_file file path
217 *
218 * @return bool
219 * @since 1.0
220 */
221 function is_network_wide( $plugin_file ) {
222 if ( ! is_multisite() ) {
223 return false;
224 }
225
226 if ( ! function_exists( 'is_plugin_active_for_network' ) ) {
227 require_once ABSPATH . '/wp-admin/includes/plugin.php';
228 }
229
230 return is_plugin_active_for_network( plugin_basename( $plugin_file ) );
231 }
232
233 /**
234 * Get login link
235 *
236 * @return mixed|string
237 */
238 function get_magic_login_url() {
239 return esc_url_raw( site_url( 'wp-login.php?action=magic_login', 'login_post' ) );
240 }
241
242 /**
243 * Get user tokens
244 *
245 * @param int $user_id User ID
246 * @param bool $clear_expired flag for clean-up expired tokens
247 *
248 * @return array|mixed
249 */
250 function get_user_tokens( $user_id, $clear_expired = false ) {
251 $tokens = get_user_meta( $user_id, TOKEN_USER_META, true );
252 $tokens = is_array( $tokens ) ? $tokens : [];
253
254 /**
255 * Filter user tokens
256 *
257 * @hook magic_login_user_tokens
258 *
259 * @param {array} $tokens User tokens.
260 * @param {int} $user_id User ID.
261 * @param {boolean} $clear_expired Whether to clear expired tokens or not.
262 *
263 * @return {array} New value
264 * @since 2.1
265 */
266 $tokens = (array) apply_filters( 'magic_login_user_tokens', $tokens, $user_id, $clear_expired );
267
268 if ( $clear_expired ) {
269 $ttl = get_ttl_by_user( $user_id );
270
271 if ( 0 === $ttl ) { // means token lives forever till used
272 return $tokens;
273 }
274
275 foreach ( $tokens as $index => $token_data ) {
276 if ( empty( $token_data ) ) {
277 unset( $tokens[ $index ] );
278 continue;
279 }
280
281 if ( time() > absint( $token_data['time'] ) + ( $ttl * MINUTE_IN_SECONDS ) ) {
282 unset( $tokens[ $index ] );
283 }
284 }
285 update_user_meta( $user_id, TOKEN_USER_META, $tokens );
286 }
287
288 return $tokens;
289 }
290
291 /**
292 * Get default redirect url for given user
293 *
294 * @param \WP_User $user User object
295 *
296 * @return string|void
297 */
298 function get_user_default_redirect( $user ) {
299 if ( is_multisite() && ! get_active_blog_for_user( $user->ID ) && ! is_super_admin( $user->ID ) ) {
300 $redirect_to = user_admin_url();
301 } elseif ( is_multisite() && ! $user->has_cap( 'read' ) ) {
302 $redirect_to = get_dashboard_url( $user->ID );
303 } elseif ( ! $user->has_cap( 'edit_posts' ) ) {
304 $redirect_to = $user->has_cap( 'read' ) ? admin_url( 'profile.php' ) : home_url();
305 } else {
306 $redirect_to = admin_url();
307 }
308
309 return $redirect_to;
310 }
311
312 /**
313 * Delete all token meta
314 */
315 function delete_all_tokens() {
316 global $wpdb;
317
318 return $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.DirectDatabaseQuery.DirectQuery
319 $wpdb->usermeta,
320 [
321 'meta_key' => TOKEN_USER_META, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
322 ]
323 );
324 }
325
326
327 /**
328 * Allowed intervals for TTL.
329 *
330 * @return array
331 * @since 1.2
332 */
333 function get_allowed_intervals() {
334 return [
335 'MINUTE' => esc_html__( 'Minute(s)', 'magic-login' ),
336 'HOUR' => esc_html__( 'Hour(s)', 'magic-login' ),
337 'DAY' => esc_html__( 'Day(s)', 'magic-login' ),
338 ];
339 }
340
341 /**
342 * Convert minutes to possible time format
343 *
344 * @param int $timeout_in_minutes TTL in minutes
345 *
346 * @return array
347 * @since 1.2
348 */
349 function get_ttl_with_interval( $timeout_in_minutes ) {
350 $ttl = $timeout_in_minutes;
351 $interval = 'MINUTE';
352
353 if ( $ttl > 0 ) {
354 if ( 0 === (int) ( $ttl % 1440 ) ) {
355 $ttl = $ttl / 1440;
356 $interval = 'DAY';
357 } elseif ( 0 === (int) ( $ttl % 60 ) ) {
358 $ttl = $ttl / 60;
359 $interval = 'HOUR';
360 }
361 }
362
363 return array(
364 $ttl,
365 $interval,
366 );
367 }
368
369
370 /**
371 * Get the documentation url
372 *
373 * @param string $path The path of documentation
374 * @param string $fragment URL Fragment
375 *
376 * @return string final URL
377 */
378 function get_doc_url( $path = null, $fragment = '' ) {
379 $doc_base = 'https://handyplugins.co/';
380 $utm_parameters = '?utm_source=wp_admin&utm_medium=plugin&utm_campaign=settings_page';
381
382 if ( ! empty( $path ) ) {
383 $doc_base .= ltrim( $path, '/' );
384 }
385
386 $doc_url = trailingslashit( $doc_base ) . $utm_parameters;
387
388 if ( ! empty( $fragment ) ) {
389 $doc_url .= '#' . $fragment;
390 }
391
392 return $doc_url;
393 }
394
395 /**
396 * Check weather current screen is magic login settings page or not
397 *
398 * @return bool
399 * @since 1.2.1
400 */
401 function is_magic_login_settings_screen() {
402 $current_screen = get_current_screen();
403
404 if ( ! is_a( $current_screen, '\WP_Screen' ) ) {
405 return false;
406 }
407
408 if ( false !== strpos( $current_screen->base, 'magic-login' ) ) {
409 return true;
410 }
411
412 return false;
413 }
414
415 /**
416 * Mask given string
417 *
418 * @param string $input_string String
419 * @param int $unmask_length The length of unmask
420 *
421 * @return string
422 * @since 2.2
423 */
424 function mask_string( $input_string, $unmask_length ) {
425 $output_string = substr( $input_string, 0, $unmask_length );
426
427 if ( strlen( $input_string ) > $unmask_length ) {
428 $output_string .= str_repeat( '*', strlen( $input_string ) - $unmask_length );
429 }
430
431 return $output_string;
432 }
433
434
435 /**
436 * Check if the given value is masked
437 *
438 * @param string $value The value to check
439 * @param int $mask_length The length of the mask
440 *
441 * @return bool
442 * @since 2.2
443 */
444 function is_masked_value( $value, $mask_length = 3 ) {
445 // Get the last characters of the string
446 $last_chars = substr( $value, - $mask_length );
447
448 // Check if the last characters are asterisks
449 return str_repeat( '*', $mask_length ) === $last_chars;
450 }
451
452 /**
453 * Get email placeholders by user
454 *
455 * @param \WP_User $user User object
456 *
457 * @return array
458 * @since 2.2
459 */
460 function get_email_placeholders_by_user( $user ) {
461 if ( is_multisite() ) {
462 $site_name = get_network()->site_name;
463 } else {
464 $site_name = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
465 }
466
467 $settings = \MagicLogin\Utils\get_settings();
468 $ttl = get_ttl_by_user( $user->ID );
469
470 list( $token_ttl, $selected_interval ) = get_ttl_with_interval( $ttl );
471 $selected_interval_str = strtolower( $selected_interval );
472 $allowed_intervals = get_allowed_intervals();
473
474 if ( isset( $allowed_intervals[ $selected_interval ] ) ) {
475 $selected_interval_str = strtolower( $allowed_intervals[ $selected_interval ] ); // translated interval
476 }
477
478 $placeholders = [
479 '{{SITEURL}}' => home_url(),
480 '{{USERNAME}}' => $user->user_login,
481 '{{FIRST_NAME}}' => $user->first_name,
482 '{{LAST_NAME}}' => $user->last_name,
483 '{{FULL_NAME}}' => $user->first_name . ' ' . $user->last_name,
484 '{{DISPLAY_NAME}}' => $user->display_name,
485 '{{USER_EMAIL}}' => $user->user_email,
486 '{{SITENAME}}' => $site_name,
487 '{{EXPIRES}}' => $ttl,
488 '{{EXPIRES_WITH_INTERVAL}}' => $token_ttl . ' ' . $selected_interval_str,
489 '{{TOKEN_VALIDITY_COUNT}}' => $settings['token_validity'],
490 ];
491
492 return $placeholders;
493 }
494
495 /**
496 * Get decrypted value
497 *
498 * @param string $value encrypted value
499 *
500 * @return bool|mixed|string
501 * @since 2.2
502 */
503 function get_decrypted_value( $value ) {
504 $encryption = new Encryption();
505 $decrypted_value = $encryption->decrypt( $value );
506
507 if ( false !== $decrypted_value ) {
508 return $decrypted_value;
509 }
510
511 return $value;
512 }
513
514 /**
515 * Get the token TTL by user
516 *
517 * @param int $user_id User ID
518 *
519 * @return int TTL in minutes
520 * @since 2.2
521 */
522 function get_ttl_by_user( $user_id ) {
523 $settings = \MagicLogin\Utils\get_settings();
524 $ttl = $settings['token_ttl'];
525
526 /**
527 * Filter the token TTL by user
528 *
529 * @hook magic_login_token_ttl_by_user
530 *
531 * @param {int} $ttl TTL in minutes
532 * @param {int} $user_id User ID
533 *
534 * @return {int} New value
535 * @since 2.2
536 */
537 return apply_filters( 'magic_login_token_ttl_by_user', $ttl, $user_id );
538 }
539
540
541 /**
542 * Default registration email message
543 *
544 * @return mixed|string|void
545 * @since 2.2
546 */
547 function get_default_registration_email_text() {
548 $email_text = __(
549 'Hi there,
550 <br><br>
551 Thank you for signing up to {{SITENAME}}! We are excited to have you on board.
552 <br>
553 To get started, simply use the magic link below to log in:
554 <br><br>
555 <a href="{{MAGIC_LINK}}" target="_blank" rel="noreferrer noopener">Click here to log in</a>
556 <br><br>
557 If the button above does not work, you can also copy and paste the following URL into your browser:
558 <br>
559 {{MAGIC_LINK}}
560 <br><br>
561 We hope you enjoy your experience with us. If you have any questions or need assistance, feel free to reach out.
562 <br><br>
563 Regards,<br>
564 All at {{SITENAME}}<br>
565 {{SITEURL}}',
566 'magic-login'
567 );
568
569 return $email_text;
570 }
571