diff --git a/admin/partials/detections-page.php b/admin/partials/detections-page.php index bc81f07..f2136c4 100644 --- a/admin/partials/detections-page.php +++ b/admin/partials/detections-page.php @@ -32,14 +32,14 @@ $range = isset($_GET['range']) ? sanitize_text_field($_GET['range']) : ''; if ($range === 'today') { - $date_from = date('Y-m-d'); - $date_to = date('Y-m-d'); + $date_from = gmdate('Y-m-d'); + $date_to = gmdate('Y-m-d'); } elseif ($range === '7d') { - $date_from = date('Y-m-d', strtotime('-7 days')); - $date_to = date('Y-m-d'); + $date_from = gmdate('Y-m-d', strtotime('-7 days')); + $date_to = gmdate('Y-m-d'); } elseif ($range === '30d') { - $date_from = date('Y-m-d', strtotime('-30 days')); - $date_to = date('Y-m-d'); + $date_from = gmdate('Y-m-d', strtotime('-30 days')); + $date_to = gmdate('Y-m-d'); } // Build query with always using prepare() for safety @@ -70,7 +70,7 @@ // Handle CSV export if (isset($_GET['action']) && $_GET['action'] === 'export_csv' && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'webdecoy_export')) { header('Content-Type: text/csv; charset=utf-8'); - header('Content-Disposition: attachment; filename="webdecoy-detections-' . date('Y-m-d') . '.csv"'); + header('Content-Disposition: attachment; filename="webdecoy-detections-' . gmdate('Y-m-d') . '.csv"'); // Neutralize CSV/spreadsheet formula injection: fields like user_agent and // flags come from untrusted request data, so a value beginning with =, +, -, @@ -103,7 +103,7 @@ ]); } - fclose($output); + fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- streaming CSV to php://output for download, not filesystem access exit; } diff --git a/admin/partials/settings-page.php b/admin/partials/settings-page.php index 3a1fa5f..7f21169 100644 --- a/admin/partials/settings-page.php +++ b/admin/partials/settings-page.php @@ -768,6 +768,7 @@ class="regular-text" autocomplete="off" />

