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 / SettingsPages / class-settings-page-new.php
wp-2fa / includes / classes / Admin / SettingsPages Last commit date
class-settings-page-email.php 5 days ago class-settings-page-general.php 5 days ago class-settings-page-new.php 5 days ago class-settings-page-passkeys.php 5 days ago class-settings-page-policies-new.php 5 days ago class-settings-page-policies.php 5 days ago class-settings-page-render.php 5 days ago class-settings-page-white-label.php 5 days ago class-settings-page-white-labeling-new.php 5 days ago class-setup-wizard-new.php 5 days ago index.php 5 days ago
class-settings-page-new.php
509 lines
1 <?php
2 /**
3 * Settings New - a new settings page implemented as a static settings class.
4 *
5 * @package wp2fa
6 * @subpackage settings-pages
7 */
8
9 namespace WP2FA\Admin\SettingsPages;
10
11 use WP2FA\WP2FA;
12 use WP2FA\Utils\Settings_Utils;
13 use WP2FA\Admin\Settings_Page;
14 use WP2FA\Admin\SettingsPages\Settings_Page_General;
15 use WP2FA\Admin\SettingsPages\Settings_Page_White_Label;
16 use WP2FA\Admin\Helpers\Email_Templates;
17
18 if ( ! class_exists( '\WP2FA\Admin\SettingsPages\Settings_Page_New' ) ) {
19 /**
20 * Class Settings_Page_New
21 *
22 * @package WP2FA\Admin\SettingsPages
23 */
24 class Settings_Page_New {
25
26 public const PAGE_SLUG = 'wp-2fa-settings-new';
27
28 /**
29 * Initialize hooks for this settings page.
30 *
31 * @since 3.1.1.2
32 */
33 public static function init() {
34
35 // Register AJAX handlers — must be outside any page-specific hook.
36 \add_action( 'wp_ajax_wp2fa_save_settings_new', array( __CLASS__, 'ajax_save' ) );
37 \add_action( 'wp_ajax_wp2fa_send_test_email_new', array( __CLASS__, 'ajax_send_test_email' ) );
38
39 \add_action(
40 'admin_enqueue_scripts',
41 function ( $hook ) {
42 if ( 'wp-2fa_page_' . constant( __CLASS__ . '::PAGE_SLUG' ) === $hook ) {
43 \wp_enqueue_style(
44 'wp_2fa_settings_new_css',
45 WP_2FA_URL . 'includes/assets/css/' . \sanitize_file_name( 'settings' ) . '.css',
46 array(),
47 WP_2FA_VERSION
48 );
49
50 // Color picker (WP core).
51 \wp_enqueue_style( 'wp-color-picker' );
52 \wp_enqueue_script( 'wp-color-picker' );
53
54 // Media uploader (WP core).
55 \wp_enqueue_media();
56
57 // Customize styles JS.
58 \wp_enqueue_script(
59 'wp_2fa_customize_styles',
60 WP_2FA_URL . 'includes/assets/js/customize-styles.js',
61 array( 'jquery', 'wp-color-picker' ),
62 WP_2FA_VERSION,
63 true
64 );
65
66 \wp_localize_script(
67 'wp_2fa_customize_styles',
68 'wp2faCustomizeStyles',
69 array(
70 'selectMediaTitle' => \esc_html__( 'Select Image', 'wp-2fa' ),
71 'selectMediaButton' => \esc_html__( 'Use this image', 'wp-2fa' ),
72 )
73 );
74
75 // Save-settings JS (vanilla, no jQuery dependency).
76 \wp_enqueue_script(
77 'wp_2fa_save_settings_new',
78 WP_2FA_URL . 'includes/assets/js/save-settings-new.js',
79 array(),
80 WP_2FA_VERSION,
81 true
82 );
83
84 // Hash-state JS — preserves section / sub-tab / accordion
85 // state in the URL fragment so a refresh or shared link
86 // returns to the same view.
87 \wp_enqueue_script(
88 'wp_2fa_settings_hash_state',
89 WP_2FA_URL . 'includes/assets/js/settings-hash-state.js',
90 array(),
91 WP_2FA_VERSION,
92 true
93 );
94
95 // Test-email JS (vanilla, no jQuery dependency).
96 \wp_enqueue_script(
97 'wp_2fa_test_email_new',
98 WP_2FA_URL . 'includes/assets/js/test-email-new.js',
99 array(),
100 WP_2FA_VERSION,
101 true
102 );
103
104 \wp_localize_script(
105 'wp_2fa_test_email_new',
106 'wp2faTestEmail',
107 array(
108 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
109 'nonce' => \wp_create_nonce( 'wp2fa_send_test_email_new_nonce' ),
110 'sendingText' => \esc_html__( 'Sending…', 'wp-2fa' ),
111 'errorText' => \esc_html__( 'An error occurred. Please try again.', 'wp-2fa' ),
112 )
113 );
114
115 \wp_localize_script(
116 'wp_2fa_save_settings_new',
117 'wp2faSaveSettingsNew',
118 array(
119 'ajaxUrl' => \admin_url( 'admin-ajax.php' ),
120 'nonce' => \wp_create_nonce( 'wp2fa_save_settings_new_nonce' ),
121 'action' => 'wp2fa_save_settings_new',
122 'saveText' => \esc_html__( 'Save settings', 'wp-2fa' ),
123 'savingText' => \esc_html__( 'Saving…', 'wp-2fa' ),
124 'successText' => \esc_html__( 'Settings saved.', 'wp-2fa' ),
125 'errorText' => \esc_html__( 'An error occurred. Please try again.', 'wp-2fa' ),
126 'errorsModalTitle' => \esc_html__( 'Settings saved with warnings', 'wp-2fa' ),
127 'errorsModalClose' => \esc_html__( 'OK, I understand', 'wp-2fa' ),
128 )
129 );
130 }
131 }
132 );
133 Settings_Page_White_Label::init();
134 }
135
136 /**
137 * Render the settings screen.
138 */
139 public static function render() {
140 if ( ! \current_user_can( 'manage_options' ) ) {
141 return;
142 }
143
144 $main_user = ! empty( WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' ) ) ? (int) WP2FA::get_wp2fa_setting( '2fa_settings_last_updated_by' ) : \get_current_user_id();
145 if ( ! empty( WP2FA::get_wp2fa_general_setting( 'limit_access' ) ) && $main_user !== \get_current_user_id() ) {
146 echo \esc_html__( 'These settings have been disabled by your site administrator, please contact them for further assistance.', 'wp-2fa' );
147 return;
148 }
149
150 // Load saved values (single option array).
151 $values = Settings_Utils::get_option( WP_2FA_NEW_SETTINGS_NAME, array() );
152
153 // sensible defaults for this page.
154 $defaults = array(
155 'enabled' => false,
156 'layout' => 'cards',
157 'accent_color' => '#4776ff',
158 'example_text' => '',
159 );
160
161 $values = \wp_parse_args( $values, $defaults );
162
163 ?>
164 <div class="wp-2fa-settings-wrapper wp-2fa-settings-new">
165 <div class="page-header">
166 <!-- <h2><?php \esc_html_e( 'Settings New', 'wp-2fa' ); ?></h2> -->
167 <div class="page-header-actions">
168 <button type="button" class="button button-primary js-save-settings-new"><?php \esc_html_e( 'Save settings', 'wp-2fa' ); ?></button>
169 </div>
170 </div>
171
172 <div class="main-settings-new">
173 <div class="wrap main-left">
174 <?php
175 $is_white_labeling_separate = Settings_Page::is_new_interface_enabled();
176
177 foreach ( self::collect_settings_tabs() as $tab => $settings ) {
178 // When the White labeling page is separate, email-settings
179 // are rendered there instead of here.
180 if ( $is_white_labeling_separate && 'email-settings' === $tab ) {
181 continue;
182 }
183 ?>
184 <div id="wp-2fa-options-tab-<?php echo \esc_attr( $tab ); ?>" class="tabs-wrap">
185
186 <?php
187 include_once \WP_2FA_PATH . '/includes/classes/Admin/Settings/templates/' . $tab . '.php';
188 ?>
189
190 </div>
191 <?php
192 }
193 if ( ! $is_white_labeling_separate ) {
194 foreach ( self::register_white_labelling_settings( array() ) as $tab => $settings ) {
195 ?>
196 <div id="wp-2fa-options-tab-<?php echo \esc_attr( $tab ); ?>" class="tabs-wrap">
197
198 <?php
199 include_once \WP_2FA_PATH . '/includes/classes/Admin/Settings/templates/' . $tab . '.php';
200 ?>
201
202 </div>
203 <?php
204 }
205 }
206 ?>
207 </div>
208
209 <?php include WP_2FA_PATH . 'includes/classes/Admin/Settings/templates/sidebar.php'; ?>
210 </div>
211
212 <div class="save-footer">
213 <button type="button" class="button button-primary button-large js-save-settings-new"><?php \esc_html_e( 'Save settings', 'wp-2fa' ); ?></button>
214 </div>
215 </div>
216 <?php
217 }
218
219 /**
220 * AJAX handler for saving settings from the Settings New page.
221 *
222 * Called via wp_ajax_wp2fa_save_settings_new.
223 * Security: nonce verified first, then manage_options capability checked.
224 * Any data inside the WP_2FA_NEW_SETTINGS_NAME option group is sanitised
225 * through the existing validate_and_sanitize() method (which fires the
226 * wp2fa_settings_new_validate filter). Other settings present in the POST
227 * payload are handed off via the wp2fa_settings_new_ajax_save action so
228 * additional option groups can hook in and save themselves.
229 *
230 * @since 3.1.1.2
231 */
232 public static function ajax_save() {
233 // 1. Nonce check — must happen before any data access.
234 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : '';
235 if ( ! \wp_verify_nonce( $nonce, 'wp2fa_save_settings_new_nonce' ) ) {
236 \wp_send_json_error(
237 array( 'message' => \esc_html__( 'Security check failed. Please refresh the page and try again.', 'wp-2fa' ) ),
238 403
239 );
240 }
241
242 // 2. Capability check.
243 if ( ! \current_user_can( 'manage_options' ) ) {
244 \wp_send_json_error(
245 array( 'message' => \esc_html__( 'You do not have permission to perform this action.', 'wp-2fa' ) ),
246 403
247 );
248 }
249
250 if ( isset( $_POST[ WP_2FA_SETTINGS_NAME ] ) && is_array( $_POST[ WP_2FA_SETTINGS_NAME ] ) ) {
251 $existing_general = Settings_Utils::get_option( WP_2FA_SETTINGS_NAME, array() );
252 $options = Settings_Page_General::validate_and_sanitize( $_POST[ WP_2FA_SETTINGS_NAME ] ); // call the generic validate_and_sanitize to trigger its filter, in case any settings tabs use it.
253
254 // Merge validated output on top of existing settings, but only
255 // for keys actually submitted by the form — see the policy
256 // handler for a detailed explanation.
257 if ( \is_array( $options ) && \is_array( $existing_general ) ) {
258 $submitted_keys = \array_keys( $_POST[ WP_2FA_SETTINGS_NAME ] );
259 $merged = $existing_general;
260 foreach ( $options as $key => $value ) {
261 if ( \in_array( $key, $submitted_keys, true ) || ! \array_key_exists( $key, $existing_general ) ) {
262 $merged[ $key ] = $value;
263 }
264 }
265 $options = $merged;
266 }
267
268 WP2FA::update_plugin_settings( $options, false, WP_2FA_SETTINGS_NAME );
269 }
270
271 if ( isset( $_POST['email_from_setting'] ) ) {
272 $options = Settings_Page_Email::validate_and_sanitize_new( \wp_unslash( $_POST ) );
273
274 Settings_Utils::update_option( WP_2FA_EMAIL_SETTINGS_NAME, $options );
275 }
276
277 // 4. Allow other option groups rendered on this page to hook in and
278 // save their own data. Each hook receives the full (unslashed) POST
279 // payload so it can extract and sanitise its own sub-array.
280 \do_action( WP_2FA_PREFIX . 'settings_new_ajax_save', \wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
281
282 // 5. Collect any validation errors added during this request and return
283 // them so the JS layer can surface them to the user.
284 global $wp_settings_errors;
285 if ( ! empty( $wp_settings_errors ) ) {
286 $errors = array();
287 foreach ( $wp_settings_errors as $error ) {
288 $errors[] = array(
289 'code' => isset( $error['code'] ) ? \sanitize_key( $error['code'] ) : '',
290 'message' => isset( $error['message'] ) ? \wp_strip_all_tags( $error['message'] ) : '',
291 'type' => isset( $error['type'] ) ? \sanitize_key( $error['type'] ) : 'error',
292 );
293 }
294 \wp_send_json_success(
295 array(
296 'message' => esc_html__( 'Settings saved with warnings.', 'wp-2fa' ),
297 'errors' => $errors,
298 )
299 );
300 }
301
302 \wp_send_json_success(
303 array( 'message' => \esc_html__( 'Settings saved.', 'wp-2fa' ) )
304 );
305 }
306
307 /**
308 * AJAX handler for sending a test email from the Settings New page.
309 *
310 * If the request includes a subject and body (from the form's textarea /
311 * TinyMCE editor), the body is run through Email_Templates::replace_email_strings()
312 * so token placeholders are resolved, then sent via Settings_Page::send_email().
313 *
314 * When no body is provided (the generic "Test email delivery" button), a
315 * simple hard-coded test message is sent instead.
316 *
317 * @since 3.1.1.2
318 */
319 public static function ajax_send_test_email() {
320 // 1. Nonce check.
321 $nonce = isset( $_POST['nonce'] ) ? \sanitize_text_field( \wp_unslash( $_POST['nonce'] ) ) : '';
322 if ( ! \wp_verify_nonce( $nonce, 'wp2fa_send_test_email_new_nonce' ) ) {
323 \wp_send_json_error(
324 array( 'message' => \esc_html__( 'Security check failed. Please refresh the page and try again.', 'wp-2fa' ) ),
325 403
326 );
327 }
328
329 // 2. Capability check.
330 if ( ! \current_user_can( 'manage_options' ) ) {
331 \wp_send_json_error(
332 array( 'message' => \esc_html__( 'You do not have permission to perform this action.', 'wp-2fa' ) ),
333 403
334 );
335 }
336
337 // 3. Resolve the recipient — always the current user.
338 $user = \wp_get_current_user();
339 $email = \sanitize_email( $user->user_email );
340
341 if ( empty( $email ) ) {
342 \wp_send_json_error(
343 array( 'message' => \esc_html__( 'Your account does not have an email address.', 'wp-2fa' ) )
344 );
345 }
346
347 // 4. Determine subject & body.
348 $raw_subject = isset( $_POST['subject'] ) ? \sanitize_text_field( \wp_unslash( $_POST['subject'] ) ) : '';
349 $raw_body = isset( $_POST['body'] ) ? \wp_kses_post( \wp_unslash( $_POST['body'] ) ) : '';
350
351 if ( '' === $raw_body ) {
352 // Generic delivery test — no template content available.
353 $subject = \esc_html__( 'Test email from WP 2FA', 'wp-2fa' );
354 $message = \esc_html__( 'This email was sent by the WP 2FA plugin to test the email delivery.', 'wp-2fa' );
355 } else {
356 $subject = $raw_subject;
357 $message = \wpautop( Email_Templates::replace_email_strings( $raw_body, (string) $user->ID ) );
358 }
359
360 // 5. Send.
361 $sent = Settings_Page::send_email( $email, $subject, $message );
362
363 if ( $sent ) {
364 \wp_send_json_success(
365 array(
366 /* translators: %s: recipient email address */
367 'message' => \wp_sprintf( \esc_html__( 'Test email was successfully sent to %s', 'wp-2fa' ), '<strong>' . \esc_html( $email ) . '</strong>' ),
368 )
369 );
370 }
371
372 \wp_send_json_error(
373 array( 'message' => \esc_html__( 'Failed to send test email. This is usually caused by an SMTP issue, a restricted "from" address, or your host blocking outgoing mail. Check your email settings or contact your hosting provider.', 'wp-2fa' ) )
374 );
375 }
376
377 /**
378 * Validate and sanitize the options submitted from the Settings New screen.
379 *
380 * @param array $input Raw input array.
381 * @return array Sanitized output array.
382 */
383 public static function validate_and_sanitize( $input ) {
384 $out = array();
385
386 $out['enabled'] = ( isset( $input['enabled'] ) && (bool) $input['enabled'] ) ? true : false;
387 $out['layout'] = ( isset( $input['layout'] ) && in_array( $input['layout'], array( 'cards', 'compact' ), true ) ) ? $input['layout'] : 'cards';
388 $out['accent_color'] = ( isset( $input['accent_color'] ) ) ? \sanitize_hex_color( $input['accent_color'] ) : '#4776ff';
389 $out['example_text'] = ( isset( $input['example_text'] ) ) ? \sanitize_text_field( $input['example_text'] ) : '';
390
391 // Let other code alter/validate before we save.
392 $out = \apply_filters( WP_2FA_PREFIX . 'settings_new_validate', $out, $input );
393
394 return $out;
395 }
396
397 /**
398 * Save network-level options (if used in multisite).
399 */
400 public static function update_wp2fa_network_options() {
401 if ( ! \current_user_can( 'manage_options' ) ) {
402 \wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-2fa' ) );
403 }
404
405 if ( isset( $_POST[ WP_2FA_NEW_SETTINGS_NAME ] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
406 \check_admin_referer( WP_2FA_NEW_SETTINGS_NAME . '-options' );
407 $options = self::validate_and_sanitize( wp_unslash( $_POST[ WP_2FA_NEW_SETTINGS_NAME ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
408 Settings_Utils::update_option( WP_2FA_NEW_SETTINGS_NAME, $options );
409 }
410 }
411
412 /**
413 * Register the general settings tabs to be rendered on the settings page.
414 *
415 * @param array $collected_general_settings Array of already collected general settings tabs (if any).
416 * @return array Updated array of general settings tabs.
417 *
418 * @since 3.1.1.2
419 */
420 public static function register_general_settings( array $collected_general_settings ): array {
421 // $general_settings = array(
422 // 'generic-settings' => array(
423 // 'id' => 'generic-settings',
424 // 'title' => \esc_html__( 'General settings', 'wp-2fa' ),
425 // ),
426 // 'providers-integrations' => array(
427 // 'id' => 'providers-integrations',
428 // 'title' => \esc_html__( '2FA Providers integrations', 'wp-2fa' ),
429 // ),
430 // 'plugins-integrations' => array(
431 // 'id' => 'plugins-integrations',
432 // 'title' => \esc_html__( 'Plugin integrations', 'wp-2fa' ),
433 // ),
434 // 'email-settings' => array(
435 // 'id' => 'email-settings',
436 // 'title' => \esc_html__( 'Emails & SMS Templates', 'wp-2fa' ),
437 // ),
438 // 'settings-import' => array(
439 // 'id' => 'settings-import',
440 // 'title' => \esc_html__( 'Export / import', 'wp-2fa' ),
441 // ),
442 // );
443
444 $general_settings = self::collect_settings_tabs();
445
446 \array_shift( $general_settings ); // remove the first one (general-settings landing), as that will be rendered in a different section.
447
448 // When the White labeling page is a separate sub-menu, email-settings
449 // are shown there instead of here.
450 if ( Settings_Page::is_new_interface_enabled() ) {
451 unset( $general_settings['email-settings'] );
452 }
453
454 return \array_merge( $collected_general_settings, $general_settings );
455 }
456
457 /**
458 * Register the white labelling settings tabs to be rendered on the settings page.
459 *
460 * @param array $collected_white_labelling_settings Array of already collected white labelling settings tabs (if any).
461 *
462 * @return array Updated array of white labelling settings tabs.
463 *
464 * @since 3.1.1.2
465 */
466 public static function register_white_labelling_settings( array $collected_white_labelling_settings ): array {
467 $white_labelling_settings = array(
468 'customize-code-page' => array(
469 'id' => 'customize-code-page',
470 'title' => \esc_html__( 'Customize 2FA code page', 'wp-2fa' ),
471 ),
472 'customize-setup-wizard' => array(
473 'id' => 'customize-setup-wizard',
474 'title' => \esc_html__( 'Customize 2FA setup wizard', 'wp-2fa' ),
475 ),
476 );
477
478 return \array_merge( $collected_white_labelling_settings, $white_labelling_settings );
479 }
480
481 /**
482 * Collect the settings tabs to be rendered on the left side of the settings page.
483 *
484 * @return array Array of tabs, each with 'id' and 'title'.
485 *
486 * @since 3.1.1.2
487 */
488 private static function collect_settings_tabs(): array {
489
490 $settings_tabs = array(
491 // Thats the main.
492 'general-settings' => array(
493 'id' => 'general-settings',
494 'title' => \esc_html__( 'Settings', 'wp-2fa' ),
495 ),
496 'generic-settings' => array(
497 'id' => 'generic-settings',
498 'title' => \esc_html__( 'General settings', 'wp-2fa' ),
499 ),
500 );
501
502
503 // $settings_tabs = \array_merge( $settings_tabs, self::register_white_labelling_settings( array() ) );
504
505 return $settings_tabs;
506 }
507 }
508 }
509