Skip to content

Commit edb2a08

Browse files
committed
Add support for Core bundle
1 parent fb20c47 commit edb2a08

File tree

3 files changed

+518
-0
lines changed

3 files changed

+518
-0
lines changed

src/DetailsCore.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace ipinfo\ipinfo;
4+
5+
/**
6+
* Holds formatted data received from Core API for a single IP address.
7+
*/
8+
class DetailsCore
9+
{
10+
public $ip;
11+
public $geo;
12+
public $asn;
13+
public $is_anonymous;
14+
public $is_anycast;
15+
public $is_hosting;
16+
public $is_mobile;
17+
public $is_satellite;
18+
public $bogon;
19+
public $all;
20+
21+
public function __construct($raw_details)
22+
{
23+
foreach ($raw_details as $property => $value) {
24+
// Handle nested 'as' object - rename to 'asn' to avoid PHP keyword
25+
if ($property === 'as') {
26+
$this->asn = (object) $value;
27+
} elseif ($property === 'geo') {
28+
$this->geo = (object) $value;
29+
} else {
30+
$this->$property = $value;
31+
}
32+
}
33+
$this->all = $raw_details;
34+
}
35+
36+
/**
37+
* Returns json string representation.
38+
*
39+
* @internal this class should implement Stringable explicitly when leaving support for PHP verision < 8.0
40+
*/
41+
public function __toString(): string
42+
{
43+
return json_encode($this);
44+
}
45+
}

