Page MenuHomeWickedGov Phorge

SpecialMassGlobalBlock.php
No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None

SpecialMassGlobalBlock.php

<?php
namespace MediaWiki\Extension\GlobalBlocking\Special;
use MediaWiki\CommentStore\CommentStore;
use MediaWiki\Exception\UserBlockedError;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingConnectionProvider;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingExpirySelectorBuilder;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingGlobalBlockDetailsRenderer;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockingLinkBuilder;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockLookup;
use MediaWiki\Extension\GlobalBlocking\Services\GlobalBlockManager;
use MediaWiki\Html\Html;
use MediaWiki\HTMLForm\HTMLForm;
use MediaWiki\Linker\Linker;
use MediaWiki\Message\Message;
use MediaWiki\Permissions\PermissionManager;
use MediaWiki\SpecialPage\SpecialPage;
use MediaWiki\User\UserIdentityLookup;
use Wikimedia\IPUtils;
use Wikimedia\ScopedCallback;
class SpecialMassGlobalBlock extends SpecialPage {
private array $targetsForLookup;
private array $targets;
private PermissionManager $permissionManager;
private UserIdentityLookup $userIdentityLookup;
private GlobalBlockManager $globalBlockManager;
private GlobalBlockLookup $globalBlockLookup;
private GlobalBlockingLinkBuilder $globalBlockingLinkBuilder;
private GlobalBlockingGlobalBlockDetailsRenderer $globalBlockDetailsRenderer;
private GlobalBlockingConnectionProvider $globalBlockingConnectionProvider;
private GlobalBlockingExpirySelectorBuilder $globalBlockingExpirySelectorBuilder;
public function __construct(
PermissionManager $permissionManager,
UserIdentityLookup $userIdentityLookup,
GlobalBlockManager $globalBlockManager,
GlobalBlockLookup $globalBlockLookup,
GlobalBlockingLinkBuilder $globalBlockingLinkBuilder,
GlobalBlockingGlobalBlockDetailsRenderer $globalBlockDetailsRenderer,
GlobalBlockingConnectionProvider $globalBlockingConnectionProvider,
GlobalBlockingExpirySelectorBuilder $globalBlockingExpirySelectorBuilder
) {
parent::__construct( 'MassGlobalBlock', 'globalblock' );
$this->permissionManager = $permissionManager;
$this->userIdentityLookup = $userIdentityLookup;
$this->globalBlockManager = $globalBlockManager;
$this->globalBlockLookup = $globalBlockLookup;
$this->globalBlockingLinkBuilder = $globalBlockingLinkBuilder;
$this->globalBlockDetailsRenderer = $globalBlockDetailsRenderer;
$this->globalBlockingConnectionProvider = $globalBlockingConnectionProvider;
$this->globalBlockingExpirySelectorBuilder = $globalBlockingExpirySelectorBuilder;
}
/**
* @inheritDoc
* @codeCoverageIgnore Merely declarative
*/
public function doesWrites(): bool {
return true;
}
/**
* @inheritDoc
* @codeCoverageIgnore Merely declarative
*/
protected function getGroupName() {
return 'users';
}
/** @inheritDoc */
public function execute( $subPage ) {
$this->checkReadOnly();
parent::execute( $subPage );
// Don't allow sitewide blocked users to use the form, to be consistent with Special:GlobalBlock.
$block = $this->getUser()->getBlock();
if ( $block && $block->isSitewide() ) {
throw new UserBlockedError( $block );
}
$this->addHelpLink( 'Extension:GlobalBlocking' );
$out = $this->getOutput();
$out->addModules( 'ext.globalBlocking' );
$out->addModuleStyles( 'ext.globalBlocking.styles' );
$out->setSubtitle( $this->globalBlockingLinkBuilder->buildSubtitleLinks( $this ) );
$request = $this->getRequest();
// Parse the list of targets provided and store them for later use in ::createTable.
$targetsAsString = $request->getText( 'wpTargets' );
if ( $targetsAsString ) {
// We want to remove empty strings and duplicates, to avoid globally blocking the same user twice.
$targets = explode( "\n", $targetsAsString );
$targets = array_map( 'trim', $targets );
$this->targetsForLookup = array_unique( array_filter( $targets ) );
} else {
$this->targetsForLookup = [];
}
$this->targets = $request->getArray( 'wpActionTarget', [] );
$blockFormAlsoShown = false;
if ( $request->wasPosted() ) {
$method = $request->getRawVal( 'wpMethod' ) ?? '';
if ( $method === 'search' && count( $this->targetsForLookup ) ) {
$this->showBlockForm();
$blockFormAlsoShown = true;
} elseif ( $method === 'block' ) {
$this->performGlobalBlockChanges();
$this->showBlockForm();
$blockFormAlsoShown = true;
}
}
$this->showSearchForm( $blockFormAlsoShown );
}
/**
* Adds to the output the form that allows a user to search for targets to globally block or unblock.
*
* @return void
*/
private function showSearchForm( bool $blockFormAlsoShown ) {
// The submit button is not the primary button if the block form is also shown.
$submitFlags = [ 'progressive' ];
if ( !$blockFormAlsoShown ) {
$submitFlags[] = 'primary';
}
$fields = [
'Targets' => [
'type' => 'textarea',
'dir' => 'ltr',
'rows' => 20,
'id' => 'mw-globalblock-addresslist',
'required' => true,
'autofocus' => true,
'placeholder' => $this->msg( 'globalblocking-mass-block-query-placeholder' )->text(),
'default' => $this->getRequest()->getText( 'wpTargets' ),
],
'Method' => [
'type' => 'hidden',
'default' => 'search',
],
'Submit' => [
'type' => 'submit',
'buttonlabel-message' => 'globalblocking-mass-block-query-submit',
'id' => 'mw-globalblocking-mass-block-query-submit',
'name' => 'mw-globalblocking-mass-block-query-submit',
'flags' => $submitFlags,
],
];
HTMLForm::factory( 'ooui', $fields, $this->getContext() )
->setWrapperLegendMsg( 'globalblocking-mass-block-query-legend' )
->setId( 'mw-globalblocking-mass-block-query' )
->suppressDefaultSubmit()
->setFormIdentifier( 'mw-globalblocking-mass-block-query' )
->prepareForm()
->displayForm( false );
}
/**
* Adds to the output the form that allows a user to perform the mass global blocks or unblocks.
*
* @return void
*/
private function showBlockForm() {
$getExpiry = $this->globalBlockingExpirySelectorBuilder->buildExpirySelector( $this->getContext() );
$fields = [
'Action' => [
'type' => 'radio',
'default' => 'block',
'options-messages' => [
'globalblocking-mass-block-block' => 'block',
'globalblocking-mass-block-unblock' => 'unblock',
]
],
'Expiry' => [
'type' => count( $getExpiry ) ? 'selectorother' : 'text',
'label-message' => 'globalblocking-block-expiry',
'id' => 'mw-globalblocking-mass-block-expiry-selector',
'required' => false,
'options' => $getExpiry,
'other' => $this->msg( 'globalblocking-block-expiry-selector-other' )->text(),
'hide-if' => [ '!==', 'Action', 'block' ],
],
'Reason' => [
'type' => 'selectandother',
'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
'label-message' => 'globalblocking-block-reason',
'id' => 'mw-globalblocking-mass-block-reason',
'options-message' => 'globalblocking-block-reason-dropdown',
],
'AnonOnly' => [
'type' => 'check',
'label-message' => 'globalblocking-ipbanononly',
'id' => 'mw-globalblocking-mass-block-anon-only',
'hide-if' => [ '!==', 'Action', 'block' ],
],
'CreateAccount' => [
'type' => 'check',
'id' => 'mw-globalblocking-mass-block-disable-account-creation',
'label-message' => 'globalblocking-block-disable-account-creation',
'default' => true,
'hide-if' => [ '!==', 'Action', 'block' ],
],
'AutoBlock' => [
'type' => 'check',
'label-message' => [
'globalblocking-block-enable-autoblock',
Message::numParam( $this->getConfig()->get( 'GlobalBlockingMaximumIPsToRetroactivelyAutoblock' ) ),
Message::durationParam( $this->getConfig()->get( 'GlobalBlockingAutoblockExpiry' ) ),
],
'id' => 'mw-globalblocking-mass-block-enable-autoblock',
'default' => true,
'hide-if' => [ '!==', 'Action', 'block' ],
],
];
if ( $this->getUser()->isAllowed( 'block' ) ) {
$fields += [
'AlsoLocal' => [
'type' => 'check',
'label-message' => 'globalblocking-also-local',
'id' => 'mw-globalblocking-mass-block-local',
'hide-if' => [ '!==', 'Action', 'block' ],
],
'AlsoLocalTalk' => [
'type' => 'check',
'label-message' => 'globalblocking-also-local-talk',
'id' => 'mw-globalblocking-mass-block-local-talk',
'hide-if' => [ 'OR', [ '!==', 'Action', 'block' ], [ '!==', 'AlsoLocal', '1' ] ],
],
'AlsoLocalEmail' => [
'type' => 'check',
'label-message' => 'globalblocking-also-local-email',
'id' => 'mw-globalblocking-mass-block-local-email',
'hide-if' => [ 'OR', [ '!==', 'Action', 'block' ], [ '!==', 'AlsoLocal', '1' ] ],
],
'AlsoLocalSoft' => [
'type' => 'check',
'label-message' => 'globalblocking-also-local-soft',
'id' => 'mw-globalblocking-mass-block-local-soft',
'hide-if' => [ 'OR', [ '!==', 'Action', 'block' ], [ '!==', 'AlsoLocal', '1' ] ],
'default' => true,
],
'AlsoLocalAccountCreation' => [
'type' => 'check',
'label-message' => 'globalblocking-also-local-disable-account-creation',
'id' => 'mw-globalblocking-mass-block-local-disable-account-creation',
'hide-if' => [ '!==', 'AlsoLocal', '1' ],
'default' => true,
]
];
}
$fields += [
'MarkBot' => [
'type' => 'check',
'label-message' => 'globalblocking-mass-block-bot',
'id' => 'mw-globalblocking-mass-block-bot',
],
'Method' => [
'type' => 'hidden',
'default' => 'block',
],
'Targets' => [
'type' => 'hidden',
'default' => implode( "\n", $this->targetsForLookup ),
],
];
$form = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
$form->setMethod()
->setWrapperLegendMsg( 'globalblocking-mass-block-legend' )
->setId( 'mw-globalblocking-mass-block-form' )
->setSubmitID( 'mw-globalblocking-mass-block-submit' )
->setSubmitName( 'mw-globalblocking-mass-block-submit' )
->setSubmitDestructive()
->setFormIdentifier( 'mw-globalblocking-mass-block' )
->addHeaderHtml( $this->createTable() )
->prepareForm()
->displayForm( false );
}
/**
* Creates the HTML of the table that shows information about the IPs, IP ranges, or accounts that the
* user may choose to globally block or unblock.
*
* @return string HTML element of the table
*/
private function createTable(): string {
$out = $this->getOutput();
$out->addModuleStyles( 'jquery.tablesorter.styles' );
$out->addModules( 'jquery.tablesorter' );
$tableHeadings = [
Html::element( 'th', [ 'class' => 'unsortable' ] ),
Html::element( 'th', [], $this->msg( 'globalblocking-list-table-heading-target' )->text() ),
Html::element( 'th', [], $this->msg( 'globalblocking-mass-block-header-status' )->text() ),
Html::element( 'th', [], $this->msg( 'globalblocking-list-table-heading-expiry' )->text() ),
Html::element( 'th', [], $this->msg( 'globalblocking-list-table-heading-params' )->text() ),
];
$tableHeaderHtml = Html::rawElement(
'thead', [],
Html::rawElement( 'tr', [], implode( "\n", $tableHeadings ) )
);
$tableRows = [];
foreach ( $this->targetsForLookup as $target ) {
$tableRows[] = $this->createTableRow( $target );
}
$tableBodyHtml = Html::rawElement( 'tbody', [], implode( "\n", $tableRows ) );
return Html::rawElement(
'table',
[ 'class' => 'wikitable sortable', 'id' => 'mw-globalblocking-mass-block-table' ],
$tableHeaderHtml . $tableBodyHtml
);
}
/**
* Creates each row of IPs, with the info on whether it's blocked,
* and the block info if relevant.
*
* @param string $target Target which has not yet been validated.
* @return string HTML element of the row
*/
private function createTableRow( string $target ): string {
$targetValidationStatus = $this->globalBlockManager->validateGlobalBlockTarget( $target, $this->getUser() );
if ( !$targetValidationStatus->isGood() ) {
return Html::rawElement(
'tr', [],
Html::element(
'td', [ 'colspan' => 5, 'data-mw-globalblocking-target' => $target ],
$this->msg( $targetValidationStatus->getMessages()[0] )->text()
)
);
}
$targetData = $targetValidationStatus->getValue();
$globalBlockId = GlobalBlockLookup::isAGlobalBlockId( $target );
if ( !$globalBlockId ) {
$globalBlockId = $this->globalBlockLookup->getGlobalBlockId( $targetData['targetForLookup'] );
}
$dbr = $this->globalBlockingConnectionProvider->getReplicaGlobalBlockingDatabase();
$globalBlockRow = $dbr->newSelectQueryBuilder()
->select( GlobalBlockLookup::selectFields() )
->from( 'globalblocks' )
->where( [ 'gb_id' => $globalBlockId ] )
->caller( __METHOD__ )
->fetchRow();
// Make the checkbox checked if wpActionTargets has no items (which means on the first load
// all users will be selected), or select the targets which are in wpActionTargets to persist
// the checked state of the targets between attempts to submit.
$shouldTargetCheckboxBeChecked = !count( $this->targets ) ||
in_array( $targetData['targetForDisplay'], $this->targets );
$checkboxAttribs = [];
if ( $shouldTargetCheckboxBeChecked ) {
$checkboxAttribs['checked'] = 'checked';
}
$row = [
Html::rawElement(
'td', [],
Html::input( "wpActionTarget[]", $target, 'checkbox', $checkboxAttribs )
),
Html::rawElement( 'td', [], $this->buildTargetForDisplay( $targetData['targetForDisplay'] ) ),
];
if ( $globalBlockRow ) {
$row[] = Html::rawElement(
'td', [],
$this->getLinkRenderer()->makeKnownLink(
SpecialPage::getTitleFor( 'GlobalBlockList' ),
$this->msg( 'globalblocking-mass-block-blocked' )->text(),
[],
[ 'target' => "#{$globalBlockRow->gb_id}" ],
)
);
$row[] = Html::rawElement( 'td', [], $this->getLanguage()->formatExpiry( $globalBlockRow->gb_expiry ) );
$options = $this->globalBlockDetailsRenderer->getBlockOptionsForDisplay(
$globalBlockRow, $this->getContext()
);
$optionsAsText = '';
if ( count( $options ) ) {
$optionsAsText = $this->getLanguage()->commaList( $options );
}
$row[] = Html::rawElement( 'td', [], $optionsAsText );
} else {
$row[] = Html::element( 'td', [], $this->msg( 'globalblocking-mass-block-not-blocked' )->text() );
$row[] = Html::element( 'td' );
// @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement
$row[] = Html::element( 'td' );
}
return Html::rawElement(
'tr',
[ 'data-mw-globalblocking-target' => $targetData['targetForDisplay'] ],
implode( "\n", $row )
);
}
/**
* Builds the HTML used in the 'Target' column for a given $target username.
*
* @param string $target Can be an IP, IP range, username, or global block ID.
* @return string HTML
*/
private function buildTargetForDisplay( string $target ): string {
$globalBlockId = GlobalBlockLookup::isAGlobalBlockId( $target );
if ( $globalBlockId ) {
return $this->msg( 'globalblocking-global-block-id', $globalBlockId )->text();
}
$targetUserIdentity = $this->userIdentityLookup->getUserIdentityByName( $target );
if ( $targetUserIdentity ) {
$targetUserId = $targetUserIdentity->getId();
} else {
$targetUserId = 0;
}
return Linker::userLink( $targetUserId, $target );
}
/**
* Actually performs the mass global block or unblock on the selected targets.
*
* @return void
*/
private function performGlobalBlockChanges() {
// Check that the CSRF token is as expected.
if ( !$this->getContext()->getCsrfTokenSet()->matchTokenField() ) {
$this->getOutput()->addHTML(
Html::errorBox( $this->msg( 'globalblocking-mass-block-token-mismatch' )->parse() )
);
return;
}
// If there are no targets to block, then return as we have nothing to do here.
if ( !count( $this->targets ) ) {
return;
}
// If we are blocking or unblocking too many targets at once, then show an error.
$request = $this->getRequest();
$action = $request->getRawVal( 'wpAction' ) ?? 'block';
$maxTargets = $this->getConfig()->get( 'GlobalBlockingMassGlobalBlockMaxTargets' );
if ( $maxTargets < count( $this->targets ) ) {
// Generates:
// * globalblocking-mass-block-too-many-targets-to-block
// * globalblocking-mass-block-too-many-targets-to-unblock
$msgKey = 'globalblocking-mass-block-too-many-targets-';
if ( $action === 'block' ) {
$msgKey .= 'to-block';
} else {
$msgKey .= 'to-unblock';
}
$this->getOutput()->addHTML( Html::errorBox( $this->msg( $msgKey, $maxTargets )->parse() ) );
return;
}
$performer = $this->getUser();
$globalSuccess = [];
$globalFailure = [];
$localSuccess = [];
$localFailure = [];
// Make the user have the 'bot' right for the duration of this method if wpMarkBot is set.
if ( $request->getCheck( 'wpMarkBot' ) ) {
$temporaryUserRightsScope = $this->permissionManager->addTemporaryUserRights( $performer, 'bot' );
}
// Construct the reason to use for the global blocks, unblocks, and/or local blocks
$reason = $request->getText( 'wpReason' );
$reasonDetail = $request->getText( 'wpReason-other' );
if ( $reason === 'other' ) {
$reason = $reasonDetail;
} elseif ( $reasonDetail ) {
$reason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $reasonDetail;
}
if ( $action === 'block' ) {
foreach ( $this->targets as $target ) {
// Validate that the target is valid for a global block. This also gets the type of target if
// this succeeds.
$targetDataStatus = $this->globalBlockManager->validateGlobalBlockTarget( $target, $performer );
if ( !$targetDataStatus->isGood() ) {
$globalFailure[] = $target;
if ( $request->getCheck( 'wpAlsoLocal' ) ) {
$localFailure[] = $target;
}
continue;
}
$targetData = $targetDataStatus->getValue();
// Generate the global block options based on the form submission and also whether the option is
// relevant for the given $target.
$options = [ 'modify' ];
if ( $request->getCheck( 'wpAnonOnly' ) && IPUtils::isIPAddress( $targetData['target'] ) ) {
$options[] = 'anon-only';
}
if ( !$request->getCheck( 'wpCreateAccount' ) ) {
$options[] = 'allow-account-creation';
}
if ( $request->getCheck( 'wpAutoBlock' ) && $targetData['targetCentralId'] !== 0 ) {
$options[] = 'enable-autoblock';
}
$alsoLocal = $request->getCheck( 'wpAlsoLocal' );
if ( $alsoLocal ) {
$localOptions = [
'isCreateAccountBlocked' => $request->getCheck( 'wpAlsoLocalAccountCreation' ),
'isEmailBlocked' => $request->getCheck( 'wpAlsoLocalEmail' ),
'isUserTalkEditBlocked' => $request->getCheck( 'wpAlsoLocalTalk' ),
'isHardBlock' => !$request->getCheck( 'wpAlsoLocalSoft' ),
'isAutoblocking' => true,
];
} else {
$localOptions = null;
}
$globalBlockStatus = $this->globalBlockManager->block(
$target, $reason, $request->getVal( 'wpExpiry' ), $performer,
$options, $localOptions
);
if ( $globalBlockStatus->isGlobalBlockOK() ) {
$globalSuccess[] = $target;
} else {
$globalFailure[] = $target;
}
if ( $alsoLocal ) {
if ( $globalBlockStatus->isLocalBlockOK() ) {
$localSuccess[] = $target;
} else {
$localFailure[] = $target;
}
}
}
$this->showSuccessBox( $globalSuccess, 'globalblocking-mass-block-success-block' );
$this->showSuccessBox( $localSuccess, 'globalblocking-mass-block-success-local' );
$this->showErrorBox( $globalFailure, 'globalblocking-mass-block-failure-block' );
$this->showErrorBox( $localFailure, 'globalblocking-mass-block-failure-local' );
} elseif ( $action === 'unblock' ) {
foreach ( $this->targets as $target ) {
$globalUnblockStatus = $this->globalBlockManager->unblock( $target, $reason, $performer );
if ( $globalUnblockStatus->isOK() ) {
$globalSuccess[] = $target;
} else {
$globalFailure[] = $target;
}
}
$this->showSuccessBox( $globalSuccess, 'globalblocking-mass-block-success-unblock' );
$this->showErrorBox( $globalFailure, 'globalblocking-mass-block-failure-unblock' );
}
ScopedCallback::consume( $temporaryUserRightsScope );
}
/**
* Adds a success box to the output to indicate targets that were successfully globally blocked, locally blocked,
* or globally unblocked
*
* @param array $relevantTargets The array of targets (usernames, IPs, global block IDs) that were successfully
* blocked/unblocked. If the array is empty, then no box is added.
* @param string $messageKey The message key to use for the success box
* @return void
*/
private function showSuccessBox( array $relevantTargets, string $messageKey ) {
if ( count( $relevantTargets ) ) {
$relevantTargetsAsString = implode( $this->msg( 'comma-separator' )->escaped(), $relevantTargets );
$this->getOutput()->addHTML( Html::successBox(
$this->msg( $messageKey, $relevantTargetsAsString )->parse()
) );
}
}
/**
* Adds a error box to the output to indicate targets that were failed to be globally blocked, locally blocked,
* or globally unblocked
*
* @param array $relevantTargets The array of targets (usernames, IPs, global block IDs) that were failed to
* be blocked/unblocked. If the array is empty, then no box is added.
* @param string $messageKey The message key to use for the error box
* @return void
*/
private function showErrorBox( array $relevantTargets, string $messageKey ) {
if ( count( $relevantTargets ) ) {
$relevantTargetsAsString = implode( $this->msg( 'comma-separator' )->escaped(), $relevantTargets );
$this->getOutput()->addHTML( Html::errorBox(
$this->msg( $messageKey, $relevantTargetsAsString )->parse()
) );
}
}
}

File Metadata

Mime Type
text/x-php
Expires
Aug 18 2026, 05:04 (8 w, 2 d ago)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
75/ab/0e8ed3af5a5a30e21dd831362a53
Default Alt Text
SpecialMassGlobalBlock.php (21 KB)

Event Timeline