Completed
Push — master ( 99f654...56520c )
by Mike
08:35
created

WC_Geolocation::geolocate_ip()   D

Complexity

Conditions 9
Paths 25

Size

Total Lines 37
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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