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