This commit is contained in:
Javier Casares 2026-02-17 14:21:39 +00:00
commit 212d64abfa
27 changed files with 8654 additions and 2 deletions

View file

@ -0,0 +1,410 @@
<?php
/**
* Audit log class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Audit_Log
*
* Logs 2FA configuration changes and enforcement actions.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Audit_Log {
/**
* Option key for audit logs.
*
* @since 0.1.0
* @var string
*/
const OPTION_KEY = 'two_factor_extended_audit_logs';
/**
* Maximum number of logs to keep.
*
* @since 0.1.0
* @var int
*/
const MAX_LOGS = 1000;
/**
* Log retention period in days.
*
* @since 0.1.0
* @var int
*/
const RETENTION_DAYS = 90;
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
// Log 2FA configuration changes.
add_action( 'two_factor_user_options_update', array( $this, 'log_user_2fa_change' ), 10, 1 );
// Log settings changes.
add_action( 'update_option_' . TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array( $this, 'log_settings_change' ), 10, 2 );
// Log enforcement actions.
add_action( 'wp_login_failed', array( $this, 'log_login_failure' ), 10, 1 );
// Daily cleanup of old logs.
add_action( 'two_factor_extended_daily_cleanup', array( $this, 'cleanup_old_logs' ) );
}
/**
* Log an event.
*
* @since 0.1.0
*
* @param string $action Action type.
* @param string $description Event description.
* @param int $user_id User ID (optional).
* @param array $metadata Additional metadata (optional).
*/
public function log_event( string $action, string $description, int $user_id = 0, array $metadata = array() ): void {
$logs = $this->get_logs();
$log_entry = array(
'timestamp' => current_time( 'timestamp' ),
'action' => sanitize_key( $action ),
'description' => sanitize_text_field( $description ),
'user_id' => $user_id,
'actor_id' => get_current_user_id(),
'ip_address' => $this->get_client_ip(),
'metadata' => $metadata,
);
// Add to beginning of array.
array_unshift( $logs, $log_entry );
// Trim to max logs.
if ( count( $logs ) > self::MAX_LOGS ) {
$logs = array_slice( $logs, 0, self::MAX_LOGS );
}
update_option( self::OPTION_KEY, $logs, false );
}
/**
* Log user 2FA configuration change.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*/
public function log_user_2fa_change( int $user_id ): void {
$user = get_userdata( $user_id );
if ( ! $user ) {
return;
}
$providers = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user_id );
$provider_names = array();
foreach ( array_keys( $providers ) as $class ) {
$names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$provider_names[] = $names[ $class ] ?? $class;
}
$this->log_event(
'2fa_config_changed',
sprintf(
'User %s changed 2FA configuration',
$user->user_login
),
$user_id,
array(
'enabled_providers' => $provider_names,
)
);
}
/**
* Log plugin settings change.
*
* @since 0.1.0
*
* @param mixed $old_value Old settings value.
* @param mixed $new_value New settings value.
*/
public function log_settings_change( $old_value, $new_value ): void {
$this->log_event(
'settings_changed',
'Plugin settings were updated',
0,
array(
'changed_keys' => $this->get_changed_keys( $old_value, $new_value ),
)
);
}
/**
* Log login failure related to 2FA requirements.
*
* @since 0.1.0
*
* @param string $username Username.
*/
public function log_login_failure( string $username ): void {
$user = get_user_by( 'login', $username );
if ( ! $user ) {
return;
}
// Check if failure was due to 2FA requirements.
$enforcement = two_factor_extended()->get_enforcement();
$required = $enforcement->get_required_providers_for_user( $user->ID );
if ( ! empty( $required ) && ! $enforcement->user_meets_requirements( $user->ID, $required ) ) {
$this->log_event(
'login_blocked',
sprintf(
'Login blocked for user %s due to missing 2FA requirements',
$username
),
$user->ID,
array(
'required_providers' => $required,
)
);
}
}
/**
* Get all audit logs.
*
* @since 0.1.0
*
* @param array $filters Optional filters (action, user_id, date_from, date_to).
*
* @return array Array of log entries.
*/
public function get_logs( array $filters = array() ): array {
$logs = get_option( self::OPTION_KEY, array() );
if ( ! is_array( $logs ) ) {
return array();
}
// Apply filters.
if ( ! empty( $filters ) ) {
$logs = $this->filter_logs( $logs, $filters );
}
return $logs;
}
/**
* Filter logs based on criteria.
*
* @since 0.1.0
*
* @param array $logs Log entries.
* @param array $filters Filter criteria.
*
* @return array Filtered logs.
*/
private function filter_logs( array $logs, array $filters ): array {
return array_filter(
$logs,
function ( $log ) use ( $filters ) {
// Filter by action.
if ( ! empty( $filters['action'] ) && $log['action'] !== $filters['action'] ) {
return false;
}
// Filter by user_id.
if ( ! empty( $filters['user_id'] ) && $log['user_id'] !== (int) $filters['user_id'] ) {
return false;
}
// Filter by date range.
if ( ! empty( $filters['date_from'] ) && $log['timestamp'] < strtotime( $filters['date_from'] ) ) {
return false;
}
if ( ! empty( $filters['date_to'] ) && $log['timestamp'] > strtotime( $filters['date_to'] ) ) {
return false;
}
return true;
}
);
}
/**
* Clear all logs.
*
* @since 0.1.0
*
* @return bool True on success.
*/
public function clear_logs(): bool {
return delete_option( self::OPTION_KEY );
}
/**
* Cleanup old logs based on retention period.
*
* @since 0.1.0
*/
public function cleanup_old_logs(): void {
$logs = $this->get_logs();
$cutoff_time = current_time( 'timestamp' ) - ( self::RETENTION_DAYS * DAY_IN_SECONDS );
$filtered_logs = array_filter(
$logs,
function ( $log ) use ( $cutoff_time ) {
return $log['timestamp'] >= $cutoff_time;
}
);
update_option( self::OPTION_KEY, array_values( $filtered_logs ), false );
}
/**
* Export logs to CSV.
*
* @since 0.1.0
*
* @param array $filters Optional filters.
*
* @return string CSV content.
*/
public function export_to_csv( array $filters = array() ): string {
$logs = $this->get_logs( $filters );
$csv = array();
$csv[] = array( 'Timestamp', 'Action', 'Description', 'User', 'Actor', 'IP Address' );
foreach ( $logs as $log ) {
$user = $log['user_id'] ? get_userdata( $log['user_id'] ) : null;
$actor = $log['actor_id'] ? get_userdata( $log['actor_id'] ) : null;
$csv[] = array(
gmdate( 'Y-m-d H:i:s', $log['timestamp'] ),
$log['action'],
$log['description'],
$user ? $user->user_login : '-',
$actor ? $actor->user_login : 'System',
$log['ip_address'],
);
}
// Convert to CSV string.
ob_start();
$handle = fopen( 'php://output', 'w' );
foreach ( $csv as $row ) {
fputcsv( $handle, $row );
}
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Simple CSV export
return ob_get_clean();
}
/**
* Get client IP address.
*
* @since 0.1.0
*
* @return string IP address.
*/
private function get_client_ip(): string {
$ip = '';
if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) );
} elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
}
return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
}
/**
* Get changed keys between old and new settings.
*
* @since 0.1.0
*
* @param mixed $old_value Old value.
* @param mixed $new_value New value.
*
* @return array Changed keys.
*/
private function get_changed_keys( $old_value, $new_value ): array {
if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
return array();
}
$changed = array();
foreach ( $new_value as $key => $value ) {
if ( ! isset( $old_value[ $key ] ) || $old_value[ $key ] !== $value ) {
$changed[] = $key;
}
}
return $changed;
}
/**
* Get log statistics.
*
* @since 0.1.0
*
* @return array Statistics.
*/
public function get_statistics(): array {
$logs = $this->get_logs();
$stats = array(
'total' => count( $logs ),
'by_action' => array(),
'recent_count' => 0,
);
$recent_cutoff = current_time( 'timestamp' ) - ( 7 * DAY_IN_SECONDS );
foreach ( $logs as $log ) {
// Count by action.
if ( ! isset( $stats['by_action'][ $log['action'] ] ) ) {
$stats['by_action'][ $log['action'] ] = 0;
}
$stats['by_action'][ $log['action'] ]++;
// Count recent logs (last 7 days).
if ( $log['timestamp'] >= $recent_cutoff ) {
$stats['recent_count']++;
}
}
return $stats;
}
}

View file