diff --git a/admin/partials/statistics-page.php b/admin/partials/statistics-page.php index cedde41..fb97c66 100644 --- a/admin/partials/statistics-page.php +++ b/admin/partials/statistics-page.php @@ -21,7 +21,7 @@ $checkout_table = $wpdb->prefix . 'webdecoy_checkout_attempts'; // 30-day detection trend -$thirty_days_ago = date('Y-m-d H:i:s', strtotime('-30 days')); +$thirty_days_ago = gmdate('Y-m-d H:i:s', strtotime('-30 days')); $daily_counts = $wpdb->get_results($wpdb->prepare( "SELECT DATE(created_at) as date, COUNT(*) as count FROM {$detections_table} WHERE created_at > %s GROUP BY DATE(created_at) ORDER BY date ASC", $thirty_days_ago @@ -30,7 +30,7 @@ // Fill in missing days with 0 $daily_data = []; for ($i = 29; $i >= 0; $i--) { - $date = date('Y-m-d', strtotime("-{$i} days")); + $date = gmdate('Y-m-d', strtotime("-{$i} days")); $daily_data[$date] = 0; } foreach ($daily_counts as $row) { @@ -92,11 +92,11 @@ $total_30d = array_sum($daily_data); $total_7d = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s", - date('Y-m-d H:i:s', strtotime('-7 days')) + gmdate('Y-m-d H:i:s', strtotime('-7 days')) )); $total_today = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$detections_table} WHERE created_at > %s", - date('Y-m-d 00:00:00') + gmdate('Y-m-d 00:00:00') )); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- static query, no user input $active_blocks = $wpdb->get_var( diff --git a/includes/class-webdecoy-blocker.php b/includes/class-webdecoy-blocker.php index 5c8d6c1..2cf4b83 100644 --- a/includes/class-webdecoy-blocker.php +++ b/includes/class-webdecoy-blocker.php @@ -58,7 +58,7 @@ public function block(string $ip, string $reason = '', ?int $duration_hours = nu $expires_at = null; if ($duration_hours !== null && $duration_hours > 0) { - $expires_at = date('Y-m-d H:i:s', strtotime("+{$duration_hours} hours")); + $expires_at = gmdate('Y-m-d H:i:s', strtotime("+{$duration_hours} hours")); } // Check if already blocked @@ -273,7 +273,7 @@ public function extend_block(string $ip, int $hours): bool $new_expires = null; if ($info['expires_at']) { - $new_expires = date('Y-m-d H:i:s', strtotime($info['expires_at'] . " +{$hours} hours")); + $new_expires = gmdate('Y-m-d H:i:s', strtotime($info['expires_at'] . " +{$hours} hours")); } $result = $wpdb->update( @@ -300,8 +300,10 @@ public function clear_all(): int $count = $wpdb->query("DELETE FROM {$table}"); - // Clear all cache - wp_cache_flush_group('webdecoy'); + // Clear all cache (wp_cache_flush_group() requires WP 6.1+) + if (function_exists('wp_cache_flush_group')) { + wp_cache_flush_group('webdecoy'); + } return $count; } @@ -598,7 +600,7 @@ public function get_stats(): array // Blocks in last 24 hours $recent = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE blocked_at > %s", - date('Y-m-d H:i:s', strtotime('-24 hours')) + gmdate('Y-m-d H:i:s', strtotime('-24 hours')) )); return [ diff --git a/includes/class-webdecoy-decoy-response.php b/includes/class-webdecoy-decoy-response.php index 0ec2c0e..a8666bb 100644 --- a/includes/class-webdecoy-decoy-response.php +++ b/includes/class-webdecoy-decoy-response.php @@ -245,7 +245,7 @@ private function serve_tarpit(): void $start = time(); $i = 0; while ((time() - $start) < $budget) { - echo '\n"; + echo '\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- template-generated decoy content, contains no user input if (function_exists('flush')) { @flush(); // phpcs:ignore } diff --git a/includes/class-webdecoy-detector.php b/includes/class-webdecoy-detector.php index e5810c7..02dd1c0 100644 --- a/includes/class-webdecoy-detector.php +++ b/includes/class-webdecoy-detector.php @@ -275,7 +275,7 @@ public function get_stats(int $days = 7): array global $wpdb; $table = $wpdb->prefix . 'webdecoy_detections'; - $since = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); $total = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE created_at > %s", @@ -317,7 +317,7 @@ public function get_unique_ips(int $days = 7): int global $wpdb; $table = $wpdb->prefix . 'webdecoy_detections'; - $since = date('Y-m-d H:i:s', strtotime("-{$days} days")); + $since = gmdate('Y-m-d H:i:s', strtotime("-{$days} days")); return (int) $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT ip_address) FROM {$table} WHERE created_at > %s", diff --git a/includes/class-webdecoy-rate-limiter.php b/includes/class-webdecoy-rate-limiter.php index b21ed33..7e9bb18 100644 --- a/includes/class-webdecoy-rate-limiter.php +++ b/includes/class-webdecoy-rate-limiter.php @@ -85,8 +85,8 @@ public function check_and_increment(string $key): array global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $now = current_time('mysql'); - $window_start_threshold = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); + $now = current_time('mysql', true); + $window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $existing = $wpdb->get_row($wpdb->prepare( "SELECT request_count, window_start FROM {$table} WHERE ip_address = %s AND window_start > %s", @@ -127,8 +127,8 @@ public function increment(string $ip): int global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $now = current_time('mysql'); - $window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); + $now = current_time('mysql', true); + $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); // Check for existing record in current window $existing = $wpdb->get_row($wpdb->prepare( @@ -169,7 +169,7 @@ public function get_count(string $ip): int global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); + $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $count = $wpdb->get_var($wpdb->prepare( "SELECT request_count FROM {$table} WHERE ip_address = %s AND window_start > %s", @@ -203,7 +203,7 @@ public function get_reset_time(string $ip): int global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $window_start_threshold = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); + $window_start_threshold = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $window_start = $wpdb->get_var($wpdb->prepare( "SELECT window_start FROM {$table} WHERE ip_address = %s AND window_start > %s", @@ -246,7 +246,7 @@ public function cleanup(): int global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $threshold = date('Y-m-d H:i:s', strtotime('-1 hour')); + $threshold = gmdate('Y-m-d H:i:s', strtotime('-1 hour')); return $wpdb->query($wpdb->prepare( "DELETE FROM {$table} WHERE window_start < %s", @@ -339,7 +339,7 @@ public function get_stats(): array global $wpdb; $table = $wpdb->prefix . 'webdecoy_rate_limits'; - $window_start = date('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); + $window_start = gmdate('Y-m-d H:i:s', strtotime("-{$this->window} seconds")); $active_ips = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT ip_address) FROM {$table} WHERE window_start > %s", diff --git a/includes/class-webdecoy-woocommerce.php b/includes/class-webdecoy-woocommerce.php index 81b3686..ac1d72b 100644 --- a/includes/class-webdecoy-woocommerce.php +++ b/includes/class-webdecoy-woocommerce.php @@ -671,7 +671,7 @@ function ($order, $request) { if ($blocker->is_blocked($ip)) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'webdecoy_blocked', - __('Your checkout has been blocked due to suspicious activity.', 'webdecoy'), + esc_html(__('Your checkout has been blocked due to suspicious activity.', 'webdecoy')), 403 ); } @@ -686,7 +686,7 @@ function ($order, $request) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'webdecoy_velocity', - __('Too many checkout attempts. Please try again later.', 'webdecoy'), + esc_html(__('Too many checkout attempts. Please try again later.', 'webdecoy')), 429 ); } @@ -701,7 +701,7 @@ function ($order, $request) { throw new \Automattic\WooCommerce\StoreApi\Exceptions\RouteException( 'webdecoy_carding', - __('Suspicious checkout activity detected.', 'webdecoy'), + esc_html(__('Suspicious checkout activity detected.', 'webdecoy')), 403 ); } diff --git a/readme.txt b/readme.txt index 6e97a50..40157d4 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ Contributors: webdecoy Donate link: https://webdecoy.com Tags: security, bot detection, spam protection, woocommerce, firewall -Requires at least: 5.6 -Tested up to: 6.8 +Requires at least: 6.1 +Tested up to: 7.0 Stable tag: 2.2.1 Requires PHP: 7.4 License: GPLv2 or later diff --git a/sdk/src/BotDetector.php b/sdk/src/BotDetector.php index 12863e0..d38d889 100644 --- a/sdk/src/BotDetector.php +++ b/sdk/src/BotDetector.php @@ -457,7 +457,7 @@ public function analyzePath(string $path): array // Extract path from URL if full URL provided if (strpos($path, '://') !== false) { - $parsed = parse_url($path); + $parsed = function_exists('wp_parse_url') ? wp_parse_url($path) : parse_url($path); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- guarded fallback for non-WordPress (standalone SDK) use $path = ($parsed['path'] ?? '/') . (isset($parsed['query']) ? '?' . $parsed['query'] : ''); } diff --git a/sdk/src/Client.php b/sdk/src/Client.php index 095dbbc..0a7da0e 100644 --- a/sdk/src/Client.php +++ b/sdk/src/Client.php @@ -308,7 +308,7 @@ public function testConnection(): bool return true; } catch (WebDecoyException $e) { if ($e->getCode() === 401 || $e->getCode() === 403) { - throw new WebDecoyException('Invalid API key or insufficient permissions', $e->getCode()); + throw new WebDecoyException('Invalid API key or insufficient permissions', $e->getCode()); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } throw $e; } @@ -328,7 +328,7 @@ public function getOrganizationId(): string /** * Make an HTTP request to the API * - * Uses WordPress HTTP API when available, falls back to cURL otherwise. + * Uses the WordPress HTTP API, which is always available inside WordPress. * * @param string $method HTTP method * @param string $endpoint API endpoint @@ -349,13 +349,12 @@ private function request(string $method, string $endpoint, array $data = [], boo $data = []; } - // Use WordPress HTTP API if available (preferred for WP plugins) - if (function_exists('wp_remote_request')) { - return $this->requestWithWordPress($method, $url, $data, $authenticated); + // Use the WordPress HTTP API (always available inside WordPress). + if (!function_exists('wp_remote_request')) { + throw new WebDecoyException('WordPress HTTP API is unavailable', 0); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } - // Fall back to cURL for non-WordPress environments - return $this->requestWithCurl($method, $url, $data, $authenticated); + return $this->requestWithWordPress($method, $url, $data, $authenticated); } /** @@ -394,7 +393,7 @@ private function requestWithWordPress(string $method, string $url, array $data, $response = wp_remote_request($url, $args); if (is_wp_error($response)) { - throw new WebDecoyException('API request failed: ' . $response->get_error_message(), 0); + throw new WebDecoyException('API request failed: ' . $response->get_error_message(), 0); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } $httpCode = wp_remote_retrieve_response_code($response); @@ -403,74 +402,7 @@ private function requestWithWordPress(string $method, string $url, array $data, if ($httpCode >= 400) { $message = $decoded['error']['message'] ?? $decoded['message'] ?? 'Unknown error'; - throw new WebDecoyException("API error: {$message}", $httpCode); - } - - return $decoded ?? []; - } - - /** - * Make HTTP request using cURL (fallback for non-WordPress environments) - * - * @param string $method HTTP method - * @param string $url Full URL - * @param array $data Request data - * @param bool $authenticated Whether to include API key auth - * @return array Decoded response - * @throws WebDecoyException On error - */ - private function requestWithCurl(string $method, string $url, array $data, bool $authenticated): array - { - $headers = [ - 'Content-Type: application/json', - 'Accept: application/json', - 'User-Agent: ' . self::userAgent(), - ]; - - if ($authenticated) { - $headers[] = 'Authorization: Bearer ' . $this->apiKey; - } - - $ch = curl_init(); - - curl_setopt_array($ch, [ - CURLOPT_URL => $url, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => $this->timeout, - CURLOPT_HTTPHEADER => $headers, - CURLOPT_SSL_VERIFYPEER => $this->verifySsl, - CURLOPT_SSL_VERIFYHOST => $this->verifySsl ? 2 : 0, - ]); - - switch ($method) { - case 'POST': - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - break; - case 'PUT': - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT'); - curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); - break; - case 'DELETE': - curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE'); - break; - } - - $response = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $error = curl_error($ch); - - curl_close($ch); - - if ($error) { - throw new WebDecoyException("API request failed: {$error}", 0); - } - - $decoded = json_decode($response, true); - - if ($httpCode >= 400) { - $message = $decoded['error']['message'] ?? $decoded['message'] ?? 'Unknown error'; - throw new WebDecoyException("API error: {$message}", $httpCode); + throw new WebDecoyException("API error: {$message}", $httpCode); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } return $decoded ?? []; diff --git a/sdk/src/GoodBotList.php b/sdk/src/GoodBotList.php index 7815b6e..cc41cd3 100644 --- a/sdk/src/GoodBotList.php +++ b/sdk/src/GoodBotList.php @@ -4,6 +4,10 @@ namespace WebDecoy; +if (!defined('ABSPATH')) { + exit; +} + // PHP 7.4 polyfill for str_ends_with (available in PHP 8.0+) // Kept here for non-WordPress contexts where webdecoy.php is not loaded if (!function_exists('str_ends_with')) { @@ -692,7 +696,9 @@ private function performReverseDNSVerification(string $ip, array $expectedSuffix // Step 2: Check if hostname ends with expected suffix $matchesSuffix = false; foreach ($expectedSuffixes as $suffix) { - if (str_ends_with($hostname, strtolower($suffix))) { + $suffixLower = strtolower($suffix); + $suffixLen = strlen($suffixLower); + if ($suffixLen === 0 || substr($hostname, -$suffixLen) === $suffixLower) { $matchesSuffix = true; break; } diff --git a/sdk/src/Rules/Filter/Parser.php b/sdk/src/Rules/Filter/Parser.php index dcea22e..012aa76 100644 --- a/sdk/src/Rules/Filter/Parser.php +++ b/sdk/src/Rules/Filter/Parser.php @@ -178,7 +178,7 @@ private function primary(): array $path = [$this->advance()['value']]; while ($this->match(TokenType::DOT)) { if (!$this->check(TokenType::IDENT)) { - throw new FilterSyntaxException('Expected identifier after "." at position ' . $this->current()['position']); + throw new FilterSyntaxException('Expected identifier after "." at position ' . $this->current()['position']); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } $path[] = $this->advance()['value']; } @@ -200,7 +200,7 @@ private function primary(): array return ['kind' => 'property', 'path' => $path]; } - throw new FilterSyntaxException('Unexpected token "' . $this->current()['value'] . '" at position ' . $this->current()['position']); + throw new FilterSyntaxException('Unexpected token "' . $this->current()['value'] . '" at position ' . $this->current()['position']); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } /** @@ -246,6 +246,6 @@ private function expect(string $type, string $message): array if ($this->check($type)) { return $this->advance(); } - throw new FilterSyntaxException($message . ', got "' . $this->current()['value'] . '" at position ' . $this->current()['position']); + throw new FilterSyntaxException($message . ', got "' . $this->current()['value'] . '" at position ' . $this->current()['position']); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } } diff --git a/sdk/src/Rules/Filter/Tokenizer.php b/sdk/src/Rules/Filter/Tokenizer.php index 58f1f48..b69d9d6 100644 --- a/sdk/src/Rules/Filter/Tokenizer.php +++ b/sdk/src/Rules/Filter/Tokenizer.php @@ -167,7 +167,7 @@ public function tokenize(): array continue; } - throw new FilterSyntaxException("Unexpected character '{$ch}' at position {$this->pos}"); + throw new FilterSyntaxException("Unexpected character '{$ch}' at position {$this->pos}"); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } $this->tokens[] = ['type' => TokenType::EOF, 'value' => '', 'position' => $this->pos]; @@ -210,7 +210,7 @@ private function readString(string $quote): void $this->pos++; } if ($this->pos >= $len) { - throw new FilterSyntaxException("Unterminated string at position {$start}"); + throw new FilterSyntaxException("Unterminated string at position {$start}"); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- internal exception, never output to the browser } $this->pos++; // skip closing quote $this->tokens[] = ['type' => TokenType::STRING, 'value' => $value, 'position' => $start]; diff --git a/sdk/src/SignalCollector.php b/sdk/src/SignalCollector.php index 0524b14..a87a5b4 100644 --- a/sdk/src/SignalCollector.php +++ b/sdk/src/SignalCollector.php @@ -65,7 +65,8 @@ private function sanitizeString(?string $value): string } // Fallback sanitization for non-WordPress environments - return htmlspecialchars(strip_tags(trim($value)), ENT_QUOTES, 'UTF-8'); + $stripped = function_exists('wp_strip_all_tags') ? wp_strip_all_tags(trim($value)) : strip_tags(trim($value)); // phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- guarded fallback for non-WordPress (standalone SDK) use + return htmlspecialchars($stripped, ENT_QUOTES, 'UTF-8'); } /** diff --git a/templates/block-page.php b/templates/block-page.php index 7b715a6..8096c05 100644 --- a/templates/block-page.php +++ b/templates/block-page.php @@ -173,7 +173,9 @@

