getLanguage(); $user = $this->getUser(); $out = $this->getOutput(); if ( !$afPermManager->canRevertFilterActions( $user ) ) { throw new PermissionsError( 'abusefilter-revert' ); } $block = $user->getBlock(); if ( $block && $block->isSitewide() ) { throw new UserBlockedError( $block ); } $this->loadParameters(); if ( $this->attemptRevert() ) { return; } $filter = $this->filter; $out->addWikiMsg( 'abusefilter-revert-intro', Message::numParam( $filter ) ); $out->setPageTitle( $this->msg( 'abusefilter-revert-title' )->numParams( $filter ) ); // First, the search form. Limit dates to avoid huge queries $RCMaxAge = $this->getConfig()->get( 'RCMaxAge' ); $min = wfTimestamp( TS_ISO_8601, time() - $RCMaxAge ); $max = wfTimestampNow(); $filterLink = $this->linkRenderer->makeLink( $this->getTitle( $filter ), $lang->formatNum( $filter ) ); $searchFields = []; $searchFields['filterid'] = [ 'type' => 'info', 'default' => $filterLink, 'raw' => true, 'label-message' => 'abusefilter-revert-filter' ]; $searchFields['periodstart'] = [ 'type' => 'datetime', 'name' => 'wpPeriodStart', 'default' => $this->origPeriodStart, 'label-message' => 'abusefilter-revert-periodstart', 'min' => $min, 'max' => $max ]; $searchFields['periodend'] = [ 'type' => 'datetime', 'name' => 'wpPeriodEnd', 'default' => $this->origPeriodEnd, 'label-message' => 'abusefilter-revert-periodend', 'min' => $min, 'max' => $max ]; HTMLForm::factory( 'ooui', $searchFields, $this->getContext() ) ->addHiddenField( 'submit', 1 ) ->setAction( $this->getTitle( "revert/$filter" )->getLocalURL() ) ->setWrapperLegendMsg( 'abusefilter-revert-search-legend' ) ->setSubmitTextMsg( 'abusefilter-revert-search' ) ->setMethod( 'post' ) ->prepareForm() ->displayForm( false ); if ( $this->mSubmit ) { // Add a summary of everything that will be reversed. $out->addWikiMsg( 'abusefilter-revert-preview-intro' ); // Look up all of them. $results = $this->doLookup(); $list = []; $context = $this->getContext(); foreach ( $results as $result ) { $displayActions = []; foreach ( $result['actions'] as $action ) { $displayActions[] = AbuseFilter::getActionDisplay( $action, $context ); } $msg = $this->msg( 'abusefilter-revert-preview-item' ) ->params( $lang->timeanddate( $result['timestamp'], true ) )->rawParams( Linker::userLink( $result['userid'], $result['user'] ) )->params( $result['action'] )->rawParams( $this->linkRenderer->makeLink( $result['title'] ) )->params( $lang->commaList( $displayActions ) )->rawParams( $this->linkRenderer->makeLink( SpecialPage::getTitleFor( 'AbuseLog' ), $this->msg( 'abusefilter-log-detailslink' )->text(), [], [ 'details' => $result['id'] ] ) )->params( $result['user'] )->parse(); $list[] = Xml::tags( 'li', null, $msg ); } $out->addHTML( Xml::tags( 'ul', null, implode( "\n", $list ) ) ); // Add a button down the bottom. $confirmForm = []; $confirmForm['edittoken'] = [ 'type' => 'hidden', 'name' => 'editToken', 'default' => $user->getEditToken( "abusefilter-revert-$filter" ) ]; $confirmForm['title'] = [ 'type' => 'hidden', 'name' => 'title', 'default' => $this->getTitle( "revert/$filter" )->getPrefixedDBkey() ]; $confirmForm['wpPeriodStart'] = [ 'type' => 'hidden', 'name' => 'wpPeriodStart', 'default' => $this->origPeriodStart ]; $confirmForm['wpPeriodEnd'] = [ 'type' => 'hidden', 'name' => 'wpPeriodEnd', 'default' => $this->origPeriodEnd ]; $confirmForm['reason'] = [ 'type' => 'text', 'label-message' => 'abusefilter-revert-reasonfield', 'name' => 'wpReason', 'id' => 'wpReason', ]; HTMLForm::factory( 'ooui', $confirmForm, $this->getContext() ) ->setAction( $this->getTitle( "revert/$filter" )->getLocalURL() ) ->setWrapperLegendMsg( 'abusefilter-revert-confirm-legend' ) ->setSubmitTextMsg( 'abusefilter-revert-confirm' ) ->setMethod( 'post' ) ->prepareForm() ->displayForm( false ); } } /** * @return array[] */ public function doLookup() { $periodStart = $this->mPeriodStart; $periodEnd = $this->mPeriodEnd; $filter = $this->filter; $conds = [ 'afl_filter' => $filter ]; $dbr = wfGetDB( DB_REPLICA ); if ( $periodStart !== null ) { $conds[] = 'afl_timestamp >= ' . $dbr->addQuotes( $dbr->timestamp( $periodStart ) ); } if ( $periodEnd !== null ) { $conds[] = 'afl_timestamp <= ' . $dbr->addQuotes( $dbr->timestamp( $periodEnd ) ); } $selectFields = [ 'afl_id', 'afl_user', 'afl_user_text', 'afl_action', 'afl_actions', 'afl_var_dump', 'afl_timestamp', 'afl_namespace', 'afl_title', 'afl_wiki', ]; $res = $dbr->select( 'abuse_filter_log', $selectFields, $conds, __METHOD__ ); $results = []; foreach ( $res as $row ) { // Don't revert if there was no action, or the action was global if ( !$row->afl_actions || $row->afl_wiki != null ) { continue; } $actions = explode( ',', $row->afl_actions ); $reversibleActions = [ 'block', 'blockautopromote', 'degroup' ]; $currentReversibleActions = array_intersect( $actions, $reversibleActions ); if ( count( $currentReversibleActions ) ) { $results[] = [ 'id' => $row->afl_id, 'actions' => $currentReversibleActions, 'user' => $row->afl_user_text, 'userid' => $row->afl_user, 'vars' => AbuseFilter::loadVarDump( $row->afl_var_dump ), 'title' => Title::makeTitle( $row->afl_namespace, $row->afl_title ), 'action' => $row->afl_action, 'timestamp' => $row->afl_timestamp ]; } } return $results; } /** * Loads parameters from request */ public function loadParameters() { $request = $this->getRequest(); $this->filter = (int)$this->mParams[1]; $this->origPeriodStart = $request->getText( 'wpPeriodStart' ); $this->mPeriodStart = strtotime( $this->origPeriodStart ) ?: null; $this->origPeriodEnd = $request->getText( 'wpPeriodEnd' ); $this->mPeriodEnd = strtotime( $this->origPeriodEnd ) ?: null; $this->mSubmit = $request->getBool( 'submit' ); $this->mReason = $request->getVal( 'wpReason' ); } /** * @return bool */ public function attemptRevert() { $filter = $this->filter; $token = $this->getRequest()->getVal( 'editToken' ); if ( !$this->getUser()->matchEditToken( $token, "abusefilter-revert-$filter" ) ) { return false; } $results = $this->doLookup(); foreach ( $results as $result ) { $actions = $result['actions']; foreach ( $actions as $action ) { $this->revertAction( $action, $result ); } } $this->getOutput()->wrapWikiMsg( '

$1

', [ 'abusefilter-revert-success', $filter, $this->getLanguage()->formatNum( $filter ) ] ); return true; } /** * @param string $action * @param array $result * @return bool * @throws MWException */ public function revertAction( $action, $result ) { switch ( $action ) { case 'block': $block = DatabaseBlock::newFromTarget( $result['user'] ); if ( !( $block && $block->getBy() === AbuseFilter::getFilterUser()->getId() ) ) { // Not blocked by abuse filter return false; } $block->delete(); $logEntry = new ManualLogEntry( 'block', 'unblock' ); $logEntry->setTarget( Title::makeTitle( NS_USER, $result['user'] ) ); $logEntry->setComment( $this->msg( 'abusefilter-revert-reason', $this->filter, $this->mReason )->inContentLanguage()->text() ); $logEntry->setPerformer( $this->getUser() ); $logEntry->publish( $logEntry->insert() ); return true; case 'blockautopromote': $target = User::newFromId( $result['userid'] ); $msg = $this->msg( 'abusefilter-revert-reason', $this->filter, $this->mReason )->inContentLanguage()->text(); return AbuseFilterServices::getBlockAutopromoteStore() ->unblockAutopromote( $target, $this->getUser(), $msg ); case 'degroup': $userGroupsManager = MediaWikiServices::getInstance()->getUserGroupManager(); // Pull the user's groups from the vars. $removedGroups = $result['vars']->getVar( 'user_groups' )->toNative(); $removedGroups = array_diff( $removedGroups, $userGroupsManager->listAllImplicitGroups() ); $user = User::newFromId( $result['userid'] ); $currentGroups = $userGroupsManager->getUserGroups( $user ); $done = false; foreach ( $removedGroups as $group ) { // TODO An addUserToGroups method with bulk updates would be nice $done = $userGroupsManager->addUserToGroup( $user, $group ) || $done; } // Don't log if no groups were added. if ( !$done ) { return false; } // TODO Core should provide a logging method $logEntry = new ManualLogEntry( 'rights', 'rights' ); $logEntry->setTarget( $user->getUserPage() ); $logEntry->setPerformer( $this->getUser() ); $logEntry->setComment( $this->msg( 'abusefilter-revert-reason', $this->filter, $this->mReason )->inContentLanguage()->text() ); $logEntry->setParameters( [ '4::oldgroups' => $currentGroups, '5::newgroups' => $userGroupsManager->getUserGroups( $user ) ] ); $logEntry->publish( $logEntry->insert() ); return true; } throw new MWException( "Invalid action $action" ); } }