@ -0,0 +1,268 @@
<?php
/**
* Bulk actions handler
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Bulk_Actions
*
* Handles bulk actions for users.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Bulk_Actions {
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
// Add bulk actions to users list.
add_filter( 'bulk_actions-users', array( $this, 'register_bulk_actions' ) );
// Handle bulk actions.
add_filter( 'handle_bulk_actions-users', array( $this, 'handle_bulk_actions' ), 10, 3 );
// Display admin notices after bulk actions.
add_action( 'admin_notices', array( $this, 'display_bulk_action_notices' ) );
}
/**
* Register bulk actions.
*
* @since 0.1.0
*
* @param array $actions Existing bulk actions.
*
* @return array Modified bulk actions.
*/
public function register_bulk_actions( array $actions ): array {
if ( ! current_user_can( 'manage_options' ) ) {
return $actions;
}
$actions['two_factor_extended_require'] = __( 'Require 2FA Setup', 'two-factor-extended' );
$actions['two_factor_extended_reset'] = __( 'Reset 2FA Grace Period', 'two-factor-extended' );
return $actions;
}
/**
* Handle bulk actions.
*
* @since 0.1.0
*
* @param string $redirect_to Redirect URL.
* @param string $action Action name.
* @param array $user_ids User IDs.
*
* @return string Modified redirect URL.
*/
public function handle_bulk_actions( string $redirect_to, string $action, array $user_ids ): string {
// Check capability.
if ( ! current_user_can( 'manage_options' ) ) {
return $redirect_to;
}
// Handle our bulk actions.
if ( 'two_factor_extended_require' === $action ) {
$processed = $this->bulk_require_2fa( $user_ids );
$redirect_to = add_query_arg(
array(
'two_factor_extended_bulk_require' => $processed,
),
$redirect_to
);
} elseif ( 'two_factor_extended_reset' === $action ) {
$processed = $this->bulk_reset_grace_period( $user_ids );
$redirect_to = add_query_arg(
array(
'two_factor_extended_bulk_reset' => $processed,
),
$redirect_to
);
}
return $redirect_to;
}
/**
* Bulk require 2FA for users.
*
* @since 0.1.0
*
* @param array $user_ids User IDs.
*
* @return int Number of users processed.
*/
private function bulk_require_2fa( array $user_ids ): int {
$processed = 0;
$enforcement = two_factor_extended()->get_enforcement();
foreach ( $user_ids as $user_id ) {
// Skip if user doesn't exist.
$user = get_userdata( $user_id );
if ( ! $user ) {
continue;
}
// Get required providers for user.
$required = $enforcement->get_required_providers_for_user( $user_id );
// Skip if no requirements.
if ( empty( $required ) ) {
continue;
}
// Set enforcement start date to now (starts grace period).
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'bulk_require_2fa',
sprintf(
'Bulk action: Require 2FA for user %s',
$user->user_login
),
$user_id,
array(
'required_providers' => $required,
)
);
$processed++;
}
return $processed;
}
/**
* Bulk reset grace period for users.
*
* @since 0.1.0
*
* @param array $user_ids User IDs.
*
* @return int Number of users processed.
*/
private function bulk_reset_grace_period( array $user_ids ): int {
$processed = 0;
foreach ( $user_ids as $user_id ) {
// Skip if user doesn't exist.
$user = get_userdata( $user_id );
if ( ! $user ) {
continue;
}
// Reset enforcement start date (restarts grace period).
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
// Clear grace period notified flag.
delete_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'bulk_reset_grace',
sprintf(
'Bulk action: Reset grace period for user %s',
$user->user_login
),
$user_id
);
$processed++;
}
return $processed;
}
/**
* Display admin notices after bulk actions.
*
* This method displays success messages after bulk actions are completed.
* No nonce verification is needed here because:
* 1. This is a read-only operation (displaying a message)
* 2. The actual bulk action was already verified in handle_bulk_actions()
* 3. The GET parameters are the result of a POST-redirect-GET pattern
* 4. Only the count is used, which is safely cast to int
* 5. User capability is verified before displaying
*
* @since 0.1.0
*/
public function display_bulk_action_notices(): void {
// Check if on users page.
$screen = get_current_screen();
if ( ! $screen || 'users' !== $screen->id ) {
return;
}
// Verify user has capability to see these notices.
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Bulk require 2FA notice.
// Use filter_input() for safe GET parameter access.
$bulk_require_count = filter_input( INPUT_GET, 'two_factor_extended_bulk_require', FILTER_VALIDATE_INT );
if ( $bulk_require_count && $bulk_require_count > 0 ) {
printf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
esc_html(
sprintf(
/* translators: %d: Number of users */
_n(
'2FA requirement set for %d user.',
'2FA requirement set for %d users.',
$bulk_require_count,
'two-factor-extended'
),
$bulk_require_count
)
)
);
}
// Bulk reset grace period notice.
$bulk_reset_count = filter_input( INPUT_GET, 'two_factor_extended_bulk_reset', FILTER_VALIDATE_INT );
if ( $bulk_reset_count && $bulk_reset_count > 0 ) {
printf(
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
esc_html(
sprintf(
/* translators: %d: Number of users */
_n(
'Grace period reset for %d user.',
'Grace period reset for %d users.',
$bulk_reset_count,
'two-factor-extended'
),
$bulk_reset_count
)
)
);
}
}
}

View file

