Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions class-two-factor-core.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,24 @@ class Two_Factor_Core {
*/
const ENABLED_PROVIDERS_OPTION_KEY = 'two_factor_enabled_providers';

/**
* The network-wide enabled providers option key.
*
* @since 0.17.0
*
* @type string
*/
const ENABLED_PROVIDERS_NETWORK_OPTION_KEY = 'two_factor_network_enabled_providers';

/**
* The network option key that controls whether subsites can override provider settings.
*
* @since 0.17.0
*
* @type string
*/
const NETWORK_ALLOW_SITE_OVERRIDE_OPTION_KEY = 'two_factor_network_allow_site_override';

/**
* The user meta nonce key.
*
Expand Down Expand Up @@ -149,6 +167,7 @@ public static function add_hooks( $compat ) {

// Add Settings link to plugin action links.
add_filter( 'plugin_action_links_' . plugin_basename( TWO_FACTOR_DIR . 'two-factor.php' ), array( __CLASS__, 'add_settings_action_link' ) );
add_filter( 'network_admin_plugin_action_links', array( __CLASS__, 'add_network_settings_action_link' ), 10, 4 );

$compat->init();
}
Expand Down Expand Up @@ -203,6 +222,12 @@ public static function uninstall() {
self::ENABLED_PROVIDERS_OPTION_KEY,
);

// Network options are stored in sitemeta on Multisite and in wp_options on single-site.
$network_option_keys = array(
self::ENABLED_PROVIDERS_NETWORK_OPTION_KEY,
self::NETWORK_ALLOW_SITE_OVERRIDE_OPTION_KEY,
);

$providers = self::get_default_providers();

/** This filter is documented in the get_providers() method */
Expand Down Expand Up @@ -244,6 +269,12 @@ public static function uninstall() {
}
}

if ( ! empty( $network_option_keys ) ) {
foreach ( $network_option_keys as $option_key ) {
delete_site_option( $option_key );
}
}

foreach ( $user_meta_keys as $meta_key ) {
delete_metadata( 'user', null, $meta_key, null, true );
}
Expand Down Expand Up @@ -398,7 +429,11 @@ public static function enable_dummy_method_for_debug( $methods ) {
* @return string[] Modified array with the User Settings link added.
*/
public static function add_settings_action_link( $links ) {
$plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' );
if ( two_factor_is_network_mode() && current_user_can( 'manage_network_options' ) ) {
$plugin_settings_url = network_admin_url( 'settings.php?page=two-factor-network-settings' );
} else {
$plugin_settings_url = admin_url( 'options-general.php?page=two-factor-settings' );
}
$plugin_settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $plugin_settings_url ),
Expand All @@ -422,6 +457,38 @@ public static function add_settings_action_link( $links ) {
return $links;
}

/**
* Add Network Settings link to plugin action links on the Network Admin plugins screen.
*
* @since 0.17.0
*
* @param string[] $actions An array of plugin action links.
* @param string $plugin_file Path to the plugin file relative to the plugins directory.
* @param array $plugin_data An array of plugin data. Unused.
* @param string $context The plugin context status. Unused.
* @return string[] Modified array with the Network Settings link added.
*/
public static function add_network_settings_action_link( $actions, $plugin_file, $plugin_data, $context ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed,VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
if ( plugin_basename( TWO_FACTOR_DIR . 'two-factor.php' ) !== $plugin_file ) {
return $actions;
}

if ( ! two_factor_is_network_mode() || ! current_user_can( 'manage_network_options' ) ) {
return $actions;
}

$network_settings_url = network_admin_url( 'settings.php?page=two-factor-network-settings' );
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( $network_settings_url ),
esc_html__( 'Plugin Settings', 'two-factor' )
);

array_unshift( $actions, $settings_link );

return $actions;
}

/**
* Register an error associated with the current request.
*
Expand Down Expand Up @@ -1186,7 +1253,7 @@ public static function login_html( $user, $login_nonce, $redirect_to, $error_msg

foreach ( $backup_providers as $backup_provider_key => $backup_provider ) {
$backup_link_args['provider'] = $backup_provider_key;
$links[] = array(
$links[] = array(
'url' => self::login_url( $backup_link_args ),
'label' => $backup_provider->get_alternative_provider_label(),
);
Expand Down
120 changes: 120 additions & 0 deletions settings/class-two-factor-network-settings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php
/**
* Network Admin settings UI for the Two-Factor plugin.
* Provides a network-wide settings screen for disabling individual Two-Factor
* providers and controlling whether subsites may override the network list.
*
* @since 0.17.0
*
* @package Two_Factor
*/

/**
* Network settings screen renderer for Two-Factor.
*
* @since 0.17.0
*/
class Two_Factor_Network_Settings {

/**
* Render the network settings page.
* Also handles saving of settings when the form is submitted.
*
* @since 0.17.0
*
* @return void
*/
public static function render_settings_page() {
if ( ! current_user_can( 'manage_network_options' ) ) {
return;
}

// Build provider list for display and validation 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();
}
}
$all_provider_keys = array_keys( $provider_instances );

