diff --git a/admin/views/visitors.php b/admin/views/visitors.php
index e3a1bdb..8105e5b 100644
--- a/admin/views/visitors.php
+++ b/admin/views/visitors.php
@@ -26,6 +26,26 @@
elseif ( isset( $_GET['lookup_error'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// translators: %s is the error message returned by the lookup.
echo '
' . esc_html( sprintf( __( 'Lookup error: %s', 'ipquery' ), urldecode( sanitize_text_field( wp_unslash( $_GET['lookup_error'] ) ) ) ) ) . '
'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ elseif ( isset( $_GET['country_deleted'] ) && 'false' !== $_GET['country_deleted'] ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $ipquery_deleted_count = (int) $_GET['country_deleted']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $ipquery_deleted_country = esc_html( strtoupper( sanitize_text_field( wp_unslash( $_GET['country_code'] ?? '' ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ printf(
+ '' . esc_html(
+ sprintf(
+ // translators: %1$d is the number of records deleted, %2$s is the country code.
+ _n(
+ '%1$d record deleted for country: %2$s.',
+ '%1$d records deleted for country: %2$s.',
+ $ipquery_deleted_count,
+ 'ipquery'
+ ),
+ $ipquery_deleted_count,
+ $ipquery_deleted_country
+ )
+ ) . '
'
+ );
+ elseif ( isset( $_GET['country_delete_error'] ) || ( isset( $_GET['country_deleted'] ) && 'false' === $_GET['country_deleted'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ echo '' . esc_html__( 'Failed to delete records. Please try again.', 'ipquery' ) . '
';
endif;
?>
@@ -250,4 +270,44 @@ function ipquery_sortable_col( string $col, string $label, string $current_order
+
+
+
+
diff --git a/assets/css/admin.css b/assets/css/admin.css
index 4e32c1a..9c38ef9 100644
--- a/assets/css/admin.css
+++ b/assets/css/admin.css
@@ -232,3 +232,8 @@
flex-direction: column;
}
}
+
+
+.ipquery-panel--spaced {
+ margin-top: 24px;
+}
\ No newline at end of file
diff --git a/includes/class-ipquery-admin.php b/includes/class-ipquery-admin.php
index 398d212..000266a 100644
--- a/includes/class-ipquery-admin.php
+++ b/includes/class-ipquery-admin.php
@@ -25,6 +25,7 @@ public static function init(): void {
add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_assets' ) );
add_action( 'admin_init', array( self::class, 'handle_settings_save' ) );
add_action( 'admin_post_ipquery_delete_ip', array( self::class, 'handle_delete_ip' ) );
+ add_action( 'admin_post_ipquery_delete_by_country', array( self::class, 'handle_delete_by_country' ) );
add_action( 'admin_post_ipquery_purge', array( self::class, 'handle_purge' ) );
add_action( 'admin_post_ipquery_lookup', array( self::class, 'handle_manual_lookup' ) );
add_action( 'wp_ajax_ipquery_chart_data', array( self::class, 'ajax_chart_data' ) );
@@ -216,6 +217,56 @@ public static function handle_delete_ip(): void {
exit;
}
+ /**
+ * Handles the delete-by-country form submission.
+ *
+ * @return void
+ */
+ public static function handle_delete_by_country(): void {
+ check_admin_referer( 'ipquery_delete_by_country' );
+ if ( ! current_user_can( 'manage_options' ) ) {
+ wp_die( esc_html__( 'Not allowed.', 'ipquery' ) );
+ }
+
+ $country_code = strtoupper(
+ sanitize_text_field( wp_unslash( $_POST['country_code'] ?? '' ) )
+ );
+
+ if ( empty( $country_code ) ) {
+ wp_safe_redirect( admin_url( 'admin.php?page=ipquery-visitors' ) );
+ exit;
+ }
+
+ $deleted = IpQuery_DB::delete_by_country( $country_code );
+
+ if ( false === $deleted ) {
+ wp_safe_redirect(
+ admin_url( 'admin.php?page=ipquery-visitors&country_delete_error=1' )
+ );
+ exit;
+ }
+
+ IpQuery_DB::log_action(
+ sprintf(
+ 'Admin "%s" deleted %d visitor record(s) for country: %s',
+ wp_get_current_user()->user_login,
+ (int) $deleted,
+ $country_code
+ )
+ );
+
+ $deleted_param = (string) (int) $deleted;
+
+ wp_safe_redirect(
+ admin_url(
+ 'admin.php?page=ipquery-visitors'
+ . '&country_deleted=' . $deleted_param
+ . '&country_code=' . rawurlencode( $country_code )
+ )
+ );
+ exit;
+ }
+
/**
* Handles the bulk-purge form submission.
*
diff --git a/includes/class-ipquery-db.php b/includes/class-ipquery-db.php
index 8c707d8..7ad63bc 100644
--- a/includes/class-ipquery-db.php
+++ b/includes/class-ipquery-db.php
@@ -305,4 +305,45 @@ public static function delete_ip( string $ip ): void {
$table = $wpdb->prefix . IPQUERY_TABLE;
$wpdb->delete( $table, array( 'ip' => $ip ), array( '%s' ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
}
+
+ /**
+ * Writes an audit message to the WordPress debug log when WP_DEBUG_LOG is enabled.
+ *
+ * @param string $message The message to log.
+ * @return void
+ */
+ public static function log_action( string $message ): void {
+ if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
+ error_log( '[IpQuery] ' . $message );
+ }
+ }
+
+ /**
+ * Delete all visitor records for a given country code.
+ *
+ * @param string $country_code Two-letter ISO country code (e.g. 'DE').
+ * @return int|false Number of rows deleted, or false on error.
+ */
+ public static function delete_by_country( string $country_code ): int|false {
+ global $wpdb;
+ $table = $wpdb->prefix . IPQUERY_TABLE;
+ return $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
+ $table,
+ array( 'country_code' => sanitize_text_field( $country_code ) ),
+ array( '%s' )
+ );
+ }
+
+ /**
+ * Returns distinct countries that have visitor records, for the deletion dropdown.
+ *
+ * @return array
+ */
+ public static function get_distinct_countries(): array {
+ global $wpdb;
+ $table = $wpdb->prefix . IPQUERY_TABLE;
+ $result = $wpdb->get_results( "SELECT DISTINCT country, country_code FROM {$table} WHERE country_code IS NOT NULL AND country_code != '' ORDER BY country ASC", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared
+ return is_array( $result ) ? $result : array();
+ }
}