@ -0,0 +1,342 @@
<?php
/**
* WP-CLI commands
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_CLI_Commands
*
* Provides WP-CLI commands for Two Factor Extended.
*
* @since 0.1.0
*/
class Two_Factor_Extended_CLI_Commands {
/**
* Display 2FA compliance status.
*
* ## OPTIONS
*
* [--role=<role>]
* : Filter by user role.
*
* [--format=<format>]
* : Output format (table, json, csv). Default: table.
*
* ## EXAMPLES
*
* wp two-factor-extended status
* wp two-factor-extended status --role=administrator
* wp two-factor-extended status --format=json
*
* @since 0.1.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
public function status( array $args, array $assoc_args ): void {
$role = $assoc_args['role'] ?? '';
$format = $assoc_args['format'] ?? 'table';
$compliance = two_factor_extended()->get_compliance_report();
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
if ( 'json' === $format ) {
WP_CLI::line( wp_json_encode( $stats, JSON_PRETTY_PRINT ) );
return;
}
// Display overview.
WP_CLI::line( WP_CLI::colorize( '%G2FA Compliance Status%n' ) );
WP_CLI::line( str_repeat( '=', 40 ) );
$overview = array(
array(
'Metric' => 'Total Users',
'Count' => $stats['total_users'],
'Percentage' => '100%',
),
array(
'Metric' => 'Compliant',
'Count' => $stats['compliant_users'],
'Percentage' => $this->calculate_percentage( $stats['compliant_users'], $stats['total_users'] ),
),
array(
'Metric' => 'Non-Compliant',
'Count' => $stats['non_compliant'],
'Percentage' => $this->calculate_percentage( $stats['non_compliant'], $stats['total_users'] ),
),
array(
'Metric' => 'In Grace Period',
'Count' => $stats['grace_period'],
'Percentage' => $this->calculate_percentage( $stats['grace_period'], $stats['total_users'] ),
),
array(
'Metric' => 'No Requirements',
'Count' => $stats['no_requirements'],
'Percentage' => $this->calculate_percentage( $stats['no_requirements'], $stats['total_users'] ),
),
);
WP_CLI\Utils\format_items( $format, $overview, array( 'Metric', 'Count', 'Percentage' ) );
// Display by role if available.
if ( ! empty( $stats['by_role'] ) ) {
WP_CLI::line( '' );
WP_CLI::line( WP_CLI::colorize( '%GBy Role%n' ) );
WP_CLI::line( str_repeat( '=', 40 ) );
$by_role = array();
foreach ( $stats['by_role'] as $role_key => $role_stats ) {
$role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role_key );
$by_role[] = array(
'Role' => $role_name,
'Total' => $role_stats['total'],
'Compliant' => $role_stats['compliant'],
'Non-Compliant' => $role_stats['non_compliant'],
'Compliance %' => $this->calculate_percentage( $role_stats['compliant'], $role_stats['total'] ),
);
}
WP_CLI\Utils\format_items( $format, $by_role, array( 'Role', 'Total', 'Compliant', 'Non-Compliant', 'Compliance %' ) );
}
}
/**
* Enforce 2FA requirements for a role.
*
* ## OPTIONS
*
* --role=<role>
* : User role to enforce.
*
* [--reset-grace]
* : Reset grace period for all users in role.
*
* ## EXAMPLES
*
* wp two-factor-extended enforce --role=administrator
* wp two-factor-extended enforce --role=editor --reset-grace
*
* @since 0.1.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
public function enforce( array $args, array $assoc_args ): void {
if ( empty( $assoc_args['role'] ) ) {
WP_CLI::error( 'Please specify a role with --role=<role>' );
return;
}
$role = $assoc_args['role'];
$reset_grace = isset( $assoc_args['reset-grace'] );
// Get users with this role.
$users = get_users( array( 'role' => $role ) );
if ( empty( $users ) ) {
WP_CLI::warning( sprintf( 'No users found with role: %s', $role ) );
return;
}
$enforcement = two_factor_extended()->get_enforcement();
$processed = 0;
$progress = WP_CLI\Utils\make_progress_bar( 'Enforcing 2FA requirements', count( $users ) );
foreach ( $users as $user ) {
$required = $enforcement->get_required_providers_for_user( $user->ID );
if ( empty( $required ) ) {
$progress->tick();
continue;
}
// Set or reset enforcement start date.
update_user_meta( $user->ID, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
if ( $reset_grace ) {
delete_user_meta( $user->ID, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
}
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'cli_enforce',
sprintf( 'WP-CLI: Enforced 2FA for user %s', $user->user_login ),
$user->ID,
array(
'role' => $role,
'required_providers' => $required,
)
);
$processed++;
$progress->tick();
}
$progress->finish();
WP_CLI::success(
sprintf(
'Enforced 2FA requirements for %d users with role: %s',
$processed,
$role
)
);
}
/**
* Generate compliance report.
*
* ## OPTIONS
*
* [--role=<role>]
* : Filter by user role.
*
* [--format=<format>]
* : Output format (table, json, csv). Default: table.
*
* [--non-compliant-only]
* : Show only non-compliant users.
*
* ## EXAMPLES
*
* wp two-factor-extended report
* wp two-factor-extended report --role=administrator
* wp two-factor-extended report --non-compliant-only --format=csv
*
* @since 0.1.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
public function report( array $args, array $assoc_args ): void {
$role = $assoc_args['role'] ?? '';
$format = $assoc_args['format'] ?? 'table';
$non_compliant_only = isset( $assoc_args['non-compliant-only'] );
$compliance = two_factor_extended()->get_compliance_report();
if ( $non_compliant_only ) {
$users = $compliance->get_non_compliant_users( array( 'role' => $role ) );
if ( empty( $users ) ) {
WP_CLI::success( 'All users are compliant!' );
return;
}
$items = array();
foreach ( $users as $user_data ) {
$items[] = array(
'User ID' => $user_data['user_id'],
'Username' => $user_data['user_login'],
'Email' => $user_data['user_email'],
'Roles' => implode( ', ', $user_data['roles'] ),
'Missing Providers' => implode( ', ', $user_data['missing_providers'] ),
'Grace Period' => $user_data['in_grace_period'] ? 'Yes' : 'No',
'Days Remaining' => $user_data['grace_remaining'],
);
}
WP_CLI\Utils\format_items(
$format,
$items,
array( 'User ID', 'Username', 'Email', 'Roles', 'Missing Providers', 'Grace Period', 'Days Remaining' )
);
} else {
// Show full statistics.
$this->status( $args, $assoc_args );
}
}
/**
* Reset 2FA grace period for a user.
*
* ## OPTIONS
*
* --user=<user>
* : User ID, login, or email.
*
* ## EXAMPLES
*
* wp two-factor-extended reset --user=admin
* wp two-factor-extended reset --user=123
* wp two-factor-extended reset --user=admin@example.com
*
* @since 0.1.0
*
* @param array $args Positional arguments.
* @param array $assoc_args Associative arguments.
*/
public function reset( array $args, array $assoc_args ): void {
if ( empty( $assoc_args['user'] ) ) {
WP_CLI::error( 'Please specify a user with --user=<user>' );
return;
}
// Get user by ID, login, or email.
$user_identifier = $assoc_args['user'];
if ( is_numeric( $user_identifier ) ) {
$user = get_userdata( $user_identifier );
} elseif ( is_email( $user_identifier ) ) {
$user = get_user_by( 'email', $user_identifier );
} else {
$user = get_user_by( 'login', $user_identifier );
}
if ( ! $user ) {
WP_CLI::error( sprintf( 'User not found: %s', $user_identifier ) );
return;
}
// Reset enforcement start date.
update_user_meta( $user->ID, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
// Clear grace period notified flag.
delete_user_meta( $user->ID, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'cli_reset_grace',
sprintf( 'WP-CLI: Reset grace period for user %s', $user->user_login ),
$user->ID
);
WP_CLI::success(
sprintf(
'Grace period reset for user: %s (ID: %d)',
$user->user_login,
$user->ID
)
);
}
/**
* Calculate percentage.
*
* @since 0.1.0
*
* @param int $part Part value.
* @param int $total Total value.
*
* @return string Percentage string.
*/
private function calculate_percentage( int $part, int $total ): string {
if ( 0 === $total ) {
return '0%';
}
return sprintf( '%.1f%%', ( $part / $total ) * 100 );
}
}

View file

@ -0,0 +1,314 @@
<?php
/**
* Compliance report class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Compliance_Report
*
* Generates compliance reports for 2FA usage.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Compliance_Report {
/**
* Get compliance statistics.
*
* @since 0.1.0
*
* @param array $args Optional arguments (role, blog_id).
*
* @return array Compliance statistics.
*/
public function get_compliance_stats( array $args = array() ): array {
$defaults = array(
'role' => '',
'blog_id' => get_current_blog_id(),
);
$args = wp_parse_args( $args, $defaults );
// Get users.
$user_args = array(
'fields' => 'all',
);
if ( ! empty( $args['role'] ) ) {
$user_args['role'] = $args['role'];
}
if ( is_multisite() && ! empty( $args['blog_id'] ) ) {
$user_args['blog_id'] = $args['blog_id'];
}
$users = get_users( $user_args );
$stats = array(
'total_users' => count( $users ),
'compliant_users' => 0,
'non_compliant' => 0,
'grace_period' => 0,
'no_requirements' => 0,
'by_role' => array(),
);
$enforcement = two_factor_extended()->get_enforcement();
foreach ( $users as $user ) {
$required = $enforcement->get_required_providers_for_user( $user->ID );
if ( empty( $required ) ) {
$stats['no_requirements']++;
continue;
}
$compliant = $enforcement->user_meets_requirements( $user->ID, $required );
$in_grace = $enforcement->is_in_grace_period( $user->ID );
if ( $compliant ) {
$stats['compliant_users']++;
} elseif ( $in_grace ) {
$stats['grace_period']++;
} else {
$stats['non_compliant']++;
}
// Count by role.
$roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user->ID );
foreach ( $roles as $role ) {
if ( ! isset( $stats['by_role'][ $role ] ) ) {
$stats['by_role'][ $role ] = array(
'total' => 0,
'compliant' => 0,
'non_compliant' => 0,
);
}
$stats['by_role'][ $role ]['total']++;
if ( $compliant ) {
$stats['by_role'][ $role ]['compliant']++;
} else {
$stats['by_role'][ $role ]['non_compliant']++;
}
}
}
return $stats;
}
/**
* Get non-compliant users.
*
* @since 0.1.0
*
* @param array $args Optional arguments.
*
* @return array Array of non-compliant user data.
*/
public function get_non_compliant_users( array $args = array() ): array {
$defaults = array(
'role' => '',
'blog_id' => get_current_blog_id(),
);
$args = wp_parse_args( $args, $defaults );
$user_args = array(
'fields' => 'all',
);
if ( ! empty( $args['role'] ) ) {
$user_args['role'] = $args['role'];
}
if ( is_multisite() && ! empty( $args['blog_id'] ) ) {
$user_args['blog_id'] = $args['blog_id'];
}
$users = get_users( $user_args );
$non_compliant = array();
$enforcement = two_factor_extended()->get_enforcement();
foreach ( $users as $user ) {
$required = $enforcement->get_required_providers_for_user( $user->ID );
if ( empty( $required ) ) {
continue;
}
$compliant = $enforcement->user_meets_requirements( $user->ID, $required );
if ( ! $compliant ) {
$enabled = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user->ID );
$missing = array_diff( $required, array_keys( $enabled ) );
$provider_names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$missing_names = array();
foreach ( $missing as $class ) {
$missing_names[] = $provider_names[ $class ] ?? $class;
}
$non_compliant[] = array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
'user_email' => $user->user_email,
'roles' => Two_Factor_Extended_Role_Manager::get_user_roles( $user->ID ),
'missing_providers' => $missing_names,
'in_grace_period' => $enforcement->is_in_grace_period( $user->ID ),
'grace_remaining' => $enforcement->get_grace_period_remaining_days( $user->ID ),
);
}
}
return $non_compliant;
}
/**
* Get network-wide compliance report (Multisite).
*
* @since 0.1.0
*
* @return array Network compliance report.
*/
public function get_network_report(): array {
if ( ! is_multisite() ) {
return array();
}
$sites = get_sites( array( 'number' => 999 ) );
$report = array(
'total_sites' => count( $sites ),
'total_users' => 0,
'compliant' => 0,
'non_compliant' => 0,
'by_site' => array(),
);
foreach ( $sites as $site ) {
switch_to_blog( $site->blog_id );
$site_stats = $this->get_compliance_stats( array( 'blog_id' => $site->blog_id ) );
$report['by_site'][ $site->blog_id ] = array(
'site_name' => get_bloginfo( 'name' ),
'site_url' => get_bloginfo( 'url' ),
'total_users' => $site_stats['total_users'],
'compliant' => $site_stats['compliant_users'],
'non_compliant' => $site_stats['non_compliant'],
);
$report['total_users'] += $site_stats['total_users'];
$report['compliant'] += $site_stats['compliant_users'];
$report['non_compliant'] += $site_stats['non_compliant'];
restore_current_blog();
}
return $report;
}
/**
* Export compliance report to CSV.
*
* @since 0.1.0
*
* @param array $args Optional arguments.
*
* @return string CSV content.
*/
public function export_to_csv( array $args = array() ): string {
$non_compliant = $this->get_non_compliant_users( $args );
$csv = array();
$csv[] = array( 'User ID', 'Username', 'Email', 'Roles', 'Missing Providers', 'Grace Period', 'Days Remaining' );
foreach ( $non_compliant as $user_data ) {
$csv[] = array(
$user_data['user_id'],
$user_data['user_login'],
$user_data['user_email'],
implode( ', ', $user_data['roles'] ),
implode( ', ', $user_data['missing_providers'] ),
$user_data['in_grace_period'] ? 'Yes' : 'No',
$user_data['grace_remaining'],
);
}
ob_start();
$handle = fopen( 'php://output', 'w' );
foreach ( $csv as $row ) {
fputcsv( $handle, $row );
}
fclose( $handle ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Simple CSV export
return ob_get_clean();
}
/**
* Send compliance report via email.
*
* @since 0.1.0
*
* @param string $to Recipient email address.
* @param array $args Optional arguments for report.
*
* @return bool True on success, false on failure.
*/
public function email_report( string $to, array $args = array() ): bool {
$stats = $this->get_compliance_stats( $args );
$subject = __( 'Two Factor Extended - Compliance Report', 'two-factor-extended' );
$message = sprintf(
/* translators: 1: Site name */
__( 'Compliance Report for %s', 'two-factor-extended' ),
get_bloginfo( 'name' )
) . "\n\n";
$message .= __( 'Summary:', 'two-factor-extended' ) . "\n";
/* translators: %d: Number of total users */
$message .= sprintf( __( 'Total Users: %d', 'two-factor-extended' ), $stats['total_users'] ) . "\n";
/* translators: %d: Number of compliant users */
$message .= sprintf( __( 'Compliant: %d', 'two-factor-extended' ), $stats['compliant_users'] ) . "\n";
/* translators: %d: Number of non-compliant users */
$message .= sprintf( __( 'Non-Compliant: %d', 'two-factor-extended' ), $stats['non_compliant'] ) . "\n";
/* translators: %d: Number of users in grace period */
$message .= sprintf( __( 'In Grace Period: %d', 'two-factor-extended' ), $stats['grace_period'] ) . "\n";
/* translators: %d: Number of users with no requirements */
$message .= sprintf( __( 'No Requirements: %d', 'two-factor-extended' ), $stats['no_requirements'] ) . "\n\n";
if ( ! empty( $stats['by_role'] ) ) {
$message .= __( 'By Role:', 'two-factor-extended' ) . "\n";
foreach ( $stats['by_role'] as $role => $role_stats ) {
$role_name = Two_Factor_Extended_Role_Manager::get_role_display_name( $role );
$message .= sprintf(
' %s: %d/%d compliant',
$role_name,
$role_stats['compliant'],
$role_stats['total']
) . "\n";
}
}
$message .= "\n" . __( 'For detailed information, please log in to the WordPress admin.', 'two-factor-extended' );
$headers = array( 'Content-Type: text/plain; charset=UTF-8' );
return wp_mail( $to, $subject, $message, $headers );
}
}