// Handle save.
if ( isset( $_POST['two_factor_network_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_network_settings', 'two_factor_network_settings_nonce' );

$posted = isset( $_POST['two_factor_network_enabled_providers'] ) && is_array( $_POST['two_factor_network_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_network_enabled_providers'] ) : array();

// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
// Remove empty values and keys that are not registered providers.
$enabled = array_values(
array_filter(
array_unique( $posted ),
function ( $key ) use ( $all_provider_keys ) {
return strlen( $key ) && in_array( $key, $all_provider_keys, true );
}
)
);

if ( empty( $enabled ) ) {
echo '<div class="notice notice-error inline"><p>' . esc_html__( 'At least one provider must be enabled at the network level. No changes were saved.', 'two-factor' ) . '</p></div>';
} else {
update_site_option( Two_Factor_Core::ENABLED_PROVIDERS_NETWORK_OPTION_KEY, $enabled );
update_site_option( Two_Factor_Core::NETWORK_ALLOW_SITE_OVERRIDE_OPTION_KEY, ! empty( $_POST['two_factor_network_allow_site_override'] ) ? 1 : 0 );

echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', 'two-factor' ) . '</p></div>';
}
}

// Default to all providers enabled when the option has never been saved.
$saved_enabled = get_site_option( Two_Factor_Core::ENABLED_PROVIDERS_NETWORK_OPTION_KEY, $all_provider_keys );
$allow_override = (bool) get_site_option( Two_Factor_Core::NETWORK_ALLOW_SITE_OVERRIDE_OPTION_KEY, false );

echo '<div class="wrap two-factor-network-settings">';
echo '<h1>' . esc_html__( 'Two-Factor Network Settings', 'two-factor' ) . '</h1>';
echo '<h2>' . esc_html__( 'Enabled Providers', 'two-factor' ) . '</h2>';
echo '<p class="description">' . esc_html__( 'Choose which Two-Factor providers are available across the network. All providers are enabled by default.', 'two-factor' ) . '</p>';
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_network_settings', 'two_factor_network_settings_nonce' );

echo '<fieldset class="two-factor-providers"><legend class="screen-reader-text">' . esc_html__( 'Providers', 'two-factor' ) . '</legend>';
echo '<table class="form-table"><tbody>';

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

echo '<p class="provider-item"><label for="network_provider_' . esc_attr( $provider_key ) . '">';
echo '<input type="checkbox" name="two_factor_network_enabled_providers[]" id="network_provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_enabled, true ), true, false ) . ' /> ';
echo esc_html( $label );
echo '</label></p>';
}

echo '</td>';
echo '</tr>';
}

echo '</tbody></table>';
echo '</fieldset>';

echo '<h2>' . esc_html__( 'Subsite Override', 'two-factor' ) . '</h2>';
echo '<table class="form-table"><tbody><tr><td>';
echo '<p class="provider-item"><label for="two_factor_network_allow_site_override">';
echo '<input type="checkbox" name="two_factor_network_allow_site_override" id="two_factor_network_allow_site_override" value="1" ' . checked( $allow_override, true, false ) . ' /> ';
echo esc_html__( 'Allow subsites to override the network provider list', 'two-factor' );
echo '</label></p>';
echo '<p class="description">' . esc_html__( 'When enabled, a subsite can only narrow the network list. Subsites cannot enable providers that are disabled here.', 'two-factor' ) . '</p>';
echo '</td></tr></tbody></table>';

submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_network_settings_submit' );
echo '</form>';

echo '</div>';
}
}
92 changes: 69 additions & 23 deletions settings/class-two-factor-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,41 +28,83 @@ public static function render_settings_page() {
return;
}

// Handle save.
if ( isset( $_POST['two_factor_settings_submit'] ) ) {
// Build provider list for display and validation 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();
}
}
$all_provider_keys = array_keys( $provider_instances );

// Determine whether the site is operating under a network-level policy.
$network_enabled = null;
$network_override = false;
if ( function_exists( 'two_factor_is_network_mode' ) && two_factor_is_network_mode() ) {
$network_enabled = get_site_option( Two_Factor_Core::ENABLED_PROVIDERS_NETWORK_OPTION_KEY, null );
if ( null !== $network_enabled ) {
$network_override = (bool) get_site_option( Two_Factor_Core::NETWORK_ALLOW_SITE_OVERRIDE_OPTION_KEY, false );
}
}
$network_managed = null !== $network_enabled && ! $network_override;

