Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
92.94% |
79 / 85 |
|
83.33% |
10 / 12 |
CRAP | |
0.00% |
0 / 1 |
GeoIp2EnterpriseInfoRetriever | |
92.94% |
79 / 85 |
|
83.33% |
10 / 12 |
28.28 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getReader | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
retrieveFromIP | |
100.00% |
45 / 45 |
|
100.00% |
1 / 1 |
6 | |||
getCoordinates | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getAsn | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOrganization | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCountryNames | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLocations | |
66.67% |
10 / 15 |
|
0.00% |
0 / 1 |
3.33 | |||
getIsp | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getConnectionType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getProxyType | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\IPInfo\InfoRetriever; |
4 | |
5 | use GeoIp2\Database\Reader; |
6 | use GeoIp2\Exception\AddressNotFoundException; |
7 | use GeoIp2\Model\AnonymousIp; |
8 | use GeoIp2\Model\Enterprise; |
9 | use MediaWiki\Config\ServiceOptions; |
10 | use MediaWiki\IPInfo\Info\Coordinates; |
11 | use MediaWiki\IPInfo\Info\Info; |
12 | use MediaWiki\IPInfo\Info\Location; |
13 | use MediaWiki\IPInfo\Info\ProxyType; |
14 | |
15 | /** |
16 | * Manager for getting information from the MaxMind GeoIp2 Enterprise database. |
17 | */ |
18 | class GeoIp2EnterpriseInfoRetriever implements InfoRetriever { |
19 | /** |
20 | * @internal For use by ServiceWiring |
21 | */ |
22 | public const CONSTRUCTOR_OPTIONS = [ |
23 | 'IPInfoGeoIP2EnterprisePath', |
24 | ]; |
25 | |
26 | /** @var ServiceOptions */ |
27 | private $options; |
28 | |
29 | /** @var ReaderFactory */ |
30 | private $readerFactory; |
31 | |
32 | /** |
33 | * @param ServiceOptions $options |
34 | * @param ReaderFactory $readerFactory |
35 | */ |
36 | public function __construct( |
37 | ServiceOptions $options, |
38 | ReaderFactory $readerFactory |
39 | ) { |
40 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
41 | $this->options = $options; |
42 | $this->readerFactory = $readerFactory; |
43 | } |
44 | |
45 | /** @inheritDoc */ |
46 | public function getName(): string { |
47 | return 'ipinfo-source-geoip2'; |
48 | } |
49 | |
50 | /** |
51 | * @param string $filename |
52 | * @return Reader|null null if the file path or file is invalid |
53 | * @codeCoverageIgnore tested when retrieveFromIP is run |
54 | */ |
55 | public function getReader( string $filename ): ?Reader { |
56 | $path = $this->options->get( 'IPInfoGeoIP2EnterprisePath' ); |
57 | |
58 | if ( $path === false ) { |
59 | return null; |
60 | } |
61 | |
62 | return $this->readerFactory->get( $path, $filename ); |
63 | } |
64 | |
65 | /** |
66 | * @inheritDoc |
67 | * @return Info |
68 | */ |
69 | public function retrieveFromIP( string $ip ): Info { |
70 | $info = array_fill_keys( |
71 | [ |
72 | 'coordinates', |
73 | 'asn', |
74 | 'organization', |
75 | 'countryNames', |
76 | 'locations', |
77 | 'isp', |
78 | 'connectionType', |
79 | 'userType', |
80 | 'proxyType', |
81 | ], |
82 | null |
83 | ); |
84 | |
85 | $enterpriseReader = $this->getReader( 'GeoIP2-Enterprise.mmdb' ); |
86 | if ( $enterpriseReader ) { |
87 | try { |
88 | $enterpriseInfo = $enterpriseReader->enterprise( $ip ); |
89 | |
90 | $info['coordinates'] = $this->getCoordinates( $enterpriseInfo ); |
91 | $info['asn'] = $this->getAsn( $enterpriseInfo ); |
92 | $info['organization'] = $this->getOrganization( $enterpriseInfo ); |
93 | $info['countryNames'] = $this->getCountryNames( $enterpriseInfo ); |
94 | $info['locations'] = $this->getLocations( $enterpriseInfo ); |
95 | $info['isp'] = $this->getIsp( $enterpriseInfo ); |
96 | $info['connectionType'] = $this->getConnectionType( $enterpriseInfo ); |
97 | $info['userType'] = $this->getUserType( $enterpriseInfo ); |
98 | } catch ( AddressNotFoundException $e ) { |
99 | // No need to do anything if it fails |
100 | // $info defaults to null values |
101 | } |
102 | } |
103 | |
104 | $anonymousIpReader = $this->getReader( 'GeoIP2-Anonymous-IP.mmdb' ); |
105 | if ( $anonymousIpReader ) { |
106 | try { |
107 | $anonymousIpInfo = $anonymousIpReader->anonymousIp( $ip ); |
108 | $isLegitimateProxy = null; |
109 | if ( isset( $enterpriseInfo ) ) { |
110 | $isLegitimateProxy = (bool)$enterpriseInfo->traits->isLegitimateProxy; |
111 | } |
112 | $info['proxyType'] = $this->getProxyType( $anonymousIpInfo, $isLegitimateProxy ); |
113 | } catch ( AddressNotFoundException $e ) { |
114 | // No need to do anything if it fails |
115 | // $info defaults to null values |
116 | } |
117 | } |
118 | |
119 | return new Info( |
120 | $info['coordinates'], |
121 | $info['asn'], |
122 | $info['organization'], |
123 | $info['countryNames'], |
124 | $info['locations'], |
125 | $info['isp'], |
126 | $info['connectionType'], |
127 | $info['userType'], |
128 | $info['proxyType'] |
129 | ); |
130 | } |
131 | |
132 | /** |
133 | * @param Enterprise $info |
134 | * @return Coordinates|null null if IP address does not return a latitude/longitude |
135 | */ |
136 | private function getCoordinates( Enterprise $info ): ?Coordinates { |
137 | $location = $info->location; |
138 | if ( !$location->latitude || !$location->longitude ) { |
139 | return null; |
140 | } |
141 | |
142 | return new Coordinates( |
143 | $location->latitude, |
144 | $location->longitude |
145 | ); |
146 | } |
147 | |
148 | /** |
149 | * @param Enterprise $info |
150 | * @return int|null null if this IP address does not return an ASN |
151 | */ |
152 | private function getAsn( Enterprise $info ): ?int { |
153 | return $info->traits->autonomousSystemNumber; |
154 | } |
155 | |
156 | /** |
157 | * @param Enterprise $info |
158 | * @return string|null null if this IP address does not return an organization |
159 | */ |
160 | private function getOrganization( Enterprise $info ): ?string { |
161 | return $info->traits->autonomousSystemOrganization; |
162 | } |
163 | |
164 | /** |
165 | * @param Enterprise $info |
166 | * @return array|null null if this IP address does not return a country |
167 | */ |
168 | private function getCountryNames( Enterprise $info ): ?array { |
169 | return $info->country->names; |
170 | } |
171 | |
172 | /** |
173 | * @param Enterprise $info |
174 | * @return Location[]|null null if this IP address does not return a location |
175 | */ |
176 | private function getLocations( Enterprise $info ): ?array { |
177 | if ( !$info->city->geonameId || !$info->city->name ) { |
178 | return null; |
179 | } |
180 | |
181 | $locations = [ new Location( |
182 | $info->city->geonameId, |
183 | $info->city->name |
184 | ) ]; |
185 | |
186 | /** MaxMind returns the locations sorted largest area to smallest. |
187 | * array_reverse is used to convert them to the preferred order of |
188 | * smallest to largest |
189 | */ |
190 | return array_merge( $locations, array_map( |
191 | static function ( $subdivision ) { |
192 | return new Location( |
193 | $subdivision->geonameId, |
194 | $subdivision->name |
195 | ); |
196 | }, |
197 | array_reverse( $info->subdivisions ) |
198 | ) ); |
199 | } |
200 | |
201 | /** |
202 | * @param Enterprise $info |
203 | * @return string|null null if GeoIP2 does not return an ISP |
204 | */ |
205 | private function getIsp( Enterprise $info ): ?string { |
206 | return $info->traits->isp; |
207 | } |
208 | |
209 | /** |
210 | * @param Enterprise $info |
211 | * @return string|null null if GeoIP2 does not return a connection type |
212 | */ |
213 | public function getConnectionType( Enterprise $info ): ?string { |
214 | return $info->traits->connectionType; |
215 | } |
216 | |
217 | /** |
218 | * @param Enterprise $info |
219 | * @return string|null null if GeoIP2 does not return a connection type |
220 | */ |
221 | private function getUserType( Enterprise $info ): ?string { |
222 | return $info->traits->userType; |
223 | } |
224 | |
225 | /** |
226 | * @param AnonymousIp $anonymousIpinfo |
227 | * @param bool|null $isLegitimateProxy |
228 | * @return ProxyType |
229 | */ |
230 | private function getProxyType( AnonymousIp $anonymousIpinfo, ?bool $isLegitimateProxy ): ProxyType { |
231 | return new ProxyType( |
232 | (bool)$anonymousIpinfo->isAnonymousVpn || null, |
233 | (bool)$anonymousIpinfo->isPublicProxy || null, |
234 | (bool)$anonymousIpinfo->isResidentialProxy || null, |
235 | $isLegitimateProxy, |
236 | (bool)$anonymousIpinfo->isTorExitNode || null, |
237 | (bool)$anonymousIpinfo->isHostingProvider || null |
238 | ); |
239 | } |
240 | } |