Merge "TitlesMultiselectWidget: pass through additional configs"
[lhc/web/wiklou.git] / includes / specials / SpecialBlock.php
1 <?php
2 /**
3 * Implements Special:Block
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 */
23
24 use MediaWiki\Block\BlockRestriction;
25 use MediaWiki\Block\Restriction\PageRestriction;
26
27 /**
28 * A special page that allows users with 'block' right to block users from
29 * editing pages and other actions
30 *
31 * @ingroup SpecialPage
32 */
33 class SpecialBlock extends FormSpecialPage {
34 /** @var User|string|null User to be blocked, as passed either by parameter (url?wpTarget=Foo)
35 * or as subpage (Special:Block/Foo) */
36 protected $target;
37
38 /** @var int Block::TYPE_ constant */
39 protected $type;
40
41 /** @var User|string The previous block target */
42 protected $previousTarget;
43
44 /** @var bool Whether the previous submission of the form asked for HideUser */
45 protected $requestedHideUser;
46
47 /** @var bool */
48 protected $alreadyBlocked;
49
50 /** @var array */
51 protected $preErrors = [];
52
53 public function __construct() {
54 parent::__construct( 'Block', 'block' );
55 }
56
57 public function doesWrites() {
58 return true;
59 }
60
61 /**
62 * Checks that the user can unblock themselves if they are trying to do so
63 *
64 * @param User $user
65 * @throws ErrorPageError
66 */
67 protected function checkExecutePermissions( User $user ) {
68 parent::checkExecutePermissions( $user );
69
70 # T17810: blocked admins should have limited access here
71 $status = self::checkUnblockSelf( $this->target, $user );
72 if ( $status !== true ) {
73 throw new ErrorPageError( 'badaccess', $status );
74 }
75 }
76
77 /**
78 * Handle some magic here
79 *
80 * @param string $par
81 */
82 protected function setParameter( $par ) {
83 # Extract variables from the request. Try not to get into a situation where we
84 # need to extract *every* variable from the form just for processing here, but
85 # there are legitimate uses for some variables
86 $request = $this->getRequest();
87 list( $this->target, $this->type ) = self::getTargetAndType( $par, $request );
88 if ( $this->target instanceof User ) {
89 # Set the 'relevant user' in the skin, so it displays links like Contributions,
90 # User logs, UserRights, etc.
91 $this->getSkin()->setRelevantUser( $this->target );
92 }
93
94 list( $this->previousTarget, /*...*/ ) =
95 Block::parseTarget( $request->getVal( 'wpPreviousTarget' ) );
96 $this->requestedHideUser = $request->getBool( 'wpHideUser' );
97 }
98
99 /**
100 * Customizes the HTMLForm a bit
101 *
102 * @param HTMLForm $form
103 */
104 protected function alterForm( HTMLForm $form ) {
105 $form->setHeaderText( '' );
106 $form->setSubmitDestructive();
107
108 $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit';
109 $form->setSubmitTextMsg( $msg );
110
111 $this->addHelpLink( 'Help:Blocking users' );
112
113 # Don't need to do anything if the form has been posted
114 if ( !$this->getRequest()->wasPosted() && $this->preErrors ) {
115 $s = $form->formatErrors( $this->preErrors );
116 if ( $s ) {
117 $form->addHeaderText( Html::rawElement(
118 'div',
119 [ 'class' => 'error' ],
120 $s
121 ) );
122 }
123 }
124 }
125
126 protected function getDisplayFormat() {
127 return 'ooui';
128 }
129
130 /**
131 * Get the HTMLForm descriptor array for the block form
132 * @return array
133 */
134 protected function getFormFields() {
135 global $wgBlockAllowsUTEdit;
136
137 $user = $this->getUser();
138
139 $suggestedDurations = self::getSuggestedDurations();
140
141 $conf = $this->getConfig();
142 $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
143 $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' );
144
145 $a = [];
146
147 $a['Target'] = [
148 'type' => 'user',
149 'ipallowed' => true,
150 'iprange' => true,
151 'label-message' => 'ipaddressorusername',
152 'id' => 'mw-bi-target',
153 'size' => '45',
154 'autofocus' => true,
155 'required' => true,
156 'validation-callback' => [ __CLASS__, 'validateTargetField' ],
157 ];
158
159 if ( $enablePartialBlocks ) {
160 $a['EditingRestriction'] = [
161 'type' => 'radio',
162 'label' => $this->msg( 'ipb-type-label' )->text(),
163 'options' => [
164 $this->msg( 'ipb-sitewide' )->text() => 'sitewide',
165 $this->msg( 'ipb-partial' )->text() => 'partial',
166 ],
167 ];
168 $a['PageRestrictions'] = [
169 'type' => 'titlesmultiselect',
170 'label' => $this->msg( 'ipb-pages-label' )->text(),
171 'exists' => true,
172 'max' => 10,
173 'cssclass' => 'mw-block-page-restrictions',
174 'showMissing' => false,
175 ];
176 }
177
178 $a['Expiry'] = [
179 'type' => 'expiry',
180 'label-message' => 'ipbexpiry',
181 'required' => true,
182 'options' => $suggestedDurations,
183 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(),
184 ];
185
186 $a['Reason'] = [
187 'type' => 'selectandother',
188 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
189 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
190 // Unicode codepoints (or 255 UTF-8 bytes for old schema).
191 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT,
192 'maxlength-unit' => 'codepoints',
193 'label-message' => 'ipbreason',
194 'options-message' => 'ipbreason-dropdown',
195 ];
196
197 $a['CreateAccount'] = [
198 'type' => 'check',
199 'label-message' => 'ipbcreateaccount',
200 'default' => true,
201 ];
202
203 if ( self::canBlockEmail( $user ) ) {
204 $a['DisableEmail'] = [
205 'type' => 'check',
206 'label-message' => 'ipbemailban',
207 ];
208 }
209
210 if ( $wgBlockAllowsUTEdit ) {
211 $a['DisableUTEdit'] = [
212 'type' => 'check',
213 'label-message' => 'ipb-disableusertalk',
214 'default' => false,
215 ];
216 }
217
218 $a['AutoBlock'] = [
219 'type' => 'check',
220 'label-message' => 'ipbenableautoblock',
221 'default' => true,
222 ];
223
224 # Allow some users to hide name from block log, blocklist and listusers
225 if ( $user->isAllowed( 'hideuser' ) ) {
226 $a['HideUser'] = [
227 'type' => 'check',
228 'label-message' => 'ipbhidename',
229 'cssclass' => 'mw-block-hideuser',
230 ];
231 }
232
233 # Watchlist their user page? (Only if user is logged in)
234 if ( $user->isLoggedIn() ) {
235 $a['Watch'] = [
236 'type' => 'check',
237 'label-message' => 'ipbwatchuser',
238 ];
239 }
240
241 $a['HardBlock'] = [
242 'type' => 'check',
243 'label-message' => 'ipb-hardblock',
244 'default' => false,
245 ];
246
247 # This is basically a copy of the Target field, but the user can't change it, so we
248 # can see if the warnings we maybe showed to the user before still apply
249 $a['PreviousTarget'] = [
250 'type' => 'hidden',
251 'default' => false,
252 ];
253
254 # We'll turn this into a checkbox if we need to
255 $a['Confirm'] = [
256 'type' => 'hidden',
257 'default' => '',
258 'label-message' => 'ipb-confirm',
259 'cssclass' => 'mw-block-confirm',
260 ];
261
262 $this->maybeAlterFormDefaults( $a );
263
264 // Allow extensions to add more fields
265 Hooks::run( 'SpecialBlockModifyFormFields', [ $this, &$a ] );
266
267 return $a;
268 }
269
270 /**
271 * If the user has already been blocked with similar settings, load that block
272 * and change the defaults for the form fields to match the existing settings.
273 * @param array &$fields HTMLForm descriptor array
274 * @return bool Whether fields were altered (that is, whether the target is
275 * already blocked)
276 */
277 protected function maybeAlterFormDefaults( &$fields ) {
278 # This will be overwritten by request data
279 $fields['Target']['default'] = (string)$this->target;
280
281 if ( $this->target ) {
282 $status = self::validateTarget( $this->target, $this->getUser() );
283 if ( !$status->isOK() ) {
284 $errors = $status->getErrorsArray();
285 $this->preErrors = array_merge( $this->preErrors, $errors );
286 }
287 }
288
289 # This won't be
290 $fields['PreviousTarget']['default'] = (string)$this->target;
291
292 $block = Block::newFromTarget( $this->target );
293
294 if ( $block instanceof Block && !$block->mAuto # The block exists and isn't an autoblock
295 && ( $this->type != Block::TYPE_RANGE # The block isn't a rangeblock
296 || $block->getTarget() == $this->target ) # or if it is, the range is what we're about to block
297 ) {
298 $fields['HardBlock']['default'] = $block->isHardblock();
299 $fields['CreateAccount']['default'] = $block->prevents( 'createaccount' );
300 $fields['AutoBlock']['default'] = $block->isAutoblocking();
301
302 if ( isset( $fields['DisableEmail'] ) ) {
303 $fields['DisableEmail']['default'] = $block->prevents( 'sendemail' );
304 }
305
306 if ( isset( $fields['HideUser'] ) ) {
307 $fields['HideUser']['default'] = $block->mHideName;
308 }
309
310 if ( isset( $fields['DisableUTEdit'] ) ) {
311 $fields['DisableUTEdit']['default'] = $block->prevents( 'editownusertalk' );
312 }
313
314 // If the username was hidden (ipb_deleted == 1), don't show the reason
315 // unless this user also has rights to hideuser: T37839
316 if ( !$block->mHideName || $this->getUser()->isAllowed( 'hideuser' ) ) {
317 $fields['Reason']['default'] = $block->mReason;
318 } else {
319 $fields['Reason']['default'] = '';
320 }
321
322 if ( $this->getRequest()->wasPosted() ) {
323 # Ok, so we got a POST submission asking us to reblock a user. So show the
324 # confirm checkbox; the user will only see it if they haven't previously
325 $fields['Confirm']['type'] = 'check';
326 } else {
327 # We got a target, but it wasn't a POST request, so the user must have gone
328 # to a link like [[Special:Block/User]]. We don't need to show the checkbox
329 # as long as they go ahead and block *that* user
330 $fields['Confirm']['default'] = 1;
331 }
332
333 if ( $block->mExpiry == 'infinity' ) {
334 $fields['Expiry']['default'] = 'infinite';
335 } else {
336 $fields['Expiry']['default'] = wfTimestamp( TS_RFC2822, $block->mExpiry );
337 }
338
339 $this->alreadyBlocked = true;
340 $this->preErrors[] = [ 'ipb-needreblock', wfEscapeWikiText( (string)$block->getTarget() ) ];
341 }
342
343 # We always need confirmation to do HideUser
344 if ( $this->requestedHideUser ) {
345 $fields['Confirm']['type'] = 'check';
346 unset( $fields['Confirm']['default'] );
347 $this->preErrors[] = [ 'ipb-confirmhideuser', 'ipb-confirmaction' ];
348 }
349
350 # Or if the user is trying to block themselves
351 if ( (string)$this->target === $this->getUser()->getName() ) {
352 $fields['Confirm']['type'] = 'check';
353 unset( $fields['Confirm']['default'] );
354 $this->preErrors[] = [ 'ipb-blockingself', 'ipb-confirmaction' ];
355 }
356
357 if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) {
358 if ( $block instanceof Block && !$block->isSitewide() ) {
359 $fields['EditingRestriction']['default'] = 'partial';
360 } else {
361 $fields['EditingRestriction']['default'] = 'sitewide';
362 }
363
364 if ( $block instanceof Block ) {
365 $pageRestrictions = [];
366 foreach ( $block->getRestrictions() as $restriction ) {
367 if ( $restriction->getType() !== 'page' ) {
368 continue;
369 }
370
371 $pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
372 }
373
374 // Sort the restrictions so they are in alphabetical order.
375 sort( $pageRestrictions );
376 $fields['PageRestrictions']['default'] = implode( "\n", $pageRestrictions );
377 }
378 }
379 }
380
381 /**
382 * Add header elements like block log entries, etc.
383 * @return string
384 */
385 protected function preText() {
386 $this->getOutput()->addModules( [ 'mediawiki.special.block' ] );
387
388 $blockCIDRLimit = $this->getConfig()->get( 'BlockCIDRLimit' );
389 $text = $this->msg( 'blockiptext', $blockCIDRLimit['IPv4'], $blockCIDRLimit['IPv6'] )->parse();
390
391 $otherBlockMessages = [];
392 if ( $this->target !== null ) {
393 $targetName = $this->target;
394 if ( $this->target instanceof User ) {
395 $targetName = $this->target->getName();
396 }
397 # Get other blocks, i.e. from GlobalBlocking or TorBlock extension
398 Hooks::run( 'OtherBlockLogLink', [ &$otherBlockMessages, $targetName ] );
399
400 if ( count( $otherBlockMessages ) ) {
401 $s = Html::rawElement(
402 'h2',
403 [],
404 $this->msg( 'ipb-otherblocks-header', count( $otherBlockMessages ) )->parse()
405 ) . "\n";
406
407 $list = '';
408
409 foreach ( $otherBlockMessages as $link ) {
410 $list .= Html::rawElement( 'li', [], $link ) . "\n";
411 }
412
413 $s .= Html::rawElement(
414 'ul',
415 [ 'class' => 'mw-blockip-alreadyblocked' ],
416 $list
417 ) . "\n";
418
419 $text .= $s;
420 }
421 }
422
423 return $text;
424 }
425
426 /**
427 * Add footer elements to the form
428 * @return string
429 */
430 protected function postText() {
431 $links = [];
432
433 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
434
435 $linkRenderer = $this->getLinkRenderer();
436 # Link to the user's contributions, if applicable
437 if ( $this->target instanceof User ) {
438 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() );
439 $links[] = $linkRenderer->makeLink(
440 $contribsPage,
441 $this->msg( 'ipb-blocklist-contribs', $this->target->getName() )->text()
442 );
443 }
444
445 # Link to unblock the specified user, or to a blank unblock form
446 if ( $this->target instanceof User ) {
447 $message = $this->msg(
448 'ipb-unblock-addr',
449 wfEscapeWikiText( $this->target->getName() )
450 )->parse();
451 $list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() );
452 } else {
453 $message = $this->msg( 'ipb-unblock' )->parse();
454 $list = SpecialPage::getTitleFor( 'Unblock' );
455 }
456 $links[] = $linkRenderer->makeKnownLink(
457 $list,
458 new HtmlArmor( $message )
459 );
460
461 # Link to the block list
462 $links[] = $linkRenderer->makeKnownLink(
463 SpecialPage::getTitleFor( 'BlockList' ),
464 $this->msg( 'ipb-blocklist' )->text()
465 );
466
467 $user = $this->getUser();
468
469 # Link to edit the block dropdown reasons, if applicable
470 if ( $user->isAllowed( 'editinterface' ) ) {
471 $links[] = $linkRenderer->makeKnownLink(
472 $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(),
473 $this->msg( 'ipb-edit-dropdown' )->text(),
474 [],
475 [ 'action' => 'edit' ]
476 );
477 }
478
479 $text = Html::rawElement(
480 'p',
481 [ 'class' => 'mw-ipb-conveniencelinks' ],
482 $this->getLanguage()->pipeList( $links )
483 );
484
485 $userTitle = self::getTargetUserTitle( $this->target );
486 if ( $userTitle ) {
487 # Get relevant extracts from the block and suppression logs, if possible
488 $out = '';
489
490 LogEventsList::showLogExtract(
491 $out,
492 'block',
493 $userTitle,
494 '',
495 [
496 'lim' => 10,
497 'msgKey' => [ 'blocklog-showlog', $userTitle->getText() ],
498 'showIfEmpty' => false
499 ]
500 );
501 $text .= $out;
502
503 # Add suppression block entries if allowed
504 if ( $user->isAllowed( 'suppressionlog' ) ) {
505 LogEventsList::showLogExtract(
506 $out,
507 'suppress',
508 $userTitle,
509 '',
510 [
511 'lim' => 10,
512 'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ],
513 'msgKey' => [ 'blocklog-showsuppresslog', $userTitle->getText() ],
514 'showIfEmpty' => false
515 ]
516 );
517
518 $text .= $out;
519 }
520 }
521
522 return $text;
523 }
524
525 /**
526 * Get a user page target for things like logs.
527 * This handles account and IP range targets.
528 * @param User|string $target
529 * @return Title|null
530 */
531 protected static function getTargetUserTitle( $target ) {
532 if ( $target instanceof User ) {
533 return $target->getUserPage();
534 } elseif ( IP::isIPAddress( $target ) ) {
535 return Title::makeTitleSafe( NS_USER, $target );
536 }
537
538 return null;
539 }
540
541 /**
542 * Determine the target of the block, and the type of target
543 * @todo Should be in Block.php?
544 * @param string $par Subpage parameter passed to setup, or data value from
545 * the HTMLForm
546 * @param WebRequest|null $request Optionally try and get data from a request too
547 * @return array [ User|string|null, Block::TYPE_ constant|null ]
548 */
549 public static function getTargetAndType( $par, WebRequest $request = null ) {
550 $i = 0;
551 $target = null;
552
553 while ( true ) {
554 switch ( $i++ ) {
555 case 0:
556 # The HTMLForm will check wpTarget first and only if it doesn't get
557 # a value use the default, which will be generated from the options
558 # below; so this has to have a higher precedence here than $par, or
559 # we could end up with different values in $this->target and the HTMLForm!
560 if ( $request instanceof WebRequest ) {
561 $target = $request->getText( 'wpTarget', null );
562 }
563 break;
564 case 1:
565 $target = $par;
566 break;
567 case 2:
568 if ( $request instanceof WebRequest ) {
569 $target = $request->getText( 'ip', null );
570 }
571 break;
572 case 3:
573 # B/C @since 1.18
574 if ( $request instanceof WebRequest ) {
575 $target = $request->getText( 'wpBlockAddress', null );
576 }
577 break;
578 case 4:
579 break 2;
580 }
581
582 list( $target, $type ) = Block::parseTarget( $target );
583
584 if ( $type !== null ) {
585 return [ $target, $type ];
586 }
587 }
588
589 return [ null, null ];
590 }
591
592 /**
593 * HTMLForm field validation-callback for Target field.
594 * @since 1.18
595 * @param string $value
596 * @param array $alldata
597 * @param HTMLForm $form
598 * @return Message
599 */
600 public static function validateTargetField( $value, $alldata, $form ) {
601 $status = self::validateTarget( $value, $form->getUser() );
602 if ( !$status->isOK() ) {
603 $errors = $status->getErrorsArray();
604
605 return $form->msg( ...$errors[0] );
606 } else {
607 return true;
608 }
609 }
610
611 /**
612 * Validate a block target.
613 *
614 * @since 1.21
615 * @param string $value Block target to check
616 * @param User $user Performer of the block
617 * @return Status
618 */
619 public static function validateTarget( $value, User $user ) {
620 global $wgBlockCIDRLimit;
621
622 /** @var User $target */
623 list( $target, $type ) = self::getTargetAndType( $value );
624 $status = Status::newGood( $target );
625
626 if ( $type == Block::TYPE_USER ) {
627 if ( $target->isAnon() ) {
628 $status->fatal(
629 'nosuchusershort',
630 wfEscapeWikiText( $target->getName() )
631 );
632 }
633
634 $unblockStatus = self::checkUnblockSelf( $target, $user );
635 if ( $unblockStatus !== true ) {
636 $status->fatal( 'badaccess', $unblockStatus );
637 }
638 } elseif ( $type == Block::TYPE_RANGE ) {
639 list( $ip, $range ) = explode( '/', $target, 2 );
640
641 if (
642 ( IP::isIPv4( $ip ) && $wgBlockCIDRLimit['IPv4'] == 32 ) ||
643 ( IP::isIPv6( $ip ) && $wgBlockCIDRLimit['IPv6'] == 128 )
644 ) {
645 // Range block effectively disabled
646 $status->fatal( 'range_block_disabled' );
647 }
648
649 if (
650 ( IP::isIPv4( $ip ) && $range > 32 ) ||
651 ( IP::isIPv6( $ip ) && $range > 128 )
652 ) {
653 // Dodgy range
654 $status->fatal( 'ip_range_invalid' );
655 }
656
657 if ( IP::isIPv4( $ip ) && $range < $wgBlockCIDRLimit['IPv4'] ) {
658 $status->fatal( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv4'] );
659 }
660
661 if ( IP::isIPv6( $ip ) && $range < $wgBlockCIDRLimit['IPv6'] ) {
662 $status->fatal( 'ip_range_toolarge', $wgBlockCIDRLimit['IPv6'] );
663 }
664 } elseif ( $type == Block::TYPE_IP ) {
665 # All is well
666 } else {
667 $status->fatal( 'badipaddress' );
668 }
669
670 return $status;
671 }
672
673 /**
674 * Given the form data, actually implement a block. This is also called from ApiBlock.
675 *
676 * @param array $data
677 * @param IContextSource $context
678 * @return bool|string
679 */
680 public static function processForm( array $data, IContextSource $context ) {
681 global $wgBlockAllowsUTEdit, $wgHideUserContribLimit;
682
683 $performer = $context->getUser();
684 $enablePartialBlocks = $context->getConfig()->get( 'EnablePartialBlocks' );
685
686 // Handled by field validator callback
687 // self::validateTargetField( $data['Target'] );
688
689 # This might have been a hidden field or a checkbox, so interesting data
690 # can come from it
691 $data['Confirm'] = !in_array( $data['Confirm'], [ '', '0', null, false ], true );
692
693 /** @var User $target */
694 list( $target, $type ) = self::getTargetAndType( $data['Target'] );
695 if ( $type == Block::TYPE_USER ) {
696 $user = $target;
697 $target = $user->getName();
698 $userId = $user->getId();
699
700 # Give admins a heads-up before they go and block themselves. Much messier
701 # to do this for IPs, but it's pretty unlikely they'd ever get the 'block'
702 # permission anyway, although the code does allow for it.
703 # Note: Important to use $target instead of $data['Target']
704 # since both $data['PreviousTarget'] and $target are normalized
705 # but $data['target'] gets overridden by (non-normalized) request variable
706 # from previous request.
707 if ( $target === $performer->getName() &&
708 ( $data['PreviousTarget'] !== $target || !$data['Confirm'] )
709 ) {
710 return [ 'ipb-blockingself', 'ipb-confirmaction' ];
711 }
712 } elseif ( $type == Block::TYPE_RANGE ) {
713 $user = null;
714 $userId = 0;
715 } elseif ( $type == Block::TYPE_IP ) {
716 $user = null;
717 $target = $target->getName();
718 $userId = 0;
719 } else {
720 # This should have been caught in the form field validation
721 return [ 'badipaddress' ];
722 }
723
724 $expiryTime = self::parseExpiryInput( $data['Expiry'] );
725
726 if (
727 // an expiry time is needed
728 ( strlen( $data['Expiry'] ) == 0 ) ||
729 // can't be a larger string as 50 (it should be a time format in any way)
730 ( strlen( $data['Expiry'] ) > 50 ) ||
731 // check, if the time could be parsed
732 !$expiryTime
733 ) {
734 return [ 'ipb_expiry_invalid' ];
735 }
736
737 // an expiry time should be in the future, not in the
738 // past (wouldn't make any sense) - bug T123069
739 if ( $expiryTime < wfTimestampNow() ) {
740 return [ 'ipb_expiry_old' ];
741 }
742
743 if ( !isset( $data['DisableEmail'] ) ) {
744 $data['DisableEmail'] = false;
745 }
746
747 # If the user has done the form 'properly', they won't even have been given the
748 # option to suppress-block unless they have the 'hideuser' permission
749 if ( !isset( $data['HideUser'] ) ) {
750 $data['HideUser'] = false;
751 }
752
753 if ( $data['HideUser'] ) {
754 if ( !$performer->isAllowed( 'hideuser' ) ) {
755 # this codepath is unreachable except by a malicious user spoofing forms,
756 # or by race conditions (user has hideuser and block rights, loads block form,
757 # and loses hideuser rights before submission); so need to fail completely
758 # rather than just silently disable hiding
759 return [ 'badaccess-group0' ];
760 }
761
762 # Recheck params here...
763 if ( $type != Block::TYPE_USER ) {
764 $data['HideUser'] = false; # IP users should not be hidden
765 } elseif ( !wfIsInfinity( $data['Expiry'] ) ) {
766 # Bad expiry.
767 return [ 'ipb_expiry_temp' ];
768 } elseif ( $wgHideUserContribLimit !== false
769 && $user->getEditCount() > $wgHideUserContribLimit
770 ) {
771 # Typically, the user should have a handful of edits.
772 # Disallow hiding users with many edits for performance.
773 return [ [ 'ipb_hide_invalid',
774 Message::numParam( $wgHideUserContribLimit ) ] ];
775 } elseif ( !$data['Confirm'] ) {
776 return [ 'ipb-confirmhideuser', 'ipb-confirmaction' ];
777 }
778 }
779
780 # Create block object.
781 $block = new Block();
782 $block->setTarget( $target );
783 $block->setBlocker( $performer );
784 $block->mReason = $data['Reason'][0];
785 $block->mExpiry = $expiryTime;
786 $block->prevents( 'createaccount', $data['CreateAccount'] );
787 $block->prevents( 'editownusertalk', ( !$wgBlockAllowsUTEdit || $data['DisableUTEdit'] ) );
788 $block->prevents( 'sendemail', $data['DisableEmail'] );
789 $block->isHardblock( $data['HardBlock'] );
790 $block->isAutoblocking( $data['AutoBlock'] );
791 $block->mHideName = $data['HideUser'];
792
793 if (
794 $enablePartialBlocks &&
795 isset( $data['EditingRestriction'] ) &&
796 $data['EditingRestriction'] === 'partial'
797 ) {
798 $block->isSitewide( false );
799 }
800
801 $reason = [ 'hookaborted' ];
802 if ( !Hooks::run( 'BlockIp', [ &$block, &$performer, &$reason ] ) ) {
803 return $reason;
804 }
805
806 $restrictions = [];
807 if ( $enablePartialBlocks ) {
808 if ( !empty( $data['PageRestrictions'] ) ) {
809 $restrictions = array_map( function ( $text ) {
810 $title = Title::newFromText( $text );
811 // Use the link cache since the title has already been loaded when
812 // the field was validated.
813 $restriction = new PageRestriction( 0, $title->getArticleId() );
814 $restriction->setTitle( $title );
815 return $restriction;
816 }, explode( "\n", $data['PageRestrictions'] ) );
817 }
818
819 $block->setRestrictions( $restrictions );
820 }
821
822 $priorBlock = null;
823 # Try to insert block. Is there a conflicting block?
824 $status = $block->insert();
825 if ( !$status ) {
826 # Indicates whether the user is confirming the block and is aware of
827 # the conflict (did not change the block target in the meantime)
828 $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
829 && $data['PreviousTarget'] !== $target );
830
831 # Special case for API - T34434
832 $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] );
833
834 # Show form unless the user is already aware of this...
835 if ( $blockNotConfirmed || $reblockNotAllowed ) {
836 return [ [ 'ipb_already_blocked', $block->getTarget() ] ];
837 # Otherwise, try to update the block...
838 } else {
839 # This returns direct blocks before autoblocks/rangeblocks, since we should
840 # be sure the user is blocked by now it should work for our purposes
841 $currentBlock = Block::newFromTarget( $target );
842 if ( $block->equals( $currentBlock ) ) {
843 return [ [ 'ipb_already_blocked', $block->getTarget() ] ];
844 }
845 # If the name was hidden and the blocking user cannot hide
846 # names, then don't allow any block changes...
847 if ( $currentBlock->mHideName && !$performer->isAllowed( 'hideuser' ) ) {
848 return [ 'cant-see-hidden-user' ];
849 }
850
851 $priorBlock = clone $currentBlock;
852 $currentBlock->isHardblock( $block->isHardblock() );
853 $currentBlock->prevents( 'createaccount', $block->prevents( 'createaccount' ) );
854 $currentBlock->mExpiry = $block->mExpiry;
855 $currentBlock->isAutoblocking( $block->isAutoblocking() );
856 $currentBlock->mHideName = $block->mHideName;
857 $currentBlock->prevents( 'sendemail', $block->prevents( 'sendemail' ) );
858 $currentBlock->prevents( 'editownusertalk', $block->prevents( 'editownusertalk' ) );
859 $currentBlock->mReason = $block->mReason;
860
861 if ( $enablePartialBlocks ) {
862 // Maintain the sitewide status. If partial blocks is not enabled,
863 // saving the block will result in a sitewide block.
864 $currentBlock->isSitewide( $block->isSitewide() );
865
866 // Set the block id of the restrictions.
867 $currentBlock->setRestrictions(
868 BlockRestriction::setBlockId( $currentBlock->getId(), $restrictions )
869 );
870 }
871
872 $status = $currentBlock->update();
873
874 $logaction = 'reblock';
875
876 # Unset _deleted fields if requested
877 if ( $currentBlock->mHideName && !$data['HideUser'] ) {
878 RevisionDeleteUser::unsuppressUserName( $target, $userId );
879 }
880
881 # If hiding/unhiding a name, this should go in the private logs
882 if ( (bool)$currentBlock->mHideName ) {
883 $data['HideUser'] = true;
884 }
885 }
886 } else {
887 $logaction = 'block';
888 }
889
890 Hooks::run( 'BlockIpComplete', [ $block, $performer, $priorBlock ] );
891
892 # Set *_deleted fields if requested
893 if ( $data['HideUser'] ) {
894 RevisionDeleteUser::suppressUserName( $target, $userId );
895 }
896
897 # Can't watch a rangeblock
898 if ( $type != Block::TYPE_RANGE && $data['Watch'] ) {
899 WatchAction::doWatch(
900 Title::makeTitle( NS_USER, $target ),
901 $performer,
902 User::IGNORE_USER_RIGHTS
903 );
904 }
905
906 # Block constructor sanitizes certain block options on insert
907 $data['BlockEmail'] = $block->prevents( 'sendemail' );
908 $data['AutoBlock'] = $block->isAutoblocking();
909
910 # Prepare log parameters
911 $logParams = [];
912 $logParams['5::duration'] = $data['Expiry'];
913 $logParams['6::flags'] = self::blockLogFlags( $data, $type );
914 $logParams['sitewide'] = $block->isSitewide();
915
916 if ( $enablePartialBlocks && !empty( $data['PageRestrictions'] ) ) {
917 $logParams['7::restrictions'] = [
918 'pages' => explode( "\n", $data['PageRestrictions'] ),
919 ];
920 }
921
922 # Make log entry, if the name is hidden, put it in the suppression log
923 $log_type = $data['HideUser'] ? 'suppress' : 'block';
924 $logEntry = new ManualLogEntry( $log_type, $logaction );
925 $logEntry->setTarget( Title::makeTitle( NS_USER, $target ) );
926 $logEntry->setComment( $data['Reason'][0] );
927 $logEntry->setPerformer( $performer );
928 $logEntry->setParameters( $logParams );
929 # Relate log ID to block IDs (T27763)
930 $blockIds = array_merge( [ $status['id'] ], $status['autoIds'] );
931 $logEntry->setRelations( [ 'ipb_id' => $blockIds ] );
932 $logId = $logEntry->insert();
933
934 if ( !empty( $data['Tags'] ) ) {
935 $logEntry->setTags( $data['Tags'] );
936 }
937
938 $logEntry->publish( $logId );
939
940 return true;
941 }
942
943 /**
944 * Get an array of suggested block durations from MediaWiki:Ipboptions
945 * @todo FIXME: This uses a rather odd syntax for the options, should it be converted
946 * to the standard "**<duration>|<displayname>" format?
947 * @param Language|null $lang The language to get the durations in, or null to use
948 * the wiki's content language
949 * @param bool $includeOther Whether to include the 'other' option in the list of
950 * suggestions
951 * @return array
952 */
953 public static function getSuggestedDurations( Language $lang = null, $includeOther = true ) {
954 $a = [];
955 $msg = $lang === null
956 ? wfMessage( 'ipboptions' )->inContentLanguage()->text()
957 : wfMessage( 'ipboptions' )->inLanguage( $lang )->text();
958
959 if ( $msg == '-' ) {
960 return [];
961 }
962
963 foreach ( explode( ',', $msg ) as $option ) {
964 if ( strpos( $option, ':' ) === false ) {
965 $option = "$option:$option";
966 }
967
968 list( $show, $value ) = explode( ':', $option );
969 $a[$show] = $value;
970 }
971
972 if ( $a && $includeOther ) {
973 // if options exist, add other to the end instead of the begining (which
974 // is what happens by default).
975 $a[ wfMessage( 'ipbother' )->text() ] = 'other';
976 }
977
978 return $a;
979 }
980
981 /**
982 * Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute
983 * ("24 May 2034", etc), into an absolute timestamp we can put into the database.
984 *
985 * @todo strtotime() only accepts English strings. This means the expiry input
986 * can only be specified in English.
987 * @see https://secure.php.net/manual/en/function.strtotime.php
988 *
989 * @param string $expiry Whatever was typed into the form
990 * @return string|bool Timestamp or 'infinity' or false on error.
991 */
992 public static function parseExpiryInput( $expiry ) {
993 if ( wfIsInfinity( $expiry ) ) {
994 return 'infinity';
995 }
996
997 $expiry = strtotime( $expiry );
998
999 if ( $expiry < 0 || $expiry === false ) {
1000 return false;
1001 }
1002
1003 return wfTimestamp( TS_MW, $expiry );
1004 }
1005
1006 /**
1007 * Can we do an email block?
1008 * @param User $user The sysop wanting to make a block
1009 * @return bool
1010 */
1011 public static function canBlockEmail( $user ) {
1012 global $wgEnableUserEmail, $wgSysopEmailBans;
1013
1014 return ( $wgEnableUserEmail && $wgSysopEmailBans && $user->isAllowed( 'blockemail' ) );
1015 }
1016
1017 /**
1018 * T17810: blocked admins should not be able to block/unblock
1019 * others, and probably shouldn't be able to unblock themselves
1020 * either.
1021 * @param User|int|string $user
1022 * @param User $performer User doing the request
1023 * @return bool|string True or error message key
1024 */
1025 public static function checkUnblockSelf( $user, User $performer ) {
1026 if ( is_int( $user ) ) {
1027 $user = User::newFromId( $user );
1028 } elseif ( is_string( $user ) ) {
1029 $user = User::newFromName( $user );
1030 }
1031
1032 if ( $performer->isBlocked() ) {
1033 if ( $user instanceof User && $user->getId() == $performer->getId() ) {
1034 # User is trying to unblock themselves
1035 if ( $performer->isAllowed( 'unblockself' ) ) {
1036 return true;
1037 # User blocked themselves and is now trying to reverse it
1038 } elseif ( $performer->blockedBy() === $performer->getName() ) {
1039 return true;
1040 } else {
1041 return 'ipbnounblockself';
1042 }
1043 } else {
1044 # User is trying to block/unblock someone else
1045 return 'ipbblocked';
1046 }
1047 } else {
1048 return true;
1049 }
1050 }
1051
1052 /**
1053 * Return a comma-delimited list of "flags" to be passed to the log
1054 * reader for this block, to provide more information in the logs
1055 * @param array $data From HTMLForm data
1056 * @param int $type Block::TYPE_ constant (USER, RANGE, or IP)
1057 * @return string
1058 */
1059 protected static function blockLogFlags( array $data, $type ) {
1060 $config = RequestContext::getMain()->getConfig();
1061
1062 $blockAllowsUTEdit = $config->get( 'BlockAllowsUTEdit' );
1063
1064 $flags = [];
1065
1066 # when blocking a user the option 'anononly' is not available/has no effect
1067 # -> do not write this into log
1068 if ( !$data['HardBlock'] && $type != Block::TYPE_USER ) {
1069 // For grepping: message block-log-flags-anononly
1070 $flags[] = 'anononly';
1071 }
1072
1073 if ( $data['CreateAccount'] ) {
1074 // For grepping: message block-log-flags-nocreate
1075 $flags[] = 'nocreate';
1076 }
1077
1078 # Same as anononly, this is not displayed when blocking an IP address
1079 if ( !$data['AutoBlock'] && $type == Block::TYPE_USER ) {
1080 // For grepping: message block-log-flags-noautoblock
1081 $flags[] = 'noautoblock';
1082 }
1083
1084 if ( $data['DisableEmail'] ) {
1085 // For grepping: message block-log-flags-noemail
1086 $flags[] = 'noemail';
1087 }
1088
1089 if ( $blockAllowsUTEdit && $data['DisableUTEdit'] ) {
1090 // For grepping: message block-log-flags-nousertalk
1091 $flags[] = 'nousertalk';
1092 }
1093
1094 if ( $data['HideUser'] ) {
1095 // For grepping: message block-log-flags-hiddenname
1096 $flags[] = 'hiddenname';
1097 }
1098
1099 return implode( ',', $flags );
1100 }
1101
1102 /**
1103 * Process the form on POST submission.
1104 * @param array $data
1105 * @param HTMLForm|null $form
1106 * @return bool|array True for success, false for didn't-try, array of errors on failure
1107 */
1108 public function onSubmit( array $data, HTMLForm $form = null ) {
1109 return self::processForm( $data, $form->getContext() );
1110 }
1111
1112 /**
1113 * Do something exciting on successful processing of the form, most likely to show a
1114 * confirmation message
1115 */
1116 public function onSuccess() {
1117 $out = $this->getOutput();
1118 $out->setPageTitle( $this->msg( 'blockipsuccesssub' ) );
1119 $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) );
1120 }
1121
1122 /**
1123 * Return an array of subpages beginning with $search that this special page will accept.
1124 *
1125 * @param string $search Prefix to search for
1126 * @param int $limit Maximum number of results to return (usually 10)
1127 * @param int $offset Number of results to skip (usually 0)
1128 * @return string[] Matching subpages
1129 */
1130 public function prefixSearchSubpages( $search, $limit, $offset ) {
1131 $user = User::newFromName( $search );
1132 if ( !$user ) {
1133 // No prefix suggestion for invalid user
1134 return [];
1135 }
1136 // Autocomplete subpage as user list - public to allow caching
1137 return UserNamePrefixSearch::search( 'public', $search, $limit, $offset );
1138 }
1139
1140 protected function getGroupName() {
1141 return 'users';
1142 }
1143 }