MediaWiki 1.43.1
SpecialBlock.php
Go to the documentation of this file.
1<?php
21namespace MediaWiki\Specials;
22
24use HtmlArmor;
58use OOUI\FieldLayout;
59use OOUI\HtmlSnippet;
60use OOUI\LabelWidget;
61use OOUI\Widget;
62use Wikimedia\IPUtils;
64
72
73 private BlockUtils $blockUtils;
74 private BlockPermissionCheckerFactory $blockPermissionCheckerFactory;
75 private BlockUserFactory $blockUserFactory;
76 private DatabaseBlockStore $blockStore;
77 private UserNameUtils $userNameUtils;
78 private UserNamePrefixSearch $userNamePrefixSearch;
79 private BlockActionInfo $blockActionInfo;
80 private TitleFormatter $titleFormatter;
81
85 protected $target;
86
88 protected $type;
89
91 protected $previousTarget;
92
95
97 protected $alreadyBlocked;
98
102 protected $preErrors = [];
103
105 protected bool $useCodex = false;
106
110 protected array $codexFormData = [];
111
112 private NamespaceInfo $namespaceInfo;
113
125 public function __construct(
126 BlockUtils $blockUtils,
127 BlockPermissionCheckerFactory $blockPermissionCheckerFactory,
128 BlockUserFactory $blockUserFactory,
129 DatabaseBlockStore $blockStore,
130 UserNameUtils $userNameUtils,
131 UserNamePrefixSearch $userNamePrefixSearch,
132 BlockActionInfo $blockActionInfo,
133 TitleFormatter $titleFormatter,
134 NamespaceInfo $namespaceInfo
135 ) {
136 parent::__construct( 'Block', 'block' );
137
138 $this->blockUtils = $blockUtils;
139 $this->blockPermissionCheckerFactory = $blockPermissionCheckerFactory;
140 $this->blockUserFactory = $blockUserFactory;
141 $this->blockStore = $blockStore;
142 $this->userNameUtils = $userNameUtils;
143 $this->userNamePrefixSearch = $userNamePrefixSearch;
144 $this->blockActionInfo = $blockActionInfo;
145 $this->titleFormatter = $titleFormatter;
146 $this->namespaceInfo = $namespaceInfo;
147 $this->useCodex = $this->getConfig()->get( MainConfigNames::UseCodexSpecialBlock ) ||
148 $this->getRequest()->getBool( 'usecodex' );
149 }
150
151 public function execute( $par ) {
152 parent::execute( $par );
153
154 if ( $this->useCodex ) {
155 $this->codexFormData[ 'blockTargetUser' ] = $this->target instanceof UserIdentity ?
156 $this->target->getName() :
157 $this->target ?? null;
158 $this->getOutput()->addJsConfigVars( $this->codexFormData );
159 }
160 }
161
162 public function doesWrites() {
163 return true;
164 }
165
172 protected function checkExecutePermissions( User $user ) {
173 parent::checkExecutePermissions( $user );
174 // T17810: blocked admins should have limited access here
175 $status = $this->blockPermissionCheckerFactory
176 ->newBlockPermissionChecker( $this->target, $user )
177 ->checkBlockPermissions();
178 if ( $status !== true ) {
179 throw new ErrorPageError( 'badaccess', $status );
180 }
181 }
182
188 public function requiresUnblock() {
189 return false;
190 }
191
197 protected function setParameter( $par ) {
198 // Extract variables from the request. Try not to get into a situation where we
199 // need to extract *every* variable from the form just for processing here, but
200 // there are legitimate uses for some variables
201 $request = $this->getRequest();
202 [ $this->target, $this->type ] = $this->getTargetAndTypeInternal( $par, $request );
203 if ( $this->target instanceof UserIdentity ) {
204 // Set the 'relevant user' in the skin, so it displays links like Contributions,
205 // User logs, UserRights, etc.
206 $this->getSkin()->setRelevantUser( $this->target );
207 }
208
209 [ $this->previousTarget, /*...*/ ] = $this->blockUtils
210 ->parseBlockTarget( $request->getVal( 'wpPreviousTarget' ) );
211 $this->requestedHideUser = $request->getBool( 'wpHideUser' );
212
213 if ( $this->useCodex ) {
214 // Parse wpExpiry param
215 $givenExpiry = $request->getVal( 'wpExpiry', '' );
216 if ( wfIsInfinity( $givenExpiry ) ) {
217 $this->codexFormData[ 'blockExpiryPreset' ] = 'infinite';
218 } else {
219 $expiry = date_parse( $givenExpiry );
220 $this->codexFormData[ 'blockExpiryPreset' ] = isset( $expiry[ 'relative' ] ) ?
221 // Relative expiry (e.g. '1 week')
222 $givenExpiry :
223 // Absolute expiry, formatted for <input type="datetime-local">
224 $this->formatExpiryForHtml( $request->getVal( 'wpExpiry', '' ) );
225 }
226
227 $this->codexFormData[ 'blockTypePreset' ] =
228 $request->getVal( 'wpEditingRestriction' ) === 'sitewide' ||
229 $request->getVal( 'wpEditingRestriction' ) === 'partial' ?
230 $request->getVal( 'wpEditingRestriction' ) :
231 'sitewide';
232 $this->codexFormData[ 'blockReasonPreset' ] = $request->getVal( 'wpReason' );
233 $this->codexFormData[ 'blockReasonOtherPreset' ] = $request->getVal( 'wpReason-other' );
234 $blockAdditionalDetailsPreset = $blockDetailsPreset = [];
235
236 if ( $request->getBool( 'wpCreateAccount' ) ) {
237 $blockDetailsPreset[] = 'wpCreateAccount';
238 }
239
240 if ( $request->getBool( 'wpDisableEmail' ) ) {
241 $blockDetailsPreset[] = 'wpDisableEmail';
242 }
243
244 if ( $request->getBool( 'wpDisableUTEdit' ) ) {
245 $blockDetailsPreset[] = 'wpDisableUTEdit';
246 }
247
248 if ( $request->getVal( 'wpAutoBlock' ) !== '0' ) {
249 $blockAdditionalDetailsPreset[] = 'wpAutoBlock';
250 }
251
252 if ( $request->getBool( 'wpWatch' ) ) {
253 $blockAdditionalDetailsPreset[] = 'wpWatch';
254 }
255
256 if ( $request->getBool( 'wpHideName' ) ) {
257 $blockAdditionalDetailsPreset[] = 'wpHideName';
258 }
259
260 if ( $request->getBool( 'wpHardBlock' ) ) {
261 $blockAdditionalDetailsPreset[] = 'wpHardBlock';
262 }
263
264 $this->codexFormData[ 'blockDetailsPreset' ] = $blockDetailsPreset;
265 $this->codexFormData[ 'blockAdditionalDetailsPreset' ] = $blockAdditionalDetailsPreset;
266 $this->codexFormData[ 'blockPageRestrictions' ] = $request->getVal( 'wpPageRestrictions' );
267 $this->codexFormData[ 'blockNamespaceRestrictions' ] = $request->getVal( 'wpNamespaceRestrictions' );
268 }
269 }
270
276 protected function alterForm( HTMLForm $form ) {
277 $form->setHeaderHtml( '' );
278 $form->setSubmitDestructive();
279
280 $msg = $this->alreadyBlocked ? 'ipb-change-block' : 'ipbsubmit';
281 $form->setSubmitTextMsg( $msg );
282
283 $this->addHelpLink( 'Help:Blocking users' );
284
285 // Don't need to do anything if the form has been posted
286 if ( !$this->getRequest()->wasPosted() && $this->preErrors ) {
287 // Mimic error messages normally generated by the form
288 $form->addHeaderHtml( (string)new FieldLayout(
289 new Widget( [] ),
290 [
291 'align' => 'top',
292 'errors' => array_map( function ( $errMsg ) {
293 return new HtmlSnippet( $this->msg( $errMsg )->parse() );
294 }, $this->preErrors ),
295 ]
296 ) );
297
298 if ( $this->useCodex ) {
299 $this->codexFormData[ 'blockPreErrors' ] = array_map( function ( $errMsg ) {
300 return $this->msg( $errMsg )->parse();
301 }, $this->preErrors );
302 }
303 }
304 }
305
306 protected function getDisplayFormat() {
307 return $this->useCodex ? 'codex' : 'ooui';
308 }
309
314 protected function getFormFields() {
315 $conf = $this->getConfig();
316 $blockAllowsUTEdit = $conf->get( MainConfigNames::BlockAllowsUTEdit );
317
318 $this->getOutput()->enableOOUI();
319
320 $user = $this->getUser();
321
322 $suggestedDurations = $this->getLanguage()->getBlockDurations();
323
324 $this->codexFormData[ 'blockEnableMultiblocks' ] = $conf->get( MainConfigNames::EnableMultiBlocks ) ||
325 $this->getRequest()->getBool( 'multiblocks' );
326
327 $a = [];
328
329 $a['Target'] = [
330 'type' => 'user',
331 'ipallowed' => true,
332 'iprange' => true,
333 'id' => 'mw-bi-target',
334 'size' => '45',
335 'autofocus' => true,
336 'required' => true,
337 'placeholder' => $this->msg( 'block-target-placeholder' )->text(),
338 'validation-callback' => function ( $value, $alldata, $form ) {
339 $status = $this->blockUtils->validateTarget( $value );
340 if ( !$status->isOK() ) {
341 $errors = $status->getMessages();
342 return $form->msg( $errors[0] );
343 }
344 return true;
345 },
346 'section' => 'target',
347 ];
348
349 $editingRestrictionOptions = $this->useCodex ?
350 // If we're using Codex, use the option-descriptions feature, which is only supported by Codex
351 [
352 'options-messages' => [
353 'ipb-sitewide' => 'sitewide',
354 'ipb-partial' => 'partial'
355 ],
356 'option-descriptions-messages' => [
357 'sitewide' => 'ipb-sitewide-help',
358 'partial' => 'ipb-partial-help'
359 ],
360 'option-descriptions-messages-parse' => true,
361 ] :
362 // Otherwise, if we're using OOUI, add the options' descriptions as part of their labels
363 [
364 'options' => [
365 $this->msg( 'ipb-sitewide' )->escaped() .
366 new LabelWidget( [
367 'classes' => [ 'oo-ui-inline-help' ],
368 'label' => new HtmlSnippet( $this->msg( 'ipb-sitewide-help' )->parse() ),
369 ] ) => 'sitewide',
370 $this->msg( 'ipb-partial' )->escaped() .
371 new LabelWidget( [
372 'classes' => [ 'oo-ui-inline-help' ],
373 'label' => new HtmlSnippet( $this->msg( 'ipb-partial-help' )->parse() ),
374 ] ) => 'partial',
375 ]
376 ];
377
378 $a['EditingRestriction'] = [
379 'type' => 'radio',
380 'cssclass' => 'mw-block-editing-restriction',
381 'default' => 'sitewide',
382 'section' => 'actions',
383 ] + $editingRestrictionOptions;
384
385 $a['PageRestrictions'] = [
386 'type' => 'titlesmultiselect',
387 'label' => $this->msg( 'ipb-pages-label' )->text(),
388 'exists' => true,
389 'max' => 10,
390 'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction',
391 'default' => '',
392 'showMissing' => false,
393 'excludeDynamicNamespaces' => true,
394 'input' => [
395 'autocomplete' => false
396 ],
397 'section' => 'actions',
398 ];
399
400 $a['NamespaceRestrictions'] = [
401 'type' => 'namespacesmultiselect',
402 'label' => $this->msg( 'ipb-namespaces-label' )->text(),
403 'exists' => true,
404 'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction',
405 'default' => '',
406 'input' => [
407 'autocomplete' => false
408 ],
409 'section' => 'actions',
410 ];
411
412 if ( $conf->get( MainConfigNames::EnablePartialActionBlocks ) ) {
413 $blockActions = $this->blockActionInfo->getAllBlockActions();
414 $optionMessages = array_combine(
415 array_map( static function ( $action ) {
416 return "ipb-action-$action";
417 }, array_keys( $blockActions ) ),
418 $blockActions
419 );
420
421 $this->codexFormData[ 'partialBlockActionOptions'] = $optionMessages;
422
423 $a['ActionRestrictions'] = [
424 'type' => 'multiselect',
425 'cssclass' => 'mw-htmlform-checkradio-indent mw-block-partial-restriction mw-block-action-restriction',
426 'options-messages' => $optionMessages,
427 'section' => 'actions',
428 ];
429 }
430
431 $a['CreateAccount'] = [
432 'type' => 'check',
433 'cssclass' => 'mw-block-restriction',
434 'label-message' => 'ipbcreateaccount',
435 'default' => true,
436 'section' => 'details',
437 ];
438
439 if ( $this->blockPermissionCheckerFactory
440 ->newBlockPermissionChecker( null, $user )
441 ->checkEmailPermissions()
442 ) {
443 $a['DisableEmail'] = [
444 'type' => 'check',
445 'cssclass' => 'mw-block-restriction',
446 'label-message' => 'ipbemailban',
447 'section' => 'details',
448 ];
449
450 $this->codexFormData[ 'blockAllowsEmailBan'] = true;
451 }
452
453 if ( $blockAllowsUTEdit ) {
454 $a['DisableUTEdit'] = [
455 'type' => 'check',
456 'cssclass' => 'mw-block-restriction',
457 'label-message' => 'ipb-disableusertalk',
458 'default' => false,
459 'section' => 'details',
460 ];
461
462 $this->codexFormData[ 'blockAllowsUTEdit'] = true;
463 }
464
465 $defaultExpiry = $this->msg( 'ipb-default-expiry' )->inContentLanguage();
466 if ( $this->type === DatabaseBlock::TYPE_RANGE || $this->type === DatabaseBlock::TYPE_IP ) {
467 $defaultExpiryIP = $this->msg( 'ipb-default-expiry-ip' )->inContentLanguage();
468 if ( !$defaultExpiryIP->isDisabled() ) {
469 $defaultExpiry = $defaultExpiryIP;
470 }
471 }
472
473 $a['Expiry'] = [
474 'type' => 'expiry',
475 'required' => true,
476 'options' => $suggestedDurations,
477 'default' => $defaultExpiry->text(),
478 'section' => 'expiry',
479 ];
480 $this->codexFormData[ 'blockExpiryOptions' ] = $suggestedDurations;
481 $this->codexFormData[ 'blockExpiryDefault' ] = $defaultExpiry->text();
482
483 $a['Reason'] = [
484 'type' => 'selectandother',
485 // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP
486 // (e.g. emojis) count for two each. This limit is overridden in JS to instead count
487 // Unicode codepoints.
488 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
489 'maxlength-unit' => 'codepoints',
490 'options-message' => 'ipbreason-dropdown',
491 'section' => 'reason',
492 ];
493
494 if ( $this->useCodex ) {
495 $blockReasonOptions = Html::listDropdownOptionsCodex(
496 Html::listDropdownOptions( $this->msg( 'ipbreason-dropdown' )->plain(),
497 [ 'other' => $this->msg( 'htmlform-selectorother-other' )->text() ]
498 ) );
499 $this->codexFormData[ 'blockReasonOptions' ] = $blockReasonOptions;
500 $this->codexFormData[ 'blockReasonMaxLength' ] = CommentStore::COMMENT_CHARACTER_LIMIT;
501 }
502
503 $a['AutoBlock'] = [
504 'type' => 'check',
505 'label-message' => [
506 'ipbenableautoblock',
508 ],
509 'default' => true,
510 'section' => 'options',
511 ];
512 $this->codexFormData['blockAutoblockExpiry'] = $this->getLanguage()
513 ->formatDuration( $conf->get( MainConfigNames::AutoblockExpiry ) );
514
515 // Allow some users to hide name from block log, blocklist and listusers
516 if ( $this->getAuthority()->isAllowed( 'hideuser' ) ) {
517 $a['HideUser'] = [
518 'type' => 'check',
519 'label-message' => 'ipbhidename',
520 'cssclass' => 'mw-block-hideuser',
521 'section' => 'options',
522 ];
523
524 $this->codexFormData['blockHideUser'] = true;
525 }
526
527 // Watchlist their user page? (Only if user is logged in)
528 if ( $user->isRegistered() ) {
529 $a['Watch'] = [
530 'type' => 'check',
531 'label-message' => 'ipbwatchuser',
532 'section' => 'options',
533 ];
534 }
535
536 $a['HardBlock'] = [
537 'type' => 'check',
538 'label-message' => 'ipb-hardblock',
539 'default' => false,
540 'section' => 'options',
541 ];
542
543 // This is basically a copy of the Target field, but the user can't change it, so we
544 // can see if the warnings we maybe showed to the user before still apply
545 $a['PreviousTarget'] = [
546 'type' => 'hidden',
547 'default' => false,
548 ];
549
550 // We'll turn this into a checkbox if we need to
551 $a['Confirm'] = [
552 'type' => 'hidden',
553 'default' => '',
554 'label-message' => 'ipb-confirm',
555 'cssclass' => 'mw-block-confirm',
556 ];
557
558 $this->maybeAlterFormDefaults( $a );
559
560 // Allow extensions to add more fields
561 $this->getHookRunner()->onSpecialBlockModifyFormFields( $this, $a );
562
563 return $a;
564 }
565
571 protected function maybeAlterFormDefaults( &$fields ) {
572 // This will be overwritten by request data
573 $fields['Target']['default'] = (string)$this->target;
574
575 if ( $this->target ) {
576 $status = $this->blockUtils->validateTarget( $this->target );
577 if ( !$status->isOK() ) {
578 $errors = $status->getMessages( 'error' );
579 $this->preErrors = array_merge( $this->preErrors, $errors );
580 }
581 }
582
583 // This won't be
584 $fields['PreviousTarget']['default'] = (string)$this->target;
585
586 $block = $this->blockStore->newFromTarget( $this->target );
587
588 // Populate fields if there is a block that is not an autoblock; if it is a range
589 // block, only populate the fields if the range is the same as $this->target
590 if ( $block instanceof DatabaseBlock && $block->getType() !== DatabaseBlock::TYPE_AUTO
591 && ( $this->type != DatabaseBlock::TYPE_RANGE
592 || ( $this->target && $block->isBlocking( $this->target ) ) )
593 ) {
594 $fields['HardBlock']['default'] = $block->isHardblock();
595 $fields['CreateAccount']['default'] = $block->isCreateAccountBlocked();
596 $fields['AutoBlock']['default'] = $block->isAutoblocking();
597
598 if ( isset( $fields['DisableEmail'] ) ) {
599 $fields['DisableEmail']['default'] = $block->isEmailBlocked();
600 }
601
602 if ( isset( $fields['HideUser'] ) ) {
603 $fields['HideUser']['default'] = $block->getHideName();
604 }
605
606 if ( isset( $fields['DisableUTEdit'] ) ) {
607 $fields['DisableUTEdit']['default'] = !$block->isUsertalkEditAllowed();
608 }
609
610 // If the username was hidden (bl_deleted == 1), don't show the reason
611 // unless this user also has rights to hideuser: T37839
612 if ( !$block->getHideName() || $this->getAuthority()->isAllowed( 'hideuser' ) ) {
613 $fields['Reason']['default'] = $block->getReasonComment()->text;
614 } else {
615 $fields['Reason']['default'] = '';
616 }
617
618 if ( $this->getRequest()->wasPosted() ) {
619 // Ok, so we got a POST submission asking us to reblock a user. So show the
620 // confirm checkbox; the user will only see it if they haven't previously
621 $fields['Confirm']['type'] = 'check';
622 } else {
623 // We got a target, but it wasn't a POST request, so the user must have gone
624 // to a link like [[Special:Block/User]]. We don't need to show the checkbox
625 // as long as they go ahead and block *that* user
626 $fields['Confirm']['default'] = 1;
627 }
628
629 if ( $block->getExpiry() == 'infinity' ) {
630 $fields['Expiry']['default'] = $this->codexFormData[ 'blockExpiryDefault' ] = 'infinite';
631 } else {
632 $fields['Expiry']['default'] = wfTimestamp( TS_RFC2822, $block->getExpiry() );
633
634 // Don't overwrite if expiry was specified in the URL
635 if ( !isset( $this->codexFormData[ 'blockExpiryPreset' ] ) ) {
636 $this->codexFormData[ 'blockExpiryPreset' ] = $this->formatExpiryForHtml( $block->getExpiry() );
637 }
638 }
639
640 if ( !$block->isSitewide() ) {
641 $fields['EditingRestriction']['default'] =
642 $this->codexFormData[ 'blockTypePreset' ] = 'partial';
643
644 $pageRestrictions = [];
645 $namespaceRestrictions = [];
646 foreach ( $block->getRestrictions() as $restriction ) {
647 if ( $restriction instanceof PageRestriction && $restriction->getTitle() ) {
648 $pageRestrictions[] = $restriction->getTitle()->getPrefixedText();
649 } elseif ( $restriction instanceof NamespaceRestriction &&
650 $this->namespaceInfo->exists( $restriction->getValue() )
651 ) {
652 $namespaceRestrictions[] = $restriction->getValue();
653 }
654 }
655
656 // Sort the restrictions so they are in alphabetical order.
657 sort( $pageRestrictions );
658 $fields['PageRestrictions']['default'] =
659 $this->codexFormData[ 'blockPageRestrictions' ] = implode( "\n", $pageRestrictions );
660 sort( $namespaceRestrictions );
661 $fields['NamespaceRestrictions']['default'] =
662 $this->codexFormData[ 'blockNamespaceRestrictions' ] = implode( "\n", $namespaceRestrictions );
663
665 $actionRestrictions = [];
666 foreach ( $block->getRestrictions() as $restriction ) {
667 if ( $restriction instanceof ActionRestriction ) {
668 $actionRestrictions[] = $restriction->getValue();
669 }
670 }
671 $fields['ActionRestrictions']['default'] = $actionRestrictions;
672 }
673 }
674
675 $this->alreadyBlocked = true;
676 $this->codexFormData[ 'blockAlreadyBlocked' ] = $this->alreadyBlocked;
677 $this->preErrors[] = $this->msg( 'ipb-needreblock', wfEscapeWikiText( $block->getTargetName() ) );
678 }
679
680 if ( $this->alreadyBlocked || $this->getRequest()->wasPosted()
681 || $this->getRequest()->getCheck( 'wpCreateAccount' )
682 ) {
683 $this->getOutput()->addJsConfigVars( 'wgCreateAccountDirty', true );
684 }
685
686 // We always need confirmation to do HideUser
687 if ( $this->requestedHideUser ) {
688 $fields['Confirm']['type'] = 'check';
689 unset( $fields['Confirm']['default'] );
690 $this->preErrors[] = $this->msg( 'ipb-confirmhideuser', 'ipb-confirmaction' );
691 }
692
693 // Or if the user is trying to block themselves
694 if ( (string)$this->target === $this->getUser()->getName() ) {
695 $fields['Confirm']['type'] = 'check';
696 unset( $fields['Confirm']['default'] );
697 $this->preErrors[] = $this->msg( 'ipb-blockingself', 'ipb-confirmaction' );
698 }
699 }
700
707 private function formatExpiryForHtml( string $expiry ): string {
708 if ( preg_match( '/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/', $expiry ) === 1 ) {
709 // YYYY-MM-DDTHH:mm which is accepted by <input type="datetime-local">, but not by MediaWiki.
710 return substr( $expiry, 0, 16 );
711 } elseif ( $expiry === '' ) {
712 // No expiry specified
713 return '';
714 }
715 return substr( wfTimestamp( TS_ISO_8601, $expiry ), 0, 16 );
716 }
717
722 protected function preHtml() {
723 $this->getOutput()->addModuleStyles( [ 'mediawiki.special' ] );
724 if ( $this->useCodex ) {
725 $this->getOutput()->addModules( [ 'mediawiki.special.block.codex' ] );
726 } else {
727 $this->getOutput()->addModules( [ 'mediawiki.special.block' ] );
728 }
729
730 $blockCIDRLimit = $this->getConfig()->get( MainConfigNames::BlockCIDRLimit );
731 $text = $this->msg( 'blockiptext', $blockCIDRLimit['IPv4'], $blockCIDRLimit['IPv6'] )->parse();
732
733 $otherBlockMessages = [];
734 if ( $this->target !== null ) {
735 $targetName = $this->target;
736 if ( $this->target instanceof UserIdentity ) {
737 $targetName = $this->target->getName();
738 }
739 // Get other blocks, i.e. from GlobalBlocking or TorBlock extension
740 $this->getHookRunner()->onOtherBlockLogLink(
741 $otherBlockMessages, $targetName );
742
743 if ( count( $otherBlockMessages ) ) {
744 $s = Html::rawElement(
745 'h2',
746 [],
747 $this->msg( 'ipb-otherblocks-header', count( $otherBlockMessages ) )->parse()
748 ) . "\n";
749
750 $list = '';
751
752 foreach ( $otherBlockMessages as $link ) {
753 $list .= Html::rawElement( 'li', [], $link ) . "\n";
754 }
755
756 $s .= Html::rawElement(
757 'ul',
758 [ 'class' => 'mw-blockip-alreadyblocked' ],
759 $list
760 ) . "\n";
761
762 $text .= $s;
763 }
764 }
765
766 return $text;
767 }
768
773 protected function postHtml() {
774 $links = [];
775
776 $this->getOutput()->addModuleStyles( 'mediawiki.special' );
777
778 $linkRenderer = $this->getLinkRenderer();
779 // Link to the user's contributions, if applicable
780 if ( $this->target instanceof UserIdentity ) {
781 $contribsPage = SpecialPage::getTitleFor( 'Contributions', $this->target->getName() );
782 $links[] = $linkRenderer->makeLink(
783 $contribsPage,
784 $this->msg( 'ipb-blocklist-contribs', $this->target->getName() )->text()
785 );
786 }
787
788 // Link to unblock the specified user, or to a blank unblock form
789 if ( $this->target instanceof UserIdentity ) {
790 $message = $this->msg(
791 'ipb-unblock-addr',
792 wfEscapeWikiText( $this->target->getName() )
793 )->parse();
794 $list = SpecialPage::getTitleFor( 'Unblock', $this->target->getName() );
795 } else {
796 $message = $this->msg( 'ipb-unblock' )->parse();
797 $list = SpecialPage::getTitleFor( 'Unblock' );
798 }
799 $links[] = $linkRenderer->makeKnownLink(
800 $list,
801 new HtmlArmor( $message )
802 );
803
804 // Link to the block list
805 $links[] = $linkRenderer->makeKnownLink(
806 SpecialPage::getTitleFor( 'BlockList' ),
807 $this->msg( 'ipb-blocklist' )->text()
808 );
809
810 // Link to edit the block dropdown reasons, if applicable
811 if ( $this->getAuthority()->isAllowed( 'editinterface' ) ) {
812 $links[] = $linkRenderer->makeKnownLink(
813 $this->msg( 'ipbreason-dropdown' )->inContentLanguage()->getTitle(),
814 $this->msg( 'ipb-edit-dropdown' )->text(),
815 [],
816 [ 'action' => 'edit' ]
817 );
818 }
819
820 $text = Html::rawElement(
821 'p',
822 [ 'class' => 'mw-ipb-conveniencelinks' ],
823 $this->getLanguage()->pipeList( $links )
824 );
825
826 $userPage = self::getTargetUserTitle( $this->target );
827 if ( $userPage ) {
828 // Get relevant extracts from the block and suppression logs, if possible
829 $out = '';
830
831 LogEventsList::showLogExtract(
832 $out,
833 'block',
834 $userPage,
835 '',
836 [
837 'lim' => 10,
838 'msgKey' => [
839 'blocklog-showlog',
840 $this->titleFormatter->getText( $userPage ),
841 ],
842 'showIfEmpty' => false
843 ]
844 );
845 $text .= $out;
846
847 // Add suppression block entries if allowed
848 if ( $this->getAuthority()->isAllowed( 'suppressionlog' ) ) {
849 LogEventsList::showLogExtract(
850 $out,
851 'suppress',
852 $userPage,
853 '',
854 [
855 'lim' => 10,
856 'conds' => [ 'log_action' => [ 'block', 'reblock', 'unblock' ] ],
857 'msgKey' => [
858 'blocklog-showsuppresslog',
859 $this->titleFormatter->getText( $userPage ),
860 ],
861 'showIfEmpty' => false
862 ]
863 );
864
865 $text .= $out;
866 }
867 }
868
869 return $text;
870 }
871
878 protected static function getTargetUserTitle( $target ): ?PageReference {
879 if ( $target instanceof UserIdentity ) {
880 return PageReferenceValue::localReference( NS_USER, $target->getName() );
881 }
882
883 if ( is_string( $target ) && IPUtils::isIPAddress( $target ) ) {
884 return PageReferenceValue::localReference( NS_USER, $target );
885 }
886
887 return null;
888 }
889
901 private function getTargetAndTypeInternal( ?string $par, WebRequest $request ) {
902 $possibleTargets = [
903 $request->getVal( 'wpTarget', null ),
904 $par,
905 $request->getVal( 'ip', null ),
906 // B/C @since 1.18
907 $request->getVal( 'wpBlockAddress', null ),
908 ];
909 foreach ( $possibleTargets as $possibleTarget ) {
910 $targetAndType = $this->blockUtils
911 ->parseBlockTarget( $possibleTarget );
912 // If type is not null then target is valid
913 if ( $targetAndType[ 1 ] !== null ) {
914 break;
915 }
916 }
917 return $targetAndType;
918 }
919
929 public static function processForm( array $data, IContextSource $context ) {
930 wfDeprecated( __METHOD__, '1.36' );
931 $services = MediaWikiServices::getInstance();
932 return self::processFormInternal(
933 $data,
934 $context->getAuthority(),
935 $services->getBlockUserFactory(),
936 $services->getBlockUtils()
937 );
938 }
939
950 private static function processFormInternal(
951 array $data,
952 Authority $performer,
953 BlockUserFactory $blockUserFactory,
954 BlockUtils $blockUtils
955 ) {
956 // Temporarily access service container until the feature flag is removed: T280532
957 $enablePartialActionBlocks = MediaWikiServices::getInstance()
958 ->getMainConfig()->get( MainConfigNames::EnablePartialActionBlocks );
959
960 $isPartialBlock = isset( $data['EditingRestriction'] ) &&
961 $data['EditingRestriction'] === 'partial';
962
963 // This might have been a hidden field or a checkbox, so interesting data
964 // can come from it
965 $data['Confirm'] = !in_array( $data['Confirm'], [ '', '0', null, false ], true );
966
967 // If the user has done the form 'properly', they won't even have been given the
968 // option to suppress-block unless they have the 'hideuser' permission
969 if ( !isset( $data['HideUser'] ) ) {
970 $data['HideUser'] = false;
971 }
972
974 [ $target, $type ] = $blockUtils->parseBlockTarget( $data['Target'] );
975 if ( $type == DatabaseBlock::TYPE_USER ) {
976 $target = $target->getName();
977
978 // Give admins a heads-up before they go and block themselves. Much messier
979 // to do this for IPs, but it's pretty unlikely they'd ever get the 'block'
980 // permission anyway, although the code does allow for it.
981 // Note: Important to use $target instead of $data['Target']
982 // since both $data['PreviousTarget'] and $target are normalized
983 // but $data['target'] gets overridden by (non-normalized) request variable
984 // from previous request.
985 if ( $target === $performer->getUser()->getName() &&
986 ( $data['PreviousTarget'] !== $target || !$data['Confirm'] )
987 ) {
988 return [ 'ipb-blockingself', 'ipb-confirmaction' ];
989 }
990
991 if ( $data['HideUser'] && !$data['Confirm'] ) {
992 return [ 'ipb-confirmhideuser', 'ipb-confirmaction' ];
993 }
994 } elseif ( $type == DatabaseBlock::TYPE_IP ) {
995 $target = $target->getName();
996 } elseif ( $type != DatabaseBlock::TYPE_RANGE ) {
997 // This should have been caught in the form field validation
998 return [ 'badipaddress' ];
999 }
1000
1001 // Reason, to be passed to the block object. For default values of reason, see
1002 // HTMLSelectAndOtherField::getDefault
1003 $blockReason = $data['Reason'][0] ?? '';
1004
1005 $pageRestrictions = [];
1006 $namespaceRestrictions = [];
1007 $actionRestrictions = [];
1008 if ( $isPartialBlock ) {
1009 if ( isset( $data['PageRestrictions'] ) && $data['PageRestrictions'] !== '' ) {
1010 $titles = explode( "\n", $data['PageRestrictions'] );
1011 foreach ( $titles as $title ) {
1012 $pageRestrictions[] = PageRestriction::newFromTitle( $title );
1013 }
1014 }
1015 if ( isset( $data['NamespaceRestrictions'] ) && $data['NamespaceRestrictions'] !== '' ) {
1016 $namespaceRestrictions = array_map( static function ( $id ) {
1017 return new NamespaceRestriction( 0, (int)$id );
1018 }, explode( "\n", $data['NamespaceRestrictions'] ) );
1019 }
1020 if (
1021 $enablePartialActionBlocks &&
1022 isset( $data['ActionRestrictions'] ) &&
1023 $data['ActionRestrictions'] !== ''
1024 ) {
1025 $actionRestrictions = array_map( static function ( $id ) {
1026 return new ActionRestriction( 0, $id );
1027 }, $data['ActionRestrictions'] );
1028 }
1029 }
1030 $restrictions = array_merge( $pageRestrictions, $namespaceRestrictions, $actionRestrictions );
1031
1032 if ( !isset( $data['Tags'] ) ) {
1033 $data['Tags'] = [];
1034 }
1035
1036 $blockOptions = [
1037 'isCreateAccountBlocked' => $data['CreateAccount'],
1038 'isHardBlock' => $data['HardBlock'],
1039 'isAutoblocking' => $data['AutoBlock'],
1040 'isHideUser' => $data['HideUser'],
1041 'isPartial' => $isPartialBlock,
1042 ];
1043
1044 if ( isset( $data['DisableUTEdit'] ) ) {
1045 $blockOptions['isUserTalkEditBlocked'] = $data['DisableUTEdit'];
1046 }
1047 if ( isset( $data['DisableEmail'] ) ) {
1048 $blockOptions['isEmailBlocked'] = $data['DisableEmail'];
1049 }
1050
1051 $blockUser = $blockUserFactory->newBlockUser(
1052 $target,
1053 $performer,
1054 $data['Expiry'],
1055 $blockReason,
1056 $blockOptions,
1057 $restrictions,
1058 $data['Tags']
1059 );
1060
1061 // Indicates whether the user is confirming the block and is aware of
1062 // the conflict (did not change the block target in the meantime)
1063 $blockNotConfirmed = !$data['Confirm'] || ( array_key_exists( 'PreviousTarget', $data )
1064 && $data['PreviousTarget'] !== $target );
1065
1066 // Special case for API - T34434
1067 $reblockNotAllowed = ( array_key_exists( 'Reblock', $data ) && !$data['Reblock'] );
1068
1069 $doReblock = !$blockNotConfirmed && !$reblockNotAllowed;
1070
1071 $status = $blockUser->placeBlock( $doReblock );
1072 if ( !$status->isOK() ) {
1073 return $status;
1074 }
1075
1076 if (
1077 // Can't watch a range block
1078 $type != DatabaseBlock::TYPE_RANGE
1079
1080 // Technically a wiki can be configured to allow anonymous users to place blocks,
1081 // in which case the 'Watch' field isn't included in the form shown, and we should
1082 // not try to access it.
1083 && array_key_exists( 'Watch', $data )
1084 && $data['Watch']
1085 ) {
1086 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
1087 $performer->getUser(),
1088 Title::makeTitle( NS_USER, $target )
1089 );
1090 }
1091
1092 return true;
1093 }
1094
1107 public static function getSuggestedDurations( ?Language $lang = null, $includeOther = true ) {
1108 wfDeprecated( __METHOD__, '1.42' );
1109 $lang ??= MediaWikiServices::getInstance()->getContentLanguage();
1110 return $lang->getBlockDurations( $includeOther );
1111 }
1112
1123 public static function parseExpiryInput( $expiry ) {
1124 wfDeprecated( __METHOD__, '1.36' );
1125 return BlockUser::parseExpiryInput( $expiry );
1126 }
1127
1136 public static function canBlockEmail( UserIdentity $user ) {
1137 wfDeprecated( __METHOD__, '1.36' );
1138 return MediaWikiServices::getInstance()
1139 ->getBlockPermissionCheckerFactory()
1140 ->newBlockPermissionChecker( null, User::newFromIdentity( $user ) )
1141 ->checkEmailPermissions();
1142 }
1143
1150 public function onSubmit( array $data, ?HTMLForm $form = null ) {
1151 return self::processFormInternal(
1152 $data,
1153 $this->getAuthority(),
1154 $this->blockUserFactory,
1155 $this->blockUtils
1156 );
1157 }
1158
1163 public function onSuccess() {
1164 $out = $this->getOutput();
1165 $out->setPageTitleMsg( $this->msg( 'blockipsuccesssub' ) );
1166 $out->addWikiMsg( 'blockipsuccesstext', wfEscapeWikiText( $this->target ) );
1167 }
1168
1177 public function prefixSearchSubpages( $search, $limit, $offset ) {
1178 $search = $this->userNameUtils->getCanonical( $search );
1179 if ( !$search ) {
1180 // No prefix suggestion for invalid user
1181 return [];
1182 }
1183 // Autocomplete subpage as user list - public to allow caching
1184 return $this->userNamePrefixSearch
1185 ->search( UserNamePrefixSearch::AUDIENCE_PUBLIC, $search, $limit, $offset );
1186 }
1187
1188 protected function getGroupName() {
1189 return 'users';
1190 }
1191}
1192
1194class_alias( SpecialBlock::class, 'SpecialBlock' );
const NS_USER
Definition Defines.php:67
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfIsInfinity( $str)
Determine input string is represents as infinity.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
An error page which can definitely be safely rendered using the OutputPage.
Marks HTML that shouldn't be escaped.
Definition HtmlArmor.php:30
Defines the actions that can be blocked by a partial block.
Handles the backend logic of blocking users.
Definition BlockUser.php:54
Backend class for blocking utils.
parseBlockTarget( $target)
From string specification or UserIdentity, get the block target and the type of target.
A DatabaseBlock (unlike a SystemBlock) is stored in the database, may give rise to autoblocks and may...
getType()
Get the type of target for this particular block.int|null AbstractBlock::TYPE_ constant,...
Restriction for partial blocks of actions.
Handle database storage of comments such as edit summaries and log reasons.
Object handling generic submission, CSRF protection, layout and other logic for UI forms in a reusabl...
Definition HTMLForm.php:209
setHeaderHtml( $html, $section=null)
Set header HTML, inside the form.
Definition HTMLForm.php:997
setSubmitTextMsg( $msg)
Set the text for the submit button to a message.
setSubmitDestructive()
Identify that the submit button in the form has a destructive action.
addHeaderHtml( $html, $section=null)
Add HTML to the header, inside the form.
Definition HTMLForm.php:977
This class is a collection of static functions that serve two purposes:
Definition Html.php:57
Base class for language-specific code.
Definition Language.php:79
A class containing constants representing the names of configuration variables.
const UseCodexSpecialBlock
Name constant for the UseCodexSpecialBlock setting, for use with Config::get()
const AutoblockExpiry
Name constant for the AutoblockExpiry setting, for use with Config::get()
const EnableMultiBlocks
Name constant for the EnableMultiBlocks setting, for use with Config::get()
const BlockAllowsUTEdit
Name constant for the BlockAllowsUTEdit setting, for use with Config::get()
const BlockCIDRLimit
Name constant for the BlockCIDRLimit setting, for use with Config::get()
const EnablePartialActionBlocks
Name constant for the EnablePartialActionBlocks setting, for use with Config::get()
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:156
static durationParam( $duration)
Definition Message.php:1233
Immutable value object representing a page reference.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Special page which uses an HTMLForm to handle processing.
string null $par
The subpage of the special page.
Parent class for all special pages.
getSkin()
Shortcut to get the skin being used for this instance.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getUser()
Shortcut to get the User executing this instance.
getConfig()
Shortcut to get main config object.
getRequest()
Get the WebRequest being used for this instance.
msg( $key,... $params)
Wrapper around wfMessage that sets the current context.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
getLanguage()
Shortcut to get user's language.
getName()
Get the canonical, unlocalized name of this special page without namespace.
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
Allow users with 'block' user right to block IPs and user accounts from editing pages and other actio...
onSubmit(array $data, ?HTMLForm $form=null)
Process the form on POST submission.
static parseExpiryInput( $expiry)
Convert a submitted expiry time, which may be relative ("2 weeks", etc) or absolute ("24 May 2034",...
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
onSuccess()
Do something exciting on successful processing of the form, most likely to show a confirmation messag...
preHtml()
Add header elements like block log entries, etc.
static canBlockEmail(UserIdentity $user)
Can we do an email block?
int $type
DatabaseBlock::TYPE_ constant.
static getSuggestedDurations(?Language $lang=null, $includeOther=true)
Get an array of suggested block durations from MediaWiki:Ipboptions.
prefixSearchSubpages( $search, $limit, $offset)
Return an array of subpages beginning with $search that this special page will accept.
User string $previousTarget
The previous block target.
alterForm(HTMLForm $form)
Customizes the HTMLForm a bit.
postHtml()
Add footer elements to the form.
requiresUnblock()
We allow certain special cases where user is blocked.
UserIdentity string null $target
User to be blocked, as passed either by parameter (url?wpTarget=Foo) or as subpage (Special:Block/Foo...
getDisplayFormat()
Get display format for the form.
setParameter( $par)
Handle some magic here.
maybeAlterFormDefaults(&$fields)
If the user has already been blocked with similar settings, load that block and change the defaults f...
checkExecutePermissions(User $user)
Check that the user can unblock themselves if they are trying to do so.
array array $codexFormData
<mixed,mixed> An associative array used to pass vars to Codex form
doesWrites()
Indicates whether POST requests to this special page require write access to the wiki.
static getTargetUserTitle( $target)
Get a user page target for things like logs.
bool $requestedHideUser
Whether the previous submission of the form asked for HideUser.
static processForm(array $data, IContextSource $context)
Given the form data, actually implement a block.
__construct(BlockUtils $blockUtils, BlockPermissionCheckerFactory $blockPermissionCheckerFactory, BlockUserFactory $blockUserFactory, DatabaseBlockStore $blockStore, UserNameUtils $userNameUtils, UserNamePrefixSearch $userNamePrefixSearch, BlockActionInfo $blockActionInfo, TitleFormatter $titleFormatter, NamespaceInfo $namespaceInfo)
getFormFields()
Get the HTMLForm descriptor array for the block form.
execute( $par)
Basic SpecialPage workflow: get a form, send it to the user; get some data back,.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Represents a title within MediaWiki.
Definition Title.php:78
Handles searching prefixes of user names.
UserNameUtils service.
internal since 1.36
Definition User.php:94
newBlockUser( $target, Authority $performer, string $expiry, string $reason='', array $blockOptions=[], array $blockRestrictions=[], $tags=[])
Create BlockUser.
Interface for objects which can provide a MediaWiki context on request.
Interface for objects (potentially) representing a page that can be viewable and linked to on a wiki.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
A title formatter service for MediaWiki.
Interface for objects representing user identity.