PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 4.0.0
WP 2FA – Two-factor authentication for WordPress v4.0.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Admin / Helpers / class-ajax-helper.php
wp-2fa / includes / classes / Admin / Helpers Last commit date
class-ajax-helper.php 5 days ago class-classes-helper.php 5 days ago class-email-templates.php 5 days ago class-file-writer.php 5 days ago class-methods-helper.php 5 days ago class-php-helper.php 5 days ago class-sms-templates.php 5 days ago class-user-helper.php 5 days ago class-wp-helper.php 5 days ago index.php 5 days ago
class-ajax-helper.php
535 lines
1 <?php
2 /**
3 * Responsible for the AJAX calls.
4 *
5 * @package wp2fa
6 * @subpackage helpers
7 *
8 * @since 2.6.0
9 *
10 * @copyright 2026 Melapress
11 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
12 *
13 * @see https://wordpress.org/plugins/wp-2fa/
14 */
15
16 declare(strict_types=1);
17
18 namespace WP2FA\Admin\Helpers;
19
20 use WP2FA\WP2FA;
21 use WP2FA\Utils\User_Utils;
22 use WP2FA\Admin\Settings_Page;
23 use WP2FA\Utils\Settings_Utils;
24 use WP2FA\Admin\Helpers\WP_Helper;
25 use WP2FA\Admin\Helpers\Email_Templates;
26 use WP2FA\Admin\SettingsPages\Settings_Page_Email;
27
28 // Exit if accessed directly.
29 if ( ! defined( 'ABSPATH' ) ) {
30 exit;
31 }
32
33 if ( ! class_exists( '\WP2FA\Admin\Helpers\Ajax_Helper' ) ) {
34 /**
35 * Responsible for the proper AJAX calls and responses.
36 */
37 class Ajax_Helper {
38
39 /**
40 * Cache TTL for search results (in seconds).
41 */
42 private const CACHE_TTL = 300; // 5 minutes
43
44 /**
45 * Rate limit attempts per minute.
46 */
47 private const RATE_LIMIT_ATTEMPTS = 5;
48
49 /**
50 * Verifies nonce and user permissions for AJAX requests.
51 *
52 * @param string $nonce_action The nonce action to verify.
53 * @param string $capability The capability to check (default: manage_options).
54 *
55 * @return void
56 *
57 * @since 3.1.1
58 */
59 private static function verify_request( string $nonce_action, string $capability = 'manage_options' ): void {
60 if ( ! \current_user_can( $capability ) ) {
61 \wp_send_json_error( \esc_html__( 'Access Denied.', 'wp-2fa' ) );
62 }
63
64 $nonce = isset( $_REQUEST['wp_2fa_nonce'] ) ? \sanitize_text_field( \wp_unslash( $_REQUEST['wp_2fa_nonce'] ) ) : null;
65 if ( null === $nonce || false === $nonce || ! \wp_verify_nonce( $nonce, $nonce_action ) ) {
66 \wp_send_json_error( \esc_html__( 'Nonce verification failed.', 'wp-2fa' ) );
67 }
68 }
69
70 /**
71 * Checks rate limiting for sensitive actions.
72 *
73 * @param string $action_key Unique key for the action.
74 *
75 * @return bool True if allowed, false if rate limited.
76 *
77 * @since 3.1.1
78 */
79 private static function check_rate_limit( string $action_key ): bool {
80 $user_id = \get_current_user_id();
81 $cache_key = 'wp2fa_rate_limit_' . $action_key . '_' . $user_id;
82 $attempts = (int) \get_transient( $cache_key );
83
84 if ( $attempts >= self::RATE_LIMIT_ATTEMPTS ) {
85 return false;
86 }
87
88 \set_transient( $cache_key, $attempts + 1, 60 ); // 1 minute TTL
89 return true;
90 }
91
92 /**
93 * Get all users in AJAX matter and returns them
94 *
95 * @since 2.6.0
96 */
97 public static function get_all_users() {
98 self::verify_request( 'wp-2fa-settings-nonce' );
99
100 $term = isset( $_GET['term'] ) ? \sanitize_text_field( \wp_unslash( $_GET['term'] ) ) : null;
101
102 if ( null === $term || false === $term ) {
103 \wp_send_json_error( \esc_html__( 'Invalid term.', 'wp-2fa' ) );
104 }
105
106 $cache_key = 'wp2fa_users_search_' . md5( $term );
107 $users = \get_transient( $cache_key );
108
109 if ( false === $users ) {
110 $users_args = array(
111 'fields' => array( 'ID', 'user_login' ),
112 'search' => '*' . $term . '*',
113 'search_columns' => array( 'user_login' ),
114 'number' => 20,
115 'orderby' => 'user_login',
116 'order' => 'ASC',
117 );
118 if ( WP_Helper::is_multisite() ) {
119 $users_args['blog_id'] = 0;
120 }
121 $users_data = \get_users( $users_args );
122
123 // Create final array which we will fill in below.
124 $users = array();
125
126 foreach ( $users_data as $user ) {
127 $users[] = array(
128 'value' => $user->user_login,
129 'label' => $user->user_login,
130 );
131 }
132
133 \set_transient( $cache_key, $users, self::CACHE_TTL );
134 }
135
136 \wp_send_json_success( $users );
137 }
138
139 /**
140 * Get all network sites in AJAX way
141 *
142 * @since 2.6.0
143 */
144 public static function get_all_network_sites() {
145 self::verify_request( 'wp-2fa-settings-nonce' );
146
147 $term = isset( $_GET['term'] ) ? \sanitize_text_field( \wp_unslash( $_GET['term'] ) ) : null;
148
149 if ( null === $term || false === $term ) {
150 \wp_send_json_error( \esc_html__( 'Invalid term.', 'wp-2fa' ) );
151 }
152
153 $cache_key = 'wp2fa_sites_search_' . md5( $term );
154 $sites_found = \get_transient( $cache_key );
155
156 if ( false === $sites_found ) {
157 $sites_found = array();
158
159 foreach ( WP_Helper::get_multi_sites() as $site ) {
160 if ( false !== stripos( $site->blogname, $term ) ) {
161 $sites_found[] = array(
162 'label' => $site->blog_id,
163 'value' => $site->blogname,
164 );
165 }
166 }
167
168 \set_transient( $cache_key, $sites_found, self::CACHE_TTL );
169 }
170
171 \wp_send_json_success( $sites_found );
172 }
173
174 /**
175 * Unlock users accounts if they have overrun grace period it is also used in AJAX calls
176 *
177 * @param int $user_id User ID.
178 *
179 * @since 2.6.0
180 */
181 public static function unlock_account( $user_id ) {
182 if ( ! self::check_rate_limit( 'unlock_account' ) ) {
183 \wp_send_json_error( \esc_html__( 'Rate limit exceeded. Please try again later.', 'wp-2fa' ) );
184 }
185
186 self::verify_request( 'wp-2fa-unlock-account-nonce' );
187
188 $user_id = isset( $_GET['user_id'] ) ? \intval( \sanitize_text_field( \wp_unslash( $_GET['user_id'] ) ) ) : null;
189
190 if ( isset( $user_id ) ) {
191
192 $grace_period = Settings_Utils::get_setting_role( User_Helper::get_user_role( $user_id ), 'grace-period' );
193 $grace_period_denominator = Settings_Utils::get_setting_role( User_Helper::get_user_role( $user_id ), 'grace-period-denominator' );
194 $create_a_string = $grace_period . ' ' . $grace_period_denominator;
195 // Turn that string into a time.
196 $grace_expiry = strtotime( $create_a_string );
197
198 User_Helper::remove_meta( WP_2FA_PREFIX . 'locked_account_notification', $user_id );
199 User_Helper::remove_grace_period( $user_id );
200 User_Helper::remove_meta( User_Helper::USER_LOCKED_STATUS, $user_id );
201
202 User_Helper::set_user_expiry_date( (string) $grace_expiry, $user_id );
203
204 // Recalculate the 2FA status based on current plugin settings.
205 $user_obj = User_Helper::get_user( $user_id );
206 if ( $user_obj instanceof \WP_User ) {
207 User_Helper::set_user_status( $user_obj );
208 }
209
210 Settings_Page::send_account_unlocked_email( $user_id );
211
212 /*
213 * Fires after the user is unlocked.
214 *
215 * @param \WP_User $user - The user for which the method has been set.
216 *
217 * @since 2.6.0
218 */
219 \do_action( WP_2FA_PREFIX . 'user_is_unlocked', User_Helper::get_user( $user_id ) );
220
221 \add_action( 'admin_notices', array( __CLASS__, 'user_unlocked_notice' ) );
222 }
223 }
224
225 /**
226 * Sets the salt key into the wp-config.php file via AJAX request.
227 *
228 * @return void
229 *
230 * @since 2.4.0
231 */
232 public static function set_salt_key() {
233 if ( \wp_doing_ajax() ) {
234 if ( isset( $_REQUEST['_wpnonce'] ) ) {
235 $nonce_check = \wp_verify_nonce( \sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'wp-2fa-set-salt-nonce' );
236 if ( ! $nonce_check ) {
237 \wp_send_json_error( new \WP_Error( 500, esc_html__( 'Nonce checking failed', 'wp-2fa' ) ), 400 );
238 } elseif ( \current_user_can( 'manage_options' ) ) {
239 if ( ! File_Writer::can_write_to_file( File_Writer::get_wp_config_file_path() ) ) {
240 \wp_send_json_error(
241 new \WP_Error(
242 500,
243 \esc_html__( 'Unable to write to wp-config.php', 'wp-2fa' )
244 ),
245 400
246 );
247 } else {
248 $secret_key = Settings_Utils::get_option( 'secret_key' );
249 if ( ! empty( $secret_key ) ) {
250 File_Writer::save_secret_key( $secret_key );
251 Settings_Utils::delete_option( 'secret_key' );
252 \wp_send_json_success(
253 \esc_html__(
254 'wp-config.php successfully update, global setting deleted',
255 'wp-2fa'
256 )
257 );
258 } else {
259 \wp_send_json_error(
260 new \WP_Error(
261 500,
262 \esc_html__( 'Unable to find global secret key', 'wp-2fa' )
263 ),
264 400
265 );
266 }
267 }
268 }
269 }
270 }
271 }
272
273 /**
274 * Removes the salt key unable to store in the wp-config file notification.
275 *
276 * @return void
277 *
278 * @since 3.0.0
279 */
280 public static function unset_salt_key(): void {
281 if ( \wp_doing_ajax() ) {
282 if ( isset( $_REQUEST['_wpnonce'] ) ) {
283 $nonce_check = \wp_verify_nonce( \sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'wp-2fa-unset-salt-nonce' );
284 if ( ! $nonce_check ) {
285 \wp_send_json_error( new \WP_Error( 500, esc_html__( 'Nonce checking failed', 'wp-2fa' ) ), 400 );
286 } elseif ( \current_user_can( 'manage_options' ) ) {
287
288 Settings_Utils::update_option( 'remove_store_salt_in_wp_config_message', true );
289
290 \wp_send_json_success(
291 \esc_html__(
292 'message is set for removal',
293 'wp-2fa'
294 )
295 );
296
297 }
298 }
299 }
300 }
301
302 /**
303 * Remove user 2fa config
304 *
305 * @param int $user_id User ID.
306 *
307 * @since 2.6.0
308 */
309 public static function remove_user_2fa( $user_id ) {
310 if ( ! self::check_rate_limit( 'remove_user_2fa' ) ) {
311 \wp_send_json_error( \esc_html__( 'Rate limit exceeded. Please try again later.', 'wp-2fa' ) );
312 }
313
314 // Allow admins or the user themselves.
315 $current_user_id = (int) \get_current_user_id();
316 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
317 $request_user_id = isset( $_POST['user_id'] ) ? \intval( \sanitize_text_field( \wp_unslash( $_POST['user_id'] ) ) ) : ( isset( $_GET['user_id'] ) ? \intval( \sanitize_text_field( \wp_unslash( $_GET['user_id'] ) ) ) : null );
318
319 if ( ! \current_user_can( 'manage_options' ) ) {
320 if ( null === $request_user_id || $request_user_id !== $current_user_id ) {
321 \wp_send_json_error( 'Access Denied.' );
322 }
323 }
324
325 // Verify nonce.
326 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
327 $nonce = isset( $_POST['wp_2fa_nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['wp_2fa_nonce'] ) ) : ( isset( $_GET['wp_2fa_nonce'] ) ? \sanitize_text_field( \wp_unslash( $_GET['wp_2fa_nonce'] ) ) : null );
328 if ( null === $nonce || false === $nonce || ! \wp_verify_nonce( $nonce, 'wp-2fa-remove-user-2fa-nonce' ) ) {
329 \wp_send_json_error( esc_html__( 'Nonce verification failed.', 'wp-2fa' ) );
330 }
331
332 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
333 $user_id = isset( $_POST['user_id'] ) ? \intval( \sanitize_text_field( \wp_unslash( $_POST['user_id'] ) ) ) : ( isset( $_GET['user_id'] ) ? \intval( \sanitize_text_field( \wp_unslash( $_GET['user_id'] ) ) ) : null );
334 // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
335 $admin_reset = isset( $_POST['admin_reset'] ) ? (bool) \intval( \sanitize_text_field( \wp_unslash( $_POST['admin_reset'] ) ) ) : ( isset( $_GET['admin_reset'] ) ? (bool) \intval( \sanitize_text_field( \wp_unslash( $_GET['admin_reset'] ) ) ) : false );
336
337 if ( isset( $user_id ) ) {
338
339 User_Helper::remove_2fa_for_user( $user_id );
340
341 if ( \wp_doing_ajax() ) {
342 \wp_send_json_success();
343 }
344
345 if ( $admin_reset ) {
346 \add_action( 'admin_notices', array( __CLASS__, 'admin_deleted_2fa_notice' ) );
347 } else {
348 \add_action( 'admin_notices', array( __CLASS__, 'user_deleted_2fa_notice' ) );
349 }
350 }
351 }
352
353 /**
354 * Returns the user roles in AJAX matter.
355 *
356 * @return void
357 *
358 * @since 2.6.0
359 */
360 public static function get_ajax_user_roles() {
361 if ( \wp_doing_ajax() ) {
362 self::verify_request( 'wp-2fa-settings-nonce' );
363
364 $roles = array();
365
366 $term = isset( $_GET['term'] ) ? \sanitize_text_field( \wp_unslash( $_GET['term'] ) ) : null;
367 if ( null === $term || false === $term ) {
368 \wp_send_json_error( esc_html__( 'Invalid term.', 'wp-2fa' ) );
369 }
370
371 $cache_key = 'wp2fa_roles_search_' . md5( $term );
372 $roles = \get_transient( $cache_key );
373
374 if ( false === $roles ) {
375 $roles = array();
376
377 foreach ( WP_Helper::get_roles_wp() as $role => $human_readable ) {
378 if ( stripos( $human_readable, $term ) !== false ) {
379 $roles[] = array(
380 'label' => \sanitize_text_field( $role ),
381 'value' => \sanitize_text_field( $human_readable ),
382 );
383 }
384 }
385
386 \set_transient( $cache_key, $roles, self::CACHE_TTL );
387 }
388
389 \wp_send_json_success( $roles );
390 }
391 }
392
393 /**
394 * Handles AJAX calls for sending test emails.
395 *
396 * @return void
397 *
398 * @since 2.6.0
399 */
400 public static function handle_send_test_email_ajax() {
401 // Check user permissions.
402 if ( ! \current_user_can( 'manage_options' ) ) {
403 \wp_send_json_error( \esc_html__( 'Access Denied.', 'wp-2fa' ) );
404 }
405
406 // Check email ID.
407 $email_id = isset( $_POST['email_id'] ) ? \sanitize_text_field( \wp_unslash( $_POST['email_id'] ) ) : null;
408 if ( null === $email_id || false === $email_id ) {
409 \wp_send_json_error( \esc_html__( 'Invalid email ID.', 'wp-2fa' ) );
410 }
411
412 // Verify nonce.
413 $nonce = isset( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : null;
414 if ( null === $nonce || false === $nonce || ! wp_verify_nonce( $nonce, 'wp-2fa-email-test-' . $email_id ) ) {
415 wp_send_json_error( esc_html__( 'Nonce verification failed.', 'wp-2fa' ) );
416 }
417
418 $user_id = \get_current_user_id();
419 $user = \get_userdata( $user_id );
420 $email = $user->user_email;
421
422 if ( 'config_test' === $email_id ) {
423 $email_sent = Settings_Page::send_email(
424 $email,
425 \esc_html__( 'Test email from WP 2FA', 'wp-2fa' ),
426 \esc_html__( 'This email was sent by the WP 2FA plugin to test the email delivery.', 'wp-2fa' )
427 );
428 if ( $email_sent ) {
429 \wp_send_json_success( esc_html__( 'Test email was successfully sent to ', 'wp-2fa' ) . '<strong>' . esc_html( $email ) . '</strong>' );
430 }
431
432 \wp_send_json_error( esc_html__( 'Failed to send test email.', 'wp-2fa' ) );
433 }
434
435 $email_templates = Settings_Page_Email::get_email_notification_definitions();
436 foreach ( $email_templates as $email_template ) {
437 if ( $email_id === $email_template->get_email_content_id() ) {
438 $subject = wp_strip_all_tags( Email_Templates::replace_email_strings( Email_Templates::get_wp2fa_email_templates( $email_id . '_email_subject' ) ) );
439 $message = \wpautop( Email_Templates::replace_email_strings( Email_Templates::get_wp2fa_email_templates( $email_id . '_email_body' ), $user_id ) );
440
441 $email_sent = Settings_Page::send_email( $email, $subject, $message );
442 if ( $email_sent ) {
443 \wp_send_json_success( esc_html__( 'Test email ', 'wp-2fa' ) . '<strong>' . \esc_html( $email_template->get_title() ) . '</strong>' . esc_html__( ' was successfully sent to ', 'wp-2fa' ) . '<strong>' . \esc_html( $email ) . '</strong>' );
444 }
445
446 \wp_send_json_error( esc_html__( 'Failed to send test email.', 'wp-2fa' ) );
447 }
448 }
449 }
450
451 /**
452 * User deleted 2FA settings notification
453 *
454 * @since 2.6.0
455 */
456 public static function user_deleted_2fa_notice() {
457 ?>
458 <div class="notice notice-success is-dismissible wp-2fa-admin-notice">
459 <p><?php \esc_html_e( '2FA settings have been removed.', 'wp-2fa' ); ?></p>
460 <button type="button" class="notice-dismiss">
461 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
462 </button>
463 </div>
464 <?php
465 }
466
467 /**
468 * Admin deleted user 2FA settings notification
469 *
470 * @since 2.6.0
471 */
472 public static function admin_deleted_2fa_notice() {
473 ?>
474 <div class="notice notice-success is-dismissible wp-2fa-admin-notice">
475 <p><?php \esc_html_e( 'User 2FA settings have been removed.', 'wp-2fa' ); ?></p>
476 <button type="button" class="notice-dismiss">
477 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
478 </button>
479 </div>
480 <?php
481 }
482
483 /**
484 * User unlocked notice.
485 *
486 * @since 2.6.0
487 */
488 public static function user_unlocked_notice() {
489 ?>
490 <div class="notice notice-success is-dismissible wp-2fa-admin-notice">
491 <p><?php \esc_html_e( 'User account successfully unlocked. User can login again.', 'wp-2fa' ); ?></p>
492 <button type="button" class="notice-dismiss">
493 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
494 </button>
495 </div>
496 <?php
497 }
498
499 /**
500 * Logs out the current user via AJAX request.
501 *
502 * @return void
503 *
504 * @since 3.1.0
505 */
506 public static function logout_account() {
507 if ( ! self::check_rate_limit( 'logout_account' ) ) {
508 \wp_send_json_error( \esc_html__( 'Rate limit exceeded. Please try again later.', 'wp-2fa' ) );
509 }
510
511 if ( ! empty( $_REQUEST['_wpnonce'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_REQUEST['_wpnonce'] ) ), 'wp-2fa-logout' ) ) {
512 $user_id = \get_current_user_id();
513
514 \wp_destroy_current_session();
515 \wp_clear_auth_cookie();
516 \wp_set_current_user( 0 );
517
518 /**
519 * Fires after a user is logged out.
520 *
521 * @since 1.5.0
522 * @since 5.5.0 Added the `$user_id` parameter.
523 *
524 * @param int $user_id ID of the user that was logged out.
525 */
526 \do_action( 'wp_logout', $user_id );
527
528 \wp_send_json_success( esc_html__( 'User successfully logged out! ', 'wp-2fa' ) );
529 }
530
531 \wp_send_json_error( esc_html__( 'Failed - wrong credentials.', 'wp-2fa' ) );
532 }
533 }
534 }
535