1: <?php
2:
3: namespace GeoIp2\WebService;
4:
5: use GeoIp2\Exception\AddressNotFoundException;
6: use GeoIp2\Exception\AuthenticationException;
7: use GeoIp2\Exception\GeoIp2Exception;
8: use GeoIp2\Exception\HttpException;
9: use GeoIp2\Exception\InvalidRequestException;
10: use GeoIp2\Exception\OutOfQueriesException;
11: use GeoIp2\ProviderInterface;
12: use Guzzle\Common\Exception\RuntimeException;
13: use Guzzle\Http\Client as GuzzleClient;
14: use Guzzle\Http\Exception\ClientErrorResponseException;
15: use Guzzle\Http\Exception\ServerErrorResponseException;
16:
17: /**
18: * This class provides a client API for all the GeoIP2 Precision web service
19: * end points. The end points are Country, City, and Insights. Each end point
20: * returns a different set of data about an IP address, with Country returning
21: * the least data and Insights the most.
22: *
23: * Each web service end point is represented by a different model class, and
24: * these model classes in turn contain multiple Record classes. The record
25: * classes have attributes which contain data about the IP address.
26: *
27: * If the web service does not return a particular piece of data for an IP
28: * address, the associated attribute is not populated.
29: *
30: * The web service may not return any information for an entire record, in
31: * which case all of the attributes for that record class will be empty.
32: *
33: * **Usage**
34: *
35: * The basic API for this class is the same for all of the web service end
36: * points. First you create a web service object with your MaxMind
37: * <code>$userId</code> and <code>$licenseKey</code>, then you call the method
38: * corresponding to a specific end point, passing it the IP address you want
39: * to look up.
40: *
41: * If the request succeeds, the method call will return a model class for
42: * the end point you called. This model in turn contains multiple record
43: * classes, each of which represents part of the data returned by the web
44: * service.
45: *
46: * If the request fails, the client class throws an exception.
47: */
48: class Client implements ProviderInterface
49: {
50: private $userId;
51: private $licenseKey;
52: private $locales;
53: private $host;
54: private $guzzleClient;
55:
56: /**
57: * Constructor.
58: *
59: * @param int $userId Your MaxMind user ID
60: * @param string $licenseKey Your MaxMind license key
61: * @param array $locales List of locale codes to use in name property
62: * from most preferred to least preferred.
63: * @param string $host Optional host parameter
64: * @param object $guzzleClient Optional Guzzle client to use (to facilitate
65: * unit testing).
66: */
67: public function __construct(
68: $userId,
69: $licenseKey,
70: $locales = array('en'),
71: $host = 'geoip.maxmind.com',
72: $guzzleClient = null
73: ) {
74: $this->userId = $userId;
75: $this->licenseKey = $licenseKey;
76: $this->locales = $locales;
77: $this->host = $host;
78: // To enable unit testing
79: $this->guzzleClient = $guzzleClient;
80: }
81:
82: /**
83: * This method calls the GeoIP2 Precision: City endpoint.
84: *
85: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
86: * address is provided, the address that the web service is called
87: * from will be used.
88: *
89: * @return \GeoIp2\Model\City
90: *
91: * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
92: * provided is not in our database (e.g., a private address).
93: * @throws \GeoIp2\Exception\AuthenticationException if there is a problem
94: * with the user ID or license key that you provided.
95: * @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
96: * of queries.
97: * @throws \GeoIp2\Exception\InvalidRequestException} if your request was
98: * received by the web service but is invalid for some other reason.
99: * This may indicate an issue with this API. Please report the error to
100: * MaxMind.
101: * @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
102: * code or message was returned. This could indicate a problem with the
103: * connection between your server and the web service or that the web
104: * service returned an invalid document or 500 error code.
105: * @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
106: * class to the above exceptions. It will be thrown directly if a 200
107: * status code is returned but the body is invalid.
108: */
109: public function city($ipAddress = 'me')
110: {
111: return $this->responseFor('city', 'City', $ipAddress);
112: }
113:
114: /**
115: * This method calls the GeoIP2 Precision: City endpoint.
116: *
117: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
118: * address is provided, the address that the web service is called
119: * from will be used.
120: *
121: * @return \GeoIp2\Model\City
122: *
123: * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
124: * provided is not in our database (e.g., a private address).
125: * @throws \GeoIp2\Exception\AuthenticationException if there is a problem
126: * with the user ID or license key that you provided.
127: * @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
128: * of queries.
129: * @throws \GeoIp2\Exception\InvalidRequestException} if your request was
130: * received by the web service but is invalid for some other reason.
131: * This may indicate an issue with this API. Please report the error to
132: * MaxMind.
133: * @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
134: * code or message was returned. This could indicate a problem with the
135: * connection between your server and the web service or that the web
136: * service returned an invalid document or 500 error code.
137: * @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
138: * class to the above exceptions. It will be thrown directly if a 200
139: * status code is returned but the body is invalid.
140: *
141: * @deprecated deprecated since version 0.7.0
142: */
143: public function cityIspOrg($ipAddress = 'me')
144: {
145: return $this->city($ipAddress);
146: }
147:
148: /**
149: * This method calls the GeoIP2 Precision: Country endpoint.
150: *
151: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
152: * address is provided, the address that the web service is called
153: * from will be used.
154: *
155: * @return \GeoIp2\Model\Country
156: *
157: * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
158: * provided is not in our database (e.g., a private address).
159: * @throws \GeoIp2\Exception\AuthenticationException if there is a problem
160: * with the user ID or license key that you provided.
161: * @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
162: * of queries.
163: * @throws \GeoIp2\Exception\InvalidRequestException} if your request was
164: * received by the web service but is invalid for some other reason.
165: * This may indicate an issue with this API. Please report the error to
166: * MaxMind.
167: * @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
168: * code or message was returned. This could indicate a problem with the
169: * connection between your server and the web service or that the web
170: * service returned an invalid document or 500 error code.
171: * @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
172: * class to the above exceptions. It will be thrown directly if a 200
173: * status code is returned but the body is invalid.
174: */
175: public function country($ipAddress = 'me')
176: {
177: return $this->responseFor('country', 'Country', $ipAddress);
178: }
179:
180: /**
181: * This method calls the GeoIP2 Precision: Insights endpoint.
182: *
183: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
184: * address is provided, the address that the web service is called
185: * from will be used.
186: *
187: * @return \GeoIp2\Model\Insights
188: *
189: * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
190: * provided is not in our database (e.g., a private address).
191: * @throws \GeoIp2\Exception\AuthenticationException if there is a problem
192: * with the user ID or license key that you provided.
193: * @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
194: * of queries.
195: * @throws \GeoIp2\Exception\InvalidRequestException} if your request was
196: * received by the web service but is invalid for some other reason.
197: * This may indicate an issue with this API. Please report the error to
198: * MaxMind.
199: * @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
200: * code or message was returned. This could indicate a problem with the
201: * connection between your server and the web service or that the web
202: * service returned an invalid document or 500 error code.
203: * @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
204: * class to the above exceptions. It will be thrown directly if a 200
205: * status code is returned but the body is invalid.
206: *
207: * @deprecated deprecated since version 0.7.0
208: */
209: public function insights($ipAddress = 'me')
210: {
211: return $this->responseFor('insights', 'Insights', $ipAddress);
212: }
213:
214: /**
215: * This method calls the GeoIP2 Precision: Insights (prev. Omni) endpoint.
216: *
217: * @param string $ipAddress IPv4 or IPv6 address as a string. If no
218: * address is provided, the address that the web service is called
219: * from will be used.
220: *
221: * @return \GeoIp2\Model\Insights
222: *
223: * @throws \GeoIp2\Exception\AddressNotFoundException if the address you
224: * provided is not in our database (e.g., a private address).
225: * @throws \GeoIp2\Exception\AuthenticationException if there is a problem
226: * with the user ID or license key that you provided.
227: * @throws \GeoIp2\Exception\OutOfQueriesException if your account is out
228: * of queries.
229: * @throws \GeoIp2\Exception\InvalidRequestException} if your request was
230: * received by the web service but is invalid for some other reason.
231: * This may indicate an issue with this API. Please report the error to
232: * MaxMind.
233: * @throws \GeoIp2\Exception\HttpException if an unexpected HTTP error
234: * code or message was returned. This could indicate a problem with the
235: * connection between your server and the web service or that the web
236: * service returned an invalid document or 500 error code.
237: * @throws \GeoIp2\Exception\GeoIp2Exception This serves as the parent
238: * class to the above exceptions. It will be thrown directly if a 200
239: * status code is returned but the body is invalid.
240: *
241: * @deprecated deprecated since version 0.7.0
242: */
243: public function omni($ipAddress = 'me')
244: {
245: return $this->insights($ipAddress);
246: }
247:
248: private function responseFor($endpoint, $class, $ipAddress)
249: {
250: $uri = implode('/', array($this->baseUri(), $endpoint, $ipAddress));
251:
252: $client = $this->guzzleClient ?
253: $this->guzzleClient : new GuzzleClient();
254: $request = $client->get($uri, array('Accept' => 'application/json'));
255: $request->setAuth($this->userId, $this->licenseKey);
256: $this->setUserAgent($request);
257:
258: try {
259: $response = $request->send();
260: } catch (ClientErrorResponseException $e) {
261: $this->handle4xx($e->getResponse(), $uri);
262: } catch (ServerErrorResponseException $e) {
263: $this->handle5xx($e->getResponse(), $uri);
264: }
265:
266: if ($response && $response->isSuccessful()) {
267: $body = $this->handleSuccess($response, $uri);
268: $class = "GeoIp2\\Model\\" . $class;
269: return new $class($body, $this->locales);
270: } else {
271: $this->handleNon200($response, $uri);
272: }
273: }
274:
275: private function handleSuccess($response, $uri)
276: {
277: if ($response->getContentLength() == 0) {
278: throw new GeoIp2Exception(
279: "Received a 200 response for $uri but did not " .
280: "receive a HTTP body."
281: );
282: }
283:
284: try {
285: return $response->json();
286: } catch (RuntimeException $e) {
287: throw new GeoIp2Exception(
288: "Received a 200 response for $uri but could not decode " .
289: "the response as JSON: " . $e->getMessage()
290: );
291:
292: }
293: }
294:
295: private function handle4xx($response, $uri)
296: {
297: $status = $response->getStatusCode();
298:
299: if ($response->getContentLength() > 0) {
300: if (strstr($response->getContentType(), 'json')) {
301: try {
302: $body = $response->json();
303: if (!isset($body['code']) || !isset($body['error'])) {
304: throw new GeoIp2Exception(
305: 'Response contains JSON but it does not specify ' .
306: 'code or error keys: ' . $response->getBody()
307: );
308: }
309: } catch (RuntimeException $e) {
310: throw new HttpException(
311: "Received a $status error for $uri but it did not " .
312: "include the expected JSON body: " .
313: $e->getMessage(),
314: $status,
315: $uri
316: );
317: }
318: } else {
319: throw new HttpException(
320: "Received a $status error for $uri with the " .
321: "following body: " . $response->getBody(),
322: $status,
323: $uri
324: );
325: }
326: } else {
327: throw new HttpException(
328: "Received a $status error for $uri with no body",
329: $status,
330: $uri
331: );
332: }
333: $this->handleWebServiceError(
334: $body['error'],
335: $body['code'],
336: $status,
337: $uri
338: );
339: }
340:
341: private function handleWebServiceError($message, $code, $status, $uri)
342: {
343: switch ($code) {
344: case 'IP_ADDRESS_NOT_FOUND':
345: case 'IP_ADDRESS_RESERVED':
346: throw new AddressNotFoundException($message);
347: case 'AUTHORIZATION_INVALID':
348: case 'LICENSE_KEY_REQUIRED':
349: case 'USER_ID_REQUIRED':
350: throw new AuthenticationException($message);
351: case 'OUT_OF_QUERIES':
352: throw new OutOfQueriesException($message);
353: default:
354: throw new InvalidRequestException(
355: $message,
356: $code,
357: $status,
358: $uri
359: );
360: }
361: }
362:
363: private function handle5xx($response, $uri)
364: {
365: $status = $response->getStatusCode();
366:
367: throw new HttpException(
368: "Received a server error ($status) for $uri",
369: $status,
370: $uri
371: );
372: }
373:
374: private function handleNon200($response, $uri)
375: {
376: $status = $response->getStatusCode();
377:
378: throw new HttpException(
379: "Received a very surprising HTTP status " .
380: "($status) for $uri",
381: $status,
382: $uri
383: );
384: }
385:
386: private function setUserAgent($request)
387: {
388: $userAgent = $request->getHeader('User-Agent');
389: $userAgent = "GeoIP2 PHP API ($userAgent)";
390: $request->setHeader('User-Agent', $userAgent);
391: }
392:
393: private function baseUri()
394: {
395: return 'https://' . $this->host . '/geoip/v2.1';
396: }
397: }
398: