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