| @@ -12,9 +12,11 @@ | ||
| 12 | 12 | //error_log('Current User: ' . print_r($current_user, true)); |
| 13 | 13 | return $current_user->user_login; // Use username as identifier |
| 14 | 14 | } else { |
| 15 | 15 | //error_log('User is not logged in. Using IP as identifier.'); |
| 16 | - return sanitize_text_field($_SERVER['REMOTE_ADDR']); // Use IP address as fallback | |
| 16 | + // Same resolver the rate limiter keys on, so the transcript | |
| 17 | + // identifier and the rate-limit counter name the same address. | |
| 18 | + return self::mxchat_get_client_ip(); | |
| 17 | 19 | } |
| 18 | 20 | } |
| 19 | 21 | |
| 20 | 22 | // Function to get user email |
| @@ -24,6 +26,100 @@ | ||
| 24 | 26 | return $current_user->user_email; |
| 25 | 27 | } |
| 26 | 28 | return null; // No email if not logged in |
| 27 | 29 | } |
| 30 | + | |
| 31 | + /** | |
| 32 | + * The visitor's address, from a source the visitor cannot choose. | |
| 33 | + * | |
| 34 | + * REMOTE_ADDR is the answer. Client-IP and X-Forwarded-For are set by | |
| 35 | + * whoever sends the request, so a counter keyed on them can be reset | |
| 36 | + * with a header. The one exception is Cloudflare: when the connection | |
| 37 | + * really comes from a Cloudflare edge (REMOTE_ADDR inside the ranges | |
| 38 | + * Cloudflare publishes), CF-Connecting-IP is the visitor. Hosts behind | |
| 39 | + * some other proxy can supply their own resolver with the | |
| 40 | + * `mxchat_client_ip` filter; whatever comes back must still be a valid | |
| 41 | + * address or REMOTE_ADDR is used. | |
| 42 | + * | |
| 43 | + * @return string A validated IP address, or 'unknown' when there is none. | |
| 44 | + */ | |
| 45 | + public static function mxchat_get_client_ip() { | |
| 46 | + $remote = isset($_SERVER['REMOTE_ADDR']) ? trim((string) wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; | |
| 47 | + $remote = filter_var($remote, FILTER_VALIDATE_IP) ? $remote : ''; | |
| 48 | + | |
| 49 | + $ip = $remote; | |
| 50 | + if ($remote !== '' && !empty($_SERVER['HTTP_CF_CONNECTING_IP']) && self::mxchat_ip_is_cloudflare($remote)) { | |
| 51 | + $candidate = trim((string) wp_unslash($_SERVER['HTTP_CF_CONNECTING_IP'])); | |
| 52 | + if (filter_var($candidate, FILTER_VALIDATE_IP)) { | |
| 53 | + $ip = $candidate; | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * Override the resolved visitor address, e.g. for a host behind a | |
| 59 | + * trusted proxy that is not Cloudflare. Return a valid IP. | |
| 60 | + * | |
| 61 | + * @param string $ip The address MxChat resolved. | |
| 62 | + * @param string $remote REMOTE_ADDR as received (validated, may be ''). | |
| 63 | + */ | |
| 64 | + $filtered = apply_filters('mxchat_client_ip', $ip, $remote); | |
| 65 | + if (is_string($filtered) && filter_var(trim($filtered), FILTER_VALIDATE_IP)) { | |
| 66 | + $ip = trim($filtered); | |
| 67 | + } | |
| 68 | + | |
| 69 | + return $ip !== '' ? $ip : 'unknown'; | |
| 70 | + } | |
| 71 | + | |
| 72 | + /** | |
| 73 | + * Cloudflare's published edge ranges (cloudflare.com/ips-v4, /ips-v6), | |
| 74 | + * as of 2026-09-16. Refresh with the plugin. | |
| 75 | + */ | |
| 76 | + public static function mxchat_cloudflare_ranges() { | |
| 77 | + return array( | |
| 78 | + '173.245.48.0/20', '103.21.244.0/22', '103.22.200.0/22', '103.31.4.0/22', | |
| 79 | + '141.101.64.0/18', '108.162.192.0/18', '190.93.240.0/20', '188.114.96.0/20', | |
| 80 | + '197.234.240.0/22', '198.41.128.0/17', '162.158.0.0/15', '104.16.0.0/13', | |
| 81 | + '104.24.0.0/14', '172.64.0.0/13', '131.0.72.0/22', | |
| 82 | + '2400:cb00::/32', '2606:4700::/32', '2803:f800::/32', '2405:b500::/32', | |
| 83 | + '2405:8100::/32', '2a06:98c0::/29', '2c0f:f248::/32', | |
| 84 | + ); | |
| 85 | + } | |
| 86 | + | |
| 87 | + public static function mxchat_ip_is_cloudflare($ip) { | |
| 88 | + foreach (self::mxchat_cloudflare_ranges() as $cidr) { | |
| 89 | + if (self::mxchat_ip_in_cidr($ip, $cidr)) { | |
| 90 | + return true; | |
| 91 | + } | |
| 92 | + } | |
| 93 | + return false; | |
| 94 | + } | |
| 95 | + | |
| 96 | + /** | |
| 97 | + * Does $ip fall inside $cidr? Works for IPv4 and IPv6; families must match. | |
| 98 | + */ | |
| 99 | + public static function mxchat_ip_in_cidr($ip, $cidr) { | |
| 100 | + if (strpos($cidr, '/') === false) { | |
| 101 | + return false; | |
| 102 | + } | |
| 103 | + list($subnet, $bits) = explode('/', $cidr, 2); | |
| 104 | + $ip_bin = @inet_pton($ip); | |
| 105 | + $subnet_bin = @inet_pton($subnet); | |
| 106 | + if ($ip_bin === false || $subnet_bin === false || strlen($ip_bin) !== strlen($subnet_bin)) { | |
| 107 | + return false; | |
| 108 | + } | |
| 109 | + $bits = (int) $bits; | |
| 110 | + $max = strlen($ip_bin) * 8; | |
| 111 | + if ($bits < 0 || $bits > $max) { | |
| 112 | + return false; | |
| 113 | + } | |
| 114 | + $full_bytes = intdiv($bits, 8); | |
| 115 | + if ($full_bytes > 0 && substr($ip_bin, 0, $full_bytes) !== substr($subnet_bin, 0, $full_bytes)) { | |
| 116 | + return false; | |
| 117 | + } | |
| 118 | + $rest = $bits % 8; | |
| 119 | + if ($rest === 0) { | |
| 120 | + return true; | |
| 121 | + } | |
| 122 | + $mask = (0xFF << (8 - $rest)) & 0xFF; | |
| 123 | + return ((ord($ip_bin[$full_bytes]) & $mask) === (ord($subnet_bin[$full_bytes]) & $mask)); | |
| 124 | + } | |
| 28 | 125 | } |
| 29 | - | |