v1.0.0
This commit is contained in:
parent
3291a5f7d2
commit
212d64abfa
27 changed files with 8654 additions and 2 deletions
410
includes/class-audit-log.php
Normal file
410
includes/class-audit-log.php
Normal 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;
|
||||
}
|
||||
}
|
||||
268
includes/class-bulk-actions.php
Normal file
268
includes/class-bulk-actions.php
Normal 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
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
342
includes/class-cli-commands.php
Normal file
342
includes/class-cli-commands.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
314
includes/class-compliance-report.php
Normal file
314
includes/class-compliance-report.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
175
includes/class-dependency-checker.php
Normal file
175
includes/class-dependency-checker.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
368
includes/class-enforcement.php
Normal file
368
includes/class-enforcement.php
Normal 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.
|
||||
}
|
||||
}
|
||||
433
includes/class-network-settings.php
Normal file
433
includes/class-network-settings.php
Normal 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'] );
|
||||
}
|
||||
}
|
||||
184
includes/class-provider-detector.php
Normal file
184
includes/class-provider-detector.php
Normal 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 );
|
||||
}
|
||||
}
|
||||
295
includes/class-provider-filter.php
Normal file
295
includes/class-provider-filter.php
Normal 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
414
includes/class-rest-api.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
||||
173
includes/class-role-manager.php
Normal file
173
includes/class-role-manager.php
Normal 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
1834
includes/class-settings.php
Normal file
File diff suppressed because it is too large
Load diff
412
includes/class-two-factor-extended.php
Normal file
412
includes/class-two-factor-extended.php
Normal 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() {
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue