'', 'country' => '', 'region' => '', 'city' => '', 'isp' => '' ]; if ( ! filter_var( $ip, FILTER_VALIDATE_IP ) || self::is_private_ip( $ip ) ) { return $out; } $key = self::CACHE_PREFIX . 'loc_' . md5( $ip ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return array_merge( $out, $cached ); } /** * Filter the location API URL. It must return ipapi.co-style JSON * (country_code, country_name, region, city, org); ip-api.com-style * JSON is understood too, for anyone pointing this at that service. * * @param string $url * @param string $ip */ $url = apply_filters( 'gemsecurity/geo_location_endpoint', 'https://ipapi.co/' . rawurlencode( $ip ) . '/json/', $ip ); $resp = wp_remote_get( $url, [ 'timeout' => 3, 'headers' => [ 'Accept' => 'application/json' ] ] ); $json = is_wp_error( $resp ) ? null : json_decode( wp_remote_retrieve_body( $resp ), true ); $ok = is_array( $json ) && empty( $json['error'] ) && ( $json['status'] ?? 'success' ) === 'success'; if ( $ok && ( ! empty( $json['country_code'] ) || ! empty( $json['countryCode'] ) ) ) { $out = [ 'country_code' => strtoupper( substr( (string) ( $json['country_code'] ?? $json['countryCode'] ), 0, 2 ) ), 'country' => sanitize_text_field( (string) ( $json['country_name'] ?? ( $json['country'] ?? '' ) ) ), 'region' => sanitize_text_field( (string) ( $json['region'] ?? ( $json['regionName'] ?? '' ) ) ), 'city' => sanitize_text_field( (string) ( $json['city'] ?? '' ) ), 'isp' => sanitize_text_field( (string) ( $json['org'] ?? ( $json['isp'] ?? '' ) ) ), ]; } else { // Fall back to the country-only resolver (CDN headers, second API). $code = self::country_for_ip( $ip ); $out['country_code'] = $code; $out['country'] = $code; } set_transient( $key, $out, '' !== $out['country_code'] ? self::CACHE_TTL : 10 * MINUTE_IN_SECONDS ); return $out; } /** * One-line "Dhaka, Dhaka Division, Bangladesh (BD) · ISP" label, or ''. * * @param string $ip * @return string */ public static function location_label( $ip ) { $l = self::location_for_ip( $ip ); $parts = array_filter( array_unique( [ $l['city'], $l['region'], $l['country'] ] ) ); if ( ! $parts ) { return ''; } $label = implode( ', ', $parts ); if ( $l['country_code'] && $l['country_code'] !== $l['country'] ) { $label .= ' (' . $l['country_code'] . ')'; } return $l['isp'] ? $label . ' · ' . $l['isp'] : $label; } /** * @return string Country from a CDN/proxy header, or ''. */ private static function from_headers() { foreach ( self::country_headers() as $key ) { if ( empty( $_SERVER[ $key ] ) ) { continue; } $code = strtoupper( substr( trim( sanitize_text_field( wp_unslash( $_SERVER[ $key ] ) ) ), 0, 2 ) ); if ( self::is_valid_code( $code ) ) { return $code; } } return ''; } /** * Query a free IP-geolocation API. Fail-open (returns '' on any error). * * @param string $ip * @return string */ private static function from_api( $ip ) { $endpoints = [ 'https://ipapi.co/' . rawurlencode( $ip ) . '/country/', ]; /** * Filter the geolocation API endpoints (allows pointing at a paid/self-hosted service). * * @param string[] $endpoints * @param string $ip */ $endpoints = apply_filters( 'gemsecurity/geo_api_endpoints', $endpoints, $ip ); foreach ( $endpoints as $url ) { $response = wp_remote_get( $url, [ 'timeout' => 3, 'headers' => [ 'Accept' => 'application/json' ] ] ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { continue; } $body = trim( wp_remote_retrieve_body( $response ) ); $code = ''; $json = json_decode( $body, true ); if ( is_array( $json ) ) { if ( isset( $json['status'] ) && 'success' !== $json['status'] ) { continue; } $code = $json['countryCode'] ?? ( $json['country_code'] ?? ( $json['country'] ?? '' ) ); } else { // Plain-text "US" style response (ipapi.co/{ip}/country/). $code = $body; } $code = strtoupper( substr( trim( (string) $code ), 0, 2 ) ); if ( self::is_valid_code( $code ) ) { return $code; } } return ''; } /** * @param string $code * @return bool */ public static function is_valid_code( $code ) { return (bool) preg_match( '/^[A-Z]{2}$/', $code ) && ! in_array( $code, [ 'XX', 'T1', 'ZZ' ], true ); } /** * @param string $ip * @return bool True for private / reserved ranges (no public geolocation). */ public static function is_private_ip( $ip ) { return ! filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ); } /** * Emergency kill-switch: define('GEMSEC_GEO_DISABLE', true) in wp-config.php. * * @return bool */ public static function is_bypassed() { return defined( 'GEMSEC_GEO_DISABLE' ) && GEMSEC_GEO_DISABLE; } }