query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall; there is no caching layer to invalidate at this point $wpdb->prepare( 'DELETE FROM %i WHERE option_name LIKE %s', $wpdb->options, $csdt_like ) ); } // User meta and post meta, by prefix for the same reason as the options above: the // hand-written list this replaced named a key nothing ever wrote and missed the pending // TOTP secret, the email-verify token and the events cursor that were left behind. // Includes csdt_cve_checks_prompt_dismissed (CSDT_Cve_Checks, when each admin last hid the // "new CVEs are not being checked" notice). The two switches it turns on go with the csdt_ options. $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall $wpdb->prepare( 'DELETE FROM %i WHERE meta_key LIKE %s', $wpdb->usermeta, 'csdt\_%' ) ); $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall $wpdb->prepare( 'DELETE FROM %i WHERE meta_key LIKE %s', $wpdb->postmeta, '\_csdt\_%' ) ); // The comment guard's note on each comment it held or marked spam. $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall $wpdb->prepare( 'DELETE FROM %i WHERE meta_key LIKE %s', $wpdb->commentmeta, '\_csdt\_%' ) ); // Every scheduled task this plugin registered, found in the cron array rather than // listed: fourteen hooks are scheduled across the plugin and a list would drift. foreach ( (array) _get_cron_array() as $csdt_ts => $csdt_hooks ) { foreach ( array_keys( (array) $csdt_hooks ) as $csdt_hook ) { if ( 0 === strpos( (string) $csdt_hook, 'csdt_' ) ) { wp_clear_scheduled_hook( (string) $csdt_hook ); } } } // The Out of Band Heartbeat's must-use loader, written only by its Install loader button. Its // schedule is deliberately not csdt_ prefixed (so deactivation leaves it running), which is why // the sweep above does not reach it. Found through core's own mu-plugin listing, and deleted // only when the file carries the loader's header, so a same-named file of someone else's stays. wp_clear_scheduled_hook( 'cloudscale_hb_fallback' ); foreach ( (array) wp_get_mu_plugins() as $csdt_mu ) { if ( 'cloudscale-heartbeat-loader.php' !== basename( (string) $csdt_mu ) || ! is_readable( (string) $csdt_mu ) ) { continue; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- the header of a local PHP file this plugin wrote; never a URL $csdt_head = (string) file_get_contents( (string) $csdt_mu, false, null, 0, 4096 ); if ( false !== strpos( $csdt_head, 'Plugin Name: CloudScale Heartbeat Loader' ) ) { wp_delete_file( (string) $csdt_mu ); } } // The directories the plugin created under uploads: its own log, image backups made // before recompression, the GeoIP database (cloudscale-cyber-devtools/geo, csdt-geo before // 1.10.889). Nothing else of the user's lives there. $csdt_upload = wp_upload_dir(); if ( empty( $csdt_upload['error'] ) && ! empty( $csdt_upload['basedir'] ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; if ( WP_Filesystem() ) { global $wp_filesystem; // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- WordPress core global // Every name carries the plugin prefix. The unprefixed pre-1.10.873 'social-formats' // is not swept: a name without the prefix may belong to someone else. foreach ( array( 'cloudscale-cyber-devtools', 'cloudscale-cyber-devtools-logs', 'cloudscale-cyber-devtools-image-backups', 'csdt-geo', 'csdt-social-formats' ) as $csdt_dir ) { $csdt_path = trailingslashit( $csdt_upload['basedir'] ) . $csdt_dir; if ( $wp_filesystem->is_dir( $csdt_path ) ) { $wp_filesystem->rmdir( $csdt_path, true ); } } } } // ── Tables ─────────────────────────────────────────────────────────────────── // No table was ever dropped here. wp_csdt_github_blocks has been created since // 1.9.0 and survived every uninstall, and its schema-version option survived too, // so a reinstall read a current version against a table it had not verified. // Dropped explicitly rather than through a helper, because uninstall.php runs // without the plugin loaded and cannot rely on any of its classes. // One literal statement per table, so the csdt_ prefix is visible on the line that drops it. // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- uninstall $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_ip_events' ) ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_firewall_log' ) ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_activity_log' ) ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_live_traffic' ) ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_github_blocks' ) ); $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . 'csdt_edge_events' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange