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