PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 2.9.2
WP 2FA – Two-factor authentication for WordPress v2.9.2
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 / class-settings-page.php
wp-2fa / includes / classes / Admin Last commit date
Controllers 10 months ago Fly-Out 10 months ago Helpers 10 months ago Methods 10 months ago SettingsPages 10 months ago Views 10 months ago class-help-contact-us.php 10 months ago class-plugin-updated-notice.php 10 months ago class-premium-features.php 10 months ago class-settings-page.php 10 months ago class-setup-wizard.php 10 months ago class-user-listing.php 10 months ago class-user-notices.php 10 months ago class-user-profile.php 10 months ago class-user-registered.php 10 months ago index.php 10 months ago
class-settings-page.php
532 lines
1 <?php
2 /**
3 * Settings rendering class.
4 *
5 * @package wp2fa
6 * @subpackage settings
7 * @copyright 2025 Melapress
8 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
9 * @link https://wordpress.org/plugins/wp-2fa/
10 */
11
12 namespace WP2FA\Admin;
13
14 use WP2FA\WP2FA;
15 use WP2FA\Admin\SettingsPages\{
16 Settings_Page_Policies,
17 Settings_Page_General,
18 Settings_Page_Email
19 };
20 use WP2FA\Utils\Settings_Utils;
21 use WP2FA\Admin\Controllers\Settings;
22 use WP2FA\Admin\SettingsPages\Settings_Page_White_Label;
23
24 /**
25 * Class for handling settings
26 */
27 if ( ! class_exists( '\WP2FA\Admin\Settings_Page' ) ) {
28 /**
29 * Class for handling settings
30 */
31 class Settings_Page {
32
33 const TOP_MENU_SLUG = 'wp-2fa-policies';
34
35 /**
36 * Create admin menu entry and settings page
37 */
38 public static function create_settings_admin_menu() {
39 // Create admin menu item.
40 \add_menu_page(
41 \esc_html__( 'WP 2FA', 'wp-2fa' ),
42 \esc_html__( 'WP 2FA', 'wp-2fa' ),
43 'manage_options',
44 self::TOP_MENU_SLUG,
45 null,
46 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( WP_2FA_PATH . 'dist/images/wp-2fa-white-icon20x28.svg' ) ),
47 81
48 );
49
50 \add_submenu_page(
51 self::TOP_MENU_SLUG,
52 \esc_html__( '2FA Policies', 'wp-2fa' ),
53 \esc_html__( '2FA Policies', 'wp-2fa' ),
54 'manage_options',
55 self::TOP_MENU_SLUG,
56 array( \WP2FA\Admin\SettingsPages\Settings_Page_Policies::class, 'render' ),
57 1
58 );
59
60 \add_submenu_page(
61 self::TOP_MENU_SLUG,
62 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
63 \esc_html__( 'Settings', 'wp-2fa' ),
64 'manage_options',
65 'wp-2fa-settings',
66 array( \WP2FA\Admin\SettingsPages\Settings_Page_Render::class, 'render' ),
67 2
68 );
69
70 // Register our policy settings.
71 \register_setting(
72 WP_2FA_POLICY_SETTINGS_NAME,
73 WP_2FA_POLICY_SETTINGS_NAME,
74 array( Settings_Page_Policies::class, 'validate_and_sanitize' )
75 );
76
77 // Register our white label settings.
78 \register_setting(
79 WP_2FA_WHITE_LABEL_SETTINGS_NAME,
80 WP_2FA_WHITE_LABEL_SETTINGS_NAME,
81 array( Settings_Page_White_Label::class, 'validate_and_sanitize' )
82 );
83
84 // Register our settings page.
85 \register_setting(
86 WP_2FA_SETTINGS_NAME,
87 WP_2FA_SETTINGS_NAME,
88 array( Settings_Page_General::class, 'validate_and_sanitize' )
89 );
90
91 \register_setting(
92 WP_2FA_EMAIL_SETTINGS_NAME,
93 WP_2FA_EMAIL_SETTINGS_NAME,
94 array( Settings_Page_Email::class, 'validate_and_sanitize' )
95 );
96
97 /**
98 * Fires after the main menu settings are registered.
99 *
100 * @param string - The menu slug.
101 * @param bool - Is that multisite install or not.
102 *
103 * @since 2.0.0
104 */
105 \do_action( WP_2FA_PREFIX . 'after_admin_menu_created', self::TOP_MENU_SLUG, false );
106
107 \add_action( WP_2FA_PREFIX . 'before_plugin_settings', array( __CLASS__, 'check_email' ) );
108 }
109
110 /**
111 * Create admin menu entry and settings page
112 */
113 public static function create_settings_admin_menu_multisite() {
114 // Create admin menu item.
115 \add_menu_page(
116 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
117 \esc_html__( 'WP 2FA', 'wp-2fa' ),
118 'manage_options',
119 self::TOP_MENU_SLUG,
120 null,
121 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( WP_2FA_PATH . 'dist/images/wp-2fa-white-icon20x28.svg' ) ),
122 81
123 );
124
125 \add_submenu_page(
126 self::TOP_MENU_SLUG,
127 \esc_html__( '2FA Policies', 'wp-2fa' ),
128 \esc_html__( '2FA Policies', 'wp-2fa' ),
129 'manage_options',
130 self::TOP_MENU_SLUG,
131 array( \WP2FA\Admin\SettingsPages\Settings_Page_Policies::class, 'render' ),
132 1
133 );
134
135 \add_submenu_page(
136 self::TOP_MENU_SLUG,
137 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
138 \esc_html__( 'Settings', 'wp-2fa' ),
139 'manage_options',
140 'wp-2fa-settings',
141 array( \WP2FA\Admin\SettingsPages\Settings_Page_Render::class, 'render' ),
142 2
143 );
144
145 /**
146 * Fires after the main menu settings are registered.
147 *
148 * @param string - The menu slug.
149 * @param bool - Is that multisite install or not.
150 *
151 * @since 2.0.0
152 */
153 \do_action( WP_2FA_PREFIX . 'after_admin_menu_created', self::TOP_MENU_SLUG, true );
154 }
155 /**
156 * Send account unlocked notification via email.
157 *
158 * @param int $user_id user ID.
159 *
160 * @return boolean
161 */
162 public static function send_account_unlocked_email( $user_id ) {
163 // Bail if the user has not enabled this email.
164 if ( 'enable_account_unlocked_email' !== WP2FA::get_wp2fa_email_templates( 'send_account_unlocked_email' ) ) {
165 return false;
166 }
167
168 // Grab user data.
169 $user = get_userdata( $user_id );
170 // Grab user email.
171 $email = $user->user_email;
172 // Setup the email contents.
173 $subject = wp_strip_all_tags( WP2FA::replace_email_strings( WP2FA::get_wp2fa_email_templates( 'user_account_unlocked_email_subject' ) ) );
174 $message = wpautop( WP2FA::replace_email_strings( WP2FA::get_wp2fa_email_templates( 'user_account_unlocked_email_body' ), $user_id ) );
175
176 return self::send_email( $email, $subject, $message );
177 }
178
179 /**
180 * Hide settings menu item
181 */
182 public static function hide_settings() {
183 $user = wp_get_current_user();
184
185 // Check we have a user before doing anything else.
186 if ( is_a( $user, '\WP_User' ) ) {
187 if ( ! empty( WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' ) ) ) {
188 $main_user = (int) WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' );
189 } else {
190 $main_user = get_current_user_id();
191 }
192 if ( ! empty( WP2FA::get_wp2fa_general_setting( 'limit_access' ) ) && $user->ID !== $main_user ) {
193 // Remove admin menu item.
194 remove_submenu_page( 'options-general.php', self::TOP_MENU_SLUG );
195 }
196 }
197 }
198
199 /**
200 * Add unlock user link to user actions.
201 *
202 * @param array $links Default row content.
203 *
204 * @return array
205 * @throws \Freemius_Exception - freemius exception.
206 */
207 public static function add_plugin_action_links( $links ) {
208 // add link to the external free trial page in free version and also in premium version if license is not active.
209 if ( ! function_exists( 'wp2fa_freemius' ) || ! wp2fa_freemius()->has_active_valid_license() ) {
210 $trial_link = 'https://melapress.com/wordpress-2fa/pricing/?utm_source=plugin&utm_medium=wp2fa&utm_campaign=upgrade_to_premium_menu';
211 $links = array_merge(
212 array(
213 '<a style="font-weight:bold" href="' . $trial_link . '" target="_blank">' . __( 'Upgrade to Premium', 'wp-2fa' ) . '</a>',
214 ),
215 $links
216 );
217 }
218
219 // add link to the plugin settings page.
220 $url = Settings::get_settings_page_link();
221 $links = array_merge(
222 array(
223 '<a href="' . \esc_url( $url ) . '">' . \esc_html__( 'Configure 2FA Settings', 'wp-2fa' ) . '</a>',
224 ),
225 $links
226 );
227
228 return $links;
229 }
230
231 /**
232 * Updates options for multisite
233 *
234 * @return void
235 *
236 * @since 2.0.0
237 */
238 public static function update_wp2fa_network_options() {
239
240 Settings_Page_Policies::update_wp2fa_network_options();
241
242 Settings_Page_General::update_wp2fa_network_options();
243
244 Settings_Page_White_Label::update_wp2fa_network_options();
245
246 /**
247 * Gives the ability for extensions to set their settings in the plugin.
248 *
249 * @since 2.2.0
250 */
251 \do_action( WP_2FA_PREFIX . 'update_network_settings' );
252 }
253
254 /**
255 * Handle saving email options to the network main site options.
256 */
257 public static function update_wp2fa_network_email_options() {
258 Settings_Page_Email::update_wp2fa_network_options();
259 }
260
261 /**
262 * These are used instead of add_settings_error which in a network site. Used to show if settings have been updated or failed.
263 */
264 public static function settings_saved_network_admin_notice() {
265 if ( isset( $_GET['wp_2fa_network_settings_updated'] ) && 'true' === $_GET['wp_2fa_network_settings_updated'] ) {
266 ?>
267 <div class="notice notice-success is-dismissible">
268 <p><?php \esc_html_e( '2FA Settings Updated', 'wp-2fa' ); ?></p>
269 <button type="button" class="notice-dismiss">
270 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
271 </button>
272 </div>
273 <?php
274 }
275 if ( isset( $_GET['wp_2fa_network_settings_updated'] ) && 'false' === $_GET['wp_2fa_network_settings_updated'] ) {
276 ?>
277 <div class="notice notice-error is-dismissible">
278 <?php
279 if ( isset( $_GET['wp_2fa_network_settings_custom_error_message'] ) ) {
280 $error = \sanitize_text_field( \wp_unslash( $_GET['wp_2fa_network_settings_custom_error_message'] ) );
281 ?>
282 <p><?php echo \esc_attr( \esc_url_raw( \urldecode_deep( $error ) ) ); ?></p>
283 <button type="button" class="notice-dismiss">
284 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
285 </button>
286 <?php
287 } else {
288 ?>
289 <p><?php \esc_html_e( 'Please ensure both custom email address and display name are provided.', 'wp-2fa' ); ?></p>
290 <button type="button" class="notice-dismiss">
291 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
292 </button>
293 <?php
294 }
295 ?>
296 </div>
297 <?php
298 }
299 if ( isset( $_GET['wp_2fa_network_settings_error'] ) ) {
300 ?>
301 <div class="notice notice-error is-dismissible">
302 <?php
303 $error = \sanitize_text_field( \wp_unslash( $_GET['wp_2fa_network_settings_error'] ) );
304
305 if ( false !== \strpos( $error, 'http' ) ) {
306 ?>
307 <p><?php echo \esc_attr( \esc_url_raw( \urldecode_deep( $error ) ) ); ?></p>
308 <?php
309 } else {
310 ?>
311 <p><?php echo \esc_attr( ( $error ) ); ?></p>
312 <?php } ?>
313 <button type="button" class="notice-dismiss">
314 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
315 </button>
316 </div>
317 <?php
318 }
319 }
320
321 /**
322 * These are used instead of add_settings_error which in a network site. Used to show if settings have been updated or failed.
323 *
324 * @return void
325 *
326 * @since 2.0.0
327 */
328 public static function settings_saved_admin_notice() {
329 if ( isset( $_GET['page'] ) && 0 === strpos( \sanitize_text_field( \wp_unslash( $_GET['page'] ) ), 'wp-2fa-' ) ) {
330 if ( isset( $_GET['settings-updated'] ) && 'true' === $_GET['settings-updated'] ) {
331 $wp_settings_errors = get_settings_errors();
332
333 if ( count( $wp_settings_errors ) ) {
334 foreach ( $wp_settings_errors as $error ) {
335 ?>
336 <div class="notice notice-<?php echo \esc_attr( $error['type'] ); ?> is-dismissible">
337 <p><?php echo \esc_html( $error['message'] ); ?></p>
338 <button type="button" class="notice-dismiss">
339 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
340 </button>
341 </div>
342 <?php
343 }
344 } else {
345 ?>
346 <div class="notice notice-success is-dismissible">
347 <p><?php \esc_html_e( '2FA Settings Updated', 'wp-2fa' ); ?></p>
348 <button type="button" class="notice-dismiss">
349 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
350 </button>
351 </div>
352 <?php
353 }
354 }
355 if ( isset( $_GET['settings-updated'] ) && 'false' === $_GET['settings-updated'] ) {
356 ?>
357 <div class="notice notice-error is-dismissible">
358 <p><?php \esc_html_e( 'Please ensure both custom email address and display name are provided.', 'wp-2fa' ); ?></p>
359 <button type="button" class="notice-dismiss">
360 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
361 </button>
362 </div>
363 <?php
364 }
365 if ( isset( $_GET['settings_error'] ) ) {
366 ?>
367 <div class="notice notice-error is-dismissible">
368 <p><?php echo \esc_attr( \esc_url_raw( \urldecode_deep( \sanitize_text_field( \wp_unslash( $_GET['settings_error'] ) ) ) ) ); ?></p>
369 <button type="button" class="notice-dismiss">
370 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
371 </button>
372 </div>
373 <?php
374 }
375 }
376 }
377
378 /**
379 * Add our custom state to our created page.
380 *
381 * @param array $post_states - array with the post states.
382 * @param WP_Post $post - the WP post.
383 *
384 * @return array
385 */
386 public static function add_display_post_states( $post_states, $post ) {
387 if ( ! empty( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) ) ) {
388 if ( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) === $post->ID ) {
389 $post_states['wp_2fa_page_for_user'] = __( 'WP 2FA User Page', 'wp-2fa' );
390 }
391 }
392
393 return $post_states;
394 }
395
396 /**
397 * Handles sending of an email. It sets necessary header such as content type and custom from email address and name.
398 *
399 * @param string $recipient_email Email address to send message to.
400 * @param string $subject Email subject.
401 * @param string $message Message contents.
402 *
403 * @return bool Whether the email contents were sent successfully.
404 */
405 public static function send_email( $recipient_email, $subject, $message ) {
406
407 // Sanitize email inputs.
408 $recipient_email = sanitize_email( $recipient_email );
409 $subject = sanitize_text_field( $subject );
410 $message = wp_kses_post( $message );
411
412 // Specify our desired headers.
413 $headers = 'Content-type: text/html;charset=utf-8' . "\r\n";
414
415 if ( 'use-custom-email' === WP2FA::get_wp2fa_email_templates( 'email_from_setting' ) ) {
416 $from_name = sanitize_text_field( WP2FA::get_wp2fa_email_templates( 'custom_from_display_name' ) );
417 $from_email = sanitize_email( WP2FA::get_wp2fa_email_templates( 'custom_from_email_address' ) );
418 $headers .= 'From: ' . $from_name . ' <' . $from_email . '>' . "\r\n";
419 } else {
420 $headers .= 'From: wp2fa <' . self::get_default_email_address() . '>' . "\r\n";
421 }
422
423 // Fire our email.
424 return wp_mail( $recipient_email, stripslashes_deep( html_entity_decode( $subject, ENT_QUOTES, 'UTF-8' ) ), $message, $headers );
425 }
426
427 /**
428 * Builds and returns the default email address used for the "from" email address when email is send
429 *
430 * @return string
431 *
432 * @since 2.6.4
433 */
434 public static function get_default_email_address(): string {
435 $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST );
436 $from_email = 'wp2fa@';
437
438 if ( ! empty( $sitename ) ) {
439 $sitename = ltrim( $sitename, 'www.' );
440 $from_email .= sanitize_text_field( $sitename );
441 }
442
443 return sanitize_email( $from_email );
444 }
445
446 /**
447 * Turns user roles data in any form and shape to an array of strings.
448 *
449 * @param mixed $value User role names (slugs) as raw value.
450 *
451 * @return string[] List of user role names (slugs).
452 *
453 * @since 3.0.0
454 */
455 public static function extract_roles_from_input( $value ) {
456 if ( is_array( $value ) ) {
457 return array_map( 'sanitize_text_field', $value );
458 }
459
460 if ( is_string( $value ) && ! empty( $value ) ) {
461 $roles = explode( ',', $value );
462 return array_map( 'sanitize_text_field', $roles );
463 }
464
465 return array();
466 }
467
468 /**
469 * Checks the email against the current domain and shows an error message if they do not match.
470 *
471 * @return void
472 *
473 * @since 2.6.0
474 */
475 public static function check_email() {
476 $is_dismissed = (bool) Settings_Utils::get_option( 'dismiss_notice_mail_domain', false );
477 if ( ! $is_dismissed ) {
478 $admin_email = null;
479 if ( 'use-custom-email' === WP2FA::get_wp2fa_email_templates( 'email_from_setting' ) ) {
480 $admin_email = sanitize_email( WP2FA::get_wp2fa_email_templates( 'custom_from_email_address' ) );
481 } else {
482 $admin_email = self::get_default_email_address();
483 }
484
485 if ( '' === trim( (string) $admin_email ) ) {
486 $email_settings_url = esc_url(
487 add_query_arg(
488 array(
489 'page' => 'wp-2fa-settings',
490 'tab' => 'email-settings',
491 ),
492 network_admin_url( 'admin.php' )
493 )
494 );
495 ?>
496 <div class="notice notice-error" style="padding-top: 10px; padding-bottom: 10px;">
497 <p class="description"><?php esc_html_e( 'By default, the plugin uses ', 'wp-2fa' ); ?> <b><?php echo sanitize_email( self::get_default_email_address() ); ?></b> <?php esc_html_e( 'as the "from address" when sending emails with the 2FA code for users to log in. Do you want to keep using this or change it?', 'wp-2fa' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></p>
498 <p>
499 <a class="button button-primary" href="<?php echo esc_url( $email_settings_url ); ?>"><?php esc_html_e( 'Change it', 'wp-2fa' ); ?></a>
500 <a class="button button-secondary 2fa-email-notice" style="margin-left:20px" href="#">
501 <?php esc_html_e( 'Keep using it', 'wp-2fa' ); ?>
502 </a>
503 </p>
504
505 <?php wp_nonce_field( 'wp2fa_dismiss_notice_mail_domain', 'wp2fa_dismiss_notice_mail_domain', false ); ?>
506 </div>
507 <?php
508 } else {
509 Settings_Utils::update_option( 'dismiss_notice_mail_domain', true );
510 }
511 }
512 }
513
514 /**
515 * Sets the email domain do not match setting as dismissed.
516 *
517 * @return void
518 *
519 * @since 2.6.0
520 */
521 public static function dismiss_notice_mail_domain() {
522 // Verify nonce.
523 if ( isset( $_POST['nonce'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ), 'wp2fa_dismiss_notice_mail_domain' ) ) {
524 Settings_Utils::update_option( 'dismiss_notice_mail_domain', true );
525 die();
526 }
527
528 die( 'Nonce verification failed!' );
529 }
530 }
531 }
532