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