diff --git a/class-two-factor-core.php b/class-two-factor-core.php index d7e67869..f41cb597 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -352,22 +352,31 @@ public static function enable_dummy_method_for_debug( $methods ) { } /** - * Add "Settings" link to the plugin action links on the Plugins screen. + * Add Plugin and User Settings link to the plugin action links on the Plugins screen. * * @since 0.14.3 * * @param string[] $links An array of plugin action links. - * @return string[] Modified array with the Settings link added. + * @return string[] Modified array with the User Settings link added. */ public static function add_settings_action_link( $links ) { - $settings_url = admin_url( 'profile.php#application-passwords-section' ); - $settings_link = sprintf( + $plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' ); + $plugin_settings_link = sprintf( '%s', - esc_url( $settings_url ), - esc_html__( 'Settings', 'two-factor' ) + esc_url( $plugin_settings_url ), + esc_html__( 'Plugin Settings', 'two-factor' ) ); - array_unshift( $links, $settings_link ); + $user_settings_url = admin_url( 'profile.php#application-passwords-section' ); + $user_settings_link = sprintf( + '%s', + esc_url( $user_settings_url ), + esc_html__( 'User Settings', 'two-factor' ) + ); + + // Show plugin settings first, then user settings. + array_unshift( $links, $user_settings_link ); + array_unshift( $links, $plugin_settings_link ); return $links; } diff --git a/readme.txt b/readme.txt index f166a0c4..45646df4 100644 --- a/readme.txt +++ b/readme.txt @@ -14,7 +14,7 @@ The Two-Factor plugin adds an extra layer of security to your WordPress login by ## Setup Instructions -**Important**: Each user must individually configure their two-factor authentication settings. There are no site-wide settings for this plugin. +**Important**: Each user must individually configure their two-factor authentication settings. ### For Individual Users @@ -32,7 +32,7 @@ The Two-Factor plugin adds an extra layer of security to your WordPress login by ### For Site Administrators -- **No global settings**: This plugin operates on a per-user basis only. For more, see [GH#249](https://github.com/WordPress/two-factor/issues/249). +- **Plugin settings**: The plugin provides a settings page under "Settings → Two-Factor" to configure which providers should be disabled site-wide. - **User management**: Administrators can configure 2FA for other users by editing their profiles - **Security recommendations**: Encourage users to enable backup methods to prevent account lockouts @@ -125,10 +125,6 @@ The plugin contributors and WordPress community take security bugs seriously. We To report a security issue, please visit the [WordPress HackerOne](https://hackerone.com/wordpress) program. -= Why doesn't this plugin have site-wide settings? = - -This plugin is designed to work on a per-user basis, allowing each user to choose their preferred authentication methods. This approach provides maximum flexibility and security. Site administrators can still configure 2FA for other users by editing their profiles. For more information, see [issue #437](https://github.com/WordPress/two-factor/issues/437). - = What if I lose access to all my authentication methods? = If you have backup codes enabled, you can use one of those to regain access. If you don't have backup codes or have used them all, you'll need to contact your site administrator to reset your account. This is why it's important to always enable backup codes and keep them in a secure location. @@ -240,3 +236,4 @@ Bumps WordPress minimum supported version to 6.3 and PHP minimum to 7.2. = 0.9.0 = Users are now asked to re-authenticate with their two-factor before making changes to their two-factor settings. This associates each login session with the two-factor login meta data for improved handling of that session. + diff --git a/settings/class-two-factor-settings.php b/settings/class-two-factor-settings.php new file mode 100644 index 00000000..73239e52 --- /dev/null +++ b/settings/class-two-factor-settings.php @@ -0,0 +1,101 @@ +

' . esc_html__( 'Settings saved.', 'two-factor' ) . '

'; + } + + // Build provider list for display using public core API. + $provider_instances = array(); + if ( class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'get_providers' ) ) { + $provider_instances = Two_Factor_Core::get_providers(); + if ( ! is_array( $provider_instances ) ) { + $provider_instances = array(); + } + } + + $saved_disabled = get_option( 'two_factor_disabled_providers', array() ); + + echo '
'; + echo '

' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '

'; + echo '

' . esc_html__( 'Disable Providers', 'two-factor' ) . '

'; + echo '

' . esc_html__( 'Disable any Two-Factor providers you do not want available on this site. By default all providers are available.', 'two-factor' ) . '

'; + echo '
'; + wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' ); + + echo '
' . esc_html__( 'Providers', 'two-factor' ) . ''; + echo ''; + + if ( empty( $provider_instances ) ) { + echo ''; + } else { + // Render a compact stacked list of provider checkboxes below the title/description. + echo ''; + echo ''; + echo ''; + } + + echo '
' . esc_html__( 'No providers found.', 'two-factor' ) . '
'; + foreach ( $provider_instances as $provider_key => $instance ) { + $label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key; + + echo '

'; + } + + echo '
'; + echo '
'; + + submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' ); + echo '
'; + + echo '
'; + } + +} diff --git a/two-factor.php b/two-factor.php index 4095b465..c1b018d9 100644 --- a/two-factor.php +++ b/two-factor.php @@ -51,9 +51,155 @@ */ require_once TWO_FACTOR_DIR . 'class-two-factor-compat.php'; +// Load settings UI class so the settings page can be rendered. +require_once TWO_FACTOR_DIR . 'settings/class-two-factor-settings.php'; + $two_factor_compat = new Two_Factor_Compat(); Two_Factor_Core::add_hooks( $two_factor_compat ); // Delete our options and user meta during uninstall. register_uninstall_hook( __FILE__, array( Two_Factor_Core::class, 'uninstall' ) ); + +/** + * Register admin menu and plugin action links. + * + * @since 0.16 + */ +function two_factor_register_admin_hooks() { + if ( is_admin() ) { + add_action( 'admin_menu', 'two_factor_add_settings_page' ); + } + + // Load settings page assets when in admin. + // Settings assets handled inline via standard markup; no extra CSS enqueued. + + /* Enforcement filters: restrict providers based on saved disabled option. */ + add_filter( 'two_factor_providers', 'two_factor_filter_disabled_providers' ); + add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_disabled_enabled_providers_for_user', 10, 2 ); +} + +add_action( 'init', 'two_factor_register_admin_hooks' ); + +/** + * Add the Two Factor settings page under Settings. + * + * @since 0.16 + */ +function two_factor_add_settings_page() { + add_options_page( + __( 'Two-Factor Settings', 'two-factor' ), + __( 'Two-Factor', 'two-factor' ), + 'manage_options', + 'two-factor-settings', + 'two_factor_render_settings_page' + ); +} + + +/** + * Render the settings page via the settings class if available. + * + * @since 0.16 + */ +function two_factor_render_settings_page() { + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + // Prefer new settings class (keeps main file small). + if ( class_exists( 'Two_Factor_Settings' ) && is_callable( array( 'Two_Factor_Settings', 'render_settings_page' ) ) ) { + Two_Factor_Settings::render_settings_page(); + return; + } + + // Fallback: no UI available. + echo '

' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '

'; + echo '

' . esc_html__( 'Settings not available.', 'two-factor' ) . '

'; +} + + +/** + * Helper: retrieve disabled providers option as an array of classnames. + * Empty array / missing option means none disabled (all allowed). + * + * @since 0.16 + * + * @return array + */ +function two_factor_get_disabled_providers_option() { + $disabled = get_option( 'two_factor_disabled_providers', array() ); + if ( empty( $disabled ) || ! is_array( $disabled ) ) { + return array(); + } + return $disabled; +} + + +/** + * Filter the registered providers according to the saved disabled providers option. + * This filter receives the providers in the same shape as core: classname => path. + * + * @since 0.16 + */ +function two_factor_filter_disabled_providers( $providers ) { + $disabled = two_factor_get_disabled_providers_option(); + + // Empty disabled list means allow all providers. + if ( empty( $disabled ) ) { + return $providers; + } + + // If we are rendering the settings page, do not filter so admins may re-enable providers. + if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) { + return $providers; + } + + foreach ( $providers as $key => $path ) { + if ( in_array( $key, $disabled, true ) ) { + unset( $providers[ $key ] ); + } + } + + return $providers; +} + + +/** + * Filter the supported providers for a specific user (instances keyed by provider key). + * + * @since 0.16 + */ +function two_factor_filter_disabled_providers_for_user( $providers, $user ) { + $disabled = two_factor_get_disabled_providers_option(); + if ( empty( $disabled ) ) { + return $providers; + } + + if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) { + return $providers; + } + + foreach ( $providers as $key => $instance ) { + if ( in_array( $key, $disabled, true ) ) { + unset( $providers[ $key ] ); + } + } + + return $providers; +} + + +/** + * Filter enabled providers for a user (classnames array) to enforce disabled list. + * + * @since 0.16 + */ +function two_factor_filter_disabled_enabled_providers_for_user( $enabled, $user_id ) { + $disabled = two_factor_get_disabled_providers_option(); + if ( empty( $disabled ) ) { + return $enabled; + } + + return array_values( array_diff( (array) $enabled, $disabled ) ); +}