diff --git a/.gitignore b/.gitignore
index 8c357b0ee..be4713c9e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,5 @@ tests/cypress/reports
tests/cypress/downloads
distributor.zip
+
+.github/phpcs-report.xml
diff --git a/includes/bootstrap.php b/includes/bootstrap.php
index 9be1200b8..e7dd98e92 100644
--- a/includes/bootstrap.php
+++ b/includes/bootstrap.php
@@ -12,6 +12,7 @@
namespace Distributor;
+use WP_Screen;
use YahnisElsts\PluginUpdateChecker\v5\PucFactory;
/**
@@ -216,6 +217,75 @@ function() {
1
);
+
+/**
+ * Add a deactivation modal when deactivating the plugin.
+ *
+ * @since x.x.x
+ */
+function register_deactivation_modal() {
+ // Exit if deactivating plugin from sub site.
+ $screen = get_current_screen();
+ if ( ! ( ! is_multisite() || $screen->in_admin( 'network' ) ) ) {
+ return;
+ }
+
+ wp_enqueue_script( 'jquery-ui-dialog' );
+ wp_enqueue_style( 'wp-jquery-ui-dialog' );
+
+ add_action(
+ 'admin_footer',
+ static function () {
+ printf(
+ '
',
+ esc_html__( 'Would you like to delete all Distributor data?', 'distributor' ),
+ esc_html__( 'By default, the database entries are not deleted when you deactivate Distributor. If you are deleting Distributor completely from your website and want those items removed as well, add the code below to wp-config.php:', 'distributor' ),
+ 'define( \'DT_REMOVE_ALL_DATA\', true );',
+ esc_html__( 'After adding this code, the Distributor plugin data will be removed from the website database when deleting the plugin. This will not delete the posts with their metadata other than the subscription. You can review uninstall.php (in the plugin root directory) to learn more about the deleted data. After deleting the Distributor plugin, you can remove the code from the wp-config.php file. Please make sure that this action cannot be undone; take a backup before proceeding.', 'distributor' )
+ );
+ }
+ );
+
+ $modal_title = wp_json_encode( __( 'Distributor Deactivation', 'distributor' ) );
+ $modal_button_title_deactivate = wp_json_encode( __( 'Deactivate', 'distributor' ) );
+ $modal_button_title_cancel = wp_json_encode( __( 'Cancel', 'distributor' ) );
+ $script = <<get_col(
+ $wpdb->prepare(
+ "SELECT ID FROM $wpdb->posts WHERE post_type = %s",
+ 'dt_subscription'
+ )
+ );
+
+ if ( ! empty( $subscription_post_ids ) ) {
+ $ids_string = implode( ',', array_map( 'intval', $subscription_post_ids ) );
+
+ // Delete subscription meta.
+ $wpdb->query(
+ "DELETE FROM $wpdb->postmeta WHERE post_id IN ($ids_string)"
+ );
+
+ // Delete subscription posts.
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $wpdb->posts WHERE ID IN (%s)",
+ $ids_string
+ )
+ );
+
+ // Clear the post cache.
+ wp_cache_set_posts_last_changed();
+ wp_cache_delete_multiple( $subscription_post_ids, 'posts' );
+ wp_cache_delete_multiple( $subscription_post_ids, 'post_meta' );
+ }
+
+ // Delete relevant options from the options table.
+ delete_site_options();
+ }
+
+ /**
+ * Delete all relevant options from the options table.
+ */
+ function delete_site_options() {
+ global $wpdb;
+
+ $option_prefixes = array(
+ 'dt_',
+ '_transient_dt_',
+ '_transient_timeout_dt_',
+ );
+
+ // Site transients can be used on single sites too.
+ // See https://github.com/10up/distributor/pull/540#discussion_r1759587692
+ if ( ! is_multisite() ) {
+ $option_prefixes = array_merge(
+ $option_prefixes,
+ array(
+ '_site_transient_dt_',
+ '_site_transient_timeout_dt_',
+ )
+ );
+ }
+
+ // Prepare the WHERE clause for the options table.
+ $where_clause = implode( ' OR ', array_fill( 0, count( $option_prefixes ), 'option_name LIKE %s' ) );
+
+ // Prepare the query.
+ $query = $wpdb->prepare(
+ sprintf(
+ "SELECT option_id, option_name FROM $wpdb->options WHERE %s;",
+ $where_clause
+ ),
+ array_map(
+ function( $prefix ) use ( $wpdb ) {
+ return $wpdb->esc_like( $prefix ) . '%';
+ },
+ $option_prefixes
+ )
+ );
+
+ // Fetch the options to delete.
+ $options_to_delete = $wpdb->get_results( $query, ARRAY_A );
+
+ if ( ! empty( $options_to_delete ) ) {
+ // Collect IDs from fetched options.
+ $ids = array_column( $options_to_delete, 'option_id' );
+ $ids_string = implode( ',', array_map( 'intval', $ids ) );
+
+ // Delete the options using the retrieved IDs.
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $wpdb->options WHERE option_id IN ($ids_string)"
+ )
+ );
+
+ // Flush the options cache.
+ $option_names = array_column( $options_to_delete, 'option_name' );
+ wp_cache_delete_multiple( $option_names, 'options' );
+
+ // Flush the alloptions cache.
+ wp_cache_delete( 'alloptions', 'options' );
+ }
+ }
+
+ /**
+ * Delete all relevant options from the sitemeta table (multisite only).
+ */
+ function delete_sitemeta_options() {
+ global $wpdb;
+
+ $option_prefixes = array(
+ 'dt_',
+ '_site_transient_dt_',
+ '_site_transient_timeout_dt_',
+ );
+
+ // Prepare the WHERE clause for the sitemeta table.
+ $where_clause = implode( ' OR ', array_fill( 0, count( $option_prefixes ), 'meta_key LIKE %s' ) );
+
+ $site_id = get_current_network_id();
+
+ $query = $wpdb->prepare(
+ sprintf(
+ "SELECT meta_id, meta_key FROM $wpdb->sitemeta WHERE site_id = %%d AND (%s);",
+ $where_clause
+ ),
+ array_merge(
+ [ $site_id ],
+ array_map(
+ function( $prefix ) use ( $wpdb ) {
+ return $wpdb->esc_like( $prefix ) . '%';
+ },
+ $option_prefixes
+ )
+ )
+ );
+
+ // Fetch the sitemeta to delete.
+ $sitemeta_to_delete = $wpdb->get_results( $query, ARRAY_A );
+
+ if ( ! empty( $sitemeta_to_delete ) ) {
+ // Collect IDs from fetched options.
+ $ids = array_column( $sitemeta_to_delete, 'meta_id' );
+ $ids_string = implode( ',', array_map( 'intval', $ids ) );
+
+ // Delete the sitemeta using the retrieved IDs.
+ $wpdb->query(
+ $wpdb->prepare(
+ "DELETE FROM $wpdb->sitemeta WHERE meta_id IN ($ids_string)"
+ )
+ );
+
+ // Flush the site options cache.
+ $key_names = array_column( $sitemeta_to_delete, 'meta_key' );
+ $key_names = array_map(
+ function( $key ) use ( $site_id ) {
+ return $site_id . ':' . $key;
+ },
+ $key_names
+ );
+ wp_cache_delete_multiple( $key_names, 'site-options' );
+ }
+ }
+
+ // Check if it's a multisite installation.
+ if ( is_multisite() ) {
+ // Loop through each site in the network.
+ $sites = get_sites();
+ foreach ( $sites as $site ) {
+ switch_to_blog( $site->blog_id );
+ dt_delete_data();
+ restore_current_blog();
+ }
+
+ // Delete network-wide sitemeta options.
+ delete_sitemeta_options();
+ } else {
+ // Single site.
+ dt_delete_data();
+ }
+}
+
diff --git a/webpack.config.release.js b/webpack.config.release.js
index 98edf3b98..757689ce4 100644
--- a/webpack.config.release.js
+++ b/webpack.config.release.js
@@ -10,6 +10,7 @@ module.exports = {
new CopyPlugin( {
patterns: [
{ from: 'readme.txt', to: './' },
+ { from: 'uninstall.php', to: './' },
{ from: 'README.md', to: './' },
{ from: 'CHANGELOG.md', to: './' },
{ from: 'composer.json', to: './' },