diff --git a/class-two-factor-core.php b/class-two-factor-core.php index c034c53d..30a39e2d 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -343,22 +343,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 4093abcf..18d0cd42 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 @@ -31,7 +31,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 @@ -119,10 +119,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. @@ -233,3 +229,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..cb7b7132 --- /dev/null +++ b/settings/class-two-factor-settings.php @@ -0,0 +1,98 @@ +

' . 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(); + } + } + + // Default to all providers enabled when the option has never been saved. + $all_provider_keys = array_keys( $provider_instances ); + $saved_enabled = get_option( 'two_factor_enabled_providers', $all_provider_keys ); + + echo '
'; + echo '

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

'; + echo '

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

'; + echo '

' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', '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 c0f38b17..6be20e19 100644 --- a/two-factor.php +++ b/two-factor.php @@ -51,9 +51,133 @@ */ 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 enabled-providers option. */ + add_filter( 'two_factor_providers', 'two_factor_filter_enabled_providers' ); + add_filter( 'two_factor_enabled_providers_for_user', 'two_factor_filter_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 the site-enabled providers option. + * Returns null when the option has never been saved (meaning all providers are allowed). + * Returns an array (possibly empty) when the admin has explicitly saved a selection. + * + * @since 0.16 + * + * @return array|null + */ +function two_factor_get_enabled_providers_option() { + $enabled = get_option( 'two_factor_enabled_providers', null ); + if ( null === $enabled ) { + return null; // Never saved — allow everything. + } + return is_array( $enabled ) ? $enabled : array(); +} + + +/** + * Filter the registered providers to only those in the site-enabled list. + * This filter receives providers in core format: classname => path. + * + * @since 0.16 + */ +function two_factor_filter_enabled_providers( $providers ) { + $site_enabled = two_factor_get_enabled_providers_option(); + + // null means the option was never saved — allow all providers. + if ( null === $site_enabled ) { + return $providers; + } + + // On the settings page itself, show all providers so admins can change the selection. + if ( is_admin() && isset( $_GET['page'] ) && 'two-factor-settings' === $_GET['page'] ) { + return $providers; + } + + foreach ( $providers as $key => $path ) { + if ( ! in_array( $key, $site_enabled, true ) ) { + unset( $providers[ $key ] ); + } + } + + return $providers; +} + + +/** + * Filter enabled providers for a user (classnames array) to enforce the site-enabled list. + * + * @since 0.16 + */ +function two_factor_filter_enabled_providers_for_user( $enabled, $user_id ) { + $site_enabled = two_factor_get_enabled_providers_option(); + + // null means the option was never saved — allow all. + if ( null === $site_enabled ) { + return $enabled; + } + + return array_values( array_intersect( (array) $enabled, $site_enabled ) ); +}