esc_like( $wpdb->prefix . 'plugixacrm_' ) . '%'; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- custom table, no object cache to read from. $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) ); foreach ( $tables as $table ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- uninstall drops the plugin's own tables; nothing to cache. $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) ); } delete_option( Settings::OPTION ); delete_option( Activator::VERSION_OPTION ); delete_option( Activator::MODULES_OPTION ); // The example key => id map. It points at rows in tables that have just been dropped, so // leaving it behind would make a later reinstall believe every example is already present // and seed nothing at all. delete_option( Seeder::OPTION ); self::delete_prefixed_options(); self::clear_cron_hooks(); Capabilities::revoke_all(); } /** * Delete every option and transient carrying the plugin's prefix. * * BY PREFIX, FOR THE REASON TABLES ARE: a module that stored an option and has since been removed, or a * listener that did not run, still leaves nothing behind. Each name goes through delete_option() rather * than one DELETE so the options cache agrees with the table afterwards. */ public static function delete_prefixed_options(): void { global $wpdb; $patterns = array( $wpdb->esc_like( self::PREFIX ) . '%', $wpdb->esc_like( '_transient_' . self::PREFIX ) . '%', $wpdb->esc_like( '_transient_timeout_' . self::PREFIX ) . '%', $wpdb->esc_like( '_site_transient_' . self::PREFIX ) . '%', $wpdb->esc_like( '_site_transient_timeout_' . self::PREFIX ) . '%', ); foreach ( $patterns as $pattern ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- enumerating our own option names for deletion; the cache is updated by delete_option(). $names = $wpdb->get_col( $wpdb->prepare( 'SELECT option_name FROM %i WHERE option_name LIKE %s', $wpdb->options, $pattern ) ); foreach ( $names as $name ) { delete_option( (string) $name ); } } } /** * Unschedule every cron hook carrying the plugin's prefix, whatever arguments it was scheduled with. * * Also run on deactivation. Modules schedule their own events and each used to clear only its own on * `plugixa_crm_deactivated` — two of seven did, so four events outlived the plugin. */ public static function clear_cron_hooks(): void { $crons = _get_cron_array(); if ( ! is_array( $crons ) ) { return; } $hooks = array(); foreach ( $crons as $events ) { foreach ( array_keys( (array) $events ) as $hook ) { if ( str_starts_with( (string) $hook, self::PREFIX ) ) { $hooks[ (string) $hook ] = true; } } } foreach ( array_keys( $hooks ) as $hook ) { wp_unschedule_hook( $hook ); } } }