get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'gatewell_ai_usage_' ) . '%' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery foreach ( $option_names as $option_name ) { delete_option( $option_name ); } } /** * Delete any surviving pre-rename imagelock_* options. * * Normally the 1.6.0 upgrade migration has already moved these to gatewell_*, * but an install that was never loaded in wp-admin between upgrading and being * deleted would still have them, and the fixed delete_option() list above only * knows the new names. */ function gatewell_delete_legacy_options() { global $wpdb; // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery // phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching $option_names = $wpdb->get_col( $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'imagelock_' ) . '%' ) ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.NoCaching // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery foreach ( $option_names as $option_name ) { delete_option( $option_name ); } } /** * Drop custom database tables. */ function gatewell_drop_custom_tables() { global $wpdb; // Still `image_lock_audit_bridge` after the Gatewell rename: this table is // the shared ImageAudit integration surface, not private plugin state, and // renaming it here without a matching change in ImageAudit would split the // two plugins across two table names. See Migration::get_bridge_table_name(). $bridge_table = $wpdb->prefix . 'image_lock_audit_bridge'; // Only delete table if ImageAudit plugin is not active. // This preserves optimization history if user still has ImageAudit installed. if ( ! defined( 'IMAGE_AUDIT_VERSION' ) ) { // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- $bridge_table is $wpdb->prefix plus a hard-coded literal, never request data, and a table name cannot be bound with prepare(). // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall-time DROP TABLE for a plugin-owned table; WordPress has no API for dropping a custom table, and there is nothing to cache. $wpdb->query( "DROP TABLE IF EXISTS {$bridge_table}" ); // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter } // Analytics tables are plugin-private, so they always go. Both prefixes are // listed: an install that upgraded from ImageLock but never reached // wp-admin still has the imagelock_* tables under their original names. $analytics_tables = array( $wpdb->prefix . 'gatewell_upload_events', $wpdb->prefix . 'gatewell_analytics_daily', $wpdb->prefix . 'gatewell_analytics_monthly', $wpdb->prefix . 'imagelock_upload_events', $wpdb->prefix . 'imagelock_analytics_daily', $wpdb->prefix . 'imagelock_analytics_monthly', ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Each name is $wpdb->prefix plus a hard-coded literal from the array above, never request data, and additionally passed through esc_sql(). A table name cannot be bound with prepare(). // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Uninstall-time DROP TABLE for plugin-owned tables; WordPress has no API for dropping a custom table, and there is nothing to cache. foreach ( $analytics_tables as $analytics_table ) { $safe_table = esc_sql( $analytics_table ); $wpdb->query( "DROP TABLE IF EXISTS {$safe_table}" ); } // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter } /** * Clean up custom upload directories. * * Uses the WordPress filesystem API. WP_Filesystem::rmdir() with $recursive = true * removes the directory and everything under it, so no hand-rolled recursion is * needed. (A previous version called wp_delete_dir(), which is not a WordPress * function and fatalled here, aborting the uninstall.) * * @since 1.0.0 */ function gatewell_cleanup_directories() { $upload_dir = wp_upload_dir(); $base_dir = $upload_dir['basedir']; // Directories to clean up. The imagelock-* names are the pre-rename // directories: an install upgraded from ImageLock keeps writing new files // to the gatewell-* folders but leaves the old ones in place on disk, so // uninstall has to remove both or the originals are orphaned in uploads. $directories = array( $base_dir . '/gatewell-temp', $base_dir . '/gatewell-originals', $base_dir . '/imagelock-temp', $base_dir . '/imagelock-originals', ); global $wp_filesystem; if ( empty( $wp_filesystem ) ) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ( empty( $wp_filesystem ) ) { return; } foreach ( $directories as $directory ) { if ( $wp_filesystem->is_dir( $directory ) ) { $wp_filesystem->rmdir( $directory, true ); } } } /** * Clear the plugin's scheduled cron events. * * Deactivation does not always run before deletion, so uninstall repeats what * gatewell_deactivate() does. wp_clear_scheduled_hook() is a no-op when nothing * is scheduled. * * @since 1.6.0 */ function gatewell_clear_scheduled_events() { $hooks = array( 'gatewell_aggregate_daily', 'gatewell_aggregate_monthly', 'gatewell_prune_analytics', ); foreach ( $hooks as $hook ) { wp_clear_scheduled_hook( $hook ); } } /** * Execute uninstall cleanup. */ gatewell_delete_plugin_options(); gatewell_delete_usage_counters(); gatewell_delete_legacy_options(); gatewell_drop_custom_tables(); gatewell_cleanup_directories(); gatewell_clear_scheduled_events();