'ids', 'number' => 0, ) ) as $site_id ) { switch_to_blog( (int) $site_id ); $every_site = self::uninstall_site() && $every_site; restore_current_blog(); } // The network's own data - its rules, its kill switch and its activity // index (Agency, ADR-16) - serves every site, so it goes only when every // site asked for its data to go. if ( $every_site ) { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Opt-in uninstall of a plugin-owned table on the trusted base prefix. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->base_prefix}adaf_network_events" ); foreach ( array( 'adaf_network_rules', 'adaf_network_lockdown', 'adaf_network_events_db_version' ) as $network_option ) { delete_site_option( $network_option ); } } } /** * Remove the current site's data, if that site opted in. * * @return bool Whether it did. */ private static function uninstall_site(): bool { global $wpdb; $delete_data = get_option( 'adaf_delete_data_on_uninstall', '' ); if ( '1' !== (string) $delete_data ) { return false; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Opt-in uninstall of plugin-owned tables; names are literals on the trusted prefix. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_agents" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_policies" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_ledger" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_pending" ); $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_snapshots" ); // Pro scan history (AF-20.4); absent on a free site, which DROP IF EXISTS allows. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_scan_runs" ); // Pro webhook delivery log (AF-22.3). $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}adaf_webhook_deliveries" ); // phpcs:enable $options = array( 'adaf_db_version', 'adaf_pending_ttl', 'adaf_delete_data_on_uninstall', 'adaf_anomaly_email', 'adaf_ability_fingerprints', 'adaf_ledger_retention_days', 'adaf_ledger_anchor', 'adaf_block_app_password_writes', // Who may approve their own agent's holds (AF-25.8). 'adaf_ban_self_approval', 'adaf_lockdown', 'adaf_health_alerts', 'adaf_pending_email', 'adaf_webhook_url', 'adaf_webhook_secret', 'adaf_report_agency', 'adaf_scan_last_run', 'adaf_license_tier', 'adaf_license_dev_override', // Pro digest (AF-20.3); harmless names on a free site. 'adaf_digest_enabled', 'adaf_digest_recipients', 'adaf_digest_secret', 'adaf_digest_state', // Pro scan history (AF-20.4). 'adaf_scan_runs_db_version', // Pro AI evaluator (AF-21.4). 'adaf_evaluator_enabled', 'adaf_evaluator_threshold', 'adaf_evaluator_daily_budget', 'adaf_evaluator_usage', // Pro webhook routing (AF-22.3). 'adaf_webhook_destinations', 'adaf_webhook_rules', 'adaf_webhook_deliveries_db_version', // Pro policy packs (AF-22.4): which are applied. The policies they // created are ordinary rows, dropped with the policies table. 'adaf_policy_templates_applied', 'adaf_policy_templates_lock', ); foreach ( $options as $option ) { delete_option( $option ); } wp_clear_scheduled_hook( 'adaf_expire_pending' ); wp_clear_scheduled_hook( 'adaf_prune_ledger' ); wp_clear_scheduled_hook( 'adaf_health_verify' ); wp_clear_scheduled_hook( 'adaf_digest' ); wp_unschedule_hook( 'adaf_ai_evaluate' ); wp_clear_scheduled_hook( 'adaf_webhook_send' ); // Claims left by AI reviews that were running (AF-21.4), one per hold. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'adaf_ai_claim_' ) . '%' ) ); return true; } }