View file

@ -0,0 +1,175 @@
<?php
/**
* Dependency checker class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Dependency_Checker
*
* Checks plugin dependencies and displays admin notices when not met.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Dependency_Checker {
/**
* Minimum required WordPress version.
*
* @since 0.1.0
* @var string
*/
const MIN_WP_VERSION = '6.7';
/**
* Minimum required PHP version.
*
* @since 0.1.0
* @var string
*/
const MIN_PHP_VERSION = '8.2';
/**
* Check all dependencies.
*
* @since 0.1.0
*
* @return bool True if all dependencies are met, false otherwise.
*/
public static function check_dependencies(): bool {
$checks = array(
self::check_two_factor_plugin(),
self::check_wordpress_version(),
self::check_php_version(),
);
return ! in_array( false, $checks, true );
}
/**
* Check if Two Factor plugin is active.
*
* @since 0.1.0
*
* @return bool True if Two Factor plugin is active, false otherwise.
*/
public static function check_two_factor_plugin(): bool {
return class_exists( 'Two_Factor_Core' );
}
/**
* Check WordPress version.
*
* @since 0.1.0
*
* @return bool True if WordPress version is sufficient, false otherwise.
*/
public static function check_wordpress_version(): bool {
global $wp_version;
return version_compare( $wp_version, self::MIN_WP_VERSION, '>=' );
}
/**
* Check PHP version.
*
* @since 0.1.0
*
* @return bool True if PHP version is sufficient, false otherwise.
*/
public static function check_php_version(): bool {
return version_compare( PHP_VERSION, self::MIN_PHP_VERSION, '>=' );
}
/**
* Display admin notice for missing Two Factor plugin.
*
* @since 0.1.0
*/
public static function admin_notice_missing_two_factor(): void {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Two Factor Extended', 'two-factor-extended' ); ?>:</strong>
<?php
printf(
/* translators: %s: Two Factor plugin link */
esc_html__( 'This plugin requires the %s plugin to be installed and activated.', 'two-factor-extended' ),
'<a href="https://wordpress.org/plugins/two-factor/" target="_blank" rel="noopener noreferrer">' . esc_html__( 'Two Factor', 'two-factor-extended' ) . '</a>'
);
?>
</p>
</div>
<?php
}
/**
* Display admin notice for insufficient WordPress version.
*
* @since 0.1.0
*/
public static function admin_notice_wordpress_version(): void {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Two Factor Extended', 'two-factor-extended' ); ?>:</strong>
<?php
printf(
/* translators: %s: Minimum WordPress version */
esc_html__( 'This plugin requires WordPress %s or higher.', 'two-factor-extended' ),
esc_html( self::MIN_WP_VERSION )
);
?>
</p>
</div>
<?php
}
/**
* Display admin notice for insufficient PHP version.
*
* @since 0.1.0
*/
public static function admin_notice_php_version(): void {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Two Factor Extended', 'two-factor-extended' ); ?>:</strong>
<?php
printf(
/* translators: %s: Minimum PHP version */
esc_html__( 'This plugin requires PHP %s or higher.', 'two-factor-extended' ),
esc_html( self::MIN_PHP_VERSION )
);
?>
</p>
</div>
<?php
}
/**
* Display all relevant admin notices.
*
* @since 0.1.0
*/
public static function display_admin_notices(): void {
if ( ! self::check_two_factor_plugin() ) {
self::admin_notice_missing_two_factor();
}
if ( ! self::check_wordpress_version() ) {
self::admin_notice_wordpress_version();
}
if ( ! self::check_php_version() ) {
self::admin_notice_php_version();
}
}
}

View file

