add_action( 'after_uninstall', [ $this, 'after_uninstall' ] ); } } /** * Freemius after_uninstall hook. * * @return void */ public function after_uninstall(): void { $this->execute_uninstall(); } /** * Callback of the Freemius uninstall hook. * * @param array|string $ids The IDs of the plugins to uninstall. * * @return void */ public static function fs_uninstall_hook( array|string $ids ): void { $instance = new self(); if ( is_array( $ids ) && ! in_array( $instance->plugin_id, $ids, true ) ) { return; } if ( is_string( $ids ) && $ids !== $instance->plugin_id ) { return; } $instance->execute_uninstall(); } /** * Native WordPress uninstall hook. * * @return void */ public static function uninstall_native(): void { $instance = new self(); $instance->execute_uninstall(); } /** * Executes the uninstall logic. * Handles multi-site if necessary. * * @return void */ public function execute_uninstall(): void { if ( ! is_multisite() ) { $this->uninstall(); return; } $site_ids = get_sites( [ 'fields' => 'ids' ] ); if ( empty( $site_ids ) ) { return; } foreach ( $site_ids as $site_id ) { switch_to_blog( $site_id ); $this->uninstall(); restore_current_blog(); } } /** * The uninstall routine. * * @return void */ private function uninstall(): void { // Never delete data a still-active sibling AdPresso installation needs. // This check applies regardless of either "keep" toggle below. if ( $this->sibling_plugin_is_active() ) { return; } // Role capabilities are not "data" - they go regardless of the toggles below. if ( class_exists( CapabilityManager::class ) ) { CapabilityManager::remove_capabilities(); // Caps are gone, so a reinstall with kept data must apply the full set again. delete_option( CapabilityManager::APPLIED_OPTION ); } // Copied plugin assets are useless without the plugin, whatever the toggles say. AssetMaskingAdmin::delete_recorded_folders(); // Independent toggles - a user can keep ad setup data but still want media cleaned up, or the other way round. if ( ! self::keep_data_is_enabled() ) { $this->delete_tables(); $this->delete_post_types(); $this->delete_options(); } if ( ! self::keep_media_is_enabled() ) { $this->delete_media(); } wp_cache_flush(); } /** * Reads the "Keep Ad Setup Data" toggle - defaults to true * * @return bool */ public static function keep_data_is_enabled(): bool { $general_settings = get_option( 'adpresso_settings_general', false ); if ( is_array( $general_settings ) && isset( $general_settings['uninstall_keep_data'] ) ) { return (bool) $general_settings['uninstall_keep_data']; } return true; } /** * Reads the "Keep Assigned Media" toggle - defaults to true * * @return bool */ public static function keep_media_is_enabled(): bool { $general_settings = get_option( 'adpresso_settings_general', false ); if ( is_array( $general_settings ) && isset( $general_settings['uninstall_keep_media'] ) ) { return (bool) $general_settings['uninstall_keep_media']; } return true; } /** * True if the other AdPresso variant (Free or Pro) is still installed and active. * * @return bool */ private function sibling_plugin_is_active(): bool { if ( ! function_exists( 'adpresso_fs' ) ) { return false; } if ( ! function_exists( 'is_plugin_active' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $fs = adpresso_fs(); $file_name = basename( ADPRESSO_FILE ); $free_basename = $fs->get_slug() . '/' . $file_name; $premium_basename = $fs->get_premium_slug() . '/' . $file_name; return is_plugin_active( $free_basename ) || is_plugin_active( $premium_basename ); } /** * Removes custom database tables. * * @return void */ private function delete_tables(): void { global $wpdb; $this->found_interim_tg = false; // Every custom table this plugin (Free or Pro) can create, anywhere in the codebase. $tables = [ 'adpresso_notifications', 'adpresso_traffic_guard_bans', 'adpresso_traffic_guard_clicks', 'adpresso_traffic_guard_impressions', 'adpresso_traffic_guard_logs', 'adpresso_traffic_guard_daily', 'adpresso_import_log', 'adpresso_clicks_log', 'adpresso_stats', 'adpresso_delivery', 'adpresso_hd_rate_limit', ]; // guard_* and the interim tg_* names are generic: drop only tables with Traffic Guard's columns. foreach ( array_keys( Installer::TG_TABLE_SIGNATURES ) as $suffix ) { if ( $this->is_traffic_guard_table( $suffix, $suffix ) ) { $tables[] = $suffix; } $interim = 'tg_' . substr( $suffix, strlen( 'guard_' ) ); if ( $this->is_traffic_guard_table( $interim, $suffix ) ) { $tables[] = $interim; $this->found_interim_tg = true; } } foreach ( $tables as $table ) { $wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $wpdb->prefix . $table ) ); } wp_clear_scheduled_hook( 'adpresso_traffic_guard_cleanup' ); wp_clear_scheduled_hook( 'adpresso_traffic_guard_daily' ); wp_clear_scheduled_hook( 'guard_cleanup' ); wp_clear_scheduled_hook( 'guard_daily' ); } /** * Whether {prefix}$table exists and has Traffic Guard's columns. * * @param string $table Table suffix. * @param string $suffix Current TG suffix, the signature key. * * @return bool */ private function is_traffic_guard_table( string $table, string $suffix ): bool { global $wpdb; $name = $wpdb->prefix . $table; if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $name ) ) ) !== $name ) { return false; } return Installer::has_traffic_guard_signature( $name, $suffix ); } /** * Removes the custom post types (Ads, Placements, Groups). * * @return void */ private function delete_post_types(): void { $post_types_to_delete = [ 'adpresso_ad', 'adpresso_placement', 'adpresso_group' ]; foreach ( $post_types_to_delete as $post_type ) { $post_ids = get_posts( [ 'post_type' => $post_type, 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', ] ); if ( ! empty( $post_ids ) ) { foreach ( $post_ids as $post_id ) { wp_delete_post( $post_id, true ); } } } } /** * Removes media items marked as used by AdPresso (MediaIntegration::META_KEY) that aren't attached to anything else (post_parent = 0) * * @return void */ private function delete_media(): void { $attachment_ids = get_posts( [ 'post_type' => 'attachment', 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'post_parent' => 0, 'meta_key' => MediaIntegration::META_KEY, ] ); foreach ( $attachment_ids as $attachment_id ) { wp_delete_attachment( $attachment_id, true ); } } /** * Removes all options and transients that start with "adpresso_", plus Traffic Guard's guard_* / tg_* ones. * * @return void */ private function delete_options(): void { global $wpdb; // Removes all options that start with "adpresso_" $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", 'adpresso\_%' ) ); // Remove transients $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_adpresso\_%' ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", '_transient_timeout_adpresso\_%' ) ); // Traffic Guard's guard_* names: exact names only, the short prefixes are too generic to wildcard. $tg_options = [ 'guard_settings', 'guard_secret', 'guard_identity_secret', 'guard_risk_history', 'guard_snoozes', 'guard_alert_state', ]; // Interim tg_* options only when their value has Traffic Guard's shape. foreach ( $tg_options as $option ) { $interim = 'tg_' . substr( $option, strlen( 'guard_' ) ); if ( Installer::is_traffic_guard_option_value( $option, get_option( $interim ) ) ) { $tg_options[] = $interim; $this->found_interim_tg = true; } } $placeholders = implode( ', ', array_fill( 0, count( $tg_options ), '%s' ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $placeholders holds only %s placeholders. $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name IN ($placeholders)", ...$tg_options ) ); $transients = [ 'guard\_stats\_%', 'guard\_rdns\_%' ]; if ( $this->found_interim_tg ) { $transients = array_merge( $transients, [ 'tg\_stats\_%', 'tg\_rdns\_%' ] ); wp_clear_scheduled_hook( 'tg_cleanup' ); wp_clear_scheduled_hook( 'tg_daily' ); } foreach ( $transients as $transient ) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s", '_transient_' . $transient, '_transient_timeout_' . $transient ) ); } } }