[ 'label' => 'Content inventory scanned', 'description' => 'All published posts and pages have been scanned for AI content detection.', 'article' => 'Art. 50(2)', 'severity' => 'critical', 'how_to' => 'Click "Scan All Content" on this page. Each scan analyses your existing posts and pages for AI-typical patterns.', 'action_url' => 'admin.php?page=ai-explainer-pro-compliance', ], 'badges_enabled' => [ 'label' => 'Transparency badges enabled', 'description' => 'Authenticity badges are displayed on content to inform readers about AI involvement.', 'article' => 'Art. 50(4)', 'severity' => 'critical', 'how_to' => 'Settings → General → enable "Show badges on Single Posts" and at least one badge type in the Badges tab.', 'action_url' => 'admin.php?page=ai-explainer-pro-settings', ], 'ai_content_labeled' => [ 'label' => 'AI-generated content labeled', 'description' => 'All content detected as AI-generated or declared as such displays a visible label.', 'article' => 'Art. 50(2)', 'severity' => 'critical', 'how_to' => 'Make sure the "AI Content" badge is enabled in Settings → Badges. The plugin auto-labels any post above the detection threshold.', 'action_url' => 'admin.php?page=ai-explainer-pro-settings', ], 'human_oversight' => [ 'label' => 'Human oversight documented', 'description' => 'Content declarations show human review of AI-generated material.', 'article' => 'Art. 14', 'severity' => 'important', 'how_to' => 'Open any post in the editor → "AI Explainer" meta box → mark it as Human, AI, or Mixed. Manual declarations override automatic detection.', 'action_url' => 'edit.php', ], 'disclosure_policy' => [ 'label' => 'Disclosure policy published', 'description' => 'A public page or policy explains your use of AI in content creation.', 'article' => 'Art. 50(1)', 'severity' => 'important', 'how_to' => 'Create a public page titled something like "AI Policy", "AI Disclosure", or "Transparency Policy" explaining when and how you use AI.', 'action_url' => 'post-new.php?post_type=page', ], 'records_maintained' => [ 'label' => 'Audit records maintained', 'description' => 'Scan history and detection logs are preserved for regulatory review.', 'article' => 'Art. 12', 'severity' => 'important', 'how_to' => 'Records are kept automatically as soon as you scan content. Avoid clicking "Clear History" unless you really need to.', 'action_url' => 'admin.php?page=ai-explainer-pro', ], 'regular_scanning' => [ 'label' => 'Regular scanning schedule', 'description' => 'Content is scanned on a regular basis (weekly or monthly recommended).', 'article' => 'Art. 9(2)', 'severity' => 'recommended', 'how_to' => 'Run "Scan All Content" at least once a month.', 'action_url' => 'admin.php?page=ai-explainer-pro-compliance', ], ]; /** * Evaluate the current compliance status * * @return array Full compliance report. */ public static function evaluate() { global $wpdb; $total_posts = (int) wp_count_posts( 'post' )->publish + (int) wp_count_posts( 'page' )->publish; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for compliance dashboard; not cached because freshness is required. $scanned_count = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_scanned_date'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for compliance dashboard. $ai_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_human_score' AND CAST(meta_value AS DECIMAL(5,3)) < %f", 0.4 ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for compliance dashboard. $human_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_human_score' AND CAST(meta_value AS DECIMAL(5,3)) >= %f", 0.7 ) ); $mixed_count = $scanned_count - $ai_count - $human_count; $unscanned = max( 0, $total_posts - $scanned_count ); $badges_enabled = (bool) get_option( 'ai_explainer_pro_show_badges', true ); $show_ai_badge = (bool) get_option( 'ai_explainer_pro_show_ai_badge', 1 ); $show_human_badge = (bool) get_option( 'ai_explainer_pro_show_human_badge', 1 ); $show_on_posts = (bool) get_option( 'ai_explainer_pro_show_on_posts', 1 ); // Declared content (manual) // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for compliance dashboard. $declared_ai = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_content_type' AND meta_value = 'ai'" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for compliance dashboard. $declared_human = (int) $wpdb->get_var( "SELECT COUNT(DISTINCT post_id) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_content_type' AND meta_value = 'human'" ); // Scan history. The dedicated history table is optional metadata; the // ground truth for "has anything been scanned?" is the per-post meta // _ai_explainer_scanned_date. Use it so that running "Scan All Content" // immediately satisfies the regular_scanning / records_maintained checks. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Aggregate read on postmeta for last-scan timestamp. $last_scan = $wpdb->get_var( "SELECT MAX(meta_value) FROM {$wpdb->postmeta} WHERE meta_key = '_ai_explainer_scanned_date'" ); $scan_count = $scanned_count; // Evaluate each checklist item $results = []; $passed = 0; $total = count( self::$checklist ); // 1. Content scanned $scan_ratio = ( $total_posts > 0 ) ? $scanned_count / $total_posts : 0; $results['content_scanned'] = [ 'passed' => $scan_ratio >= 0.9, 'value' => sprintf( '%d / %d (%.0f%%)', $scanned_count, $total_posts, $scan_ratio * 100 ), ]; // 2. Badges enabled $results['badges_enabled'] = [ 'passed' => $badges_enabled && $show_on_posts && ( $show_ai_badge || $show_human_badge ), 'value' => $badges_enabled ? __( 'Enabled', 'ai-explainer-pro' ) : __( 'Disabled', 'ai-explainer-pro' ), ]; // 3. AI content labeled $ai_labeled = ( $ai_count === 0 && $declared_ai === 0 ) || ( $show_ai_badge && $badges_enabled ); $results['ai_content_labeled'] = [ 'passed' => $ai_labeled, /* translators: 1: number of AI posts detected, 2: number of posts declared as AI */ 'value' => sprintf( __( '%1$d AI posts detected, %2$d declared', 'ai-explainer-pro' ), $ai_count, $declared_ai ), ]; // 4. Human oversight $results['human_oversight'] = [ 'passed' => ( $declared_human + $declared_ai ) > 0, /* translators: %d: number of manual declarations */ 'value' => sprintf( __( '%d manual declarations', 'ai-explainer-pro' ), $declared_human + $declared_ai ), ]; // 5. Disclosure policy $has_policy = self::detect_disclosure_page(); $results['disclosure_policy'] = [ 'passed' => $has_policy, 'value' => $has_policy ? __( 'Found', 'ai-explainer-pro' ) : __( 'Not found', 'ai-explainer-pro' ), ]; // 6. Records maintained $results['records_maintained'] = [ 'passed' => $scan_count >= 1, /* translators: %d: number of scan records */ 'value' => sprintf( __( '%d scan records', 'ai-explainer-pro' ), $scan_count ), ]; // 7. Regular scanning $days_since = $last_scan ? ( time() - strtotime( $last_scan ) ) / DAY_IN_SECONDS : 999; $results['regular_scanning'] = [ 'passed' => $days_since <= 30, 'value' => $last_scan /* translators: 1: formatted date of last scan, 2: number of days since last scan */ ? sprintf( __( 'Last scan: %1$s (%2$d days ago)', 'ai-explainer-pro' ), date_i18n( get_option( 'date_format' ), strtotime( $last_scan ) ), (int) $days_since ) : __( 'Never scanned', 'ai-explainer-pro' ), ]; foreach ( $results as $r ) { if ( $r['passed'] ) { $passed++; } } // Compliance score $score = ( $total > 0 ) ? round( ( $passed / $total ) * 100 ) : 0; // Risk level $risk = 'high'; if ( $score >= 80 ) { $risk = 'low'; } elseif ( $score >= 50 ) { $risk = 'medium'; } return [ 'score' => $score, 'risk' => $risk, 'passed' => $passed, 'total' => $total, 'checklist' => self::$checklist, 'results' => $results, 'inventory' => [ 'total_posts' => $total_posts, 'scanned' => $scanned_count, 'unscanned' => $unscanned, 'ai_detected' => $ai_count, 'human_detected' => $human_count, 'mixed' => $mixed_count, 'declared_ai' => $declared_ai, 'declared_human' => $declared_human, ], 'scan_history' => [ 'total_scans' => $scan_count, 'last_scan' => $last_scan, ], ]; } /** * Generate an HTML compliance report suitable for download * * @return string HTML report. */ public static function generate_report_html() { $data = self::evaluate(); $site_name = get_bloginfo( 'name' ); $site_url = home_url(); $date = date_i18n( 'F j, Y \a\t H:i' ); $inv = $data['inventory']; $risk_colors = [ 'low' => '#10B981', 'medium' => '#F59E0B', 'high' => '#EF4444' ]; $risk_color = $risk_colors[ $data['risk'] ] ?? '#6B7280'; $rows = ''; foreach ( $data['checklist'] as $key => $item ) { $r = $data['results'][ $key ]; $icon = $r['passed'] ? '✓' : '✗'; $color = $r['passed'] ? '#10B981' : '#EF4444'; $sev = ucfirst( $item['severity'] ); $rows .= " {$icon} {$item['label']}
{$item['description']} {$item['article']} {$sev} {$r['value']} "; } $html_recs = ''; foreach ( $data['checklist'] as $key => $item ) { if ( ! $data['results'][ $key ]['passed'] ) { $html_recs .= "
  • {$item['label']} — {$item['description']} ({$item['article']})
  • "; } } if ( empty( $html_recs ) ) { $html_recs = '
  • All requirements met. Continue regular scanning to maintain compliance.
  • '; } // Standalone HTML report served as a downloadable document — no // wp_enqueue context is available here, so the stylesheet is read // from disk and embedded inline. $css_path = AIEP_PLUGIN_DIR . 'assets/css/compliance-report.css'; // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Plugin-owned static asset on the local filesystem; wp_remote_get would be incorrect here. $css = file_exists( $css_path ) ? (string) file_get_contents( $css_path ) : ''; return " EU AI Act Compliance Report — " . esc_html( $site_name ) . "

    🛡 EU AI Act Compliance Report

    Site: " . esc_html( $site_name ) . " (" . esc_url( $site_url ) . ")
    Generated: {$date}
    Plugin: AI Explainer v" . AI_EXPLAINER_PRO_VERSION . "

    Compliance Score

    {$data['score']}%
    Risk level: " . strtoupper( $data['risk'] ) . "

    {$data['passed']} of {$data['total']} requirements met.

    Content Inventory

    {$inv['total_posts']}Total content
    {$inv['scanned']}Scanned
    {$inv['unscanned']}Unscanned
    {$inv['human_detected']}Human
    {$inv['mixed']}Mixed
    {$inv['ai_detected']}AI detected
    {$inv['declared_ai']}Declared AI
    {$inv['declared_human']}Declared human

    Compliance Checklist

    {$rows}
    RequirementEU AI ActSeverityStatus

    Recommendations

    "; } /** * Check if site has a disclosure/AI policy page * * @return bool */ private static function detect_disclosure_page() { global $wpdb; $keywords = [ 'ai policy', 'artificial intelligence', 'ai disclosure', 'transparency policy', 'politique ia', 'transparence ia' ]; foreach ( $keywords as $kw ) { $like = '%' . $wpdb->esc_like( $kw ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off lookup against $wpdb->posts; not cached because the dashboard refresh requires up-to-date data. $hit = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type = 'page' AND post_status = 'publish' AND (LOWER(post_title) LIKE %s OR LOWER(post_content) LIKE %s) LIMIT 1", $like, $like ) ); if ( $hit > 0 ) { return true; } } return false; } }