'page', 'numberposts' => -1, 'post_status' => array_keys(get_post_stati()), )); foreach ($rfcc_pages as $rfcc_page) { $rfcc_content = $rfcc_page->post_content; if ($rfcc_page->ID === $rfcc_gpx_page_id) { $rfcc_is_ours = $rfcc_page->post_modified_gmt === $rfcc_page->post_date_gmt; } else { $rfcc_rest = preg_replace( array( '/\[\/?rfcc_[^\]]*\]/', // [rfcc_...] shortcodes '//', // routefinder/ blocks '//', // shortcode block wrappers ), '', $rfcc_content ); $rfcc_is_ours = $rfcc_rest !== $rfcc_content && trim(wp_strip_all_tags($rfcc_rest)) === ''; } if ($rfcc_is_ours) { wp_delete_post($rfcc_page->ID, true); } } /** * Remove the ACF field groups (and their fields) the setup wizard imported * from acf.json, matched by their ACF keys. Fields can nest, so each * group's descendants are collected level by level before deleting. */ $rfcc_acf_json = __DIR__ . '/acf.json'; $rfcc_acf_groups = file_exists($rfcc_acf_json) ? json_decode((string) file_get_contents($rfcc_acf_json), true) : array(); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local config file read foreach ((array) $rfcc_acf_groups as $rfcc_acf_group) { if (empty($rfcc_acf_group['key'])) { continue; } $rfcc_acf_ids = array_map('intval', $wpdb->get_col($wpdb->prepare( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching "SELECT ID FROM {$wpdb->posts} WHERE post_name = %s AND post_type IN ('acf-field-group', 'acf-post-type', 'acf-taxonomy')", $rfcc_acf_group['key'] ))); $rfcc_acf_level = $rfcc_acf_ids; while ($rfcc_acf_level) { $rfcc_acf_level = array_map('intval', $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- integer IDs "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'acf-field' AND post_parent IN (" . implode(',', $rfcc_acf_level) . ")" )); $rfcc_acf_ids = array_merge($rfcc_acf_ids, $rfcc_acf_level); } foreach (array_reverse($rfcc_acf_ids) as $rfcc_acf_id) { wp_delete_post($rfcc_acf_id, true); } } /** * Remove plugin options * * AUTOMATIC DETECTION APPROACH: * This uses a database query to find ALL options starting with 'rfcc_', * automatically capturing current and future plugin options without requiring * manual maintenance. This matches the same approach used in the export function * (export-import-functionality.php) for consistency. * * This includes the migration/import backup options (full copies of the * plugin settings, including API keys). Transients are cleaned up * separately below. The underscore is escaped so LIKE matches a literal * "rfcc_" prefix, not "rfcc" followed by any character. */ $rfcc_options_to_delete = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s ORDER BY option_name", $wpdb->esc_like('rfcc_') . '%' ) ); // Delete all plugin options foreach ($rfcc_options_to_delete as $rfcc_option) { delete_option($rfcc_option); delete_site_option($rfcc_option); // For multisite } /** * Remove all transients (and site transients, which single-site installs * also keep in the options table) */ foreach (array('_transient_rfcc_', '_transient_timeout_rfcc_', '_site_transient_rfcc_', '_site_transient_timeout_rfcc_') as $rfcc_transient_prefix) { $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like($rfcc_transient_prefix) . '%')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } // For multisite if (is_multisite()) { foreach (array('_site_transient_rfcc_', '_site_transient_timeout_rfcc_') as $rfcc_transient_prefix) { $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s", $wpdb->esc_like($rfcc_transient_prefix) . '%')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } } /** * Remove custom post types and their meta */ $rfcc_post_types = array('rfcc_route', 'rfcc_teastop'); foreach ($rfcc_post_types as $rfcc_post_type) { // Get all posts of this type $rfcc_posts = get_posts(array( 'post_type' => $rfcc_post_type, 'numberposts' => -1, // Every status - 'any' leaves out trashed and auto-draft posts. 'post_status' => array_keys(get_post_stati()), 'fields' => 'ids' )); // Delete posts and their meta foreach ($rfcc_posts as $rfcc_post_id) { // Delete all post meta $wpdb->delete($wpdb->postmeta, array('post_id' => $rfcc_post_id)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching // Delete the post wp_delete_post($rfcc_post_id, true); } } /** * Remove this plugin's meta from any remaining posts (e.g. the * _rfcc_ride_planner_week_start marker on ride planner pages - the pages * themselves are the site's content and are kept) */ foreach (array('rfcc_', '_rfcc_') as $rfcc_postmeta_prefix) { $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like($rfcc_postmeta_prefix) . '%')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Remove custom database tables */ $rfcc_custom_tables = array( $wpdb->prefix . 'rfcc_route_stats', $wpdb->prefix . 'rfcc_gpx_tracking', // admin/routes-gpx-tracking.php $wpdb->prefix . 'rfcc_global_passwords', // admin/routes-password-protection.php (password hashes) ); foreach ($rfcc_custom_tables as $rfcc_table) { $wpdb->query( "DROP TABLE IF EXISTS `" . esc_sql( $rfcc_table ) . "`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Table name is safely constructed from $wpdb->prefix } /** * Remove uploaded files (GPX files, backup files, etc.) */ $rfcc_upload_dir = wp_upload_dir(); $rfcc_plugin_upload_dir = $rfcc_upload_dir['basedir'] . '/route-finder/'; $rfcc_gpx_dir = $rfcc_upload_dir['basedir'] . '/rfcc-gpx/'; $rfcc_backup_dir = $rfcc_upload_dir['basedir'] . '/rfcc-backups/'; $rfcc_temp_dir = $rfcc_upload_dir['basedir'] . '/rfcc_temp/'; // Helper function to recursively delete directory if (!function_exists('rfcc_delete_directory')) { function rfcc_delete_directory($dir) { if (!is_dir($dir)) { return false; } global $wp_filesystem; if ( empty($wp_filesystem) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } // WP_Filesystem() can fail (e.g. it needs FTP credentials) and leave // $wp_filesystem unset - skip rather than fatal mid-uninstall. if ( ! $wp_filesystem instanceof WP_Filesystem_Base ) { return false; } return $wp_filesystem->rmdir($dir, true); } } // Delete all plugin directories $rfcc_directories_to_delete = array($rfcc_plugin_upload_dir, $rfcc_gpx_dir, $rfcc_backup_dir, $rfcc_temp_dir); foreach ($rfcc_directories_to_delete as $rfcc_dir) { if (is_dir($rfcc_dir)) { rfcc_delete_directory($rfcc_dir); } } /** * Clear scheduled cron events - every event of every rfcc_ hook, whatever * its arguments (wp_clear_scheduled_hook() without args misses the per-post * events, which are scheduled with args). */ $rfcc_cron_hooks = array( 'rfcc_scheduled_cache_refresh', 'rfcc_delayed_cache_refresh', 'rfcc_async_cache_rebuild', 'rfcc_deferred_route_detour_sync', 'rfcc_deferred_destination_detour_sync', ); $rfcc_cron = _get_cron_array(); if (is_array($rfcc_cron)) { foreach ($rfcc_cron as $rfcc_timestamp_hooks) { foreach (array_keys((array) $rfcc_timestamp_hooks) as $rfcc_hook) { if (strpos($rfcc_hook, 'rfcc_') === 0) { $rfcc_cron_hooks[] = $rfcc_hook; } } } } foreach (array_unique($rfcc_cron_hooks) as $rfcc_hook) { wp_unschedule_hook($rfcc_hook); } /** * Remove the route/destination capabilities this plugin gave the Editor * role (includes/permission-management.php add_editor_capabilities()). */ $rfcc_editor_role = get_role('editor'); if ($rfcc_editor_role) { foreach (array('routes', 'teastops') as $rfcc_cap_type) { foreach (array('edit_%s', 'edit_others_%s', 'edit_published_%s', 'publish_%s', 'read_private_%s', 'delete_%s', 'delete_others_%s', 'delete_published_%s', 'delete_private_%s') as $rfcc_cap_format) { $rfcc_editor_role->remove_cap(sprintf($rfcc_cap_format, $rfcc_cap_type)); } } } /** * Clear any cached data */ if (function_exists('wp_cache_flush')) { wp_cache_flush(); } /** * Remove user meta related to the plugin (rfcc_* preferences and the * _rfcc_ride_planner_draft style private keys) */ foreach (array('rfcc_', '_rfcc_') as $rfcc_usermeta_prefix) { $wpdb->query($wpdb->prepare("DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s", $wpdb->esc_like($rfcc_usermeta_prefix) . '%')); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching } /** * Clear rewrite rules */ flush_rewrite_rules();