From ce37b255c14144ebb518871373cbef907804bece Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Taavi=20V=C3=A4=C3=A4n=C3=A4nen?= <hi@taavi.wtf>
Date: Fri, 10 Dec 2021 21:10:40 +0200
Subject: [PATCH] SECURITY: Call EditFilterMergedContent in McrUndo

This patch updates the McrUndo action to run the
EditFilterMergedContent hook to let extensions like AbuseFilter prevent
certain content from being saved.

Thanks to Dylsss for the original report.

Bug: T297322
Change-Id: I23505e89d7c2cb5d4bcc7ea28203f2b322b827b7
---
 includes/actions/McrUndoAction.php | 53 ++++++++++++++++++++++++++----
 1 file changed, 46 insertions(+), 7 deletions(-)

diff --git a/includes/actions/McrUndoAction.php b/includes/actions/McrUndoAction.php
index a372299819..9f6f570631 100644
--- a/includes/actions/McrUndoAction.php
+++ b/includes/actions/McrUndoAction.php
@@ -170,9 +170,10 @@ class McrUndoAction extends FormAction {
 	}
 
 	/**
-	 * @return MutableRevisionRecord
+	 * @param bool $save Whether to run checks only relevant when actually saving the new revision
+	 * @return Status
 	 */
-	private function getNewRevision() {
+	private function getNewRevision( $save = false ) {
 		$undoRev = $this->revisionLookup->getRevisionById( $this->undo );
 		$oldRev = $this->revisionLookup->getRevisionById( $this->undoafter );
 		$curRev = $this->curRev;
@@ -186,9 +187,10 @@ class McrUndoAction extends FormAction {
 			throw new ErrorPageError( 'mcrundofailed', 'undo-norev' );
 		}
 
-		if ( $isLatest ) {
+		if ( $isLatest && !$save ) {
 			// Short cut! Undoing the current revision means we just restore the old.
-			return MutableRevisionRecord::newFromParentRevision( $oldRev );
+			// except that when saving, we need to run the onEditFilterMergedContent hook
+			return Status::newGood( MutableRevisionRecord::newFromParentRevision( $oldRev ) );
 		}
 
 		$newRev = MutableRevisionRecord::newFromParentRevision( $curRev );
@@ -248,6 +250,8 @@ class McrUndoAction extends FormAction {
 			}
 		}
 
+		$hookRunner = Hooks::runner();
+
 		// Try to merge anything that's left.
 		foreach ( $rolesToMerge as $role ) {
 			$oldContent = $oldRev->getSlot( $role, RevisionRecord::RAW )->getContent();
@@ -258,14 +262,42 @@ class McrUndoAction extends FormAction {
 			if ( !$newContent ) {
 				throw new ErrorPageError( 'mcrundofailed', 'undo-failure' );
 			}
+
+			if ( $save ) {
+				$status = new Status();
+				$hookResult = $hookRunner->onEditFilterMergedContent(
+					$this->getContext(),
+					$newContent,
+					$status,
+					trim( $this->getRequest()->getVal( 'wpSummary' ) ),
+					$this->getUser(),
+					false
+				);
+
+				if ( !$hookResult ) {
+					if ( $status->isGood() ) {
+						$status->error( 'hookaborted' );
+					}
+
+					return $status;
+				}
+			}
+
 			$newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
 		}
 
-		return $newRev;
+		return Status::newGood( $newRev );
 	}
 
 	private function generateDiffOrPreview() {
-		$newRev = $this->getNewRevision();
+		$newRevStatus = $this->getNewRevision();
+
+		if ( !$newRevStatus->isGood() ) {
+			throw new ErrorPageError( 'mcrundofailed', $newRevStatus->getMessage() );
+		}
+
+		$newRev = $newRevStatus->getValue();
+
 		if ( $newRev->hasSameContent( $this->curRev ) ) {
 			throw new ErrorPageError( 'mcrundofailed', 'undo-nochange' );
 		}
@@ -371,7 +403,14 @@ class McrUndoAction extends FormAction {
 			throw new PermissionsError( 'edit', $errors );
 		}
 
-		$newRev = $this->getNewRevision();
+		$newRevStatus = $this->getNewRevision( true );
+
+		if ( !$newRevStatus->isGood() ) {
+			return $newRevStatus;
+		}
+
+		$newRev = $newRevStatus->getValue();
+
 		if ( !$newRev->hasSameContent( $curRev ) ) {
 
 			// Copy new slots into the PageUpdater, and remove any removed slots.
-- 
2.33.0

