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-passkeys.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-passkeys.php
648 lines
1 <?php
2 /**
3 * Policy settings class.
4 *
5 * @package wp2fa
6 * @subpackage settings-pages
7 *
8 * @copyright 2026 Melapress
9 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
10 *
11 * @see https://wordpress.org/plugins/wp-2fa/
12 */
13
14 namespace WP2FA\Admin\SettingsPages;
15
16 use WP2FA\WP2FA;
17 use WP2FA\Utils\Debugging;
18 use WP2FA\Methods\Passkeys;
19 use WP2FA\Admin\Settings_Page;
20 use WP2FA\Utils\Settings_Utils;
21 use WP2FA\Admin\Helpers\WP_Helper;
22 use WP2FA\Extensions\RoleSettings\Role_Settings_Controller;
23
24 /*
25 * Policies settings tab
26 */
27 if ( ! class_exists( '\WP2FA\Admin\SettingsPages\Settings_Page_Passkeys' ) ) {
28 /**
29 * Settings_Page_Passkeys - Class for handling settings.
30 *
31 * @since 2.0.0
32 */
33 class Settings_Page_Passkeys {
34
35 public const TOP_MENU_SLUG = 'wp-2fa-passkeys';
36
37 /**
38 * Initialize hooks for the passkeys settings page.
39 *
40 * @since 3.1.1.2
41 */
42 public static function init() {
43 \add_action(
44 'admin_enqueue_scripts',
45 function ( $hook ) {
46 $allowed_hooks = array(
47 'wp-2fa_page_' . self::TOP_MENU_SLUG,
48 'wp-2fa_page_' . self::TOP_MENU_SLUG . '-network',
49 );
50
51 $page = isset( $_GET['page'] ) ? \sanitize_text_field( \wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
52
53 if ( ! \in_array( $hook, $allowed_hooks, true ) && self::TOP_MENU_SLUG !== $page ) {
54 return;
55 }
56
57 \wp_enqueue_style(
58 'wp_2fa_admin_settings',
59 WP_2FA_URL . 'includes/assets/css/settings.css',
60 array(),
61 WP_2FA_VERSION
62 );
63 }
64 );
65 }
66
67 /**
68 * Renders the settings using the new template system.
69 *
70 * @return void
71 *
72 * @since 2.0.0
73 */
74 public static function render() {
75 if ( ! \current_user_can( 'manage_options' ) ) {
76 return;
77 }
78
79 $user = \wp_get_current_user();
80 $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();
81
82 if ( ! empty( WP2FA::get_wp2fa_general_setting( 'limit_access' ) ) && $main_user !== $user->ID ) {
83 echo \esc_html__( 'These settings have been disabled by your site administrator, please contact them for further assistance.', 'wp-2fa' );
84 return;
85 }
86
87 \do_action( WP_2FA_PREFIX . 'before_plugin_settings_passkeys' );
88
89 if ( WP_Helper::is_multisite() ) {
90 $action = 'edit.php?action=update_wp2fa_network_options';
91 } else {
92 $action = 'options.php';
93 }
94 ?>
95 <div class="wp-2fa-settings-wrapper wp-2fa-policies-new">
96 <div class="page-header">
97 </div>
98
99 <form id="wp-2fa-admin-settings" action="<?php echo \esc_attr( $action ); ?>" method="post" autocomplete="off" data-disabled-note="<?php echo \esc_attr__( 'Please select at least one role to save.', 'wp-2fa' ); ?>">
100 <?php \settings_fields( WP_2FA_PASSKEYS_SETTINGS_NAME ); ?>
101
102 <div class="main-settings-new">
103 <div class="wrap main-left">
104 <?php include \WP_2FA_PATH . '/includes/classes/Admin/Settings/templates/passkeys/passkeys-settings.php'; ?>
105 </div>
106
107 <?php include \WP_2FA_PATH . '/includes/classes/Admin/Settings/templates/sidebar.php'; ?>
108 </div>
109
110 <div class="save-footer">
111 <?php \submit_button( \esc_html__( 'Save settings', 'wp-2fa' ), 'primary', 'submit', false ); ?>
112 <?php if ( isset( $_GET['settings-updated'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended ?>
113 <span class="wp2fa-save-notice wp2fa-save-notice--success" style="margin-left:12px;font-weight:500;color:#00a32a;"><?php \esc_html_e( 'Settings saved.', 'wp-2fa' ); ?></span>
114 <script>setTimeout(function(){var n=document.querySelector('.save-footer .wp2fa-save-notice');if(n)n.remove();},3500);</script>
115 <?php endif; ?>
116 </div>
117 </form>
118 </div>
119 <?php
120 }
121
122 /**
123 * Renders the settings (old / legacy interface).
124 *
125 * Kept for backward compatibility — not called anywhere.
126 *
127 * @return void
128 *
129 * @since 2.0.0
130 */
131 public static function render_old() {
132 if ( ! \current_user_can( 'manage_options' ) ) {
133 return;
134 }
135
136 $user = \wp_get_current_user();
137 $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();
138
139 /**
140 * Used from user settings controller.
141 *
142 * @param bool - Default at this point is false - no user settings.
143 *
144 * @since 2.4.0
145 */
146 $roles_controller = \apply_filters( WP_2FA_PREFIX . 'roles_controller_exists', false );
147 ?>
148
149 <div class="wrap wp-2fa-settings-wrapper wp2fa-form-styles">
150 <h2><?php \esc_html_e( 'Passkeys', 'wp-2fa' ); ?></h2>
151 <hr>
152 <?php if ( ! empty( WP2FA::get_wp2fa_general_setting( 'limit_access' ) ) && $main_user !== $user->ID ) { ?>
153 <?php
154 echo \esc_html__( 'These settings have been disabled by your site administrator, please contact them for further assistance.', 'wp-2fa' );
155 ?>
156 <?php } else { ?>
157 <?php
158 /**
159 * Fires before the plugin settings rendering.
160 *
161 * @since 2.0.0
162 */
163 \do_action( WP_2FA_PREFIX . 'before_plugin_settings_passkeys' );
164 ?>
165 <?php
166 if ( WP_Helper::is_multisite() ) {
167 $action = 'edit.php?action=update_wp2fa_network_options';
168 } else {
169 $action = 'options.php';
170 }
171 ?>
172 <br/>
173 <?php
174 printf(
175 '<p class="description">%1$s %2$s</p>',
176 \esc_html__( 'Passkeys are not a two-factor authentication (2FA) method. They are a passwordless multi-factor authentication (MFA) solution that allows users to securely log in without using a password.', 'wp-2fa' ),
177 \wp_kses(
178 sprintf(
179 /* translators: 1: opening link tag, 2: closing link tag */
180 __( 'For more information about Passkeys and how to configure these settings refer to the guide %1$sHow to set up Passkeys in WP 2FA%2$s.', 'wp-2fa' ),
181 '<a href="' . \esc_url( 'https://melapress.com/support/kb/wp-2fa-set-up-passkeys/?#utm_source=plugin&utm_medium=wp2fa&utm_campaign=passkeys_admin' ) . '" target="_blank" rel="noopener noreferrer">',
182 '</a>'
183 ),
184 array(
185 'a' => array(
186 'href' => array(),
187 'target' => array(),
188 'rel' => array(),
189 ),
190 )
191 )
192 );
193 ?>
194 <br/>
195
196 <form id="wp-2fa-admin-settings" action='<?php echo \esc_attr( $action ); ?>' method='post' autocomplete="off" data-disabled-note="<?php echo \esc_attr__( 'Please select at least one role to save.', 'wp-2fa' ); ?>">
197 <?php
198 \settings_fields( WP_2FA_PASSKEYS_SETTINGS_NAME );
199 ?>
200 <table class="form-table">
201 <tbody>
202 <tr>
203 <th>
204 <label for="passkeys-enabled">
205 <?php \esc_html_e( 'Allow users to register Passkeys', 'wp-2fa' ); ?>
206 </label>
207 </th>
208 <td>
209 <fieldset>
210 <label for="passkeys-enabled">
211 <?php
212 $enabled = (bool) Settings_Utils::get_setting_role( null, Passkeys::POLICY_SETTINGS_NAME );
213 $first_check = $enabled;
214 $second_check = false;
215 if ( ! $enabled ) {
216 $enabled = (bool) Settings_Utils::get_setting_role( null, Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' );
217 $second_check = $enabled;
218 $first_check = false;
219 }
220 ?>
221 <input type="checkbox" id="passkeys-enabled" value="yes"
222 name="wp_2fa_passkeys[<?php echo \esc_attr( Passkeys::POLICY_SETTINGS_NAME ); ?>]"
223 <?php \checked( true, $enabled ); ?>
224 >
225 </label>
226 </fieldset>
227 <?php
228 if ( class_exists( Role_Settings_Controller::class, false ) ) {
229 // Preload roles for the enforcement policy row below.
230 $roles = array_flip( WP_Helper::get_roles() );
231 }
232 ?>
233 </td>
234 <?php if ( class_exists( Role_Settings_Controller::class, false ) ) { ?>
235 <tr class="enforcement-policy-row" style="display:none;">
236 <th>
237 <label>
238 <?php \esc_html_e( 'Allow the following users', 'wp-2fa' ); ?>
239 </label>
240 </th>
241 <td>
242 <label style="margin:.35em 0 .5em !important; display: block;">
243 <?php $first_check = (bool) ( ! $second_check ); ?>
244 <input type="radio" name="wp_2fa_passkeys[enforcement-policy]" id="all-roles" value="all-roles" <?php \checked( true, $first_check ); ?>>
245 <span><?php \esc_html_e( 'All users with any role', 'wp-2fa' ); ?></span>
246 </label>
247 <label style="margin:.35em 0 .5em !important; display: block;">
248 <?php $checked = in_array( WP2FA::get_wp2fa_setting( 'enforcement-policy' ), array( 'certain-roles-only', 'certain-users-only' ), true ); ?>
249 <input type="radio" name="wp_2fa_passkeys[enforcement-policy]" id="certain-roles-only" value="certain-roles-only" <?php checked( true, $second_check ); ?> data-unhide-when-checked=".certain-roles-only-inputs, .certain-users-only-inputs">
250 <span><?php \esc_html_e( 'Only users with the below user roles:', 'wp-2fa' ); ?></span>
251 </label>
252
253 <div class="roles-selector-wrapper" style="margin-left:24px;">
254 <?php
255 foreach ( $roles as $role => $value ) {
256 ?>
257 <div>
258 <label for="passkeys-enabled-role-<?php echo \esc_attr( $role ); ?>">
259 <input type="checkbox" id="passkeys-enabled-role-<?php echo \esc_attr( $role ); ?>" value="yes" name="wp_2fa_passkeys[enabled_roles][<?php echo \esc_attr( $role ); ?>]"
260 <?php
261 if ( Passkeys::is_enabled( $role ) ) {
262 ?>
263 checked="checked"<?php } ?>><?php echo \esc_html( $value ); ?>
264 </label>
265 </div>
266 <?php
267 }
268 ?>
269 </div>
270 </td>
271 </tr>
272 <?php } ?>
273 </tr>
274 <?php
275 // @free:start
276 ?>
277 <tr>
278 <td colspan="2" style="padding:0px;">
279 <?php
280 echo wp_sprintf(
281 /* translators: 1: opening bold tag, 2: closing bold tag, 3: opening link tag, 4: closing link tag */
282 __( '%1$sNote:%2$s In the Free edition of WP 2FA, each user can register only one Passkey, and Passkey logins bypass 2FA. The %3$sPremium edition%4$s enables users to register multiple Passkeys and gives you the option to enforce 2FA even when users sign in with a Passkey.', 'wp-2fa' ),
283 '<b>',
284 '</b>',
285 '<a href="https://melapress.com/wordpress-2fa/?utm_source=plugin&utm_medium=wp2fa&utm_campaign=passkeys+settings+page" target="_blank" rel="noopener noreferrer">',
286 '</a>'
287 );
288 ?>
289 </td>
290 </tr>
291 <?php
292 // @free:end
293 ?>
294 </tbody>
295 </table>
296 <?php
297
298 // @free:start
299 ?>
300 <input type="hidden" name="wp_2fa_passkeys[skip_2fa_for_passkeys]" value="1">
301 <?php
302 // @free:end
303
304
305 \submit_button();
306
307 $javascript = <<<JS
308 <script>
309 document.addEventListener('DOMContentLoaded', function(){
310 const passkeysEnabledCheckbox = document.getElementById('passkeys-enabled');
311 const certainRolesRadio = document.getElementById('certain-roles-only');
312 const allRolesRadio = document.getElementById('all-roles');
313 const certainRolesWrapper = document.querySelectorAll('.roles-selector-wrapper');
314 const enforcementRow = document.querySelector('.enforcement-policy-row');
315 const form = document.getElementById('wp-2fa-admin-settings');
316 const submitBtn = form ? form.querySelector('#submit, [type="submit"]') : null;
317 const noteText = form ? (form.dataset.disabledNote || 'Please select at least one role to save.') : 'Please select at least one role to save.';
318 let submitNoteEl = null;
319
320 function ensureSubmitNote() {
321 if (!submitBtn) return;
322 if (!submitNoteEl) {
323 submitNoteEl = document.createElement('span');
324 submitNoteEl.className = 'wp2fa-submit-disabled-note description';
325 submitNoteEl.style.marginLeft = '8px';
326 submitNoteEl.style.color = '#646970';
327 submitNoteEl.style.fontSize = '12px';
328 submitNoteEl.style.fontStyle = 'italic';
329 submitBtn.insertAdjacentElement('afterend', submitNoteEl);
330 }
331 }
332
333 function updateSubmitState() {
334 if (!submitBtn) return;
335 const rolesWrapper = document.querySelector('.roles-selector-wrapper');
336 const requireSelection = !!(passkeysEnabledCheckbox && certainRolesRadio && rolesWrapper && rolesWrapper.style.display !== 'none' && passkeysEnabledCheckbox.checked && certainRolesRadio.checked);
337 let checkedCount = 0;
338 if (requireSelection) {
339 checkedCount = rolesWrapper.querySelectorAll('input[type="checkbox"]:checked').length;
340 }
341 const shouldDisable = requireSelection && checkedCount === 0;
342 submitBtn.disabled = shouldDisable;
343 ensureSubmitNote();
344 if (submitNoteEl) {
345 submitNoteEl.textContent = shouldDisable ? noteText : '';
346 submitNoteEl.style.display = shouldDisable ? 'inline' : 'none';
347 }
348 }
349
350 function toggleCertainRolesInputs() {
351 if ( passkeysEnabledCheckbox.checked ) {
352 // Show enforcement row when passkeys enabled.
353 if ( enforcementRow ) enforcementRow.style.display = 'table-row';
354 // Show roles selector only when certain-roles is selected.
355 if ( certainRolesRadio.checked ) {
356 certainRolesWrapper.forEach(function(element) { element.style.display = 'block'; });
357 } else {
358 certainRolesWrapper.forEach(function(element) { element.style.display = 'none'; });
359 }
360 } else {
361 // Hide enforcement row and roles if passkeys disabled.
362 if ( enforcementRow ) enforcementRow.style.display = 'none';
363 certainRolesWrapper.forEach(function(element) { element.style.display = 'none'; });
364 }
365
366 // Re-evaluate submit availability when UI visibility changes
367 updateSubmitState();
368 }
369
370 passkeysEnabledCheckbox.addEventListener('change', toggleCertainRolesInputs);
371 certainRolesRadio.addEventListener('change', toggleCertainRolesInputs);
372 allRolesRadio.addEventListener('change', toggleCertainRolesInputs);
373
374 // Role checkbox listeners to enable/disable submit
375 document.querySelectorAll('.roles-selector-wrapper input[type="checkbox"]').forEach(function(cb){
376 cb.addEventListener('change', updateSubmitState);
377 });
378
379 // Initial check
380 toggleCertainRolesInputs();
381 updateSubmitState();
382 });
383 </script>
384 JS;
385
386 if ( class_exists( Role_Settings_Controller::class, false ) ) {
387
388 echo $javascript;
389 }
390 ?>
391 </form>
392
393 <?php } ?>
394 </div>
395 <?php
396 }
397
398 /**
399 * Validate options before saving.
400 *
401 * @param array $input The settings array.
402 *
403 * @return array|void
404 *
405 * @since 2.0.0
406 */
407 public static function validate_and_sanitize( $input ) {
408 Debugging::log( 'The following settings will be processed (Passkeys): ' . "\n" . \wp_json_encode( $input ) );
409
410 /*
411 * Adds the ability to check the referer and act accordingly.
412 *
413 * @since 2.0.0
414 */
415 \do_action( WP_2FA_PREFIX . 'change_referer' );
416
417 // Bail if user doesn't have permissions to be here.
418 if ( ! \current_user_can( 'manage_options' ) || ! isset( $_POST['action'] ) && ! \check_admin_referer( 'wp2fa-step-choose-method' ) ) {
419 return;
420 }
421 $output = array();
422
423 $simple_settings_we_can_loop = array(
424 Passkeys::POLICY_SETTINGS_NAME,
425 );
426
427 $settings_to_turn_into_bools = array(
428 Passkeys::POLICY_SETTINGS_NAME,
429 );
430
431 $settings_to_turn_into_array = array(
432 'enabled_roles',
433 );
434
435 foreach ( $simple_settings_we_can_loop as $simple_setting ) {
436 if ( ! in_array( $simple_setting, $settings_to_turn_into_bools, true ) ) {
437 // Is item is not one of our possible settings we want to turn into a bool, process.
438 $output[ $simple_setting ] = ( isset( $input[ $simple_setting ] ) && ! empty( $input[ $simple_setting ] ) ) ? trim( (string) \sanitize_text_field( $input[ $simple_setting ] ) ) : false;
439 } else {
440 // This item is one we treat as a bool, so process correctly.
441 $output[ $simple_setting ] = ( isset( $input[ $simple_setting ] ) && ! empty( $input[ $simple_setting ] ) ) ? true : false;
442 }
443 }
444
445 foreach ( $settings_to_turn_into_array as $setting ) {
446 if ( isset( $input[ $setting ] ) ) {
447 $output[ $setting ] = $input[ $setting ];
448 } else {
449 $output[ $setting ] = array();
450 }
451 }
452
453 // Remove duplicates from settings errors. We do this as this sanitization callback is actually fired twice, so we end up with duplicates when saving the settings for the FIRST TIME only. The issue is not present once the settings are in the DB as the sanitization wont fire again. For details on this core issue - https://core.trac.wordpress.org/ticket/21989.
454 global $wp_settings_errors;
455 if ( isset( $wp_settings_errors ) ) {
456 $errors = array_map( 'unserialize', array_unique( array_map( 'serialize', $wp_settings_errors ) ) );
457 $wp_settings_errors = $errors; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
458 }
459
460 $settings = WP2FA::get_policy_settings();
461
462 if ( isset( $output['enable_passkeys'] ) && true === (bool) $output['enable_passkeys'] ) {
463 $settings[ Passkeys::POLICY_SETTINGS_NAME ] = Passkeys::POLICY_SETTINGS_NAME;
464 unset( $settings[ Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' ] );
465
466 if ( class_exists( Role_Settings_Controller::class, false ) ) {
467 if ( isset(
468 $input['enforcement-policy']
469 ) && 'certain-roles-only' === $input['enforcement-policy'] ) {
470 $settings[ Passkeys::POLICY_SETTINGS_NAME ] = '';
471 $settings[ Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' ] = 'certain-roles-only';
472
473 $roles_options_passkeys = array();
474 $roles = WP_Helper::get_roles();
475 foreach ( $roles as $role ) {
476 if ( ! isset( $input['enabled_roles'][ $role ] ) || empty( $input['enabled_roles'][ $role ] ) ) {
477 continue;
478 }
479
480 $roles_options_passkeys[ $role ][ Passkeys::POLICY_SETTINGS_NAME ] = Passkeys::POLICY_SETTINGS_NAME;
481 }
482
483 $roles_options = (array) Settings_Utils::get_option( Role_Settings_Controller::SETTINGS_NAME );
484
485 foreach ( $roles as $role_name ) {
486 if ( isset( $roles_options_passkeys[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME ] ) ) {
487 $roles_options[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME ] = Passkeys::POLICY_SETTINGS_NAME;
488 }
489
490 if ( ! isset( $roles_options_passkeys[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME ] ) && isset( $roles_options[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME ] ) ) {
491 unset( $roles_options[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME ] );
492 unset( $roles_options[ $role_name ][ Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' ] );
493 }
494 }
495 }
496 if ( isset(
497 $input['enforcement-policy']
498 ) && 'all-roles' === $input['enforcement-policy'] ) {
499 $roles_options = (array) Settings_Utils::get_option( Role_Settings_Controller::SETTINGS_NAME );
500
501 $roles = WP_Helper::get_roles();
502 foreach ( $roles as $role ) {
503 if ( isset( $roles_options[ $role ][ Passkeys::POLICY_SETTINGS_NAME ] ) ) {
504 unset( $roles_options[ $role ][ Passkeys::POLICY_SETTINGS_NAME ] );
505 unset( $roles_options[ $role ][ Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' ] );
506 }
507 }
508 }
509 }
510 } else {
511 unset( $settings[ Passkeys::POLICY_SETTINGS_NAME ] );
512 unset( $settings[ Passkeys::POLICY_SETTINGS_NAME . '_certain_roles' ] );
513 if ( class_exists( Role_Settings_Controller::class, false ) ) {
514 $roles_options = (array) Settings_Utils::get_option( Role_Settings_Controller::SETTINGS_NAME );
515
516 $roles = WP_Helper::get_roles();
517 foreach ( $roles as $role ) {
518 if ( isset( $roles_options[ $role ][ Passkeys::POLICY_SETTINGS_NAME ] ) ) {
519 unset( $roles_options[ $role ][ Passkeys::POLICY_SETTINGS_NAME ] );
520 }
521 }
522 }
523 }
524
525 if ( class_exists( Role_Settings_Controller::class, false ) ) {
526
527 if ( isset( $roles_options ) && ! empty( $roles_options ) ) {
528
529 Settings_Utils::update_option( Role_Settings_Controller::SETTINGS_NAME, $roles_options );
530 unset( $settings['role-settings'] );
531 $settings['role-settings'] = $roles_options;
532
533 foreach ( $roles_options as $role_name => $value ) {
534 $settings[ $role_name ] = $value;
535 }
536 }
537 \remove_filter( WP_2FA_PREFIX . 'filter_output_content', array( Role_Settings_Controller::class, 'validate_and_sanitize' ), 10, 2 );
538 }
539
540 // Remove the policy sanitize callback so that update_option( 'wp_2fa_policy' ) does
541 // not funnel the value through Settings_Page_Policies::validate_and_sanitize(),
542 // which would strip out the passkeys keys it does not know about.
543 \remove_filter( 'sanitize_option_' . WP_2FA_POLICY_SETTINGS_NAME, array( Settings_Page_Policies::class, 'validate_and_sanitize' ) );
544
545 // WordPress saves the option to the database, but we still need to do some work when the settings are saved.
546 WP2FA::update_plugin_settings( $settings );
547
548 Debugging::log( 'The following settings are being saved (Passkeys): ' . "\n" . \wp_json_encode( $output ) );
549
550 // We have overridden any defaults by now so can clear this.
551 // Unless it is not Wizard process in which case we will leave that as is for now.
552 if ( ! isset( $_GET['page'] ) || ( isset( $_GET['page'] ) && 'wp-2fa-setup' !== $_GET['page'] ) ) {
553 Settings_Utils::delete_option( WP_2FA_PREFIX . 'default_settings_applied' );
554 Settings_Utils::delete_option( 'wizard_not_finished' );
555 }
556
557 /**
558 * Notify the extensions and 3rd party developers that the settings array is saved.
559 *
560 * @param array - Array with all the stored settings.
561 *
562 * @since 2.6.0
563 */
564 \do_action( WP_2FA_PREFIX . 'after_settings_save', Settings_Utils::get_option( WP_2FA_POLICY_SETTINGS_NAME, array() ) );
565
566 // Remove the general-settings sanitize callback so that update_option( 'wp_2fa_settings' )
567 // does not funnel the value through Settings_Page_General::validate_and_sanitize().
568 \remove_filter( 'sanitize_option_' . WP_2FA_SETTINGS_NAME, array( Settings_Page_General::class, 'validate_and_sanitize' ) );
569
570 // Read from WP_2FA_SETTINGS_NAME (general settings), NOT from WP_2FA_POLICY_SETTINGS_NAME
571 // (policy settings). Previously the code read from the policy option and wrote to the
572 // general-settings option, completely overwriting all general settings with policy data.
573 $options = Settings_Utils::get_option( WP_2FA_SETTINGS_NAME, array() );
574
575 if ( isset( $_POST[ WP_2FA_SETTINGS_NAME . 'pass' ]['skip_2fa_for_passkeys'] ) && true === (bool) $_POST[ WP_2FA_SETTINGS_NAME . 'pass' ]['skip_2fa_for_passkeys'] ) {
576 $options['skip_2fa_for_passkeys'] = true;
577 } else {
578 $options['skip_2fa_for_passkeys'] = false;
579 }
580
581 WP2FA::update_plugin_settings( $options, false, WP_2FA_SETTINGS_NAME );
582
583 \wp_safe_redirect(
584 \add_query_arg(
585 array(
586 'page' => self::TOP_MENU_SLUG,
587 'settings-updated' => 'true',
588 ),
589 \network_admin_url( 'admin.php' )
590 )
591 );
592 exit;
593
594 remove_filter( 'sanitize_option_' . self::TOP_MENU_SLUG, array( __CLASS__, 'validate_and_sanitize' ) );
595 add_filter( 'default_option_' . self::TOP_MENU_SLUG, '__return_false' );
596
597 return $settings;
598 }
599
600 /**
601 * Updates global policy network options.
602 *
603 * @return void
604 *
605 * @since 2.0.0
606 */
607 public static function update_wp2fa_network_options() {
608 if ( ! \current_user_can( 'manage_options' ) ) {
609 \wp_die( \esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-2fa' ) );
610 }
611
612 if ( isset( $_POST[ WP_2FA_PASSKEYS_SETTINGS_NAME ] ) ) {
613 \check_admin_referer( WP_2FA_PASSKEYS_SETTINGS_NAME . '-options' );
614 $options = self::validate_and_sanitize( wp_unslash( $_POST[ WP_2FA_PASSKEYS_SETTINGS_NAME ] ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
615 $settings_errors = \get_settings_errors( WP_2FA_PASSKEYS_SETTINGS_NAME );
616 if ( ! empty( $settings_errors ) ) {
617 Settings_Page::set_network_admin_notice( 'error', $settings_errors[0]['message'] );
618 // redirect back to our options page.
619 \wp_safe_redirect(
620 \add_query_arg(
621 array(
622 'page' => Settings_Page::TOP_MENU_SLUG,
623 ),
624 \network_admin_url( 'admin.php' )
625 )
626 );
627 exit;
628 }
629 WP2FA::update_plugin_settings( $options );
630
631 Settings_Page::set_network_admin_notice( 'success' );
632 // redirect back to our options page.
633 \wp_safe_redirect(
634 \add_query_arg(
635 array(
636 'page' => self::TOP_MENU_SLUG,
637 'settings-updated' => 'true',
638 ),
639 \network_admin_url( 'admin.php' )
640 )
641 );
642 exit;
643 }
644 }
645 }
646 }
647
648