Merge "Selenium: replace UserLoginPage with BlankPage where possible"
[lhc/web/wiklou.git] / includes / block / AbstractBlock.php
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21 namespace MediaWiki\Block;
22
23 use IContextSource;
24 use InvalidArgumentException;
25 use IP;
26 use RequestContext;
27 use Title;
28 use User;
29
30 /**
31 * @since 1.34 Factored out from DatabaseBlock (previously Block).
32 */
33 abstract class AbstractBlock {
34 /** @var string */
35 public $mReason;
36
37 /** @var string */
38 public $mTimestamp;
39
40 /** @var string */
41 public $mExpiry = '';
42
43 /** @var bool */
44 protected $mBlockEmail = false;
45
46 /** @var bool */
47 protected $allowUsertalk = false;
48
49 /** @var bool */
50 protected $blockCreateAccount = false;
51
52 /** @var bool */
53 public $mHideName = false;
54
55 /** @var User|string */
56 protected $target;
57
58 /**
59 * @var int AbstractBlock::TYPE_ constant. After the block has been loaded
60 * from the database, this can only be USER, IP or RANGE.
61 */
62 protected $type;
63
64 /** @var User */
65 protected $blocker;
66
67 /** @var bool */
68 protected $isSitewide = true;
69
70 # TYPE constants
71 const TYPE_USER = 1;
72 const TYPE_IP = 2;
73 const TYPE_RANGE = 3;
74 const TYPE_AUTO = 4;
75 const TYPE_ID = 5;
76
77 /**
78 * Create a new block with specified parameters on a user, IP or IP range.
79 *
80 * @param array $options Parameters of the block:
81 * address string|User Target user name, User object, IP address or IP range
82 * by int User ID of the blocker
83 * reason string Reason of the block
84 * timestamp string The time at which the block comes into effect
85 * byText string Username of the blocker (for foreign users)
86 */
87 public function __construct( array $options = [] ) {
88 $defaults = [
89 'address' => '',
90 'by' => null,
91 'reason' => '',
92 'timestamp' => '',
93 'byText' => '',
94 ];
95
96 $options += $defaults;
97
98 $this->setTarget( $options['address'] );
99
100 if ( $options['by'] ) {
101 # Local user
102 $this->setBlocker( User::newFromId( $options['by'] ) );
103 } else {
104 # Foreign user
105 $this->setBlocker( $options['byText'] );
106 }
107
108 $this->setReason( $options['reason'] );
109 $this->setTimestamp( wfTimestamp( TS_MW, $options['timestamp'] ) );
110 }
111
112 /**
113 * Get the user id of the blocking sysop
114 *
115 * @return int (0 for foreign users)
116 */
117 public function getBy() {
118 return $this->getBlocker()->getId();
119 }
120
121 /**
122 * Get the username of the blocking sysop
123 *
124 * @return string
125 */
126 public function getByName() {
127 return $this->getBlocker()->getName();
128 }
129
130 /**
131 * Get the block ID
132 * @return int|null
133 */
134 public function getId() {
135 return null;
136 }
137
138 /**
139 * Get the reason given for creating the block
140 *
141 * @since 1.33
142 * @return string
143 */
144 public function getReason() {
145 return $this->mReason;
146 }
147
148 /**
149 * Set the reason for creating the block
150 *
151 * @since 1.33
152 * @param string $reason
153 */
154 public function setReason( $reason ) {
155 $this->mReason = $reason;
156 }
157
158 /**
159 * Get whether the block hides the target's username
160 *
161 * @since 1.33
162 * @return bool The block hides the username
163 */
164 public function getHideName() {
165 return $this->mHideName;
166 }
167
168 /**
169 * Set whether ths block hides the target's username
170 *
171 * @since 1.33
172 * @param bool $hideName The block hides the username
173 */
174 public function setHideName( $hideName ) {
175 $this->mHideName = $hideName;
176 }
177
178 /**
179 * Indicates that the block is a sitewide block. This means the user is
180 * prohibited from editing any page on the site (other than their own talk
181 * page).
182 *
183 * @since 1.33
184 * @param null|bool $x
185 * @return bool
186 */
187 public function isSitewide( $x = null ) {
188 return wfSetVar( $this->isSitewide, $x );
189 }
190
191 /**
192 * Get or set the flag indicating whether this block blocks the target from
193 * creating an account. (Note that the flag may be overridden depending on
194 * global configs.)
195 *
196 * @since 1.33
197 * @param null|bool $x Value to set (if null, just get the property value)
198 * @return bool Value of the property
199 */
200 public function isCreateAccountBlocked( $x = null ) {
201 return wfSetVar( $this->blockCreateAccount, $x );
202 }
203
204 /**
205 * Get or set the flag indicating whether this block blocks the target from
206 * sending emails. (Note that the flag may be overridden depending on
207 * global configs.)
208 *
209 * @since 1.33
210 * @param null|bool $x Value to set (if null, just get the property value)
211 * @return bool Value of the property
212 */
213 public function isEmailBlocked( $x = null ) {
214 return wfSetVar( $this->mBlockEmail, $x );
215 }
216
217 /**
218 * Get or set the flag indicating whether this block blocks the target from
219 * editing their own user talk page. (Note that the flag may be overridden
220 * depending on global configs.)
221 *
222 * @since 1.33
223 * @param null|bool $x Value to set (if null, just get the property value)
224 * @return bool Value of the property
225 */
226 public function isUsertalkEditAllowed( $x = null ) {
227 return wfSetVar( $this->allowUsertalk, $x );
228 }
229
230 /**
231 * Determine whether the block prevents a given right. A right
232 * may be blacklisted or whitelisted, or determined from a
233 * property on the block object. For certain rights, the property
234 * may be overridden according to global configs.
235 *
236 * @since 1.33
237 * @param string $right Right to check
238 * @return bool|null null if unrecognized right or unset property
239 */
240 public function appliesToRight( $right ) {
241 $config = RequestContext::getMain()->getConfig();
242 $blockDisablesLogin = $config->get( 'BlockDisablesLogin' );
243
244 $res = null;
245 switch ( $right ) {
246 case 'edit':
247 // TODO: fix this case to return proper value
248 $res = true;
249 break;
250 case 'createaccount':
251 $res = $this->isCreateAccountBlocked();
252 break;
253 case 'sendemail':
254 $res = $this->isEmailBlocked();
255 break;
256 case 'upload':
257 // Until T6995 is completed
258 $res = $this->isSitewide();
259 break;
260 case 'read':
261 $res = false;
262 break;
263 case 'purge':
264 $res = false;
265 break;
266 }
267 if ( !$res && $blockDisablesLogin ) {
268 // If a block would disable login, then it should
269 // prevent any right that all users cannot do
270 $anon = new User;
271 $res = $anon->isAllowed( $right ) ? $res : true;
272 }
273
274 return $res;
275 }
276
277 /**
278 * Get/set whether the block prevents a given action
279 *
280 * @deprecated since 1.33, use appliesToRight to determine block
281 * behaviour, and specific methods to get/set properties
282 * @param string $action Action to check
283 * @param bool|null $x Value for set, or null to just get value
284 * @return bool|null Null for unrecognized rights.
285 */
286 public function prevents( $action, $x = null ) {
287 $config = RequestContext::getMain()->getConfig();
288 $blockDisablesLogin = $config->get( 'BlockDisablesLogin' );
289 $blockAllowsUTEdit = $config->get( 'BlockAllowsUTEdit' );
290
291 $res = null;
292 switch ( $action ) {
293 case 'edit':
294 # For now... <evil laugh>
295 $res = true;
296 break;
297 case 'createaccount':
298 $res = wfSetVar( $this->blockCreateAccount, $x );
299 break;
300 case 'sendemail':
301 $res = wfSetVar( $this->mBlockEmail, $x );
302 break;
303 case 'upload':
304 // Until T6995 is completed
305 $res = $this->isSitewide();
306 break;
307 case 'editownusertalk':
308 // NOTE: this check is not reliable on partial blocks
309 // since partially blocked users are always allowed to edit
310 // their own talk page unless a restriction exists on the
311 // page or User_talk: namespace
312 wfSetVar( $this->allowUsertalk, $x === null ? null : !$x );
313 $res = !$this->isUsertalkEditAllowed();
314
315 // edit own user talk can be disabled by config
316 if ( !$blockAllowsUTEdit ) {
317 $res = true;
318 }
319 break;
320 case 'read':
321 $res = false;
322 break;
323 case 'purge':
324 $res = false;
325 break;
326 }
327 if ( !$res && $blockDisablesLogin ) {
328 // If a block would disable login, then it should
329 // prevent any action that all users cannot do
330 $anon = new User;
331 $res = $anon->isAllowed( $action ) ? $res : true;
332 }
333
334 return $res;
335 }
336
337 /**
338 * From an existing block, get the target and the type of target.
339 * Note that, except for null, it is always safe to treat the target
340 * as a string; for User objects this will return User::__toString()
341 * which in turn gives User::getName().
342 *
343 * @param string|int|User|null $target
344 * @return array [ User|String|null, AbstractBlock::TYPE_ constant|null ]
345 */
346 public static function parseTarget( $target ) {
347 # We may have been through this before
348 if ( $target instanceof User ) {
349 if ( IP::isValid( $target->getName() ) ) {
350 return [ $target, self::TYPE_IP ];
351 } else {
352 return [ $target, self::TYPE_USER ];
353 }
354 } elseif ( $target === null ) {
355 return [ null, null ];
356 }
357
358 $target = trim( $target );
359
360 if ( IP::isValid( $target ) ) {
361 # We can still create a User if it's an IP address, but we need to turn
362 # off validation checking (which would exclude IP addresses)
363 return [
364 User::newFromName( IP::sanitizeIP( $target ), false ),
365 self::TYPE_IP
366 ];
367
368 } elseif ( IP::isValidRange( $target ) ) {
369 # Can't create a User from an IP range
370 return [ IP::sanitizeRange( $target ), self::TYPE_RANGE ];
371 }
372
373 # Consider the possibility that this is not a username at all
374 # but actually an old subpage (T31797)
375 if ( strpos( $target, '/' ) !== false ) {
376 # An old subpage, drill down to the user behind it
377 $target = explode( '/', $target )[0];
378 }
379
380 $userObj = User::newFromName( $target );
381 if ( $userObj instanceof User ) {
382 # Note that since numbers are valid usernames, a $target of "12345" will be
383 # considered a User. If you want to pass a block ID, prepend a hash "#12345",
384 # since hash characters are not valid in usernames or titles generally.
385 return [ $userObj, self::TYPE_USER ];
386
387 } elseif ( preg_match( '/^#\d+$/', $target ) ) {
388 # Autoblock reference in the form "#12345"
389 return [ substr( $target, 1 ), self::TYPE_AUTO ];
390
391 } else {
392 return [ null, null ];
393 }
394 }
395
396 /**
397 * Get the type of target for this particular block.
398 * @return int AbstractBlock::TYPE_ constant, will never be TYPE_ID
399 */
400 public function getType() {
401 return $this->type;
402 }
403
404 /**
405 * Get the target and target type for this particular block. Note that for autoblocks,
406 * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
407 * in this situation.
408 * @return array [ User|String, AbstractBlock::TYPE_ constant ]
409 * @todo FIXME: This should be an integral part of the block member variables
410 */
411 public function getTargetAndType() {
412 return [ $this->getTarget(), $this->getType() ];
413 }
414
415 /**
416 * Get the target for this particular block. Note that for autoblocks,
417 * this returns the unredacted name; frontend functions need to call $block->getRedactedName()
418 * in this situation.
419 * @return User|string
420 */
421 public function getTarget() {
422 return $this->target;
423 }
424
425 /**
426 * Get the block expiry time
427 *
428 * @since 1.19
429 * @return string
430 */
431 public function getExpiry() {
432 return $this->mExpiry;
433 }
434
435 /**
436 * Set the block expiry time
437 *
438 * @since 1.33
439 * @param string $expiry
440 */
441 public function setExpiry( $expiry ) {
442 $this->mExpiry = $expiry;
443 }
444
445 /**
446 * Get the timestamp indicating when the block was created
447 *
448 * @since 1.33
449 * @return string
450 */
451 public function getTimestamp() {
452 return $this->mTimestamp;
453 }
454
455 /**
456 * Set the timestamp indicating when the block was created
457 *
458 * @since 1.33
459 * @param string $timestamp
460 */
461 public function setTimestamp( $timestamp ) {
462 $this->mTimestamp = $timestamp;
463 }
464
465 /**
466 * Set the target for this block, and update $this->type accordingly
467 * @param mixed $target
468 */
469 public function setTarget( $target ) {
470 list( $this->target, $this->type ) = static::parseTarget( $target );
471 }
472
473 /**
474 * Get the user who implemented this block
475 * @return User User object. May name a foreign user.
476 */
477 public function getBlocker() {
478 return $this->blocker;
479 }
480
481 /**
482 * Set the user who implemented (or will implement) this block
483 * @param User|string $user Local User object or username string
484 */
485 public function setBlocker( $user ) {
486 if ( is_string( $user ) ) {
487 $user = User::newFromName( $user, false );
488 }
489
490 if ( $user->isAnon() && User::isUsableName( $user->getName() ) ) {
491 throw new InvalidArgumentException(
492 'Blocker must be a local user or a name that cannot be a local user'
493 );
494 }
495
496 $this->blocker = $user;
497 }
498
499 /**
500 * Get the key and parameters for the corresponding error message.
501 *
502 * @since 1.22
503 * @param IContextSource $context
504 * @return array
505 */
506 abstract public function getPermissionsError( IContextSource $context );
507
508 /**
509 * Get block information used in different block error messages
510 *
511 * @since 1.33
512 * @param IContextSource $context
513 * @return array
514 */
515 public function getBlockErrorParams( IContextSource $context ) {
516 $lang = $context->getLanguage();
517
518 $blocker = $this->getBlocker();
519 if ( $blocker instanceof User ) { // local user
520 $blockerUserpage = $blocker->getUserPage();
521 $blockerText = $lang->embedBidi( $blockerUserpage->getText() );
522 $link = "[[{$blockerUserpage->getPrefixedText()}|{$blockerText}]]";
523 } else { // foreign user
524 $link = $blocker;
525 }
526
527 $reason = $this->getReason();
528 if ( $reason == '' ) {
529 $reason = $context->msg( 'blockednoreason' )->text();
530 }
531
532 /* $ip returns who *is* being blocked, $intended contains who was meant to be blocked.
533 * This could be a username, an IP range, or a single IP. */
534 $intended = (string)$this->getTarget();
535
536 return [
537 $link,
538 $reason,
539 $context->getRequest()->getIP(),
540 $lang->embedBidi( $this->getByName() ),
541 // TODO: SystemBlock replaces this with the system block type. Clean up
542 // error params so that this is not necessary.
543 $this->getId(),
544 $lang->formatExpiry( $this->getExpiry() ),
545 $lang->embedBidi( $intended ),
546 $lang->userTimeAndDate( $this->getTimestamp(), $context->getUser() ),
547 ];
548 }
549
550 /**
551 * Determine whether the block allows the user to edit their own
552 * user talk page. This is done separately from
553 * AbstractBlock::appliesToRight because there is no right for
554 * editing one's own user talk page and because the user's talk
555 * page needs to be passed into the block object, which is unaware
556 * of the user.
557 *
558 * The ipb_allow_usertalk flag (which corresponds to the property
559 * allowUsertalk) is used on sitewide blocks and partial blocks
560 * that contain a namespace restriction on the user talk namespace,
561 * but do not contain a page restriction on the user's talk page.
562 * For all other (i.e. most) partial blocks, the flag is ignored,
563 * and the user can always edit their user talk page unless there
564 * is a page restriction on their user talk page, in which case
565 * they can never edit it. (Ideally the flag would be stored as
566 * null in these cases, but the database field isn't nullable.)
567 *
568 * This method does not validate that the passed in talk page belongs to the
569 * block target since the target (an IP) might not be the same as the user's
570 * talk page (if they are logged in).
571 *
572 * @since 1.33
573 * @param Title|null $usertalk The user's user talk page. If null,
574 * and if the target is a User, the target's userpage is used
575 * @return bool The user can edit their talk page
576 */
577 public function appliesToUsertalk( Title $usertalk = null ) {
578 if ( !$usertalk ) {
579 if ( $this->target instanceof User ) {
580 $usertalk = $this->target->getTalkPage();
581 } else {
582 throw new InvalidArgumentException(
583 '$usertalk must be provided if block target is not a user/IP'
584 );
585 }
586 }
587
588 if ( $usertalk->getNamespace() !== NS_USER_TALK ) {
589 throw new InvalidArgumentException(
590 '$usertalk must be a user talk page'
591 );
592 }
593
594 if ( !$this->isSitewide() ) {
595 if ( $this->appliesToPage( $usertalk->getArticleID() ) ) {
596 return true;
597 }
598 if ( !$this->appliesToNamespace( NS_USER_TALK ) ) {
599 return false;
600 }
601 }
602
603 // This is a type of block which uses the ipb_allow_usertalk
604 // flag. The flag can still be overridden by global configs.
605 $config = RequestContext::getMain()->getConfig();
606 if ( !$config->get( 'BlockAllowsUTEdit' ) ) {
607 return true;
608 }
609 return !$this->isUsertalkEditAllowed();
610 }
611
612 /**
613 * Checks if a block applies to a particular title
614 *
615 * This check does not consider whether `$this->isUsertalkEditAllowed`
616 * returns false, as the identity of the user making the hypothetical edit
617 * isn't known here (particularly in the case of IP hardblocks, range
618 * blocks, and auto-blocks).
619 *
620 * @param Title $title
621 * @return bool
622 */
623 public function appliesToTitle( Title $title ) {
624 return $this->isSitewide();
625 }
626
627 /**
628 * Checks if a block applies to a particular namespace
629 *
630 * @since 1.33
631 *
632 * @param int $ns
633 * @return bool
634 */
635 public function appliesToNamespace( $ns ) {
636 return $this->isSitewide();
637 }
638
639 /**
640 * Checks if a block applies to a particular page
641 *
642 * This check does not consider whether `$this->isUsertalkEditAllowed`
643 * returns false, as the identity of the user making the hypothetical edit
644 * isn't known here (particularly in the case of IP hardblocks, range
645 * blocks, and auto-blocks).
646 *
647 * @since 1.33
648 *
649 * @param int $pageId
650 * @return bool
651 */
652 public function appliesToPage( $pageId ) {
653 return $this->isSitewide();
654 }
655
656 /**
657 * Check if the block should be tracked with a cookie.
658 *
659 * @since 1.33
660 * @deprecated since 1.34 Use BlockManager::trackBlockWithCookie instead
661 * of calling this directly.
662 * @param bool $isAnon The user is logged out
663 * @return bool The block should be tracked with a cookie
664 */
665 public function shouldTrackWithCookie( $isAnon ) {
666 wfDeprecated( __METHOD__, '1.34' );
667 return false;
668 }
669
670 /**
671 * Check if the block prevents a user from resetting their password
672 *
673 * @since 1.33
674 * @return bool The block blocks password reset
675 */
676 public function appliesToPasswordReset() {
677 return $this->isCreateAccountBlocked();
678 }
679
680 }