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