@ -0,0 +1,368 @@
<?php
/**
* 2FA requirement enforcement class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Enforcement
*
* Enforces 2FA requirements based on user roles.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Enforcement {
/**
* User meta key for enforcement start date.
*
* @since 0.1.0
* @var string
*/
const META_ENFORCEMENT_START = 'two_factor_extended_enforcement_start';
/**
* User meta key for grace period notified.
*
* @since 0.1.0
* @var string
*/
const META_GRACE_NOTIFIED = 'two_factor_extended_grace_notified';
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
add_filter( 'authenticate', array( $this, 'check_user_requirements' ), 30, 1 );
add_action( 'admin_notices', array( $this, 'display_user_notices' ) );
add_action( 'two_factor_extended_daily_check', array( $this, 'check_all_users_compliance' ) );
}
/**
* Check user's 2FA requirements on login.
*
* @since 0.1.0
*
* @param WP_User|WP_Error|null $user User object or error.
*
* @return WP_User|WP_Error User object or error if requirements not met.
*/
public function check_user_requirements( $user ) {
// Skip if not a user object.
if ( ! $user instanceof WP_User ) {
return $user;
}
// Skip if Two Factor not available.
if ( ! class_exists( 'Two_Factor_Core' ) ) {
return $user;
}
// Get required providers for user.
$required_providers = $this->get_required_providers_for_user( $user->ID );
if ( empty( $required_providers ) ) {
return $user;
}
// Check if user is in grace period.
if ( $this->is_in_grace_period( $user->ID ) ) {
$this->set_grace_period_notice( $user->ID );
return $user;
}
// Check if user meets requirements.
if ( ! $this->user_meets_requirements( $user->ID, $required_providers ) ) {
return new WP_Error(
'two_factor_extended_required',
sprintf(
/* translators: %s: Required providers list */
__( 'Your account requires the following 2FA methods to be configured: %s. Please contact an administrator for assistance.', 'two-factor-extended' ),
implode( ', ', $this->get_provider_labels( $required_providers ) )
)
);
}
return $user;
}
/**
* Get required providers for user based on their roles.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array Array of required provider class names.
*/
public function get_required_providers_for_user( int $user_id ): array {
$required = array();
// Check if user is super admin (Multisite).
if ( is_multisite() && Two_Factor_Extended_Role_Manager::is_super_admin( $user_id ) ) {
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
if ( isset( $network_settings['super_admin_requirements'] ) && is_array( $network_settings['super_admin_requirements'] ) ) {
$required = array_merge( $required, $network_settings['super_admin_requirements'] );
}
}
// Check network-wide settings (Multisite).
if ( is_multisite() ) {
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
if ( ! empty( $network_settings['enforce_network_wide'] ) ) {
// Network enforcement is enabled.
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
if ( isset( $network_settings['role_requirements'] ) && is_array( $network_settings['role_requirements'] ) ) {
foreach ( $user_roles as $role ) {
if ( isset( $network_settings['role_requirements'][ $role ] ) && is_array( $network_settings['role_requirements'][ $role ] ) ) {
$required = array_merge( $required, $network_settings['role_requirements'][ $role ] );
}
}
}
// If site override is not allowed, return only network requirements.
if ( empty( $network_settings['allow_site_override'] ) ) {
return array_unique( $required );
}
}
}
// Get site-level requirements.
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
if ( ! empty( $user_roles ) && isset( $settings['role_requirements'] ) ) {
foreach ( $user_roles as $role ) {
if ( isset( $settings['role_requirements'][ $role ] ) ) {
$role_requirements = $settings['role_requirements'][ $role ];
if ( is_array( $role_requirements ) ) {
$required = array_merge( $required, $role_requirements );
}
}
}
}
return array_unique( $required );
}
/**
* Check if user meets 2FA requirements.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param array $required_providers Required provider class names.
*
* @return bool True if user meets requirements, false otherwise.
*/
public function user_meets_requirements( int $user_id, array $required_providers ): bool {
if ( empty( $required_providers ) ) {
return true;
}
$enabled_providers = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user_id );
foreach ( $required_providers as $provider_class ) {
if ( ! isset( $enabled_providers[ $provider_class ] ) ) {
return false;
}
}
return true;
}
/**
* Check if user is in grace period.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return bool True if in grace period, false otherwise.
*/
public function is_in_grace_period( int $user_id ): bool {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$grace_days = isset( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 0;
// No grace period configured.
if ( 0 === $grace_days ) {
return false;
}
$start_date = get_user_meta( $user_id, self::META_ENFORCEMENT_START, true );
// No enforcement start date set - set it now.
if ( empty( $start_date ) ) {
update_user_meta( $user_id, self::META_ENFORCEMENT_START, time() );
return true;
}
$days_elapsed = ( time() - (int) $start_date ) / DAY_IN_SECONDS;
return $days_elapsed < $grace_days;
}
/**
* Get remaining grace period days.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return int Remaining days in grace period.
*/
public function get_grace_period_remaining_days( int $user_id ): int {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$grace_days = isset( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 0;
$start_date = get_user_meta( $user_id, self::META_ENFORCEMENT_START, true );
if ( empty( $start_date ) || 0 === $grace_days ) {
return 0;
}
$days_elapsed = ( time() - (int) $start_date ) / DAY_IN_SECONDS;
$remaining = $grace_days - $days_elapsed;
return max( 0, (int) ceil( $remaining ) );
}
/**
* Set grace period notice flag.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*/
private function set_grace_period_notice( int $user_id ): void {
update_user_meta( $user_id, self::META_GRACE_NOTIFIED, time() );
}
/**
* Display user notices for 2FA requirements.
*
* @since 0.1.0
*/
public function display_user_notices(): void {
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
$required_providers = $this->get_required_providers_for_user( $user_id );
if ( empty( $required_providers ) ) {
return;
}
// Check if user meets requirements.
if ( $this->user_meets_requirements( $user_id, $required_providers ) ) {
return;
}
// Check if in grace period.
if ( $this->is_in_grace_period( $user_id ) ) {
$remaining_days = $this->get_grace_period_remaining_days( $user_id );
?>
<div class="notice notice-warning">
<p>
<strong><?php esc_html_e( 'Two Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
esc_html(
/* translators: 1: Number of days, 2: Required providers list */
_n(
'You have %1$d day remaining to configure the required 2FA methods: %2$s',
'You have %1$d days remaining to configure the required 2FA methods: %2$s',
$remaining_days,
'two-factor-extended'
)
),
(int) $remaining_days,
esc_html( implode( ', ', $this->get_provider_labels( $required_providers ) ) )
);
?>
</p>
<p>
<a href="<?php echo esc_url( admin_url( 'profile.php#two-factor-options' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Configure 2FA Now', 'two-factor-extended' ); ?>
</a>
</p>
</div>
<?php
} else {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Two Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
/* translators: %s: Required providers list */
esc_html__( 'Your account requires the following 2FA methods: %s. Your access may be restricted until you configure them.', 'two-factor-extended' ),
esc_html( implode( ', ', $this->get_provider_labels( $required_providers ) ) )
);
?>
</p>
<p>
<a href="<?php echo esc_url( admin_url( 'profile.php#two-factor-options' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Configure 2FA Now', 'two-factor-extended' ); ?>
</a>
</p>
</div>
<?php
}
}
/**
* Get provider labels from class names.
*
* @since 0.1.0
*
* @param array $provider_classes Array of provider class names.
*
* @return array Array of provider labels.
*/
private function get_provider_labels( array $provider_classes ): array {
$names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$labels = array();
foreach ( $provider_classes as $class ) {
$labels[] = $names[ $class ] ?? $class;
}
return $labels;
}
/**
* Check compliance for all users (scheduled task).
*
* @since 0.1.0
*/
public function check_all_users_compliance(): void {
// This will be implemented for reporting/audit purposes.
// For now, it's a placeholder for the daily scheduled check.
}
}

View file

