Passed
Push — master ( 1ce22f...41fbb9 )
by Brian
09:44 queued 04:28
created

GetPaid_Geolocation::geolocate_ip()   B

Complexity

Conditions 9
Paths 14

Size

Total Lines 65
Code Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 30
c 0
b 0
f 0
dl 0
loc 65
rs 8.0555
cc 9
nc 14
nop 3

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Geolocation class
4
 *
5
 * Handles geolocation of IP Addresses.
6
 *
7
 */
8
9
defined( 'ABSPATH' ) || exit;
10
11
/**
12
 * GetPaid_Geolocation Class.
13
 */
14
class GetPaid_Geolocation {
15
16
	/**
17
	 * Holds the current user's IP Address.
18
	 *
19
	 * @var string
20
	 */
21
	public static $current_user_ip;
22
23
	/**
24
	 * API endpoints for looking up a user IP address.
25
	 *
26
	 * For example, in case a user is on localhost.
27
	 *
28
	 * @var array
29
	 */
30
	protected static $ip_lookup_apis = array(
31
		'ipify'             => 'http://api.ipify.org/',
32
		'ipecho'            => 'http://ipecho.net/plain',
33
		'ident'             => 'http://ident.me',
34
		'whatismyipaddress' => 'http://bot.whatismyipaddress.com',
35
	);
36
37
	/**
38
	 * API endpoints for geolocating an IP address
39
	 *
40
	 * @var array
41
	 */
42
	protected static $geoip_apis = array(
43
		'ipinfo.io'  => 'https://ipinfo.io/%s/json',
44
		'ip-api.com' => 'http://ip-api.com/json/%s',
45
	);
46
47
	/**
48
	 * Get current user IP Address.
49
	 *
50
	 * @return string
51
	 */
52
	public static function get_ip_address() {
53
		return wpinv_get_ip();
54
	}
55
56
	/**
57
	 * Get user IP Address using an external service.
58
	 * This can be used as a fallback for users on localhost where
59
	 * get_ip_address() will be a local IP and non-geolocatable.
60
	 *
61
	 * @return string
62
	 */
63
	public static function get_external_ip_address() {
64
65
		$transient_name = 'external_ip_address_0.0.0.0';
66
67
		if ( '' !== self::get_ip_address() ) {
68
			$transient_name      = 'external_ip_address_' . self::get_ip_address();
69
		}
70
71
		// Try retrieving from cache.
72
		$external_ip_address = get_transient( $transient_name );
73
74
		if ( false === $external_ip_address ) {
75
			$external_ip_address     = '0.0.0.0';
76
			$ip_lookup_services      = apply_filters( 'getpaid_geolocation_ip_lookup_apis', self::$ip_lookup_apis );
77
			$ip_lookup_services_keys = array_keys( $ip_lookup_services );
78
			shuffle( $ip_lookup_services_keys );
79
80
			foreach ( $ip_lookup_services_keys as $service_name ) {
81
				$service_endpoint = $ip_lookup_services[ $service_name ];
82
				$response         = wp_safe_remote_get( $service_endpoint, array( 'timeout' => 2 ) );
83
84
				if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) {
85
					$external_ip_address = apply_filters( 'getpaid_geolocation_ip_lookup_api_response', wpinv_clean( $response['body'] ), $service_name );
86
					break;
87
				}
88
89
			}
90
91
			set_transient( $transient_name, $external_ip_address, WEEK_IN_SECONDS );
92
		}
93
94
		return $external_ip_address;
95
	}
96
97
	/**
98
	 * Geolocate an IP address.
99
	 *
100
	 * @param  string $ip_address   IP Address.
101
	 * @param  bool   $fallback     If true, fallbacks to alternative IP detection (can be slower).
102
	 * @param  bool   $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower).
103
	 * @return array
104
	 */
105
	public static function geolocate_ip( $ip_address = '', $fallback = false, $api_fallback = true ) {
106
107
		if ( empty( $ip_address ) ) {
108
			$ip_address = self::get_ip_address();
109
		}
110
111
		// Update the current user's IP Address.
112
		self::$current_user_ip = $ip_address;
113
114
		// Filter to allow custom geolocation of the IP address.
115
		$country_code = apply_filters( 'getpaid_geolocate_ip', false, $ip_address, $fallback, $api_fallback );
116
117
		if ( false !== $country_code ) {
118
119
			return array(
120
				'country'  => $country_code,
121
				'state'    => '',
122
				'city'     => '',
123
				'postcode' => '',
124
			);
125
126
		}
127
128
		$country_code = self::get_country_code_from_headers();
129
130
		/**
131
		 * Get geolocation filter.
132
		 *
133
		 * @since 1.0.19
134
		 * @param array  $geolocation Geolocation data, including country, state, city, and postcode.
135
		 * @param string $ip_address  IP Address.
136
		 */
137
		$geolocation  = apply_filters(
138
			'getpaid_get_geolocation',
139
			array(
140
				'country'  => $country_code,
141
				'state'    => '',
142
				'city'     => '',
143
				'postcode' => '',
144
			),
145
			$ip_address
146
		);
147
148
		// If we still haven't found a country code, let's consider doing an API lookup.
149
		if ( '' === $geolocation['country'] && $api_fallback ) {
150
			$geolocation['country'] = self::geolocate_via_api( $ip_address );
151
		}
152
153
		// It's possible that we're in a local environment, in which case the geolocation needs to be done from the
154
		// external address.
155
		if ( '' === $geolocation['country'] && $fallback ) {
156
			$external_ip_address = self::get_external_ip_address();
157
158
			// Only bother with this if the external IP differs.
159
			if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) {
160
				return self::geolocate_ip( $external_ip_address, false, $api_fallback );
161
			}
162
163
		}
