Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.26% covered (warning)
54.26%
70 / 129
60.61% covered (warning)
60.61%
20 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractBlock
54.26% covered (warning)
54.26%
70 / 129
60.61% covered (warning)
60.61%
20 / 33
442.72
0.00% covered (danger)
0.00%
0 / 1
 __construct
77.78% covered (warning)
77.78%
21 / 27
0.00% covered (danger)
0.00%
0 / 1
5.27
 getBy
n/a
0 / 0
n/a
0 / 0
0
 getByName
n/a
0 / 0
n/a
0 / 0
0
 getId
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getReasonComment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setReason
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHideName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHideName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setHideBlock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHideBlock
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isSitewide
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isCreateAccountBlocked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isEmailBlocked
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isUsertalkEditAllowed
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isHardblock
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 appliesToRight
57.14% covered (warning)
57.14%
12 / 21
0.00% covered (danger)
0.00%
0 / 1
15.38
 getTarget
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRedactedTarget
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getTargetUserIdentity
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getTargetName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBlocking
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 getExpiry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isIndefinite
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setExpiry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTimestamp
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTarget
33.33% covered (danger)
33.33%
5 / 15
0.00% covered (danger)
0.00%
0 / 1
8.74
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 appliesToUsertalk
22.73% covered (danger)
22.73%
5 / 22
0.00% covered (danger)
0.00%
0 / 1
37.53
 appliesToTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToNamespace
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 appliesToPasswordReset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 toArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Block;
8
9use InvalidArgumentException;
10use LogicException;
11use MediaWiki\CommentStore\CommentStoreComment;
12use MediaWiki\DAO\WikiAwareEntityTrait;
13use MediaWiki\MainConfigNames;
14use MediaWiki\MediaWikiServices;
15use MediaWiki\Message\Message;
16use MediaWiki\Title\Title;
17use MediaWiki\User\UserIdentity;
18use Wikimedia\Timestamp\TimestampFormat as TS;
19
20/**
21 * @note Extensions should not subclass this, as MediaWiki currently does not
22 *   support custom block types.
23 * @since 1.34 Factored out from DatabaseBlock (previously Block).
24 */
25abstract class AbstractBlock implements Block {
26    use WikiAwareEntityTrait;
27
28    /** @var CommentStoreComment */
29    protected $reason;
30
31    /** @var string */
32    protected $timestamp = '';
33
34    /** @var string */
35    protected $expiry = '';
36
37    /** @var bool */
38    protected $blockEmail = false;
39
40    /** @var bool */
41    protected $allowUsertalk = false;
42
43    /** @var bool */
44    protected $blockCreateAccount = false;
45
46    /** @var bool */
47    protected $hideName = false;
48
49    protected bool $hideBlock = false;
50
51    /** @var bool */
52    protected $isHardblock;
53
54    /** @var BlockTarget|null */
55    protected $target;
56
57    /** @var bool */
58    protected $isSitewide = true;
59
60    protected string|false $wikiId;
61
62    /**
63     * Create a new block with specified parameters on a user, IP or IP range.
64     *
65     * @param array $options Parameters of the block, with supported options:
66     *  - target: (BlockTarget) The target object (since 1.44)
67     *  - address: (string|UserIdentity) Deprecated since 1.45, use 'target'.
68     *  - wiki: (string|false) The wiki the block has been issued in,
69     *    self::LOCAL for the local wiki (since 1.38)
70     *  - reason: (string|Message|CommentStoreComment) Reason for the block
71     *  - timestamp: (string) The time at which the block comes into effect,
72     *    in any format supported by wfTimestamp()
73     *  - decodedTimestamp: (string) The timestamp in MW 14-character format
74     *  - hideName: (bool) Hide the target user name
75     *  - hideBlock: (bool) Hide the block (since 1.47)
76     *  - anonOnly: (bool) Used if the target is an IP address. The block only
77     *    applies to anon and temporary users using this IP address, and not to
78     *    logged-in users.
79     */
80    public function __construct( array $options = [] ) {
81        $defaults = [
82            'wiki'            => self::LOCAL,
83            'reason'          => '',
84            'timestamp'       => '',
85            'hideName'        => false,
86            'hideBlock'       => false,
87            'anonOnly'        => false,
88        ];
89
90        $options += $defaults;
91
92        $this->wikiId = $options['wiki'];
93        if ( isset( $options['target'] ) ) {
94            if ( !( $options['target'] instanceof BlockTarget ) ) {
95                throw new InvalidArgumentException( 'Invalid block target' );
96            }
97            $this->setTarget( $options['target'] );
98        } elseif ( isset( $options['address'] ) ) {
99            wfDeprecatedMsg(
100                'The address parameter to AbstractBlock::__construct is deprecated since 1.45',
101                '1.45' );
102            $this->setTarget( $options['address'] );
103        } else {
104            $this->setTarget( null );
105        }
106        $this->setReason( $options['reason'] );
107        if ( isset( $options['decodedTimestamp'] ) ) {
108            $this->setTimestamp( $options['decodedTimestamp'] );
109        } else {
110            $this->setTimestamp( wfTimestamp( TS::MW, $options['timestamp'] ) );
111        }
112        $this->setHideName( (bool)$options['hideName'] );
113        $this->setHideBlock( (bool)$options['hideBlock'] );
114        $this->isHardblock( !$options['anonOnly'] );
115    }
116
117    /**
118     * Get the user id of the blocking sysop
119     *
120     * @param string|false $wikiId (since 1.38)
121     * @return int (0 for foreign users)
122     */
123    abstract public function getBy( $wikiId = self::LOCAL ): int;
124
125    /**
126     * Get the username of the blocking sysop
127     *
128     * @return string
129     */
130    abstract public function getByName();
131
132    /**
133     * @inheritDoc
134     */
135    public function getId( $wikiId = self::LOCAL ): ?int {
136        $this->assertWiki( $wikiId );
137        return null;
138    }
139
140    /**
141     * Get the reason for creating the block.
142     *
143     * @since 1.35
144     * @return CommentStoreComment
145     */
146    public function getReasonComment(): CommentStoreComment {
147        return $this->reason;
148    }
149
150    /**
151     * Set the reason for creating the block.
152     *
153     * @since 1.33
154     * @param string|Message|CommentStoreComment $reason
155     */
156    public function setReason( $reason ) {
157        $this->reason = CommentStoreComment::newUnsavedComment( $reason );
158    }
159
160    /**
161     * Get whether the block hides the target's username
162     *
163     * @since 1.33
164     * @return bool The block hides the username
165     */
166    public function getHideName() {
167        return $this->hideName;
168    }
169
170    /**
171     * Set whether the block hides the target's username.
172     * If you only want to hide the block, then use {@link self::setHideBlock} instead.
173     *
174     * @since 1.33
175     * @param bool $hideName The block hides the username
176     */
177    public function setHideName( $hideName ) {
178        $this->hideName = $hideName;
179    }
180
181    /**
182     * Set whether the block is hidden. This is different to
183     * {@link self::setHideName} which hides the target of the block everywhere.
184     *
185     * @since 1.46
186     */
187    public function setHideBlock( bool $hideBlock ): void {
188        $this->hideBlock = $hideBlock;
189    }
190
191    /**
192     * Get whether the block is hidden. This always returns true
193     * if {@link self::getHideName} returns `true`.
194     *
195     * @since 1.46
196     */
197    public function getHideBlock(): bool {
198        return $this->hideBlock || $this->hideName;
199    }
200
201    /**
202     * Indicates that the block is a sitewide block. This means the user is
203     * prohibited from editing any page on the site (other than their own talk
204     * page).
205     *
206     * @since 1.33
207     * @param null|bool $x
208     * @return bool
209     */
210    public function isSitewide( $x = null ): bool {
211        return wfSetVar( $this->isSitewide, $x );
212    }
213
214    /**
215     * Get or set the flag indicating whether this block blocks the target from
216     * creating an account. (Note that the flag may be overridden depending on
217     * global configs.)
218     *
219     * @since 1.33
220     * @param null|bool $x Value to set (if null, just get the property value)
221     * @return bool Value of the property
222     */
223    public function isCreateAccountBlocked( $x = null ): bool {
224        return wfSetVar( $this->blockCreateAccount, $x );
225    }
226
227    /**
228     * Get or set the flag indicating whether this block blocks the target from
229     * sending emails. (Note that the flag may be overridden depending on
230     * global configs.)
231     *
232     * @since 1.33
233     * @param null|bool $x Value to set (if null, just get the property value)
234     * @return bool Value of the property
235     */
236    public function isEmailBlocked( $x = null ): bool {
237        return wfSetVar( $this->blockEmail, $x );
238    }
239
240    /**
241     * Get or set the flag indicating whether this block blocks the target from
242     * editing their own user talk page. (Note that the flag may be overridden
243     * depending on global configs.)
244     *
245     * @since 1.33
246     * @param null|bool $x Value to set (if null, just get the property value)
247     * @return bool Value of the property
248     */
249    public function isUsertalkEditAllowed( $x = null ) {
250        return wfSetVar( $this->allowUsertalk, $x );
251    }
252
253    /**
254     * Get/set whether the block is a hard block (affects logged-in users on a
255     * given IP/range).
256     *
257     * Note that temporary users are not considered logged-in here - they are
258     * always blocked by IP-address blocks.
259     *
260     * Note that user blocks are always hard blocks, since the target is logged
261     * in by definition.
262     *
263     * @since 1.36 Moved up from DatabaseBlock
264     * @param bool|null $x
265     * @return bool
266     */
267    public function isHardblock( $x = null ): bool {
268        wfSetVar( $this->isHardblock, $x );
269
270        return $this->getType() == self::TYPE_USER
271            ? true
272            : $this->isHardblock;
273    }
274
275    /**
276     * Determine whether the block prevents a given right. A right may be
277     * allowed or disallowed by default, or determined from a property on the
278     * block object. For certain rights, the property may be overridden
279     * according to global configs.
280     *
281     * @since 1.33
282     * @param string $right
283     * @return bool|null The block applies to the right, or null if
284     *  unsure (e.g. unrecognized right or unset property)
285     */
286    public function appliesToRight( $right ) {
287        $blockDisablesLogin = MediaWikiServices::getInstance()->getMainConfig()
288            ->get( MainConfigNames::BlockDisablesLogin );
289
290        $res = null;
291        switch ( $right ) {
292            case 'autocreateaccount':
293            case 'createaccount':
294                $res = $this->isCreateAccountBlocked();
295                break;
296            case 'sendemail':
297                $res = $this->isEmailBlocked();
298                break;
299            case 'upload':
300                // Sitewide blocks always block upload. This may be overridden in a subclass.
301                $res = $this->isSitewide();
302                break;
303            case 'read':
304                $res = false;
305                break;
306        }
307        if ( !$res && $blockDisablesLogin ) {
308            // If a block would disable login, then it should
309            // prevent any right that all users cannot do
310            $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
311            $anon = MediaWikiServices::getInstance()->getUserFactory()->newAnonymous();
312            $res = $permissionManager->userHasRight( $anon, $right ) ? $res : true;
313        }
314
315        return $res;
316    }
317
318    public function getTarget(): ?BlockTarget {
319        return $this->target;
320    }
321
322    public function getRedactedTarget(): ?BlockTarget {
323        $target = $this->getTarget();
324        if ( $this->getType() === Block::TYPE_AUTO
325            && !( $target instanceof AutoBlockTarget )
326        ) {
327            $id = $this->getId( $this->wikiId );
328            if ( $id === null ) {
329                throw new LogicException( 'no ID available for autoblock redaction' );
330            }
331            $target = new AutoBlockTarget( $id, $this->wikiId );
332        }
333        return $target;
334    }
335
336    /**
337     * Get the type of target for this particular block.
338     * @return int|null AbstractBlock::TYPE_ constant
339     */
340    public function getType(): ?int {
341        return $this->target ? $this->target->getType() : null;
342    }
343
344    /**
345     * @since 1.37
346     * @return ?UserIdentity
347     */
348    public function getTargetUserIdentity(): ?UserIdentity {
349        return $this->target instanceof BlockTargetWithUserIdentity
350            ? $this->target->getUserIdentity() : null;
351    }
352
353    /**
354     * @since 1.37
355     * @return string
356     */
357    public function getTargetName(): string {
358        return (string)$this->target;
359    }
360
361    /**
362     * @param BlockTarget|UserIdentity|string $target
363     *
364     * @return bool
365     * @since 1.37
366     */
367    public function isBlocking( $target ): bool {
368        $targetName = $target instanceof UserIdentity
369            ? $target->getName()
370            : (string)$target;
371
372        return $targetName === $this->getTargetName();
373    }
374
375    /**
376     * Get the block expiry time
377     *
378     * @since 1.19
379     * @return string
380     */
381    public function getExpiry(): string {
382        return $this->expiry;
383    }
384
385    /** @inheritDoc */
386    public function isIndefinite(): bool {
387        return wfIsInfinity( $this->getExpiry() );
388    }
389
390    /**
391     * Set the block expiry time
392     *
393     * @since 1.33
394     * @param string $expiry
395     */
396    public function setExpiry( $expiry ) {
397        // Force string so getExpiry() return typehint doesn't break things
398        $this->expiry = (string)$expiry;
399    }
400
401    /**
402     * Get the timestamp indicating when the block was created
403     *
404     * @since 1.33
405     * @return string
406     */
407    public function getTimestamp(): string {
408        return $this->timestamp;
409    }
410
411    /**
412     * Set the timestamp indicating when the block was created
413     *
414     * @since 1.33
415     * @param string $timestamp
416     */
417    public function setTimestamp( $timestamp ) {
418        // Force string so getTimestamp() return typehint doesn't break things
419        $this->timestamp = (string)$timestamp;
420    }
421
422    /**
423     * Set the target for this block
424     * @param BlockTarget|string|UserIdentity|null $target Passing UserIdentity|string is deprecated
425     *   since 1.45. Set the target by passing BlockTarget|null.
426     */
427    public function setTarget( $target ) {
428        // Small optimization to make this code testable, this is what would happen anyway
429        if ( $target === '' || $target === null ) {
430            $this->target = null;
431        } elseif ( $target instanceof BlockTarget ) {
432            $this->assertWiki( $target->getWikiId() );
433            $this->target = $target;
434        } else {
435            wfDeprecatedMsg(
436                'Passing UserIdentity|string to AbstractBlock::setTarget is deprecated since 1.45',
437                '1.45'
438            );
439            $parsedTarget = MediaWikiServices::getInstance()
440                ->getCrossWikiBlockTargetFactory()
441                ->getFactory( $this->wikiId )
442                ->newFromLegacyUnion( $target );
443            $this->assertWiki( $parsedTarget->getWikiId() );
444            $this->target = $parsedTarget;
445        }
446    }
447
448    /**
449     * @since 1.38
450     * @return string|false
451     */
452    public function getWikiId() {
453        return $this->wikiId;
454    }
455
456    /**
457     * Determine whether the block allows the user to edit their own
458     * user talk page. This is done separately from
459     * AbstractBlock::appliesToRight because there is no right for
460     * editing one's own user talk page and because the user's talk
461     * page needs to be passed into the block object, which is unaware
462     * of the user.
463     *
464     * The bl_allow_usertalk flag (which corresponds to the property
465     * allowUsertalk) is used on sitewide blocks and partial blocks
466     * that contain a namespace restriction on the user talk namespace,
467     * but do not contain a page restriction on the user's talk page.
468     * For all other (i.e. most) partial blocks, the flag is ignored,
469     * and the user can always edit their user talk page unless there
470     * is a page restriction on their user talk page, in which case
471     * they can never edit it. (Ideally the flag would be stored as
472     * null in these cases, but the database field isn't nullable.)
473     *
474     * This method does not validate that the passed in talk page belongs to the
475     * block target since the target (an IP) might not be the same as the user's
476     * talk page (if they are logged in).
477     *
478     * @since 1.33
479     * @param Title|null $usertalk The user's user talk page. If null,
480     *  and if the target is a User, the target's userpage is used
481     * @return bool The user can edit their talk page
482     */
483    public function appliesToUsertalk( ?Title $usertalk = null ): bool {
484        if ( !$usertalk ) {
485            if ( $this->target instanceof BlockTargetWithUserPage ) {
486                $usertalk = Title::makeTitle(
487                    NS_USER_TALK,
488                    $this->target->getUserPage()->getDBkey()
489                );
490            } else {
491                throw new InvalidArgumentException(
492                    '$usertalk must be provided if block target is not a user/IP'
493                );
494            }
495        }
496
497        if ( $usertalk->getNamespace() !== NS_USER_TALK ) {
498            throw new InvalidArgumentException(
499                '$usertalk must be a user talk page'
500            );
501        }
502
503        if ( !$this->isSitewide() ) {
504            if ( $this->appliesToPage( $usertalk->getArticleID() ) ) {
505                return true;
506            }
507            if ( !$this->appliesToNamespace( NS_USER_TALK ) ) {
508                return false;
509            }
510        }
511
512        // This is a type of block which uses the bl_allow_usertalk
513        // flag. The flag can still be overridden by global configs.
514        if ( !MediaWikiServices::getInstance()->getMainConfig()
515            ->get( MainConfigNames::BlockAllowsUTEdit )
516        ) {
517            return true;
518        }
519        return !$this->isUsertalkEditAllowed();
520    }
521
522    /**
523     * Checks if a block applies to a particular title
524     *
525     * This check does not consider whether `$this->isUsertalkEditAllowed`
526     * returns false, as the identity of the user making the hypothetical edit
527     * isn't known here (particularly in the case of IP hard blocks, range
528     * blocks, and auto-blocks).
529     *
530     * @param Title $title
531     * @return bool
532     */
533    public function appliesToTitle( Title $title ) {
534        return $this->isSitewide();
535    }
536
537    /**
538     * Checks if a block applies to a particular namespace
539     *
540     * @since 1.33
541     *
542     * @param int $ns
543     * @return bool
544     */
545    public function appliesToNamespace( $ns ) {
546        return $this->isSitewide();
547    }
548
549    /**
550     * Checks if a block applies to a particular page
551     *
552     * This check does not consider whether `$this->isUsertalkEditAllowed`
553     * returns false, as the identity of the user making the hypothetical edit
554     * isn't known here (particularly in the case of IP hard blocks, range
555     * blocks, and auto-blocks).
556     *
557     * @since 1.33
558     *
559     * @param int $pageId
560     * @return bool
561     */
562    public function appliesToPage( $pageId ) {
563        return $this->isSitewide();
564    }
565
566    /**
567     * Check if the block prevents a user from resetting their password
568     *
569     * @since 1.33
570     * @return bool The block blocks password reset
571     */
572    public function appliesToPasswordReset() {
573        return $this->isCreateAccountBlocked();
574    }
575
576    /**
577     * @return AbstractBlock[]
578     */
579    public function toArray(): array {
580        return [ $this ];
581    }
582
583}