$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} 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 $args Optional arguments. * * @return array> Array of non-compliant user data. */ public function get_non_compliant_users( array $args = array() ): array { $defaults = array( 'role' => '', 'blog_id' => get_current_blog_id(), ); $args = wp_parse_args( $args, $defaults ); $user_args = array( 'fields' => 'all', ); if ( ! empty( $args['role'] ) ) { $user_args['role'] = $args['role']; } if ( is_multisite() && ! empty( $args['blog_id'] ) ) { $user_args['blog_id'] = $args['blog_id']; } $users = get_users( $user_args ); $non_compliant = array(); $enforcement = two_factor_extended()->get_enforcement(); 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 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 $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 $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 ); } }