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