From 03a81194058bf1b865ef13ffbc739286a564eb28 Mon Sep 17 00:00:00 2001
From: Dreamy Jazz <wpgbrown@wikimedia.org>
Date: Sun, 18 Feb 2024 23:13:16 +0000
Subject: [PATCH] SECURITY: Limit subpages displayed on Special:MovePage form

Why:
* Special:MovePage shows the list of subpages for the page
  provided before the user submits the form.
* There is currently no limit on the number of subpages shown
  and as such loading the move page for a page with tens of
  thousands of subpages causes request timeouts when trying
  to generate the link HTML for each subpage.
* Special:MovePage does not need to display all the subpages
  and can limit the list to wgMaximumMovedPages subpages as
  the user who submits the form would only move that many
  subpages if they specified to move these subpages.
* A user wanting to find the full list can use Special:Prefix
  Index which provides paging.

What:
* Provide a $limit to Title::getSubpages for both calls in
  SpecialMovePage::showSubpages. $limit is defined as
  $wgMaximumMovedPages plus 1, where the extra subpage is
  used to determine if the results were truncated similar to
  IndexPager.
* Because i18n modifications in security patches are to be
  strongly avoided, hard code the message shown when the
  list of subpages is truncated. This should be replaced with
  an actual message key when this is publicly uploaded to
  Gerrit.

Bug: T357760
Change-Id: I78fa0b04d2bc82c8deffa2ed5433eb2563c17962
---
 includes/specials/SpecialMovepage.php | 30 ++++++++++++++++++++++++---
 1 file changed, 27 insertions(+), 3 deletions(-)

diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index 26c2860238f..c95f7594ef5 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -927,12 +927,13 @@ class MovePageForm extends UnlistedSpecialPage {
 	 * @param Title $title Page being moved.
 	 */
 	private function showSubpages( $title ) {
+		$maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
 		$nsHasSubpages = $this->nsInfo->hasSubpages( $title->getNamespace() );
-		$subpages = $title->getSubpages();
+		$subpages = $title->getSubpages( $maximumMovedPages + 1 );
 		$count = $subpages instanceof TitleArray ? $subpages->count() : 0;
 
 		$titleIsTalk = $title->isTalkPage();
-		$subpagesTalk = $title->getTalkPage()->getSubpages();
+		$subpagesTalk = $title->getTalkPage()->getSubpages( $maximumMovedPages + 1 );
 		$countTalk = $subpagesTalk instanceof TitleArray ? $subpagesTalk->count() : 0;
 		$totalCount = $count + $countTalk;
 
@@ -963,7 +964,19 @@ class MovePageForm extends UnlistedSpecialPage {
 			return;
 		}
 
-		$out->addWikiMsg( $wikiMsg, $this->getLanguage()->formatNum( $pagecount ) );
+		$maximumMovedPages = $this->getConfig()->get( MainConfigNames::MaximumMovedPages );
+
+		if ( $pagecount > $maximumMovedPages ) {
+			$subpages = $this->truncateSubpagesList( $subpages );
+			// TODO: Replace with a message key once this is uploaded to Gerrit. This is hardcoded to avoid
+			//  having the i18n rebuilt for all deployments due to this security patch.
+			$out->addWikiTextAsInterface(
+				"The first $maximumMovedPages {{PLURAL:$maximumMovedPages|subpage|subpages}} " .
+				( $noSubpageMsg ? 'for this page' : 'for the corresponding talk page' ) . ' are shown below.'
+			);
+		} else {
+			$out->addWikiMsg( $wikiMsg, $this->getLanguage()->formatNum( $pagecount ) );
+		}
 		$out->addHTML( "<ul>\n" );
 
 		$linkBatch = $this->linkBatchFactory->newLinkBatch( $subpages );
@@ -978,6 +991,17 @@ class MovePageForm extends UnlistedSpecialPage {
 		$out->addHTML( "</ul>\n" );
 	}
 
+	private function truncateSubpagesList( iterable $subpages ): array {
+		$returnArray = [];
+		foreach ( $subpages as $subpage ) {
+			$returnArray[] = $subpage;
+			if ( count( $returnArray ) >= $this->getConfig()->get( MainConfigNames::MaximumMovedPages ) ) {
+				break;
+			}
+		}
+		return $returnArray;
+	}
+
 	/**
 	 * Return an array of subpages beginning with $search that this special page will accept.
 	 *
-- 
2.34.1

