| @@ -2,23 +2,23 @@ | ||
| 2 | 2 | |
| 3 | 3 | /** |
| 4 | 4 | * Fired when the plugin is uninstalled. |
| 5 | 5 | * |
| 6 | - * When populating this file, consider the following flow | |
| 7 | - * of control: | |
| 6 | + * Cleanup policy: | |
| 8 | 7 | * |
| 9 | - * - This method should be static | |
| 10 | - * - Check if the $_REQUEST content actually is the plugin name | |
| 11 | - * - Run an admin referrer check to make sure it goes through authentication | |
| 12 | - * - Verify the output of $_GET makes sense | |
| 13 | - * - Repeat with other user roles. Best directly by using the links/query string parameters. | |
| 14 | - * - Repeat things for multisite. Once for a single site in the network, once sitewide. | |
| 8 | + * - Credentials (the Google Indexing service account — a plaintext private | |
| 9 | + * key — and the IndexNow API key) are ALWAYS deleted. They must never be | |
| 10 | + * left behind in wp_options by a plugin that is no longer installed. | |
| 11 | + * - Everything else (custom tables, options, transients, per-post SEO meta, | |
| 12 | + * log files) is only removed when the site owner opted in via the | |
| 13 | + * "Delete all plugin data when the plugin is uninstalled" toggle in | |
| 14 | + * Advanced Settings. The default is OFF, per WordPress uninstall guidelines. | |
| 15 | 15 | * |
| 16 | - * This file may be updated more in future version of the Boilerplate; however, this is the | |
| 17 | - * general skeleton and outline for how the file should work. | |
| 16 | + * The opt-in option is read BEFORE any options are deleted, because the | |
| 17 | + * option itself is plugin data. | |
| 18 | 18 | * |
| 19 | - * For more information, see the following discussion: | |
| 20 | - * https://github.com/tommcfarlin/WordPress-Plugin-Boilerplate/pull/123#issuecomment-28541913 | |
| 19 | + * For multisite, the cleanup runs once per site when the plugin is | |
| 20 | + * network-activated and removed. | |
| 21 | 21 | * |
| 22 | 22 | * @link https://searchatlas.com |
| 23 | 23 | * @since 1.0.0 |
| 24 | 24 | * |
| @@ -27,5 +27,123 @@ | ||
| 27 | 27 | |
| 28 | 28 | // If uninstall not called from WordPress, then exit. |
| 29 | 29 | if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) { |
| 30 | 30 | exit; |
| 31 | +} | |
| 32 | + | |
| 33 | +/** | |
| 34 | + * Remove plugin data for the current site. | |
| 35 | + * | |
| 36 | + * @return void | |
| 37 | + */ | |
| 38 | +function metasync_uninstall_cleanup() { | |
| 39 | + global $wpdb; | |
| 40 | + | |
| 41 | + // Read the opt-in first: the option itself is plugin data and is removed | |
| 42 | + // by the prefix sweep below. | |
| 43 | + $delete_all_data = ( 'yes' === get_option( 'metasync_delete_data_on_uninstall', 'no' ) ); | |
| 44 | + | |
| 45 | + // Credentials are always removed, regardless of the opt-in. | |
| 46 | + delete_option( 'google_index_service_account' ); | |
| 47 | + delete_option( 'metasync_options_bing_instant_indexing' ); | |
| 48 | + // Legacy instant-indexing option: holds the Google service-account JSON key. | |
| 49 | + delete_option( 'metasync_options_instant_indexing' ); | |
| 50 | + | |
| 51 | + if ( ! $delete_all_data ) { | |
| 52 | + return; | |
| 53 | + } | |
| 54 | + | |
| 55 | + // Scheduled events. | |
| 56 | + $cron_hooks = array( | |
| 57 | + 'metasync_daily_cleanup', | |
| 58 | + 'metasync_otto_js_check_event', | |
| 59 | + 'metasync_process_otto_batch_cache_job', | |
| 60 | + 'metasync_process_otto_crawl_url_job', | |
| 61 | + 'metasync_sitemap_async_warmup_event', | |
| 62 | + 'metasync_media_batch_optimize_cron', | |
| 63 | + 'metasync_check_debug_limits', | |
| 64 | + 'metasync_speed_cache_cleanup', | |
| 65 | + 'metasync_bing_indexnow_submit_event', | |
| 66 | + ); | |
| 67 | + foreach ( $cron_hooks as $cron_hook ) { | |
| 68 | + wp_clear_scheduled_hook( $cron_hook ); | |
| 69 | + } | |
| 70 | + | |
| 71 | + // Custom tables. | |
| 72 | + $tables = array( | |
| 73 | + 'metasync_404_logs', | |
| 74 | + 'metasync_redirections', | |
| 75 | + 'metasync_heartbeat_error_logs', | |
| 76 | + 'metasync_sync_history', | |
| 77 | + 'metasync_otto_excluded_urls', | |
| 78 | + 'metasync_robots_txt_backups', | |
| 79 | + 'metasync_otto_bot_stats', | |
| 80 | + 'metasync_otto_bot_logs', | |
| 81 | + ); | |
| 82 | + foreach ( $tables as $table ) { | |
| 83 | + $wpdb->query( "DROP TABLE IF EXISTS {$wpdb->prefix}{$table}" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- static table names, no user input. | |
| 84 | + } | |
| 85 | + | |
| 86 | + // Plugin options and transients (transients live in the options table | |
| 87 | + // under _transient_/_transient_timeout_ prefixes when no object cache | |
| 88 | + // persists them; object-cache-backed transients age out on their own). | |
| 89 | + $option_patterns = array( | |
| 90 | + 'metasync\_%', | |
| 91 | + '\_transient\_metasync\_%', | |
| 92 | + '\_transient\_timeout\_metasync\_%', | |
| 93 | + '\_site\_transient\_metasync\_%', | |
| 94 | + '\_site\_transient\_timeout\_metasync\_%', | |
| 95 | + ); | |
| 96 | + foreach ( $option_patterns as $option_pattern ) { | |
| 97 | + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $option_pattern ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from core. | |
| 98 | + } | |
| 99 | + | |
| 100 | + // Per-post SEO values saved by the plugin (custom titles, meta | |
| 101 | + // descriptions, robots directives, schema, Open Graph, canonical URLs). | |
| 102 | + // Several metaboxes store their values without the leading underscore | |
| 103 | + // (metasync_common_robots, metasync_schema_markup, metasync_advance_robots, | |
| 104 | + // metasync_post, metasync_post_redirection_meta), so both prefixes are | |
| 105 | + // swept. | |
| 106 | + $wpdb->query( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE '\_metasync\_%' OR meta_key LIKE 'metasync\_%'" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- literal patterns, no user input. | |
| 107 | + | |
| 108 | + // On-disk data directory (error logs, zipped logs, cache artifacts). | |
| 109 | + $metasync_data_dir = WP_CONTENT_DIR . '/metasync_data'; | |
| 110 | + if ( is_dir( $metasync_data_dir ) ) { | |
| 111 | + metasync_uninstall_rrmdir( $metasync_data_dir ); | |
| 112 | + } | |
| 113 | +} | |
| 114 | + | |
| 115 | +/** | |
| 116 | + * Recursively delete a directory. | |
| 117 | + * | |
| 118 | + * @param string $dir Absolute directory path. | |
| 119 | + * @return void | |
| 120 | + */ | |
| 121 | +function metasync_uninstall_rrmdir( $dir ) { | |
| 122 | + if ( ! is_dir( $dir ) ) { | |
| 123 | + return; | |
| 124 | + } | |
| 125 | + foreach ( scandir( $dir ) as $entry ) { | |
| 126 | + if ( '.' === $entry || '..' === $entry ) { | |
| 127 | + continue; | |
| 128 | + } | |
| 129 | + $path = $dir . '/' . $entry; | |
| 130 | + if ( is_dir( $path ) ) { | |
| 131 | + metasync_uninstall_rrmdir( $path ); | |
| 132 | + } else { | |
| 133 | + @unlink( $path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort cleanup on uninstall. | |
| 134 | + } | |
| 135 | + } | |
| 136 | + @rmdir( $dir ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- best-effort cleanup on uninstall. | |
| 137 | +} | |
| 138 | + | |
| 139 | +if ( is_multisite() ) { | |
| 140 | + $metasync_uninstall_site_ids = get_sites( array( 'fields' => 'ids', 'number' => 0 ) ); | |
| 141 | + foreach ( $metasync_uninstall_site_ids as $metasync_uninstall_site_id ) { | |
| 142 | + switch_to_blog( $metasync_uninstall_site_id ); | |
| 143 | + metasync_uninstall_cleanup(); | |
| 144 | + } | |
| 145 | + restore_current_blog(); | |
| 146 | + unset( $metasync_uninstall_site_ids, $metasync_uninstall_site_id ); | |
| 147 | +} else { | |
| 148 | + metasync_uninstall_cleanup(); | |
| 31 | 149 | } |