antispamGetConfig(); $ip = self::antispamGetClientIp(); if ($config['protection']) { // Honeypot $honeypot = isset($_REQUEST[self::$antispam_honeypot_field]) ? trim((string) wp_unslash($_REQUEST[self::$antispam_honeypot_field])) : ''; if ($honeypot !== '') { return $this->antispamReject('honeypot', $restaurant, $ip); } // Minimum time between picking the slot (hold) and submitting the form $min_seconds = (int) apply_filters('alexr_antispam_min_seconds', self::$antispam_min_seconds); if ($min_seconds > 0 && !empty($booking->date_created)) { try { $created = \Cake\Chronos\Chronos::parse($booking->date_created); $elapsed = evavel_date_now()->getTimestamp() - $created->getTimestamp(); if ($elapsed >= 0 && $elapsed < $min_seconds) { return $this->antispamReject('too_fast ('.$elapsed.'s)', $restaurant, $ip); } } catch (\Exception $e) { // Unparseable date: do not block } } } // Rate limit per IP $max = $config['max_per_hour']; if ($max > 0 && $ip && !is_user_logged_in()) { if (count($this->antispamGetHits($restaurant->id, $ip)) >= $max) { return $this->antispamReject('rate_limit ('.$max.'/h)', $restaurant, $ip, __eva('Too many reservations from your connection. Please try again later or contact the restaurant.')); } } return true; } /** * Count a booking successfully created for the rate limit. */ protected function antispamRecordBooking($restaurant) { $config = $this->antispamGetConfig(); $ip = self::antispamGetClientIp(); if ($config['max_per_hour'] <= 0 || !$ip || is_user_logged_in()) return; $hits = $this->antispamGetHits($restaurant->id, $ip); $hits[] = time(); set_transient($this->antispamKey($restaurant->id, $ip), $hits, HOUR_IN_SECONDS); } protected function antispamGetConfig() { $protection = true; $max_per_hour = self::$antispam_default_max_per_hour; $widget_id = isset($_REQUEST['widgetId']) ? intval($_REQUEST['widgetId']) : 0; $w_form = $widget_id ? \Alexr\Settings\WidgetForm::where('id', $widget_id)->first() : null; if ($w_form && is_array($w_form->form_config)) { $form_config = $w_form->form_config; if (isset($form_config['antispam_protection']) && $form_config['antispam_protection'] == 'no') { $protection = false; } if (isset($form_config['antispam_max_bookings_per_hour']) && $form_config['antispam_max_bookings_per_hour'] !== '') { $value = $form_config['antispam_max_bookings_per_hour']; $max_per_hour = ($value === 'off') ? 0 : max(0, intval($value)); } } return [ 'protection' => (bool) apply_filters('alexr_antispam_protection', $protection, $widget_id), 'max_per_hour' => (int) apply_filters('alexr_antispam_max_bookings_per_hour', $max_per_hour, $widget_id), ]; } protected function antispamKey($restaurant_id, $ip) { return 'alexr_rl_'.intval($restaurant_id).'_'.md5($ip); } protected function antispamGetHits($restaurant_id, $ip) { $hits = get_transient($this->antispamKey($restaurant_id, $ip)); if (!is_array($hits)) return []; $limit = time() - HOUR_IN_SECONDS; return array_values(array_filter($hits, function($t) use ($limit) { return intval($t) > $limit; })); } protected function antispamReject($reason, $restaurant, $ip, $message = null) { $email = isset($_REQUEST['email']) ? sanitize_text_field(wp_unslash($_REQUEST['email'])) : ''; evavel_log_to_file(\Evavel\Log\Log::ANTISPAM_REJECTED, json_encode([ 'reason' => $reason, 'restaurant_id' => $restaurant->id, 'widget_id' => isset($_REQUEST['widgetId']) ? intval($_REQUEST['widgetId']) : null, 'ip' => $ip, 'email' => $email, 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? substr(sanitize_text_field($_SERVER['HTTP_USER_AGENT']), 0, 200) : '', ])); do_action('alexr_antispam_rejected', $reason, $restaurant->id, $ip); wp_send_json_error([ 'message' => $message ? $message : __eva('Sorry, something went wrong, please try again.') ]); return false; } /** * Client IP without trusting spoofable headers: * - REMOTE_ADDR is a Cloudflare edge -> CF-Connecting-IP * - REMOTE_ADDR is private/reserved (local reverse proxy, docker...) -> * rightmost public IP of X-Forwarded-For that is not Cloudflare, or X-Real-IP * - otherwise REMOTE_ADDR */ public static function antispamGetClientIp() { $remote = isset($_SERVER['REMOTE_ADDR']) ? trim($_SERVER['REMOTE_ADDR']) : ''; $ip = filter_var($remote, FILTER_VALIDATE_IP) ? $remote : ''; if ($ip && self::antispamIpInRanges($ip, self::$antispam_cloudflare_ranges)) { $cf = isset($_SERVER['HTTP_CF_CONNECTING_IP']) ? trim($_SERVER['HTTP_CF_CONNECTING_IP']) : ''; if (filter_var($cf, FILTER_VALIDATE_IP)) { $ip = $cf; } } else if ($ip && !self::antispamIsPublicIp($ip)) { $forwarded = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']) : []; $found = ''; foreach (array_reverse($forwarded) as $candidate) { $candidate = trim($candidate); if (!filter_var($candidate, FILTER_VALIDATE_IP)) continue; if (!self::antispamIsPublicIp($candidate)) continue; if (self::antispamIpInRanges($candidate, self::$antispam_cloudflare_ranges)) continue; $found = $candidate; break; } if (!$found && isset($_SERVER['HTTP_X_REAL_IP'])) { $real = trim($_SERVER['HTTP_X_REAL_IP']); if (filter_var($real, FILTER_VALIDATE_IP)) $found = $real; } if ($found) $ip = $found; } return apply_filters('alexr_antispam_client_ip', $ip); } protected static function antispamIsPublicIp($ip) { return (bool) filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE); } protected static function antispamIpInRanges($ip, $ranges) { $ip_bin = @inet_pton($ip); if ($ip_bin === false) return false; foreach ($ranges as $range) { list($subnet, $bits) = explode('/', $range); $subnet_bin = @inet_pton($subnet); if ($subnet_bin === false || strlen($subnet_bin) !== strlen($ip_bin)) continue; $bits = intval($bits); $bytes = intdiv($bits, 8); $rest = $bits % 8; if (substr($ip_bin, 0, $bytes) !== substr($subnet_bin, 0, $bytes)) continue; if ($rest === 0) return true; $mask = chr((0xFF << (8 - $rest)) & 0xFF); if ((($ip_bin[$bytes] ^ $subnet_bin[$bytes]) & $mask) === chr(0)) return true; } return false; } }