@ -0,0 +1,433 @@
<?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( $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 $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 = $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 $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 = $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 = $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'] )
? 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 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() );
return wp_parse_args( $settings, $defaults );
}
/**
* Sanitize network settings.
*
* @since 0.1.0
*
* @param array $input Raw input.
*
* @return array 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'] ) ) {
$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( 'sanitize_text_field', $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( $role );
if ( Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) && is_array( $providers ) ) {
$sanitized['role_requirements'][ $role_slug ] = array_map( 'sanitize_text_field', $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'] );
}
}

View file

@ -0,0 +1,184 @@
<?php
/**
* 2FA provider detector class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Provider_Detector
*
* Detects and manages Two Factor authentication providers.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Provider_Detector {
/**
* Get all available Two Factor providers.
*
* @since 0.1.0
*
* @return array Array of provider class names.
*/
public static function get_all_providers(): array {
if ( ! class_exists( 'Two_Factor_Core' ) ) {
return array();
}
return Two_Factor_Core::get_providers();
}
/**
* Get provider display names.
*
* @since 0.1.0
*
* @return array Array of provider class => display name.
*/
public static function get_provider_names(): array {
$providers = self::get_all_providers();
$names = array();
foreach ( $providers as $class_name => $provider ) {
if ( is_object( $provider ) && method_exists( $provider, 'get_label' ) ) {
$names[ $class_name ] = $provider->get_label();
} else {
$names[ $class_name ] = $class_name;
}
}
return $names;
}
/**
* Get user's enabled providers.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array Array of enabled provider class names.
*/
public static function get_user_enabled_providers( int $user_id ): array {
if ( ! class_exists( 'Two_Factor_Core' ) ) {
return array();
}
return Two_Factor_Core::get_enabled_providers_for_user( $user_id );
}
/**
* Get user's primary provider.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return string|null Primary provider class name or null.
*/
public static function get_user_primary_provider( int $user_id ): ?string {
if ( ! class_exists( 'Two_Factor_Core' ) ) {
return null;
}
$primary = Two_Factor_Core::get_primary_provider_for_user( $user_id );
return $primary ? get_class( $primary ) : null;
}
/**
* Check if user has specific provider enabled.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $provider_class Provider class name.
*
* @return bool True if provider is enabled, false otherwise.
*/
public static function user_has_provider_enabled( int $user_id, string $provider_class ): bool {
$enabled_providers = self::get_user_enabled_providers( $user_id );
return isset( $enabled_providers[ $provider_class ] );
}
/**
* Check if Two Factor plugin is available.
*
* @since 0.1.0
*
* @return bool True if Two Factor is available, false otherwise.
*/
public static function is_two_factor_available(): bool {
return class_exists( 'Two_Factor_Core' );
}
/**
* Get provider by class name.
*
* @since 0.1.0
*
* @param string $class_name Provider class name.
*
* @return object|null Provider instance or null if not found.
*/
public static function get_provider_instance( string $class_name ): ?object {
$providers = self::get_all_providers();
return $providers[ $class_name ] ?? null;
}
/**
* Get email provider class name.
*
* @since 0.1.0
*
* @return string|null Email provider class name or null if not available.
*/
public static function get_email_provider_class(): ?string {
$providers = self::get_all_providers();
// Common email provider class names.
$email_classes = array(
'Two_Factor_Email',
'Two_Factor_Email_Provider',
);
foreach ( $email_classes as $class ) {
if ( isset( $providers[ $class ] ) ) {
return $class;
}
}
// Fallback: check if any provider has 'email' in the name.
foreach ( array_keys( $providers ) as $class ) {
if ( stripos( $class, 'email' ) !== false ) {
return $class;
}
}
return null;
}
/**
* Check if user has 2FA enabled.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return bool True if user has at least one provider enabled.
*/
public static function user_has_2fa_enabled( int $user_id ): bool {
$enabled_providers = self::get_user_enabled_providers( $user_id );
return ! empty( $enabled_providers );
}
}

View file

@ -0,0 +1,295 @@
<?php
/**
* Provider filter class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Provider_Filter
*
* Filters visible 2FA providers based on user roles.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Provider_Filter {
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
// Filter available providers for user.
add_filter( 'two_factor_providers', array( $this, 'filter_providers_by_role' ), 10, 1 );
// Add explanation text in user profile.
add_action( 'show_user_profile', array( $this, 'render_visibility_explanation' ), 5 );
add_action( 'edit_user_profile', array( $this, 'render_visibility_explanation' ), 5 );
}
/**
* Filter providers based on user role.
*
* @since 0.1.0
*
* @param array $providers Available providers.
*
* @return array Filtered providers.
*/
public function filter_providers_by_role( array $providers ): array {
// IMPORTANT: Only filter providers on user profile pages, NOT on admin settings pages.
// Check if we're on a user profile/edit page.
global $pagenow;
$is_profile_page = ( 'profile.php' === $pagenow || 'user-edit.php' === $pagenow );
// Don't filter on our settings page or other admin pages.
if ( ! $is_profile_page ) {
return $providers;
}
// Get current user being edited.
$user_id = $this->get_profile_user_id();
if ( ! $user_id ) {
return $providers;
}
// Get visible providers for user.
$visible_providers = $this->get_visible_providers_for_user( $user_id );
// If no visibility rules, show all providers.
if ( null === $visible_providers ) {
return $providers;
}
// Filter providers.
$filtered = array();
foreach ( $providers as $class_name => $provider ) {
// Always show required providers.
if ( $this->is_required_provider( $user_id, $class_name ) ) {
$filtered[ $class_name ] = $provider;
continue;
}
// Show if visible for user's roles.
if ( in_array( $class_name, $visible_providers, true ) ) {
$filtered[ $class_name ] = $provider;
}
}
return $filtered;
}
/**
* Get visible providers for user based on their roles.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array|null Array of visible provider class names, or null if no rules.
*/
public function get_visible_providers_for_user( int $user_id ): ?array {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
if ( empty( $user_roles ) || ! isset( $settings['provider_visibility'] ) ) {
return null;
}
$visible = array();
// Collect visible providers from all user roles.
foreach ( $user_roles as $role ) {
if ( isset( $settings['provider_visibility'][ $role ] ) ) {
$role_visible = $settings['provider_visibility'][ $role ];
if ( is_array( $role_visible ) ) {
$visible = array_merge( $visible, $role_visible );
}
}
}
// If no visibility rules for any role, return null (show all).
if ( empty( $visible ) ) {
return null;
}
return array_unique( $visible );
}
/**
* Check if provider is required for user.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $provider_class Provider class name.
*
* @return bool True if provider is required.
*/
private function is_required_provider( int $user_id, string $provider_class ): bool {
$enforcement = two_factor_extended()->get_enforcement();
if ( ! $enforcement ) {
return false;
}
$required_providers = $enforcement->get_required_providers_for_user( $user_id );
return in_array( $provider_class, $required_providers, true );
}
/**
* Get user ID from profile page context.
*
* Uses filter_input() for safe GET parameter access when determining
* which user's profile is being viewed. This is a read-only operation
* used for displaying the correct 2FA provider options.
*
* @since 0.1.0
*
* @return int|null User ID or null.
*/
private function get_profile_user_id(): ?int {
// Check if editing another user's profile (e.g., wp-admin/user-edit.php?user_id=123).
// Use filter_input() for safe access to GET parameter.
$user_id = filter_input( INPUT_GET, 'user_id', FILTER_VALIDATE_INT );
if ( $user_id && $user_id > 0 ) {
return $user_id;
}
// Check if editing own profile.
return get_current_user_id();
}
/**
* Render visibility explanation in user profile.
*
* @since 0.1.0
*
* @param WP_User $user User object.
*/
public function render_visibility_explanation( WP_User $user ): void {
$visible_providers = $this->get_visible_providers_for_user( $user->ID );
// No visibility rules set.
if ( null === $visible_providers ) {
return;
}
$all_providers = Two_Factor_Extended_Provider_Detector::get_all_providers();
$required_providers = two_factor_extended()->get_enforcement()->get_required_providers_for_user( $user->ID );
$provider_names = Two_Factor_Extended_Provider_Detector::get_provider_names();
// Calculate hidden providers.
$hidden_providers = array();
foreach ( array_keys( $all_providers ) as $class ) {
// Skip required providers (always visible).
if ( in_array( $class, $required_providers, true ) ) {
continue;
}
// Not in visible list = hidden.
if ( ! in_array( $class, $visible_providers, true ) ) {
$hidden_providers[] = $provider_names[ $class ] ?? $class;
}
}
if ( empty( $hidden_providers ) ) {
return;
}
?>
<div class="notice notice-info inline">
<p>
<strong><?php esc_html_e( 'Two Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
/* translators: %s: List of hidden providers */
esc_html__( 'Some 2FA methods are hidden based on your role: %s', 'two-factor-extended' ),
'<strong>' . esc_html( implode( ', ', $hidden_providers ) ) . '</strong>'
);
?>
</p>
</div>
<?php
}
/**
* Get provider visibility inheritance for multiple roles.
*
* Implements union logic: user sees providers visible to ANY of their roles.
*
* @since 0.1.0
*
* @param array $roles Array of role slugs.
*
* @return array Array of visible provider class names.
*/
public function get_inherited_visibility( array $roles ): array {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$visible = array();
if ( ! isset( $settings['provider_visibility'] ) ) {
return array();
}
foreach ( $roles as $role ) {
if ( isset( $settings['provider_visibility'][ $role ] ) ) {
$role_visible = $settings['provider_visibility'][ $role ];
if ( is_array( $role_visible ) ) {
$visible = array_merge( $visible, $role_visible );
}
}
}
return array_unique( $visible );
}
/**
* Check if provider is visible for user.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $provider_class Provider class name.
*
* @return bool True if provider is visible, false otherwise.
*/
public function is_provider_visible( int $user_id, string $provider_class ): bool {
// Required providers are always visible.
if ( $this->is_required_provider( $user_id, $provider_class ) ) {
return true;
}
$visible_providers = $this->get_visible_providers_for_user( $user_id );
// No visibility rules = all visible.
if ( null === $visible_providers ) {
return true;
}
return in_array( $provider_class, $visible_providers, true );
}
}

414
includes/class-rest-api.php Normal file
View file

