PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.2.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.2.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / utils / traits / geolocation.php

geolocation.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.2.0, at includes/utils/traits/geolocation.php

262 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * GeoLocation Helper API trait.
4 *
5 * @version 1.0.0
6 * @since 1.5.6
7 *
8 * @package StoreEngine/Utils/traits
9 */
10
11 namespace StoreEngine\Utils\traits;
12
13 use StoreEngine\Utils\Formatting;
14 use StoreEngine\Utils\Helper;
15
16 trait Geolocation {
17 /**
18 * API endpoints for looking up user IP address.
19 *
20 * @var array
21 */
22 private static array $ip_lookup_apis = [
23 'ipify' => 'http://api.ipify.org/',
24 'ipecho' => 'http://ipecho.net/plain',
25 'ident' => 'http://ident.me',
26 'tnedi' => 'http://tnedi.me',
27 ];
28
29 /**
30 * API endpoints for geolocating an IP address
31 *
32 * @var array
33 */
34 private static array $geoip_apis = [
35 'ipinfo.io' => 'http://ipinfo.io/%s/json',
36 'ip-api.com' => 'http://ip-api.com/json/%s',
37 'freeipapi.com' => 'http://freeipapi.com/api/json/%s',
38 'ipapi.co' => 'http://ipapi.co/%s/json/',
39 'ipwhois.app' => 'https://ipwhois.app/json/%s',
40 ];
41
42 /**
43 * Check if geolocation is enabled.
44 *
45 * @return bool
46 */
47 public static function is_geolocation_enabled(): bool {
48 return in_array( Helper::get_settings( 'default_customer_address', 'store-base' ), [ 'ip-geolocation', 'geolocation' ], true );
49 }
50
51 /**
52 * Get user IP Address using an external service.
53 * This can be used as a fallback for users on localhost where
54 * get_ip_address() will be a local IP and non-geolocatable.
55 *
56 * @return string
57 */
58 public static function get_external_ip_address(): string {
59 $external_ip_address = '0.0.0.0';
60
61 if ( '' !== self::get_user_ip() ) {
62 $transient_name = 'external_ip_address_' . md5( self::get_user_ip() );
63 $external_ip_address = apply_filters( 'storeengine/geolocation/external_ip_address', get_transient( $transient_name ) );
64 }
65
66 if ( false === $external_ip_address ) {
67 $external_ip_address = '0.0.0.0';
68 $ip_lookup_services = apply_filters( 'storeengine/geolocation/ip_lookup_apis', self::$ip_lookup_apis );
69 $ip_lookup_services_keys = array_keys( $ip_lookup_services );
70 shuffle( $ip_lookup_services_keys );
71
72 foreach ( $ip_lookup_services_keys as $service_name ) {
73 $response = wp_safe_remote_get( $ip_lookup_services[ $service_name ], [
74 'timeout' => 2,
75 'user-agent' => 'StoreEngine/' . STOREENGINE_VERSION,
76 ] );
77
78 if ( ! is_wp_error( $response ) && rest_is_ip_address( $response['body'] ) ) {
79 $external_ip_address = apply_filters( 'storeengine/geolocation/ip_lookup_api_response', Formatting::clean( $response['body'] ), $service_name );
80 break;
81 }
82 }
83
84 /** @noinspection PhpUndefinedVariableInspection */
85 set_transient( $transient_name, $external_ip_address, DAY_IN_SECONDS );
86 }
87
88 return $external_ip_address;
89 }
90
91 /**
92 * Geolocate an IP address.
93 *
94 * @param string $ip_address IP Address.
95 * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower).
96 * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower).
97 *
98 * @return array
99 */
100 public static function geolocate_ip( string $ip_address = '', bool $fallback = false, bool $api_fallback = true ): array {
101 /**
102 * Filter to allow custom geolocation of the IP address.
103 *
104 * @param string $geolocation Country code.
105 * @param string $ip_address IP Address.
106 * @param bool $fallback If true, fallbacks to alternative IP detection (can be slower).
107 * @param bool $api_fallback If true, uses geolocation APIs if the database file doesn't exist (can be slower).
108 * @return string
109 */
110 $country_code = apply_filters( 'storeengine/geolocation/pre-geo-locate-ip', false, $ip_address, $fallback, $api_fallback );
111 if ( false !== $country_code ) {
112 return [
113 'country' => $country_code,
114 'state' => '',
115 'city' => '',
116 'postcode' => '',
117 ];
118 }
119
120 if ( empty( $ip_address ) ) {
121 $ip_address = self::get_user_ip();
122 $country_code = self::get_country_code_from_headers();
123 }
124
125 /**
126 * Get geolocation filter.
127 *
128 * @param array $geolocation Geolocation data, including country, state, city, and postcode.
129 * @param string $ip_address IP Address.
130 */
131 $geolocation = apply_filters(
132 'storeengine/geolocation/geo-locate-ip',
133 [
134 'country' => $country_code,
135 'state' => '',
136 'city' => '',
137 'postcode' => '',
138 ],
139 $ip_address
140 );
141
142 // If we still haven't found a country code, let's consider doing an API lookup.
143 if ( '' === $geolocation['country'] && $api_fallback ) {
144 $geolocation['country'] = self::geolocate_via_api( $ip_address );
145 }
146
147 // It's possible that we're in a local environment, in which case the geolocation needs to be done from the
148 // external address.
149 if ( '' === $geolocation['country'] && $fallback ) {
150 $external_ip_address = self::get_external_ip_address();
151
152 // Only bother with this if the external IP differs.
153 if ( '0.0.0.0' !== $external_ip_address && $external_ip_address !== $ip_address ) {
154 return self::geolocate_ip( $external_ip_address, false, $api_fallback );
155 }
156 }
157
158 return [
159 'country' => $geolocation['country'],
160 'state' => $geolocation['state'],
161 'city' => $geolocation['city'],
162 'postcode' => $geolocation['postcode'],
163 ];
164 }
165
166 /**
167 * Fetches the country code from the request headers, if one is available.
168 *
169 * @return string The country code pulled from the headers, or empty string if one was not found.
170 */
171 private static function get_country_code_from_headers() {
172 $country_code = '';
173
174 $headers = [
175 'MM_COUNTRY_CODE',
176 'GEOIP_COUNTRY_CODE',
177 'HTTP_CF_IPCOUNTRY',
178 'HTTP_X_COUNTRY_CODE',
179 ];
180
181 foreach ( $headers as $header ) {
182 if ( empty( $_SERVER[ $header ] ) ) {
183 continue;
184 }
185
186 $country_code = strtoupper( sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) );
187 break;
188 }
189
190 return $country_code;
191 }
192
193 /**
194 * Use APIs to Geolocate the user.
195 *
196 * Geolocation APIs can be added through the use of the storeengine/geolocation/geoip_apis filter.
197 * Provide a name=>value pair for service-slug=>endpoint.
198 *
199 * If APIs are defined, one will be chosen at random to fulfil the request. After completing, the result
200 * will be cached in a transient.
201 *
202 * @param string $ip_address IP address.
203 * @return string
204 */
205 private static function geolocate_via_api( string $ip_address ) {
206 $country_code = get_transient( 'se_geoip_' . md5( $ip_address ) );
207
208 if ( false === $country_code ) {
209 $geoip_services = apply_filters( 'storeengine/geolocation/geoip_apis', self::$geoip_apis );
210
211 if ( empty( $geoip_services ) ) {
212 return '';
213 }
214
215 $geoip_services_keys = array_keys( $geoip_services );
216
217 shuffle( $geoip_services_keys );
218
219 foreach ( $geoip_services_keys as $service_name ) {
220 $response = wp_safe_remote_get( sprintf( $geoip_services[ $service_name ], $ip_address ), [
221 'timeout' => 2,
222 'user-agent' => 'StoreEngine/' . STOREENGINE_VERSION,
223 ] );
224
225 if ( ! is_wp_error( $response ) && $response['body'] ) {
226 switch ( $service_name ) {
227 case 'ipinfo.io':
228 case 'ipapi.co':
229 $data = json_decode( $response['body'] );
230 $country_code = $data->country ?? '';
231 break;
232 case 'ip-api.com':
233 case 'freeipapi.com':
234 $data = json_decode( $response['body'] );
235 $country_code = $data->countryCode ?? '';
236 break;
237 case 'ipwhois.app':
238 $data = json_decode( $response['body'] );
239 $country_code = $data->country_code ?? '';
240 break;
241 default:
242 $country_code = apply_filters( 'storeengine/geolocation/geoip_response_' . $service_name, '', $response['body'] );
243 break;
244 }
245
246 $country_code = sanitize_text_field( strtoupper( $country_code ) );
247
248 if ( $country_code ) {
249 break;
250 }
251 }
252 }
253
254 set_transient( 'se_geoip_' . md5( $ip_address ), $country_code, DAY_IN_SECONDS );
255 }
256
257 return $country_code;
258 }
259 }
260
261 // End of file geolocation.php.
262