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 |