164
165
		return array(
166
			'country'  => $geolocation['country'],
167
			'state'    => $geolocation['state'],
168
			'city'     => $geolocation['city'],
169
			'postcode' => $geolocation['postcode'],
170
		);
171
172
	}
173
174
	/**
175
	 * Fetches the country code from the request headers, if one is available.
176
	 *
177
	 * @since 1.0.19
178
	 * @return string The country code pulled from the headers, or empty string if one was not found.
179
	 */
180
	protected static function get_country_code_from_headers() {
181
		$country_code = '';
182
183
		$headers = array(
184
			'MM_COUNTRY_CODE',
185
			'GEOIP_COUNTRY_CODE',
186
			'HTTP_CF_IPCOUNTRY',
187
			'HTTP_X_COUNTRY_CODE',
188
		);
189
190
		foreach ( $headers as $header ) {
191
			if ( empty( $_SERVER[ $header ] ) ) {
192
				continue;
193
			}
194
195
			$country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
0 ignored issues
show
Bug introduced by
It seems like wp_unslash($_SERVER[$header]) can also be of type string[]; however, parameter $str of sanitize_text_field() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

195
			$country_code = strtoupper( sanitize_text_field( /** @scrutinizer ignore-type */ wp_unslash( $_SERVER[ $header ] ) ) );
Loading history...
196
			break;
197
		}
198
199
		return $country_code;
200
	}
201
202
	/**
203
	 * Use APIs to Geolocate the user.
204
	 *
205
	 * Geolocation APIs can be added through the use of the getpaid_geolocation_geoip_apis filter.
206
	 * Provide a name=>value pair for service-slug=>endpoint.
207
	 *
208
	 * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result
209
	 * will be cached in a transient.
210
	 *
211
	 * @param  string $ip_address IP address.
212
	 * @return string
213
	 */
214
	protected static function geolocate_via_api( $ip_address ) {
215
216
		// Retrieve from cache...
217
		$country_code = get_transient( 'geoip_' . $ip_address );
218
219
		// If missing, retrieve from the API.
220
		if ( false === $country_code ) {
221
			$geoip_services = apply_filters( 'getpaid_geolocation_geoip_apis', self::$geoip_apis );
222
223
			if ( empty( $geoip_services ) ) {
224
				return '';
225
			}
226
227
			$geoip_services_keys = array_keys( $geoip_services );
228
229
			shuffle( $geoip_services_keys );
230
231
			foreach ( $geoip_services_keys as $service_name ) {
232
233
				$service_endpoint = $geoip_services[ $service_name ];
234
				$response         = wp_safe_remote_get( sprintf( $service_endpoint, $ip_address ), array( 'timeout' => 2 ) );
235
				$country_code     = sanitize_text_field( strtoupper( self::handle_geolocation_response( $response, $service_name ) ) );
0 ignored issues
show
Bug introduced by
It seems like $response can also be of type array; however, parameter $geolocation_response of GetPaid_Geolocation::handle_geolocation_response() does only seem to accept WP_Error|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
				$country_code     = sanitize_text_field( strtoupper( self::handle_geolocation_response( /** @scrutinizer ignore-type */ $response, $service_name ) ) );
Loading history...
236
237
				if ( ! empty( $country_code ) ) {
238
					break;
239
				}
240
241
			}
242
243
			set_transient( 'geoip_' . $ip_address, $country_code, WEEK_IN_SECONDS );
244
		}
245
246
		return $country_code;
247
	}
248
249
	/**
250
	 * Handles geolocation response
251
	 *
252
	 * @param  WP_Error|String $geolocation_response
253
	 * @param  String $geolocation_service
254
	 * @return string Country code
255
	 */
256
	protected static function handle_geolocation_response( $geolocation_response, $geolocation_service ) {
257
258
		if ( is_wp_error( $geolocation_response ) || empty( $geolocation_response['body'] ) ) {
259
			return '';
260
		}
261
262
		if ( $geolocation_service === 'ipinfo.io' ) {
263
			$data = json_decode( $geolocation_response['body'] );
264
			return empty( $data ) ? '' : $data->country;
265
		}
266
267
		if ( $geolocation_service === 'ip-api.com' ) {
268
			$data = json_decode( $geolocation_response['body'] );
269
			return empty( $data ) ? '' : $data->countryCode;
270
		}
271
272
		return apply_filters( 'getpaid_geolocation_geoip_response_' . $geolocation_service, '', $geolocation_response['body'] );
273
274
	}
275
276
}
277