From f13d4130f8bd39d18c1831b8e6308c4a492f8876 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 EditFilterMerged
content to let hooks like AbuseFilter prevent certain content from
being saved.

Thanks to Dylsss for the original report.

Bug: T297322
Change-Id: I23505e89d7c2cb5d4bcc7ea28203f2b322b827b7
---
 includes/actions/McrUndoAction.php | 31 +++++++++++++++++++++++++++---
 1 file changed, 28 insertions(+), 3 deletions(-)

diff --git a/includes/actions/McrUndoAction.php b/includes/actions/McrUndoAction.php
index a372299819..55464a55d3 100644
--- a/includes/actions/McrUndoAction.php
+++ b/includes/actions/McrUndoAction.php
@@ -170,9 +170,10 @@ class McrUndoAction extends FormAction {
 	}
 
 	/**
+	 * @param bool $save Whether to run checks only relevant when actually saving the new revision
 	 * @return MutableRevisionRecord
 	 */
-	private function getNewRevision() {
+	private function getNewRevision( $save = false ) {
 		$undoRev = $this->revisionLookup->getRevisionById( $this->undo );
 		$oldRev = $this->revisionLookup->getRevisionById( $this->undoafter );
 		$curRev = $this->curRev;
@@ -186,8 +187,9 @@ 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.
+			// except that when saving, we need to run the onEditFilterMergedContent hook
 			return MutableRevisionRecord::newFromParentRevision( $oldRev );
 		}
 
@@ -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,6 +262,27 @@ 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' );
+					}
+
+					throw new ErrorPageError( 'mcrundofailed', $status->getMessage() );
+				}
+			}
+
 			$newRev->setSlot( SlotRecord::newUnsaved( $role, $newContent ) );
 		}
 
@@ -371,7 +396,7 @@ class McrUndoAction extends FormAction {
 			throw new PermissionsError( 'edit', $errors );
 		}
 
-		$newRev = $this->getNewRevision();
+		$newRev = $this->getNewRevision( true );
 		if ( !$newRev->hasSameContent( $curRev ) ) {
 
 			// Copy new slots into the PageUpdater, and remove any removed slots.
-- 
2.33.0

