prefix (server-controlled) and the // plugin's own suffix — not user input — so interpolation is safe // after esc_sql(). // ----------------------------------------------------------------- $tables = array( $wpdb->prefix . 'attendant_chunks', $wpdb->prefix . 'attendant_qa', $wpdb->prefix . 'attendant_logs', $wpdb->prefix . 'attendant_queue', ); foreach ( $tables as $table ) { $safe_table = esc_sql( $table ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name is from $wpdb->prefix + fixed suffix, escaped with esc_sql(). $wpdb->query( "DROP TABLE IF EXISTS `{$safe_table}`" ); } // ----------------------------------------------------------------- // 2. Delete wp_options entries. // ----------------------------------------------------------------- $options = array( 'attendant_settings', 'attendant_db_version', 'attendant_index_status', 'attendant_schema', 'attendant_api_key_openai', 'attendant_api_key_anthropic', 'attendant_api_key_google', 'attendant_slack_bot_token', 'attendant_slack_signing_secret', 'attendant_slack_debug', 'attendant_embedding_provider', 'attendant_monthly_usage', 'attendant_field_config', 'attendant_onboarded', 'attendant_daily_usage', 'attendant_index_activity', 'attendant_process_key', ); foreach ( $options as $option ) { delete_option( $option ); } // ----------------------------------------------------------------- // 3. Delete all transients created by this plugin. // // LIKE queries on wp_options are the only practical way to bulk-delete // transients by prefix — WordPress has no built-in API for this. // The pattern '_transient_attendant_%' catches all plugin transients. // ----------------------------------------------------------------- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_attendant_%' OR option_name LIKE '_transient_timeout_attendant_%' OR option_name LIKE '_site_transient_attendant_%' OR option_name LIKE '_site_transient_timeout_attendant_%'" ); // ----------------------------------------------------------------- // 4. Clear scheduled cron events. // ----------------------------------------------------------------- $cron_hooks = array( 'attendant_weekly_schema_scan', 'attendant_process_index_queue', ); foreach ( $cron_hooks as $hook ) { wp_clear_scheduled_hook( $hook ); } // ----------------------------------------------------------------- // 4b. Delete this site's file-based chat logs (uploads directory). // Runs per site so multisite sub-site logs are removed too — // wp_upload_dir() is blog-aware inside switch_to_blog(). // ----------------------------------------------------------------- if ( class_exists( 'ATTENDANT_Chat_Log' ) ) { ATTENDANT_Chat_Log::delete_all(); } // ----------------------------------------------------------------- // 5. Delete any post meta written by this plugin. // (None in Phase 1, but the placeholder keeps uninstall complete // for future phases that may add post meta.) // ----------------------------------------------------------------- // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '_attendant_%'" ); } // ----------------------------------------------------------------- // Load the chat-log class so each (sub-)site's log directory can be // removed inside attendant_uninstall_single_site(). delete_all() only needs // WordPress core — the plugin's own bootstrap is intentionally NOT // loaded during uninstall. // ----------------------------------------------------------------- if ( file_exists( __DIR__ . '/includes/class-attendant-chat-log.php' ) ) { require_once __DIR__ . '/includes/class-attendant-chat-log.php'; } // ----------------------------------------------------------------- // Run for a standard (single) site install. // ----------------------------------------------------------------- attendant_uninstall_single_site(); // ----------------------------------------------------------------- // Multisite: also clean every sub-site's own tables and options. // // switch_to_blog() changes $wpdb->prefix so the same helper function // correctly targets each sub-site's tables. // ----------------------------------------------------------------- if ( is_multisite() ) { $site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0, ) ); foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); attendant_uninstall_single_site(); restore_current_blog(); } }