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 / class-settings-page.php
wp-2fa / includes / classes / Admin Last commit date
AdminSettings 18 hours ago Controllers 18 hours ago Fly-Out 18 hours ago Helpers 18 hours ago Methods 18 hours ago Settings 18 hours ago SettingsPages 18 hours ago Views 18 hours ago class-about-us.php 18 hours ago class-docs-and-support.php 18 hours ago class-free-support-notice.php 18 hours ago class-help-contact-us.php 18 hours ago class-license-page.php 18 hours ago class-new-interface-notice.php 18 hours ago class-plugin-updated-notice.php 18 hours ago class-premium-features.php 18 hours ago class-profile-section-renderer.php 18 hours ago class-settings-page.php 18 hours ago class-setup-wizard.php 18 hours ago class-top-bar-banner.php 18 hours ago class-user-listing.php 18 hours ago class-user-notices.php 18 hours ago class-user-profile.php 18 hours ago class-user-registered.php 18 hours ago class-wizard-integration.php 18 hours ago index.php 18 hours ago
class-settings-page.php
1103 lines
1 <?php
2 /**
3 * Settings rendering class.
4 *
5 * @package wp2fa
6 * @subpackage settings
7 * @copyright 2026 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_Policies_New,
18 Settings_Page_General,
19 Settings_Page_Email,
20 Settings_Page_New
21 };
22 use WP2FA\Utils\Settings_Utils;
23 use WP2FA\Admin\Controllers\Settings;
24 use WP2FA\Licensing\Licensing_Factory;
25 use WP2FA\Admin\Helpers\Email_Templates;
26 use WP2FA\Admin\SettingsPages\Settings_Page_Render;
27 use WP2FA\Admin\SettingsPages\Settings_Page_Passkeys;
28 use WP2FA\Admin\SettingsPages\Settings_Page_White_Label;
29 use WP2FA\Admin\SettingsPages\Settings_Page_White_Labeling_New;
30
31 /**
32 * Class for handling settings
33 */
34 if ( ! class_exists( '\WP2FA\Admin\Settings_Page' ) ) {
35 /**
36 * Class for handling settings
37 */
38 class Settings_Page {
39
40 const TOP_MENU_SLUG = 'wp-2fa-policies';
41
42 /**
43 * Checks if the new interface is enabled.
44 *
45 * Returns true for fresh installs (setting not yet stored).
46 * Returns false for upgrades (migration sets it to false).
47 *
48 * @return bool
49 *
50 * @since 4.0.0
51 */
52 public static function is_new_interface_enabled(): bool {
53 $setting = WP2FA::get_wp2fa_general_setting( 'use_new_interface', true );
54
55 // Fresh installs: setting is not in DB yet → default to enabled.
56 if ( null === $setting || '' === $setting ) {
57 return true;
58 }
59
60 return Settings_Utils::string_to_bool( $setting );
61 }
62
63 /**
64 * Redirects users to the correct settings page when the interface mode changes.
65 *
66 * After toggling "Use new interface", WordPress redirects back to the
67 * previous page slug which no longer exists – causing a white-screen.
68 * This method runs on admin_init (before the page-access check) and
69 * sends the user to the matching page.
70 *
71 * @return void
72 *
73 * @since 4.0.0
74 */
75 public static function redirect_on_interface_switch() {
76 if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
77 return;
78 }
79
80 $page = \sanitize_text_field( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
81 $use_new_interface = self::is_new_interface_enabled();
82 $admin_url_func = \is_network_admin() ? 'network_admin_url' : 'admin_url';
83
84 // Old settings page requested but new interface is active → redirect.
85 if ( 'wp-2fa-settings' === $page && $use_new_interface ) {
86 \wp_safe_redirect( $admin_url_func( 'admin.php?page=wp-2fa-settings-new&settings-updated=true' ) );
87 exit;
88 }
89
90 // New settings page requested but old interface is active → redirect.
91 if ( 'wp-2fa-settings-new' === $page && ! $use_new_interface ) {
92 \wp_safe_redirect( $admin_url_func( 'admin.php?page=wp-2fa-settings&settings-updated=true' ) );
93 exit;
94 }
95 }
96
97 /**
98 * Fallback redirect callback for a settings page whose slug is still
99 * registered but belongs to the inactive interface mode.
100 *
101 * Registered as the render callback of the hidden page so that if
102 * admin_init redirect did not fire (e.g. cached redirect from options.php),
103 * the user is still sent to the correct page instead of seeing a blank screen.
104 *
105 * @return void
106 *
107 * @since 4.0.0
108 */
109 public static function redirect_to_active_settings_page() {
110 $use_new_interface = self::is_new_interface_enabled();
111 $target_page = $use_new_interface ? 'wp-2fa-settings-new' : 'wp-2fa-settings';
112 $admin_url_func = \is_network_admin() ? 'network_admin_url' : 'admin_url';
113
114 \wp_safe_redirect( $admin_url_func( 'admin.php?page=' . $target_page . '&settings-updated=true' ) );
115 exit;
116 }
117
118 /**
119 * Create admin menu entry and settings page
120 */
121 public static function create_settings_admin_menu() {
122 $use_new_interface = self::is_new_interface_enabled();
123
124 // Create admin menu item.
125 \add_menu_page(
126 \esc_html__( 'WP 2FA', 'wp-2fa' ),
127 \esc_html__( 'WP 2FA', 'wp-2fa' ),
128 'manage_options',
129 self::TOP_MENU_SLUG,
130 null,
131 ' data:image/svg+xml;base64,' . base64_encode( file_get_contents( WP_2FA_PATH . 'dist/images/wp-2fa-white-icon20x28.svg' ) ),
132 81
133 );
134
135 if ( $use_new_interface ) {
136 // New interface: "Policies New" takes the place of the default policies page.
137 \add_submenu_page(
138 self::TOP_MENU_SLUG,
139 \esc_html__( '2FA policies', 'wp-2fa' ),
140 \esc_html__( '2FA policies', 'wp-2fa' ),
141 'manage_options',
142 self::TOP_MENU_SLUG,
143 array( Settings_Page_Policies_New::class, 'render' ),
144 1
145 );
146 } else {
147 // Old interface: original policies page.
148 \add_submenu_page(
149 self::TOP_MENU_SLUG,
150 \esc_html__( '2FA policies', 'wp-2fa' ),
151 \esc_html__( '2FA policies', 'wp-2fa' ),
152 'manage_options',
153 self::TOP_MENU_SLUG,
154 array( Settings_Page_Policies::class, 'render' ),
155 1
156 );
157 }
158
159 \add_submenu_page(
160 self::TOP_MENU_SLUG,
161 \esc_html__( 'Passkeys', 'wp-2fa' ),
162 \esc_html__( 'Passkeys', 'wp-2fa' ),
163 'manage_options',
164 Settings_Page_Passkeys::TOP_MENU_SLUG,
165 array( Settings_Page_Passkeys::class, 'render' ),
166 1
167 );
168
169 if ( ! $use_new_interface ) {
170 // Old interface: show old settings page.
171 \add_submenu_page(
172 self::TOP_MENU_SLUG,
173 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
174 \esc_html__( 'Settings', 'wp-2fa' ),
175 'manage_options',
176 'wp-2fa-settings',
177 array( Settings_Page_Render::class, 'render' ),
178 2
179 );
180
181 // Old interface: also show new pages as separate items.
182 // \add_submenu_page(
183 // self::TOP_MENU_SLUG,
184 // \esc_html__( 'Settings New', 'wp-2fa' ),
185 // \esc_html__( 'Settings New', 'wp-2fa' ),
186 // 'manage_options',
187 // 'wp-2fa-settings-new',
188 // array( Settings_Page_New::class, 'render' ),
189 // 3
190 // );
191
192 // \add_submenu_page(
193 // self::TOP_MENU_SLUG,
194 // \esc_html__( '2FA policies New', 'wp-2fa' ),
195 // \esc_html__( '2FA policies New', 'wp-2fa' ),
196 // 'manage_options',
197 // Settings_Page_Policies_New::PAGE_SLUG,
198 // array( Settings_Page_Policies_New::class, 'render' ),
199 // 4
200 // );
201 } else {
202 // New interface: separate White labeling sub-menu.
203 \add_submenu_page(
204 self::TOP_MENU_SLUG,
205 \esc_html__( 'White labeling', 'wp-2fa' ),
206 \esc_html__( 'White labeling', 'wp-2fa' ),
207 'manage_options',
208 Settings_Page_White_Labeling_New::PAGE_SLUG,
209 array( Settings_Page_White_Labeling_New::class, 'render' ),
210 2
211 );
212
213 // New interface: show Settings New as the main "Settings" page.
214 \add_submenu_page(
215 self::TOP_MENU_SLUG,
216 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
217 \esc_html__( 'Settings', 'wp-2fa' ),
218 'manage_options',
219 'wp-2fa-settings-new',
220 array( Settings_Page_New::class, 'render' ),
221 4
222 );
223
224 // Keep old slug registered (hidden) so options.php redirects
225 // back to it don't hit a "not allowed" dead end.
226 \add_submenu_page(
227 '',
228 '',
229 '',
230 'manage_options',
231 'wp-2fa-settings',
232 array( __CLASS__, 'redirect_to_active_settings_page' )
233 );
234 }
235
236 // Register our policy settings.
237 \register_setting(
238 WP_2FA_POLICY_SETTINGS_NAME,
239 WP_2FA_POLICY_SETTINGS_NAME,
240 array( Settings_Page_Policies::class, 'validate_and_sanitize' )
241 );
242
243 // Register our passkeys settings.
244 \register_setting(
245 WP_2FA_PASSKEYS_SETTINGS_NAME,
246 WP_2FA_PASSKEYS_SETTINGS_NAME,
247 array( Settings_Page_Passkeys::class, 'validate_and_sanitize' )
248 );
249
250 // Register our white label settings.
251 \register_setting(
252 WP_2FA_WHITE_LABEL_SETTINGS_NAME,
253 WP_2FA_WHITE_LABEL_SETTINGS_NAME,
254 array( Settings_Page_White_Label::class, 'validate_and_sanitize' )
255 );
256
257 // Register our settings page.
258 \register_setting(
259 WP_2FA_SETTINGS_NAME,
260 WP_2FA_SETTINGS_NAME,
261 array( Settings_Page_General::class, 'validate_and_sanitize' )
262 );
263
264 \register_setting(
265 WP_2FA_EMAIL_SETTINGS_NAME,
266 WP_2FA_EMAIL_SETTINGS_NAME,
267 array( Settings_Page_Email::class, 'validate_and_sanitize' )
268 );
269
270 // Register our "Settings New" option so Settings API calls on this page work.
271 \register_setting(
272 WP_2FA_NEW_SETTINGS_NAME,
273 WP_2FA_NEW_SETTINGS_NAME,
274 array( Settings_Page_New::class, 'validate_and_sanitize' )
275 );
276
277 /**
278 * Fires after the main menu settings are registered.
279 *
280 * @param string - The menu slug.
281 * @param bool - Is that multisite install or not.
282 *
283 * @since 2.0.0
284 */
285 \do_action( WP_2FA_PREFIX . 'after_admin_menu_created', self::TOP_MENU_SLUG, false );
286
287 self::maybe_add_locked_reports_menu();
288
289 \add_action( WP_2FA_PREFIX . 'before_plugin_settings', array( __CLASS__, 'check_email' ) );
290 }
291
292 /**
293 * Create admin menu entry and settings page
294 */
295 public static function create_settings_admin_menu_multisite() {
296 $use_new_interface = self::is_new_interface_enabled();
297
298 // Create admin menu item.
299 \add_menu_page(
300 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
301 \esc_html__( 'WP 2FA', 'wp-2fa' ),
302 'manage_options',
303 self::TOP_MENU_SLUG,
304 null,
305 'data:image/svg+xml;base64,' . base64_encode( file_get_contents( WP_2FA_PATH . 'dist/images/wp-2fa-white-icon20x28.svg' ) ),
306 81
307 );
308
309 if ( $use_new_interface ) {
310 // New interface: "Policies New" takes the place of the default policies page.
311 \add_submenu_page(
312 self::TOP_MENU_SLUG,
313 \esc_html__( '2FA policies', 'wp-2fa' ),
314 \esc_html__( '2FA policies', 'wp-2fa' ),
315 'manage_options',
316 self::TOP_MENU_SLUG,
317 array( Settings_Page_Policies_New::class, 'render' ),
318 1
319 );
320 } else {
321 // Old interface: original policies page.
322 \add_submenu_page(
323 self::TOP_MENU_SLUG,
324 \esc_html__( '2FA policies', 'wp-2fa' ),
325 \esc_html__( '2FA policies', 'wp-2fa' ),
326 'manage_options',
327 self::TOP_MENU_SLUG,
328 array( Settings_Page_Policies::class, 'render' ),
329 1
330 );
331 }
332
333 \add_submenu_page(
334 self::TOP_MENU_SLUG,
335 \esc_html__( 'Passkeys', 'wp-2fa' ),
336 \esc_html__( 'Passkeys', 'wp-2fa' ),
337 'manage_options',
338 Settings_Page_Passkeys::TOP_MENU_SLUG,
339 array( Settings_Page_Passkeys::class, 'render' ),
340 1
341 );
342
343 if ( ! $use_new_interface ) {
344 // Old interface: show old settings page.
345 \add_submenu_page(
346 self::TOP_MENU_SLUG,
347 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
348 \esc_html__( 'Settings', 'wp-2fa' ),
349 'manage_options',
350 'wp-2fa-settings',
351 array( Settings_Page_Render::class, 'render' ),
352 2
353 );
354
355 // Old interface: also show new pages as separate items.
356 // \add_submenu_page(
357 // self::TOP_MENU_SLUG,
358 // \esc_html__( 'Settings New', 'wp-2fa' ),
359 // \esc_html__( 'Settings New', 'wp-2fa' ),
360 // 'manage_options',
361 // 'wp-2fa-settings-new',
362 // array( Settings_Page_New::class, 'render' ),
363 // 3
364 // );
365
366 // \add_submenu_page(
367 // self::TOP_MENU_SLUG,
368 // \esc_html__( '2FA policies New', 'wp-2fa' ),
369 // \esc_html__( '2FA policies New', 'wp-2fa' ),
370 // 'manage_options',
371 // Settings_Page_Policies_New::PAGE_SLUG,
372 // array( Settings_Page_Policies_New::class, 'render' ),
373 // 4
374 // );
375 } else {
376 // New interface: separate White labeling sub-menu.
377 \add_submenu_page(
378 self::TOP_MENU_SLUG,
379 \esc_html__( 'White labeling', 'wp-2fa' ),
380 \esc_html__( 'White labeling', 'wp-2fa' ),
381 'manage_options',
382 Settings_Page_White_Labeling_New::PAGE_SLUG,
383 array( Settings_Page_White_Labeling_New::class, 'render' ),
384 2
385 );
386
387 // New interface: show Settings New as the main "Settings" page.
388 \add_submenu_page(
389 self::TOP_MENU_SLUG,
390 \esc_html__( 'WP 2FA Settings', 'wp-2fa' ),
391 \esc_html__( 'Settings', 'wp-2fa' ),
392 'manage_options',
393 'wp-2fa-settings-new',
394 array( Settings_Page_New::class, 'render' ),
395 4
396 );
397
398 // Keep old slug registered (hidden) so options.php redirects
399 // back to it don't hit a "not allowed" dead end.
400 \add_submenu_page(
401 null,
402 '',
403 '',
404 'manage_options',
405 'wp-2fa-settings',
406 array( __CLASS__, 'redirect_to_active_settings_page' )
407 );
408 }
409
410 /**
411 * Fires after the main menu settings are registered.
412 *
413 * @param string - The menu slug.
414 * @param bool - Is that multisite install or not.
415 *
416 * @since 2.0.0
417 */
418 \do_action( WP_2FA_PREFIX . 'after_admin_menu_created', self::TOP_MENU_SLUG, true );
419
420 self::maybe_add_locked_reports_menu();
421 }
422 /**
423 * Send account unlocked notification via email.
424 *
425 * @param int $user_id user ID.
426 *
427 * @return boolean
428 */
429 public static function send_account_unlocked_email( $user_id ) {
430 // Bail if the user has not enabled this email.
431 if ( 'enable_account_unlocked_email' !== Email_Templates::get_wp2fa_email_templates( 'send_account_unlocked_email' ) ) {
432 return false;
433 }
434
435 // Grab user data.
436 $user = get_userdata( $user_id );
437 // Grab user email.
438 $email = $user->user_email;
439 // Setup the email contents.
440 $subject = wp_strip_all_tags( Email_Templates::replace_email_strings( Email_Templates::get_wp2fa_email_templates( 'user_account_unlocked_email_subject' ) ) );
441 $message = wpautop( Email_Templates::replace_email_strings( Email_Templates::get_wp2fa_email_templates( 'user_account_unlocked_email_body' ), $user_id ) );
442
443 // @free:start
444 $message .= '<p>' . \esc_html__( 'Email sent by', 'wp-2fa' );
445 $message .= ' <a href="https://melapress.com/wordpress-2fa/?&utm_source=plugin&utm_medium=wp2fa&utm_campaign=melapress_wp_2fa_plugin_link" target="_blank">' . \esc_html__( 'WP 2FA plugin.', 'wp-2fa' ) . '</a>';
446 $message .= '</p>';
447 // @free:end
448
449 return self::send_email( $email, $subject, $message );
450 }
451
452 /**
453 * Hide settings menu item
454 */
455 public static function hide_settings() {
456 $user = wp_get_current_user();
457
458 // Check we have a user before doing anything else.
459 if ( is_a( $user, '\WP_User' ) ) {
460 if ( ! empty( WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' ) ) ) {
461 $main_user = (int) WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' );
462 } else {
463 $main_user = get_current_user_id();
464 }
465 if ( ! empty( WP2FA::get_wp2fa_general_setting( 'limit_access' ) ) && $user->ID !== $main_user ) {
466 // Remove legacy admin menu item.
467 remove_submenu_page( 'options-general.php', self::TOP_MENU_SLUG );
468
469 // Remove all plugin submenu pages.
470 remove_submenu_page( self::TOP_MENU_SLUG, self::TOP_MENU_SLUG );
471 remove_submenu_page( self::TOP_MENU_SLUG, Settings_Page_Passkeys::TOP_MENU_SLUG );
472 remove_submenu_page( self::TOP_MENU_SLUG, 'wp-2fa-settings' );
473 remove_submenu_page( self::TOP_MENU_SLUG, 'wp-2fa-settings-new' );
474 remove_submenu_page( self::TOP_MENU_SLUG, Settings_Page_White_Labeling_New::PAGE_SLUG );
475 remove_submenu_page( self::TOP_MENU_SLUG, Docs_And_Support::TOP_MENU_SLUG );
476 remove_submenu_page( self::TOP_MENU_SLUG, License_Page::PAGE_SLUG );
477
478 // Remove the top-level menu item itself.
479 remove_menu_page( self::TOP_MENU_SLUG );
480 }
481 }
482 }
483
484 /**
485 * Add unlock user link to user actions.
486 *
487 * @param array $links Default row content.
488 *
489 * @return array
490 * @throws \Freemius_Exception - freemius exception.
491 */
492 public static function add_plugin_action_links( $links ) {
493 // add link to the external free trial page in free version and also in premium version if license is not active.
494 if ( ! Licensing_Factory::has_active_valid_license() ) {
495 $trial_link = 'https://melapress.com/wordpress-2fa/pricing/?utm_source=plugin&utm_medium=wp2fa&utm_campaign=upgrade_to_premium_menu';
496 $links = array_merge(
497 array(
498 '<a style="font-weight:bold" href="' . $trial_link . '" target="_blank">' . __( 'Upgrade to Premium', 'wp-2fa' ) . '</a>',
499 ),
500 $links
501 );
502 }
503
504 // add link to the plugin settings page.
505 $url = Settings::get_settings_page_link();
506 $links = array_merge(
507 array(
508 '<a href="' . \esc_url( $url ) . '">' . \esc_html__( 'Configure 2FA Settings', 'wp-2fa' ) . '</a>',
509 ),
510 $links
511 );
512
513 return $links;
514 }
515
516 /**
517 * Updates options for multisite
518 *
519 * @return void
520 *
521 * @since 2.0.0
522 */
523 public static function update_wp2fa_network_options() {
524
525 Settings_Page_Policies::update_wp2fa_network_options();
526
527 Settings_Page_General::update_wp2fa_network_options();
528
529 Settings_Page_White_Label::update_wp2fa_network_options();
530
531 Settings_Page_Passkeys::update_wp2fa_network_options();
532
533 /**
534 * Gives the ability for extensions to set their settings in the plugin.
535 *
536 * @since 2.2.0
537 */
538 \do_action( WP_2FA_PREFIX . 'update_network_settings' );
539 }
540
541 /**
542 * Handle saving email options to the network main site options.
543 */
544 public static function update_wp2fa_network_email_options() {
545 Settings_Page_Email::update_wp2fa_network_options();
546 }
547
548 /**
549 * Stores a network admin notice in a site transient keyed to the current user.
550 * This replaces passing messages via URL parameters to prevent reflected content injection.
551 *
552 * @param string $type Notice type: 'success' or 'error'.
553 * @param string $message The notice message (already translated). Empty string for default error message.
554 *
555 * @return void
556 *
557 * @since 2.8.0
558 */
559 public static function set_network_admin_notice( string $type, string $message = '' ) {
560 \set_site_transient(
561 'wp_2fa_notice_' . \get_current_user_id(),
562 array(
563 'type' => $type,
564 'message' => $message,
565 ),
566 30
567 );
568 }
569
570 /**
571 * These are used instead of add_settings_error which in a network site. Used to show if settings have been updated or failed.
572 */
573 public static function settings_saved_network_admin_notice() {
574 $transient_key = 'wp_2fa_notice_' . \get_current_user_id();
575 $notice = \get_site_transient( $transient_key );
576
577 if ( ! $notice || ! is_array( $notice ) || ! isset( $notice['type'] ) ) {
578 return;
579 }
580
581 \delete_site_transient( $transient_key );
582
583 $type = $notice['type'];
584 $message = isset( $notice['message'] ) ? $notice['message'] : '';
585
586 if ( 'success' === $type ) {
587 ?>
588 <div class="notice notice-success is-dismissible wp-2fa-admin-notice">
589 <p><?php \esc_html_e( '2FA Settings Updated', 'wp-2fa' ); ?></p>
590 <button type="button" class="notice-dismiss">
591 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
592 </button>
593 </div>
594 <?php
595 } elseif ( 'error' === $type ) {
596 ?>
597 <div class="notice notice-error is-dismissible wp-2fa-admin-notice">
598 <?php if ( ! empty( $message ) ) { ?>
599 <p><?php echo \esc_html( $message ); ?></p>
600 <?php } else { ?>
601 <p><?php \esc_html_e( 'Please ensure both custom email address and display name are provided.', 'wp-2fa' ); ?></p>
602 <?php } ?>
603 <button type="button" class="notice-dismiss">
604 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
605 </button>
606 </div>
607 <?php
608 }
609 }
610
611 /**
612 * These are used instead of add_settings_error which in a network site. Used to show if settings have been updated or failed.
613 *
614 * @return void
615 *
616 * @since 2.0.0
617 */
618 public static function settings_saved_admin_notice() {
619 if ( isset( $_GET['page'] ) && 0 === strpos( \sanitize_text_field( \wp_unslash( $_GET['page'] ) ), 'wp-2fa-' ) ) {
620 if ( isset( $_GET['settings-updated'] ) && 'true' === $_GET['settings-updated'] ) {
621 $wp_settings_errors = get_settings_errors();
622
623 if ( count( $wp_settings_errors ) ) {
624 foreach ( $wp_settings_errors as $error ) {
625 ?>
626 <div class="notice notice-<?php echo \esc_attr( $error['type'] ); ?> is-dismissible wp-2fa-admin-notice">
627 <p><?php echo \esc_html( $error['message'] ); ?></p>
628 <button type="button" class="notice-dismiss">
629 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
630 </button>
631 </div>
632 <?php
633 }
634 } else {
635 ?>
636 <div class="notice notice-success is-dismissible wp-2fa-admin-notice">
637 <p><?php \esc_html_e( '2FA Settings Updated', 'wp-2fa' ); ?></p>
638 <button type="button" class="notice-dismiss">
639 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
640 </button>
641 </div>
642 <?php
643 }
644 }
645 if ( isset( $_GET['settings-updated'] ) && 'false' === $_GET['settings-updated'] ) {
646 ?>
647 <div class="notice notice-error is-dismissible wp-2fa-admin-notice">
648 <p><?php \esc_html_e( 'Please ensure both custom email address and display name are provided.', 'wp-2fa' ); ?></p>
649 <button type="button" class="notice-dismiss">
650 <span class="screen-reader-text"><?php \esc_html_e( 'Dismiss this notice.', 'wp-2fa' ); ?></span>
651 </button>
652 </div>
653 <?php
654 }
655 }
656 }
657
658 /**
659 * Add our custom state to our created page.
660 *
661 * @param array $post_states - array with the post states.
662 * @param WP_Post $post - the WP post.
663 *
664 * @return array
665 */
666 public static function add_display_post_states( $post_states, $post ) {
667 if ( ! empty( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) ) ) {
668 if ( WP2FA::get_wp2fa_setting( 'custom-user-page-id' ) === $post->ID ) {
669 $post_states['wp_2fa_page_for_user'] = __( 'WP 2FA User Page', 'wp-2fa' );
670 }
671 }
672
673 return $post_states;
674 }
675
676 /**
677 * Handles sending of an email. It sets necessary header such as content type and custom from email address and name.
678 *
679 * @param string $recipient_email Email address to send message to.
680 * @param string $subject Email subject.
681 * @param string $message Message contents.
682 *
683 * @return bool Whether the email contents were sent successfully.
684 */
685 public static function send_email( $recipient_email, $subject, $message ) {
686
687 // Sanitize email inputs.
688 $recipient_email = sanitize_email( $recipient_email );
689 $subject = str_replace( array( "\r", "\n" ), '', \sanitize_text_field( $subject ) );
690 $message = wp_kses_post( $message );
691
692 // Specify our desired headers.
693 $headers = 'Content-type: text/html;charset=utf-8' . "\r\n";
694
695 if ( 'use-custom-email' === Email_Templates::get_wp2fa_email_templates( 'email_from_setting' ) ) {
696 $from_name = sanitize_text_field( Email_Templates::get_wp2fa_email_templates( 'custom_from_display_name' ) );
697 $from_email = sanitize_email( Email_Templates::get_wp2fa_email_templates( 'custom_from_email_address' ) );
698 $headers .= 'From: ' . $from_name . ' <' . $from_email . '>' . "\r\n";
699 } else {
700 $headers .= 'From: WP 2FA from Melapress <' . self::get_default_email_address() . '>' . "\r\n";
701 }
702
703 // Fire our email.
704 return \wp_mail( $recipient_email, stripslashes_deep( html_entity_decode( $subject, ENT_QUOTES, 'UTF-8' ) ), $message, $headers );
705 }
706
707 /**
708 * Builds and returns the default email address used for the "from" email address when email is send
709 *
710 * @return string
711 *
712 * @since 2.6.4
713 */
714 public static function get_default_email_address(): string {
715 $sitename = wp_parse_url( network_home_url(), PHP_URL_HOST );
716 $from_email = 'wp2fa@';
717
718 if ( ! empty( $sitename ) ) {
719 if ( str_starts_with( $sitename, 'www.' ) ) {
720 $sitename = substr( $sitename, 4 );
721 }
722 $from_email .= sanitize_text_field( $sitename );
723 }
724
725 return sanitize_email( $from_email );
726 }
727
728 /**
729 * Turns user roles data in any form and shape to an array of strings.
730 *
731 * @param mixed $value User role names (slugs) as raw value.
732 *
733 * @return string[] List of user role names (slugs).
734 *
735 * @since 3.0.0
736 */
737 public static function extract_roles_from_input( $value ) {
738 if ( is_array( $value ) ) {
739 return array_map( 'sanitize_text_field', $value );
740 }
741
742 if ( is_string( $value ) && ! empty( $value ) ) {
743 $roles = explode( ',', $value );
744 return array_map( 'sanitize_text_field', $roles );
745 }
746
747 return array();
748 }
749
750 /**
751 * Checks the email against the current domain and shows an error message if they do not match.
752 *
753 * @return void
754 *
755 * @since 2.6.0
756 */
757 public static function check_email() {
758 $is_dismissed = (bool) Settings_Utils::get_option( 'dismiss_notice_mail_domain', false );
759 if ( ! $is_dismissed ) {
760 $admin_email = null;
761 if ( 'use-custom-email' === Email_Templates::get_wp2fa_email_templates( 'email_from_setting' ) ) {
762 $admin_email = sanitize_email( Email_Templates::get_wp2fa_email_templates( 'custom_from_email_address' ) );
763 } else {
764 $admin_email = self::get_default_email_address();
765 }
766
767 if ( '' === trim( (string) $admin_email ) ) {
768 $email_settings_url = esc_url(
769 add_query_arg(
770 array(
771 'page' => 'wp-2fa-settings',
772 'tab' => 'email-settings',
773 ),
774 network_admin_url( 'admin.php' )
775 )
776 );
777 ?>
778 <div class="notice notice-error wp-2fa-admin-notice">
779 <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>
780 <p>
781 <a class="button button-primary" href="<?php echo esc_url( $email_settings_url ); ?>"><?php esc_html_e( 'Change it', 'wp-2fa' ); ?></a>
782 <a class="button button-secondary wp-2fa-email-notice" style="margin-left:20px" href="#">
783 <?php esc_html_e( 'Keep using it', 'wp-2fa' ); ?>
784 </a>
785 </p>
786
787 <?php wp_nonce_field( 'wp2fa_dismiss_notice_mail_domain', 'wp2fa_dismiss_notice_mail_domain', false ); ?>
788 </div>
789 <?php
790 } else {
791 Settings_Utils::update_option( 'dismiss_notice_mail_domain', true );
792 }
793 }
794 }
795
796 /**
797 * Sets the email domain do not match setting as dismissed.
798 *
799 * @return void
800 *
801 * @since 2.6.0
802 */
803 public static function dismiss_notice_mail_domain() {
804 // Verify nonce.
805 if ( isset( $_POST['nonce'] ) && \wp_verify_nonce( \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ), 'wp2fa_dismiss_notice_mail_domain' ) ) {
806 Settings_Utils::update_option( 'dismiss_notice_mail_domain', true );
807 die();
808 }
809
810 die( 'Nonce verification failed!' );
811 }
812
813 /**
814 * Adds a locked "Reports" submenu item when no valid premium license is present.
815 *
816 * The menu item displays a lock icon and does nothing when clicked.
817 *
818 * @return void
819 *
820 * @since 4.0.0
821 */
822 public static function maybe_add_locked_reports_menu() {
823 if ( Licensing_Factory::has_active_valid_license() ) {
824 return;
825 }
826
827 $lock_icon = '<span class="wp2fa-menu-lock-icon" aria-hidden="true"></span>';
828
829 \add_submenu_page(
830 self::TOP_MENU_SLUG,
831 \esc_html__( 'Reports', 'wp-2fa' ),
832 \esc_html__( 'Reports', 'wp-2fa' ) . $lock_icon,
833 'manage_options',
834 'wp-2fa-reports-locked',
835 array( __CLASS__, 'render_locked_reports_page' ),
836 3
837 );
838
839 \add_action( 'admin_head', array( __CLASS__, 'locked_reports_menu_styles' ) );
840 }
841
842 /**
843 * Outputs CSS for the locked Reports menu item lock icon.
844 *
845 * @return void
846 *
847 * @since 4.0.0
848 */
849 public static function locked_reports_menu_styles() {
850 ?>
851 <style>
852 #adminmenu .wp-submenu a[href*="wp-2fa-reports-locked"] {
853 cursor: pointer;
854 position: relative;
855 display: flex !important;
856 align-items: center;
857 justify-content: space-between;
858 }
859 #adminmenu .wp-submenu a[href*="wp-2fa-reports-locked"]:hover {
860 color: #72aee6;
861 }
862 .wp2fa-menu-lock-icon {
863 display: inline-flex;
864 align-items: center;
865 justify-content: center;
866 width: 20px;
867 height: 20px;
868 background: #2c3338;
869 border-radius: 4px;
870 flex-shrink: 0;
871 margin-left: 8px;
872 }
873 #adminmenu .wp-submenu li.current a[href*="wp-2fa-reports-locked"] .wp2fa-menu-lock-icon,
874 #adminmenu .wp-submenu a.current[href*="wp-2fa-reports-locked"] .wp2fa-menu-lock-icon {
875 background: #2271b1;
876 }
877 .wp2fa-menu-lock-icon::before {
878 content: '';
879 display: block;
880 width: 12px;
881 height: 12px;
882 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='%23c3c4c7'%3E%3Cpath d='M18 10h-1V7c0-2.8-2.2-5-5-5S7 4.2 7 7v3H6c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zM9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v3H9V7zm9 13H6v-8h12v8zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z'/%3E%3C/svg%3E");
883 background-size: contain;
884 background-repeat: no-repeat;
885 background-position: center;
886 }
887 #adminmenu .wp-submenu li.current a[href*="wp-2fa-reports-locked"] .wp2fa-menu-lock-icon::before,
888 #adminmenu .wp-submenu a.current[href*="wp-2fa-reports-locked"] .wp2fa-menu-lock-icon::before {
889 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M18 10h-1V7c0-2.8-2.2-5-5-5S7 4.2 7 7v3H6c-1.1 0-2 .9-2 2v8c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2v-8c0-1.1-.9-2-2-2zM9 7c0-1.7 1.3-3 3-3s3 1.3 3 3v3H9V7zm9 13H6v-8h12v8zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z'/%3E%3C/svg%3E");
890 }
891
892 .wp2fa-reports-teaser-wrap {
893 max-width: 100%;
894 margin-top: 20px;
895 margin-right: 20px;
896 }
897 .wp2fa-reports-teaser-card {
898 display: grid;
899 grid-template-columns: 1fr 1fr;
900 gap: 36px;
901 background: #fff;
902 border: 1px solid #dcdcde;
903 border-radius: 8px;
904 padding: 28px;
905 }
906 .wp2fa-reports-teaser-title-wrap {
907 display: flex;
908 align-items: center;
909 gap: 14px;
910 margin-bottom: 18px;
911 }
912 .wp2fa-reports-teaser-icon {
913 width: 52px;
914 height: 52px;
915 border-radius: 8px;
916 object-fit: contain;
917 }
918 .wp2fa-reports-teaser-title {
919 margin: 0;
920 font-size: 22px;
921 line-height: 1.22;
922 font-weight: 700;
923 color: #3c434a;
924 }
925 .wp2fa-reports-teaser-content {
926 font-size: 14px;
927 }
928 .wp2fa-reports-teaser-title span {
929 color: #2271b1;
930 }
931 .wp2fa-reports-teaser-description {
932 margin: 0 0 26px;
933 font-size: 14px;
934 line-height: 1.45;
935 font-weight: 400;
936 color: #50575e;
937 }
938 .wp2fa-reports-teaser-list {
939 list-style: none;
940 margin: 0 0 28px;
941 padding: 0;
942 display: flex;
943 flex-direction: column;
944 gap: 14px;
945 }
946 .wp2fa-reports-teaser-list li {
947 display: flex;
948 align-items: flex-start;
949 gap: 10px;
950 }
951 .wp2fa-reports-teaser-list-icon {
952 width: 32px;
953 height: 32px;
954 border-radius: 50%;
955 background: #2271b1;
956 flex-shrink: 0;
957 margin-top: 2px;
958 position: relative;
959 }
960 .wp2fa-reports-teaser-list-icon::before {
961 content: '';
962 position: absolute;
963 top: 50%;
964 left: 50%;
965 width: 17px;
966 height: 17px;
967 transform: translate(-50%, -50%);
968 background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'%3E%3Cpath d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41L9 16.17z'/%3E%3C/svg%3E");
969 background-repeat: no-repeat;
970 background-size: contain;
971 }
972 .wp2fa-reports-teaser-point-title {
973 display: block;
974 font-size: 16px;
975 line-height: 1.2;
976 font-weight: 600;
977 color: #3c434a;
978 }
979 .wp2fa-reports-teaser-point-description {
980 display: block;
981 margin-top: 2px;
982 font-size: 14px;
983 line-height: 1.34;
984 color: #646970;
985 }
986 .wp2fa-reports-teaser-cta {
987 display: inline-block;
988 padding: 9px 18px;
989 border-radius: 7px;
990 background: #2271b1;
991 border: 1px solid #2271b1;
992 color: #fff;
993 font-size: 17px;
994 font-weight: 500;
995 text-decoration: none;
996 }
997 .wp2fa-reports-teaser-cta:hover,
998 .wp2fa-reports-teaser-cta:focus {
999 background: #135e96;
1000 border-color: #135e96;
1001 color: #fff;
1002 }
1003 .wp2fa-reports-teaser-preview {
1004 background: #efeff0;
1005 border-radius: 10px;
1006 padding: 16px;
1007 overflow: hidden;
1008 }
1009 .wp2fa-reports-teaser-preview img {
1010 width: 100%;
1011 height: auto;
1012 display: block;
1013 border-radius: 8px;
1014 opacity: 0.94;
1015 margin-left: 60px;
1016 }
1017
1018 @media screen and (max-width: 980px) {
1019 .wp2fa-reports-teaser-card {
1020 grid-template-columns: 1fr;
1021 }
1022 .wp2fa-reports-teaser-preview img {
1023 margin-left: 0;
1024 }
1025 }
1026 </style>
1027 <?php
1028 }
1029
1030 /**
1031 * Renders teaser page for the locked reports menu item.
1032 *
1033 * @return void
1034 *
1035 * @since 4.0.0
1036 */
1037 public static function render_locked_reports_page() {
1038 if ( ! \current_user_can( 'manage_options' ) ) {
1039 return;
1040 }
1041
1042 ?>
1043 <div class="wp2fa-reports-teaser-wrap">
1044 <div class="wp2fa-reports-teaser-card">
1045 <div class="wp2fa-reports-teaser-content">
1046 <div class="wp2fa-reports-teaser-title-wrap">
1047 <svg class="wp2fa-reports-teaser-icon" xmlns="http://www.w3.org/2000/svg" width="33" height="49" viewBox="0 0 33 49" fill="none" aria-hidden="true" focusable="false">
1048 <path d="M10.7607 16.2846L18.7944 8.17949L26.9017 0H13.414H0V8.17949V16.2846L2.65332 13.6077L5.38035 10.8564L8.03367 13.6077L10.7607 16.2846Z" fill="#99FFFF"/>
1049 <path d="M32.2094 48.9278V37.997V27.1406L21.5224 37.997L10.7617 48.7791L21.5224 48.8534L32.2094 48.9278Z" fill="#3E6BFF"/>
1050 <path d="M10.7607 27.1406L8.03367 24.3893L5.38035 21.7124L2.65332 18.9611L0 16.2841V27.1406V37.997L2.65332 35.2457L5.38035 32.5688L8.03367 35.2457L10.7607 37.997L18.7944 29.8175L26.9017 21.7124L29.555 24.3893L32.2084 27.1406V16.2841V5.42773L21.5214 16.2841L10.7607 27.1406Z" fill="#40D3F0"/>
1051 </svg>
1052 <h1 class="wp2fa-reports-teaser-title"><?php echo \esc_html__( 'Unlock 2FA Reports & Statistics with Premium', 'wp-2fa' ); ?></h1>
1053 </div>
1054
1055 <p class="wp2fa-reports-teaser-description"><?php echo \esc_html__( 'Get a clear overview of two-factor authentication adoption across your website and identify users who are not yet protected.', 'wp-2fa' ); ?></p>
1056 <p class="wp2fa-reports-teaser-description"><?php echo \esc_html__( 'WP 2FA Premium includes detailed reports and statistics that help you monitor 2FA usage, track adoption by user role, review authentication methods in use, and export data for auditing and compliance purposes.', 'wp-2fa' ); ?></p>
1057
1058 <ul class="wp2fa-reports-teaser-list">
1059 <li>
1060 <span class="wp2fa-reports-teaser-list-icon" aria-hidden="true"></span>
1061 <span>
1062 <span class="wp2fa-reports-teaser-point-description"><?php echo \esc_html__( 'Monitor 2FA adoption across your entire website', 'wp-2fa' ); ?></span>
1063 </span>
1064 </li>
1065 <li>
1066 <span class="wp2fa-reports-teaser-list-icon" aria-hidden="true"></span>
1067 <span>
1068 <span class="wp2fa-reports-teaser-point-description"><?php echo \esc_html__( 'Identify users and roles that have not yet configured 2FA', 'wp-2fa' ); ?></span>
1069 </span>
1070 </li>
1071 <li>
1072 <span class="wp2fa-reports-teaser-list-icon" aria-hidden="true"></span>
1073 <span>
1074 <span class="wp2fa-reports-teaser-point-description"><?php echo \esc_html__( 'Track which authentication methods users are relying on', 'wp-2fa' ); ?></span>
1075 </span>
1076 </li>
1077 <li>
1078 <span class="wp2fa-reports-teaser-list-icon" aria-hidden="true"></span>
1079 <span>
1080 <span class="wp2fa-reports-teaser-point-description"><?php echo \esc_html__( 'Export reports as CSV for auditing, compliance, and internal reviews', 'wp-2fa' ); ?></span>
1081 </span>
1082 </li>
1083 <li>
1084 <span class="wp2fa-reports-teaser-list-icon" aria-hidden="true"></span>
1085 <span>
1086 <span class="wp2fa-reports-teaser-point-description"><?php echo \esc_html__( 'Measure the impact of your 2FA rollout and security policies', 'wp-2fa' ); ?></span>
1087 </span>
1088 </li>
1089 </ul>
1090
1091 <a class="wp2fa-reports-teaser-cta" href="<?php echo \esc_url( 'https://melapress.com/wordpress-2fa/?utm_source=plugin&utm_medium=wp2fa&utm_campaign=free-reports-page' ); ?>" target="_blank" rel="noopener noreferrer"><?php echo \esc_html__( 'Unlock 2FA Reports & Statistics', 'wp-2fa' ); ?></a>
1092 </div>
1093
1094 <div class="wp2fa-reports-teaser-preview" aria-hidden="true">
1095 <img src="<?php echo \esc_url( WP_2FA_URL . 'includes/assets/images/reports-teaser-preview.png' ); ?>" alt="<?php echo \esc_attr__( 'Reports page preview', 'wp-2fa' ); ?>" />
1096 </div>
1097 </div>
1098 </div>
1099 <?php
1100 }
1101 }
1102 }
1103