Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.60% covered (danger)
21.60%
54 / 250
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
BlockUser
21.60% covered (danger)
21.60%
54 / 250
0.00% covered (danger)
0.00%
0 / 15
4170.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
156
 setLogDeletionFlags
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseExpiryInput
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isPartial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 configureBlock
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 placeBlock
58.33% covered (warning)
58.33%
21 / 36
0.00% covered (danger)
0.00%
0 / 1
28.18
 placeBlockUnsafe
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
306
 placeBlockInternal
76.74% covered (warning)
76.74%
33 / 43
0.00% covered (danger)
0.00%
0 / 1
12.52
 getNamespaceRestrictions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getPageRestrictions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getActionRestrictions
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 constructLogParams
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 prepareLogEntry
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 log
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 blockLogFlags
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
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
22namespace MediaWiki\Block;
23
24use ChangeTags;
25use InvalidArgumentException;
26use ManualLogEntry;
27use MediaWiki\Block\Restriction\AbstractRestriction;
28use MediaWiki\Block\Restriction\ActionRestriction;
29use MediaWiki\Block\Restriction\NamespaceRestriction;
30use MediaWiki\Block\Restriction\PageRestriction;
31use MediaWiki\Config\ServiceOptions;
32use MediaWiki\Deferred\DeferredUpdates;
33use MediaWiki\HookContainer\HookContainer;
34use MediaWiki\HookContainer\HookRunner;
35use MediaWiki\MainConfigNames;
36use MediaWiki\Message\Message;
37use MediaWiki\Permissions\Authority;
38use MediaWiki\Status\Status;
39use MediaWiki\Title\MalformedTitleException;
40use MediaWiki\Title\Title;
41use MediaWiki\Title\TitleFactory;
42use MediaWiki\User\UserEditTracker;
43use MediaWiki\User\UserFactory;
44use MediaWiki\User\UserIdentity;
45use Psr\Log\LoggerInterface;
46use RevisionDeleteUser;
47use Wikimedia\ParamValidator\TypeDef\ExpiryDef;
48
49/**
50 * Handles the backend logic of blocking users
51 *
52 * @since 1.36
53 */
54class 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}