@ -0,0 +1,414 @@
<?php
/**
* REST API endpoints
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_REST_API
*
* Provides REST API endpoints for Two Factor Extended.
*
* @since 0.1.0
*/
class Two_Factor_Extended_REST_API {
/**
* API namespace.
*
* @since 0.1.0
* @var string
*/
const NAMESPACE = 'two-factor-extended/v1';
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
}
/**
* Register REST API routes.
*
* @since 0.1.0
*/
public function register_routes(): void {
// GET /two-factor-extended/v1/status.
register_rest_route(
self::NAMESPACE,
'/status',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_status' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
),
)
);
// GET /two-factor-extended/v1/users.
register_rest_route(
self::NAMESPACE,
'/users',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_users' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
'non_compliant_only' => array(
'type' => 'boolean',
'default' => false,
),
),
)
);
// POST /two-factor-extended/v1/enforce.
register_rest_route(
self::NAMESPACE,
'/enforce',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'enforce_requirements' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'user_ids' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'integer',
),
),
'reset_grace' => array(
'type' => 'boolean',
'default' => false,
),
),
)
);
// POST /two-factor-extended/v1/reset.
register_rest_route(
self::NAMESPACE,
'/reset',
array(
'methods' => WP_REST_Server::CREATABLE,
'callback' => array( $this, 'reset_grace_period' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'user_ids' => array(
'type' => 'array',
'required' => true,
'items' => array(
'type' => 'integer',
),
),
),
)
);
// GET /two-factor-extended/v1/report.
register_rest_route(
self::NAMESPACE,
'/report',
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( $this, 'get_report' ),
'permission_callback' => array( $this, 'check_manage_options_permission' ),
'args' => array(
'role' => array(
'type' => 'string',
'sanitize_callback' => 'sanitize_key',
'default' => '',
),
'format' => array(
'type' => 'string',
'enum' => array( 'json', 'csv' ),
'default' => 'json',
),
),
)
);
}
/**
* Check if user has manage_options permission.
*
* @since 0.1.0
*
* @return bool True if user has permission.
*/
public function check_manage_options_permission(): bool {
return current_user_can( 'manage_options' );
}
/**
* Get compliance status.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_status( WP_REST_Request $request ): WP_REST_Response {
try {
$role = $request->get_param( 'role' );
$compliance = two_factor_extended()->get_compliance_report();
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
return new WP_REST_Response(
array(
'success' => true,
'data' => $stats,
),
200
);
} catch ( Exception $e ) {
return new WP_REST_Response(
array(
'success' => false,
'message' => __( 'Unable to retrieve compliance status.', 'two-factor-extended' ),
),
500
);
}
}
/**
* Get users list.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_users( WP_REST_Request $request ): WP_REST_Response {
$role = $request->get_param( 'role' );
$non_compliant_only = $request->get_param( 'non_compliant_only' );
$compliance = two_factor_extended()->get_compliance_report();
if ( $non_compliant_only ) {
$users = $compliance->get_non_compliant_users( array( 'role' => $role ) );
} else {
// Get all users with compliance status.
$user_args = array( 'fields' => 'all' );
if ( ! empty( $role ) ) {
$user_args['role'] = $role;
}
$all_users = get_users( $user_args );
$enforcement = two_factor_extended()->get_enforcement();
$users = array();
foreach ( $all_users as $user ) {
$required = $enforcement->get_required_providers_for_user( $user->ID );
$users[] = array(
'user_id' => $user->ID,
'user_login' => $user->user_login,
'user_email' => $user->user_email,
'roles' => Two_Factor_Extended_Role_Manager::get_user_roles( $user->ID ),
'compliant' => empty( $required ) || $enforcement->user_meets_requirements( $user->ID, $required ),
'in_grace' => $enforcement->is_in_grace_period( $user->ID ),
'grace_remaining' => $enforcement->get_grace_period_remaining_days( $user->ID ),
);
}
}
return new WP_REST_Response(
array(
'success' => true,
'data' => $users,
'total' => count( $users ),
),
200
);
}
/**
* Enforce 2FA requirements.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function enforce_requirements( WP_REST_Request $request ): WP_REST_Response {
$user_ids = $request->get_param( 'user_ids' );
$reset_grace = $request->get_param( 'reset_grace' );
$enforcement = two_factor_extended()->get_enforcement();
$processed = 0;
$errors = array();
foreach ( $user_ids as $user_id ) {
$user = get_userdata( $user_id );
if ( ! $user ) {
$errors[] = sprintf( 'User not found: %d', $user_id );
continue;
}
$required = $enforcement->get_required_providers_for_user( $user_id );
if ( empty( $required ) ) {
$errors[] = sprintf( 'No 2FA requirements for user: %s', $user->user_login );
continue;
}
// Set enforcement start date.
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
if ( $reset_grace ) {
delete_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
}
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'api_enforce',
sprintf( 'REST API: Enforced 2FA for user %s', $user->user_login ),
$user_id,
array( 'required_providers' => $required )
);
$processed++;
}
return new WP_REST_Response(
array(
'success' => true,
'processed' => $processed,
'errors' => $errors,
),
200
);
}
/**
* Reset grace period.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function reset_grace_period( WP_REST_Request $request ): WP_REST_Response {
$user_ids = $request->get_param( 'user_ids' );
$processed = 0;
$errors = array();
foreach ( $user_ids as $user_id ) {
$user = get_userdata( $user_id );
if ( ! $user ) {
$errors[] = sprintf( 'User not found: %d', $user_id );
continue;
}
// Reset enforcement start date.
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
// Clear grace period notified flag.
delete_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
// Log the action.
two_factor_extended()->get_audit_log()->log_event(
'api_reset_grace',
sprintf( 'REST API: Reset grace period for user %s', $user->user_login ),
$user_id
);
$processed++;
}
return new WP_REST_Response(
array(
'success' => true,
'processed' => $processed,
'errors' => $errors,
),
200
);
}
/**
* Get compliance report.
*
* @since 0.1.0
*
* @param WP_REST_Request $request Request object.
*
* @return WP_REST_Response Response object.
*/
public function get_report( WP_REST_Request $request ): WP_REST_Response {
$role = $request->get_param( 'role' );
$format = $request->get_param( 'format' );
$compliance = two_factor_extended()->get_compliance_report();
if ( 'csv' === $format ) {
$csv = $compliance->export_to_csv( array( 'role' => $role ) );
return new WP_REST_Response(
array(
'success' => true,
'data' => $csv,
'format' => 'csv',
),
200,
array(
'Content-Type' => 'text/csv',
)
);
}
// JSON format.
$non_compliant = $compliance->get_non_compliant_users( array( 'role' => $role ) );
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
return new WP_REST_Response(
array(
'success' => true,
'statistics' => $stats,
'non_compliant' => $non_compliant,
),
200
);
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* Role manager class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Role_Manager
*
* Manages WordPress roles and role detection.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Role_Manager {
/**
* Get all WordPress roles.
*
* @since 0.1.0
*
* @return array Array of role slugs and names.
*/
public static function get_all_roles(): array {
if ( ! function_exists( 'get_editable_roles' ) ) {
require_once ABSPATH . 'wp-admin/includes/user.php';
}
$roles = array();
$wp_roles = get_editable_roles();
foreach ( $wp_roles as $role_slug => $role_data ) {
$roles[ $role_slug ] = translate_user_role( $role_data['name'] );
}
return $roles;
}
/**
* Get role display name.
*
* @since 0.1.0
*
* @param string $role_slug Role slug.
*
* @return string Role display name or slug if not found.
*/
public static function get_role_display_name( string $role_slug ): string {
$roles = self::get_all_roles();
return $roles[ $role_slug ] ?? $role_slug;
}
/**
* Get user's roles.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array Array of role slugs.
*/
public static function get_user_roles( int $user_id ): array {
$user = get_userdata( $user_id );
if ( ! $user || ! isset( $user->roles ) ) {
return array();
}
return $user->roles;
}
/**
* Check if user has specific role.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $role_slug Role slug to check.
*
* @return bool True if user has the role, false otherwise.
*/
public static function user_has_role( int $user_id, string $role_slug ): bool {
$user_roles = self::get_user_roles( $user_id );
return in_array( $role_slug, $user_roles, true );
}
/**
* Get user's primary role (first role).
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return string|null Primary role slug or null if user has no roles.
*/
public static function get_user_primary_role( int $user_id ): ?string {
$roles = self::get_user_roles( $user_id );
return ! empty( $roles ) ? $roles[0] : null;
}
/**
* Check if role exists.
*
* @since 0.1.0
*
* @param string $role_slug Role slug.
*
* @return bool True if role exists, false otherwise.
*/
public static function role_exists( string $role_slug ): bool {
$roles = self::get_all_roles();
return isset( $roles[ $role_slug ] );
}
/**
* Get roles for Multisite (network vs site roles).
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param int $blog_id Blog ID (optional, defaults to current blog).
*
* @return array Array of role slugs for the specified blog.
*/
public static function get_user_roles_for_blog( int $user_id, int $blog_id = 0 ): array {
if ( ! is_multisite() ) {
return self::get_user_roles( $user_id );
}
if ( 0 === $blog_id ) {
$blog_id = get_current_blog_id();
}
$user = get_userdata( $user_id );
if ( ! $user ) {
return array();
}
// Get roles for specific blog.
$roles_key = $GLOBALS['wpdb']->get_blog_prefix( $blog_id ) . 'capabilities';
$roles = isset( $user->{$roles_key} ) ? array_keys( $user->{$roles_key} ) : array();
return $roles;
}
/**
* Check if user is super admin (Multisite).
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return bool True if user is super admin, false otherwise.
*/
public static function is_super_admin( int $user_id ): bool {
if ( ! is_multisite() ) {
return false;
}
return is_super_admin( $user_id );
}
}

1834
includes/class-settings.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,412 @@
<?php
/**
* Main plugin class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended
*
* Main plugin class implementing singleton pattern.
*
* @since 0.1.0
*/
class Two_Factor_Extended {
/**
* Plugin instance.
*
* @since 0.1.0
* @var Two_Factor_Extended|null
*/
private static $instance = null;
/**
* Plugin version.
*
* @since 0.1.0
* @var string
*/
private string $version = '1.0.0';
/**
* Plugin directory path.
*
* @since 0.1.0
* @var string
*/
private string $plugin_path;
/**
* Plugin directory URL.
*
* @since 0.1.0
* @var string
*/
private string $plugin_url;
/**
* Plugin basename.
*
* @since 0.1.0
* @var string
*/
private string $plugin_basename;
/**
* Settings instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Settings|null
*/
private $settings = null;
/**
* Enforcement instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Enforcement|null
*/
private $enforcement = null;
/**
* Provider filter instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Provider_Filter|null
*/
private $provider_filter = null;
/**
* Network settings instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Network_Settings|null
*/
private $network_settings = null;
/**
* Audit log instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Audit_Log|null
*/
private $audit_log = null;
/**
* Compliance report instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Compliance_Report|null
*/
private $compliance_report = null;
/**
* Bulk actions instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_Bulk_Actions|null
*/
private $bulk_actions = null;
/**
* REST API instance.
*
* @since 0.1.0
* @var Two_Factor_Extended_REST_API|null
*/
private $rest_api = null;
/**
* Get plugin instance (singleton).
*
* @since 0.1.0
*
* @return Two_Factor_Extended Plugin instance.
*/
public static function get_instance(): Two_Factor_Extended {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor.
*
* Private to enforce singleton pattern.
*
* @since 0.1.0
*/
private function __construct() {
$this->plugin_path = plugin_dir_path( dirname( __FILE__ ) );
$this->plugin_url = plugin_dir_url( dirname( __FILE__ ) );
$this->plugin_basename = plugin_basename( dirname( dirname( __FILE__ ) ) . '/two-factor-extended.php' );
$this->init();
}
/**
* Initialize the plugin.
*
* @since 0.1.0
*/
private function init(): void {
// Load dependencies.
$this->load_dependencies();
// Initialize components.
$this->init_hooks();
}
/**
* Load plugin dependencies.
*
* @since 0.1.0
*/
private function load_dependencies(): void {
require_once $this->plugin_path . 'includes/class-dependency-checker.php';
require_once $this->plugin_path . 'includes/class-role-manager.php';
require_once $this->plugin_path . 'includes/class-provider-detector.php';
require_once $this->plugin_path . 'includes/class-enforcement.php';
require_once $this->plugin_path . 'includes/class-provider-filter.php';
require_once $this->plugin_path . 'includes/class-network-settings.php';
require_once $this->plugin_path . 'includes/class-audit-log.php';
require_once $this->plugin_path . 'includes/class-compliance-report.php';
require_once $this->plugin_path . 'includes/class-bulk-actions.php';
require_once $this->plugin_path . 'includes/class-rest-api.php';
require_once $this->plugin_path . 'includes/class-settings.php';
// Load CLI commands if WP-CLI is available.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
require_once $this->plugin_path . 'includes/class-cli-commands.php';
}
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
// Plugin loaded hook.
add_action( 'plugins_loaded', array( $this, 'on_plugins_loaded' ) );
// Load text domain.
add_action( 'init', array( $this, 'load_textdomain' ) );
}
/**
* Actions to run when all plugins are loaded.
*
* @since 0.1.0
*/
public function on_plugins_loaded(): void {
// Check dependencies.
if ( ! Two_Factor_Extended_Dependency_Checker::check_dependencies() ) {
return;
}
// Initialize network settings (Multisite).
$this->network_settings = new Two_Factor_Extended_Network_Settings();
// Initialize audit log.
$this->audit_log = new Two_Factor_Extended_Audit_Log();
// Initialize compliance report.
$this->compliance_report = new Two_Factor_Extended_Compliance_Report();
// Initialize settings.
$this->settings = new Two_Factor_Extended_Settings();
// Initialize enforcement.
$this->enforcement = new Two_Factor_Extended_Enforcement();
// Initialize provider filter.
$this->provider_filter = new Two_Factor_Extended_Provider_Filter();
// Initialize bulk actions.
$this->bulk_actions = new Two_Factor_Extended_Bulk_Actions();
// Initialize REST API.
$this->rest_api = new Two_Factor_Extended_REST_API();
// Register WP-CLI commands if available.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command( 'two-factor-extended', 'Two_Factor_Extended_CLI_Commands' );
}
// Plugin is ready to work.
do_action( 'two_factor_extended_loaded' );
}
/**
* Get settings instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Settings Settings instance.
*/
public function get_settings(): Two_Factor_Extended_Settings {
return $this->settings;
}
/**
* Get enforcement instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Enforcement Enforcement instance.
*/
public function get_enforcement(): Two_Factor_Extended_Enforcement {
return $this->enforcement;
}
/**
* Get provider filter instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Provider_Filter Provider filter instance.
*/
public function get_provider_filter(): Two_Factor_Extended_Provider_Filter {
return $this->provider_filter;
}
/**
* Get network settings instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Network_Settings Network settings instance.
*/
public function get_network_settings(): Two_Factor_Extended_Network_Settings {
return $this->network_settings;
}
/**
* Get audit log instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Audit_Log Audit log instance.
*/
public function get_audit_log(): Two_Factor_Extended_Audit_Log {
return $this->audit_log;
}
/**
* Get compliance report instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Compliance_Report Compliance report instance.
*/
public function get_compliance_report(): Two_Factor_Extended_Compliance_Report {
return $this->compliance_report;
}
/**
* Get bulk actions instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_Bulk_Actions Bulk actions instance.
*/
public function get_bulk_actions(): Two_Factor_Extended_Bulk_Actions {
return $this->bulk_actions;
}
/**
* Get REST API instance.
*
* @since 0.1.0
*
* @return Two_Factor_Extended_REST_API REST API instance.
*/
public function get_rest_api(): Two_Factor_Extended_REST_API {
return $this->rest_api;
}
/**
* Load plugin text domain.
*
* @since 0.1.0
*/
public function load_textdomain(): void {
// phpcs:ignore PluginCheck.CodeAnalysis.DiscouragedFunctions.load_plugin_textdomainFound -- Needed for custom translation loading
load_plugin_textdomain(
'two-factor-extended',
false,
dirname( $this->plugin_basename ) . '/languages'
);
}
/**
* Get plugin version.
*
* @since 0.1.0
*
* @return string Plugin version.
*/
public function get_version(): string {
return $this->version;
}
/**
* Get plugin directory path.
*
* @since 0.1.0
*
* @return string Plugin directory path with trailing slash.
*/
public function get_plugin_path(): string {
return $this->plugin_path;
}
/**
* Get plugin directory URL.
*
* @since 0.1.0
*
* @return string Plugin directory URL with trailing slash.
*/
public function get_plugin_url(): string {
return $this->plugin_url;
}
/**
* Get plugin basename.
*
* @since 0.1.0
*
* @return string Plugin basename (e.g., 'two-factor-extended/two-factor-extended.php').
*/
public function get_plugin_basename(): string {
return $this->plugin_basename;
}
/**
* Prevent cloning.
*
* @since 0.1.0
*/
private function __clone() {
}
/**
* Prevent unserialization.
*
* @since 0.1.0
*/
public function __wakeup() {
}
}