WC_Geolocation

WC_Geolocation Class.

Defined (1)

The class is defined in the following location(s).

/includes/class-wc-geolocation.php  
  1. class WC_Geolocation { 
  2.  
  3. /** URL to the geolocation database we're using */ 
  4. const GEOLITE_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz'; 
  5. const GEOLITE_IPV6_DB = 'http://geolite.maxmind.com/download/geoip/database/GeoIPv6.dat.gz'; 
  6.  
  7. /** @var array API endpoints for looking up user IP address */ 
  8. private static $ip_lookup_apis = array( 
  9. 'icanhazip' => 'http://icanhazip.com',  
  10. 'ipify' => 'http://api.ipify.org/',  
  11. 'ipecho' => 'http://ipecho.net/plain',  
  12. 'ident' => 'http://ident.me',  
  13. 'whatismyipaddress' => 'http://bot.whatismyipaddress.com',  
  14. 'ip.appspot' => 'http://ip.appspot.com',  
  15. ); 
  16.  
  17. /** @var array API endpoints for geolocating an IP address */ 
  18. private static $geoip_apis = array( 
  19. 'freegeoip' => 'https://freegeoip.net/json/%s',  
  20. 'ipinfo.io' => 'https://ipinfo.io/%s/json',  
  21. 'ip-api.com' => 'http://ip-api.com/json/%s',  
  22. ); 
  23.  
  24. /** 
  25. * Hook in tabs. 
  26. */ 
  27. public static function init() { 
  28. // Only download the database from MaxMind if the geolocation function is enabled, or a plugin specifically requests it 
  29. if ( 'geolocation' === get_option( 'woocommerce_default_customer_address' ) || apply_filters( 'woocommerce_geolocation_update_database_periodically', false ) ) { 
  30. add_action( 'woocommerce_geoip_updater', array( __CLASS__, 'update_database' ) ); 
  31. add_filter( 'pre_update_option_woocommerce_default_customer_address', array( __CLASS__, 'maybe_update_database' ), 10, 2 ); 
  32.  
  33. /** 
  34. * Maybe trigger a DB update for the first time. 
  35. * @param string $new_value 
  36. * @param string $old_value 
  37. * @return string 
  38. */ 
  39. public static function maybe_update_database( $new_value, $old_value ) { 
  40. if ( $new_value !== $old_value && 'geolocation' === $new_value ) { 
  41. self::update_database(); 
  42. return $new_value; 
  43.  
  44. /** 
  45. * Check if is a valid IP address. 
  46. * @since 3.0.6 
  47. * @param string $ip_address IP address. 
  48. * @return string|bool The valid IP address, otherwise false. 
  49. */ 
  50. private function is_ip_address( $ip_address ) { 
  51. // WP 4.7+ only. 
  52. if ( function_exists( 'rest_is_ip_address' ) ) { 
  53. return rest_is_ip_address( $ip_address ); 
  54.  
  55. // Support for WordPress 4.4 to 4.6. 
  56. if ( ! class_exists( 'Requests_IPv6', false ) ) { 
  57. include_once( dirname( __FILE__ ) . '/vendor/class-requests-ipv6.php' ); 
  58.  
  59. $ipv4_pattern = '/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.) {3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/'; 
  60.  
  61. if ( ! preg_match( $ipv4_pattern, $ip ) && ! Requests_IPv6::check_ipv6( $ip ) ) { 
  62. return false; 
  63.  
  64. return $ip; 
  65.  
  66. /** 
  67. * Get current user IP Address. 
  68. * @return string 
  69. */ 
  70. public static function get_ip_address() { 
  71. if ( isset( $_SERVER['X-Real-IP'] ) ) { 
  72. return $_SERVER['X-Real-IP']; 
  73. } elseif ( isset( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { 
  74. // Proxy servers can send through this header like this: X-Forwarded-For: client1, proxy1, proxy2 
  75. // Make sure we always only send through the first IP in the list which should always be the client IP. 
  76. return (string) self::is_ip_address( trim( current( explode( ', ', $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ); 
  77. } elseif ( isset( $_SERVER['REMOTE_ADDR'] ) ) { 
  78. return $_SERVER['REMOTE_ADDR']; 
  79. return ''; 
  80.  
  81. /** 
  82. * Get user IP Address using an external service. 
  83. * This is used mainly as a fallback for users on localhost where 
  84. * get_ip_address() will be a local IP and non-geolocatable. 
  85. * @return string 
  86. */ 
  87. public static function get_external_ip_address() { 
  88. $external_ip_address = '0.0.0.0'; 
  89.  
  90. if ( '' !== self::get_ip_address() ) { 
  91. $transient_name = 'external_ip_address_' . self::get_ip_address(); 
  92. $external_ip_address = get_transient( $transient_name ); 
  93.  
  94. if ( false === $external_ip_address ) { 
  95. $external_ip_address = '0.0.0.0'; 
  96. $ip_lookup_services = apply_filters( 'woocommerce_geolocation_ip_lookup_apis', self::$ip_lookup_apis ); 
  97. $ip_lookup_services_keys = array_keys( $ip_lookup_services ); 
  98. shuffle( $ip_lookup_services_keys ); 
  99.  
  100. foreach ( $ip_lookup_services_keys as $service_name ) { 
  101. $service_endpoint = $ip_lookup_services[ $service_name ]; 
  102. $response = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) ); 
  103.  
  104. if ( ! is_wp_error( $response ) && $response['body'] ) { 
  105. $external_ip_address = apply_filters( 'woocommerce_geolocation_ip_lookup_api_response', wc_clean( $response['body'] ), $service_name ); 
  106. break; 
  107.  
  108. set_transient( $transient_name, $external_ip_address, WEEK_IN_SECONDS ); 
  109.  
  110. return $external_ip_address; 
  111.  
  112. /** 
  113. * Geolocate an IP address. 
  114. * @param string $ip_address 
  115. * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower). 
  116. * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower). 
  117. * @return array 
  118. */ 
  119. public static function geolocate_ip( $ip_address = '', $fallback = true, $api_fallback = true ) { 
  120. // Filter to allow custom geolocation of the IP address. 
  121. $country_code = apply_filters( 'woocommerce_geolocate_ip', false, $ip_address, $fallback, $api_fallback ); 
  122.  
  123. if ( false === $country_code ) { 
  124. // If GEOIP is enabled in CloudFlare, we can use that (Settings -> CloudFlare Settings -> Settings Overview) 
  125. if ( ! empty( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ) { 
  126. $country_code = sanitize_text_field( strtoupper( $_SERVER['HTTP_CF_IPCOUNTRY'] ) ); 
  127. // WP.com VIP has a variable available. 
  128. } elseif ( ! empty( $_SERVER['GEOIP_COUNTRY_CODE'] ) ) { 
  129. $country_code = sanitize_text_field( strtoupper( $_SERVER['GEOIP_COUNTRY_CODE'] ) ); 
  130. // VIP Go has a variable available also. 
  131. } elseif ( ! empty( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ) { 
  132. $country_code = sanitize_text_field( strtoupper( $_SERVER['HTTP_X_COUNTRY_CODE'] ) ); 
  133. } else { 
  134. $ip_address = $ip_address ? $ip_address : self::get_ip_address(); 
  135.  
  136. if ( self::is_IPv6( $ip_address ) ) { 
  137. $database = self::get_local_database_path( 'v6' ); 
  138. } else { 
  139. $database = self::get_local_database_path(); 
  140.  
  141. if ( file_exists( $database ) ) { 
  142. $country_code = self::geolocate_via_db( $ip_address ); 
  143. } elseif ( $api_fallback ) { 
  144. $country_code = self::geolocate_via_api( $ip_address ); 
  145. } else { 
  146. $country_code = ''; 
  147.  
  148. if ( ! $country_code && $fallback ) { 
  149. // May be a local environment - find external IP 
  150. return self::geolocate_ip( self::get_external_ip_address(), false, $api_fallback ); 
  151.  
  152. return array( 
  153. 'country' => $country_code,  
  154. 'state' => '',  
  155. ); 
  156.  
  157. /** 
  158. * Path to our local db. 
  159. * @param string $version 
  160. * @return string 
  161. */ 
  162. public static function get_local_database_path( $version = 'v4' ) { 
  163. $version = ( 'v4' == $version ) ? '' : 'v6'; 
  164. $upload_dir = wp_upload_dir(); 
  165.  
  166. return apply_filters( 'woocommerce_geolocation_local_database_path', $upload_dir['basedir'] . '/GeoIP' . $version . '.dat', $version ); 
  167.  
  168. /** 
  169. * Update geoip database. Adapted from https://wordpress.org/plugins/geoip-detect/. 
  170. */ 
  171. public static function update_database() { 
  172. $logger = wc_get_logger(); 
  173.  
  174. if ( ! is_callable( 'gzopen' ) ) { 
  175. $logger->notice( 'Server does not support gzopen', array( 'source' => 'geolocation' ) ); 
  176. return; 
  177.  
  178. require_once( ABSPATH . 'wp-admin/includes/file.php' ); 
  179.  
  180. $tmp_databases = array( 
  181. 'v4' => download_url( self::GEOLITE_DB ),  
  182. 'v6' => download_url( self::GEOLITE_IPV6_DB ),  
  183. ); 
  184.  
  185. foreach ( $tmp_databases as $tmp_database_version => $tmp_database_path ) { 
  186. if ( ! is_wp_error( $tmp_database_path ) ) { 
  187. $gzhandle = @gzopen( $tmp_database_path, 'r' ); 
  188. $handle = @fopen( self::get_local_database_path( $tmp_database_version ), 'w' ); 
  189.  
  190. if ( $gzhandle && $handle ) { 
  191. while ( $string = gzread( $gzhandle, 4096 ) ) { 
  192. fwrite( $handle, $string, strlen( $string ) ); 
  193. gzclose( $gzhandle ); 
  194. fclose( $handle ); 
  195. } else { 
  196. $logger->notice( 'Unable to open database file', array( 'source' => 'geolocation' ) ); 
  197. @unlink( $tmp_database_path ); 
  198. } else { 
  199. $logger->notice( 
  200. 'Unable to download GeoIP Database: ' . $tmp_database_path->get_error_message(),  
  201. array( 'source' => 'geolocation' ) 
  202. ); 
  203.  
  204. /** 
  205. * Use MAXMIND GeoLite database to geolocation the user. 
  206. * @param string $ip_address 
  207. * @return string 
  208. */ 
  209. private static function geolocate_via_db( $ip_address ) { 
  210. if ( ! class_exists( 'WC_Geo_IP', false ) ) { 
  211. include_once( WC_ABSPATH . 'includes/class-wc-geo-ip.php' ); 
  212.  
  213. $gi = new WC_Geo_IP(); 
  214.  
  215. if ( self::is_IPv6( $ip_address ) ) { 
  216. $database = self::get_local_database_path( 'v6' ); 
  217. $gi->geoip_open( $database, 0 ); 
  218. $country_code = $gi->geoip_country_code_by_addr_v6( $ip_address ); 
  219. } else { 
  220. $database = self::get_local_database_path(); 
  221. $gi->geoip_open( $database, 0 ); 
  222. $country_code = $gi->geoip_country_code_by_addr( $ip_address ); 
  223.  
  224. $gi->geoip_close(); 
  225.  
  226. return sanitize_text_field( strtoupper( $country_code ) ); 
  227.  
  228. /** 
  229. * Use APIs to Geolocate the user. 
  230. * @param string $ip_address 
  231. * @return string|bool 
  232. */ 
  233. private static function geolocate_via_api( $ip_address ) { 
  234. $country_code = get_transient( 'geoip_' . $ip_address ); 
  235.  
  236. if ( false === $country_code ) { 
  237. $geoip_services = apply_filters( 'woocommerce_geolocation_geoip_apis', self::$geoip_apis ); 
  238. $geoip_services_keys = array_keys( $geoip_services ); 
  239. shuffle( $geoip_services_keys ); 
  240.  
  241. foreach ( $geoip_services_keys as $service_name ) { 
  242. $service_endpoint = $geoip_services[ $service_name ]; 
  243. $response = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) ); 
  244.  
  245. if ( ! is_wp_error( $response ) && $response['body'] ) { 
  246. switch ( $service_name ) { 
  247. case 'ipinfo.io' : 
  248. $data = json_decode( $response['body'] ); 
  249. $country_code = isset( $data->country ) ? $data->country : ''; 
  250. break; 
  251. case 'ip-api.com' : 
  252. $data = json_decode( $response['body'] ); 
  253. $country_code = isset( $data->countryCode ) ? $data->countryCode : ''; 
  254. break; 
  255. case 'freegeoip' : 
  256. $data = json_decode( $response['body'] ); 
  257. $country_code = isset( $data->country_code ) ? $data->country_code : ''; 
  258. break; 
  259. default : 
  260. $country_code = apply_filters( 'woocommerce_geolocation_geoip_response_' . $service_name, '', $response['body'] ); 
  261. break; 
  262.  
  263. $country_code = sanitize_text_field( strtoupper( $country_code ) ); 
  264.  
  265. if ( $country_code ) { 
  266. break; 
  267.  
  268. set_transient( 'geoip_' . $ip_address, $country_code, WEEK_IN_SECONDS ); 
  269.  
  270. return $country_code; 
  271.  
  272. /** 
  273. * Test if is IPv6. 
  274. * @since 2.4.0 
  275. * @param string $ip_address 
  276. * @return bool 
  277. */ 
  278. private static function is_IPv6( $ip_address ) { 
  279. return false !== filter_var( $ip_address, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6 );