1961 lines
62 KiB
PHP
1961 lines
62 KiB
PHP
<?php
|
|
/**
|
|
* Settings management class
|
|
*
|
|
* @package TwoFactorExtended
|
|
* @since 0.1.0
|
|
*/
|
|
|
|
// Prevent direct access.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Class Two_Factor_Extended_Settings
|
|
*
|
|
* Handles plugin settings and admin UI.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
class Two_Factor_Extended_Settings {
|
|
|
|
/**
|
|
* Settings page slug.
|
|
*
|
|
* @since 0.1.0
|
|
* @var string
|
|
*/
|
|
const PAGE_SLUG = 'two-factor-extended';
|
|
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function __construct() {
|
|
$this->init_hooks();
|
|
}
|
|
|
|
/**
|
|
* Initialize WordPress hooks.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function init_hooks(): void {
|
|
// Priority 20 ensures our page registers after Two Factor (default priority 10).
|
|
add_action( 'admin_menu', array( $this, 'register_settings_page' ), 20 );
|
|
add_action( 'network_admin_menu', array( $this, 'register_network_settings_page' ), 20 );
|
|
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
|
add_action( 'admin_init', array( $this, 'handle_import_export' ) );
|
|
add_action( 'admin_init', array( $this, 'handle_reset_settings' ) );
|
|
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_admin_scripts' ) );
|
|
}
|
|
|
|
/**
|
|
* Register settings page in admin menu.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function register_settings_page(): void {
|
|
// Check capability.
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
return;
|
|
}
|
|
|
|
// Main settings page with tabs (Settings, Audit Log, Compliance).
|
|
add_options_page(
|
|
__( 'Two Factor Extended', 'two-factor-extended' ),
|
|
__( 'Two Factor Extended', 'two-factor-extended' ),
|
|
'manage_options',
|
|
self::PAGE_SLUG,
|
|
array( $this, 'render_settings_page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register network settings page for Multisite.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function register_network_settings_page(): void {
|
|
// Only for Multisite.
|
|
if ( ! is_multisite() ) {
|
|
return;
|
|
}
|
|
|
|
// Check capability.
|
|
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_settings_page' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register settings sections and fields.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function register_settings(): void {
|
|
// Register main settings.
|
|
register_setting(
|
|
TWO_FACTOR_EXTENDED_OPTION_SETTINGS,
|
|
TWO_FACTOR_EXTENDED_OPTION_SETTINGS,
|
|
array(
|
|
'type' => 'array',
|
|
'sanitize_callback' => array( $this, 'sanitize_settings' ),
|
|
'default' => $this->get_default_settings(),
|
|
)
|
|
);
|
|
|
|
// Register data removal option.
|
|
register_setting(
|
|
TWO_FACTOR_EXTENDED_OPTION_REMOVE_DATA,
|
|
TWO_FACTOR_EXTENDED_OPTION_REMOVE_DATA,
|
|
array(
|
|
'type' => 'boolean',
|
|
'sanitize_callback' => 'rest_sanitize_boolean',
|
|
'default' => false,
|
|
)
|
|
);
|
|
|
|
// Register sections.
|
|
$this->register_sections();
|
|
|
|
// Register fields.
|
|
$this->register_fields();
|
|
}
|
|
|
|
/**
|
|
* Register settings sections.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function register_sections(): void {
|
|
// General settings section.
|
|
add_settings_section(
|
|
'two_factor_extended_general',
|
|
__( 'General Settings', 'two-factor-extended' ),
|
|
array( $this, 'render_general_section' ),
|
|
self::PAGE_SLUG
|
|
);
|
|
|
|
// Role requirements section.
|
|
add_settings_section(
|
|
'two_factor_extended_role_requirements',
|
|
__( 'Role-Based 2FA Requirements', 'two-factor-extended' ),
|
|
array( $this, 'render_role_requirements_section' ),
|
|
self::PAGE_SLUG
|
|
);
|
|
|
|
// Provider visibility section.
|
|
add_settings_section(
|
|
'two_factor_extended_provider_visibility',
|
|
__( 'Role-Based Provider Visibility', 'two-factor-extended' ),
|
|
array( $this, 'render_provider_visibility_section' ),
|
|
self::PAGE_SLUG
|
|
);
|
|
|
|
// Data management section.
|
|
add_settings_section(
|
|
'two_factor_extended_data',
|
|
__( 'Data Management', 'two-factor-extended' ),
|
|
array( $this, 'render_data_section' ),
|
|
self::PAGE_SLUG
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Register settings fields.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function register_fields(): void {
|
|
// Grace period field.
|
|
add_settings_field(
|
|
'grace_period_days',
|
|
__( 'Grace Period (Days)', 'two-factor-extended' ),
|
|
array( $this, 'render_number_field' ),
|
|
self::PAGE_SLUG,
|
|
'two_factor_extended_general',
|
|
array(
|
|
'option_name' => TWO_FACTOR_EXTENDED_OPTION_SETTINGS,
|
|
'id' => 'grace_period_days',
|
|
'field_key' => 'grace_period_days',
|
|
'description' => __( 'Number of days users have to configure required 2FA methods before enforcement. Set to 0 for immediate enforcement.', 'two-factor-extended' ),
|
|
'min' => 0,
|
|
'max' => 365,
|
|
'default' => 7,
|
|
)
|
|
);
|
|
|
|
// Role requirements field.
|
|
add_settings_field(
|
|
'role_requirements',
|
|
__( 'Role Requirements', 'two-factor-extended' ),
|
|
array( $this, 'render_role_requirements_field' ),
|
|
self::PAGE_SLUG,
|
|
'two_factor_extended_role_requirements',
|
|
array()
|
|
);
|
|
|
|
// Provider visibility field.
|
|
add_settings_field(
|
|
'provider_visibility',
|
|
__( 'Provider Visibility', 'two-factor-extended' ),
|
|
array( $this, 'render_provider_visibility_field' ),
|
|
self::PAGE_SLUG,
|
|
'two_factor_extended_provider_visibility',
|
|
array()
|
|
);
|
|
|
|
// Data removal field.
|
|
add_settings_field(
|
|
'remove_data_on_uninstall',
|
|
__( 'Remove Data on Uninstall', 'two-factor-extended' ),
|
|
array( $this, 'render_checkbox_field' ),
|
|
self::PAGE_SLUG,
|
|
'two_factor_extended_data',
|
|
array(
|
|
'option_name' => TWO_FACTOR_EXTENDED_OPTION_REMOVE_DATA,
|
|
'id' => 'remove_data_on_uninstall',
|
|
'label' => __( 'Delete all plugin data when the plugin is uninstalled', 'two-factor-extended' ),
|
|
'description' => __( 'Warning: This action cannot be undone. All plugin settings and data will be permanently deleted.', 'two-factor-extended' ),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render general settings section.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_general_section(): void {
|
|
?>
|
|
<p>
|
|
<?php esc_html_e( 'Configure general plugin settings.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
$this->render_network_inheritance_notice();
|
|
}
|
|
|
|
/**
|
|
* Render role requirements section.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_role_requirements_section(): void {
|
|
?>
|
|
<p>
|
|
<?php esc_html_e( 'Configure which 2FA methods are required for each user role. Users with multiple roles must satisfy all role requirements.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render provider visibility section.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_provider_visibility_section(): void {
|
|
?>
|
|
<p>
|
|
<?php esc_html_e( 'Control which 2FA methods are visible to each role. Users with multiple roles see methods visible to ANY of their roles (union logic). Required methods are always visible.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render data management section.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_data_section(): void {
|
|
?>
|
|
<p>
|
|
<?php esc_html_e( 'Manage plugin data and cleanup options.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
|
|
<h3><?php esc_html_e( 'Import / Export Settings', 'two-factor-extended' ); ?></h3>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th scope="row">
|
|
<?php esc_html_e( 'Export Settings', 'two-factor-extended' ); ?>
|
|
</th>
|
|
<td>
|
|
<form method="post" style="display: inline;">
|
|
<?php wp_nonce_field( 'two_factor_extended_export', 'two_factor_extended_export_nonce' ); ?>
|
|
<button type="submit" name="two_factor_extended_export" class="button">
|
|
<?php esc_html_e( 'Export Settings', 'two-factor-extended' ); ?>
|
|
</button>
|
|
</form>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Export all plugin settings to a JSON file. This can be used to backup or transfer settings to another site.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<th scope="row">
|
|
<?php esc_html_e( 'Import Settings', 'two-factor-extended' ); ?>
|
|
</th>
|
|
<td>
|
|
<form method="post" enctype="multipart/form-data" id="two-factor-extended-import-form">
|
|
<?php wp_nonce_field( 'two_factor_extended_import', 'two_factor_extended_import_nonce' ); ?>
|
|
<input
|
|
type="file"
|
|
name="two_factor_extended_import_file"
|
|
id="two_factor_extended_import_file"
|
|
accept=".json,application/json"
|
|
required
|
|
aria-required="true"
|
|
aria-describedby="import-file-description"
|
|
/>
|
|
<button type="submit" name="two_factor_extended_import" class="button">
|
|
<?php esc_html_e( 'Import Settings', 'two-factor-extended' ); ?>
|
|
</button>
|
|
</form>
|
|
<p class="description" id="import-file-description">
|
|
<?php esc_html_e( 'Import plugin settings from a previously exported JSON file. This will overwrite your current settings. Maximum file size: 1MB.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<h3><?php esc_html_e( 'Reset Plugin', 'two-factor-extended' ); ?></h3>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th scope="row">
|
|
<?php esc_html_e( 'Reset to Default Settings', 'two-factor-extended' ); ?>
|
|
</th>
|
|
<td>
|
|
<form method="post" id="two-factor-extended-reset-form" onsubmit="return confirm('<?php echo esc_js( __( 'Are you sure you want to reset all plugin settings to their default values? This action cannot be undone.', 'two-factor-extended' ) ); ?>');">
|
|
<?php wp_nonce_field( 'two_factor_extended_reset', 'two_factor_extended_reset_nonce' ); ?>
|
|
<button type="submit" name="two_factor_extended_reset" class="button button-secondary">
|
|
<?php esc_html_e( 'Reset All Settings', 'two-factor-extended' ); ?>
|
|
</button>
|
|
</form>
|
|
<p class="description" style="color: #d63638;">
|
|
<?php esc_html_e( 'Warning: This will reset all plugin settings to their default values, including role requirements and provider visibility. User grace period data will be cleared. This action cannot be undone.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render number field.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $args Field arguments.
|
|
*/
|
|
public function render_number_field( array $args ): void {
|
|
$option_name = isset( $args['option_name'] ) && is_string( $args['option_name'] ) ? $args['option_name'] : '';
|
|
$id = isset( $args['id'] ) && is_string( $args['id'] ) ? $args['id'] : '';
|
|
$field_key = isset( $args['field_key'] ) && is_string( $args['field_key'] ) ? $args['field_key'] : '';
|
|
$description = isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '';
|
|
$min = isset( $args['min'] ) && is_numeric( $args['min'] ) ? (int) $args['min'] : 0;
|
|
$max = isset( $args['max'] ) && is_numeric( $args['max'] ) ? (int) $args['max'] : 999;
|
|
$default = isset( $args['default'] ) && is_numeric( $args['default'] ) ? (int) $args['default'] : 0;
|
|
|
|
if ( empty( $option_name ) || empty( $id ) ) {
|
|
return;
|
|
}
|
|
|
|
$settings = get_option( $option_name, array() );
|
|
$value = is_array( $settings ) && isset( $settings[ $field_key ] ) && is_numeric( $settings[ $field_key ] ) ? (int) $settings[ $field_key ] : $default;
|
|
?>
|
|
<input
|
|
type="number"
|
|
id="<?php echo esc_attr( $id ); ?>"
|
|
name="<?php echo esc_attr( $option_name . '[' . $field_key . ']' ); ?>"
|
|
value="<?php echo esc_attr( (string) $value ); ?>"
|
|
min="<?php echo esc_attr( (string) $min ); ?>"
|
|
max="<?php echo esc_attr( (string) $max ); ?>"
|
|
step="1"
|
|
required
|
|
aria-required="true"
|
|
class="small-text"
|
|
/>
|
|
<?php if ( ! empty( $description ) ) : ?>
|
|
<p class="description">
|
|
<?php echo esc_html( $description ); ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render role requirements field.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $args Field arguments.
|
|
*/
|
|
public function render_role_requirements_field( array $args ): void {
|
|
// Clear any object cache to ensure fresh data.
|
|
wp_cache_delete( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, 'options' );
|
|
wp_cache_flush();
|
|
|
|
$raw_settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
|
$settings = is_array( $raw_settings ) ? $raw_settings : array();
|
|
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
|
|
|
// Force fresh provider detection directly from Two Factor Core.
|
|
$providers = array();
|
|
if ( class_exists( 'Two_Factor_Core' ) ) {
|
|
$all_providers = Two_Factor_Core::get_providers();
|
|
|
|
foreach ( $all_providers as $class_name => $provider ) {
|
|
if ( is_object( $provider ) && method_exists( $provider, 'get_label' ) ) {
|
|
$providers[ $class_name ] = $provider->get_label();
|
|
} else {
|
|
$providers[ $class_name ] = $class_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $providers ) ) {
|
|
?>
|
|
<p class="description">
|
|
<?php esc_html_e( 'No 2FA providers detected. Please ensure the Two Factor plugin is active and properly configured.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
return;
|
|
}
|
|
|
|
$requirements = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
|
|
?>
|
|
<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_req_raw = $requirements[ $role_slug ] ?? array();
|
|
$role_requirements = is_array( $role_req_raw ) ? $role_req_raw : array();
|
|
|
|
foreach ( $providers as $provider_class => $provider_name ) :
|
|
$checked = in_array( $provider_class, $role_requirements, true );
|
|
?>
|
|
<label style="display: block; margin-bottom: 5px;">
|
|
<input
|
|
type="checkbox"
|
|
name="<?php echo esc_attr( TWO_FACTOR_EXTENDED_OPTION_SETTINGS . '[role_requirements][' . $role_slug . '][]' ); ?>"
|
|
value="<?php echo esc_attr( $provider_class ); ?>"
|
|
<?php checked( $checked ); ?>
|
|
/>
|
|
<?php echo esc_html( $provider_name ); ?>
|
|
</label>
|
|
<?php endforeach; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Select which 2FA methods each role must have configured. Users will not be able to log in without these methods.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render provider visibility field.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $args Field arguments.
|
|
*/
|
|
public function render_provider_visibility_field( array $args ): void {
|
|
// Clear any object cache to ensure fresh data.
|
|
wp_cache_delete( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, 'options' );
|
|
wp_cache_flush();
|
|
|
|
$raw_settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
|
$settings = is_array( $raw_settings ) ? $raw_settings : array();
|
|
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
|
|
|
// Force fresh provider detection directly from Two Factor Core.
|
|
$providers = array();
|
|
if ( class_exists( 'Two_Factor_Core' ) ) {
|
|
$all_providers = Two_Factor_Core::get_providers();
|
|
|
|
foreach ( $all_providers as $class_name => $provider ) {
|
|
if ( is_object( $provider ) && method_exists( $provider, 'get_label' ) ) {
|
|
$providers[ $class_name ] = $provider->get_label();
|
|
} else {
|
|
$providers[ $class_name ] = $class_name;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( empty( $providers ) ) {
|
|
?>
|
|
<p class="description">
|
|
<?php esc_html_e( 'No 2FA providers detected. Please ensure the Two Factor plugin is active and properly configured.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
return;
|
|
}
|
|
|
|
$visibility = isset( $settings['provider_visibility'] ) && is_array( $settings['provider_visibility'] ) ? $settings['provider_visibility'] : array();
|
|
$requirements = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
|
|
?>
|
|
<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( 'Visible 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_vis_raw = $visibility[ $role_slug ] ?? array();
|
|
$role_req_raw = $requirements[ $role_slug ] ?? array();
|
|
$role_visibility = is_array( $role_vis_raw ) ? $role_vis_raw : array();
|
|
$role_required = is_array( $role_req_raw ) ? $role_req_raw : array();
|
|
|
|
foreach ( $providers as $provider_class => $provider_name ) :
|
|
$checked = in_array( $provider_class, $role_visibility, true );
|
|
$is_required = in_array( $provider_class, $role_required, true );
|
|
$disabled = $is_required ? 'disabled' : '';
|
|
$checked = $checked || $is_required;
|
|
?>
|
|
<label style="display: block; margin-bottom: 5px;">
|
|
<input
|
|
type="checkbox"
|
|
name="<?php echo esc_attr( TWO_FACTOR_EXTENDED_OPTION_SETTINGS . '[provider_visibility][' . $role_slug . '][]' ); ?>"
|
|
value="<?php echo esc_attr( $provider_class ); ?>"
|
|
<?php checked( $checked ); ?>
|
|
<?php echo esc_attr( $disabled ); ?>
|
|
/>
|
|
<?php echo esc_html( $provider_name ); ?>
|
|
<?php if ( $is_required ) : ?>
|
|
<em style="color: #d63638;"><?php esc_html_e( '(Required)', 'two-factor-extended' ); ?></em>
|
|
<?php endif; ?>
|
|
</label>
|
|
<?php
|
|
// Add hidden input for required providers so they're always included.
|
|
if ( $is_required ) :
|
|
?>
|
|
<input
|
|
type="hidden"
|
|
name="<?php echo esc_attr( TWO_FACTOR_EXTENDED_OPTION_SETTINGS . '[provider_visibility][' . $role_slug . '][]' ); ?>"
|
|
value="<?php echo esc_attr( $provider_class ); ?>"
|
|
/>
|
|
<?php
|
|
endif;
|
|
endforeach;
|
|
?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Uncheck methods to hide them from users with this role. Required methods are always visible and cannot be hidden.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render checkbox field.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $args Field arguments.
|
|
*/
|
|
public function render_checkbox_field( array $args ): void {
|
|
$option_name = isset( $args['option_name'] ) && is_string( $args['option_name'] ) ? $args['option_name'] : '';
|
|
$id = isset( $args['id'] ) && is_string( $args['id'] ) ? $args['id'] : '';
|
|
$label = isset( $args['label'] ) && is_string( $args['label'] ) ? $args['label'] : '';
|
|
$description = isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '';
|
|
|
|
if ( empty( $option_name ) || empty( $id ) ) {
|
|
return;
|
|
}
|
|
|
|
$value = get_option( $option_name, false );
|
|
?>
|
|
<fieldset>
|
|
<label for="<?php echo esc_attr( $id ); ?>">
|
|
<input
|
|
type="checkbox"
|
|
id="<?php echo esc_attr( $id ); ?>"
|
|
name="<?php echo esc_attr( $option_name ); ?>"
|
|
value="1"
|
|
<?php checked( $value, true ); ?>
|
|
/>
|
|
<?php echo esc_html( $label ); ?>
|
|
</label>
|
|
<?php if ( ! empty( $description ) ) : ?>
|
|
<p class="description">
|
|
<?php echo esc_html( $description ); ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
</fieldset>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render network inheritance notice.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function render_network_inheritance_notice(): void {
|
|
if ( ! is_multisite() ) {
|
|
return;
|
|
}
|
|
|
|
$network_settings_raw = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
|
$network_settings = is_array( $network_settings_raw ) ? $network_settings_raw : array();
|
|
|
|
if ( empty( $network_settings['enforce_network_wide'] ) ) {
|
|
return;
|
|
}
|
|
|
|
$can_override = ! empty( $network_settings['allow_site_override'] );
|
|
|
|
?>
|
|
<div class="notice notice-info inline">
|
|
<p>
|
|
<strong><?php esc_html_e( 'Network Settings Active:', 'two-factor-extended' ); ?></strong>
|
|
<?php
|
|
if ( $can_override ) {
|
|
esc_html_e( 'This site inherits settings from the network, but you can override them below.', 'two-factor-extended' );
|
|
} else {
|
|
esc_html_e( 'This site inherits settings from the network. Site-level changes are disabled.', 'two-factor-extended' );
|
|
}
|
|
?>
|
|
</p>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Enqueue admin scripts.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param string $hook Current admin page hook.
|
|
*/
|
|
public function enqueue_admin_scripts( string $hook ): void {
|
|
// Only load on our settings page.
|
|
if ( 'settings_page_two-factor-extended' !== $hook ) {
|
|
return;
|
|
}
|
|
|
|
// Inline CSS for tabs.
|
|
$css = '
|
|
.tab-content {
|
|
margin-top: 20px;
|
|
}
|
|
.tab-content .card {
|
|
max-width: none;
|
|
margin-bottom: 20px;
|
|
}
|
|
';
|
|
wp_add_inline_style( 'wp-admin', $css );
|
|
|
|
// Inline script for import validation.
|
|
$script = "
|
|
(function() {
|
|
const importForm = document.getElementById('two-factor-extended-import-form');
|
|
if (importForm) {
|
|
importForm.addEventListener('submit', function(e) {
|
|
const fileInput = document.getElementById('two_factor_extended_import_file');
|
|
if (!fileInput || !fileInput.files.length) {
|
|
return;
|
|
}
|
|
|
|
const file = fileInput.files[0];
|
|
const maxSize = 1024 * 1024; // 1MB
|
|
|
|
// Check file size.
|
|
if (file.size > maxSize) {
|
|
e.preventDefault();
|
|
alert('" . esc_js( __( 'File size exceeds 1MB limit. Please use a smaller file.', 'two-factor-extended' ) ) . "');
|
|
return false;
|
|
}
|
|
|
|
// Check file extension.
|
|
if (!file.name.endsWith('.json')) {
|
|
e.preventDefault();
|
|
alert('" . esc_js( __( 'Please select a valid JSON file (.json extension).', 'two-factor-extended' ) ) . "');
|
|
return false;
|
|
}
|
|
|
|
// Confirm before proceeding.
|
|
if (!confirm('" . esc_js( __( 'Importing will overwrite your current settings. Are you sure you want to continue?', 'two-factor-extended' ) ) . "')) {
|
|
e.preventDefault();
|
|
return false;
|
|
}
|
|
});
|
|
}
|
|
})();
|
|
";
|
|
|
|
wp_add_inline_script( 'jquery', $script );
|
|
}
|
|
|
|
/**
|
|
* Handle import/export actions.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function handle_import_export(): void {
|
|
// Check if export action.
|
|
if ( isset( $_POST['two_factor_extended_export'] ) ) {
|
|
// Verify nonce and capability.
|
|
check_admin_referer( 'two_factor_extended_export', 'two_factor_extended_export_nonce' );
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
$this->export_settings();
|
|
}
|
|
|
|
// Check if import action.
|
|
if ( isset( $_POST['two_factor_extended_import'] ) && isset( $_FILES['two_factor_extended_import_file'] ) ) {
|
|
// Verify nonce and capability.
|
|
check_admin_referer( 'two_factor_extended_import', 'two_factor_extended_import_nonce' );
|
|
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
$this->import_settings();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export settings to JSON file.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @throws Exception If JSON encoding fails.
|
|
*/
|
|
private function export_settings(): void {
|
|
try {
|
|
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
|
|
|
$export_data = array(
|
|
'version' => TWO_FACTOR_EXTENDED_VERSION,
|
|
'timestamp' => current_time( 'timestamp' ),
|
|
'settings' => $settings,
|
|
);
|
|
|
|
// Encode JSON.
|
|
$json = wp_json_encode( $export_data, JSON_PRETTY_PRINT );
|
|
|
|
if ( false === $json ) {
|
|
throw new Exception( 'Failed to encode settings to JSON' );
|
|
}
|
|
|
|
// Log the export.
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
if ( null !== $audit_log ) {
|
|
$audit_log->log_event(
|
|
'settings_exported',
|
|
'Plugin settings exported',
|
|
0
|
|
);
|
|
}
|
|
|
|
// Set headers for download.
|
|
header( 'Content-Type: application/json; charset=utf-8' );
|
|
header( 'Content-Disposition: attachment; filename=two-factor-extended-settings-' . gmdate( 'Y-m-d-H-i-s' ) . '.json' );
|
|
header( 'Pragma: no-cache' );
|
|
header( 'Expires: 0' );
|
|
|
|
echo wp_json_encode( json_decode( $json ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already JSON encoded
|
|
exit;
|
|
} catch ( Exception $e ) {
|
|
// Log error without exposing details to user.
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
if ( null !== $audit_log ) {
|
|
$audit_log->log_event(
|
|
'settings_export_failed',
|
|
'Failed to export settings',
|
|
0,
|
|
array( 'error' => 'Export failed' )
|
|
);
|
|
}
|
|
|
|
wp_die(
|
|
esc_html__( 'Unable to export settings. Please try again later.', 'two-factor-extended' ),
|
|
esc_html__( 'Export Error', 'two-factor-extended' ),
|
|
array( 'response' => 500 )
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Import settings from JSON file.
|
|
*
|
|
* Note: Nonce verification is performed in handle_import_export() at line 764
|
|
* via check_admin_referer() before calling this method. This is a private
|
|
* method that should only be called after nonce and capability checks.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function import_settings(): void {
|
|
/*
|
|
* Disable PHPCS warnings for $_FILES access in this method.
|
|
* Justification:
|
|
* - Nonce is verified in handle_import_export() line 764 via check_admin_referer()
|
|
* - This is a private method only called after nonce and capability verification
|
|
* - All file data is validated below (size, extension, content)
|
|
* - File name is sanitized with sanitize_file_name()
|
|
* - Content is decoded and validated as JSON before use
|
|
*/
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
|
// phpcs:disable WordPress.Security.ValidatedSanitizedInput
|
|
|
|
// Check file upload.
|
|
$upload_file = isset( $_FILES['two_factor_extended_import_file'] ) && is_array( $_FILES['two_factor_extended_import_file'] )
|
|
? $_FILES['two_factor_extended_import_file']
|
|
: array();
|
|
|
|
$upload_error = isset( $upload_file['error'] ) && is_int( $upload_file['error'] ) ? $upload_file['error'] : UPLOAD_ERR_NO_FILE;
|
|
$upload_size = isset( $upload_file['size'] ) && is_int( $upload_file['size'] ) ? $upload_file['size'] : 0;
|
|
$upload_name = isset( $upload_file['name'] ) && is_string( $upload_file['name'] ) ? $upload_file['name'] : '';
|
|
$upload_tmp_name = isset( $upload_file['tmp_name'] ) && is_string( $upload_file['tmp_name'] ) ? $upload_file['tmp_name'] : '';
|
|
|
|
if ( empty( $upload_file ) || UPLOAD_ERR_OK !== $upload_error ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'Error uploading file. Please try again.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check file size (1MB limit).
|
|
$max_size = 1024 * 1024; // 1MB.
|
|
if ( $upload_size > $max_size ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'File size exceeds 1MB limit. Please use a smaller file.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Check file extension.
|
|
$file_name = sanitize_file_name( $upload_name );
|
|
if ( ! str_ends_with( strtolower( $file_name ), '.json' ) ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'Invalid file type. Please upload a JSON file.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Read file contents.
|
|
$file_content = file_get_contents( $upload_tmp_name );
|
|
|
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
|
// phpcs:enable WordPress.Security.ValidatedSanitizedInput
|
|
|
|
if ( false === $file_content ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'Error reading file. Please try again.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Decode JSON.
|
|
$import_data = json_decode( $file_content, true );
|
|
|
|
if ( null === $import_data || ! is_array( $import_data ) ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'Invalid JSON file. Please upload a valid settings export file.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Validate import data structure.
|
|
if ( ! isset( $import_data['settings'] ) || ! is_array( $import_data['settings'] ) ) {
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_error',
|
|
__( 'Invalid settings file structure.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Sanitize and update settings. Ensure keys are strings (from JSON decode).
|
|
$settings_to_import = array();
|
|
foreach ( $import_data['settings'] as $key => $value ) {
|
|
if ( is_string( $key ) ) {
|
|
$settings_to_import[ $key ] = $value;
|
|
}
|
|
}
|
|
$sanitized_settings = $this->sanitize_settings( $settings_to_import );
|
|
update_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, $sanitized_settings );
|
|
|
|
// Log the import.
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
|
|
if ( null !== $audit_log ) {
|
|
$audit_log->log_event(
|
|
'settings_imported',
|
|
'Plugin settings imported',
|
|
0,
|
|
array(
|
|
'imported_version' => $import_data['version'] ?? 'unknown',
|
|
)
|
|
);
|
|
}
|
|
|
|
add_settings_error(
|
|
'two_factor_extended_import',
|
|
'import_success',
|
|
__( 'Settings imported successfully!', 'two-factor-extended' ),
|
|
'success'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render settings page.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_settings_page(): void {
|
|
// Check capability.
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
// Get current tab.
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab parameter for UI navigation
|
|
$tab_param = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? $_GET['tab'] : '';
|
|
$current_tab = $tab_param ? sanitize_key( $tab_param ) : 'settings';
|
|
|
|
// Define tabs.
|
|
$tabs = array(
|
|
'settings' => __( 'Settings', 'two-factor-extended' ),
|
|
'audit-log' => __( 'Audit Log', 'two-factor-extended' ),
|
|
'compliance' => __( 'Compliance', 'two-factor-extended' ),
|
|
);
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
|
|
|
<?php settings_errors(); ?>
|
|
|
|
<!-- Tab Navigation -->
|
|
<h2 class="nav-tab-wrapper">
|
|
<?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
|
|
<a href="<?php echo esc_url( add_query_arg( 'tab', $tab_key, admin_url( 'options-general.php?page=' . self::PAGE_SLUG ) ) ); ?>"
|
|
class="nav-tab <?php echo $current_tab === $tab_key ? 'nav-tab-active' : ''; ?>">
|
|
<?php echo esc_html( $tab_label ); ?>
|
|
</a>
|
|
<?php endforeach; ?>
|
|
</h2>
|
|
|
|
<!-- Tab Content -->
|
|
<div class="tab-content">
|
|
<?php
|
|
switch ( $current_tab ) {
|
|
case 'audit-log':
|
|
$this->render_audit_log_tab();
|
|
break;
|
|
case 'compliance':
|
|
$this->render_compliance_tab();
|
|
break;
|
|
case 'settings':
|
|
default:
|
|
$this->render_settings_tab();
|
|
break;
|
|
}
|
|
?>
|
|
</div>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render settings tab content.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
private function render_settings_tab(): void {
|
|
// Check if site settings are disabled by network.
|
|
$settings_disabled = $this->are_site_settings_disabled();
|
|
|
|
if ( $settings_disabled ) :
|
|
?>
|
|
<div class="notice notice-warning">
|
|
<p>
|
|
<?php esc_html_e( 'Site-level settings are currently disabled by network administrator. Please contact your network administrator to make changes.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<form method="post" action="options.php">
|
|
<?php
|
|
settings_fields( TWO_FACTOR_EXTENDED_OPTION_SETTINGS );
|
|
do_settings_sections( self::PAGE_SLUG );
|
|
|
|
if ( ! $settings_disabled ) {
|
|
submit_button();
|
|
}
|
|
?>
|
|
</form>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render audit log tab content.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
private function render_audit_log_tab(): void {
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
|
|
if ( null === $audit_log ) {
|
|
echo '<div class="notice notice-error"><p>' . esc_html__( 'Audit log module is not available.', 'two-factor-extended' ) . '</p></div>';
|
|
return;
|
|
}
|
|
|
|
// Handle export.
|
|
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_export_audit' );
|
|
|
|
$csv = $audit_log->export_to_csv();
|
|
|
|
header( 'Content-Type: text/csv' );
|
|
header( 'Content-Disposition: attachment; filename="audit-log-' . gmdate( 'Y-m-d' ) . '.csv"' );
|
|
echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
exit;
|
|
}
|
|
|
|
// Handle clear logs.
|
|
if ( isset( $_POST['action'] ) && 'clear' === $_POST['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_clear_audit' );
|
|
$audit_log->clear_logs();
|
|
echo '<div class="notice notice-success"><p>' . esc_html__( 'Audit logs cleared successfully.', 'two-factor-extended' ) . '</p></div>';
|
|
}
|
|
|
|
$logs = $audit_log->get_logs();
|
|
$stats = $audit_log->get_statistics();
|
|
|
|
?>
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Statistics', 'two-factor-extended' ); ?></h2>
|
|
<p>
|
|
<?php
|
|
/* translators: %d: Total number of audit logs */
|
|
printf( esc_html__( 'Total logs: %d', 'two-factor-extended' ), (int) $stats['total'] );
|
|
?>
|
|
</p>
|
|
<p>
|
|
<?php
|
|
/* translators: %d: Number of recent audit logs */
|
|
printf( esc_html__( 'Recent activity (7 days): %d', 'two-factor-extended' ), (int) $stats['recent_count'] );
|
|
?>
|
|
</p>
|
|
</div>
|
|
|
|
<p>
|
|
<a href="
|
|
<?php
|
|
echo esc_url(
|
|
wp_nonce_url(
|
|
add_query_arg(
|
|
array(
|
|
'tab' => 'audit-log',
|
|
'action' => 'export',
|
|
)
|
|
),
|
|
'two_factor_extended_export_audit'
|
|
)
|
|
);
|
|
?>
|
|
" class="button">
|
|
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
|
</a>
|
|
</p>
|
|
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Date', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Action', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Description', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'User', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'IP Address', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if ( ! empty( $logs ) ) : ?>
|
|
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
|
<?php
|
|
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
|
|
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
|
|
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
|
|
$log_actor_id = isset( $log['actor_id'] ) && is_int( $log['actor_id'] ) ? $log['actor_id'] : 0;
|
|
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '-';
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
|
|
<td><?php echo esc_html( $log_action ); ?></td>
|
|
<td><?php echo esc_html( $log_desc ); ?></td>
|
|
<td>
|
|
<?php
|
|
if ( $log_actor_id ) {
|
|
$user = get_userdata( $log_actor_id );
|
|
echo $user ? esc_html( $user->display_name ) : esc_html__( 'Unknown', 'two-factor-extended' );
|
|
} else {
|
|
esc_html_e( 'System', 'two-factor-extended' );
|
|
}
|
|
?>
|
|
</td>
|
|
<td><?php echo esc_html( $log_ip ); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else : ?>
|
|
<tr>
|
|
<td colspan="5"><?php esc_html_e( 'No audit logs found.', 'two-factor-extended' ); ?></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<?php if ( count( $logs ) > 50 ) : ?>
|
|
<p class="description">
|
|
<?php
|
|
/* translators: %d: Total number of audit logs */
|
|
printf( esc_html__( 'Showing 50 most recent logs. Total logs: %d', 'two-factor-extended' ), (int) count( $logs ) );
|
|
?>
|
|
</p>
|
|
<?php endif; ?>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render compliance tab content.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
private function render_compliance_tab(): void {
|
|
$compliance = two_factor_extended()->get_compliance_report();
|
|
|
|
if ( null === $compliance ) {
|
|
echo '<div class="notice notice-error"><p>' . esc_html__( 'Compliance report module is not available.', 'two-factor-extended' ) . '</p></div>';
|
|
return;
|
|
}
|
|
|
|
// Handle export.
|
|
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_export_compliance' );
|
|
|
|
$csv = $compliance->export_to_csv();
|
|
|
|
header( 'Content-Type: text/csv' );
|
|
header( 'Content-Disposition: attachment; filename="compliance-report-' . gmdate( 'Y-m-d' ) . '.csv"' );
|
|
echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
exit;
|
|
}
|
|
|
|
$stats = $compliance->get_compliance_stats();
|
|
$non_compliant = $compliance->get_non_compliant_users();
|
|
|
|
?>
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Overview', 'two-factor-extended' ); ?></h2>
|
|
<table class="widefat fixed">
|
|
<tr>
|
|
<th><?php esc_html_e( 'Total Users', 'two-factor-extended' ); ?></th>
|
|
<td><?php echo esc_html( (string) $stats['total_users'] ); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
|
<td style="color: green;"><strong><?php echo esc_html( (string) $stats['compliant_users'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
|
<td style="color: red;"><strong><?php echo esc_html( (string) $stats['non_compliant'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'In Grace Period', 'two-factor-extended' ); ?></th>
|
|
<td style="color: orange;"><strong><?php echo esc_html( (string) $stats['grace_period'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'No Requirements', 'two-factor-extended' ); ?></th>
|
|
<td><?php echo esc_html( (string) $stats['no_requirements'] ); ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ( ! empty( $stats['by_role'] ) ) : ?>
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Compliance by Role', 'two-factor-extended' ); ?></h2>
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Role', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Total', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ( $stats['by_role'] as $role => $role_stats ) : ?>
|
|
<tr>
|
|
<td><strong><?php echo esc_html( Two_Factor_Extended_Role_Manager::get_role_display_name( $role ) ); ?></strong></td>
|
|
<td><?php echo esc_html( (string) $role_stats['total'] ); ?></td>
|
|
<td><?php echo esc_html( (string) $role_stats['compliant'] ); ?></td>
|
|
<td><?php echo esc_html( (string) $role_stats['non_compliant'] ); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<h2><?php esc_html_e( 'Non-Compliant Users', 'two-factor-extended' ); ?></h2>
|
|
|
|
<p>
|
|
<a href="
|
|
<?php
|
|
echo esc_url(
|
|
wp_nonce_url(
|
|
add_query_arg(
|
|
array(
|
|
'tab' => 'compliance',
|
|
'action' => 'export',
|
|
)
|
|
),
|
|
'two_factor_extended_export_compliance'
|
|
)
|
|
);
|
|
?>
|
|
" class="button">
|
|
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
|
</a>
|
|
</p>
|
|
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'User', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Email', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Roles', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Status', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Missing Providers', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if ( ! empty( $non_compliant ) ) : ?>
|
|
<?php foreach ( $non_compliant as $user_data ) : ?>
|
|
<?php
|
|
$user_id_val = isset( $user_data['user_id'] ) && is_int( $user_data['user_id'] ) ? $user_data['user_id'] : 0;
|
|
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
|
|
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
|
|
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
|
$missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
|
$in_grace = ! empty( $user_data['in_grace_period'] );
|
|
$grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0;
|
|
// Get user object for display name.
|
|
$user = $user_id_val ? get_userdata( $user_id_val ) : null;
|
|
$roles_display = array_map(
|
|
function ( $role_slug ) {
|
|
if ( ! is_string( $role_slug ) ) {
|
|
return '';
|
|
}
|
|
return Two_Factor_Extended_Role_Manager::get_role_display_name( $role_slug );
|
|
},
|
|
$roles_raw
|
|
);
|
|
$missing_display = array_filter( $missing_raw, 'is_string' );
|
|
?>
|
|
<tr>
|
|
<td><?php echo $user ? esc_html( $user->display_name ) : esc_html( $user_login_val ); ?></td>
|
|
<td><?php echo esc_html( $user_email_val ); ?></td>
|
|
<td><?php echo esc_html( implode( ', ', $roles_display ) ); ?></td>
|
|
<td>
|
|
<?php if ( $in_grace ) : ?>
|
|
<span style="color: orange;">
|
|
<?php
|
|
printf(
|
|
/* translators: %d: Number of days remaining in grace period */
|
|
esc_html__( 'Grace period (%d days left)', 'two-factor-extended' ),
|
|
absint( $grace_remaining )
|
|
);
|
|
?>
|
|
</span>
|
|
<?php else : ?>
|
|
<span style="color: red;"><?php esc_html_e( 'Non-compliant', 'two-factor-extended' ); ?></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td><?php echo esc_html( implode( ', ', $missing_display ) ); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php else : ?>
|
|
<tr>
|
|
<td colspan="5"><?php esc_html_e( 'All users are compliant!', 'two-factor-extended' ); ?></td>
|
|
</tr>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Check if site settings are disabled by network.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @return bool True if disabled.
|
|
*/
|
|
private function are_site_settings_disabled(): bool {
|
|
if ( ! is_multisite() ) {
|
|
return false;
|
|
}
|
|
|
|
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
|
|
|
if ( ! is_array( $network_settings ) ) {
|
|
return false;
|
|
}
|
|
|
|
return ! empty( $network_settings['enforce_network_wide'] )
|
|
&& empty( $network_settings['allow_site_override'] );
|
|
}
|
|
|
|
/**
|
|
* Render audit log page.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_audit_log_page(): void {
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
|
|
if ( null === $audit_log ) {
|
|
wp_die( esc_html__( 'Audit log module is not available.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
// Handle export.
|
|
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_export_audit' );
|
|
|
|
$csv = $audit_log->export_to_csv();
|
|
|
|
header( 'Content-Type: text/csv' );
|
|
header( 'Content-Disposition: attachment; filename="audit-log-' . gmdate( 'Y-m-d' ) . '.csv"' );
|
|
echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
exit;
|
|
}
|
|
|
|
// Handle clear logs.
|
|
if ( isset( $_POST['action'] ) && 'clear' === $_POST['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_clear_audit' );
|
|
$audit_log->clear_logs();
|
|
echo '<div class="notice notice-success"><p>' . esc_html__( 'Audit logs cleared successfully.', 'two-factor-extended' ) . '</p></div>';
|
|
}
|
|
|
|
$logs = $audit_log->get_logs();
|
|
$stats = $audit_log->get_statistics();
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e( 'Two Factor Extended - Audit Log', 'two-factor-extended' ); ?></h1>
|
|
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Statistics', 'two-factor-extended' ); ?></h2>
|
|
<p>
|
|
<?php
|
|
/* translators: %d: Total number of audit logs */
|
|
printf( esc_html__( 'Total logs: %d', 'two-factor-extended' ), (int) $stats['total'] );
|
|
?>
|
|
</p>
|
|
<p>
|
|
<?php
|
|
/* translators: %d: Number of recent audit logs */
|
|
printf( esc_html__( 'Recent activity (7 days): %d', 'two-factor-extended' ), (int) $stats['recent_count'] );
|
|
?>
|
|
</p>
|
|
</div>
|
|
|
|
<p>
|
|
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'action', 'export' ), 'two_factor_extended_export_audit' ) ); ?>" class="button">
|
|
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
|
</a>
|
|
</p>
|
|
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Date', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Action', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Description', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'User', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'IP Address', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if ( empty( $logs ) ) : ?>
|
|
<tr>
|
|
<td colspan="5"><?php esc_html_e( 'No audit logs found.', 'two-factor-extended' ); ?></td>
|
|
</tr>
|
|
<?php else : ?>
|
|
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
|
<?php
|
|
$log_user_id = isset( $log['user_id'] ) && is_int( $log['user_id'] ) ? $log['user_id'] : 0;
|
|
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
|
|
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
|
|
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
|
|
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '';
|
|
$user = $log_user_id ? get_userdata( $log_user_id ) : null;
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
|
|
<td><?php echo esc_html( $log_action ); ?></td>
|
|
<td><?php echo esc_html( $log_desc ); ?></td>
|
|
<td><?php echo $user ? esc_html( $user->user_login ) : '-'; ?></td>
|
|
<td><?php echo esc_html( $log_ip ); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
|
|
<?php if ( count( $logs ) > 50 ) : ?>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Showing 50 most recent logs. Export to CSV for full history.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
<?php endif; ?>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render compliance report page.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_compliance_page(): void {
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
$compliance = two_factor_extended()->get_compliance_report();
|
|
|
|
if ( null === $compliance ) {
|
|
wp_die( esc_html__( 'Compliance report module is not available.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
// Handle export.
|
|
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_export_compliance' );
|
|
|
|
$csv = $compliance->export_to_csv();
|
|
|
|
header( 'Content-Type: text/csv' );
|
|
header( 'Content-Disposition: attachment; filename="compliance-report-' . gmdate( 'Y-m-d' ) . '.csv"' );
|
|
echo $csv; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
exit;
|
|
}
|
|
|
|
// Handle email report.
|
|
if ( isset( $_POST['action'] ) && 'email' === $_POST['action'] ) {
|
|
check_admin_referer( 'two_factor_extended_email_compliance' );
|
|
|
|
$email = isset( $_POST['email'] ) && is_string( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
|
|
|
if ( ! empty( $email ) ) {
|
|
$sent = $compliance->email_report( $email );
|
|
|
|
if ( $sent ) {
|
|
echo '<div class="notice notice-success"><p>' . esc_html__( 'Report sent successfully.', 'two-factor-extended' ) . '</p></div>';
|
|
} else {
|
|
echo '<div class="notice notice-error"><p>' . esc_html__( 'Failed to send report.', 'two-factor-extended' ) . '</p></div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
$stats = $compliance->get_compliance_stats();
|
|
$non_compliant = $compliance->get_non_compliant_users();
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php esc_html_e( 'Two Factor Extended - Compliance Report', 'two-factor-extended' ); ?></h1>
|
|
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Overview', 'two-factor-extended' ); ?></h2>
|
|
<table class="widefat fixed">
|
|
<tr>
|
|
<th><?php esc_html_e( 'Total Users', 'two-factor-extended' ); ?></th>
|
|
<td><?php echo esc_html( (string) $stats['total_users'] ); ?></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
|
<td style="color: green;"><strong><?php echo esc_html( (string) $stats['compliant_users'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
|
<td style="color: red;"><strong><?php echo esc_html( (string) $stats['non_compliant'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'In Grace Period', 'two-factor-extended' ); ?></th>
|
|
<td style="color: orange;"><strong><?php echo esc_html( (string) $stats['grace_period'] ); ?></strong></td>
|
|
</tr>
|
|
<tr>
|
|
<th><?php esc_html_e( 'No Requirements', 'two-factor-extended' ); ?></th>
|
|
<td><?php echo esc_html( (string) $stats['no_requirements'] ); ?></td>
|
|
</tr>
|
|
</table>
|
|
</div>
|
|
|
|
<?php if ( ! empty( $stats['by_role'] ) ) : ?>
|
|
<div class="card">
|
|
<h2><?php esc_html_e( 'Compliance by Role', 'two-factor-extended' ); ?></h2>
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Role', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Total', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ( $stats['by_role'] as $role => $role_stats ) : ?>
|
|
<tr>
|
|
<td><strong><?php echo esc_html( Two_Factor_Extended_Role_Manager::get_role_display_name( $role ) ); ?></strong></td>
|
|
<td><?php echo esc_html( (string) $role_stats['total'] ); ?></td>
|
|
<td><?php echo esc_html( (string) $role_stats['compliant'] ); ?></td>
|
|
<td><?php echo esc_html( (string) $role_stats['non_compliant'] ); ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<h2><?php esc_html_e( 'Non-Compliant Users', 'two-factor-extended' ); ?></h2>
|
|
|
|
<p>
|
|
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( 'action', 'export' ), 'two_factor_extended_export_compliance' ) ); ?>" class="button">
|
|
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
|
</a>
|
|
</p>
|
|
|
|
<table class="widefat fixed striped">
|
|
<thead>
|
|
<tr>
|
|
<th><?php esc_html_e( 'Username', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Email', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Roles', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Missing Providers', 'two-factor-extended' ); ?></th>
|
|
<th><?php esc_html_e( 'Status', 'two-factor-extended' ); ?></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if ( empty( $non_compliant ) ) : ?>
|
|
<tr>
|
|
<td colspan="5"><?php esc_html_e( 'All users are compliant!', 'two-factor-extended' ); ?></td>
|
|
</tr>
|
|
<?php else : ?>
|
|
<?php foreach ( $non_compliant as $user_data ) : ?>
|
|
<?php
|
|
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
|
|
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
|
|
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
|
$missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
|
$in_grace = ! empty( $user_data['in_grace_period'] );
|
|
$grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0;
|
|
$roles_display = array_filter( $roles_raw, 'is_string' );
|
|
$missing_display = array_filter( $missing_raw, 'is_string' );
|
|
?>
|
|
<tr>
|
|
<td><?php echo esc_html( $user_login_val ); ?></td>
|
|
<td><?php echo esc_html( $user_email_val ); ?></td>
|
|
<td><?php echo esc_html( implode( ', ', $roles_display ) ); ?></td>
|
|
<td><?php echo esc_html( implode( ', ', $missing_display ) ); ?></td>
|
|
<td>
|
|
<?php if ( $in_grace ) : ?>
|
|
<span style="color: orange;">
|
|
<?php
|
|
printf(
|
|
/* translators: %d: Number of days */
|
|
esc_html__( 'Grace: %d days', 'two-factor-extended' ),
|
|
absint( $grace_remaining )
|
|
);
|
|
?>
|
|
</span>
|
|
<?php else : ?>
|
|
<span style="color: red;"><strong><?php esc_html_e( 'Non-compliant', 'two-factor-extended' ); ?></strong></span>
|
|
<?php endif; ?>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Render network settings page.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
public function render_network_settings_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' ) );
|
|
}
|
|
|
|
// Handle form submission.
|
|
if ( isset( $_POST['submit'] ) ) {
|
|
// 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' ) );
|
|
}
|
|
|
|
// Process network settings.
|
|
$this->save_network_settings();
|
|
|
|
// Redirect with success message.
|
|
wp_safe_redirect( add_query_arg( 'updated', 'true', network_admin_url( 'settings.php?page=' . self::PAGE_SLUG ) ) );
|
|
exit;
|
|
}
|
|
|
|
?>
|
|
<div class="wrap">
|
|
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
|
|
|
|
<?php 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="">
|
|
<?php wp_nonce_field( 'two_factor_extended_network_settings', 'two_factor_extended_network_nonce' ); ?>
|
|
|
|
<table class="form-table">
|
|
<tr>
|
|
<th scope="row">
|
|
<?php esc_html_e( 'Network-wide Settings', 'two-factor-extended' ); ?>
|
|
</th>
|
|
<td>
|
|
<p class="description">
|
|
<?php esc_html_e( 'Network settings will be available in future versions.', 'two-factor-extended' ); ?>
|
|
</p>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
|
|
<?php submit_button( __( 'Save Network Settings', 'two-factor-extended' ) ); ?>
|
|
</form>
|
|
</div>
|
|
<?php
|
|
}
|
|
|
|
/**
|
|
* Save network settings.
|
|
*
|
|
* @since 0.1.0
|
|
*/
|
|
private function save_network_settings(): void {
|
|
// Placeholder for network settings save logic.
|
|
// Will be implemented when network features are added.
|
|
}
|
|
|
|
/**
|
|
* Get default settings.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @return array<string, mixed> Default settings.
|
|
*/
|
|
private function get_default_settings(): array {
|
|
$all_roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
|
$defaults = array(
|
|
'enabled' => false,
|
|
'grace_period_days' => 7,
|
|
'role_requirements' => array(),
|
|
'provider_visibility' => array(),
|
|
);
|
|
|
|
// Initialize all roles with empty arrays.
|
|
foreach ( array_keys( $all_roles ) as $role_slug ) {
|
|
$defaults['role_requirements'][ $role_slug ] = array();
|
|
$defaults['provider_visibility'][ $role_slug ] = array();
|
|
}
|
|
|
|
return $defaults;
|
|
}
|
|
|
|
/**
|
|
* Get plugin settings.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @return array<string, mixed> Plugin settings.
|
|
*/
|
|
public function get_settings(): array {
|
|
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
|
|
|
if ( ! is_array( $settings ) ) {
|
|
$settings = array();
|
|
}
|
|
|
|
return wp_parse_args( $settings, $this->get_default_settings() );
|
|
}
|
|
|
|
/**
|
|
* Update plugin settings.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $settings New settings values.
|
|
*
|
|
* @return bool True if settings were updated successfully.
|
|
*/
|
|
public function update_settings( array $settings ): bool {
|
|
$sanitized = $this->sanitize_settings( $settings );
|
|
|
|
return update_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, $sanitized );
|
|
}
|
|
|
|
/**
|
|
* Sanitize settings.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $input Raw settings input.
|
|
*
|
|
* @return array<string, mixed> Sanitized settings.
|
|
*/
|
|
public function sanitize_settings( array $input ): array {
|
|
$sanitized = array();
|
|
|
|
// Sanitize enabled setting.
|
|
if ( isset( $input['enabled'] ) ) {
|
|
$sanitized['enabled'] = (bool) $input['enabled'];
|
|
}
|
|
|
|
// Sanitize grace period days.
|
|
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
|
|
$grace_days = (int) $input['grace_period_days'];
|
|
$sanitized['grace_period_days'] = max( 0, min( 365, $grace_days ) );
|
|
}
|
|
|
|
// Get all roles once for both role requirements and provider visibility.
|
|
$all_roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
|
|
|
// Sanitize role requirements.
|
|
// Initialize all roles with empty arrays to preserve structure when all checkboxes are unchecked.
|
|
$sanitized['role_requirements'] = array();
|
|
|
|
foreach ( array_keys( $all_roles ) as $role_slug ) {
|
|
$sanitized['role_requirements'][ $role_slug ] = array();
|
|
}
|
|
|
|
// Now merge any submitted role requirements data.
|
|
if ( isset( $input['role_requirements'] ) && is_array( $input['role_requirements'] ) ) {
|
|
foreach ( $input['role_requirements'] as $role => $providers ) {
|
|
if ( ! is_string( $role ) ) {
|
|
continue;
|
|
}
|
|
$role_slug = sanitize_key( $role );
|
|
|
|
if ( ! Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( is_array( $providers ) ) {
|
|
$sanitized['role_requirements'][ $role_slug ] = array_map(
|
|
function ( $item ) {
|
|
return sanitize_text_field( is_scalar( $item ) ? (string) $item : '' );
|
|
},
|
|
$providers
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sanitize provider visibility.
|
|
// Initialize all roles with empty arrays to preserve structure when all checkboxes are unchecked.
|
|
$sanitized['provider_visibility'] = array();
|
|
|
|
foreach ( array_keys( $all_roles ) as $role_slug ) {
|
|
$sanitized['provider_visibility'][ $role_slug ] = array();
|
|
}
|
|
|
|
// Now merge any submitted provider visibility data.
|
|
if ( isset( $input['provider_visibility'] ) && is_array( $input['provider_visibility'] ) ) {
|
|
foreach ( $input['provider_visibility'] as $role => $providers ) {
|
|
if ( ! is_string( $role ) ) {
|
|
continue;
|
|
}
|
|
$role_slug = sanitize_key( $role );
|
|
|
|
if ( ! Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( is_array( $providers ) ) {
|
|
// Remove duplicates and sanitize.
|
|
$sanitized['provider_visibility'][ $role_slug ] = array_unique(
|
|
array_map(
|
|
function ( $item ) {
|
|
return sanitize_text_field( is_scalar( $item ) ? (string) $item : '' );
|
|
},
|
|
$providers
|
|
)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear object cache to prevent stale data.
|
|
wp_cache_delete( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, 'options' );
|
|
|
|
return $sanitized;
|
|
}
|
|
|
|
/**
|
|
* Validate settings input.
|
|
*
|
|
* @since 0.1.0
|
|
*
|
|
* @param array<string, mixed> $input Settings input to validate.
|
|
*
|
|
* @return bool True if valid, false otherwise.
|
|
*/
|
|
public function validate_settings( array $input ): bool {
|
|
// Basic validation - can be extended.
|
|
// Since parameter is typed as array, it's always valid.
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Handle reset settings action.
|
|
*
|
|
* @since 1.0.0
|
|
*/
|
|
public function handle_reset_settings(): void {
|
|
// Check if reset was requested.
|
|
if ( ! isset( $_POST['two_factor_extended_reset'] ) ) {
|
|
return;
|
|
}
|
|
|
|
// Verify nonce.
|
|
if ( ! isset( $_POST['two_factor_extended_reset_nonce'] ) || ! is_string( $_POST['two_factor_extended_reset_nonce'] ) ||
|
|
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['two_factor_extended_reset_nonce'] ) ), 'two_factor_extended_reset' ) ) {
|
|
wp_die( esc_html__( 'Security check failed.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
// Check capability.
|
|
if ( ! current_user_can( 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'You do not have sufficient permissions to perform this action.', 'two-factor-extended' ) );
|
|
}
|
|
|
|
try {
|
|
// Get default settings.
|
|
$default_settings = $this->get_default_settings();
|
|
|
|
// Reset plugin settings.
|
|
update_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, $default_settings );
|
|
|
|
// Clear user grace period data.
|
|
$users = get_users( array( 'fields' => 'ID' ) );
|
|
foreach ( $users as $user_id ) {
|
|
delete_user_meta( (int) $user_id, 'two_factor_extended_enforcement_date' );
|
|
}
|
|
|
|
// Log the reset action.
|
|
$audit_log = two_factor_extended()->get_audit_log();
|
|
|
|
if ( null !== $audit_log ) {
|
|
$audit_log->log_event(
|
|
'settings_reset',
|
|
'Plugin settings reset to default values'
|
|
);
|
|
}
|
|
|
|
// Redirect with success message.
|
|
add_settings_error(
|
|
'two_factor_extended_messages',
|
|
'two_factor_extended_reset',
|
|
__( 'Plugin settings have been reset to default values successfully.', 'two-factor-extended' ),
|
|
'success'
|
|
);
|
|
set_transient( 'settings_errors', get_settings_errors(), 30 );
|
|
|
|
$redirect_url = add_query_arg(
|
|
array(
|
|
'page' => self::PAGE_SLUG,
|
|
'settings-updated' => 'true',
|
|
),
|
|
admin_url( 'options-general.php' )
|
|
);
|
|
|
|
wp_safe_redirect( $redirect_url );
|
|
exit;
|
|
|
|
} catch ( Exception $e ) {
|
|
// Handle error gracefully.
|
|
add_settings_error(
|
|
'two_factor_extended_messages',
|
|
'two_factor_extended_reset_error',
|
|
__( 'Failed to reset plugin settings. Please try again.', 'two-factor-extended' ),
|
|
'error'
|
|
);
|
|
}
|
|
}
|
|
}
|