Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
21.60% |
54 / 250 |
|
0.00% |
0 / 15 |
CRAP | |
0.00% |
0 / 1 |
BlockUser | |
21.60% |
54 / 250 |
|
0.00% |
0 / 15 |
4170.72 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
156 | |||
setLogDeletionFlags | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseExpiryInput | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isPartial | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
configureBlock | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
12 | |||
placeBlock | |
58.33% |
21 / 36 |
|
0.00% |
0 / 1 |
28.18 | |||
placeBlockUnsafe | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
306 | |||
placeBlockInternal | |
76.74% |
33 / 43 |
|
0.00% |
0 / 1 |
12.52 | |||
getNamespaceRestrictions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getPageRestrictions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getActionRestrictions | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
constructLogParams | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
prepareLogEntry | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
log | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
blockLogFlags | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
110 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or modify |
5 | * it under the terms of the GNU General Public License as published by |
6 | * the Free Software Foundation; either version 2 of the License, or |
7 | * (at your option) any later version. |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License along |
15 | * with this program; if not, write to the Free Software Foundation, Inc., |
16 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * http://www.gnu.org/copyleft/gpl.html |
18 | * |
19 | * @file |
20 | */ |
21 | |
22 | namespace MediaWiki\Block; |
23 | |
24 | use ChangeTags; |
25 | use InvalidArgumentException; |
26 | use ManualLogEntry; |
27 | use MediaWiki\Block\Restriction\AbstractRestriction; |
28 | use MediaWiki\Block\Restriction\ActionRestriction; |
29 | use MediaWiki\Block\Restriction\NamespaceRestriction; |
30 | use MediaWiki\Block\Restriction\PageRestriction; |
31 | use MediaWiki\Config\ServiceOptions; |
32 | use MediaWiki\Deferred\DeferredUpdates; |
33 | use MediaWiki\HookContainer\HookContainer; |
34 | use MediaWiki\HookContainer\HookRunner; |
35 | use MediaWiki\MainConfigNames; |
36 | use MediaWiki\Message\Message; |
37 | use MediaWiki\Permissions\Authority; |
38 | use MediaWiki\Status\Status; |
39 | use MediaWiki\Title\MalformedTitleException; |
40 | use MediaWiki\Title\Title; |
41 | use MediaWiki\Title\TitleFactory; |
42 | use MediaWiki\User\UserEditTracker; |
43 | use MediaWiki\User\UserFactory; |
44 | use MediaWiki\User\UserIdentity; |
45 | use Psr\Log\LoggerInterface; |
46 | use RevisionDeleteUser; |
47 | use Wikimedia\ParamValidator\TypeDef\ExpiryDef; |
48 | |
49 | /** |
50 | * Handles the backend logic of blocking users |
51 | * |
52 | * @since 1.36 |
53 | */ |
54 | class BlockUser { |
55 | /** |
56 | * @var UserIdentity|string|null |
57 | * |
58 | * Target of the block |
59 | * |
60 | * This is null in case BlockUtils::parseBlockTarget failed to parse the target. |
61 | * Such case is detected in placeBlockUnsafe, by calling validateTarget from SpecialBlock. |
62 | */ |
63 | private $target; |
64 | |
65 | /** |
66 | * @var int |
67 | * |
68 | * One of AbstractBlock::TYPE_* constants |
69 | * |
70 | * This will be -1 if BlockUtils::parseBlockTarget failed to parse the target. |
71 | */ |
72 | private $targetType; |
73 | |
74 | /** @var Authority Performer of the block */ |
75 | private $performer; |
76 | |
77 | /** @var ServiceOptions */ |
78 | private $options; |
79 | |
80 | /** @var BlockRestrictionStore */ |
81 | private $blockRestrictionStore; |
82 | |
83 | /** @var BlockPermissionChecker */ |
84 | private $blockPermissionChecker; |
85 | |
86 | /** @var BlockUtils */ |
87 | private $blockUtils; |
88 | |
89 | /** @var HookRunner */ |
90 | private $hookRunner; |
91 | |
92 | /** @var DatabaseBlockStore */ |
93 | private $blockStore; |
94 | |
95 | /** @var UserFactory */ |
96 | private $userFactory; |
97 | |
98 | /** @var UserEditTracker */ |
99 | private $userEditTracker; |
100 | |
101 | /** @var LoggerInterface */ |
102 | private $logger; |
103 | |
104 | /** @var TitleFactory */ |
105 | private $titleFactory; |
106 | |
107 | /** @var BlockActionInfo */ |
108 | private $blockActionInfo; |
109 | |
110 | /** |
111 | * @internal For use by UserBlockCommandFactory |
112 | */ |
113 | public const CONSTRUCTOR_OPTIONS = [ |
114 | MainConfigNames::HideUserContribLimit, |
115 | MainConfigNames::BlockAllowsUTEdit, |
116 | ]; |
117 | |
118 | /** |
119 | * @var string |
120 | * |
121 | * Expiry of the to-be-placed block exactly as it was passed to the constructor. |
122 | */ |
123 | private $rawExpiry; |
124 | |
125 | /** |
126 | * @var string|bool |
127 | * |
128 | * Parsed expiry. This may be false in case of an error in parsing. |
129 | */ |
130 | private $expiryTime; |
131 | |
132 | /** @var string */ |
133 | private $reason; |
134 | |
135 | /** @var bool */ |
136 | private $isCreateAccountBlocked = false; |
137 | |
138 | /** |
139 | * @var bool|null |
140 | * |
141 | * This may be null when an invalid option was passed to the constructor. |
142 | * Such a case is caught in placeBlockUnsafe. |
143 | */ |
144 | private $isUserTalkEditBlocked = null; |
145 | |
146 | /** @var bool */ |
147 | private $isEmailBlocked = false; |
148 | |
149 | /** @var bool */ |
150 | private $isHardBlock = true; |
151 | |
152 | /** @var bool */ |
153 | private $isAutoblocking = true; |
154 | |
155 | /** @var bool */ |
156 | private $isHideUser = false; |
157 | |
158 | /** |
159 | * @var bool |
160 | * |
161 | * Flag that needs to be true when the to-be-created block allows all editing, |
162 | * but does not allow some other action. |
163 | * |
164 | * This flag is used only by isPartial(), and should not be used anywhere else, |
165 | * even within this class. If you want to determine whether the block will be partial, |
166 | * use $this->isPartial(). |
167 | */ |
168 | private $isPartialRaw; |
169 | |
170 | /** @var AbstractRestriction[] */ |
171 | private $blockRestrictions = []; |
172 | |
173 | /** @var string[] */ |
174 | private $tags = []; |
175 | |
176 | /** @var int|null */ |
177 | private $logDeletionFlags; |
178 | |
179 | /** |
180 | * @param ServiceOptions $options |
181 | * @param BlockRestrictionStore $blockRestrictionStore |
182 | * @param BlockPermissionCheckerFactory $blockPermissionCheckerFactory |
183 | * @param BlockUtils $blockUtils |
184 | * @param BlockActionInfo $blockActionInfo |
185 | * @param HookContainer $hookContainer |
186 | * @param DatabaseBlockStore $databaseBlockStore |
187 | * @param UserFactory $userFactory |
188 | * @param UserEditTracker $userEditTracker |
189 | * @param LoggerInterface $logger |
190 | * @param TitleFactory $titleFactory |
191 | * @param string|UserIdentity $target Target of the block |
192 | * @param Authority $performer Performer of the block |
193 | * @param string $expiry Expiry of the block (timestamp or 'infinity') |
194 | * @param string $reason Reason of the block |
195 | * @param bool[] $blockOptions |
196 | * Valid options: |
197 | * - isCreateAccountBlocked : Are account creations prevented? |
198 | * - isEmailBlocked : Is emailing other users prevented? |
199 | * - isHardBlock : Are named (non-temporary) users prevented from editing? |
200 | * - isAutoblocking : Should this block spread to others to |
201 | * limit block evasion? |
202 | * - isUserTalkEditBlocked : Is editing blocked user's own talk page prevented? |
203 | * - isHideUser : Should blocked user's name be hidden (needs hideuser)? |
204 | * - isPartial : Is this block partial? This is ignored when |
205 | * blockRestrictions is not an empty array. |
206 | * @param AbstractRestriction[] $blockRestrictions |
207 | * @param string[] $tags Tags that should be assigned to the log entry |
208 | */ |
209 | public function __construct( |
210 | ServiceOptions $options, |
211 | BlockRestrictionStore $blockRestrictionStore, |
212 | BlockPermissionCheckerFactory $blockPermissionCheckerFactory, |
213 | BlockUtils $blockUtils, |
214 | BlockActionInfo $blockActionInfo, |
215 | HookContainer $hookContainer, |
216 | DatabaseBlockStore $databaseBlockStore, |
217 | UserFactory $userFactory, |
218 | UserEditTracker $userEditTracker, |
219 | LoggerInterface $logger, |
220 | TitleFactory $titleFactory, |
221 | $target, |
222 | Authority $performer, |
223 | string $expiry, |
224 | string $reason, |
225 | array $blockOptions, |
226 | array $blockRestrictions, |
227 | array $tags |
228 | ) { |
229 | // Process dependencies |
230 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
231 | $this->options = $options; |
232 | $this->blockRestrictionStore = $blockRestrictionStore; |
233 | $this->blockPermissionChecker = $blockPermissionCheckerFactory |
234 | ->newBlockPermissionChecker( |
235 | $target, |
236 | $performer |
237 | ); |
238 | $this->blockUtils = $blockUtils; |
239 | $this->hookRunner = new HookRunner( $hookContainer ); |
240 | $this->blockStore = $databaseBlockStore; |
241 | $this->userFactory = $userFactory; |
242 | $this->userEditTracker = $userEditTracker; |
243 | $this->logger = $logger; |
244 | $this->titleFactory = $titleFactory; |
245 | $this->blockActionInfo = $blockActionInfo; |
246 | |
247 | // Process block target |
248 | [ $this->target, $rawTargetType ] = $this->blockUtils->parseBlockTarget( $target ); |
249 | if ( $rawTargetType !== null ) { // Guard against invalid targets |
250 | $this->targetType = $rawTargetType; |
251 | } else { |
252 | $this->targetType = -1; |
253 | } |
254 | |
255 | // Process other block parameters |
256 | $this->performer = $performer; |
257 | $this->rawExpiry = $expiry; |
258 | $this->expiryTime = self::parseExpiryInput( $this->rawExpiry ); |
259 | $this->reason = $reason; |
260 | $this->blockRestrictions = $blockRestrictions; |
261 | $this->tags = $tags; |
262 | |
263 | // Process blockOptions |
264 | foreach ( [ |
265 | 'isCreateAccountBlocked', |
266 | 'isEmailBlocked', |
267 | 'isHardBlock', |
268 | 'isAutoblocking', |
269 | ] as $possibleBlockOption ) { |
270 | if ( isset( $blockOptions[ $possibleBlockOption ] ) ) { |
271 | $this->$possibleBlockOption = $blockOptions[ $possibleBlockOption ]; |
272 | } |
273 | } |
274 | |
275 | $this->isPartialRaw = !empty( $blockOptions['isPartial'] ) && !$blockRestrictions; |
276 | |
277 | if ( |
278 | !$this->isPartial() || |
279 | in_array( NS_USER_TALK, $this->getNamespaceRestrictions() ) |
280 | ) { |
281 | |
282 | // It is possible to block user talk edit. User talk edit is: |
283 | // - always blocked if the config says so; |
284 | // - otherwise blocked/unblocked if the option was passed in; |
285 | // - otherwise defaults to not blocked. |
286 | if ( !$this->options->get( MainConfigNames::BlockAllowsUTEdit ) ) { |
287 | $this->isUserTalkEditBlocked = true; |
288 | } else { |
289 | $this->isUserTalkEditBlocked = $blockOptions['isUserTalkEditBlocked'] ?? false; |
290 | } |
291 | |
292 | } else { |
293 | |
294 | // It is not possible to block user talk edit. If the option |
295 | // was passed, an error will be thrown in ::placeBlockUnsafe. |
296 | // Otherwise, set to not blocked. |
297 | if ( !isset( $blockOptions['isUserTalkEditBlocked'] ) || !$blockOptions['isUserTalkEditBlocked'] ) { |
298 | $this->isUserTalkEditBlocked = false; |
299 | } |
300 | |
301 | } |
302 | |
303 | if ( |
304 | isset( $blockOptions['isHideUser'] ) && |
305 | $this->targetType === AbstractBlock::TYPE_USER |
306 | ) { |
307 | $this->isHideUser = $blockOptions['isHideUser']; |
308 | } |
309 | } |
310 | |
311 | /** |
312 | * @unstable This method might be removed without prior notice (see T271101) |
313 | * @param int $flags One of LogPage::* constants |
314 | */ |
315 | public function setLogDeletionFlags( int $flags ): void { |
316 | $this->logDeletionFlags = $flags; |
317 | } |
318 | |
319 | /** |
320 | * Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute |
321 | * ("24 May 2034", etc), into an absolute timestamp we can put into the database. |
322 | * |
323 | * @todo strtotime() only accepts English strings. This means the expiry input |
324 | * can only be specified in English. |
325 | * @see https://www.php.net/manual/en/function.strtotime.php |
326 | * |
327 | * @param string $expiry Whatever was typed into the form |
328 | * |
329 | * @return string|false Timestamp (format TS_MW) or 'infinity' or false on error. |
330 | */ |
331 | public static function parseExpiryInput( string $expiry ) { |
332 | try { |
333 | return ExpiryDef::normalizeExpiry( $expiry, TS_MW ); |
334 | } catch ( InvalidArgumentException $e ) { |
335 | return false; |
336 | } |
337 | } |
338 | |
339 | /** |
340 | * Is the to-be-placed block partial? |
341 | * |
342 | * @return bool |
343 | */ |
344 | private function isPartial(): bool { |
345 | return $this->blockRestrictions !== [] || $this->isPartialRaw; |
346 | } |
347 | |
348 | /** |
349 | * Configure DatabaseBlock according to class properties |
350 | * |
351 | * @param DatabaseBlock|null $sourceBlock Copy any options from this block. |
352 | * Null to construct a new one. |
353 | * |
354 | * @return DatabaseBlock |
355 | */ |
356 | private function configureBlock( $sourceBlock = null ): DatabaseBlock { |
357 | if ( $sourceBlock === null ) { |
358 | $block = new DatabaseBlock(); |
359 | } else { |
360 | $block = clone $sourceBlock; |
361 | } |
362 | |
363 | $isSitewide = !$this->isPartial(); |
364 | |
365 | $block->setTarget( $this->target ); |
366 | $block->setBlocker( $this->performer->getUser() ); |
367 | $block->setReason( $this->reason ); |
368 | $block->setExpiry( $this->expiryTime ); |
369 | $block->isCreateAccountBlocked( $this->isCreateAccountBlocked ); |
370 | $block->isEmailBlocked( $this->isEmailBlocked ); |
371 | $block->isHardblock( $this->isHardBlock ); |
372 | $block->isAutoblocking( $this->isAutoblocking ); |
373 | $block->isSitewide( $isSitewide ); |
374 | $block->isUsertalkEditAllowed( !$this->isUserTalkEditBlocked ); |
375 | $block->setHideName( $this->isHideUser ); |
376 | |
377 | $blockId = $block->getId(); |
378 | if ( $blockId === null ) { |
379 | // Block wasn't inserted into the DB yet |
380 | $block->setRestrictions( $this->blockRestrictions ); |
381 | } else { |
382 | // Block is in the DB, we need to set restrictions through a service |
383 | $block->setRestrictions( |
384 | $this->blockRestrictionStore->setBlockId( |
385 | $blockId, |
386 | $this->blockRestrictions |
387 | ) |
388 | ); |
389 | } |
390 | |
391 | return $block; |
392 | } |
393 | |
394 | /** |
395 | * Place a block, checking permissions |
396 | * |
397 | * @param bool $reblock Should this reblock? |
398 | * |
399 | * @return Status If the block is successful, the value of the returned |
400 | * Status is an instance of a newly placed block. |
401 | */ |
402 | public function placeBlock( bool $reblock = false ): Status { |
403 | $priorBlock = $this->blockStore |
404 | ->newFromTarget( $this->target, null, /*fromPrimary=*/true ); |
405 | $priorHideUser = $priorBlock instanceof DatabaseBlock && $priorBlock->getHideName(); |
406 | if ( |
407 | $this->blockPermissionChecker |
408 | ->checkBasePermissions( |
409 | $this->isHideUser || $priorHideUser |
410 | ) !== true |
411 | ) { |
412 | $this->logger->debug( 'placeBlock: checkBasePermissions failed' ); |
413 | return Status::newFatal( $priorHideUser ? 'cant-see-hidden-user' : 'badaccess-group0' ); |
414 | } |
415 | |
416 | $blockCheckResult = $this->blockPermissionChecker->checkBlockPermissions(); |
417 | if ( $blockCheckResult !== true ) { |
418 | $this->logger->debug( 'placeBlock: checkBlockPermissions failed' ); |
419 | return Status::newFatal( $blockCheckResult ); |
420 | } |
421 | |
422 | if ( |
423 | $this->isEmailBlocked && |
424 | !$this->blockPermissionChecker->checkEmailPermissions() |
425 | ) { |
426 | // TODO: Maybe not ignore the error here? |
427 | $this->isEmailBlocked = false; |
428 | } |
429 | |
430 | if ( $this->tags !== [] ) { |
431 | $status = ChangeTags::canAddTagsAccompanyingChange( |
432 | $this->tags, |
433 | $this->performer |
434 | ); |
435 | |
436 | if ( !$status->isOK() ) { |
437 | $this->logger->debug( 'placeBlock: ChangeTags::canAddTagsAccompanyingChange failed' ); |
438 | return $status; |
439 | } |
440 | } |
441 | |
442 | $status = Status::newGood(); |
443 | foreach ( $this->getPageRestrictions() as $pageRestriction ) { |
444 | try { |
445 | $title = $this->titleFactory->newFromTextThrow( $pageRestriction ); |
446 | if ( !$title->exists() ) { |
447 | $this->logger->debug( "placeBlock: nonexistent page restriction $title" ); |
448 | $status->fatal( 'cant-block-nonexistent-page', $pageRestriction ); |
449 | } |
450 | } catch ( MalformedTitleException $e ) { |
451 | $this->logger->debug( 'placeBlock: malformed page restriction title' ); |
452 | $status->fatal( $e->getMessageObject() ); |
453 | } |
454 | } |
455 | if ( !$status->isOK() ) { |
456 | return $status; |
457 | } |
458 | |
459 | return $this->placeBlockUnsafe( $reblock ); |
460 | } |
461 | |
462 | /** |
463 | * Place a block without any sort of permissions checks. |
464 | * |
465 | * @param bool $reblock Should this reblock? |
466 | * |
467 | * @return Status If the block is successful, the value of the returned |
468 | * Status is an instance of a newly placed block. |
469 | */ |
470 | public function placeBlockUnsafe( bool $reblock = false ): Status { |
471 | $status = $this->blockUtils->validateTarget( $this->target ); |
472 | |
473 | if ( !$status->isOK() ) { |
474 | $this->logger->debug( 'placeBlockUnsafe: invalid target' ); |
475 | return $status; |
476 | } |
477 | |
478 | if ( $this->isUserTalkEditBlocked === null ) { |
479 | $this->logger->debug( 'placeBlockUnsafe: partial block on user talk page' ); |
480 | return Status::newFatal( 'ipb-prevent-user-talk-edit' ); |
481 | } |
482 | |
483 | if ( |
484 | // There should be some expiry |
485 | strlen( $this->rawExpiry ) === 0 || |
486 | // can't be a larger string as 50 (it should be a time format in any way) |
487 | strlen( $this->rawExpiry ) > 50 || |
488 | // the time can't be parsed |
489 | !$this->expiryTime |
490 | ) { |
491 | $this->logger->debug( 'placeBlockUnsafe: invalid expiry' ); |
492 | return Status::newFatal( 'ipb_expiry_invalid' ); |
493 | } |
494 | |
495 | if ( $this->expiryTime < wfTimestampNow() ) { |
496 | $this->logger->debug( 'placeBlockUnsafe: expiry in the past' ); |
497 | return Status::newFatal( 'ipb_expiry_old' ); |
498 | } |
499 | |
500 | if ( $this->isHideUser ) { |
501 | if ( $this->isPartial() ) { |
502 | $this->logger->debug( 'placeBlockUnsafe: partial block cannot hide user' ); |
503 | return Status::newFatal( 'ipb_hide_partial' ); |
504 | } |
505 | |
506 | if ( !wfIsInfinity( $this->rawExpiry ) ) { |
507 | $this->logger->debug( 'placeBlockUnsafe: temp user block has expiry' ); |
508 | return Status::newFatal( 'ipb_expiry_temp' ); |
509 | } |
510 | |
511 | $hideUserContribLimit = $this->options->get( MainConfigNames::HideUserContribLimit ); |
512 | if ( |
513 | $hideUserContribLimit !== false && |
514 | $this->userEditTracker->getUserEditCount( $this->target ) > $hideUserContribLimit |
515 | ) { |
516 | $this->logger->debug( 'placeBlockUnsafe: hide user with too many contribs' ); |
517 | return Status::newFatal( 'ipb_hide_invalid', Message::numParam( $hideUserContribLimit ) ); |
518 | } |
519 | } |
520 | |
521 | if ( $this->isPartial() ) { |
522 | if ( |
523 | $this->blockRestrictions === [] && |
524 | !$this->isEmailBlocked && |
525 | !$this->isCreateAccountBlocked && |
526 | !$this->isUserTalkEditBlocked |
527 | ) { |
528 | $this->logger->debug( 'placeBlockUnsafe: empty partial block' ); |
529 | return Status::newFatal( 'ipb-empty-block' ); |
530 | } |
531 | } |
532 | |
533 | return $this->placeBlockInternal( $reblock ); |
534 | } |
535 | |
536 | /** |
537 | * Places a block without any sort of permission or double checking, hooks can still |
538 | * abort the block through, as well as already existing block. |
539 | * |
540 | * @param bool $reblock Should this reblock? |
541 | * |
542 | * @return Status |
543 | */ |
544 | private function placeBlockInternal( bool $reblock = true ): Status { |
545 | $block = $this->configureBlock(); |
546 | |
547 | $denyReason = [ 'hookaborted' ]; |
548 | $legacyUser = $this->userFactory->newFromAuthority( $this->performer ); |
549 | if ( !$this->hookRunner->onBlockIp( $block, $legacyUser, $denyReason ) ) { |
550 | $status = Status::newGood(); |
551 | foreach ( $denyReason as $key ) { |
552 | $this->logger->debug( "placeBlockInternal: hook aborted with message \"$key\"" ); |
553 | $status->fatal( $key ); |
554 | } |
555 | return $status; |
556 | } |
557 | |
558 | // Is there a conflicting block? |
559 | // xxx: there is an identical call at the beginning of ::placeBlock |
560 | $priorBlock = $this->blockStore |
561 | ->newFromTarget( $this->target, null, /*fromPrimary=*/true ); |
562 | |
563 | // T287798: we are blocking an IP that is currently autoblocked |
564 | // we can ignore the block because ipb_address_unique allows the IP address |
565 | // be both manually blocked and autoblocked |
566 | // this will work as long as DatabaseBlockStore::newLoad prefers manual IP blocks |
567 | // over autoblocks |
568 | if ( $priorBlock !== null |
569 | && $priorBlock->getType() === AbstractBlock::TYPE_AUTO |
570 | && $this->targetType === AbstractBlock::TYPE_IP |
571 | ) { |
572 | $priorBlock = null; |
573 | } |
574 | |
575 | if ( $priorBlock !== null ) { |
576 | // Reblock only if the caller wants so |
577 | if ( !$reblock ) { |
578 | $this->logger->debug( |
579 | 'placeBlockInternal: already blocked and reblock not requested' ); |
580 | return Status::newFatal( 'ipb_already_blocked', $block->getTargetName() ); |
581 | } |
582 | |
583 | if ( $block->equals( $priorBlock ) ) { |
584 | // Block settings are equal => user is already blocked |
585 | $this->logger->debug( 'placeBlockInternal: already blocked, no change' ); |
586 | return Status::newFatal( 'ipb_already_blocked', $block->getTargetName() ); |
587 | } |
588 | |
589 | $currentBlock = $this->configureBlock( $priorBlock ); |
590 | $logEntry = $this->prepareLogEntry( true ); |
591 | $this->blockStore->updateBlock( $currentBlock ); // TODO handle failure |
592 | $block = $currentBlock; |
593 | } else { |
594 | $logEntry = $this->prepareLogEntry( false ); |
595 | // Try to insert block. |
596 | $insertStatus = $this->blockStore->insertBlock( $block ); |
597 | if ( !$insertStatus ) { |
598 | $this->logger->warning( 'Block could not be inserted. No existing block was found.' ); |
599 | return Status::newFatal( 'ipb-block-not-found', $block->getTargetName() ); |
600 | } |
601 | } |
602 | // Relate log ID to block ID (T27763) |
603 | $logEntry->setRelations( [ 'ipb_id' => $block->getId() ] ); |
604 | |
605 | // Set *_deleted fields if requested |
606 | if ( $this->isHideUser ) { |
607 | // This should only be the case of $this->target is a user, so we can |
608 | // safely call ->getId() |
609 | RevisionDeleteUser::suppressUserName( $this->target->getName(), $this->target->getId() ); |
610 | } |
611 | |
612 | DeferredUpdates::addCallableUpdate( function () use ( $block, $legacyUser, $priorBlock ) { |
613 | $this->hookRunner->onBlockIpComplete( $block, $legacyUser, $priorBlock ); |
614 | } ); |
615 | |
616 | // DatabaseBlock constructor sanitizes certain block options on insert |
617 | $this->isEmailBlocked = $block->isEmailBlocked(); |
618 | $this->isAutoblocking = $block->isAutoblocking(); |
619 | |
620 | $this->log( $logEntry ); |
621 | |
622 | $this->logger->debug( 'placeBlockInternal: success' ); |
623 | return Status::newGood( $block ); |
624 | } |
625 | |
626 | /** |
627 | * Build namespace restrictions array from $this->blockRestrictions |
628 | * |
629 | * Returns an array of namespace IDs. |
630 | * |
631 | * @return int[] |
632 | */ |
633 | private function getNamespaceRestrictions(): array { |
634 | $namespaceRestrictions = []; |
635 | foreach ( $this->blockRestrictions as $restriction ) { |
636 | if ( $restriction instanceof NamespaceRestriction ) { |
637 | $namespaceRestrictions[] = $restriction->getValue(); |
638 | } |
639 | } |
640 | return $namespaceRestrictions; |
641 | } |
642 | |
643 | /** |
644 | * Build an array of page restrictions from $this->blockRestrictions |
645 | * |
646 | * Returns an array of stringified full page titles. |
647 | * |
648 | * @return string[] |
649 | */ |
650 | private function getPageRestrictions(): array { |
651 | $pageRestrictions = []; |
652 | foreach ( $this->blockRestrictions as $restriction ) { |
653 | if ( $restriction instanceof PageRestriction ) { |
654 | $pageRestrictions[] = $restriction->getTitle()->getFullText(); |
655 | } |
656 | } |
657 | return $pageRestrictions; |
658 | } |
659 | |
660 | /** |
661 | * Build an array of actions from $this->blockRestrictions |
662 | * |
663 | * Returns an array of stringified actions. |
664 | * |
665 | * @return string[] |
666 | */ |
667 | private function getActionRestrictions(): array { |
668 | $actionRestrictions = []; |
669 | foreach ( $this->blockRestrictions as $restriction ) { |
670 | if ( $restriction instanceof ActionRestriction ) { |
671 | $actionRestrictions[] = $this->blockActionInfo->getActionFromId( $restriction->getValue() ); |
672 | } |
673 | } |
674 | return $actionRestrictions; |
675 | } |
676 | |
677 | /** |
678 | * Prepare $logParams |
679 | * |
680 | * Helper method for $this->log() |
681 | * |
682 | * @return array |
683 | */ |
684 | private function constructLogParams(): array { |
685 | $logExpiry = wfIsInfinity( $this->rawExpiry ) ? 'infinity' : $this->rawExpiry; |
686 | $logParams = [ |
687 | '5::duration' => $logExpiry, |
688 | '6::flags' => $this->blockLogFlags(), |
689 | 'sitewide' => !$this->isPartial() |
690 | ]; |
691 | |
692 | if ( $this->isPartial() ) { |
693 | $pageRestrictions = $this->getPageRestrictions(); |
694 | $namespaceRestrictions = $this->getNamespaceRestrictions(); |
695 | $actionRestrictions = $this->getActionRestrictions(); |
696 | |
697 | if ( count( $pageRestrictions ) > 0 ) { |
698 | $logParams['7::restrictions']['pages'] = $pageRestrictions; |
699 | } |
700 | if ( count( $namespaceRestrictions ) > 0 ) { |
701 | $logParams['7::restrictions']['namespaces'] = $namespaceRestrictions; |
702 | } |
703 | if ( count( $actionRestrictions ) ) { |
704 | $logParams['7::restrictions']['actions'] = $actionRestrictions; |
705 | } |
706 | } |
707 | return $logParams; |
708 | } |
709 | |
710 | /** |
711 | * Create the log entry object to be inserted. Do read queries here before |
712 | * we start locking block_target rows. |
713 | * |
714 | * @param bool $isReblock |
715 | * @return ManualLogEntry |
716 | */ |
717 | private function prepareLogEntry( bool $isReblock ) { |
718 | $logType = $this->isHideUser ? 'suppress' : 'block'; |
719 | $logAction = $isReblock ? 'reblock' : 'block'; |
720 | $title = Title::makeTitle( NS_USER, $this->target ); |
721 | // Preload the page_id: needed for log_page in ManualLogEntry::insert() |
722 | $title->getArticleID(); |
723 | |
724 | $logEntry = new ManualLogEntry( $logType, $logAction ); |
725 | $logEntry->setTarget( $title ); |
726 | $logEntry->setComment( $this->reason ); |
727 | $logEntry->setPerformer( $this->performer->getUser() ); |
728 | $logEntry->setParameters( $this->constructLogParams() ); |
729 | $logEntry->addTags( $this->tags ); |
730 | if ( $this->logDeletionFlags !== null ) { |
731 | $logEntry->setDeleted( $this->logDeletionFlags ); |
732 | } |
733 | return $logEntry; |
734 | } |
735 | |
736 | /** |
737 | * Log the block to Special:Log |
738 | * |
739 | * @param ManualLogEntry $logEntry |
740 | */ |
741 | private function log( ManualLogEntry $logEntry ) { |
742 | $logId = $logEntry->insert(); |
743 | $logEntry->publish( $logId ); |
744 | } |
745 | |
746 | /** |
747 | * Return a comma-delimited list of flags to be passed to the log |
748 | * reader for this block, to provide more information in the logs. |
749 | * |
750 | * @return string |
751 | */ |
752 | private function blockLogFlags(): string { |
753 | $flags = []; |
754 | |
755 | if ( $this->targetType != AbstractBlock::TYPE_USER && !$this->isHardBlock ) { |
756 | // For grepping: message block-log-flags-anononly |
757 | $flags[] = 'anononly'; |
758 | } |
759 | |
760 | if ( $this->isCreateAccountBlocked ) { |
761 | // For grepping: message block-log-flags-nocreate |
762 | $flags[] = 'nocreate'; |
763 | } |
764 | |
765 | if ( $this->targetType == AbstractBlock::TYPE_USER && !$this->isAutoblocking ) { |
766 | // For grepping: message block-log-flags-noautoblock |
767 | $flags[] = 'noautoblock'; |
768 | } |
769 | |
770 | if ( $this->isEmailBlocked ) { |
771 | // For grepping: message block-log-flags-noemail |
772 | $flags[] = 'noemail'; |
773 | } |
774 | |
775 | if ( $this->options->get( MainConfigNames::BlockAllowsUTEdit ) && $this->isUserTalkEditBlocked ) { |
776 | // For grepping: message block-log-flags-nousertalk |
777 | $flags[] = 'nousertalk'; |
778 | } |
779 | |
780 | if ( $this->isHideUser ) { |
781 | // For grepping: message block-log-flags-hiddenname |
782 | $flags[] = 'hiddenname'; |
783 | } |
784 | |
785 | return implode( ',', $flags ); |
786 | } |
787 | } |