prefix` follows the switch. $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}acid_aeo_events" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared foreach ( [ '_acid_aeo_summary', '_acid_aeo_faq', '_acid_aeo_exclude', '_acid_aeo_noai', '_acid_aeo_schema_type', '_acid_aeo_license', '_acid_aeo_credit', '_acid_aeo_creator', '_acid_aeo_hide_updated' ] as $post_meta ) { delete_metadata( 'post', 0, $post_meta, '', true ); } // Transients are versioned, so they are matched by prefix rather than by // name. `$wpdb->options` follows the switch too. $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", $wpdb->esc_like( '_transient_acid_aeo_' ) . '%', $wpdb->esc_like( '_transient_timeout_acid_aeo_' ) . '%' ) ); if ( ! $shared ) { return; } // `wp_usermeta` is one table for the whole network and these keys carry no // site prefix, so this is a single global delete, not a per-site one. foreach ( [ 'acid_aeo_job_title', 'acid_aeo_same_as', 'acid_aeo_knows_about', 'acid_aeo_alumni_of', 'acid_aeo_notice_dismissed' ] as $user_meta ) { delete_metadata( 'user', 0, $user_meta, '', true ); } // Physical files are only removed when the plugin wrote them. The text // files sign themselves with a comment line; ai-context.json cannot carry a // comment, so its `instructions.generator` field is the signature. Both are // matched as substrings, so a file written by an older version is still // recognised. robots.txt is deliberately absent: the plugin never writes it. // Where the files were written, which is not always where WordPress lives: // on a site given its own directory the webroot is the one above `ABSPATH`. // The class the module writes through is loaded rather than copied, so the // delete cannot drift from the write. require_once __DIR__ . '/includes/Support/Filesystem.php'; $webroot = \AcidStudios\AEO\Support\Filesystem::webroot(); foreach ( [ 'llms.txt', 'llms-full.txt', 'humans.txt', 'ai.txt', 'ai-context.json', '.well-known/security.txt' ] as $file ) { $path = $webroot . $file; if ( ! is_file( $path ) || ! is_readable( $path ) ) { continue; } $contents = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents if ( ! is_string( $contents ) ) { continue; } foreach ( [ 'generated by Acid AEO', '"generator": "Acid AEO' ] as $marker ) { if ( false !== strpos( $contents, $marker ) ) { wp_delete_file( $path ); break; } } } } if ( ! is_multisite() ) { acid_aeo_uninstall_site( true ); return; } $acid_aeo_main_site_id = (int) get_main_site_id(); foreach ( get_sites( [ 'fields' => 'ids', 'number' => 0, ] ) as $acid_aeo_site_id ) { switch_to_blog( (int) $acid_aeo_site_id ); // The `finally` unwinds the switch whatever the body did. A site whose // tables are half gone can make `DROP TABLE` or `delete_metadata()` throw, // and a switch left on the stack would point the rest of the uninstall — // and every site after this one — at the wrong database tables. try { acid_aeo_uninstall_site( (int) $acid_aeo_site_id === $acid_aeo_main_site_id ); } finally { restore_current_blog(); } }