getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- operational failure diagnostic: deliberately loud so a partial uninstall reaches server logs without WP_DEBUG } } } /** D2: the opt-in, read raw — no settings abstraction at uninstall time. */ public static function delete_all_requested() { return (bool) get_option(self::TOGGLE_OPTION, false); } /** D5.1 — enumerated secrets, deleted by name. */ private static function purge_secrets() { foreach (self::SECRET_OPTIONS as $option) { delete_option($option); } } /** * D5.2 — every plugin transient, including session-keyed rows * (answerrails_consent_state_{id}) that only prefix SQL can catch. * Both value and timeout rows, both transient flavours. */ private static function purge_transients() { global $wpdb; $prefixes = array( '_transient_' . self::OPTION_PREFIX, '_transient_timeout_' . self::OPTION_PREFIX, '_site_transient_' . self::OPTION_PREFIX, '_site_transient_timeout_' . self::OPTION_PREFIX, ); foreach ($prefixes as $prefix) { $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like($prefix) . '%' ) ); } } /** D5.3 — all schedules, recurring and single. */ private static function clear_cron() { foreach (self::CRON_HOOKS as $hook) { wp_clear_scheduled_hook($hook); } foreach (self::ARG_CRON_HOOKS as $hook) { wp_unschedule_hook($hook); } } /** * D6.2 — plugin CPT posts + their postmeta, batched. Direct $wpdb * on purpose: wp_delete_post() fires hooks and rebuilds caches per * post, which is both pointless and slow while the plugin is being * removed. */ private static function delete_content() { global $wpdb; // #331 D5: the retired names are swept alongside the current ones, // for an install deleted before migration 1.12.0 ever ran. $types = array_merge(self::POST_TYPES, self::LEGACY_POST_TYPES); $placeholders = implode(',', array_fill(0, count($types), '%s')); do { // phpcs:disable WordPress.Security, WordPress.DB -- uninstall-time direct deletion; no user input beyond the fixed CPT manifest (placeholders generated from it; IDs intval'd). $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type IN ($placeholders) LIMIT " . self::DELETE_BATCH, $types ) ); if (empty($ids)) { break; } $id_list = implode(',', array_map('intval', $ids)); $wpdb->query("DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($id_list)"); $wpdb->query("DELETE FROM {$wpdb->posts} WHERE ID IN ($id_list)"); // phpcs:enable WordPress.Security } while (count($ids) === self::DELETE_BATCH); } /** D6.3 — the chunks table (migration 1.3.0). */ private static function drop_table() { global $wpdb; $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}" . self::TABLE); // phpcs:ignore WordPress.Security, WordPress.DB -- fixed table name from the pinned manifest; uninstall-time DDL. } /** * D6.4 — every remaining plugin option, by prefix. Sweeps the * literal keys, the dynamic families, the orphans (#155's * answerrails_logging_mode class) and, last of all, the toggle row. * fs_* rows do not match the prefix and are untouched (D3). */ private static function delete_options() { global $wpdb; $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like(self::OPTION_PREFIX) . '%' ) ); wp_cache_flush(); } }