Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
91.55% covered (success)
91.55%
65 / 71
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
GeoLite2InfoRetriever
91.55% covered (success)
91.55%
65 / 71
81.82% covered (warning)
81.82%
9 / 11
28.47
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getReader
n/a
0 / 0
n/a
0 / 0
2
 retrieveFromIP
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 getCoordinates
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
5.01
 getAsn
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getOrganization
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 getCountryNames
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getLocations
76.19% covered (warning)
76.19%
16 / 21
0.00% covered (danger)
0.00%
0 / 1
5.34
 getIsp
n/a
0 / 0
n/a
0 / 0
1
 getConnectionType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUserType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getProxyType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\IPInfo\InfoRetriever;
4
5use GeoIp2\Database\Reader;
6use GeoIp2\Exception\AddressNotFoundException;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\IPInfo\Info\Coordinates;
9use MediaWiki\IPInfo\Info\Info;
10use MediaWiki\IPInfo\Info\Location;
11
12/**
13 * Manager for getting information from the MaxMind GeoLite2 databases.
14 */
15class 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}