- +
@@ -181,7 +183,7 @@

- +

diff --git a/webdecoy.php b/webdecoy.php index 556b03b..7f692a0 100644 --- a/webdecoy.php +++ b/webdecoy.php @@ -4,7 +4,7 @@ * Plugin URI: https://webdecoy.com/wordpress * Description: Protect your WordPress site from bots, spam, and carding attacks with WebDecoy's advanced threat detection. * Version: 2.2.1 - * Requires at least: 5.6 + * Requires at least: 6.1 * Requires PHP: 7.4 * Author: WebDecoy * Author URI: https://webdecoy.com @@ -1365,7 +1365,7 @@ private function serve_challenge_page(string $ip): void $message = __('Please verify that you are human to continue.', 'webdecoy'); $challenge_data = $pow->generate_challenge($ip, intval($this->options['pow_difficulty'] ?? 4)); $redirect_url = $this->get_current_url(); - if (parse_url($redirect_url, PHP_URL_HOST) !== parse_url(home_url(), PHP_URL_HOST)) { + if (wp_parse_url($redirect_url, PHP_URL_HOST) !== wp_parse_url(home_url(), PHP_URL_HOST)) { $redirect_url = home_url('/'); } $ajax_url = admin_url('admin-ajax.php'); @@ -1941,8 +1941,10 @@ public function admin_scripts(string $hook): void 'testSuccess' => __('Connection successful!', 'webdecoy'), 'testFailed' => __('Connection failed:', 'webdecoy'), 'connectionFailed' => __('Connection failed', 'webdecoy'), + /* translators: %s: IP address to block */ 'confirmBlock' => __('Are you sure you want to block %s?', 'webdecoy'), 'confirmUnblock' => __('Are you sure you want to unblock this IP?', 'webdecoy'), + /* translators: %d: number of IP addresses to block */ 'confirmBulkBlock' => __('Are you sure you want to block %d IPs?', 'webdecoy'), 'selectIPs' => __('Please select at least one IP to block.', 'webdecoy'), 'toggleVisibility' => __('Toggle visibility', 'webdecoy'), @@ -2346,8 +2348,8 @@ public function ajax_get_stats(): void try { $stats = $client->getStats( - date('Y-m-d', strtotime('-7 days')), - date('Y-m-d') + gmdate('Y-m-d', strtotime('-7 days')), + gmdate('Y-m-d') ); wp_send_json_success($stats); } catch (\Exception $e) {