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