From aa753770566fb0dedffdc677e7438563de861534 Mon Sep 17 00:00:00 2001 From: Suraj Jangavali Date: Sat, 2 May 2026 18:37:44 +0530 Subject: [PATCH 1/3] feat: add country-based data deletion for GDPR erasure (closes #42) --- admin/views/visitors.php | 46 +++++++++++++++++++++++++++++++ includes/class-ipquery-admin.php | 47 ++++++++++++++++++++++++++++++++ includes/class-ipquery-db.php | 45 ++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/admin/views/visitors.php b/admin/views/visitors.php index e3a1bdb..63c7758 100644 --- a/admin/views/visitors.php +++ b/admin/views/visitors.php @@ -26,6 +26,12 @@ 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'] ) ) : // phpcs:ignore WordPress.Security.NonceVerification.Recommended + printf( + '

' . esc_html__( '%1$d record(s) deleted for country: %2$s.', 'ipquery' ) . '

', + (int) $_GET['country_deleted'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended + esc_html( strtoupper( sanitize_text_field( wp_unslash( $_GET['country_code'] ?? '' ) ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ); endif; ?> @@ -250,4 +256,44 @@ function ipquery_sortable_col( string $col, string $label, string $current_order + +
+

+

+
+ + + + "return confirm('" . esc_js( __( 'Are you sure? This will permanently delete ALL visitor records from the selected country. This cannot be undone.', 'ipquery' ) ) . "')", + ) + ); + ?> +
+
+ + diff --git a/includes/class-ipquery-admin.php b/includes/class-ipquery-admin.php index 398d212..c9af4ad 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,52 @@ 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 ( $deleted !== false ) { + + 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 + ) + ); + } + + wp_safe_redirect( + admin_url( + 'admin.php?page=ipquery-visitors' + . '&country_deleted=' . (int) $deleted + . '&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..ab70637 100644 --- a/includes/class-ipquery-db.php +++ b/includes/class-ipquery-db.php @@ -305,4 +305,49 @@ 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( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT DISTINCT country, country_code FROM {$table} WHERE country_code IS NOT NULL AND country_code != '' ORDER BY country ASC", + ARRAY_A + ); + return is_array( $result ) ? $result : array(); + } + } From 88bd1c616803130db994d2fee5551b54d3d9ac79 Mon Sep 17 00:00:00 2001 From: Suraj Jangavali Date: Sat, 2 May 2026 20:04:30 +0530 Subject: [PATCH 2/3] fix: use _n() for plural notice, preserve false vs 0 in redirect, move inline style to CSS --- admin/views/visitors.php | 24 +++++++++++++++++++----- assets/css/admin.css | 5 +++++ includes/class-ipquery-admin.php | 27 ++++++++++++++++----------- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/admin/views/visitors.php b/admin/views/visitors.php index 63c7758..4d4b186 100644 --- a/admin/views/visitors.php +++ b/admin/views/visitors.php @@ -26,12 +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'] ) ) : // 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__( '%1$d record(s) deleted for country: %2$s.', 'ipquery' ) . '

', - (int) $_GET['country_deleted'], // phpcs:ignore WordPress.Security.NonceVerification.Recommended - esc_html( strtoupper( sanitize_text_field( wp_unslash( $_GET['country_code'] ?? '' ) ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + '

' . esc_html( + // translators: %1$d is the number of records deleted, %2$s is the country code. + sprintf( + _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; ?> @@ -257,7 +271,7 @@ 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 c9af4ad..2a63c90 100644 --- a/includes/class-ipquery-admin.php +++ b/includes/class-ipquery-admin.php @@ -239,28 +239,33 @@ public static function handle_delete_by_country(): void { $deleted = IpQuery_DB::delete_by_country( $country_code ); - if ( $deleted !== false ) { - - 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 - ) + 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=' . (int) $deleted + . '&country_deleted=' . $deleted_param . '&country_code=' . rawurlencode( $country_code ) ) ); exit; } - /** From a11eeab90cccfac96f90f3a42cfdc76861cb3952 Mon Sep 17 00:00:00 2001 From: Guilherme Branco Stracini Date: Thu, 7 May 2026 16:53:22 +0100 Subject: [PATCH 3/3] refactor: format cleanup and minor optimizations Improve code readability by cleaning up formatting, such as adding missing colons for type hints and removing trailing spaces. Condense SQL query assignments by moving the comment to the end, making it more concise and easier to read. These changes enhance overall code maintenance and readability without altering functionality. --- admin/views/visitors.php | 2 +- includes/class-ipquery-admin.php | 1 - includes/class-ipquery-db.php | 12 ++++-------- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/admin/views/visitors.php b/admin/views/visitors.php index 4d4b186..8105e5b 100644 --- a/admin/views/visitors.php +++ b/admin/views/visitors.php @@ -31,8 +31,8 @@ $ipquery_deleted_country = esc_html( strtoupper( sanitize_text_field( wp_unslash( $_GET['country_code'] ?? '' ) ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended printf( '

' . esc_html( - // translators: %1$d is the number of records deleted, %2$s is the country code. 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.', diff --git a/includes/class-ipquery-admin.php b/includes/class-ipquery-admin.php index 2a63c90..000266a 100644 --- a/includes/class-ipquery-admin.php +++ b/includes/class-ipquery-admin.php @@ -266,7 +266,6 @@ public static function handle_delete_by_country(): void { ); exit; } - /** * Handles the bulk-purge form submission. diff --git a/includes/class-ipquery-db.php b/includes/class-ipquery-db.php index ab70637..7ad63bc 100644 --- a/includes/class-ipquery-db.php +++ b/includes/class-ipquery-db.php @@ -305,7 +305,7 @@ 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. * @@ -325,7 +325,7 @@ public static function log_action( string $message ): void { * @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 { + 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 @@ -343,11 +343,7 @@ public static function delete_by_country( string $country_code ) : int|false { public static function get_distinct_countries(): array { global $wpdb; $table = $wpdb->prefix . IPQUERY_TABLE; - $result = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.InterpolatedNotPrepared - "SELECT DISTINCT country, country_code FROM {$table} WHERE country_code IS NOT NULL AND country_code != '' ORDER BY country ASC", - ARRAY_A - ); + $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(); - } - + } }