plugin = $plugin_instance; } /** * Downloads and processes the community blocklist JSON file. * Uses the V4 token endpoint if available, falling back to V2 static file. * * @return int|bool Returns the number of IPs synced, or false on failure. */ public function update_list($force = false) { if (!$force) { $last_update = (int) get_option($this->last_update_option, 0); if ($last_update > 0 && (time() - $last_update) < (2 * HOUR_IN_SECONDS)) { return false; } if (get_transient('advaipbl_community_sync_running')) { return false; } } set_transient('advaipbl_community_sync_running', true, 10 * MINUTE_IN_SECONDS); $use_v4 = false; $feed_data = false; $api_token_v3 = $this->plugin->options['api_token_v3'] ?? ''; if (!empty($api_token_v3)) { $headers = [ 'Authorization' => 'Bearer ' . $api_token_v3, 'X-AIB-Auth' => 'Bearer ' . $api_token_v3, ]; $saved_etag = get_option('advaipbl_community_etag', ''); if (!empty($saved_etag)) { $headers['If-None-Match'] = $saved_etag; } $response = wp_remote_get($this->feed_url_v4, [ 'headers' => $headers, 'timeout' => 30 ]); if (!is_wp_error($response)) { $response_code = wp_remote_retrieve_response_code($response); if ($response_code === 304) { update_option($this->last_update_option, time()); delete_transient('advaipbl_community_sync_running'); return true; } if ($response_code === 200) { $feed_data = wp_remote_retrieve_body($response); $new_etag = wp_remote_retrieve_header($response, 'etag'); if (!empty($new_etag)) { update_option('advaipbl_community_etag', sanitize_text_field($new_etag)); } $use_v4 = true; } elseif ($response_code === 429) { if (!get_transient('advaipbl_community_api_429_logged')) { $retry_after = wp_remote_retrieve_header($response, 'retry-after'); $body_raw = wp_remote_retrieve_body($response); $body_json = json_decode($body_raw, true); $server_msg = ''; if (is_array($body_json) && !empty($body_json['message'])) { /* translators: %s: Error message returned by the central server. */ $server_msg = ' ' . sprintf(__('Server response: "%s".', 'advanced-ip-blocker'), sanitize_text_field($body_json['message'])); } /* translators: %s: Cooldown time in seconds before retrying. */ $retry_msg = !empty($retry_after) ? ' ' . sprintf(__('Retry-After: %ss.', 'advanced-ip-blocker'), sanitize_text_field($retry_after)) : ''; $parsed_url = wp_parse_url($this->feed_url_v4, PHP_URL_PATH); $endpoint_path = !empty($parsed_url) ? $parsed_url : $this->feed_url_v4; $log_message = sprintf( /* translators: 1: Endpoint path, 2: Server response message, 3: Retry-After header info. */ __('AIB Network Sync: Central API rate limit reached (HTTP 429) on endpoint [%1$s].%2$s%3$s Sync temporarily postponed for 1 hour to prevent server overload.', 'advanced-ip-blocker'), $endpoint_path, $server_msg, $retry_msg ); $this->plugin->log_event($log_message, 'warning'); set_transient('advaipbl_community_api_429_logged', true, HOUR_IN_SECONDS); } set_transient('advaipbl_community_api_error_cooldown_v4', true, HOUR_IN_SECONDS); delete_transient('advaipbl_community_sync_running'); return false; } } else { $error_msg = $response->get_error_message(); if (!get_transient('advaipbl_community_api_error_cooldown_v4')) { $this->plugin->log_event('AIB Network Sync: V4 failed (' . $error_msg . '), falling back to V2.', 'warning'); set_transient('advaipbl_community_api_error_cooldown_v4', true, HOUR_IN_SECONDS); } } } if (!$use_v4) { $response = wp_remote_get($this->feed_url_v2, [ 'timeout' => 30 ]); if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) { $error_msg = is_wp_error($response) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code($response); if (!get_transient('advaipbl_community_api_error_cooldown_v2')) { $this->plugin->log_event('AIB Network list download failed completely. Reason: ' . $error_msg, 'error'); set_transient('advaipbl_community_api_error_cooldown_v2', true, HOUR_IN_SECONDS); } update_option(ADVAIPBL_Main::OPTION_COMMUNITY_SYNC_TIME, time()); delete_transient('advaipbl_community_sync_running'); return false; } $feed_data = wp_remote_retrieve_body($response); } if (!$feed_data) { delete_transient('advaipbl_community_sync_running'); if (!get_transient('advaipbl_community_api_error_cooldown_nodata')) { $this->plugin->log_event('AIB Network list download failed: No data received from V4 or V2.', 'error'); set_transient('advaipbl_community_api_error_cooldown_nodata', true, HOUR_IN_SECONDS); } return false; } $data = json_decode($feed_data, true); if (!$data || !isset($data['ips']) || !is_array($data['ips'])) { delete_transient('advaipbl_community_sync_running'); $this->plugin->log_event('AIB Network list download failed: Invalid data format.', 'error'); return false; } if (isset($data['status']) && $data['status'] === 'degraded') { update_option('advaipbl_network_degraded', true); } else { delete_option('advaipbl_network_degraded'); } global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_community_ips'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query("TRUNCATE TABLE {$table_name}"); $raw_ips = $data['ips']; $actions = $data['actions'] ?? []; $min_score = (int) ($this->plugin->options['community_min_score'] ?? 1); if ($min_score > 1 && isset($data['scores']) && is_array($data['scores'])) { $filtered_ips = []; foreach ($raw_ips as $ip) { $score = $data['scores'][$ip] ?? 0; if ($score >= $min_score) { $filtered_ips[] = $ip; } } $ips = array_unique($filtered_ips); } else { $ips = array_unique($raw_ips); } $raw_whitelist = get_option('advaipbl_ips_whitelist', []); $whitelisted_ips_flat = []; if (is_array($raw_whitelist)) { foreach ($raw_whitelist as $key => $val) { if (filter_var($key, FILTER_VALIDATE_IP)) { $whitelisted_ips_flat[] = (string)$key; } elseif (is_string($val) && filter_var($val, FILTER_VALIDATE_IP)) { $whitelisted_ips_flat[] = $val; } } } $server_ip = $this->plugin->get_server_ip(); if ($server_ip) { $whitelisted_ips_flat[] = $server_ip; } $whitelisted_ips_flat[] = '127.0.0.1'; $whitelisted_ips_flat[] = '::1'; $whitelisted_ips_flat = array_unique($whitelisted_ips_flat); if (!empty($whitelisted_ips_flat)) { $ips = array_diff($ips, $whitelisted_ips_flat); } $batch_size = 1000; $total_ips = count($ips); $chunks = array_chunk($ips, $batch_size); foreach ($chunks as $chunk) { $placeholders = []; $values = []; foreach ($chunk as $ip) { if (filter_var($ip, FILTER_VALIDATE_IP)) { $action = $actions[$ip] ?? null; $placeholders[] = "(%s, %s)"; $values[] = $ip; $values[] = $action; } } if (!empty($placeholders)) { $query = "INSERT IGNORE INTO {$table_name} (ip, action) VALUES " . implode(', ', $placeholders); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query($wpdb->prepare($query, $values)); } } update_option($this->last_update_option, time()); delete_transient('advaipbl_community_ips_count'); delete_option('advaipbl_community_blocklist'); delete_transient('advaipbl_community_sync_running'); return $total_ips; } /** * Checks if an IP is in the community table. * Direct high-performance SQL query (O(1) thanks to Primary Key). * * @param string $ip The IP to check. * @return bool True if blocked. */ public function is_ip_blocked($ip) { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_community_ips'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $exists = $wpdb->get_var($wpdb->prepare("SELECT 1 FROM {$table_name} WHERE ip = %s LIMIT 1", $ip)); return (bool) $exists; } /** * Gets the specific action override for a community IP (e.g. 'challenge'). * * @param string $ip The IP to check. * @return string|null The action string, or null if no override exists, or false if not blocked. */ public function get_ip_action($ip) { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_community_ips'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $row = $wpdb->get_row($wpdb->prepare("SELECT action FROM {$table_name} WHERE ip = %s LIMIT 1", $ip), ARRAY_A); if (!$row) { return false; // Not in the network } return !empty($row['action']) ? $row['action'] : null; } public function get_stats() { global $wpdb; $table_name = $wpdb->prefix . 'advaipbl_community_ips'; $count = get_transient('advaipbl_community_ips_count'); if (false === $count) { // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, PluginCheck.Security.DirectDB.UnescapedDBParameter $count = $wpdb->get_var("SELECT COUNT(*) FROM {$table_name}"); set_transient('advaipbl_community_ips_count', $count, 24 * HOUR_IN_SECONDS); } return [ 'count' => (int) $count, 'last_update' => get_option($this->last_update_option, 0) ]; } /** * Registers the site with the Central Server and gets a V3 API Token. * Can be called manually via AJAX or automatically during upgrades. * * @return array|WP_Error Returns an array with 'api_token' on success, or WP_Error on failure. */ public function register_site() { $site_url = home_url(); $response = wp_remote_post('https://advaipbl.com/wp-json/aib-api/v3/register?v=' . ADVAIPBL_VERSION, [ 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'body' => wp_json_encode(['site_url' => $site_url]), 'timeout' => 15 ]); if (is_wp_error($response)) { $this->plugin->log_event('Community Network Registration failed: ' . $response->get_error_message(), 'error'); return new WP_Error('registration_failed', $response->get_error_message()); } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code !== 200) { $error_msg = $body['message'] ?? __('Unknown error during registration.', 'advanced-ip-blocker'); $this->plugin->log_event("Community Network Registration failed (HTTP {$status_code}): {$error_msg}", 'error'); if ($status_code === 429 || (isset($body['code']) && $body['code'] === 'site_already_registered')) { return new WP_Error('registration_failed_handled', $error_msg, ['status' => $status_code]); } return new WP_Error('registration_failed', $error_msg, ['status' => $status_code]); } if (isset($body['status']) && $body['status'] === 'success' && !empty($body['api_token'])) { $options = $this->plugin->options; $options['api_token_v3'] = sanitize_text_field($body['api_token']); update_option(ADVAIPBL_Main::OPTION_SETTINGS, $options); $this->plugin->options = $options; return [ 'api_token' => $body['api_token'] ]; } $error_msg = $body['message'] ?? __('Failed to generate API Key.', 'advanced-ip-blocker'); return new WP_Error('registration_failed', $error_msg); } }