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 + +
+

+

+
+ + + + "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/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(); + } }