v1.0.1
This commit is contained in:
parent
212d64abfa
commit
b8e17df2db
17 changed files with 1145 additions and 732 deletions
|
|
@ -80,7 +80,7 @@ class Two_Factor_Extended_Audit_Log {
|
|||
* @param string $action Action type.
|
||||
* @param string $description Event description.
|
||||
* @param int $user_id User ID (optional).
|
||||
* @param array $metadata Additional metadata (optional).
|
||||
* @param array<string, mixed> $metadata Additional metadata (optional).
|
||||
*/
|
||||
public function log_event( string $action, string $description, int $user_id = 0, array $metadata = array() ): void {
|
||||
$logs = $this->get_logs();
|
||||
|
|
@ -176,6 +176,11 @@ class Two_Factor_Extended_Audit_Log {
|
|||
|
||||
// Check if failure was due to 2FA requirements.
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$required = $enforcement->get_required_providers_for_user( $user->ID );
|
||||
|
||||
if ( ! empty( $required ) && ! $enforcement->user_meets_requirements( $user->ID, $required ) ) {
|
||||
|
|
@ -198,17 +203,26 @@ class Two_Factor_Extended_Audit_Log {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $filters Optional filters (action, user_id, date_from, date_to).
|
||||
* @param array<string, mixed> $filters Optional filters (action, user_id, date_from, date_to).
|
||||
*
|
||||
* @return array Array of log entries.
|
||||
* @return array<int, array<string, mixed>> Array of log entries.
|
||||
*/
|
||||
public function get_logs( array $filters = array() ): array {
|
||||
$logs = get_option( self::OPTION_KEY, array() );
|
||||
$raw = get_option( self::OPTION_KEY, array() );
|
||||
|
||||
if ( ! is_array( $logs ) ) {
|
||||
if ( ! is_array( $raw ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Validate that each entry is an array.
|
||||
$logs = array();
|
||||
|
||||
foreach ( $raw as $entry ) {
|
||||
if ( is_array( $entry ) ) {
|
||||
$logs[] = $entry;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply filters.
|
||||
if ( ! empty( $filters ) ) {
|
||||
$logs = $this->filter_logs( $logs, $filters );
|
||||
|
|
@ -222,36 +236,51 @@ class Two_Factor_Extended_Audit_Log {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $logs Log entries.
|
||||
* @param array $filters Filter criteria.
|
||||
* @param array<int, array<string, mixed>> $logs Log entries.
|
||||
* @param array<string, mixed> $filters Filter criteria.
|
||||
*
|
||||
* @return array Filtered logs.
|
||||
* @return array<int, array<string, mixed>> Filtered logs.
|
||||
*/
|
||||
private function filter_logs( array $logs, array $filters ): array {
|
||||
return array_filter(
|
||||
return array_values(
|
||||
array_filter(
|
||||
$logs,
|
||||
function ( $log ) use ( $filters ) {
|
||||
function ( array $log ) use ( $filters ): bool {
|
||||
// Filter by action.
|
||||
if ( ! empty( $filters['action'] ) && $log['action'] !== $filters['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'] ) {
|
||||
$filter_user_id = isset( $filters['user_id'] ) && is_numeric( $filters['user_id'] ) ? (int) $filters['user_id'] : 0;
|
||||
$log_user_id = isset( $log['user_id'] ) && is_numeric( $log['user_id'] ) ? (int) $log['user_id'] : 0;
|
||||
|
||||
if ( ! empty( $filters['user_id'] ) && $log_user_id !== $filter_user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by date range.
|
||||
if ( ! empty( $filters['date_from'] ) && $log['timestamp'] < strtotime( $filters['date_from'] ) ) {
|
||||
$timestamp = isset( $log['timestamp'] ) && is_numeric( $log['timestamp'] ) ? (int) $log['timestamp'] : 0;
|
||||
|
||||
if ( ! empty( $filters['date_from'] ) && is_string( $filters['date_from'] ) ) {
|
||||
$from = strtotime( $filters['date_from'] );
|
||||
|
||||
if ( false !== $from && $timestamp < $from ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $filters['date_to'] ) && $log['timestamp'] > strtotime( $filters['date_to'] ) ) {
|
||||
if ( ! empty( $filters['date_to'] ) && is_string( $filters['date_to'] ) ) {
|
||||
$to = strtotime( $filters['date_to'] );
|
||||
|
||||
if ( false !== $to && $timestamp > $to ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -277,8 +306,10 @@ class Two_Factor_Extended_Audit_Log {
|
|||
|
||||
$filtered_logs = array_filter(
|
||||
$logs,
|
||||
function ( $log ) use ( $cutoff_time ) {
|
||||
return $log['timestamp'] >= $cutoff_time;
|
||||
function ( array $log ) use ( $cutoff_time ): bool {
|
||||
$ts = isset( $log['timestamp'] ) && is_numeric( $log['timestamp'] ) ? (int) $log['timestamp'] : 0;
|
||||
|
||||
return $ts >= $cutoff_time;
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -290,7 +321,7 @@ class Two_Factor_Extended_Audit_Log {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $filters Optional filters.
|
||||
* @param array<string, mixed> $filters Optional filters.
|
||||
*
|
||||
* @return string CSV content.
|
||||
*/
|
||||
|
|
@ -301,16 +332,20 @@ class Two_Factor_Extended_Audit_Log {
|
|||
$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;
|
||||
$log_user_id = isset( $log['user_id'] ) && is_int( $log['user_id'] ) ? $log['user_id'] : 0;
|
||||
$log_actor_id = isset( $log['actor_id'] ) && is_int( $log['actor_id'] ) ? $log['actor_id'] : 0;
|
||||
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
|
||||
|
||||
$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'],
|
||||
$log_ts ? gmdate( 'Y-m-d H:i:s', $log_ts ) : '',
|
||||
isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '',
|
||||
isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '',
|
||||
$user ? $user->user_login : '-',
|
||||
$actor ? $actor->user_login : 'System',
|
||||
$log['ip_address'],
|
||||
isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -318,13 +353,17 @@ class Two_Factor_Extended_Audit_Log {
|
|||
ob_start();
|
||||
$handle = fopen( 'php://output', 'w' );
|
||||
|
||||
if ( false !== $handle ) {
|
||||
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();
|
||||
$output = ob_get_clean();
|
||||
|
||||
return false !== $output ? $output : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -337,11 +376,11 @@ class Two_Factor_Extended_Audit_Log {
|
|||
private function get_client_ip(): string {
|
||||
$ip = '';
|
||||
|
||||
if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
|
||||
if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) && is_string( $_SERVER['HTTP_CLIENT_IP'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) );
|
||||
} elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
|
||||
} elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) && is_string( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) );
|
||||
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
|
||||
} elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) && is_string( $_SERVER['REMOTE_ADDR'] ) ) {
|
||||
$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
|
||||
}
|
||||
|
||||
|
|
@ -356,7 +395,7 @@ class Two_Factor_Extended_Audit_Log {
|
|||
* @param mixed $old_value Old value.
|
||||
* @param mixed $new_value New value.
|
||||
*
|
||||
* @return array Changed keys.
|
||||
* @return array<int, string|int> Changed keys.
|
||||
*/
|
||||
private function get_changed_keys( $old_value, $new_value ): array {
|
||||
if ( ! is_array( $old_value ) || ! is_array( $new_value ) ) {
|
||||
|
|
@ -379,7 +418,7 @@ class Two_Factor_Extended_Audit_Log {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Statistics.
|
||||
* @return array{total: int, by_action: array<string, int>, recent_count: int} Statistics.
|
||||
*/
|
||||
public function get_statistics(): array {
|
||||
$logs = $this->get_logs();
|
||||
|
|
@ -393,14 +432,17 @@ class Two_Factor_Extended_Audit_Log {
|
|||
$recent_cutoff = current_time( 'timestamp' ) - ( 7 * DAY_IN_SECONDS );
|
||||
|
||||
foreach ( $logs as $log ) {
|
||||
$action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
|
||||
$ts = isset( $log['timestamp'] ) && is_numeric( $log['timestamp'] ) ? (int) $log['timestamp'] : 0;
|
||||
|
||||
// Count by action.
|
||||
if ( ! isset( $stats['by_action'][ $log['action'] ] ) ) {
|
||||
$stats['by_action'][ $log['action'] ] = 0;
|
||||
if ( ! isset( $stats['by_action'][ $action ] ) ) {
|
||||
$stats['by_action'][ $action ] = 0;
|
||||
}
|
||||
$stats['by_action'][ $log['action'] ]++;
|
||||
$stats['by_action'][ $action ]++;
|
||||
|
||||
// Count recent logs (last 7 days).
|
||||
if ( $log['timestamp'] >= $recent_cutoff ) {
|
||||
if ( $ts >= $recent_cutoff ) {
|
||||
$stats['recent_count']++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,9 +50,9 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $actions Existing bulk actions.
|
||||
* @param array<string, string> $actions Existing bulk actions.
|
||||
*
|
||||
* @return array Modified bulk actions.
|
||||
* @return array<string, string> Modified bulk actions.
|
||||
*/
|
||||
public function register_bulk_actions( array $actions ): array {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
|
|
@ -72,7 +72,7 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
*
|
||||
* @param string $redirect_to Redirect URL.
|
||||
* @param string $action Action name.
|
||||
* @param array $user_ids User IDs.
|
||||
* @param int[] $user_ids User IDs.
|
||||
*
|
||||
* @return string Modified redirect URL.
|
||||
*/
|
||||
|
|
@ -111,7 +111,7 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $user_ids User IDs.
|
||||
* @param int[] $user_ids User IDs.
|
||||
*
|
||||
* @return int Number of users processed.
|
||||
*/
|
||||
|
|
@ -119,6 +119,10 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
$processed = 0;
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
// Skip if user doesn't exist.
|
||||
$user = get_userdata( $user_id );
|
||||
|
|
@ -138,7 +142,10 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'bulk_require_2fa',
|
||||
sprintf(
|
||||
'Bulk action: Require 2FA for user %s',
|
||||
|
|
@ -149,6 +156,7 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
'required_providers' => $required,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
|
@ -161,7 +169,7 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $user_ids User IDs.
|
||||
* @param int[] $user_ids User IDs.
|
||||
*
|
||||
* @return int Number of users processed.
|
||||
*/
|
||||
|
|
@ -182,7 +190,10 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
delete_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'bulk_reset_grace',
|
||||
sprintf(
|
||||
'Bulk action: Reset grace period for user %s',
|
||||
|
|
@ -190,6 +201,7 @@ class Two_Factor_Extended_Bulk_Actions {
|
|||
),
|
||||
$user_id
|
||||
);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,14 +39,20 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string> $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();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
WP_CLI::error( 'Compliance report module is not available.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
|
||||
|
||||
if ( 'json' === $format ) {
|
||||
|
|
@ -129,8 +135,8 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string> $assoc_args Associative arguments.
|
||||
*/
|
||||
public function enforce( array $args, array $assoc_args ): void {
|
||||
if ( empty( $assoc_args['role'] ) ) {
|
||||
|
|
@ -150,6 +156,12 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
}
|
||||
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
WP_CLI::error( 'Enforcement module is not available.' );
|
||||
return;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
|
||||
$progress = WP_CLI\Utils\make_progress_bar( 'Enforcing 2FA requirements', count( $users ) );
|
||||
|
|
@ -170,7 +182,10 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
}
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'cli_enforce',
|
||||
sprintf( 'WP-CLI: Enforced 2FA for user %s', $user->user_login ),
|
||||
$user->ID,
|
||||
|
|
@ -179,6 +194,7 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
'required_providers' => $required,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
$progress->tick();
|
||||
|
|
@ -217,8 +233,8 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string> $assoc_args Associative arguments.
|
||||
*/
|
||||
public function report( array $args, array $assoc_args ): void {
|
||||
$role = $assoc_args['role'] ?? '';
|
||||
|
|
@ -227,6 +243,11 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
WP_CLI::error( 'Compliance report module is not available.' );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $non_compliant_only ) {
|
||||
$users = $compliance->get_non_compliant_users( array( 'role' => $role ) );
|
||||
|
||||
|
|
@ -237,12 +258,18 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
|
||||
$items = array();
|
||||
foreach ( $users as $user_data ) {
|
||||
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
||||
$missing_providers_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
||||
|
||||
$roles = array_filter( $roles_raw, 'is_string' );
|
||||
$missing_providers = array_filter( $missing_providers_raw, 'is_string' );
|
||||
|
||||
$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'] ),
|
||||
'Roles' => implode( ', ', $roles ),
|
||||
'Missing Providers' => implode( ', ', $missing_providers ),
|
||||
'Grace Period' => $user_data['in_grace_period'] ? 'Yes' : 'No',
|
||||
'Days Remaining' => $user_data['grace_remaining'],
|
||||
);
|
||||
|
|
@ -275,8 +302,8 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Positional arguments.
|
||||
* @param array $assoc_args Associative arguments.
|
||||
* @param array<int, string> $args Positional arguments.
|
||||
* @param array<string, string> $assoc_args Associative arguments.
|
||||
*/
|
||||
public function reset( array $args, array $assoc_args ): void {
|
||||
if ( empty( $assoc_args['user'] ) ) {
|
||||
|
|
@ -288,7 +315,7 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
$user_identifier = $assoc_args['user'];
|
||||
|
||||
if ( is_numeric( $user_identifier ) ) {
|
||||
$user = get_userdata( $user_identifier );
|
||||
$user = get_userdata( (int) $user_identifier );
|
||||
} elseif ( is_email( $user_identifier ) ) {
|
||||
$user = get_user_by( 'email', $user_identifier );
|
||||
} else {
|
||||
|
|
@ -307,11 +334,15 @@ class Two_Factor_Extended_CLI_Commands {
|
|||
delete_user_meta( $user->ID, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$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(
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Optional arguments (role, blog_id).
|
||||
* @param array<string, mixed> $args Optional arguments (role, blog_id).
|
||||
*
|
||||
* @return array Compliance statistics.
|
||||
* @return array{total_users: int, compliant_users: int, non_compliant: int, grace_period: int, no_requirements: int, by_role: array<string, array{total: int, compliant: int, non_compliant: int}>} Compliance statistics.
|
||||
*/
|
||||
public function get_compliance_stats( array $args = array() ): array {
|
||||
$defaults = array(
|
||||
|
|
@ -63,6 +63,10 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return $stats;
|
||||
}
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$required = $enforcement->get_required_providers_for_user( $user->ID );
|
||||
|
||||
|
|
@ -112,9 +116,9 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Optional arguments.
|
||||
* @param array<string, mixed> $args Optional arguments.
|
||||
*
|
||||
* @return array Array of non-compliant user data.
|
||||
* @return array<int, array<string, mixed>> Array of non-compliant user data.
|
||||
*/
|
||||
public function get_non_compliant_users( array $args = array() ): array {
|
||||
$defaults = array(
|
||||
|
|
@ -140,6 +144,10 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
$non_compliant = array();
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return $non_compliant;
|
||||
}
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$required = $enforcement->get_required_providers_for_user( $user->ID );
|
||||
|
||||
|
|
@ -152,7 +160,6 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
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();
|
||||
|
||||
|
|
@ -180,7 +187,7 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Network compliance report.
|
||||
* @return array<string, mixed> Network compliance report.
|
||||
*/
|
||||
public function get_network_report(): array {
|
||||
if ( ! is_multisite() ) {
|
||||
|
|
@ -197,7 +204,7 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
);
|
||||
|
||||
foreach ( $sites as $site ) {
|
||||
switch_to_blog( $site->blog_id );
|
||||
switch_to_blog( (int) $site->blog_id );
|
||||
|
||||
$site_stats = $this->get_compliance_stats( array( 'blog_id' => $site->blog_id ) );
|
||||
|
||||
|
|
@ -224,7 +231,7 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Optional arguments.
|
||||
* @param array<string, mixed> $args Optional arguments.
|
||||
*
|
||||
* @return string CSV content.
|
||||
*/
|
||||
|
|
@ -235,27 +242,41 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
$csv[] = array( 'User ID', 'Username', 'Email', 'Roles', 'Missing Providers', 'Grace Period', 'Days Remaining' );
|
||||
|
||||
foreach ( $non_compliant as $user_data ) {
|
||||
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
||||
$missing_providers_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
||||
$roles = array_filter( $roles_raw, 'is_string' );
|
||||
$missing_providers = array_filter( $missing_providers_raw, 'is_string' );
|
||||
$user_id_val = isset( $user_data['user_id'] ) && is_int( $user_data['user_id'] ) ? (string) $user_data['user_id'] : '';
|
||||
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
|
||||
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
|
||||
$grace_remaining_val = isset( $user_data['grace_remaining'] ) && is_scalar( $user_data['grace_remaining'] ) ? (string) $user_data['grace_remaining'] : '';
|
||||
$in_grace_val = ! empty( $user_data['in_grace_period'] ) ? 'Yes' : 'No';
|
||||
|
||||
$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'],
|
||||
$user_id_val,
|
||||
$user_login_val,
|
||||
$user_email_val,
|
||||
implode( ', ', $roles ),
|
||||
implode( ', ', $missing_providers ),
|
||||
$in_grace_val,
|
||||
$grace_remaining_val,
|
||||
);
|
||||
}
|
||||
|
||||
ob_start();
|
||||
$handle = fopen( 'php://output', 'w' );
|
||||
|
||||
if ( false !== $handle ) {
|
||||
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();
|
||||
$output = ob_get_clean();
|
||||
|
||||
return false !== $output ? $output : '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -264,7 +285,7 @@ class Two_Factor_Extended_Compliance_Report {
|
|||
* @since 0.1.0
|
||||
*
|
||||
* @param string $to Recipient email address.
|
||||
* @param array $args Optional arguments for report.
|
||||
* @param array<string, mixed> $args Optional arguments for report.
|
||||
*
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ class Two_Factor_Extended_Enforcement {
|
|||
*
|
||||
* @param WP_User|WP_Error|null $user User object or error.
|
||||
*
|
||||
* @return WP_User|WP_Error User object or error if requirements not met.
|
||||
* @return WP_User|WP_Error|null User object or error if requirements not met.
|
||||
*/
|
||||
public function check_user_requirements( $user ) {
|
||||
// Skip if not a user object.
|
||||
|
|
@ -111,7 +111,7 @@ class Two_Factor_Extended_Enforcement {
|
|||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return array Array of required provider class names.
|
||||
* @return array<int, string> Array of required provider class names.
|
||||
*/
|
||||
public function get_required_providers_for_user( int $user_id ): array {
|
||||
$required = array();
|
||||
|
|
@ -119,15 +119,25 @@ class Two_Factor_Extended_Enforcement {
|
|||
// 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 ( ! is_array( $network_settings ) ) {
|
||||
$network_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'] );
|
||||
foreach ( $network_settings['super_admin_requirements'] as $provider ) {
|
||||
if ( is_string( $provider ) ) {
|
||||
$required[] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check network-wide settings (Multisite).
|
||||
if ( is_multisite() ) {
|
||||
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
||||
if ( ! is_array( $network_settings ) ) {
|
||||
$network_settings = array();
|
||||
}
|
||||
|
||||
if ( ! empty( $network_settings['enforce_network_wide'] ) ) {
|
||||
// Network enforcement is enabled.
|
||||
|
|
@ -136,35 +146,46 @@ class Two_Factor_Extended_Enforcement {
|
|||
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 ] );
|
||||
foreach ( $network_settings['role_requirements'][ $role ] as $provider ) {
|
||||
if ( is_string( $provider ) ) {
|
||||
$required[] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If site override is not allowed, return only network requirements.
|
||||
if ( empty( $network_settings['allow_site_override'] ) ) {
|
||||
return array_unique( $required );
|
||||
return array_values( array_unique( $required ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get site-level requirements.
|
||||
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
|
||||
|
||||
if ( ! empty( $user_roles ) && isset( $settings['role_requirements'] ) ) {
|
||||
if ( ! empty( $user_roles ) && isset( $settings['role_requirements'] ) && is_array( $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 );
|
||||
foreach ( $role_requirements as $provider ) {
|
||||
if ( is_string( $provider ) ) {
|
||||
$required[] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique( $required );
|
||||
return array_values( array_unique( $required ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -173,7 +194,7 @@ class Two_Factor_Extended_Enforcement {
|
|||
* @since 0.1.0
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param array $required_providers Required provider class names.
|
||||
* @param array<int, string> $required_providers Required provider class names.
|
||||
*
|
||||
* @return bool True if user meets requirements, false otherwise.
|
||||
*/
|
||||
|
|
@ -204,7 +225,10 @@ class Two_Factor_Extended_Enforcement {
|
|||
*/
|
||||
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;
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
$grace_days = isset( $settings['grace_period_days'] ) && is_numeric( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 0;
|
||||
|
||||
// No grace period configured.
|
||||
if ( 0 === $grace_days ) {
|
||||
|
|
@ -219,7 +243,7 @@ class Two_Factor_Extended_Enforcement {
|
|||
return true;
|
||||
}
|
||||
|
||||
$days_elapsed = ( time() - (int) $start_date ) / DAY_IN_SECONDS;
|
||||
$days_elapsed = ( time() - ( is_numeric( $start_date ) ? (int) $start_date : 0 ) ) / DAY_IN_SECONDS;
|
||||
|
||||
return $days_elapsed < $grace_days;
|
||||
}
|
||||
|
|
@ -235,14 +259,17 @@ class Two_Factor_Extended_Enforcement {
|
|||
*/
|
||||
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;
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
$grace_days = isset( $settings['grace_period_days'] ) && is_numeric( $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;
|
||||
$days_elapsed = ( time() - ( is_numeric( $start_date ) ? (int) $start_date : 0 ) ) / DAY_IN_SECONDS;
|
||||
$remaining = $grace_days - $days_elapsed;
|
||||
|
||||
return max( 0, (int) ceil( $remaining ) );
|
||||
|
|
@ -341,9 +368,9 @@ class Two_Factor_Extended_Enforcement {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $provider_classes Array of provider class names.
|
||||
* @param array<int, string> $provider_classes Array of provider class names.
|
||||
*
|
||||
* @return array Array of provider labels.
|
||||
* @return array<int, string> Array of provider labels.
|
||||
*/
|
||||
private function get_provider_labels( array $provider_classes ): array {
|
||||
$names = Two_Factor_Extended_Provider_Detector::get_provider_names();
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ class Two_Factor_Extended_Network_Settings {
|
|||
type="number"
|
||||
id="network_grace_period"
|
||||
name="network_settings[grace_period_days]"
|
||||
value="<?php echo esc_attr( $settings['grace_period_days'] ?? 7 ); ?>"
|
||||
value="<?php echo esc_attr( (string) ( isset( $settings['grace_period_days'] ) && is_numeric( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 7 ) ); ?>"
|
||||
min="0"
|
||||
max="365"
|
||||
class="small-text"
|
||||
|
|
@ -204,11 +204,11 @@ class Two_Factor_Extended_Network_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $settings Current network settings.
|
||||
* @param array<string, mixed> $settings Current network settings.
|
||||
*/
|
||||
private function render_super_admin_requirements( array $settings ): void {
|
||||
$providers = Two_Factor_Extended_Provider_Detector::get_provider_names();
|
||||
$super_admin_reqs = $settings['super_admin_requirements'] ?? array();
|
||||
$super_admin_reqs = isset( $settings['super_admin_requirements'] ) && is_array( $settings['super_admin_requirements'] ) ? $settings['super_admin_requirements'] : array();
|
||||
|
||||
if ( empty( $providers ) ) {
|
||||
?>
|
||||
|
|
@ -244,12 +244,12 @@ class Two_Factor_Extended_Network_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $settings Current network settings.
|
||||
* @param array<string, mixed> $settings Current network settings.
|
||||
*/
|
||||
private function render_network_role_requirements( array $settings ): void {
|
||||
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
||||
$providers = Two_Factor_Extended_Provider_Detector::get_provider_names();
|
||||
$network_reqs = $settings['role_requirements'] ?? array();
|
||||
$network_reqs = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
|
||||
|
||||
if ( empty( $providers ) ) {
|
||||
?>
|
||||
|
|
@ -274,7 +274,7 @@ class Two_Factor_Extended_Network_Settings {
|
|||
<td><strong><?php echo esc_html( $role_name ); ?></strong></td>
|
||||
<td>
|
||||
<?php
|
||||
$role_reqs = $network_reqs[ $role_slug ] ?? array();
|
||||
$role_reqs = isset( $network_reqs[ $role_slug ] ) && is_array( $network_reqs[ $role_slug ] ) ? $network_reqs[ $role_slug ] : array();
|
||||
|
||||
foreach ( $providers as $class => $name ) :
|
||||
?>
|
||||
|
|
@ -308,7 +308,7 @@ class Two_Factor_Extended_Network_Settings {
|
|||
}
|
||||
|
||||
// Verify nonce.
|
||||
$nonce = isset( $_POST['two_factor_extended_network_nonce'] )
|
||||
$nonce = isset( $_POST['two_factor_extended_network_nonce'] ) && is_string( $_POST['two_factor_extended_network_nonce'] )
|
||||
? sanitize_text_field( wp_unslash( $_POST['two_factor_extended_network_nonce'] ) )
|
||||
: '';
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ class Two_Factor_Extended_Network_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Network settings.
|
||||
* @return array<string, mixed> Network settings.
|
||||
*/
|
||||
public function get_network_settings(): array {
|
||||
$defaults = array(
|
||||
|
|
@ -357,6 +357,10 @@ class Two_Factor_Extended_Network_Settings {
|
|||
|
||||
$settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
||||
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
|
||||
return wp_parse_args( $settings, $defaults );
|
||||
}
|
||||
|
||||
|
|
@ -365,9 +369,9 @@ class Two_Factor_Extended_Network_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $input Raw input.
|
||||
* @param array<string, mixed> $input Raw input.
|
||||
*
|
||||
* @return array Sanitized settings.
|
||||
* @return array<string, mixed> Sanitized settings.
|
||||
*/
|
||||
private function sanitize_network_settings( array $input ): array {
|
||||
$sanitized = array();
|
||||
|
|
@ -379,14 +383,19 @@ class Two_Factor_Extended_Network_Settings {
|
|||
$sanitized['allow_site_override'] = ! empty( $input['allow_site_override'] );
|
||||
|
||||
// Grace period days.
|
||||
if ( isset( $input['grace_period_days'] ) ) {
|
||||
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
|
||||
$days = (int) $input['grace_period_days'];
|
||||
$sanitized['grace_period_days'] = max( 0, min( 365, $days ) );
|
||||
}
|
||||
|
||||
// Super admin requirements.
|
||||
if ( isset( $input['super_admin_requirements'] ) && is_array( $input['super_admin_requirements'] ) ) {
|
||||
$sanitized['super_admin_requirements'] = array_map( 'sanitize_text_field', $input['super_admin_requirements'] );
|
||||
$sanitized['super_admin_requirements'] = array_map(
|
||||
function ( mixed $item ): string {
|
||||
return sanitize_text_field( is_string( $item ) ? $item : '' );
|
||||
},
|
||||
$input['super_admin_requirements']
|
||||
);
|
||||
}
|
||||
|
||||
// Role requirements.
|
||||
|
|
@ -394,10 +403,15 @@ class Two_Factor_Extended_Network_Settings {
|
|||
$sanitized['role_requirements'] = array();
|
||||
|
||||
foreach ( $input['role_requirements'] as $role => $providers ) {
|
||||
$role_slug = sanitize_key( $role );
|
||||
$role_slug = sanitize_key( is_string( $role ) ? $role : '' );
|
||||
|
||||
if ( Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) && is_array( $providers ) ) {
|
||||
$sanitized['role_requirements'][ $role_slug ] = array_map( 'sanitize_text_field', $providers );
|
||||
$sanitized['role_requirements'][ $role_slug ] = array_map(
|
||||
function ( mixed $item ): string {
|
||||
return sanitize_text_field( is_string( $item ) ? $item : '' );
|
||||
},
|
||||
$providers
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Two_Factor_Extended_Provider_Detector {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Array of provider class names.
|
||||
* @return array<string, object> Array of provider instances keyed by class name.
|
||||
*/
|
||||
public static function get_all_providers(): array {
|
||||
if ( ! class_exists( 'Two_Factor_Core' ) ) {
|
||||
|
|
@ -40,14 +40,14 @@ class Two_Factor_Extended_Provider_Detector {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Array of provider class => display name.
|
||||
* @return array<string, string> 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' ) ) {
|
||||
if ( method_exists( $provider, 'get_label' ) ) {
|
||||
$names[ $class_name ] = $provider->get_label();
|
||||
} else {
|
||||
$names[ $class_name ] = $class_name;
|
||||
|
|
@ -64,7 +64,7 @@ class Two_Factor_Extended_Provider_Detector {
|
|||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return array Array of enabled provider class names.
|
||||
* @return array<string, object> Array of enabled provider instances keyed by class name.
|
||||
*/
|
||||
public static function get_user_enabled_providers( int $user_id ): array {
|
||||
if ( ! class_exists( 'Two_Factor_Core' ) ) {
|
||||
|
|
@ -90,7 +90,13 @@ class Two_Factor_Extended_Provider_Detector {
|
|||
|
||||
$primary = Two_Factor_Core::get_primary_provider_for_user( $user_id );
|
||||
|
||||
return $primary ? get_class( $primary ) : null;
|
||||
if ( ! $primary ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = get_class( $primary );
|
||||
|
||||
return false !== $class ? $class : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $providers Available providers.
|
||||
* @param array<string, object> $providers Available providers.
|
||||
*
|
||||
* @return array Filtered providers.
|
||||
* @return array<string, object> Filtered providers.
|
||||
*/
|
||||
public function filter_providers_by_role( array $providers ): array {
|
||||
// IMPORTANT: Only filter providers on user profile pages, NOT on admin settings pages.
|
||||
|
|
@ -104,13 +104,16 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return array|null Array of visible provider class names, or null if no rules.
|
||||
* @return array<int, string>|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() );
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
|
||||
|
||||
if ( empty( $user_roles ) || ! isset( $settings['provider_visibility'] ) ) {
|
||||
if ( empty( $user_roles ) || ! isset( $settings['provider_visibility'] ) || ! is_array( $settings['provider_visibility'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +125,11 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
$role_visible = $settings['provider_visibility'][ $role ];
|
||||
|
||||
if ( is_array( $role_visible ) ) {
|
||||
$visible = array_merge( $visible, $role_visible );
|
||||
foreach ( $role_visible as $provider ) {
|
||||
if ( is_string( $provider ) ) {
|
||||
$visible[] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +139,7 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
return null;
|
||||
}
|
||||
|
||||
return array_unique( $visible );
|
||||
return array_values( array_unique( $visible ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -166,9 +173,9 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return int|null User ID or null.
|
||||
* @return int User ID.
|
||||
*/
|
||||
private function get_profile_user_id(): ?int {
|
||||
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 );
|
||||
|
|
@ -197,9 +204,13 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
}
|
||||
|
||||
$all_providers = Two_Factor_Extended_Provider_Detector::get_all_providers();
|
||||
$required_providers = two_factor_extended()->get_enforcement()->get_required_providers_for_user( $user->ID );
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
$provider_names = Two_Factor_Extended_Provider_Detector::get_provider_names();
|
||||
|
||||
$required_providers = null !== $enforcement
|
||||
? $enforcement->get_required_providers_for_user( $user->ID )
|
||||
: array();
|
||||
|
||||
// Calculate hidden providers.
|
||||
$hidden_providers = array();
|
||||
|
||||
|
|
@ -242,15 +253,15 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $roles Array of role slugs.
|
||||
* @param array<string> $roles Array of role slugs.
|
||||
*
|
||||
* @return array Array of visible provider class names.
|
||||
* @return array<string> 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'] ) ) {
|
||||
if ( ! is_array( $settings ) || ! isset( $settings['provider_visibility'] ) || ! is_array( $settings['provider_visibility'] ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
|
|
@ -259,12 +270,16 @@ class Two_Factor_Extended_Provider_Filter {
|
|||
$role_visible = $settings['provider_visibility'][ $role ];
|
||||
|
||||
if ( is_array( $role_visible ) ) {
|
||||
$visible = array_merge( $visible, $role_visible );
|
||||
foreach ( $role_visible as $provider ) {
|
||||
if ( is_string( $provider ) ) {
|
||||
$visible[] = $provider;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique( $visible );
|
||||
return array_values( array_unique( $visible ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -185,7 +185,18 @@ class Two_Factor_Extended_REST_API {
|
|||
$role = $request->get_param( 'role' );
|
||||
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
|
||||
|
||||
if ( null === $compliance ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
$stats = $compliance->get_compliance_stats( array( 'role' => is_string( $role ) ? $role : '' ) );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
|
|
@ -217,23 +228,44 @@ class Two_Factor_Extended_REST_API {
|
|||
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' );
|
||||
$role_string = is_string( $role ) ? $role : '';
|
||||
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
if ( $non_compliant_only ) {
|
||||
$users = $compliance->get_non_compliant_users( array( 'role' => $role ) );
|
||||
$users = $compliance->get_non_compliant_users( array( 'role' => $role_string ) );
|
||||
} else {
|
||||
// Get all users with compliance status.
|
||||
$user_args = array( 'fields' => 'all' );
|
||||
|
||||
if ( ! empty( $role ) ) {
|
||||
$user_args['role'] = $role;
|
||||
if ( ! empty( $role_string ) ) {
|
||||
$user_args['role'] = $role_string;
|
||||
}
|
||||
|
||||
$all_users = get_users( $user_args );
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
$users = array();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Enforcement module is not available.', 'two-factor-extended' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $all_users as $user ) {
|
||||
$required = $enforcement->get_required_providers_for_user( $user->ID );
|
||||
|
||||
|
|
@ -272,19 +304,45 @@ class Two_Factor_Extended_REST_API {
|
|||
$user_ids = $request->get_param( 'user_ids' );
|
||||
$reset_grace = $request->get_param( 'reset_grace' );
|
||||
|
||||
if ( ! is_array( $user_ids ) ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Invalid user_ids parameter.', 'two-factor-extended' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
$enforcement = two_factor_extended()->get_enforcement();
|
||||
|
||||
if ( null === $enforcement ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Enforcement module is not available.', 'two-factor-extended' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$errors = array();
|
||||
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! is_numeric( $user_id ) ) {
|
||||
$errors[] = 'Invalid user_id value provided.';
|
||||
continue;
|
||||
}
|
||||
$user_id_int = (int) $user_id;
|
||||
$user = get_userdata( $user_id_int );
|
||||
|
||||
if ( ! $user ) {
|
||||
$errors[] = sprintf( 'User not found: %d', $user_id );
|
||||
$errors[] = sprintf( 'User not found: %d', $user_id_int );
|
||||
continue;
|
||||
}
|
||||
|
||||
$required = $enforcement->get_required_providers_for_user( $user_id );
|
||||
$required = $enforcement->get_required_providers_for_user( $user_id_int );
|
||||
|
||||
if ( empty( $required ) ) {
|
||||
$errors[] = sprintf( 'No 2FA requirements for user: %s', $user->user_login );
|
||||
|
|
@ -292,19 +350,23 @@ class Two_Factor_Extended_REST_API {
|
|||
}
|
||||
|
||||
// Set enforcement start date.
|
||||
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
|
||||
update_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
|
||||
|
||||
if ( $reset_grace ) {
|
||||
delete_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
|
||||
delete_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
|
||||
}
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'api_enforce',
|
||||
sprintf( 'REST API: Enforced 2FA for user %s', $user->user_login ),
|
||||
$user_id,
|
||||
$user_id_int,
|
||||
array( 'required_providers' => $required )
|
||||
);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
|
@ -333,26 +395,45 @@ class Two_Factor_Extended_REST_API {
|
|||
$processed = 0;
|
||||
$errors = array();
|
||||
|
||||
if ( ! is_array( $user_ids ) ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Invalid user_ids parameter.', 'two-factor-extended' ),
|
||||
),
|
||||
400
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
$user = get_userdata( $user_id );
|
||||
if ( ! is_numeric( $user_id ) ) {
|
||||
$errors[] = 'Invalid user_id value provided.';
|
||||
continue;
|
||||
}
|
||||
$user_id_int = (int) $user_id;
|
||||
$user = get_userdata( $user_id_int );
|
||||
|
||||
if ( ! $user ) {
|
||||
$errors[] = sprintf( 'User not found: %d', $user_id );
|
||||
$errors[] = sprintf( 'User not found: %d', $user_id_int );
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reset enforcement start date.
|
||||
update_user_meta( $user_id, Two_Factor_Extended_Enforcement::META_ENFORCEMENT_START, time() );
|
||||
update_user_meta( $user_id_int, 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 );
|
||||
delete_user_meta( $user_id_int, Two_Factor_Extended_Enforcement::META_GRACE_NOTIFIED );
|
||||
|
||||
// Log the action.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'api_reset_grace',
|
||||
sprintf( 'REST API: Reset grace period for user %s', $user->user_login ),
|
||||
$user_id
|
||||
$user_id_int
|
||||
);
|
||||
}
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
|
@ -379,11 +460,22 @@ class Two_Factor_Extended_REST_API {
|
|||
public function get_report( WP_REST_Request $request ): WP_REST_Response {
|
||||
$role = $request->get_param( 'role' );
|
||||
$format = $request->get_param( 'format' );
|
||||
$role_string = is_string( $role ) ? $role : '';
|
||||
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'success' => false,
|
||||
'message' => __( 'Compliance report module is not available.', 'two-factor-extended' ),
|
||||
),
|
||||
500
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'csv' === $format ) {
|
||||
$csv = $compliance->export_to_csv( array( 'role' => $role ) );
|
||||
$csv = $compliance->export_to_csv( array( 'role' => $role_string ) );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
|
|
@ -399,8 +491,8 @@ class Two_Factor_Extended_REST_API {
|
|||
}
|
||||
|
||||
// JSON format.
|
||||
$non_compliant = $compliance->get_non_compliant_users( array( 'role' => $role ) );
|
||||
$stats = $compliance->get_compliance_stats( array( 'role' => $role ) );
|
||||
$non_compliant = $compliance->get_non_compliant_users( array( 'role' => $role_string ) );
|
||||
$stats = $compliance->get_compliance_stats( array( 'role' => $role_string ) );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class Two_Factor_Extended_Role_Manager {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Array of role slugs and names.
|
||||
* @return array<string, string> Array of role slugs and names.
|
||||
*/
|
||||
public static function get_all_roles(): array {
|
||||
if ( ! function_exists( 'get_editable_roles' ) ) {
|
||||
|
|
@ -64,12 +64,12 @@ class Two_Factor_Extended_Role_Manager {
|
|||
*
|
||||
* @param int $user_id User ID.
|
||||
*
|
||||
* @return array Array of role slugs.
|
||||
* @return array<string> Array of role slugs.
|
||||
*/
|
||||
public static function get_user_roles( int $user_id ): array {
|
||||
$user = get_userdata( $user_id );
|
||||
|
||||
if ( ! $user || ! isset( $user->roles ) ) {
|
||||
if ( ! $user ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +130,7 @@ class Two_Factor_Extended_Role_Manager {
|
|||
* @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.
|
||||
* @return array<string> 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() ) {
|
||||
|
|
@ -148,8 +148,9 @@ class Two_Factor_Extended_Role_Manager {
|
|||
}
|
||||
|
||||
// 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();
|
||||
global $wpdb;
|
||||
$roles_key = $wpdb->get_blog_prefix( $blog_id ) . 'capabilities';
|
||||
$roles = isset( $user->{$roles_key} ) && is_array( $user->{$roles_key} ) ? array_keys( $user->{$roles_key} ) : array();
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,21 +28,6 @@ class Two_Factor_Extended_Settings {
|
|||
*/
|
||||
const PAGE_SLUG = 'two-factor-extended';
|
||||
|
||||
/**
|
||||
* Settings sections.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array
|
||||
*/
|
||||
private array $sections = array();
|
||||
|
||||
/**
|
||||
* Settings fields.
|
||||
*
|
||||
* @since 0.1.0
|
||||
* @var array
|
||||
*/
|
||||
private array $fields = array();
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
|
|
@ -59,8 +44,9 @@ class Two_Factor_Extended_Settings {
|
|||
* @since 0.1.0
|
||||
*/
|
||||
private function init_hooks(): void {
|
||||
add_action( 'admin_menu', array( $this, 'register_settings_page' ) );
|
||||
add_action( 'network_admin_menu', array( $this, 'register_network_settings_page' ) );
|
||||
// Priority 20 ensures our page registers after Two Factor (default priority 10).
|
||||
add_action( 'admin_menu', array( $this, 'register_settings_page' ), 20 );
|
||||
add_action( 'network_admin_menu', array( $this, 'register_network_settings_page' ), 20 );
|
||||
add_action( 'admin_init', array( $this, 'register_settings' ) );
|
||||
add_action( 'admin_init', array( $this, 'handle_import_export' ) );
|
||||
add_action( 'admin_init', array( $this, 'handle_reset_settings' ) );
|
||||
|
|
@ -186,13 +172,6 @@ class Two_Factor_Extended_Settings {
|
|||
array( $this, 'render_data_section' ),
|
||||
self::PAGE_SLUG
|
||||
);
|
||||
|
||||
$this->sections = array(
|
||||
'two_factor_extended_general',
|
||||
'two_factor_extended_role_requirements',
|
||||
'two_factor_extended_provider_visibility',
|
||||
'two_factor_extended_data',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -380,31 +359,31 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Field arguments.
|
||||
* @param array<string, mixed> $args Field arguments.
|
||||
*/
|
||||
public function render_number_field( array $args ): void {
|
||||
$option_name = $args['option_name'] ?? '';
|
||||
$id = $args['id'] ?? '';
|
||||
$field_key = $args['field_key'] ?? '';
|
||||
$description = $args['description'] ?? '';
|
||||
$min = $args['min'] ?? 0;
|
||||
$max = $args['max'] ?? 999;
|
||||
$default = $args['default'] ?? 0;
|
||||
$option_name = isset( $args['option_name'] ) && is_string( $args['option_name'] ) ? $args['option_name'] : '';
|
||||
$id = isset( $args['id'] ) && is_string( $args['id'] ) ? $args['id'] : '';
|
||||
$field_key = isset( $args['field_key'] ) && is_string( $args['field_key'] ) ? $args['field_key'] : '';
|
||||
$description = isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '';
|
||||
$min = isset( $args['min'] ) && is_numeric( $args['min'] ) ? (int) $args['min'] : 0;
|
||||
$max = isset( $args['max'] ) && is_numeric( $args['max'] ) ? (int) $args['max'] : 999;
|
||||
$default = isset( $args['default'] ) && is_numeric( $args['default'] ) ? (int) $args['default'] : 0;
|
||||
|
||||
if ( empty( $option_name ) || empty( $id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$settings = get_option( $option_name, array() );
|
||||
$value = $settings[ $field_key ] ?? $default;
|
||||
$value = is_array( $settings ) && isset( $settings[ $field_key ] ) && is_numeric( $settings[ $field_key ] ) ? (int) $settings[ $field_key ] : $default;
|
||||
?>
|
||||
<input
|
||||
type="number"
|
||||
id="<?php echo esc_attr( $id ); ?>"
|
||||
name="<?php echo esc_attr( $option_name . '[' . $field_key . ']' ); ?>"
|
||||
value="<?php echo esc_attr( $value ); ?>"
|
||||
min="<?php echo esc_attr( $min ); ?>"
|
||||
max="<?php echo esc_attr( $max ); ?>"
|
||||
value="<?php echo esc_attr( (string) $value ); ?>"
|
||||
min="<?php echo esc_attr( (string) $min ); ?>"
|
||||
max="<?php echo esc_attr( (string) $max ); ?>"
|
||||
step="1"
|
||||
required
|
||||
aria-required="true"
|
||||
|
|
@ -423,17 +402,18 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Field arguments.
|
||||
* @param array<string, mixed> $args Field arguments.
|
||||
*/
|
||||
public function render_role_requirements_field( array $args ): void {
|
||||
// Clear any object cache to ensure fresh data.
|
||||
wp_cache_delete( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, 'options' );
|
||||
wp_cache_flush();
|
||||
|
||||
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
$raw_settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
$settings = is_array( $raw_settings ) ? $raw_settings : array();
|
||||
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
||||
|
||||
// Force fresh provider detection directly from Two Factor Core
|
||||
// Force fresh provider detection directly from Two Factor Core.
|
||||
$providers = array();
|
||||
if ( class_exists( 'Two_Factor_Core' ) ) {
|
||||
$all_providers = Two_Factor_Core::get_providers();
|
||||
|
|
@ -456,7 +436,7 @@ class Two_Factor_Extended_Settings {
|
|||
return;
|
||||
}
|
||||
|
||||
$requirements = $settings['role_requirements'] ?? array();
|
||||
$requirements = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
|
||||
?>
|
||||
<table class="widefat fixed striped">
|
||||
<thead>
|
||||
|
|
@ -473,7 +453,8 @@ class Two_Factor_Extended_Settings {
|
|||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$role_requirements = $requirements[ $role_slug ] ?? array();
|
||||
$role_req_raw = $requirements[ $role_slug ] ?? array();
|
||||
$role_requirements = is_array( $role_req_raw ) ? $role_req_raw : array();
|
||||
|
||||
foreach ( $providers as $provider_class => $provider_name ) :
|
||||
$checked = in_array( $provider_class, $role_requirements, true );
|
||||
|
|
@ -504,17 +485,18 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Field arguments.
|
||||
* @param array<string, mixed> $args Field arguments.
|
||||
*/
|
||||
public function render_provider_visibility_field( array $args ): void {
|
||||
// Clear any object cache to ensure fresh data.
|
||||
wp_cache_delete( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, 'options' );
|
||||
wp_cache_flush();
|
||||
|
||||
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
$raw_settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
$settings = is_array( $raw_settings ) ? $raw_settings : array();
|
||||
$roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
||||
|
||||
// Force fresh provider detection directly from Two Factor Core
|
||||
// Force fresh provider detection directly from Two Factor Core.
|
||||
$providers = array();
|
||||
if ( class_exists( 'Two_Factor_Core' ) ) {
|
||||
$all_providers = Two_Factor_Core::get_providers();
|
||||
|
|
@ -537,8 +519,8 @@ class Two_Factor_Extended_Settings {
|
|||
return;
|
||||
}
|
||||
|
||||
$visibility = $settings['provider_visibility'] ?? array();
|
||||
$requirements = $settings['role_requirements'] ?? array();
|
||||
$visibility = isset( $settings['provider_visibility'] ) && is_array( $settings['provider_visibility'] ) ? $settings['provider_visibility'] : array();
|
||||
$requirements = isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ? $settings['role_requirements'] : array();
|
||||
?>
|
||||
<table class="widefat fixed striped">
|
||||
<thead>
|
||||
|
|
@ -555,8 +537,10 @@ class Two_Factor_Extended_Settings {
|
|||
</td>
|
||||
<td>
|
||||
<?php
|
||||
$role_visibility = $visibility[ $role_slug ] ?? array();
|
||||
$role_required = $requirements[ $role_slug ] ?? array();
|
||||
$role_vis_raw = $visibility[ $role_slug ] ?? array();
|
||||
$role_req_raw = $requirements[ $role_slug ] ?? array();
|
||||
$role_visibility = is_array( $role_vis_raw ) ? $role_vis_raw : array();
|
||||
$role_required = is_array( $role_req_raw ) ? $role_req_raw : array();
|
||||
|
||||
foreach ( $providers as $provider_class => $provider_name ) :
|
||||
$checked = in_array( $provider_class, $role_visibility, true );
|
||||
|
|
@ -606,13 +590,13 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $args Field arguments.
|
||||
* @param array<string, mixed> $args Field arguments.
|
||||
*/
|
||||
public function render_checkbox_field( array $args ): void {
|
||||
$option_name = $args['option_name'] ?? '';
|
||||
$id = $args['id'] ?? '';
|
||||
$label = $args['label'] ?? '';
|
||||
$description = $args['description'] ?? '';
|
||||
$option_name = isset( $args['option_name'] ) && is_string( $args['option_name'] ) ? $args['option_name'] : '';
|
||||
$id = isset( $args['id'] ) && is_string( $args['id'] ) ? $args['id'] : '';
|
||||
$label = isset( $args['label'] ) && is_string( $args['label'] ) ? $args['label'] : '';
|
||||
$description = isset( $args['description'] ) && is_string( $args['description'] ) ? $args['description'] : '';
|
||||
|
||||
if ( empty( $option_name ) || empty( $id ) ) {
|
||||
return;
|
||||
|
|
@ -650,7 +634,8 @@ class Two_Factor_Extended_Settings {
|
|||
return;
|
||||
}
|
||||
|
||||
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
||||
$network_settings_raw = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
||||
$network_settings = is_array( $network_settings_raw ) ? $network_settings_raw : array();
|
||||
|
||||
if ( empty( $network_settings['enforce_network_wide'] ) ) {
|
||||
return;
|
||||
|
|
@ -688,7 +673,7 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
// Inline CSS for tabs.
|
||||
$css = "
|
||||
$css = '
|
||||
.tab-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
|
@ -696,7 +681,7 @@ class Two_Factor_Extended_Settings {
|
|||
max-width: none;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
";
|
||||
';
|
||||
wp_add_inline_style( 'wp-admin', $css );
|
||||
|
||||
// Inline script for import validation.
|
||||
|
|
@ -775,6 +760,8 @@ class Two_Factor_Extended_Settings {
|
|||
* Export settings to JSON file.
|
||||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @throws Exception If JSON encoding fails.
|
||||
*/
|
||||
private function export_settings(): void {
|
||||
try {
|
||||
|
|
@ -794,11 +781,14 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
// Log the export.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'settings_exported',
|
||||
'Plugin settings exported',
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
// Set headers for download.
|
||||
header( 'Content-Type: application/json; charset=utf-8' );
|
||||
|
|
@ -810,12 +800,15 @@ class Two_Factor_Extended_Settings {
|
|||
exit;
|
||||
} catch ( Exception $e ) {
|
||||
// Log error without exposing details to user.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'settings_export_failed',
|
||||
'Failed to export settings',
|
||||
0,
|
||||
array( 'error' => 'Export failed' )
|
||||
);
|
||||
}
|
||||
|
||||
wp_die(
|
||||
esc_html__( 'Unable to export settings. Please try again later.', 'two-factor-extended' ),
|
||||
|
|
@ -848,7 +841,16 @@ class Two_Factor_Extended_Settings {
|
|||
// phpcs:disable WordPress.Security.ValidatedSanitizedInput
|
||||
|
||||
// Check file upload.
|
||||
if ( ! isset( $_FILES['two_factor_extended_import_file'] ) || UPLOAD_ERR_OK !== $_FILES['two_factor_extended_import_file']['error'] ) {
|
||||
$upload_file = isset( $_FILES['two_factor_extended_import_file'] ) && is_array( $_FILES['two_factor_extended_import_file'] )
|
||||
? $_FILES['two_factor_extended_import_file']
|
||||
: array();
|
||||
|
||||
$upload_error = isset( $upload_file['error'] ) && is_int( $upload_file['error'] ) ? $upload_file['error'] : UPLOAD_ERR_NO_FILE;
|
||||
$upload_size = isset( $upload_file['size'] ) && is_int( $upload_file['size'] ) ? $upload_file['size'] : 0;
|
||||
$upload_name = isset( $upload_file['name'] ) && is_string( $upload_file['name'] ) ? $upload_file['name'] : '';
|
||||
$upload_tmp_name = isset( $upload_file['tmp_name'] ) && is_string( $upload_file['tmp_name'] ) ? $upload_file['tmp_name'] : '';
|
||||
|
||||
if ( empty( $upload_file ) || UPLOAD_ERR_OK !== $upload_error ) {
|
||||
add_settings_error(
|
||||
'two_factor_extended_import',
|
||||
'import_error',
|
||||
|
|
@ -860,7 +862,7 @@ class Two_Factor_Extended_Settings {
|
|||
|
||||
// Check file size (1MB limit).
|
||||
$max_size = 1024 * 1024; // 1MB.
|
||||
if ( $_FILES['two_factor_extended_import_file']['size'] > $max_size ) {
|
||||
if ( $upload_size > $max_size ) {
|
||||
add_settings_error(
|
||||
'two_factor_extended_import',
|
||||
'import_error',
|
||||
|
|
@ -871,7 +873,7 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
// Check file extension.
|
||||
$file_name = sanitize_file_name( $_FILES['two_factor_extended_import_file']['name'] );
|
||||
$file_name = sanitize_file_name( $upload_name );
|
||||
if ( ! str_ends_with( strtolower( $file_name ), '.json' ) ) {
|
||||
add_settings_error(
|
||||
'two_factor_extended_import',
|
||||
|
|
@ -883,7 +885,7 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
// Read file contents.
|
||||
$file_content = file_get_contents( $_FILES['two_factor_extended_import_file']['tmp_name'] );
|
||||
$file_content = file_get_contents( $upload_tmp_name );
|
||||
|
||||
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
||||
// phpcs:enable WordPress.Security.ValidatedSanitizedInput
|
||||
|
|
@ -922,12 +924,21 @@ class Two_Factor_Extended_Settings {
|
|||
return;
|
||||
}
|
||||
|
||||
// Sanitize and update settings.
|
||||
$sanitized_settings = $this->sanitize_settings( $import_data['settings'] );
|
||||
// Sanitize and update settings. Ensure keys are strings (from JSON decode).
|
||||
$settings_to_import = array();
|
||||
foreach ( $import_data['settings'] as $key => $value ) {
|
||||
if ( is_string( $key ) ) {
|
||||
$settings_to_import[ $key ] = $value;
|
||||
}
|
||||
}
|
||||
$sanitized_settings = $this->sanitize_settings( $settings_to_import );
|
||||
update_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, $sanitized_settings );
|
||||
|
||||
// Log the import.
|
||||
two_factor_extended()->get_audit_log()->log_event(
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'settings_imported',
|
||||
'Plugin settings imported',
|
||||
0,
|
||||
|
|
@ -935,6 +946,7 @@ class Two_Factor_Extended_Settings {
|
|||
'imported_version' => $import_data['version'] ?? 'unknown',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
add_settings_error(
|
||||
'two_factor_extended_import',
|
||||
|
|
@ -957,7 +969,8 @@ class Two_Factor_Extended_Settings {
|
|||
|
||||
// Get current tab.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Tab parameter for UI navigation
|
||||
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'settings';
|
||||
$tab_param = isset( $_GET['tab'] ) && is_string( $_GET['tab'] ) ? $_GET['tab'] : '';
|
||||
$current_tab = $tab_param ? sanitize_key( $tab_param ) : 'settings';
|
||||
|
||||
// Define tabs.
|
||||
$tabs = array(
|
||||
|
|
@ -1012,7 +1025,8 @@ class Two_Factor_Extended_Settings {
|
|||
// Check if site settings are disabled by network.
|
||||
$settings_disabled = $this->are_site_settings_disabled();
|
||||
|
||||
if ( $settings_disabled ) : ?>
|
||||
if ( $settings_disabled ) :
|
||||
?>
|
||||
<div class="notice notice-warning">
|
||||
<p>
|
||||
<?php esc_html_e( 'Site-level settings are currently disabled by network administrator. Please contact your network administrator to make changes.', 'two-factor-extended' ); ?>
|
||||
|
|
@ -1041,6 +1055,11 @@ class Two_Factor_Extended_Settings {
|
|||
private function render_audit_log_tab(): void {
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null === $audit_log ) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__( 'Audit log module is not available.', 'two-factor-extended' ) . '</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle export.
|
||||
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
||||
check_admin_referer( 'two_factor_extended_export_audit' );
|
||||
|
|
@ -1081,7 +1100,21 @@ class Two_Factor_Extended_Settings {
|
|||
</div>
|
||||
|
||||
<p>
|
||||
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'tab' => 'audit-log', 'action' => 'export' ) ), 'two_factor_extended_export_audit' ) ); ?>" class="button">
|
||||
<a href="
|
||||
<?php
|
||||
echo esc_url(
|
||||
wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'tab' => 'audit-log',
|
||||
'action' => 'export',
|
||||
)
|
||||
),
|
||||
'two_factor_extended_export_audit'
|
||||
)
|
||||
);
|
||||
?>
|
||||
" class="button">
|
||||
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -1099,21 +1132,28 @@ class Two_Factor_Extended_Settings {
|
|||
<tbody>
|
||||
<?php if ( ! empty( $logs ) ) : ?>
|
||||
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
||||
<?php
|
||||
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
|
||||
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
|
||||
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
|
||||
$log_actor_id = isset( $log['actor_id'] ) && is_int( $log['actor_id'] ) ? $log['actor_id'] : 0;
|
||||
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '-';
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log['timestamp'] ) ); ?></td>
|
||||
<td><?php echo esc_html( $log['action'] ); ?></td>
|
||||
<td><?php echo esc_html( $log['description'] ); ?></td>
|
||||
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
|
||||
<td><?php echo esc_html( $log_action ); ?></td>
|
||||
<td><?php echo esc_html( $log_desc ); ?></td>
|
||||
<td>
|
||||
<?php
|
||||
if ( $log['actor_id'] ) {
|
||||
$user = get_userdata( $log['actor_id'] );
|
||||
if ( $log_actor_id ) {
|
||||
$user = get_userdata( $log_actor_id );
|
||||
echo $user ? esc_html( $user->display_name ) : esc_html__( 'Unknown', 'two-factor-extended' );
|
||||
} else {
|
||||
esc_html_e( 'System', 'two-factor-extended' );
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<td><?php echo esc_html( $log['ip_address'] ?? '-' ); ?></td>
|
||||
<td><?php echo esc_html( $log_ip ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
|
@ -1143,6 +1183,11 @@ class Two_Factor_Extended_Settings {
|
|||
private function render_compliance_tab(): void {
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
echo '<div class="notice notice-error"><p>' . esc_html__( 'Compliance report module is not available.', 'two-factor-extended' ) . '</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle export.
|
||||
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
||||
check_admin_referer( 'two_factor_extended_export_compliance' );
|
||||
|
|
@ -1164,23 +1209,23 @@ class Two_Factor_Extended_Settings {
|
|||
<table class="widefat fixed">
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Total Users', 'two-factor-extended' ); ?></th>
|
||||
<td><?php echo esc_html( $stats['total_users'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $stats['total_users'] ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: green;"><strong><?php echo esc_html( $stats['compliant_users'] ); ?></strong></td>
|
||||
<td style="color: green;"><strong><?php echo esc_html( (string) $stats['compliant_users'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: red;"><strong><?php echo esc_html( $stats['non_compliant'] ); ?></strong></td>
|
||||
<td style="color: red;"><strong><?php echo esc_html( (string) $stats['non_compliant'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'In Grace Period', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: orange;"><strong><?php echo esc_html( $stats['grace_period'] ); ?></strong></td>
|
||||
<td style="color: orange;"><strong><?php echo esc_html( (string) $stats['grace_period'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'No Requirements', 'two-factor-extended' ); ?></th>
|
||||
<td><?php echo esc_html( $stats['no_requirements'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $stats['no_requirements'] ); ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -1201,9 +1246,9 @@ class Two_Factor_Extended_Settings {
|
|||
<?php foreach ( $stats['by_role'] as $role => $role_stats ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( Two_Factor_Extended_Role_Manager::get_role_display_name( $role ) ); ?></strong></td>
|
||||
<td><?php echo esc_html( $role_stats['total'] ); ?></td>
|
||||
<td><?php echo esc_html( $role_stats['compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( $role_stats['non_compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['total'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['non_compliant'] ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -1214,7 +1259,21 @@ class Two_Factor_Extended_Settings {
|
|||
<h2><?php esc_html_e( 'Non-Compliant Users', 'two-factor-extended' ); ?></h2>
|
||||
|
||||
<p>
|
||||
<a href="<?php echo esc_url( wp_nonce_url( add_query_arg( array( 'tab' => 'compliance', 'action' => 'export' ) ), 'two_factor_extended_export_compliance' ) ); ?>" class="button">
|
||||
<a href="
|
||||
<?php
|
||||
echo esc_url(
|
||||
wp_nonce_url(
|
||||
add_query_arg(
|
||||
array(
|
||||
'tab' => 'compliance',
|
||||
'action' => 'export',
|
||||
)
|
||||
),
|
||||
'two_factor_extended_export_compliance'
|
||||
)
|
||||
);
|
||||
?>
|
||||
" class="button">
|
||||
<?php esc_html_e( 'Export to CSV', 'two-factor-extended' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
|
|
@ -1233,21 +1292,38 @@ class Two_Factor_Extended_Settings {
|
|||
<?php if ( ! empty( $non_compliant ) ) : ?>
|
||||
<?php foreach ( $non_compliant as $user_data ) : ?>
|
||||
<?php
|
||||
$user_id_val = isset( $user_data['user_id'] ) && is_int( $user_data['user_id'] ) ? $user_data['user_id'] : 0;
|
||||
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
|
||||
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
|
||||
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
||||
$missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
||||
$in_grace = ! empty( $user_data['in_grace_period'] );
|
||||
$grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0;
|
||||
// Get user object for display name.
|
||||
$user = get_userdata( $user_data['user_id'] );
|
||||
$user = $user_id_val ? get_userdata( $user_id_val ) : null;
|
||||
$roles_display = array_map(
|
||||
function ( $role_slug ) {
|
||||
if ( ! is_string( $role_slug ) ) {
|
||||
return '';
|
||||
}
|
||||
return Two_Factor_Extended_Role_Manager::get_role_display_name( $role_slug );
|
||||
},
|
||||
$roles_raw
|
||||
);
|
||||
$missing_display = array_filter( $missing_raw, 'is_string' );
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo $user ? esc_html( $user->display_name ) : esc_html( $user_data['user_login'] ); ?></td>
|
||||
<td><?php echo esc_html( $user_data['user_email'] ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', array_map( array( 'Two_Factor_Extended_Role_Manager', 'get_role_display_name' ), $user_data['roles'] ) ) ); ?></td>
|
||||
<td><?php echo $user ? esc_html( $user->display_name ) : esc_html( $user_login_val ); ?></td>
|
||||
<td><?php echo esc_html( $user_email_val ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $roles_display ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( $user_data['in_grace_period'] ) : ?>
|
||||
<?php if ( $in_grace ) : ?>
|
||||
<span style="color: orange;">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of days remaining in grace period */
|
||||
esc_html__( 'Grace period (%d days left)', 'two-factor-extended' ),
|
||||
(int) $user_data["grace_remaining"]
|
||||
absint( $grace_remaining )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
|
|
@ -1255,7 +1331,7 @@ class Two_Factor_Extended_Settings {
|
|||
<span style="color: red;"><?php esc_html_e( 'Non-compliant', 'two-factor-extended' ); ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?php echo esc_html( implode( ', ', $user_data['missing_providers'] ) ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $missing_display ) ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
|
@ -1282,6 +1358,10 @@ class Two_Factor_Extended_Settings {
|
|||
|
||||
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
|
||||
|
||||
if ( ! is_array( $network_settings ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ! empty( $network_settings['enforce_network_wide'] )
|
||||
&& empty( $network_settings['allow_site_override'] );
|
||||
}
|
||||
|
|
@ -1298,6 +1378,10 @@ class Two_Factor_Extended_Settings {
|
|||
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null === $audit_log ) {
|
||||
wp_die( esc_html__( 'Audit log module is not available.', 'two-factor-extended' ) );
|
||||
}
|
||||
|
||||
// Handle export.
|
||||
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
||||
check_admin_referer( 'two_factor_extended_export_audit' );
|
||||
|
|
@ -1364,14 +1448,19 @@ class Two_Factor_Extended_Settings {
|
|||
<?php else : ?>
|
||||
<?php foreach ( array_slice( $logs, 0, 50 ) as $log ) : ?>
|
||||
<?php
|
||||
$user = $log['user_id'] ? get_userdata( $log['user_id'] ) : null;
|
||||
$log_user_id = isset( $log['user_id'] ) && is_int( $log['user_id'] ) ? $log['user_id'] : 0;
|
||||
$log_ts = isset( $log['timestamp'] ) && is_int( $log['timestamp'] ) ? $log['timestamp'] : null;
|
||||
$log_action = isset( $log['action'] ) && is_string( $log['action'] ) ? $log['action'] : '';
|
||||
$log_desc = isset( $log['description'] ) && is_string( $log['description'] ) ? $log['description'] : '';
|
||||
$log_ip = isset( $log['ip_address'] ) && is_string( $log['ip_address'] ) ? $log['ip_address'] : '';
|
||||
$user = $log_user_id ? get_userdata( $log_user_id ) : null;
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log['timestamp'] ) ); ?></td>
|
||||
<td><?php echo esc_html( $log['action'] ); ?></td>
|
||||
<td><?php echo esc_html( $log['description'] ); ?></td>
|
||||
<td><?php echo esc_html( gmdate( 'Y-m-d H:i:s', $log_ts ) ); ?></td>
|
||||
<td><?php echo esc_html( $log_action ); ?></td>
|
||||
<td><?php echo esc_html( $log_desc ); ?></td>
|
||||
<td><?php echo $user ? esc_html( $user->user_login ) : '-'; ?></td>
|
||||
<td><?php echo esc_html( $log['ip_address'] ); ?></td>
|
||||
<td><?php echo esc_html( $log_ip ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
|
|
@ -1399,6 +1488,10 @@ class Two_Factor_Extended_Settings {
|
|||
|
||||
$compliance = two_factor_extended()->get_compliance_report();
|
||||
|
||||
if ( null === $compliance ) {
|
||||
wp_die( esc_html__( 'Compliance report module is not available.', 'two-factor-extended' ) );
|
||||
}
|
||||
|
||||
// Handle export.
|
||||
if ( isset( $_GET['action'] ) && 'export' === $_GET['action'] ) {
|
||||
check_admin_referer( 'two_factor_extended_export_compliance' );
|
||||
|
|
@ -1415,7 +1508,7 @@ class Two_Factor_Extended_Settings {
|
|||
if ( isset( $_POST['action'] ) && 'email' === $_POST['action'] ) {
|
||||
check_admin_referer( 'two_factor_extended_email_compliance' );
|
||||
|
||||
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
$email = isset( $_POST['email'] ) && is_string( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
|
||||
|
||||
if ( ! empty( $email ) ) {
|
||||
$sent = $compliance->email_report( $email );
|
||||
|
|
@ -1440,23 +1533,23 @@ class Two_Factor_Extended_Settings {
|
|||
<table class="widefat fixed">
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Total Users', 'two-factor-extended' ); ?></th>
|
||||
<td><?php echo esc_html( $stats['total_users'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $stats['total_users'] ); ?></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Compliant', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: green;"><strong><?php echo esc_html( $stats['compliant_users'] ); ?></strong></td>
|
||||
<td style="color: green;"><strong><?php echo esc_html( (string) $stats['compliant_users'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'Non-Compliant', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: red;"><strong><?php echo esc_html( $stats['non_compliant'] ); ?></strong></td>
|
||||
<td style="color: red;"><strong><?php echo esc_html( (string) $stats['non_compliant'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'In Grace Period', 'two-factor-extended' ); ?></th>
|
||||
<td style="color: orange;"><strong><?php echo esc_html( $stats['grace_period'] ); ?></strong></td>
|
||||
<td style="color: orange;"><strong><?php echo esc_html( (string) $stats['grace_period'] ); ?></strong></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><?php esc_html_e( 'No Requirements', 'two-factor-extended' ); ?></th>
|
||||
<td><?php echo esc_html( $stats['no_requirements'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $stats['no_requirements'] ); ?></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
|
@ -1477,9 +1570,9 @@ class Two_Factor_Extended_Settings {
|
|||
<?php foreach ( $stats['by_role'] as $role => $role_stats ) : ?>
|
||||
<tr>
|
||||
<td><strong><?php echo esc_html( Two_Factor_Extended_Role_Manager::get_role_display_name( $role ) ); ?></strong></td>
|
||||
<td><?php echo esc_html( $role_stats['total'] ); ?></td>
|
||||
<td><?php echo esc_html( $role_stats['compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( $role_stats['non_compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['total'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['compliant'] ); ?></td>
|
||||
<td><?php echo esc_html( (string) $role_stats['non_compliant'] ); ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -1512,19 +1605,29 @@ class Two_Factor_Extended_Settings {
|
|||
</tr>
|
||||
<?php else : ?>
|
||||
<?php foreach ( $non_compliant as $user_data ) : ?>
|
||||
<?php
|
||||
$user_login_val = isset( $user_data['user_login'] ) && is_string( $user_data['user_login'] ) ? $user_data['user_login'] : '';
|
||||
$user_email_val = isset( $user_data['user_email'] ) && is_string( $user_data['user_email'] ) ? $user_data['user_email'] : '';
|
||||
$roles_raw = isset( $user_data['roles'] ) && is_array( $user_data['roles'] ) ? $user_data['roles'] : array();
|
||||
$missing_raw = isset( $user_data['missing_providers'] ) && is_array( $user_data['missing_providers'] ) ? $user_data['missing_providers'] : array();
|
||||
$in_grace = ! empty( $user_data['in_grace_period'] );
|
||||
$grace_remaining = isset( $user_data['grace_remaining'] ) && is_numeric( $user_data['grace_remaining'] ) ? (int) $user_data['grace_remaining'] : 0;
|
||||
$roles_display = array_filter( $roles_raw, 'is_string' );
|
||||
$missing_display = array_filter( $missing_raw, 'is_string' );
|
||||
?>
|
||||
<tr>
|
||||
<td><?php echo esc_html( $user_data['user_login'] ); ?></td>
|
||||
<td><?php echo esc_html( $user_data['user_email'] ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $user_data['roles'] ) ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $user_data['missing_providers'] ) ); ?></td>
|
||||
<td><?php echo esc_html( $user_login_val ); ?></td>
|
||||
<td><?php echo esc_html( $user_email_val ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $roles_display ) ); ?></td>
|
||||
<td><?php echo esc_html( implode( ', ', $missing_display ) ); ?></td>
|
||||
<td>
|
||||
<?php if ( $user_data['in_grace_period'] ) : ?>
|
||||
<?php if ( $in_grace ) : ?>
|
||||
<span style="color: orange;">
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %d: Number of days */
|
||||
esc_html__( 'Grace: %d days', 'two-factor-extended' ),
|
||||
(int) $user_data["grace_remaining"]
|
||||
absint( $grace_remaining )
|
||||
);
|
||||
?>
|
||||
</span>
|
||||
|
|
@ -1555,7 +1658,7 @@ class Two_Factor_Extended_Settings {
|
|||
// Handle form submission.
|
||||
if ( isset( $_POST['submit'] ) ) {
|
||||
// Verify nonce.
|
||||
$nonce = isset( $_POST['two_factor_extended_network_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['two_factor_extended_network_nonce'] ) ) : '';
|
||||
$nonce = isset( $_POST['two_factor_extended_network_nonce'] ) && is_string( $_POST['two_factor_extended_network_nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['two_factor_extended_network_nonce'] ) ) : '';
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'two_factor_extended_network_settings' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'two-factor-extended' ) );
|
||||
|
|
@ -1616,7 +1719,7 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Default settings.
|
||||
* @return array<string, mixed> Default settings.
|
||||
*/
|
||||
private function get_default_settings(): array {
|
||||
$all_roles = Two_Factor_Extended_Role_Manager::get_all_roles();
|
||||
|
|
@ -1641,11 +1744,15 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return array Plugin settings.
|
||||
* @return array<string, mixed> Plugin settings.
|
||||
*/
|
||||
public function get_settings(): array {
|
||||
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
|
||||
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
|
||||
return wp_parse_args( $settings, $this->get_default_settings() );
|
||||
}
|
||||
|
||||
|
|
@ -1654,7 +1761,7 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $settings New settings values.
|
||||
* @param array<string, mixed> $settings New settings values.
|
||||
*
|
||||
* @return bool True if settings were updated successfully.
|
||||
*/
|
||||
|
|
@ -1669,20 +1776,20 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $input Raw settings input.
|
||||
* @param array<string, mixed> $input Raw settings input.
|
||||
*
|
||||
* @return array Sanitized settings.
|
||||
* @return array<string, mixed> Sanitized settings.
|
||||
*/
|
||||
public function sanitize_settings( array $input ): array {
|
||||
$sanitized = array();
|
||||
|
||||
// Sanitize enabled setting.
|
||||
if ( isset( $input['enabled'] ) ) {
|
||||
$sanitized['enabled'] = rest_sanitize_boolean( $input['enabled'] );
|
||||
$sanitized['enabled'] = (bool) $input['enabled'];
|
||||
}
|
||||
|
||||
// Sanitize grace period days.
|
||||
if ( isset( $input['grace_period_days'] ) ) {
|
||||
if ( isset( $input['grace_period_days'] ) && is_numeric( $input['grace_period_days'] ) ) {
|
||||
$grace_days = (int) $input['grace_period_days'];
|
||||
$sanitized['grace_period_days'] = max( 0, min( 365, $grace_days ) );
|
||||
}
|
||||
|
|
@ -1701,6 +1808,9 @@ class Two_Factor_Extended_Settings {
|
|||
// Now merge any submitted role requirements data.
|
||||
if ( isset( $input['role_requirements'] ) && is_array( $input['role_requirements'] ) ) {
|
||||
foreach ( $input['role_requirements'] as $role => $providers ) {
|
||||
if ( ! is_string( $role ) ) {
|
||||
continue;
|
||||
}
|
||||
$role_slug = sanitize_key( $role );
|
||||
|
||||
if ( ! Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) ) {
|
||||
|
|
@ -1708,7 +1818,12 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
if ( is_array( $providers ) ) {
|
||||
$sanitized['role_requirements'][ $role_slug ] = array_map( 'sanitize_text_field', $providers );
|
||||
$sanitized['role_requirements'][ $role_slug ] = array_map(
|
||||
function ( $item ) {
|
||||
return sanitize_text_field( is_scalar( $item ) ? (string) $item : '' );
|
||||
},
|
||||
$providers
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1724,6 +1839,9 @@ class Two_Factor_Extended_Settings {
|
|||
// Now merge any submitted provider visibility data.
|
||||
if ( isset( $input['provider_visibility'] ) && is_array( $input['provider_visibility'] ) ) {
|
||||
foreach ( $input['provider_visibility'] as $role => $providers ) {
|
||||
if ( ! is_string( $role ) ) {
|
||||
continue;
|
||||
}
|
||||
$role_slug = sanitize_key( $role );
|
||||
|
||||
if ( ! Two_Factor_Extended_Role_Manager::role_exists( $role_slug ) ) {
|
||||
|
|
@ -1733,7 +1851,12 @@ class Two_Factor_Extended_Settings {
|
|||
if ( is_array( $providers ) ) {
|
||||
// Remove duplicates and sanitize.
|
||||
$sanitized['provider_visibility'][ $role_slug ] = array_unique(
|
||||
array_map( 'sanitize_text_field', $providers )
|
||||
array_map(
|
||||
function ( $item ) {
|
||||
return sanitize_text_field( is_scalar( $item ) ? (string) $item : '' );
|
||||
},
|
||||
$providers
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1750,13 +1873,14 @@ class Two_Factor_Extended_Settings {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @param array $input Settings input to validate.
|
||||
* @param array<string, mixed> $input Settings input to validate.
|
||||
*
|
||||
* @return bool True if valid, false otherwise.
|
||||
*/
|
||||
public function validate_settings( array $input ): bool {
|
||||
// Basic validation - can be extended.
|
||||
return is_array( $input );
|
||||
// Since parameter is typed as array, it's always valid.
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1771,7 +1895,7 @@ class Two_Factor_Extended_Settings {
|
|||
}
|
||||
|
||||
// Verify nonce.
|
||||
if ( ! isset( $_POST['two_factor_extended_reset_nonce'] ) ||
|
||||
if ( ! isset( $_POST['two_factor_extended_reset_nonce'] ) || ! is_string( $_POST['two_factor_extended_reset_nonce'] ) ||
|
||||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['two_factor_extended_reset_nonce'] ) ), 'two_factor_extended_reset' ) ) {
|
||||
wp_die( esc_html__( 'Security check failed.', 'two-factor-extended' ) );
|
||||
}
|
||||
|
|
@ -1791,15 +1915,18 @@ class Two_Factor_Extended_Settings {
|
|||
// Clear user grace period data.
|
||||
$users = get_users( array( 'fields' => 'ID' ) );
|
||||
foreach ( $users as $user_id ) {
|
||||
delete_user_meta( $user_id, 'two_factor_extended_enforcement_date' );
|
||||
delete_user_meta( (int) $user_id, 'two_factor_extended_enforcement_date' );
|
||||
}
|
||||
|
||||
// Log the reset action.
|
||||
$audit_log = two_factor_extended()->get_audit_log();
|
||||
|
||||
if ( null !== $audit_log ) {
|
||||
$audit_log->log_event(
|
||||
'settings_reset',
|
||||
'Plugin settings reset to default values'
|
||||
);
|
||||
}
|
||||
|
||||
// Redirect with success message.
|
||||
add_settings_error(
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Two_Factor_Extended {
|
|||
* @since 0.1.0
|
||||
* @var string
|
||||
*/
|
||||
private string $version = '1.0.0';
|
||||
private string $version = '1.0.1';
|
||||
|
||||
/**
|
||||
* Plugin directory path.
|
||||
|
|
@ -147,9 +147,9 @@ class Two_Factor_Extended {
|
|||
* @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->plugin_path = plugin_dir_path( __DIR__ );
|
||||
$this->plugin_url = plugin_dir_url( __DIR__ );
|
||||
$this->plugin_basename = plugin_basename( dirname( __DIR__ ) . '/two-factor-extended.php' );
|
||||
|
||||
$this->init();
|
||||
}
|
||||
|
|
@ -253,9 +253,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Settings Settings instance.
|
||||
* @return Two_Factor_Extended_Settings|null Settings instance or null if not initialized.
|
||||
*/
|
||||
public function get_settings(): Two_Factor_Extended_Settings {
|
||||
public function get_settings(): ?Two_Factor_Extended_Settings {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
|
|
@ -264,9 +264,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Enforcement Enforcement instance.
|
||||
* @return Two_Factor_Extended_Enforcement|null Enforcement instance or null if not initialized.
|
||||
*/
|
||||
public function get_enforcement(): Two_Factor_Extended_Enforcement {
|
||||
public function get_enforcement(): ?Two_Factor_Extended_Enforcement {
|
||||
return $this->enforcement;
|
||||
}
|
||||
|
||||
|
|
@ -275,9 +275,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Provider_Filter Provider filter instance.
|
||||
* @return Two_Factor_Extended_Provider_Filter|null Provider filter instance or null if not initialized.
|
||||
*/
|
||||
public function get_provider_filter(): Two_Factor_Extended_Provider_Filter {
|
||||
public function get_provider_filter(): ?Two_Factor_Extended_Provider_Filter {
|
||||
return $this->provider_filter;
|
||||
}
|
||||
|
||||
|
|
@ -286,9 +286,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Network_Settings Network settings instance.
|
||||
* @return Two_Factor_Extended_Network_Settings|null Network settings instance or null if not initialized.
|
||||
*/
|
||||
public function get_network_settings(): Two_Factor_Extended_Network_Settings {
|
||||
public function get_network_settings(): ?Two_Factor_Extended_Network_Settings {
|
||||
return $this->network_settings;
|
||||
}
|
||||
|
||||
|
|
@ -297,9 +297,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Audit_Log Audit log instance.
|
||||
* @return Two_Factor_Extended_Audit_Log|null Audit log instance or null if not initialized.
|
||||
*/
|
||||
public function get_audit_log(): Two_Factor_Extended_Audit_Log {
|
||||
public function get_audit_log(): ?Two_Factor_Extended_Audit_Log {
|
||||
return $this->audit_log;
|
||||
}
|
||||
|
||||
|
|
@ -308,9 +308,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Compliance_Report Compliance report instance.
|
||||
* @return Two_Factor_Extended_Compliance_Report|null Compliance report instance or null if not initialized.
|
||||
*/
|
||||
public function get_compliance_report(): Two_Factor_Extended_Compliance_Report {
|
||||
public function get_compliance_report(): ?Two_Factor_Extended_Compliance_Report {
|
||||
return $this->compliance_report;
|
||||
}
|
||||
|
||||
|
|
@ -319,9 +319,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_Bulk_Actions Bulk actions instance.
|
||||
* @return Two_Factor_Extended_Bulk_Actions|null Bulk actions instance or null if not initialized.
|
||||
*/
|
||||
public function get_bulk_actions(): Two_Factor_Extended_Bulk_Actions {
|
||||
public function get_bulk_actions(): ?Two_Factor_Extended_Bulk_Actions {
|
||||
return $this->bulk_actions;
|
||||
}
|
||||
|
||||
|
|
@ -330,9 +330,9 @@ class Two_Factor_Extended {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*
|
||||
* @return Two_Factor_Extended_REST_API REST API instance.
|
||||
* @return Two_Factor_Extended_REST_API|null REST API instance or null if not initialized.
|
||||
*/
|
||||
public function get_rest_api(): Two_Factor_Extended_REST_API {
|
||||
public function get_rest_api(): ?Two_Factor_Extended_REST_API {
|
||||
return $this->rest_api;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt
|
|||
Tags: two-factor, 2fa, authentication, security
|
||||
Requires at least: 6.7
|
||||
Tested up to: 6.9
|
||||
Stable tag: 1.0.0
|
||||
Stable tag: 1.0.1
|
||||
Requires PHP: 8.2
|
||||
Version: 1.0.0
|
||||
Version: 1.0.1
|
||||
License: GPL-3.0-or-later
|
||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
exit;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- Shared generic updater library, intentionally unprefixed.
|
||||
if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
||||
/**
|
||||
* Class Robotstxt_Updater
|
||||
|
|
@ -21,6 +22,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
* Reads plugin headers and constructs update URL automatically.
|
||||
*/
|
||||
class Robotstxt_Updater {
|
||||
// phpcs:enable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound
|
||||
|
||||
/**
|
||||
* Plugin file path.
|
||||
|
|
@ -60,7 +62,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Plugin headers.
|
||||
*
|
||||
* @var array
|
||||
* @var array<string, bool|string>
|
||||
*/
|
||||
private array $plugin_data;
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Get plugin headers.
|
||||
*
|
||||
* @return array Plugin data.
|
||||
* @return array<string, bool|string> Plugin data.
|
||||
*/
|
||||
private function get_plugin_data(): array {
|
||||
if ( ! function_exists( 'get_plugin_data' ) ) {
|
||||
|
|
@ -125,7 +127,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
*/
|
||||
private function build_json_url(): string {
|
||||
// Try Gitea Plugin URI (format: "OWNER/REPO" or full URL).
|
||||
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) ) {
|
||||
if ( ! empty( $this->plugin_data['Gitea Plugin URI'] ) && is_string( $this->plugin_data['Gitea Plugin URI'] ) ) {
|
||||
$gitea_uri = $this->plugin_data['Gitea Plugin URI'];
|
||||
|
||||
// If it's already a full URL, use it.
|
||||
|
|
@ -141,7 +143,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
}
|
||||
|
||||
// Fallback: try to extract from Plugin URI.
|
||||
if ( ! empty( $this->plugin_data['PluginURI'] ) ) {
|
||||
if ( ! empty( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ) {
|
||||
$plugin_uri = $this->plugin_data['PluginURI'];
|
||||
if ( str_contains( $plugin_uri, 'git.robotstxt.es' ) ) {
|
||||
return rtrim( $plugin_uri, '/' ) . '/raw/branch/main/update.json';
|
||||
|
|
@ -155,12 +157,12 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Inject update info into WP's plugin update transient.
|
||||
*
|
||||
* @param object|mixed $transient The update_plugins transient.
|
||||
* @param mixed $transient The update_plugins transient.
|
||||
*
|
||||
* @return object The modified transient.
|
||||
* @return stdClass The modified transient.
|
||||
*/
|
||||
public function inject_update_info( $transient ) {
|
||||
if ( ! is_object( $transient ) ) {
|
||||
public function inject_update_info( $transient ): stdClass {
|
||||
if ( ! ( $transient instanceof stdClass ) ) {
|
||||
$transient = new stdClass();
|
||||
}
|
||||
|
||||
|
|
@ -172,7 +174,8 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
return $transient;
|
||||
}
|
||||
|
||||
$current_version = $transient->checked[ $this->plugin_basename ];
|
||||
$current_version_raw = $transient->checked[ $this->plugin_basename ];
|
||||
$current_version = is_string( $current_version_raw ) ? $current_version_raw : '';
|
||||
$remote = $this->get_remote_data();
|
||||
|
||||
if ( empty( $remote['version'] ) || empty( $remote['download_url'] ) ) {
|
||||
|
|
@ -183,18 +186,26 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
return $transient;
|
||||
}
|
||||
|
||||
if ( version_compare( $remote['version'], $current_version, '>' ) ) {
|
||||
$remote_version = is_string( $remote['version'] ) ? $remote['version'] : '';
|
||||
|
||||
if ( version_compare( $remote_version, $current_version, '>' ) ) {
|
||||
$plugin_uri = isset( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ? $this->plugin_data['PluginURI'] : '';
|
||||
|
||||
$update = (object) array(
|
||||
'slug' => $remote['slug'] ?? $this->plugin_slug,
|
||||
'slug' => isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug,
|
||||
'plugin' => $this->plugin_basename,
|
||||
'new_version' => $remote['version'],
|
||||
'url' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
|
||||
'package' => $remote['download_url'],
|
||||
'tested' => $remote['tested'] ?? '',
|
||||
'requires' => $remote['requires'] ?? '',
|
||||
'requires_php' => $remote['requires_php'] ?? '',
|
||||
'new_version' => $remote_version,
|
||||
'url' => isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : $plugin_uri,
|
||||
'package' => is_string( $remote['download_url'] ) ? $remote['download_url'] : '',
|
||||
'tested' => isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : '',
|
||||
'requires' => isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : '',
|
||||
'requires_php' => isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : '',
|
||||
);
|
||||
|
||||
if ( ! property_exists( $transient, 'response' ) || ! is_array( $transient->response ) ) {
|
||||
$transient->response = array();
|
||||
}
|
||||
|
||||
$transient->response[ $this->plugin_basename ] = $update;
|
||||
}
|
||||
|
||||
|
|
@ -204,11 +215,11 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Provide "View details" modal content.
|
||||
*
|
||||
* @param false|object|array $result The result object or array.
|
||||
* @param false|object|array<mixed> $result The result object or array.
|
||||
* @param string $action The type of information being requested.
|
||||
* @param object $args Plugin API arguments.
|
||||
*
|
||||
* @return false|object The plugin information object or false.
|
||||
* @return false|object|array<mixed> The plugin information object or false.
|
||||
*/
|
||||
public function provide_plugin_details( $result, string $action, object $args ) {
|
||||
if ( 'plugin_information' !== $action ) {
|
||||
|
|
@ -225,27 +236,39 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
return $result;
|
||||
}
|
||||
|
||||
$plugin_name = isset( $remote['name'] ) && is_string( $remote['name'] ) ? $remote['name'] : ( isset( $this->plugin_data['Name'] ) && is_string( $this->plugin_data['Name'] ) ? $this->plugin_data['Name'] : $this->plugin_slug );
|
||||
$plugin_slug = isset( $remote['slug'] ) && is_string( $remote['slug'] ) ? $remote['slug'] : $this->plugin_slug;
|
||||
$plugin_version = is_string( $remote['version'] ) ? $remote['version'] : '';
|
||||
$plugin_author = isset( $remote['author'] ) && is_string( $remote['author'] ) ? $remote['author'] : ( isset( $this->plugin_data['Author'] ) && is_string( $this->plugin_data['Author'] ) ? $this->plugin_data['Author'] : '' );
|
||||
$plugin_homepage = isset( $remote['homepage'] ) && is_string( $remote['homepage'] ) ? $remote['homepage'] : ( isset( $this->plugin_data['PluginURI'] ) && is_string( $this->plugin_data['PluginURI'] ) ? $this->plugin_data['PluginURI'] : '' );
|
||||
$plugin_requires = isset( $remote['requires'] ) && is_string( $remote['requires'] ) ? $remote['requires'] : '';
|
||||
$plugin_tested = isset( $remote['tested'] ) && is_string( $remote['tested'] ) ? $remote['tested'] : '';
|
||||
$plugin_requires_php = isset( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ? $remote['requires_php'] : '';
|
||||
$plugin_description = isset( $remote['description'] ) && is_string( $remote['description'] ) ? $remote['description'] : ( isset( $this->plugin_data['Description'] ) && is_string( $this->plugin_data['Description'] ) ? $this->plugin_data['Description'] : '' );
|
||||
$plugin_changelog = isset( $remote['changelog'] ) && is_string( $remote['changelog'] ) ? $remote['changelog'] : '';
|
||||
$plugin_download = isset( $remote['download_url'] ) && is_string( $remote['download_url'] ) ? $remote['download_url'] : '';
|
||||
|
||||
return (object) array(
|
||||
'name' => $remote['name'] ?? $this->plugin_data['Name'] ?? $this->plugin_slug,
|
||||
'slug' => $remote['slug'] ?? $this->plugin_slug,
|
||||
'version' => $remote['version'],
|
||||
'author' => $remote['author'] ?? $this->plugin_data['Author'] ?? '',
|
||||
'homepage' => $remote['homepage'] ?? $this->plugin_data['PluginURI'] ?? '',
|
||||
'requires' => $remote['requires'] ?? '',
|
||||
'tested' => $remote['tested'] ?? '',
|
||||
'requires_php' => $remote['requires_php'] ?? '',
|
||||
'name' => $plugin_name,
|
||||
'slug' => $plugin_slug,
|
||||
'version' => $plugin_version,
|
||||
'author' => $plugin_author,
|
||||
'homepage' => $plugin_homepage,
|
||||
'requires' => $plugin_requires,
|
||||
'tested' => $plugin_tested,
|
||||
'requires_php' => $plugin_requires_php,
|
||||
'sections' => array(
|
||||
'description' => $remote['description'] ?? $this->plugin_data['Description'] ?? '',
|
||||
'changelog' => $remote['changelog'] ?? '',
|
||||
'description' => $plugin_description,
|
||||
'changelog' => $plugin_changelog,
|
||||
),
|
||||
'download_link' => $remote['download_url'] ?? '',
|
||||
'download_link' => $plugin_download,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remote data with caching and HMAC signature verification.
|
||||
*
|
||||
* @return array Remote data.
|
||||
* @return array<string, mixed> Remote data.
|
||||
*/
|
||||
private function get_remote_data(): array {
|
||||
$cached = get_site_transient( $this->cache_key );
|
||||
|
|
@ -255,7 +278,9 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
if ( is_array( $cached ) && isset( $cached['signature'], $cached['data'] ) ) {
|
||||
$expected_sig = hash_hmac( 'sha256', $this->cache_key . serialize( $cached['data'] ), AUTH_SALT );
|
||||
|
||||
if ( hash_equals( $expected_sig, $cached['signature'] ) ) {
|
||||
$cached_sig = is_string( $cached['signature'] ) ? $cached['signature'] : '';
|
||||
|
||||
if ( hash_equals( $expected_sig, $cached_sig ) ) {
|
||||
// Signature valid, return data.
|
||||
return is_array( $cached['data'] ) ? $cached['data'] : array();
|
||||
}
|
||||
|
|
@ -273,17 +298,17 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
// Store with HMAC signature if AUTH_SALT is available.
|
||||
if ( defined( 'AUTH_SALT' ) && '' !== AUTH_SALT ) {
|
||||
$payload = array(
|
||||
'data' => $remote ?: array(),
|
||||
'data' => $remote,
|
||||
'timestamp' => time(),
|
||||
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ?: array() ), AUTH_SALT ),
|
||||
'signature' => hash_hmac( 'sha256', $this->cache_key . serialize( $remote ), AUTH_SALT ),
|
||||
);
|
||||
set_site_transient( $this->cache_key, $payload, 6 * HOUR_IN_SECONDS );
|
||||
} else {
|
||||
// Fallback to standard caching.
|
||||
set_site_transient( $this->cache_key, $remote ?: array(), 6 * HOUR_IN_SECONDS );
|
||||
set_site_transient( $this->cache_key, $remote, 6 * HOUR_IN_SECONDS );
|
||||
}
|
||||
|
||||
return is_array( $remote ) ? $remote : array();
|
||||
return $remote;
|
||||
}
|
||||
|
||||
// Legacy cache format without signature (backward compatibility).
|
||||
|
|
@ -293,7 +318,7 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Fetch JSON from remote URL.
|
||||
*
|
||||
* @return array Decoded JSON data.
|
||||
* @return array<string, mixed> Decoded JSON data.
|
||||
*/
|
||||
private function fetch_json(): array {
|
||||
$response = wp_remote_get(
|
||||
|
|
@ -324,18 +349,18 @@ if ( ! class_exists( 'Robotstxt_Updater' ) ) {
|
|||
/**
|
||||
* Check compatibility.
|
||||
*
|
||||
* @param array $remote Remote data.
|
||||
* @param array<string, mixed> $remote Remote data.
|
||||
*
|
||||
* @return bool True if compatible.
|
||||
*/
|
||||
private function is_compatible( array $remote ): bool {
|
||||
if ( ! empty( $remote['requires_php'] ) ) {
|
||||
if ( ! empty( $remote['requires_php'] ) && is_string( $remote['requires_php'] ) ) {
|
||||
if ( version_compare( PHP_VERSION, $remote['requires_php'], '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $remote['requires'] ) ) {
|
||||
if ( ! empty( $remote['requires'] ) && is_string( $remote['requires'] ) ) {
|
||||
if ( version_compare( get_bloginfo( 'version' ), $remote['requires'], '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
* Plugin Name: Two Factor Extended
|
||||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/two-factor-extended
|
||||
* Description: Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations.
|
||||
* Version: 1.0.0
|
||||
* Version: 1.0.1
|
||||
* Requires at least: 6.7
|
||||
* Requires PHP: 8.2
|
||||
* Requires Plugins: two-factor
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
* Primary Branch: main
|
||||
*
|
||||
* @package TwoFactorExtended
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
*/
|
||||
|
||||
// Prevent direct access.
|
||||
|
|
@ -27,7 +27,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||
}
|
||||
|
||||
// Define plugin constants.
|
||||
define( 'TWO_FACTOR_EXTENDED_VERSION', '1.0.0' );
|
||||
define( 'TWO_FACTOR_EXTENDED_VERSION', '1.0.1' );
|
||||
define( 'TWO_FACTOR_EXTENDED_PLUGIN_FILE', __FILE__ );
|
||||
define( 'TWO_FACTOR_EXTENDED_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
define( 'TWO_FACTOR_EXTENDED_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
|
@ -48,7 +48,7 @@ define( 'TWO_FACTOR_EXTENDED_NETWORK_OPTION_VERSION', 'two_factor_extended_netwo
|
|||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
function two_factor_extended_activate() {
|
||||
function two_factor_extended_activate(): void {
|
||||
// Store plugin version.
|
||||
update_option( TWO_FACTOR_EXTENDED_OPTION_VERSION, TWO_FACTOR_EXTENDED_VERSION );
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ function two_factor_extended_activate() {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
function two_factor_extended_deactivate() {
|
||||
function two_factor_extended_deactivate(): void {
|
||||
// Clear any scheduled cron events.
|
||||
wp_clear_scheduled_hook( 'two_factor_extended_daily_cleanup' );
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
|||
*
|
||||
* @since 0.1.0
|
||||
*/
|
||||
function two_factor_extended_uninstall() {
|
||||
function two_factor_extended_uninstall(): void {
|
||||
// Get user preference for data removal.
|
||||
$remove_data = get_option( 'two_factor_extended_remove_data_on_uninstall', false );
|
||||
|
||||
|
|
@ -65,7 +65,7 @@ function two_factor_extended_uninstall() {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function two_factor_extended_delete_all_transients() {
|
||||
function two_factor_extended_delete_all_transients(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Get all transient keys using WordPress API wrapper (not direct SQL).
|
||||
|
|
@ -97,7 +97,7 @@ function two_factor_extended_delete_all_transients() {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function two_factor_extended_delete_all_site_transients() {
|
||||
function two_factor_extended_delete_all_site_transients(): void {
|
||||
global $wpdb;
|
||||
|
||||
// Get all site transient keys using WordPress API wrapper (not direct SQL).
|
||||
|
|
@ -128,7 +128,7 @@ function two_factor_extended_delete_all_site_transients() {
|
|||
*
|
||||
* @since 1.0.0
|
||||
*/
|
||||
function two_factor_extended_delete_all_user_meta() {
|
||||
function two_factor_extended_delete_all_user_meta(): void {
|
||||
// Get all user IDs (using WordPress API).
|
||||
$user_ids = get_users(
|
||||
array(
|
||||
|
|
|
|||
10
update.json
10
update.json
|
|
@ -1,20 +1,20 @@
|
|||
{
|
||||
"name": "Two Factor Extended",
|
||||
"slug": "two-factor-extended",
|
||||
"version": "1.0.0",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/releases/download/1.0.0/two-factor-extended-1.0.0.zip",
|
||||
"version": "1.0.1",
|
||||
"download_url": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended/releases/download/1.0.1/two-factor-extended-1.0.1.zip",
|
||||
"requires": "6.7",
|
||||
"requires_php": "8.2",
|
||||
"tested": "6.9",
|
||||
"last_updated": "2026-02-17",
|
||||
"last_updated": "2026-03-28",
|
||||
"author": "javiercasares, ROBOTSTXT",
|
||||
"author_profile": "https://www.robotstxt.es/",
|
||||
"homepage": "https://git.robotstxt.es/ROBOTSTXT/two-factor-extended",
|
||||
"description": "Extends the WordPress Two Factor plugin with advanced role-based controls, forced 2FA methods, and enhanced administrative features for single-site and Multisite installations.",
|
||||
"changelog": "<h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Documentation:</strong> Complete user and developer guides, security audit report, testing matrix, accessibility documentation</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
|
||||
"changelog": "<h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
|
||||
"sections": {
|
||||
"description": "Two Factor Extended is a comprehensive extension for the WordPress Two Factor plugin that provides administrators with enterprise-level controls over two-factor authentication across their site.<br><br><strong>Core Features:</strong><ul><li>Role-Based 2FA Requirements</li><li>Provider Visibility Control</li><li>Grace Period Enforcement (0-365 days)</li><li>WordPress Multisite Support</li><li>Compliance Reporting</li><li>Audit Logging</li><li>Bulk Operations</li><li>WP-CLI Commands</li><li>REST API Endpoints</li><li>Import/Export Settings</li></ul><strong>Security:</strong> Grade A security audit, comprehensive security hardening, CSRF protection, XSS prevention, SQL injection protection.<br><br><strong>Quality Assurance:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, WordPress Coding Standards compliant, PHP 8.2-8.5 compatible.",
|
||||
"changelog": "<h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Documentation:</strong> Complete user and developer guides, security audit report, testing matrix, accessibility documentation</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
|
||||
"changelog": "<h3>1.0.1 - 2026-03-28</h3><ul><li><strong>Compatibility:</strong> Two Factor 0.16 settings panel — settings menu now always appears after Two Factor in admin</li><li><strong>Code Quality:</strong> PHPStan level 9 clean, PHPCS WordPress Coding Standards clean</li><li><strong>Tests:</strong> PHPUnit 9.6 compatible, 28 unit tests passing (PHP 8.5, WordPress 6.7)</li></ul><h3>1.0.0 - 2026-02-17</h3><ul><li><strong>Initial Production Release:</strong> Complete enterprise-level two-factor authentication management for WordPress</li><li><strong>Core Features:</strong> Role-based 2FA requirements, provider visibility control, grace period enforcement, WordPress Multisite support</li><li><strong>Advanced Features:</strong> Audit logging, compliance reporting, bulk operations, WP-CLI integration, REST API, import/export settings</li><li><strong>Security:</strong> Grade A security audit, comprehensive input validation, output escaping, CSRF protection, XSS prevention</li><li><strong>Quality:</strong> 28 unit tests, WCAG 2.1 Level AA compliant, performance optimized, WordPress Coding Standards compliant</li><li><strong>Compatibility:</strong> WordPress 6.7-6.9, PHP 8.2-8.5, Two Factor 0.15.0 tested</li></ul>",
|
||||
"installation": "<ol><li>Install the Two Factor plugin (required dependency)</li><li>Upload and activate Two Factor Extended plugin</li><li>Go to Settings → Two Factor Extended</li><li>Configure grace period (7-14 days recommended)</li><li>Set role-based 2FA requirements</li><li>Configure provider visibility per role</li><li>Monitor compliance via Compliance tab</li><li>Review audit logs via Audit Log tab</li></ol>",
|
||||
"faq": "<h4>Does this plugin replace the Two Factor plugin?</h4><p>No, Two Factor Extended is an extension that works alongside the Two Factor plugin. Both plugins must be installed and activated.</p><h4>Is this compatible with WordPress Multisite?</h4><p>Yes! Two Factor Extended fully supports WordPress Multisite with network-wide settings and site-level overrides.</p><h4>Which PHP versions are supported?</h4><p>Two Factor Extended requires PHP 8.2 or higher.</p><h4>Can users bypass the 2FA requirements?</h4><p>No. When a 2FA method is marked as required for a user's role, they cannot disable or remove it.</p>"
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue