prefix . $fspd_table; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- own table. $wpdb->query("DROP TABLE IF EXISTS {$fspd_name}"); } $fspd_options = [ 'fspd_version', 'fspd_db_version', 'fspd_caps_seeded', 'fspd_author_flags', 'fspd_settings', 'fspd_author_sync_lock', 'fspd_recurring_seeded', ]; foreach ($fspd_options as $fspd_option) { delete_option($fspd_option); } // Deleted through delete_option() to invalidate the cache; underscores are LIKE-escaped. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $fspd_dynamic_options = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE 'fspd\\_default\\_author\\_%' OR option_name LIKE 'fspd\\_create\\_user\\_%'" ); foreach ($fspd_dynamic_options as $fspd_dynamic_option) { delete_option($fspd_dynamic_option); } // The per-site connector cache; delete_transient() also drops its timeout row. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $fspd_transients = $wpdb->get_col( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE '\\_transient\\_fspd\\_connector\\_%'" ); foreach ($fspd_transients as $fspd_transient) { delete_transient(substr($fspd_transient, strlen('_transient_'))); } // No bulk delete-by-prefix API exists, so raw-delete then invalidate the meta caches. // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $fspd_meta_post_ids = $wpdb->get_col( "SELECT DISTINCT post_id FROM {$wpdb->postmeta} WHERE meta_key LIKE '\\_fspd\\_%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\\_fspd\\_%'"); foreach ($fspd_meta_post_ids as $fspd_meta_post_id) { wp_cache_delete((int) $fspd_meta_post_id, 'post_meta'); } // Uninstall loads only this file, so Action Scheduler may be missing; then our rows are deleted directly. if (function_exists('as_unschedule_all_actions')) { as_unschedule_all_actions('', [], 'fspd'); } else { // Underscores are LIKE wildcards, so the table name is escaped before the existence check. // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- shared library's table, no core API. $fspd_action_scheduler_present = $wpdb->get_var( $wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->esc_like($wpdb->prefix . 'actionscheduler_groups')) ); if ($fspd_action_scheduler_present) { // phpcs:disable WordPress.DB.DirectDatabaseQuery -- shared library's table, no core API. // Logs are keyed by action_id, so they go first or they are orphaned by the delete below. $wpdb->query( "DELETE l FROM {$wpdb->prefix}actionscheduler_logs l INNER JOIN {$wpdb->prefix}actionscheduler_actions a ON a.action_id = l.action_id INNER JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE g.slug = 'fspd'" ); $wpdb->query( "DELETE a FROM {$wpdb->prefix}actionscheduler_actions a INNER JOIN {$wpdb->prefix}actionscheduler_groups g ON g.group_id = a.group_id WHERE g.slug = 'fspd'" ); $wpdb->query("DELETE FROM {$wpdb->prefix}actionscheduler_groups WHERE slug = 'fspd'"); // phpcs:enable WordPress.DB.DirectDatabaseQuery } } // 'capabilities' must be set: core merges the key unconditionally, and a fatal here aborts the uninstall. $fspd_capabilities = get_post_type_capabilities((object) [ 'capability_type' => ['fspd_post', 'fspd_posts'], 'map_meta_cap' => true, 'capabilities' => [], ]); $fspd_legacy_capabilities = get_post_type_capabilities((object) [ 'capability_type' => ['remote_post', 'remote_posts'], 'map_meta_cap' => true, 'capabilities' => [], ]); $fspd_legacy_capabilities = array_filter( (array) $fspd_legacy_capabilities, static function ($fspd_capability): bool { return strpos($fspd_capability, 'remote_post') !== false; } ); foreach (['contributor', 'author', 'editor', 'administrator'] as $fspd_role_name) { $fspd_role = get_role($fspd_role_name); if (!$fspd_role) { continue; } foreach ($fspd_capabilities as $fspd_capability) { // map_meta_cap also yields core's bare 'read', so only the plugin's own primitives are stripped. if (strpos($fspd_capability, 'fspd_post') === false) { continue; } $fspd_role->remove_cap($fspd_capability); } // Installs that predate the prefixed names still carry the originals. foreach ($fspd_legacy_capabilities as $fspd_capability) { $fspd_role->remove_cap($fspd_capability); } } // Members keep their accounts, with no role, rather than being deleted with the plugin. remove_role('fspd_member'); }; /** Users are shared across a network, so their meta is cleared once rather than per site. */ $fspd_uninstall_user_meta = static function (): void { global $wpdb; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $fspd_meta_user_ids = $wpdb->get_col( "SELECT DISTINCT user_id FROM {$wpdb->usermeta} WHERE meta_key LIKE '\\_fspd\\_%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery -- fixed LIKE. $wpdb->query("DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE '\\_fspd\\_%'"); foreach ($fspd_meta_user_ids as $fspd_meta_user_id) { wp_cache_delete((int) $fspd_meta_user_id, 'user_meta'); } }; if (is_multisite()) { foreach (get_sites(['fields' => 'ids', 'number' => 0]) as $fspd_blog_id) { switch_to_blog((int) $fspd_blog_id); try { $fspd_uninstall_site(); } finally { restore_current_blog(); } } } else { $fspd_uninstall_site(); } $fspd_uninstall_user_meta();