// Handle save. Site-level values are only meaningful when the network
// allows subsite overrides or when the network has not configured a list.
if ( ! $network_managed && isset( $_POST['two_factor_settings_submit'] ) ) {
check_admin_referer( 'two_factor_save_settings', 'two_factor_settings_nonce' );

$posted = isset( $_POST['two_factor_enabled_providers'] ) && is_array( $_POST['two_factor_enabled_providers'] ) ? wp_unslash( $_POST['two_factor_enabled_providers'] ) : array();

// Sanitize posted values immediately.
$posted = array_map( 'sanitize_text_field', (array) $posted );
// Remove empty values.
$enabled = array_values( array_filter( $posted, 'strlen' ) );
// Remove empty values and keys that are not registered providers.
$enabled = array_values(
array_filter(
array_unique( $posted ),
function ( $key ) use ( $all_provider_keys ) {
return strlen( $key ) && in_array( $key, $all_provider_keys, true );
}
)
);

// When the network allows overrides, a subsite cannot expand beyond the network list.
if ( $network_override && is_array( $network_enabled ) ) {
$enabled = array_values( array_intersect( $enabled, $network_enabled ) );
}

update_option( Two_Factor_Core::ENABLED_PROVIDERS_OPTION_KEY, array_values( array_unique( $enabled ) ) );
update_option( Two_Factor_Core::ENABLED_PROVIDERS_OPTION_KEY, $enabled );

echo '<div class="updated"><p>' . esc_html__( 'Settings saved.', 'two-factor' ) . '</p></div>';
}

// 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 site option has never been saved.
$saved_enabled = get_option( Two_Factor_Core::ENABLED_PROVIDERS_OPTION_KEY, $all_provider_keys );
if ( $network_managed ) {
// Managed by network: show the network list, regardless of any stale site option.
$saved_enabled = $network_enabled;
} elseif ( $network_override && is_array( $network_enabled ) ) {
// Override allowed: show the effective intersection so disabled providers are unchecked.
$saved_enabled = array_values( array_intersect( (array) $saved_enabled, $network_enabled ) );
}

// 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_Core::ENABLED_PROVIDERS_OPTION_KEY, $all_provider_keys );

echo '<div class="wrap two-factor-settings">';
echo '<h1>' . esc_html__( 'Two-Factor Settings', 'two-factor' ) . '</h1>';
echo '<h2>' . esc_html__( 'Enabled Providers', 'two-factor' ) . '</h2>';
echo '<p class="description">' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', 'two-factor' ) . '</p>';
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' );

if ( $network_managed ) {
echo '<div class="notice notice-info inline"><p>' . esc_html__( 'Provider settings are managed at the network level.', 'two-factor' ) . '</p></div>';
echo '<p class="description">' . esc_html__( 'The network administrator has chosen the providers available on this site.', 'two-factor' ) . '</p>';
} elseif ( $network_override ) {
echo '<div class="notice notice-info inline"><p>' . esc_html__( 'The network has enabled the following providers. This site can only narrow the list.', 'two-factor' ) . '</p></div>';
} else {
echo '<p class="description">' . esc_html__( 'Choose which Two-Factor providers are available on this site. All providers are enabled by default.', 'two-factor' ) . '</p>';
}

if ( ! $network_managed ) {
echo '<form method="post" action="">';
wp_nonce_field( 'two_factor_save_settings', 'two_factor_settings_nonce' );
}

echo '<fieldset class="two-factor-providers"><legend class="screen-reader-text">' . esc_html__( 'Providers', 'two-factor' ) . '</legend>';
echo '<table class="form-table"><tbody>';
Expand All @@ -74,10 +116,12 @@ public static function render_settings_page() {
echo '<tr>';
echo '<td>';
foreach ( $provider_instances as $provider_key => $instance ) {
$label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key;
$label = method_exists( $instance, 'get_label' ) ? $instance->get_label() : $provider_key;
$is_in_network = is_array( $network_enabled ) && in_array( $provider_key, $network_enabled, true );
$disabled = $network_managed || ( $network_override && ! $is_in_network );

echo '<p class="provider-item"><label for="provider_' . esc_attr( $provider_key ) . '">';
echo '<input type="checkbox" name="two_factor_enabled_providers[]" id="provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_enabled, true ), true, false ) . ' /> ';
echo '<input type="checkbox" ' . ( $network_managed ? '' : 'name="two_factor_enabled_providers[]" ' ) . 'id="provider_' . esc_attr( $provider_key ) . '" value="' . esc_attr( $provider_key ) . '" ' . checked( in_array( $provider_key, (array) $saved_enabled, true ), true, false ) . ( $disabled ? ' disabled="disabled"' : '' ) . ' /> ';
echo esc_html( $label );
echo '</label></p>';
}
Expand All @@ -89,8 +133,10 @@ public static function render_settings_page() {
echo '</tbody></table>';
echo '</fieldset>';

submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' );
echo '</form>';
if ( ! $network_managed ) {
submit_button( __( 'Save Settings', 'two-factor' ), 'primary', 'two_factor_settings_submit' );
echo '</form>';
}

echo '</div>';
}
Expand Down
Loading
Loading