src/IPinfoCore.php

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
<?php
2+
3+
namespace ipinfo\ipinfo;
4+
5+
require_once __DIR__ . "/Const.php";
6+
7+
use Exception;
8+
use ipinfo\ipinfo\cache\DefaultCache;
9+
use GuzzleHttp\Client;
10+
use Symfony\Component\HttpFoundation\IpUtils;
11+
12+
/**
13+
* Exposes the IPinfo Core API to client code.
14+
*/
15+
class IPinfoCore
16+
{
17+
const API_URL = "https://api.ipinfo.io/lookup";
18+
const COUNTRY_FLAG_URL = "https://cdn.ipinfo.io/static/images/countries-flags/";
19+
const STATUS_CODE_QUOTA_EXCEEDED = 429;
20+
const REQUEST_TIMEOUT_DEFAULT = 2; // seconds
21+
22+
const CACHE_MAXSIZE = 4096;
23+
const CACHE_TTL = 86400; // 24 hours as seconds
24+
const CACHE_KEY_VSN = "1"; // update when cache vals change for same key.
25+
26+
const COUNTRIES_DEFAULT = COUNTRIES;
27+
const EU_COUNTRIES_DEFAULT = EU;
28+
const COUNTRIES_FLAGS_DEFAULT = FLAGS;
29+
const COUNTRIES_CURRENCIES_DEFAULT = CURRENCIES;
30+
const CONTINENTS_DEFAULT = CONTINENTS;
31+
32+
public $access_token;
33+
public $settings;
34+
public $cache;
35+
public $countries;
36+
public $eu_countries;
37+
public $countries_flags;
38+
public $countries_currencies;
39+
public $continents;
40+
protected $http_client;
41+
42+
public function __construct($access_token = null, $settings = [])
43+
{
44+
$this->access_token = $access_token;
45+
$this->settings = $settings;
46+
47+
/*
48+
Support a timeout first-class, then a `guzzle_opts` key that can
49+
override anything.
50+
*/
51+
$guzzle_opts = [
52+
"http_errors" => false,
53+
"headers" => $this->buildHeaders(),
54+
"timeout" => $settings["timeout"] ?? self::REQUEST_TIMEOUT_DEFAULT,
55+
];
56+
if (isset($settings["guzzle_opts"])) {
57+
$guzzle_opts = array_merge($guzzle_opts, $settings["guzzle_opts"]);
58+
}
59+
$this->http_client = new Client($guzzle_opts);
60+
61+
$this->countries = $settings["countries"] ?? self::COUNTRIES_DEFAULT;
62+
$this->countries_flags =
63+
$settings["countries_flags"] ?? self::COUNTRIES_FLAGS_DEFAULT;
64+
$this->countries_currencies =
65+
$settings["countries_currencies"] ??
66+
self::COUNTRIES_CURRENCIES_DEFAULT;
67+
$this->eu_countries =
68+
$settings["eu_countries"] ?? self::EU_COUNTRIES_DEFAULT;
69+
$this->continents = $settings["continents"] ?? self::CONTINENTS_DEFAULT;
70+
71+
if (
72+
!array_key_exists("cache_disabled", $this->settings) ||
73+
$this->settings["cache_disabled"] == false
74+
) {
75+
if (array_key_exists("cache", $settings)) {
76+
$this->cache = $settings["cache"];
77+
} else {
78+
$maxsize = $settings["cache_maxsize"] ?? self::CACHE_MAXSIZE;
79+
$ttl = $settings["cache_ttl"] ?? self::CACHE_TTL;
80+
$this->cache = new DefaultCache($maxsize, $ttl);
81+
}
82+
} else {
83+
$this->cache = null;
84+
}
85+
}
86+
87+
/**
88+
* Get formatted details for an IP address.
89+
* @param string|null $ip_address IP address to look up.
90+
* @return DetailsCore Formatted IPinfo data.
91+
* @throws IPinfoException
92+
*/
93+
public function getDetails($ip_address = null)
94+
{
95+
$response_details = $this->getRequestDetails((string) $ip_address);
96+
return $this->formatDetailsObject($response_details);
97+
}
98+
99+
public function formatDetailsObject($details = [])
100+
{
101+
// Enrich geo object if present
102+
if (isset($details["geo"]) && isset($details["geo"]["country_code"])) {
103+
$country_code = $details["geo"]["country_code"];
104+
$details["geo"]["country_name"] = $this->countries[$country_code] ?? null;
105+
$details["geo"]["is_eu"] = in_array($country_code, $this->eu_countries);
106+
$details["geo"]["country_flag"] =
107+
$this->countries_flags[$country_code] ?? null;
108+
$details["geo"]["country_flag_url"] =
109+
self::COUNTRY_FLAG_URL . $country_code . ".svg";
110+
$details["geo"]["country_currency"] =
111+
$this->countries_currencies[$country_code] ?? null;
112+
$details["geo"]["continent_info"] =
113+
$this->continents[$country_code] ?? null;
114+
}
115+
116+
return new DetailsCore($details);
117+
}
118+
119+
/**
120+
* Get details for a specific IP address.
121+
* @param string $ip_address IP address to query API for.
122+
* @return array IP response data.
123+
* @throws IPinfoException
124+
*/
125+
public function getRequestDetails(string $ip_address)
126+
{
127+
if (
128+
// Avoid checking if bogon if the user provided no IP or explicitly
129+
// set it to "me" to get its IP info
130+
$ip_address &&
131+
$ip_address != "me" &&
132+
$this->isBogon($ip_address)
133+
) {
134+
return [
135+
"ip" => $ip_address,
136+
"bogon" => true,
137+
];
138+
}
139+
140+
if ($this->cache != null) {
141+
$cachedRes = $this->cache->get($this->cacheKey($ip_address));
142+
if ($cachedRes != null) {
143+
return $cachedRes;
144+
}
145+
}
146+
147+
$url = self::API_URL;
148+
if ($ip_address) {
149+
$url .= "/$ip_address";
150+
} else {
151+
$url .= "/me";
152+
}
153+
154+
try {
155+
$response = $this->http_client->request("GET", $url);
156+
} catch (GuzzleException $e) {
157+
throw new IPinfoException($e->getMessage());
158+
} catch (Exception $e) {
159+
throw new IPinfoException($e->getMessage());
160+
}
161+
162+
if ($response->getStatusCode() == self::STATUS_CODE_QUOTA_EXCEEDED) {
163+
throw new IPinfoException("IPinfo request quota exceeded.");
164+
} elseif ($response->getStatusCode() >= 400) {
165+
throw new IPinfoException(
166+
"Exception: " .
167+
json_encode([
168+
"status" => $response->getStatusCode(),
169+
"reason" => $response->getReasonPhrase(),
170+
])
171+
);
172+
}
173+
174+
$raw_details = json_decode($response->getBody(), true);
175+
176+
if ($this->cache != null) {
177+
$this->cache->set($this->cacheKey($ip_address), $raw_details);
178+
}
179+
180+
return $raw_details;
181+
}
182+
183+
/**
184+
* Build headers for API request.
185+
* @return array Headers for API request.
186+
*/
187+
private function buildHeaders()
188+
{
189+
$headers = [
190+
"user-agent" => "IPinfoClient/PHP/3.2.0",
191+
"accept" => "application/json",
192+
"content-type" => "application/json",
193+
];
194+
195+
if ($this->access_token) {
196+
$headers["authorization"] = "Bearer {$this->access_token}";
197+
}
198+
199+
return $headers;
200+
}
201+
202+
/**
203+
* Check if IP address is bogon.
204+
* @param string $ip_address IP address.
205+
* @return bool true if bogon, else false.
206+
*/
207+
private function isBogon(string $ip_address)
208+
{
209+
return IpUtils::checkIp($ip_address, [
210+
"0.0.0.0/8",
211+
"10.0.0.0/8",
212+
"100.64.0.0/10",
213+
"127.0.0.0/8",
214+
"169.254.0.0/16",
215+
"172.16.0.0/12",
216+
"192.0.0.0/24",
217+
"192.0.2.0/24",
218+
"192.168.0.0/16",
219+
"198.18.0.0/15",
220+
"198.51.100.0/24",
221+
"203.0.113.0/24",
222+
"224.0.0.0/4",
223+
"240.0.0.0/4",
224+
"255.255.255.255/32",
225+
"::/128",
226+
"::1/128",
227+
"::ffff:0:0/96",
228+
"::/96",
229+
"100::/64",
230+
"2001:10::/28",
231+
"2001:db8::/32",
232+
"fc00::/7",
233+
"fe80::/10",
234+
"fec0::/10",
235+
"ff00::/8",
236+
"2002::/24",
237+
"2002:a00::/24",
238+
"2002:7f00::/24",
239+
"2002:a9fe::/32",
240+
"2002:ac10::/28",
241+
"2002:c000::/40",
242+
"2002:c000:200::/40",
243+
"2002:c0a8::/32",
244+
"2002:c612::/31",
245+
"2002:c633:6400::/40",
246+
"2002:cb00:7100::/40",
247+
"2002:e000::/20",
248+
"2002:f000::/20",
249+
"2002:ffff:ffff::/48",
250+
"2001::/40",
251+
"2001:0:a00::/40",
252+
"2001:0:7f00::/40",
253+
"2001:0:a9fe::/48",
254+
"2001:0:ac10::/44",
255+
"2001:0:c000::/56",
256+
"2001:0:c000:200::/56",
257+
"2001:0:c0a8::/48",
258+
"2001:0:c612::/47",
259+
"2001:0:c633:6400::/56",
260+
"2001:0:cb00:7100::/56",
261+
"2001:0:e000::/36",
262+
"2001:0:f000::/36",
263+
"2001:0:ffff:ffff::/64",
264+
]);
265+
}
266+
267+
/**
268+
* Generate cache key for an IP address.
269+
* @param string $ip IP address.
270+
* @return string Cache key.
271+
*/
272+
private function cacheKey(string $ip)
273+
{
274+
return "core_" . self::CACHE_KEY_VSN . "_" . $ip;
275+
}
276+
}

0 commit comments

Comments
 (0)