two-factor-extended/includes/class-network-settings.php
2026-03-28 09:59:03 +00:00

447 lines
13 KiB
PHP

<?php
/**
* Network settings class for Multisite
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Network_Settings
*
* Handles network-wide settings for Multisite installations.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Network_Settings {
/**
* Network settings page slug.
*
* @since 0.1.0
* @var string
*/
const PAGE_SLUG = 'two-factor-extended-network';
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
// Only initialize for Multisite.
if ( ! is_multisite() ) {
return;
}
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
add_action( 'network_admin_menu', array( $this, 'register_network_menu' ) );
add_action( 'network_admin_edit_two_factor_extended_network', array( $this, 'save_network_settings' ) );
}
/**
* Register network admin menu.
*
* @since 0.1.0
*/
public function register_network_menu(): void {
if ( ! current_user_can( 'manage_network_options' ) ) {
return;
}
add_submenu_page(
'settings.php',
__( 'Two Factor Extended', 'two-factor-extended' ),
__( 'Two Factor Extended', 'two-factor-extended' ),
'manage_network_options',
self::PAGE_SLUG,
array( $this, 'render_network_page' )
);
}
/**
* Render network settings page.
*
* @since 0.1.0
*/
public function render_network_page(): void {
// Check capability.
if ( ! current_user_can( 'manage_network_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'two-factor-extended' ) );
}
$settings = $this->get_network_settings();
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php
// Show success message.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only check
if ( isset( $_GET['updated'] ) && 'true' === $_GET['updated'] ) :
?>
<div class="notice notice-success is-dismissible">
<p><?php esc_html_e( 'Network settings saved successfully.', 'two-factor-extended' ); ?></p>
</div>
<?php endif; ?>
<form method="post" action="edit.php?action=two_factor_extended_network">
<?php wp_nonce_field( 'two_factor_extended_network_settings', 'two_factor_extended_network_nonce' ); ?>
<h2><?php esc_html_e( 'Network-Wide Settings', 'two-factor-extended' ); ?></h2>
<table class="form-table">
<tr>
<th scope="row">
<label for="network_enforce">
<?php esc_html_e( 'Network-Wide Enforcement', 'two-factor-extended' ); ?>
</label>
</th>
<td>
<fieldset>
<label>
<input
type="checkbox"
id="network_enforce"
name="network_settings[enforce_network_wide]"
value="1"
<?php checked( $settings['enforce_network_wide'] ?? false ); ?>
/>
<?php esc_html_e( 'Apply these settings to all sites in the network', 'two-factor-extended' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'When enabled, network settings override individual site settings.', 'two-factor-extended' ); ?>
</p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<label for="allow_site_override">
<?php esc_html_e( 'Allow Site Override', 'two-factor-extended' ); ?>
</label>
</th>
<td>
<fieldset>
<label>
<input
type="checkbox"
id="allow_site_override"
name="network_settings[allow_site_override]"
value="1"
<?php checked( $settings['allow_site_override'] ?? false ); ?>
/>
<?php esc_html_e( 'Allow individual sites to override network settings', 'two-factor-extended' ); ?>
</label>
<p class="description">
<?php esc_html_e( 'Site administrators can configure their own 2FA requirements.', 'two-factor-extended' ); ?>
</p>
</fieldset>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Super Admin Requirements', 'two-factor-extended' ); ?>
</th>
<td>
<?php $this->render_super_admin_requirements( $settings ); ?>
</td>
</tr>
<tr>
<th scope="row">
<?php esc_html_e( 'Network Grace Period', 'two-factor-extended' ); ?>
</th>
<td>
<input
type="number"
id="network_grace_period"
name="network_settings[grace_period_days]"
value="<?php echo esc_attr( (string) ( isset( $settings['grace_period_days'] ) && is_numeric( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 7 ) ); ?>"
min="0"
max="365"
class="small-text"
/>
<?php esc_html_e( 'days', 'two-factor-extended' ); ?>
<p class="description">
<?php esc_html_e( 'Grace period for all sites in the network. Set to 0 for immediate enforcement.', 'two-factor-extended' ); ?>
</p>
</td>
</tr>
</table>
<h2><?php esc_html_e( 'Network Role Requirements', 'two-factor-extended' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Configure 2FA requirements that apply across all sites in the network.', 'two-factor-extended' ); ?>
</p>
<?php $this->render_network_role_requirements( $settings ); ?>
<?php submit_button( __( 'Save Network Settings', 'two-factor-extended' ) ); ?>
</form>
</div>
<?php
}
/**
* Render super admin requirements field.
*
* @since 0.1.0
*
* @param array<string, mixed> $settings Current network settings.
*/
private function render_super_admin_requirements( array $settings ): void {
$providers = Two_Factor_Extended_Provider_Detector::get_provider_names();
$super_admin_reqs = isset( $settings['super_admin_requirements'] ) && is_array( $settings['super_admin_requirements'] ) ? $settings['super_admin_requirements'] : array();
if ( empty( $providers ) ) {
?>
<p class="description">
<?php esc_html_e( 'No 2FA providers detected.', 'two-factor-extended' ); ?>
</p>
<?php
return;
}
?>
<fieldset>
<?php foreach ( $providers as $class => $name ) : ?>
<label style="display: block; margin-bottom: 5px;">
<input
type="checkbox"
name="network_settings[super_admin_requirements][]"
value="<?php echo esc_attr( $class ); ?>"
<?php checked( in_array( $class, $super_admin_reqs, true ) ); ?>
/>
<?php echo esc_html( $name ); ?>
</label>
<?php endforeach; ?>
<p class="description">
<?php esc_html_e( 'Required 2FA methods for super administrators.', 'two-factor-extended' ); ?>
</p>
</fieldset>
<?php
}
/**
* Render network role requirements field.
*
* @since 0.1.0
*
* @param array<string, mixed> $settings Current network settings.
*/
private function render_network_role_requirements( array $settings ): void {
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
$providers = Two_Factor_Extended_Provider_Detector::get_provider_names();
$network_reqs = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
if ( empty( $providers ) ) {
?>
<p class="description">
<?php esc_html_e( 'No 2FA providers detected.', 'two-factor-extended' ); ?>
</p>
<?php
return;
}
?>
<table class="widefat fixed striped">
<thead>
<tr>
<th class="column-title"><?php esc_html_e( 'Role', 'two-factor-extended' ); ?></th>
<th><?php esc_html_e( 'Required 2FA Methods', 'two-factor-extended' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $roles as $role_slug => $role_name ) : ?>
<tr>
<td><strong><?php echo esc_html( $role_name ); ?></strong></td>
<td>
<?php
$role_reqs = isset( $network_reqs[ $role_slug ] ) && is_array( $network_reqs[ $role_slug ] ) ? $network_reqs[ $role_slug ] : array();
foreach ( $providers as $class => $name ) :
?>
<label style="display: block; margin-bottom: 5px;">
<input
type="checkbox"
name="network_settings[role_requirements][<?php echo esc_attr( $role_slug ); ?>][]"
value="<?php echo esc_attr( $class ); ?>"
<?php checked( in_array( $class, $role_reqs, true ) ); ?>
/>
<?php echo esc_html( $name ); ?>
</label>
<?php endforeach; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php
}
/**
* Save network settings.
*
* @since 0.1.0
*/
public function save_network_settings(): void {
// Check capability.
if ( ! current_user_can( 'manage_network_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'two-factor-extended' ) );
}
// Verify nonce.
$nonce = isset( $_POST['two_factor_extended_network_nonce'] ) && is_string( $_POST['two_factor_extended_network_nonce'] )
? sanitize_text_field( wp_unslash( $_POST['two_factor_extended_network_nonce'] ) )
: '';
if ( ! wp_verify_nonce( $nonce, 'two_factor_extended_network_settings' ) ) {
wp_die( esc_html__( 'Security check failed.', 'two-factor-extended' ) );
}
// Get and sanitize input.
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Data sanitized by sanitize_network_settings() method before use
$input = isset( $_POST['network_settings'] ) && is_array( $_POST['network_settings'] )
? wp_unslash( $_POST['network_settings'] )
: array();
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$sanitized = $this->sanitize_network_settings( $input );
// Save settings.
update_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, $sanitized );
// Redirect with success message.
wp_safe_redirect(
add_query_arg(
'updated',
'true',
network_admin_url( 'settings.php?page=' . self::PAGE_SLUG )
)
);
exit;
}
/**
* Get network settings.
*
* @since 0.1.0
*
* @return array<string, mixed> Network settings.
*/
public function get_network_settings(): array {
$defaults = array(
'enforce_network_wide' => false,
'allow_site_override' => true,
'grace_period_days' => 7,
'super_admin_requirements' => array(),
'role_requirements' => array(),
);
$settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
return wp_parse_args( $settings, $defaults );
}
/**
* Sanitize network settings.
*
* @since 0.1.0
*
* @param array<string, mixed> $input Raw input.
*
* @return array<string, mixed> Sanitized settings.
*/
private function sanitize_network_settings( array $input ): array {
$sanitized = array();
// Enforce network wide.
$sanitized['enforce_network_wide'] = ! empty( $input['enforce_network_wide'] );
// Allow site override.
$sanitized['allow_site_override'] = ! empty( $input['allow_site_override'] );
// Grace period days.
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
$days = (int) $input['grace_period_days'];
$sanitized['grace_period_days'] = max( 0, min( 365, $days ) );
}
// Super admin requirements.
if ( isset( $input['super_admin_requirements'] ) && is_array( $input['super_admin_requirements'] ) ) {
$sanitized['super_admin_requirements'] = array_map(
function ( mixed $item ): string {
return sanitize_text_field( is_string( $item ) ? $item : '' );
},
$input['super_admin_requirements']
);
}
// Role requirements.
if ( isset( $input['role_requirements'] ) && is_array( $input['role_requirements'] ) ) {
$sanitized['role_requirements'] = array();
foreach ( $input['role_requirements'] as $role => $providers ) {
$role_slug = sanitize_key( is_string( $role ) ? $role : '' );
if ( Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) && is_array( $providers ) ) {
$sanitized['role_requirements'][ $role_slug ] = array_map(
function ( mixed $item ): string {
return sanitize_text_field( is_string( $item ) ? $item : '' );
},
$providers
);
}
}
}
return $sanitized;
}
/**
* Check if network enforcement is enabled.
*
* @since 0.1.0
*
* @return bool True if network enforcement is enabled.
*/
public function is_network_enforcement_enabled(): bool {
$settings = $this->get_network_settings();
return ! empty( $settings['enforce_network_wide'] );
}
/**
* Check if site override is allowed.
*
* @since 0.1.0
*
* @return bool True if site override is allowed.
*/
public function is_site_override_allowed(): bool {
$settings = $this->get_network_settings();
return ! empty( $settings['allow_site_override'] );
}
}