/.php", * relative to the plugins directory. The expected value is the main plugin file * sitting beside this one, so both sides are reduced with plugin_basename() - * which normalises separators and strips the plugins directory - and compared in * full. Comparing only the directory would accept a registered path naming a * different file in the same directory. * * Without this check, a request that managed to define the constant could use * this cleanup to drop tables while some other plugin was being removed. */ $aive_uninstall_expected = plugin_basename( __DIR__ . '/summitcite.php' ); $aive_uninstall_actual = plugin_basename( (string) WP_UNINSTALL_PLUGIN ); if ( $aive_uninstall_expected !== $aive_uninstall_actual ) { exit; } unset( $aive_uninstall_expected, $aive_uninstall_actual ); if ( ! function_exists( 'aive_uninstall_drop_tables' ) ) { /** * Drops every table whose name carries the plugin's prefix. * * @since 1.0.0 * * @return void */ function aive_uninstall_drop_tables() { global $wpdb; $like = $wpdb->esc_like( $wpdb->prefix . 'aive_' ) . '%'; // SHOW TABLES LIKE takes a string literal, so the pattern binds normally. $tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', $like ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Schema inventory during uninstall; there is nothing to cache. if ( empty( $tables ) ) { return; } foreach ( $tables as $table ) { /* * A table identifier cannot be bound as a prepared parameter. These * names come from the database itself and are constrained to the * plugin's own prefix; backticks are stripped so the quoting cannot * be broken out of. */ $wpdb->query( 'DROP TABLE IF EXISTS `' . str_replace( '`', '', (string) $table ) . '`' ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- DDL on the plugin's own tables; identifiers cannot be prepared. } } } if ( ! function_exists( 'aive_uninstall_delete_transients' ) ) { /** * Deletes the plugin's transients, including orphaned timeout rows. * * Goes through delete_transient() rather than a bulk DELETE so that a * persistent object cache drops the cached copy too. * * @since 1.0.0 * * @return void */ function aive_uninstall_delete_transients() { global $wpdb; $groups = array( '_transient_' => 'delete_transient', '_site_transient_' => 'delete_site_transient', '_transient_timeout_' => 'delete_option', '_site_transient_timeout_' => 'delete_site_option', ); foreach ( $groups as $prefix => $deleter ) { $like = $wpdb->esc_like( $prefix . 'aive_' ) . '%'; $names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Transient inventory during uninstall; no cached equivalent exists. $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); foreach ( (array) $names as $name ) { if ( 'delete_option' === $deleter || 'delete_site_option' === $deleter ) { // Orphaned timeout rows are deleted under their full option name. $deleter( (string) $name ); continue; } $deleter( substr( (string) $name, strlen( $prefix ) ) ); } } } } if ( ! function_exists( 'aive_uninstall_delete_options' ) ) { /** * Deletes every option whose key carries the plugin's prefix. * * Covers `aive_settings` and `aive_db_version`, plus anything a later module * adds under the same prefix. Each key goes through delete_option() so the * options cache is invalidated rather than left holding deleted rows. * * @since 1.0.0 * * @return void */ function aive_uninstall_delete_options() { global $wpdb; // Escape the underscore: unescaped, "aive_%" would also match "aivex...". $like = $wpdb->esc_like( 'aive_' ) . '%'; $names = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Option inventory during uninstall; no cached equivalent exists. $wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like ) ); foreach ( (array) $names as $name ) { delete_option( (string) $name ); } } } if ( ! function_exists( 'aive_uninstall_clear_cron' ) ) { /** * Unschedules every cron event whose hook carries the plugin's prefix. * * Reads the stored cron array directly rather than through the private * _get_cron_array(), then unschedules by hook name so that duplicate or * differently-argued instances of an event are removed as well. * * @since 1.0.0 * * @return void */ function aive_uninstall_clear_cron() { $cron = get_option( 'cron' ); if ( ! is_array( $cron ) ) { return; } $hooks = array(); foreach ( $cron as $timestamp => $events ) { // The cron array carries a non-numeric "version" key alongside the schedule. if ( ! is_numeric( $timestamp ) || ! is_array( $events ) ) { continue; } foreach ( array_keys( $events ) as $hook ) { if ( 0 === strpos( (string) $hook, 'aive_' ) ) { $hooks[ (string) $hook ] = true; } } } foreach ( array_keys( $hooks ) as $hook ) { wp_unschedule_hook( $hook ); } } } aive_uninstall_clear_cron(); aive_uninstall_delete_transients(); aive_uninstall_delete_options(); aive_uninstall_drop_tables(); /* * A persistent object cache stores transients outside the options table, where * they cannot be enumerated. Flushing is the only way to guarantee no stale AIVE * value survives the uninstall. The cost is acceptable here: an object cache is * rebuildable by definition, and this runs once, on explicit user action. */ wp_cache_flush();