esc_like($commentmate_notification_prefix) . '%'; $commentmate_notification_options = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall removes only plugin tables and matching options; fresh discovery ensures complete opted-in erasure. $wpdb->prepare( 'SELECT option_name FROM %i WHERE option_name LIKE %s', $wpdb->options, $commentmate_notification_pattern ) ); foreach (is_array($commentmate_notification_options) ? $commentmate_notification_options : array() as $commentmate_notification_option_name) { delete_option((string) $commentmate_notification_option_name); } } // The owner can opt out from the Advanced tab before using WordPress's normal // plugin Delete action. Keep every CommentMate record and private file when // that preference is off, so a later reinstall can recover the review data. // Missing settings deliberately default to a final purge for older installs. $commentmate_delete_data_on_uninstall = get_option('commentmate_delete_data_on_uninstall', '1'); if ('0' === (string) $commentmate_delete_data_on_uninstall) { return; } // Remove the direct dedicated capability from every local user. It was never // assigned to a role, so this cannot affect an unrelated plugin capability. if (function_exists('get_users')) { $commentmate_user_ids = get_users( array( 'fields' => 'ID', 'number' => -1, ) ); foreach ($commentmate_user_ids as $commentmate_user_id) { delete_user_meta((int) $commentmate_user_id, 'commentmate_display_name'); $commentmate_user = get_user_by('id', (int) $commentmate_user_id); if ($commentmate_user instanceof WP_User) { $commentmate_user->remove_cap('manage_commentmate'); } } } wp_clear_scheduled_hook('commentmate_cleanup_cron'); $commentmate_tables = array( 'commentmate_reviews', 'commentmate_share_tokens', 'commentmate_guest_sessions', 'commentmate_pages', 'commentmate_comments', 'commentmate_comment_messages', 'commentmate_attachments', 'commentmate_audit_events', 'commentmate_rate_limits', 'commentmate_idempotency', ); foreach ($commentmate_tables as $commentmate_suffix) { $commentmate_table = $wpdb->prefix . $commentmate_suffix; $wpdb->query($wpdb->prepare('DROP TABLE IF EXISTS %i', $commentmate_table)); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Versioned plugin schema migration or opted-in uninstall requires this table change; identifiers are bound and core object APIs cannot express it. } delete_option('commentmate_owner_user_id'); delete_option('commentmate_schema_version'); delete_option('commentmate_version'); delete_option('commentmate_storage_limit_mb'); delete_option('commentmate_delete_data_on_uninstall'); delete_option('commentmate_notification_settings'); // Remove inert metadata retained from pre-directory installations on explicit erase. delete_option('commentmate_update_metadata_cache'); delete_option('commentmate_update_high_water'); delete_option('commentmate_update_state_lock'); // Review encryption keys and one-time owner notices have dynamic option names. // Escape the literal underscores so LIKE cannot reach unrelated options. $commentmate_option_prefixes = array('commentmate_review_key_', 'commentmate_cleanup_state_', 'commentmate_cleanup_lease_', 'commentmate_export_receipt_', '_transient_commentmate_admin_notice_', '_transient_timeout_commentmate_admin_notice_', '_transient_commentmate_stopped_offer_', '_transient_timeout_commentmate_stopped_offer_'); foreach ($commentmate_option_prefixes as $commentmate_prefix) { $commentmate_pattern = $wpdb->esc_like($commentmate_prefix) . '%'; $commentmate_options = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall removes only plugin tables and matching options; fresh discovery ensures complete opted-in erasure. $wpdb->prepare( 'SELECT option_name FROM %i WHERE option_name LIKE %s', $wpdb->options, $commentmate_pattern ) ); foreach ($commentmate_options as $commentmate_option_name) { delete_option((string) $commentmate_option_name); } } // Load only storage path/ownership helpers, without booting the plugin. require_once __DIR__ . '/includes/class-private-storage.php'; // Do not traverse links. Share Private_Storage's non-webroot location logic. // A configured constant is a parent; CommentMate owns only its child. The marker is // required before recursive deletion so uninstall cannot wipe an arbitrary // non-webroot directory selected by a site owner. $commentmate_configured_storage = defined('COMMENTMATE_PRIVATE_STORAGE_DIR') ? constant('COMMENTMATE_PRIVATE_STORAGE_DIR') : ''; $commentmate_storage_install_id = get_option('commentmate_private_storage_install_id', ''); $commentmate_has_storage_install_id = is_string($commentmate_storage_install_id) && (bool) preg_match('/\A[a-f0-9]{24}\z/', $commentmate_storage_install_id); $commentmate_storage_directory = $commentmate_has_storage_install_id ? 'commentmate-private-' . $commentmate_storage_install_id : ''; $commentmate_marker_content = $commentmate_has_storage_install_id ? "CommentMate private storage\ninstall=" . $commentmate_storage_install_id . "\n" : ''; $commentmate_default_storage = $commentmate_has_storage_install_id ? dirname(untrailingslashit(\CommentMate\Private_Storage::wordpress_root())) . DIRECTORY_SEPARATOR . $commentmate_storage_directory : ''; $commentmate_tmp_storage = $commentmate_has_storage_install_id && function_exists('sys_get_temp_dir') ? untrailingslashit(sys_get_temp_dir()) . DIRECTORY_SEPARATOR . $commentmate_storage_directory : ''; $commentmate_historical_storage = get_option('commentmate_private_storage_roots', array()); if ($commentmate_has_storage_install_id && is_string($commentmate_configured_storage) && '' !== trim($commentmate_configured_storage)) { $commentmate_configured_storage = untrailingslashit(trim($commentmate_configured_storage)) . DIRECTORY_SEPARATOR . $commentmate_storage_directory; } // A site may have changed COMMENTMATE_PRIVATE_STORAGE_DIR since an earlier plugin // version used the default or temporary root. Evaluate every known dedicated // child instead of selecting the first plausible candidate, and delete only // exact marker-owned directories. This removes old encrypted data without // ever recursively touching an arbitrary configured parent. $commentmate_public_roots = \CommentMate\Private_Storage::public_roots(); $commentmate_seen_storage = array(); $commentmate_candidate_storages = $commentmate_has_storage_install_id ? array($commentmate_configured_storage, $commentmate_default_storage, $commentmate_tmp_storage) : array(); if ($commentmate_has_storage_install_id && is_array($commentmate_historical_storage)) { $commentmate_candidate_storages = array_merge($commentmate_candidate_storages, $commentmate_historical_storage); } foreach ($commentmate_candidate_storages as $commentmate_candidate_storage) { if (!is_string($commentmate_candidate_storage) || '' === trim($commentmate_candidate_storage)) { continue; } $commentmate_candidate_storage = untrailingslashit(trim($commentmate_candidate_storage)); if (basename($commentmate_candidate_storage) !== $commentmate_storage_directory) { continue; } if (DIRECTORY_SEPARATOR !== substr($commentmate_candidate_storage, 0, 1) && !preg_match('/\A[A-Za-z]:[\\\\\/]/', $commentmate_candidate_storage)) { continue; } $commentmate_candidate_parent = realpath(dirname($commentmate_candidate_storage)); if (false === $commentmate_candidate_parent) { continue; } $commentmate_candidate_path = $commentmate_candidate_parent . DIRECTORY_SEPARATOR . basename($commentmate_candidate_storage); $commentmate_is_public_path = false; foreach ($commentmate_public_roots as $commentmate_public_root) { if (DIRECTORY_SEPARATOR === $commentmate_public_root || $commentmate_candidate_path === $commentmate_public_root || 0 === strpos($commentmate_candidate_path, $commentmate_public_root . DIRECTORY_SEPARATOR)) { $commentmate_is_public_path = true; break; } } if ($commentmate_is_public_path || isset($commentmate_seen_storage[$commentmate_candidate_path])) { continue; } $commentmate_seen_storage[$commentmate_candidate_path] = true; $commentmate_storage_parent = realpath(dirname($commentmate_candidate_path)); $commentmate_storage_basename = basename($commentmate_candidate_path); $commentmate_marker = $commentmate_candidate_path . DIRECTORY_SEPARATOR . '.commentmate-storage'; if (!is_dir($commentmate_candidate_path) || is_link($commentmate_candidate_path) || !is_file($commentmate_marker) || is_link($commentmate_marker) || !hash_equals($commentmate_marker_content, (string) file_get_contents($commentmate_marker))) { continue; } $commentmate_real_storage = realpath($commentmate_candidate_path); if (false === $commentmate_storage_parent || false === $commentmate_real_storage || dirname($commentmate_real_storage) !== $commentmate_storage_parent || $commentmate_storage_basename !== basename($commentmate_real_storage)) { continue; } try { $commentmate_iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($commentmate_real_storage, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST ); foreach ($commentmate_iterator as $commentmate_item) { /** @var SplFileInfo $commentmate_item */ if ($commentmate_item->isLink() || $commentmate_item->isFile()) { @unlink($commentmate_item->getPathname()); // phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink -- verified marker-owned child path. } elseif ($commentmate_item->isDir()) { @rmdir($commentmate_item->getPathname()); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- verified marker-owned child path. } } @rmdir($commentmate_real_storage); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- verified exact marker-owned storage root. } catch (UnexpectedValueException $commentmate_exception) { // Best effort only: remaining files are inaccessible random-name review // storage and can be removed manually if the filesystem is unavailable. } } delete_option('commentmate_private_storage_roots'); delete_option('commentmate_private_storage_install_id');