db = $db ?? $wpdb; } /** * Removing the plugin's data from every site it was used on. */ public static function run(): void { $uninstaller = new self(); if ( is_multisite() ) { $uninstaller->clean_network(); } else { $uninstaller->clean_site(); } // User meta is network-wide, so it is cleared once. $uninstaller->delete_user_meta(); } /** * Removing the plugin's data from every site on the network. */ private function clean_network(): void { $offset = 0; while ( true ) { $site_ids = get_sites( array( 'fields' => 'ids', 'number' => self::SITES_PER_BATCH, 'offset' => $offset, ) ); if ( array() === $site_ids ) { return; } foreach ( $site_ids as $site_id ) { switch_to_blog( (int) $site_id ); $this->clean_site(); restore_current_blog(); } $offset += self::SITES_PER_BATCH; } } /** * Removing the plugin's data from the current site. */ private function clean_site(): void { wp_clear_scheduled_hook( RemoteListsServiceProvider::REFRESH_ACTION ); $this->delete_options(); $this->delete_post_meta(); $table = new TokenTable( $this->db ); $table->drop(); } /** * Deleting the plugin's options, including its transients. */ private function delete_options(): void { $prefix = $this->db->esc_like( Options::PREFIX ) . '%'; $patterns = array( $prefix, $this->db->esc_like( '_transient_' . Options::PREFIX ) . '%', $this->db->esc_like( '_transient_timeout_' . Options::PREFIX ) . '%', ); foreach ( $patterns as $pattern ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete by name pattern; no core API takes a pattern, and the object cache is flushed below. $names = $this->db->get_col( $this->db->prepare( 'SELECT option_name FROM %i WHERE option_name LIKE %s', $this->db->options, $pattern ) ); if ( ! is_array( $names ) ) { continue; } foreach ( $names as $name ) { delete_option( (string) $name ); } } } /** * Deleting the post meta the plugin wrote, by exact key so site fields sharing a prefix survive. */ private function delete_post_meta(): void { foreach ( MetaKeys::post_meta_keys() as $meta_key ) { $this->delete_meta_rows( $this->db->postmeta, 'post_id', $meta_key ); } wp_cache_flush(); } /** * Deleting the user meta the plugin wrote. */ private function delete_user_meta(): void { foreach ( MetaKeys::user_meta_keys() as $meta_key ) { $this->delete_meta_rows( $this->db->usermeta, 'user_id', $meta_key ); } wp_cache_flush(); } /** * Deleting every row holding one meta key, a batch at a time. * * @param string $table Meta table name. * @param string $id_column Name of the object ID column, used only to keep the LIMIT deterministic. * @param string $meta_key Key to delete. */ private function delete_meta_rows( string $table, string $id_column, string $meta_key ): void { do { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk delete of one meta key on uninstall; delete_metadata() would run one query per object, and the cache is flushed by the caller. $deleted = $this->db->query( $this->db->prepare( 'DELETE FROM %i WHERE meta_key = %s ORDER BY %i ASC LIMIT %d', $table, $meta_key, $id_column, self::ROWS_PER_BATCH ) ); } while ( is_int( $deleted ) && $deleted >= self::ROWS_PER_BATCH ); } }