Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.59% |
240 / 241 |
|
93.75% |
15 / 16 |
CRAP | |
0.00% |
0 / 1 |
GlobalBlockLookup | |
99.59% |
240 / 241 |
|
93.75% |
15 / 16 |
73 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
1 | |||
getUserBlock | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
getUserBlockErrors | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
addToUserBlockDetailsCache | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getUserBlockDetailsCacheResult | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUserBlockDetails | |
100.00% |
89 / 89 |
|
100.00% |
1 / 1 |
17 | |||
getTargetType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
chooseMostSpecificBlock | |
100.00% |
18 / 18 |
|
100.00% |
1 / 1 |
7 | |||
getGlobalBlockingBlock | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
2 | |||
getRangeCondition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGlobalBlockLookupConditions | |
100.00% |
32 / 32 |
|
100.00% |
1 / 1 |
15 | |||
getIpFragment | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
checkIpsForBlock | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
9 | |||
getAppliedBlock | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
getGlobalBlockId | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
5 | |||
selectFields | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\GlobalBlocking\Services; |
4 | |
5 | use InvalidArgumentException; |
6 | use Language; |
7 | use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface; |
8 | use MediaWiki\Config\ServiceOptions; |
9 | use MediaWiki\Extension\GlobalBlocking\GlobalBlock; |
10 | use MediaWiki\Extension\GlobalBlocking\Hook\GlobalBlockingHookRunner; |
11 | use MediaWiki\HookContainer\HookContainer; |
12 | use MediaWiki\User\CentralId\CentralIdLookup; |
13 | use MediaWiki\User\User; |
14 | use MediaWiki\User\UserIdentity; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use Message; |
17 | use RequestContext; |
18 | use stdClass; |
19 | use Wikimedia\IPUtils; |
20 | use Wikimedia\Rdbms\IExpression; |
21 | use Wikimedia\Rdbms\IResultWrapper; |
22 | use Wikimedia\Rdbms\LikeValue; |
23 | use Wikimedia\Rdbms\OrExpressionGroup; |
24 | |
25 | /** |
26 | * Allows looking up global blocks in the globalblocks table. |
27 | * |
28 | * @since 1.42 |
29 | */ |
30 | class GlobalBlockLookup { |
31 | |
32 | public const CONSTRUCTOR_OPTIONS = [ |
33 | 'GlobalBlockingAllowedRanges', |
34 | 'GlobalBlockingBlockXFF', |
35 | 'GlobalBlockingCIDRLimit', |
36 | 'GlobalBlockingAllowGlobalAccountBlocks', |
37 | ]; |
38 | |
39 | private const TYPE_USER = 1; |
40 | private const TYPE_IP = 2; |
41 | private const TYPE_RANGE = 3; |
42 | |
43 | /** @var int Flag to ignore blocks on IP addresses which are marked as anon-only. */ |
44 | public const SKIP_SOFT_IP_BLOCKS = 1; |
45 | /** @var int Flag to ignore all blocks on IP addresses. */ |
46 | public const SKIP_IP_BLOCKS = 2; |
47 | /** @var int Flag to skip checking if the blocks that affect a target are locally disabled. */ |
48 | public const SKIP_LOCAL_DISABLE_CHECK = 4; |
49 | /** @var int Flag to skip the excluding of IP blocks in the GlobalBlockingAllowedRanges config. */ |
50 | public const SKIP_ALLOWED_RANGES_CHECK = 8; |
51 | |
52 | private ServiceOptions $options; |
53 | private GlobalBlockingConnectionProvider $globalBlockingConnectionProvider; |
54 | private StatsdDataFactoryInterface $statsdFactory; |
55 | private GlobalBlockingHookRunner $hookRunner; |
56 | private CentralIdLookup $centralIdLookup; |
57 | private Language $contentLanguage; |
58 | private GlobalBlockReasonFormatter $globalBlockReasonFormatter; |
59 | private GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup; |
60 | private GlobalBlockingLinkBuilder $globalBlockingLinkBuilder; |
61 | |
62 | private array $getUserBlockDetailsCache = []; |
63 | |
64 | public function __construct( |
65 | ServiceOptions $options, |
66 | GlobalBlockingConnectionProvider $globalBlockingConnectionProvider, |
67 | StatsdDataFactoryInterface $statsdFactory, |
68 | HookContainer $hookContainer, |
69 | CentralIdLookup $centralIdLookup, |
70 | Language $contentLanguage, |
71 | GlobalBlockReasonFormatter $globalBlockReasonFormatter, |
72 | GlobalBlockLocalStatusLookup $globalBlockLocalStatusLookup, |
73 | GlobalBlockingLinkBuilder $globalBlockingLinkBuilder |
74 | ) { |
75 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
76 | $this->options = $options; |
77 | $this->globalBlockingConnectionProvider = $globalBlockingConnectionProvider; |
78 | $this->statsdFactory = $statsdFactory; |
79 | $this->hookRunner = new GlobalBlockingHookRunner( $hookContainer ); |
80 | $this->centralIdLookup = $centralIdLookup; |
81 | $this->contentLanguage = $contentLanguage; |
82 | $this->globalBlockReasonFormatter = $globalBlockReasonFormatter; |
83 | $this->globalBlockLocalStatusLookup = $globalBlockLocalStatusLookup; |
84 | $this->globalBlockingLinkBuilder = $globalBlockingLinkBuilder; |
85 | } |
86 | |
87 | /** |
88 | * Given a target and the IP address being used to make the request, get an existing |
89 | * GlobalBlock object that applies to the target or IP address being used. If no |
90 | * block exists, then this method returns null. |
91 | * |
92 | * @param User $user Filter for GlobalBlock objects that target this user or IP address |
93 | * @param string|null $ip The IP address being used by the user, used to apply global blocks |
94 | * on IPs or IP ranges that are not anon-only. Specifying null when $user is an IP address |
95 | * and is not the session user will cause the value to be autogenerated. |
96 | * @return GlobalBlock|null The GlobalBlock that applies to the given user or IP, or null if no block applies. |
97 | */ |
98 | public function getUserBlock( User $user, ?string $ip ) { |
99 | $details = $this->getUserBlockDetails( $user, $ip ); |
100 | |
101 | if ( $details['block'] ) { |
102 | $row = $details['block']; |
103 | return new GlobalBlock( |
104 | $row, |
105 | [ |
106 | 'address' => $row->gb_address, |
107 | 'reason' => $row->gb_reason, |
108 | 'timestamp' => $row->gb_timestamp, |
109 | 'anonOnly' => $row->gb_anon_only, |
110 | 'expiry' => $row->gb_expiry, |
111 | 'xff' => $details['xff'] ?? false, |
112 | ] |
113 | ); |
114 | } |
115 | |
116 | return null; |
117 | } |
118 | |
119 | /** |
120 | * Given a target and the IP address being used to make the request, get the human |
121 | * readable error message(s) describing the GlobalBlock that applies to the user. If |
122 | * no GlobalBlock exists, then this returns an empty array. |
123 | * |
124 | * @param User $user Filter for GlobalBlock objects that target this user or IP address |
125 | * @param string|null $ip The IP address being used by the user, used to apply global blocks |
126 | * on IPs or IP ranges that are not anon-only. Specifying null when $user is an IP address |
127 | * and is not the session user will cause the value to be autogenerated. |
128 | * @return Message[] empty or message objects |
129 | * @deprecated Since 1.42. Use ::getUserBlock instead to get the block and then use BlockErrorFormatter to get |
130 | * a human readable error message. |
131 | */ |
132 | public function getUserBlockErrors( User $user, ?string $ip ): array { |
133 | $details = $this->getUserBlockDetails( $user, $ip ); |
134 | return $details['error']; |
135 | } |
136 | |
137 | /** |
138 | * Add the $result to the instance cache under the username of the given $user. |
139 | * |
140 | * @param array $result |
141 | * @param UserIdentity $user |
142 | * @return array The value of $result |
143 | */ |
144 | private function addToUserBlockDetailsCache( array $result, UserIdentity $user ) { |
145 | $this->getUserBlockDetailsCache[$user->getName()] = $result; |
146 | return $result; |
147 | } |
148 | |
149 | /** |
150 | * Get the cached result of ::getUserBlockDetails for the given user. |
151 | * |
152 | * @param UserIdentity $userIdentity |
153 | * @return array|null Array if the result is cached, null if no cache exists |
154 | */ |
155 | protected function getUserBlockDetailsCacheResult( UserIdentity $userIdentity ): ?array { |
156 | return $this->getUserBlockDetailsCache[$userIdentity->getName()] ?? null; |
157 | } |
158 | |
159 | /** |
160 | * Given a target and the IP address being used to make the request, get the |
161 | * most specific block that applies along with a human readable error message |
162 | * associated with the block. If no block exists, this returns an array with |
163 | * no block and an empty array of error messages. |
164 | * |
165 | * @param User $user See ::getUserBlock. Note this may not be the session user. |
166 | * @param string|null $ip See ::getUserBlock. |
167 | * @return array ['block' => DB row or null, 'error' => empty or message objects]. The |
168 | * error message key is deprecated since 1.42. |
169 | * @phan-return array{block:stdClass|null,error:Message[]} |
170 | */ |
171 | private function getUserBlockDetails( User $user, ?string $ip ) { |
172 | // Check first if the instance cache has the result. |
173 | $cachedResult = $this->getUserBlockDetailsCacheResult( $user ); |
174 | if ( $cachedResult !== null ) { |
175 | return $cachedResult; |
176 | } |
177 | |
178 | $this->statsdFactory->increment( 'global_blocking.get_user_block' ); |
179 | |
180 | // We have callers from different code paths which may leave $ip as null when providing an |
181 | // IP address as the $user where the IP address is not the session user. In this case, populate |
182 | // the $ip argument with the IP provided in $user to get all the blocks that apply to the IP. |
183 | $context = RequestContext::getMain(); |
184 | $isSessionUser = $user->equals( $context->getUser() ); |
185 | if ( $ip === null && !$isSessionUser && IPUtils::isIPAddress( $user->getName() ) ) { |
186 | // Populate the IP for checking blocks against non-session users. |
187 | $ip = $user->getName(); |
188 | } |
189 | |
190 | $flags = 0; |
191 | if ( $user->isAllowedAny( 'ipblock-exempt', 'globalblock-exempt' ) ) { |
192 | // User is exempt from IP blocks. |
193 | $flags |= self::SKIP_IP_BLOCKS; |
194 | } |
195 | if ( $user->isNamed() ) { |
196 | // User is a named account, so skip anon-only (soft) IP blocks. |
197 | $flags |= self::SKIP_SOFT_IP_BLOCKS; |
198 | } |
199 | |
200 | $centralId = 0; |
201 | if ( $this->options->get( 'GlobalBlockingAllowGlobalAccountBlocks' ) && $user->isRegistered() ) { |
202 | $centralId = $this->centralIdLookup->centralIdFromLocalUser( $user, CentralIdLookup::AUDIENCE_RAW ); |
203 | } |
204 | |
205 | $this->statsdFactory->increment( 'global_blocking.get_user_block_db_query' ); |
206 | |
207 | $lang = $context->getLanguage(); |
208 | $block = $this->getGlobalBlockingBlock( $ip, $centralId, $flags ); |
209 | if ( $block ) { |
210 | // The following code in this if block, except the returning of $block, is deprecated since 1.42 (T358776). |
211 | $blockTimestamp = $lang->timeanddate( wfTimestamp( TS_MW, $block->gb_timestamp ), true ); |
212 | $blockExpiry = $lang->formatExpiry( $block->gb_expiry ); |
213 | $display_wiki = WikiMap::getWikiName( $block->gb_by_wiki ); |
214 | $blockingUser = $this->globalBlockingLinkBuilder->maybeLinkUserpage( |
215 | $block->gb_by_wiki, |
216 | $this->centralIdLookup->nameFromCentralId( $block->gb_by_central_id ) ?? '' |
217 | ); |
218 | |
219 | // The following Hooks are deprecated and the message it generates is not used anywhere. |
220 | // The hooks will be removed in the future through T358776. |
221 | // Allow site customization of blocked message. |
222 | if ( IPUtils::isValid( $block->gb_address ) ) { |
223 | $errorMsg = 'globalblocking-ipblocked'; |
224 | $this->hookRunner->onGlobalBlockingBlockedIpMsg( $errorMsg ); |
225 | } elseif ( IPUtils::isValidRange( $block->gb_address ) ) { |
226 | $errorMsg = 'globalblocking-ipblocked-range'; |
227 | $this->hookRunner->onGlobalBlockingBlockedIpRangeMsg( $errorMsg ); |
228 | } else { |
229 | $errorMsg = false; |
230 | } |
231 | $blockDetails = [ |
232 | 'block' => $block, |
233 | 'error' => [], |
234 | ]; |
235 | if ( $errorMsg ) { |
236 | $blockDetails['error'][] = wfMessage( |
237 | $errorMsg, |
238 | $blockingUser, |
239 | $display_wiki, |
240 | $this->globalBlockReasonFormatter->format( $block->gb_reason, $this->contentLanguage->getCode() ), |
241 | $blockTimestamp, |
242 | $blockExpiry, |
243 | $ip, |
244 | $block->gb_address |
245 | ); |
246 | } |
247 | |
248 | return $this->addToUserBlockDetailsCache( $blockDetails, $user ); |
249 | } |
250 | |
251 | $request = $context->getRequest(); |
252 | // Checking non-session users are not applicable to the XFF block. |
253 | if ( $this->options->get( 'GlobalBlockingBlockXFF' ) && $isSessionUser ) { |
254 | $xffIps = $request->getHeader( 'X-Forwarded-For' ); |
255 | if ( $xffIps ) { |
256 | $xffIps = array_map( 'trim', explode( ',', $xffIps ) ); |
257 | // Always skip the allowed ranges check when checking the XFF IPs as the value of this header |
258 | // is easy to spoof. |
259 | $xffFlags = $flags | self::SKIP_ALLOWED_RANGES_CHECK; |
260 | $appliedBlock = $this->getAppliedBlock( |
261 | $xffIps, $this->checkIpsForBlock( $xffIps, $xffFlags ) |
262 | ); |
263 | if ( $appliedBlock !== null ) { |
264 | // The following code in this if block, except the returning of $block, is deprecated since 1.42. |
265 | [ $blockIP, $block ] = $appliedBlock; |
266 | $blockTimestamp = $lang->timeanddate( |
267 | wfTimestamp( TS_MW, $block->gb_timestamp ), |
268 | true |
269 | ); |
270 | $blockExpiry = $lang->formatExpiry( $block->gb_expiry ); |
271 | $display_wiki = WikiMap::getWikiName( $block->gb_by_wiki ); |
272 | $blockingUser = $this->globalBlockingLinkBuilder->maybeLinkUserpage( |
273 | $block->gb_by_wiki, |
274 | $this->centralIdLookup->nameFromCentralId( $block->gb_by_central_id ) ?? '' |
275 | ); |
276 | // Allow site customization of blocked message. |
277 | $blockedIpXffMsg = 'globalblocking-ipblocked-xff'; |
278 | $this->hookRunner->onGlobalBlockingBlockedIpXffMsg( $blockedIpXffMsg ); |
279 | return $this->addToUserBlockDetailsCache( [ |
280 | 'block' => $block, |
281 | 'error' => [ |
282 | wfMessage( |
283 | $blockedIpXffMsg, |
284 | $blockingUser, |
285 | $display_wiki, |
286 | $block->gb_reason, |
287 | $blockTimestamp, |
288 | $blockExpiry, |
289 | $blockIP |
290 | ) |
291 | ], |
292 | 'xff' => true, |
293 | ], $user ); |
294 | } |
295 | } |
296 | } |
297 | |
298 | return $this->addToUserBlockDetailsCache( [ 'block' => null, 'error' => [] ], $user ); |
299 | } |
300 | |
301 | /** |
302 | * Returns the ::TYPE_* constant for the given target. |
303 | * |
304 | * @param string $target |
305 | * @return int |
306 | */ |
307 | private function getTargetType( string $target ) { |
308 | if ( IPUtils::isValid( $target ) ) { |
309 | return self::TYPE_IP; |
310 | } elseif ( IPUtils::isValidRange( $target ) ) { |
311 | return self::TYPE_RANGE; |
312 | } else { |
313 | return self::TYPE_USER; |
314 | } |
315 | } |
316 | |
317 | /** |
318 | * Choose the most specific block from some combination of user, IP and IP range |
319 | * blocks. Decreasing order of specificity: IP > narrower IP range > wider IP |
320 | * range. A range that encompasses one IP address is ranked equally to a single IP. |
321 | * |
322 | * Note that DatabaseBlock::chooseBlocks chooses blocks in a different way. |
323 | * |
324 | * This is based on DatabaseBlock::chooseMostSpecificBlock |
325 | * |
326 | * @param IResultWrapper $blocks These should not include autoblocks or ID blocks |
327 | * @param int $flags The $flags provided. This method only checks for BLOCK_FLAG_SKIP_LOCAL_DISABLE_CHECK, |
328 | * and callers are in charge of checking for other relevant flags. |
329 | * @return stdClass|null The block with the most specific target |
330 | */ |
331 | private function chooseMostSpecificBlock( IResultWrapper $blocks, int $flags ): ?stdClass { |
332 | // This result could contain a block on the user, a block on the IP, and a russian-doll |
333 | // set of rangeblocks. We want to choose the most specific one, so keep a leader board. |
334 | $bestBlock = null; |
335 | |
336 | // Lower will be better |
337 | $bestBlockScore = 100; |
338 | foreach ( $blocks as $block ) { |
339 | // Check for local whitelisting, unless the flag is set to skip the check. |
340 | if ( |
341 | !( $flags & self::SKIP_LOCAL_DISABLE_CHECK ) && |
342 | $this->globalBlockLocalStatusLookup->getLocalWhitelistInfo( $block->gb_id ) |
343 | ) { |
344 | continue; |
345 | } |
346 | $target = $block->gb_address; |
347 | $type = $this->getTargetType( $target ); |
348 | if ( $type == self::TYPE_RANGE ) { |
349 | // This is the number of bits that are allowed to vary in the block, give |
350 | // or take some floating point errors |
351 | $max = IPUtils::isIPv6( $target ) ? 128 : 32; |
352 | [ $network, $bits ] = IPUtils::parseCIDR( $target ); |
353 | $size = $max - $bits; |
354 | |
355 | // Rank a range block covering a single IP equally with a single-IP block |
356 | $score = self::TYPE_RANGE - 1 + ( $size / $max ); |
357 | } else { |
358 | $score = $type; |
359 | } |
360 | |
361 | if ( $score < $bestBlockScore ) { |
362 | $bestBlockScore = $score; |
363 | $bestBlock = $block; |
364 | } |
365 | } |
366 | |
367 | return $bestBlock; |
368 | } |
369 | |
370 | /** |
371 | * Get the most specific row from the `globalblocks` table that applies to the given IP address |
372 | * or the central user. |
373 | * |
374 | * This does not check if the user is exempt from IP blocks. As such it should not be used to determine |
375 | * if a block should be applied to a user. Use ::getUserBlock for that. |
376 | * |
377 | * @param string|null $ip The IP address used by the user. If null, then no IP blocks will be checked. |
378 | * @param int $centralId The central ID of the user. 0 if the user is anonymous. Setting this as |
379 | * a boolean is soft deprecated and will be treated as 0. |
380 | * @param int $flags Flags to control the behavior of the block lookup |
381 | * @return stdClass|null The most specific row from the `globalblocks` table, or null if no row was found |
382 | */ |
383 | public function getGlobalBlockingBlock( ?string $ip, int $centralId, int $flags = 0 ): ?stdClass { |
384 | $conds = $this->getGlobalBlockLookupConditions( $ip, $centralId, $flags ); |
385 | if ( $conds === null ) { |
386 | // No conditions, so don't perform the query and assume the user is not targeted by any block |
387 | return null; |
388 | } |
389 | |
390 | $blocks = $this->globalBlockingConnectionProvider |
391 | ->getReplicaGlobalBlockingDatabase() |
392 | ->newSelectQueryBuilder() |
393 | ->select( self::selectFields() ) |
394 | ->from( 'globalblocks' ) |
395 | ->where( $conds ) |
396 | ->caller( __METHOD__ ) |
397 | ->fetchResultSet(); |
398 | |
399 | // Get the most specific block for the global blocks that apply to the user. |
400 | return $this->chooseMostSpecificBlock( $blocks, $flags ); |
401 | } |
402 | |
403 | /** |
404 | * Get the SQL WHERE conditions that allow looking up all blocks from the |
405 | * `globalblocks` table that apply to the given IP address or range. |
406 | * |
407 | * @param string $ip The IP address or range |
408 | * @deprecated Since 1.42. Use ::getGlobalBlockLookupConditions. |
409 | * @return IExpression |
410 | */ |
411 | public function getRangeCondition( string $ip ): IExpression { |
412 | // This method does not return null if an IP is provided and the allowed ranges check is skipped. |
413 | // @phan-suppress-next-line PhanTypeMismatchReturnNullable |
414 | return $this->getGlobalBlockLookupConditions( $ip, 0, self::SKIP_ALLOWED_RANGES_CHECK ); |
415 | } |
416 | |
417 | /** |
418 | * Get the SQL WHERE conditions that allow looking up all blocks from the `globalblocks` table. |
419 | * |
420 | * @param ?string $ip The IP address or range. If null, then no IP blocks will be checked. |
421 | * @param int $centralId The central ID of the user. 0 if the user is anonymous and 0 will skip |
422 | * checking user specific blocks. |
423 | * @param int $flags Flags which control what conditions are returned. Ignores the |
424 | * ::BLOCK_FLAG_SKIP_LOCAL_DISABLE_CHECK flag and callers are expected to check if the block is |
425 | * locally disabled if this is needed. |
426 | * @return IExpression|null The conditions to be used in a SQL query to look up global blocks, or null if no valid |
427 | * conditions could be generated. |
428 | */ |
429 | public function getGlobalBlockLookupConditions( ?string $ip, int $centralId = 0, int $flags = 0 ): ?IExpression { |
430 | $dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase(); |
431 | $ipExpr = null; |
432 | $userExpr = null; |
433 | |
434 | if ( $ip !== null && !IPUtils::isIPAddress( $ip ) ) { |
435 | // The provided IP is invalid, so throw. |
436 | throw new InvalidArgumentException( |
437 | "Invalid IP address or range provided to GlobalBlockLookup::getGlobalBlockLookupConditions." |
438 | ); |
439 | } |
440 | |
441 | if ( $ip !== null && !( $flags & self::SKIP_ALLOWED_RANGES_CHECK ) ) { |
442 | $ranges = $this->options->get( 'GlobalBlockingAllowedRanges' ); |
443 | foreach ( $ranges as $range ) { |
444 | if ( IPUtils::isInRange( $ip, $range ) ) { |
445 | // IP is in a range that is exempt from IP blocks, so treat the user as having |
446 | // global IP block exemption for this specific IP address |
447 | $flags |= self::SKIP_IP_BLOCKS; |
448 | break; |
449 | } |
450 | } |
451 | } |
452 | |
453 | if ( $ip !== null && !( $flags & self::SKIP_IP_BLOCKS ) ) { |
454 | // If we have been provided an IP address or range in $ip, then |
455 | // add conditions to the query to lookup blocks that apply to the IP address / range. |
456 | [ $start, $end ] = IPUtils::parseRange( $ip ); |
457 | $chunk = $this->getIpFragment( $start ); |
458 | $ipExpr = $dbr->expr( 'gb_range_start', IExpression::LIKE, new LikeValue( $chunk, $dbr->anyString() ) ) |
459 | ->and( 'gb_range_start', '<=', $start ) |
460 | ->and( 'gb_range_end', '>=', $end ); |
461 | |
462 | if ( $flags & self::SKIP_SOFT_IP_BLOCKS ) { |
463 | // If the flags say to skip soft IP blocks, then exclude blocks with gb_anon_only |
464 | // set to 1 (which should only be soft blocks on IP addresses or ranges). |
465 | $ipExpr = $ipExpr->and( 'gb_anon_only', '!=', 1 ); |
466 | } |
467 | } |
468 | |
469 | if ( $centralId !== 0 ) { |
470 | // If we have been provided a non-zero central ID, then also look for blocks that target the |
471 | // given central ID. |
472 | $userExpr = $dbr->expr( 'gb_target_central_id', '=', $centralId ); |
473 | } |
474 | |
475 | // Combine the IP conditions and user IExpressions |
476 | if ( $userExpr !== null && $ipExpr !== null ) { |
477 | // If we have conditions for both the IP and the user, then combine them with an OR |
478 | // to allow selecting blocks that apply to either the IP or the user. |
479 | $targetExpr = $userExpr->orExpr( $ipExpr ); |
480 | } elseif ( $userExpr !== null ) { |
481 | // If we only have conditions for the user, then use that IExpression. |
482 | $targetExpr = $userExpr; |
483 | } elseif ( $ipExpr !== null ) { |
484 | // If we only have conditions for the IP, then use that IExpression. |
485 | $targetExpr = $ipExpr; |
486 | } else { |
487 | // No conditions, so don't perform the query otherwise we will select all blocks from the DB. |
488 | // In this case, we can assume the user or their IP is not affected by any global block. |
489 | return null; |
490 | } |
491 | // @todo expiry shouldn't be in this function |
492 | return $dbr->expr( 'gb_expiry', '>', $dbr->timestamp() ) |
493 | ->andExpr( $targetExpr ); |
494 | } |
495 | |
496 | /** |
497 | * Get the component of an IP address which is certain to be the same between an IP |
498 | * address and a range block containing that IP address. |
499 | * |
500 | * This mostly duplicates the logic in DatabaseStoreBlock::getIpFragment, but with the |
501 | * CIDR limit config being the GlobalBlocking extension specific one. |
502 | * |
503 | * @param string $hex Hexadecimal IP representation |
504 | * @return string |
505 | */ |
506 | private function getIpFragment( string $hex ): string { |
507 | $blockCIDRLimit = $this->options->get( 'GlobalBlockingCIDRLimit' ); |
508 | if ( str_starts_with( $hex, 'v6-' ) ) { |
509 | return 'v6-' . substr( substr( $hex, 3 ), 0, (int)floor( $blockCIDRLimit['IPv6'] / 4 ) ); |
510 | } else { |
511 | return substr( $hex, 0, (int)floor( $blockCIDRLimit['IPv4'] / 4 ) ); |
512 | } |
513 | } |
514 | |
515 | /** |
516 | * Find all rows from the `globalblocks` table that target at least one of |
517 | * the given IP addresses. |
518 | * |
519 | * This method filters out blocks that are locally disabled, but does not |
520 | * check whether the given session user can be exempt from the block. |
521 | * |
522 | * @param string[] $ips The array of IP addresses to be checked |
523 | * @param int $flags Flags which control what blocks are returned. |
524 | * @return stdClass[] Array of applicable blocks as rows from the `globalblocks` table |
525 | */ |
526 | private function checkIpsForBlock( array $ips, int $flags = 0 ): array { |
527 | if ( $flags & self::SKIP_IP_BLOCKS ) { |
528 | // If the flags say to skip IP blocks, then don't even make the query. |
529 | return []; |
530 | } |
531 | |
532 | $dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase(); |
533 | $conds = []; |
534 | foreach ( $ips as $ip ) { |
535 | if ( IPUtils::isValid( $ip ) ) { |
536 | $ipConds = $this->getGlobalBlockLookupConditions( $ip, 0, $flags ); |
537 | if ( $ipConds !== null ) { |
538 | $conds[] = $ipConds; |
539 | } |
540 | } |
541 | } |
542 | |
543 | if ( !$conds ) { |
544 | // No valid IPs provided so don't even make the query. Bug 59705 |
545 | return []; |
546 | } |
547 | $results = $dbr->newSelectQueryBuilder() |
548 | ->select( self::selectFields() ) |
549 | ->from( 'globalblocks' ) |
550 | ->where( new OrExpressionGroup( ...$conds ) ) |
551 | ->caller( __METHOD__ ) |
552 | ->fetchResultSet(); |
553 | |
554 | $blocks = []; |
555 | foreach ( $results as $block ) { |
556 | if ( |
557 | ( $flags & self::SKIP_LOCAL_DISABLE_CHECK ) || |
558 | !$this->globalBlockLocalStatusLookup->getLocalWhitelistInfo( $block->gb_id ) |
559 | ) { |
560 | $blocks[] = $block; |
561 | } |
562 | } |
563 | |
564 | return $blocks; |
565 | } |
566 | |
567 | /** |
568 | * Using the result of ::checkIpsForBlock and the IPs provided to that method, |
569 | * choose the block that will be shown to the end user. |
570 | * |
571 | * For the time being, this will be the first block that applies. |
572 | * |
573 | * @param string[] $ips The array of IP addresses to be checked |
574 | * @param \stdClass[] $blocks The array returned by ::checkIpsForBlock |
575 | * @return array|null An array where the first element is the IP address |
576 | * that the block applies to, and the second element is the block itself. |
577 | * If no block applies, then this method returns null. |
578 | * @phan-return array{string,stdClass}|null |
579 | */ |
580 | private function getAppliedBlock( array $ips, array $blocks ): ?array { |
581 | foreach ( $blocks as $block ) { |
582 | foreach ( $ips as $ip ) { |
583 | $ipHex = IPUtils::toHex( $ip ); |
584 | if ( $block->gb_range_start <= $ipHex && $block->gb_range_end >= $ipHex ) { |
585 | return [ $ip, $block ]; |
586 | } |
587 | } |
588 | } |
589 | |
590 | return null; |
591 | } |
592 | |
593 | /** |
594 | * Given a specific target, find the ID for the global block that applies to it. |
595 | * If no global block targets this IP address specifically, then this method |
596 | * returns 0. |
597 | * |
598 | * @param string $target The specific target which can be a username, IP address or range. The target being |
599 | * specific means that if you provide a single IP which is covered by a range block, the range block will |
600 | * not be returned. Use ::getGlobalBlockingBlock to include these blocks. |
601 | * @param int $dbtype Either DB_REPLICA or DB_PRIMARY. |
602 | * @return int |
603 | */ |
604 | public function getGlobalBlockId( string $target, int $dbtype = DB_REPLICA ): int { |
605 | if ( $dbtype === DB_PRIMARY ) { |
606 | $db = $this->globalBlockingConnectionProvider->getPrimaryGlobalBlockingDatabase(); |
607 | } else { |
608 | $db = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase(); |
609 | } |
610 | |
611 | $queryBuilder = $db->newSelectQueryBuilder() |
612 | ->select( 'gb_id' ) |
613 | ->from( 'globalblocks' ); |
614 | |
615 | if ( IPUtils::isIPAddress( $target ) ) { |
616 | $queryBuilder->where( [ 'gb_address' => $target ] ); |
617 | } elseif ( $this->options->get( 'GlobalBlockingAllowGlobalAccountBlocks' ) ) { |
618 | $centralId = $this->centralIdLookup->centralIdFromName( $target, CentralIdLookup::AUDIENCE_RAW ); |
619 | if ( !$centralId ) { |
620 | // If we are looking up a block by a central ID of a user, then the user must have a central ID |
621 | // for a block to apply to them. |
622 | return 0; |
623 | } |
624 | $queryBuilder->where( [ 'gb_target_central_id' => $centralId ] ); |
625 | } else { |
626 | return 0; |
627 | } |
628 | |
629 | return (int)$queryBuilder |
630 | ->caller( __METHOD__ ) |
631 | ->fetchField(); |
632 | } |
633 | |
634 | /** |
635 | * @return string[] The fields needed to construct a GlobalBlock object |
636 | */ |
637 | public static function selectFields(): array { |
638 | return [ |
639 | 'gb_id', 'gb_address', 'gb_target_central_id', 'gb_by', 'gb_by_central_id', 'gb_by_wiki', 'gb_reason', |
640 | 'gb_timestamp', 'gb_anon_only', 'gb_expiry', 'gb_range_start', 'gb_range_end' |
641 | ]; |
642 | } |
643 | } |