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). [](https://github.com/guibranco/ipquery-wordpress/actions/workflows/pages.yml) -[](https://github.com/guibranco/ipquery-wordpress/releases) +[](https://github.com/guibranco/ipquery-wordpress/releases) [](https://wordpress.org) [](https://www.php.net) [](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