Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.86% covered (success)
92.86%
78 / 84
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
IPInfoHandler
92.86% covered (success)
92.86%
78 / 84
80.00% covered (warning)
80.00%
4 / 5
19.13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getInfo
n/a
0 / 0
n/a
0 / 0
0
 run
85.71% covered (warning)
85.71%
36 / 42
0.00% covered (danger)
0.00%
0 / 1
15.66
 logAccess
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getParamSettings
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
1
 getBodyParamSettings
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\IPInfo\Rest\Handler;
4
5use ExtensionRegistry;
6use JobQueueGroup;
7use JobSpecification;
8use MediaWiki\IPInfo\AccessLevelTrait;
9use MediaWiki\IPInfo\InfoManager;
10use MediaWiki\IPInfo\Rest\Presenter\DefaultPresenter;
11use MediaWiki\Languages\LanguageFallback;
12use MediaWiki\Permissions\PermissionManager;
13use MediaWiki\Rest\LocalizedHttpException;
14use MediaWiki\Rest\Response;
15use MediaWiki\Rest\SimpleHandler;
16use MediaWiki\Rest\TokenAwareHandlerTrait;
17use MediaWiki\User\Options\UserOptionsLookup;
18use MediaWiki\User\UserFactory;
19use MediaWiki\User\UserIdentity;
20use Wikimedia\Message\MessageValue;
21use Wikimedia\ParamValidator\ParamValidator;
22
23abstract class IPInfoHandler extends SimpleHandler {
24
25    use AccessLevelTrait;
26    use TokenAwareHandlerTrait;
27
28    /**
29     * An array of contexts and the data
30     * that should be available in those contexts
31     *
32     * @var array
33     */
34    protected const VIEWING_CONTEXTS = [
35        'popup' => [
36            'country',
37            'countryNames',
38            'location',
39            'organization',
40            'numActiveBlocks',
41            'numLocalEdits',
42            'numRecentEdits',
43        ],
44        'infobox' => [
45            'country',
46            'countryNames',
47            'location',
48            'connectionType',
49            'userType',
50            'asn',
51            'isp',
52            'organization',
53            'proxyType',
54            'behaviors',
55            'risks',
56            'connectionTypes',
57            'tunnelOperators',
58            'proxies',
59            'numUsersOnThisIP',
60            'numActiveBlocks',
61            'numLocalEdits',
62            'numRecentEdits',
63            'numDeletedEdits',
64        ]
65    ];
66
67    protected InfoManager $infoManager;
68
69    protected PermissionManager $permissionManager;
70
71    protected UserOptionsLookup $userOptionsLookup;
72
73    protected UserFactory $userFactory;
74
75    protected DefaultPresenter $presenter;
76
77    protected JobQueueGroup $jobQueueGroup;
78
79    protected LanguageFallback $languageFallback;
80
81    public function __construct(
82        InfoManager $infoManager,
83        PermissionManager $permissionManager,
84        UserOptionsLookup $userOptionsLookup,
85        UserFactory $userFactory,
86        DefaultPresenter $presenter,
87        JobQueueGroup $jobQueueGroup,
88        LanguageFallback $languageFallback
89    ) {
90        $this->infoManager = $infoManager;
91        $this->permissionManager = $permissionManager;
92        $this->userOptionsLookup = $userOptionsLookup;
93        $this->userFactory = $userFactory;
94        $this->presenter = $presenter;
95        $this->jobQueueGroup = $jobQueueGroup;
96        $this->languageFallback = $languageFallback;
97    }
98
99    /**
100     * Get information about an IP address (or IP addresses) associated with some entity,
101     * given an ID for the entity.
102     *
103     * Concrete subclasses handle for specific entity types, e.g. revision, log entry, etc.
104     *
105     * @param int $id
106     * @return array[]
107     *  Each array in this array has the following structure:
108     *  - 'subject': IP address
109     *  - 'data': array of arrays, each with the following structure:
110     *    - data source as a string => data array
111     *  TODO: Task to codify this better
112     */
113    abstract protected function getInfo( int $id ): array;
114
115    /**
116     * Get information about an IP address, based on the ID of some related entity
117     * (e.g. a revision, log entry, etc.)
118     *
119     * @param int $id
120     * @return Response
121     */
122    public function run( int $id ): Response {
123        $isBetaFeaturesLoaded = ExtensionRegistry::getInstance()->isLoaded( 'BetaFeatures' );
124        // Disallow access to API if BetaFeatures is enabled but the feature is not
125        if ( $isBetaFeaturesLoaded &&
126            !$this->userOptionsLookup->getOption( $this->getAuthority()->getUser(), 'ipinfo-beta-feature-enable' ) ) {
127            throw new LocalizedHttpException(
128                new MessageValue( 'ipinfo-rest-access-denied' ),
129                $this->getAuthority()->getUser()->isRegistered() ? 403 : 401
130            );
131        }
132
133        if (
134            !$this->permissionManager->userHasRight( $this->getAuthority()->getUser(), 'ipinfo' ) ||
135            !$this->userOptionsLookup->getOption( $this->getAuthority()->getUser(), 'ipinfo-use-agreement' )
136        ) {
137            throw new LocalizedHttpException(
138                new MessageValue( 'ipinfo-rest-access-denied' ),
139                $this->getAuthority()->getUser()->isRegistered() ? 403 : 401
140            );
141        }
142        $user = $this->userFactory->newFromUserIdentity( $this->getAuthority()->getUser() );
143
144        // Users with blocks on their accounts shouldn't be allowed to view ip info
145        if ( $user->getBlock() ) {
146            throw new LocalizedHttpException(
147                new MessageValue( 'ipinfo-rest-access-denied-blocked-user' ),
148                403
149            );
150        }
151
152        // Validate the CSRF token. We shouldn't need to allow anon CSRF tokens.
153        $this->validateToken();
154
155        $info = $this->getInfo( $id );
156        $userLang = strtolower( $this->getValidatedParams()['language'] );
157        $langCodes = array_unique( array_merge(
158            [ $userLang ],
159            $this->languageFallback->getAll( $userLang )
160        ) );
161
162        // Only show data required for the context
163        $dataContext = $this->getValidatedParams()['dataContext'];
164        foreach ( $info as $index => $set ) {
165            if ( !isset( $set['data'] ) ) {
166                continue;
167            }
168            $info[$index] += [ 'language-fallback' => $langCodes ];
169            foreach ( $set['data'] as $provider => $dataset ) {
170                foreach ( $dataset as $datum => $value ) {
171                    if ( !in_array( $datum, self::VIEWING_CONTEXTS[$dataContext] ?? [] ) ) {
172                        unset( $info[$index]['data'][$provider][$datum] );
173                    }
174                }
175            }
176        }
177
178        foreach ( $info as $index => $set ) {
179            if ( !isset( $set['subject'] ) ) {
180                continue;
181            }
182            $this->logAccess( $this->getAuthority()->getUser(), $set['subject'], $dataContext );
183        }
184
185        $response = $this->getResponseFactory()->createJson( [ 'info' => $info ] );
186        $response->setHeader( 'Cache-Control', 'private, max-age=86400' );
187        return $response;
188    }
189
190    /**
191     * Log that the IP information was accessed. See also LogIPInfoAccessJob.
192     *
193     * @param UserIdentity $accessingUser
194     * @param string $ip IP address whose information was accessed
195     * @param string $dataContext 'infobox' or 'popup'
196     */
197    protected function logAccess( $accessingUser, $ip, $dataContext ) {
198        $level = $this->highestAccessLevel(
199            $this->permissionManager->getUserPermissions( $accessingUser )
200        );
201        $this->jobQueueGroup->push(
202            new JobSpecification(
203                'ipinfoLogIPInfoAccess',
204                [
205                    'performer' => $accessingUser->getName(),
206                    'ip' => $ip,
207                    'dataContext' => $dataContext,
208                    'timestamp' => (int)wfTimestamp(),
209                    'access_level' => $level,
210                ],
211                [],
212                null
213            )
214        );
215    }
216
217    /** @inheritDoc */
218    public function getParamSettings() {
219        return [
220                'id' => [
221                    self::PARAM_SOURCE => 'path',
222                    ParamValidator::PARAM_TYPE => 'integer',
223                    ParamValidator::PARAM_REQUIRED => true,
224                ],
225                'dataContext' => [
226                    self::PARAM_SOURCE => 'query',
227                    ParamValidator::PARAM_TYPE => 'string',
228                    ParamValidator::PARAM_REQUIRED => true,
229                ],
230                'language' => [
231                    self::PARAM_SOURCE => 'query',
232                    ParamValidator::PARAM_TYPE => 'string',
233                    ParamValidator::PARAM_REQUIRED => true,
234                ],
235            ];
236    }
237
238    public function getBodyParamSettings(): array {
239        return $this->getTokenParamDefinition();
240    }
241
242}