diff --git a/README.md b/README.md index 53d88ef..e707937 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A WordPress plugin that enriches visitor IP addresses with real-time geolocation, ISP data, and risk intelligence โ€” powered by the [IpQuery API](https://ipquery.io) via [guibranco/ipquery-php](https://github.com/guibranco/ipquery-php). [![Build](https://img.shields.io/github/actions/workflow/status/guibranco/ipquery-wordpress/pages.yml?label=docs&style=flat-square)](https://github.com/guibranco/ipquery-wordpress/actions/workflows/pages.yml) -[![Version](https://img.shields.io/badge/version-1.0.0-blue?style=flat-square)](https://github.com/guibranco/ipquery-wordpress/releases) +[![Version](https://img.shields.io/badge/version-1.2.0-blue?style=flat-square)](https://github.com/guibranco/ipquery-wordpress/releases) [![WordPress](https://img.shields.io/badge/WordPress-%E2%89%A56.0-21759B?style=flat-square&logo=wordpress)](https://wordpress.org) [![PHP](https://img.shields.io/badge/PHP-%E2%89%A58.2-777BB4?style=flat-square&logo=php)](https://www.php.net) [![License](https://img.shields.io/badge/license-MIT-green?style=flat-square)](LICENSE) @@ -32,6 +32,7 @@ A WordPress plugin that enriches visitor IP addresses with real-time geolocation | ๐Ÿ—„๏ธ | **Smart caching** | WordPress transients cache each IP for 1 hour; only one API call per IP per hour | | ๐Ÿงน | **Auto-retention** | Configurable data retention with daily WP-Cron cleanup | | ๐Ÿ”’ | **Privacy controls** | Exclude IPs, skip logged-in users or admins, disable tracking at any time | +| ๐Ÿ“ฅ | **CSV export** | Download all visitor data (or the current filtered view) as a UTF-8 CSV file | --- diff --git a/admin/views/visitors.php b/admin/views/visitors.php index 8105e5b..003ef22 100644 --- a/admin/views/visitors.php +++ b/admin/views/visitors.php @@ -91,6 +91,15 @@ class="regular-text"> + + +
+ + + + + +
$search, + 'risk_filter' => $risk_filter, + ) + ); + $filename = 'ipquery-visitors-' . gmdate( 'Y-m-d' ) . '.csv'; + + header( 'Content-Type: text/csv; charset=utf-8' ); + header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); + header( 'Pragma: no-cache' ); + header( 'Expires: 0' ); + + $output = fopen( 'php://output', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen + + // UTF-8 BOM for Excel compatibility. + fwrite( $output, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite + + fputcsv( // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv + $output, + array( + 'IP', + 'Country', + 'Country Code', + 'City', + 'State', + 'Zipcode', + 'Latitude', + 'Longitude', + 'Timezone', + 'ASN', + 'Org', + 'ISP', + 'Is Mobile', + 'Is VPN', + 'Is Tor', + 'Is Proxy', + 'Is Datacenter', + 'Risk Score', + 'First Seen', + 'Last Seen', + 'Visit Count', + ) + ); + + foreach ( $rows as $row ) { + fputcsv( // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fputcsv + $output, + array( + $row['ip'], + $row['country'] ?? '', + $row['country_code'] ?? '', + $row['city'] ?? '', + $row['state'] ?? '', + $row['zipcode'] ?? '', + $row['latitude'] ?? '', + $row['longitude'] ?? '', + $row['timezone'] ?? '', + $row['asn'] ?? '', + $row['org'] ?? '', + $row['isp'] ?? '', + $row['is_mobile'] ? 'Yes' : 'No', + $row['is_vpn'] ? 'Yes' : 'No', + $row['is_tor'] ? 'Yes' : 'No', + $row['is_proxy'] ? 'Yes' : 'No', + $row['is_datacenter'] ? 'Yes' : 'No', + $row['risk_score'] ?? 0, + $row['first_seen'] ?? '', + $row['last_seen'] ?? '', + $row['visit_count'] ?? 0, + ) + ); + } + + fclose( $output ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose + exit; + } + // ------------------------------------------------------------------------- // AJAX endpoints. // ------------------------------------------------------------------------- diff --git a/includes/class-ipquery-db.php b/includes/class-ipquery-db.php index 7ad63bc..706a35d 100644 --- a/includes/class-ipquery-db.php +++ b/includes/class-ipquery-db.php @@ -346,4 +346,59 @@ public static function get_distinct_countries(): array { $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(); } + + /** + * Returns all visitor rows matching the given filters, with no pagination limit, for CSV export. + * + * @param array $args Query arguments (orderby, order, search, country_code, risk_filter). + * @return array> + */ + public static function get_all_for_export( array $args = array() ): array { + global $wpdb; + $table = $wpdb->prefix . IPQUERY_TABLE; + + $defaults = array( + 'orderby' => 'last_seen', + 'order' => 'DESC', + 'search' => '', + 'country_code' => '', + 'risk_filter' => '', + ); + $args = wp_parse_args( $args, $defaults ); + + $where = array(); + $values = array(); + + if ( ! empty( $args['search'] ) ) { + $like = '%' . $wpdb->esc_like( $args['search'] ) . '%'; + $where[] = '(ip LIKE %s OR city LIKE %s OR country LIKE %s OR isp LIKE %s)'; + $values = array_merge( $values, array( $like, $like, $like, $like ) ); + } + if ( ! empty( $args['country_code'] ) ) { + $where[] = 'country_code = %s'; + $values[] = $args['country_code']; + } + if ( ! empty( $args['risk_filter'] ) ) { + $allowed_flags = array( 'is_vpn', 'is_proxy', 'is_tor', 'is_datacenter', 'is_mobile' ); + if ( in_array( $args['risk_filter'], $allowed_flags, true ) ) { + $where[] = $args['risk_filter'] . ' = 1'; + } + } + + $where_sql = $where ? 'WHERE ' . implode( ' AND ', $where ) : ''; + + $allowed_orderby = array( 'last_seen', 'first_seen', 'visit_count', 'country', 'risk_score', 'ip' ); + $orderby = in_array( $args['orderby'], $allowed_orderby, true ) ? $args['orderby'] : 'last_seen'; + $order = 'ASC' === strtoupper( $args['order'] ) ? 'ASC' : 'DESC'; + + $sql = "SELECT * FROM {$table} {$where_sql} ORDER BY {$orderby} {$order}"; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + if ( $values ) { + $rows = $wpdb->get_results( $wpdb->prepare( $sql, ...$values ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,PluginCheck.Security.DirectDB.UnescapedDBParameter + } else { + $rows = $wpdb->get_results( $sql, ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared + } + + return is_array( $rows ) ? $rows : array(); + } } diff --git a/ipquery.php b/ipquery.php index 53d2ff0..326849f 100644 --- a/ipquery.php +++ b/ipquery.php @@ -3,7 +3,7 @@ * Plugin Name: IpQuery * Plugin URI: https://guilherme.stracini.com.br/ipquery-wordpress/ * Description: Track and analyse visitor IP data using the IpQuery API (via guibranco/ipquery-php). Displays location maps, traffic heatmaps, and VPN/proxy statistics. - * Version: 1.1.4 + * Version: 1.2.0 * Requires at least: 6.0 * Requires PHP: 8.2 * Author: Guilherme Branco Stracini @@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit; -define( 'IPQUERY_VERSION', '1.1.4' ); +define( 'IPQUERY_VERSION', '1.2.0' ); define( 'IPQUERY_FILE', __FILE__ ); define( 'IPQUERY_DIR', plugin_dir_path( __FILE__ ) ); define( 'IPQUERY_URL', plugin_dir_url( __FILE__ ) ); diff --git a/readme.txt b/readme.txt index 7bffa87..beed482 100644 --- a/readme.txt +++ b/readme.txt @@ -4,7 +4,7 @@ Tags: ip, geolocation, analytics, security, heatmap Requires at least: 6.0 Tested up to: 6.9 Requires PHP: 8.2 -Stable tag: 1.1.4 +Stable tag: 1.2.0 License: MIT License URI: https://opensource.org/licenses/MIT @@ -27,6 +27,7 @@ IpQuery enriches every visitor's IP address with real-time geolocation, ISP data * **Auto-retention** โ€” Configurable data retention with daily WP-Cron cleanup * **Privacy controls** โ€” Exclude IPs, skip logged-in users or admins, disable tracking at any time * **GDPR erasure tools** โ€” Delete individual visitor records or bulk-erase all data for a specific country +* **CSV export** โ€” Download all visitor data (or the current filtered view) as a UTF-8 CSV file == Installation == @@ -70,6 +71,11 @@ All data is stored in your own WordPress database in the `wp_ipquery_visitors` t == Changelog == += 1.2.0 = +* Added CSV export โ€” download all visitor records (or the current filtered view) directly from the Visitors screen +* Export honours active search and risk-type filters so you can export exactly the subset you need +* Exported CSV is UTF-8 with BOM for seamless Excel compatibility; filename includes today's date + = 1.1.1 = * Maintenance and bug fixes @@ -85,6 +91,9 @@ All data is stored in your own WordPress database in the `wp_ipquery_visitors` t == Upgrade Notice == += 1.2.0 = +Adds CSV export of visitor data from the Visitors screen. No database changes โ€” safe to upgrade. + = 1.1.1 = Maintenance release. Safe to upgrade.