Add more details to post-delete page

This adds the following to the post-delete page:
* Number of pages queued for deletion (or deleted immediately)
* Username, talk page link, and Special:Block link for the target
  (if the target is a user)
* Number of pages not queued for deletion (i.e. skipped)
* List of pages not queued for deletion

This adds relevant tests, and also modifies the "uploadTestFile"
utility function, which now randomizes file name to ensure that
files uploaded by users regardless of title are also deleted.

Bug: T364223
Bug: T364225
Change-Id: I60cf1a1c54cdaa3ae4570b4d2bef7e7cf39b47e5
This commit is contained in:
Chlod Alejandro 2024-11-17 17:53:02 +08:00
parent 38f3b94dd2
commit def1219fda
No known key found for this signature in database
GPG key ID: A1E67C59037B0CC1
4 changed files with 203 additions and 24 deletions

View file

@ -23,9 +23,13 @@
"nuke-userorip": "Username, IP address or blank:", "nuke-userorip": "Username, IP address or blank:",
"nuke-maxpages": "Number of pages to retrieve (maximum 500):", "nuke-maxpages": "Number of pages to retrieve (maximum 500):",
"nuke-editby": "Created by [[Special:Contributions/$1|{{GENDER:$1|$1}}]].", "nuke-editby": "Created by [[Special:Contributions/$1|{{GENDER:$1|$1}}]].",
"nuke-delete-summary": "Queued '''{{PLURAL:$1|one page|$1 pages}}''' for deletion.",
"nuke-delete-summary-user": "Queued '''{{PLURAL:$1|one page|$1 pages}}''' {{GENDER:$2|created}} by [[{{ns:user}}:$2|$2]] ([[{{ns:user_talk}}:$2|{{int:talkpagelinktext}}]] {{int:pipe-separator}} [[{{ns:special}}:Contributions/$2|{{int:contribslink}}]] {{int:pipe-separator}} [[{{ns:special}}:Block/$2|{{int:blocklink}}]]) for deletion.",
"nuke-deleted": "Page '''$1''' has been deleted.", "nuke-deleted": "Page '''$1''' has been deleted.",
"nuke-deletion-queued": "Page '''$1''' has been queued for deletion.", "nuke-deletion-queued": "Page '''$1''' has been queued for deletion.",
"nuke-not-deleted": "Page [[:$1]] '''could not''' be deleted.", "nuke-not-deleted": "Page [[:$1]] '''could not''' be deleted.",
"nuke-skipped-summary": "'''{{PLURAL:$1|One page''' was|$1 pages''' were}} not selected for deletion.",
"nuke-skipped": "[[:$1]] ([[:$2|talk]] {{int:pipe-separator}} [[{{ns:special}}:PageHistory/$1|history]])",
"nuke-delete-more": "[[Special:Nuke|Delete more pages]]", "nuke-delete-more": "[[Special:Nuke|Delete more pages]]",
"nuke-pattern": "SQL LIKE pattern (e.g. %) for the page name:", "nuke-pattern": "SQL LIKE pattern (e.g. %) for the page name:",
"nuke-nopages-global": "There are no page titles matching your search.", "nuke-nopages-global": "There are no page titles matching your search.",

View file

@ -37,9 +37,13 @@
"nuke-userorip": "Used as label for \"target\" input box.", "nuke-userorip": "Used as label for \"target\" input box.",
"nuke-maxpages": "Used as label for \"nuke limit\" input box.", "nuke-maxpages": "Used as label for \"nuke limit\" input box.",
"nuke-editby": "This message is followed by {{msg-mw|Comma-separator}} and {{msg-mw|Nuke-viewchanges}}.\n\nParameters:\n* $1 - a username", "nuke-editby": "This message is followed by {{msg-mw|Comma-separator}} and {{msg-mw|Nuke-viewchanges}}.\n\nParameters:\n* $1 - a username",
"nuke-delete-summary": "Used at the top of the Nuke (mass deletion) result page. Shown when a username was not provided.\n\nParameters:\n* $1 - the number of pages queued for deletion\n\nSee also:\n* {{msg-mw|nuke-delete-summary-user}}",
"nuke-delete-summary-user": "Used at the top of the Nuke (mass deletion) result page. Shown when a username was provided.\n\nParameters:\n* $1 - the number of pages queued for deletion\n* $2 - the username of the user targeted for deletion\n\nSee also:\n* {{msg-mw|nuke-delete-summary}}",
"nuke-deleted": "Used as success result of deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-not-deleted}}", "nuke-deleted": "Used as success result of deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-not-deleted}}",
"nuke-deletion-queued": "Used when the page is queued for deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-deleted}}", "nuke-deletion-queued": "Used when the page is queued for deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-deleted}}",
"nuke-not-deleted": "Used as failure result of deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-deleted}}", "nuke-not-deleted": "Used as failure result of deletion. Parameters:\n* $1 - page title\nSee also:\n* {{msg-mw|Nuke-deleted}}",
"nuke-skipped-summary": "Used at the middle (or bottom) of the Nuke (mass deletion) result page. Shown only when some pages were not selected when mass deleting.\n\nParameters:\n* $1 - the number of pages queued for deletion\n\nSee also: \n*{{msg-mw|nuke-skipped}}",
"nuke-skipped": "Used to show links to pages that were skipped, for further action by administrators.\n\nParameters:\n* $1 - the title of the skipped page\n* $2 - the title of the talk page for the skipped page\n\nSee also: \n*{{msg-mw|nuke-skipped}}",
"nuke-delete-more": "Used at the bottom of the Nuke (mass deletion) result page.", "nuke-delete-more": "Used at the bottom of the Nuke (mass deletion) result page.",
"nuke-pattern": "{{doc-important|Do not translate <code>LIKE</code>, it is an SQL operator.}}\nUsed as label for \"nuke pattern\" input box.", "nuke-pattern": "{{doc-important|Do not translate <code>LIKE</code>, it is an SQL operator.}}\nUsed as label for \"nuke pattern\" input box.",
"nuke-nopages-global": "Used if there are no pages to delete and the username is empty.\n\nSee also:\n* {{msg-mw|Nuke-nopages}}", "nuke-nopages-global": "Used if there are no pages to delete and the username is empty.\n\nSee also:\n* {{msg-mw|Nuke-nopages}}",

View file

@ -136,12 +136,16 @@ class SpecialNuke extends SpecialPage {
&& $currentUser->matchEditToken( $req->getVal( 'wpEditToken' ) ) && $currentUser->matchEditToken( $req->getVal( 'wpEditToken' ) )
) { ) {
if ( $req->getRawVal( 'action' ) === 'delete' ) { if ( $req->getRawVal( 'action' ) === 'delete' ) {
$pages = $req->getArray( 'pages' ); $pages = $req->getArray( 'pages' ) ?? [];
$originalPageList
= explode( '|', $req->getText( 'originalPageList' ) );
if ( $pages ) { if ( count( $originalPageList ) === 1 && !$originalPageList[0] ) {
$this->doDelete( $pages, $reason ); // No page list was provided.
return; $originalPageList = [];
} }
$this->doDelete( $pages, $originalPageList, $reason, $target );
} elseif ( $req->getRawVal( 'action' ) === 'submit' ) { } elseif ( $req->getRawVal( 'action' ) === 'submit' ) {
// if the target is an ip addresss and temp account lookup is available, // if the target is an ip addresss and temp account lookup is available,
// list pages created by the ip user or by temp accounts associated with the ip address // list pages created by the ip user or by temp accounts associated with the ip address
@ -362,6 +366,7 @@ class SpecialNuke extends SpecialPage {
'name' => 'nukelist' ] 'name' => 'nukelist' ]
) . ) .
Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) . Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ) .
Html::hidden( 'target', $username ) .
$dropdown . $dropdown .
$reasonField . $reasonField .
// Select: All, None, Invert // Select: All, None, Invert
@ -369,6 +374,8 @@ class SpecialNuke extends SpecialPage {
'<ul>' '<ul>'
); );
$titles = [];
$wordSeparator = $this->msg( 'word-separator' )->escaped(); $wordSeparator = $this->msg( 'word-separator' )->escaped();
$commaSeparator = $this->msg( 'comma-separator' )->escaped(); $commaSeparator = $this->msg( 'comma-separator' )->escaped();
$pipeSeparator = $this->msg( 'pipe-separator' )->escaped(); $pipeSeparator = $this->msg( 'pipe-separator' )->escaped();
@ -379,6 +386,7 @@ class SpecialNuke extends SpecialPage {
/** /**
* @var $title Title * @var $title Title
*/ */
$titles[] = $title->getPrefixedDBkey();
$image = $title->inNamespace( NS_FILE ) ? $localRepo->newFile( $title ) : false; $image = $title->inNamespace( NS_FILE ) ? $localRepo->newFile( $title ) : false;
$thumb = $image && $image->exists() ? $thumb = $image && $image->exists() ?
@ -421,6 +429,7 @@ class SpecialNuke extends SpecialPage {
$out->addHTML( $out->addHTML(
"</ul>\n" . "</ul>\n" .
Html::hidden( 'originalPageList', implode( '|', $titles ) ) .
Html::submitButton( $this->msg( 'nuke-submit-delete' )->text() ) . Html::submitButton( $this->msg( 'nuke-submit-delete' )->text() ) .
'</form>' '</form>'
); );
@ -590,18 +599,47 @@ class SpecialNuke extends SpecialPage {
* Does the actual deletion of the pages. * Does the actual deletion of the pages.
* *
* @param array $pages The pages to delete * @param array $pages The pages to delete
* @param array $originalPageList The original list of pages shown to the user
* @param string $reason * @param string $reason
* @param string $target
* @throws PermissionsError * @throws PermissionsError
*/ */
protected function doDelete( array $pages, $reason ): void { protected function doDelete(
array $pages, array $originalPageList, string $reason, string $target
): void {
$res = []; $res = [];
$jobs = []; $jobs = [];
$skippedRes = [];
$user = $this->getUser(); $user = $this->getUser();
$queuedCount = 0;
// Get a list of all pages involved and what to do with them.
// Pages in $pages will always be deleted, even if they are not present in
// $originalPageList.
$willDeleteList = [];
foreach ( $pages as $page ) {
$willDeleteList[$page] = true;
}
foreach ( $originalPageList as $originalPage ) {
if ( !isset( $willDeleteList[$originalPage] ) ) {
$willDeleteList[$originalPage] = false;
}
}
$localRepo = $this->repoGroup->getLocalRepo(); $localRepo = $this->repoGroup->getLocalRepo();
foreach ( $pages as $page ) { foreach ( $willDeleteList as $page => $willDelete ) {
$title = Title::newFromText( $page ); $title = Title::newFromText( $page );
if ( !$willDelete ) {
// If this page was skipped, add it to the list of skipped pages and move on.
$skippedRes[] = $this->msg(
'nuke-skipped',
wfEscapeWikiText( $title->getPrefixedText() ),
wfEscapeWikiText( $title->getTalkPageIfDefined() )
)->parse();
continue;
}
$deletionResult = false; $deletionResult = false;
if ( !$this->getNukeHookRunner()->onNukeDeletePage( $title, $reason, $deletionResult ) ) { if ( !$this->getNukeHookRunner()->onNukeDeletePage( $title, $reason, $deletionResult ) ) {
$res[] = $this->msg( $res[] = $this->msg(
@ -649,11 +687,15 @@ class SpecialNuke extends SpecialPage {
'nuke-deletion-queued', 'nuke-deletion-queued',
wfEscapeWikiText( $title->getPrefixedText() ) wfEscapeWikiText( $title->getPrefixedText() )
)->parse(); )->parse();
$queuedCount++;
} else { } else {
$res[] = $this->msg( $res[] = $this->msg(
$status->isOK() ? 'nuke-deleted' : 'nuke-not-deleted', $status->isOK() ? 'nuke-deleted' : 'nuke-not-deleted',
wfEscapeWikiText( $title->getPrefixedText() ) wfEscapeWikiText( $title->getPrefixedText() )
)->parse(); )->parse();
if ( $status->isOK() ) {
$queuedCount++;
}
} }
} }
@ -661,11 +703,27 @@ class SpecialNuke extends SpecialPage {
$this->jobQueueGroup->push( $jobs ); $this->jobQueueGroup->push( $jobs );
} }
// Show the main summary, regardless of whether we deleted pages or not.
if ( $target ) {
$this->getOutput()->addWikiMsg( 'nuke-delete-summary-user', $queuedCount, $target );
} else {
$this->getOutput()->addWikiMsg( 'nuke-delete-summary', $queuedCount );
}
if ( $queuedCount ) {
$this->getOutput()->addHTML( $this->getOutput()->addHTML(
"<ul>\n<li>" . "<ul>\n<li>" .
implode( "</li>\n<li>", $res ) . implode( "</li>\n<li>", $res ) .
"</li>\n</ul>\n" "</li>\n</ul>\n"
); );
}
if ( count( $skippedRes ) ) {
$this->getOutput()->addWikiMsg( 'nuke-skipped-summary', count( $skippedRes ) );
$this->getOutput()->addHTML(
"<ul>\n<li>" .
implode( "</li>\n<li>", $skippedRes ) .
"</li>\n</ul>\n"
);
}
$this->getOutput()->addWikiMsg( 'nuke-delete-more' ); $this->getOutput()->addWikiMsg( 'nuke-delete-more' );
} }

View file

@ -9,6 +9,7 @@ use MediaWiki\Permissions\UltimateAuthority;
use MediaWiki\Request\FauxRequest; use MediaWiki\Request\FauxRequest;
use MediaWiki\Tests\User\TempUser\TempUserTestTrait; use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
use MediaWiki\Title\Title; use MediaWiki\Title\Title;
use MediaWiki\User\User;
use PermissionsError; use PermissionsError;
use SpecialPageTestBase; use SpecialPageTestBase;
use UploadFromFile; use UploadFromFile;
@ -653,7 +654,7 @@ class SpecialNukeTest extends SpecialPageTestBase {
} }
public function testListFiles() { public function testListFiles() {
$this->uploadTestFile(); $testFileName = $this->uploadTestFile()['title']->getPrefixedText();
$admin = $this->getTestSysop()->getUser(); $admin = $this->getTestSysop()->getUser();
$request = new FauxRequest( [ $request = new FauxRequest( [
@ -664,10 +665,8 @@ class SpecialNukeTest extends SpecialPageTestBase {
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer ); [ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$expectedTitle = Title::makeTitle( NS_FILE, 'Example.png' );
// The title should be in the list // The title should be in the list
$this->assertStringContainsString( $expectedTitle->getPrefixedText(), $html ); $this->assertStringContainsString( $testFileName, $html );
// There should also be an image preview // There should also be an image preview
$this->assertStringContainsString( "<img src", $html ); $this->assertStringContainsString( "<img src", $html );
} }
@ -737,6 +736,8 @@ class SpecialNukeTest extends SpecialPageTestBase {
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer ); [ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$deleteCount = count( $pages );
$this->assertStringContainsString( "(nuke-delete-summary: $deleteCount)", $html );
$this->assertStringContainsString( '(nuke-deletion-queued: Page123)', $html ); $this->assertStringContainsString( '(nuke-deletion-queued: Page123)', $html );
$this->assertStringContainsString( '(nuke-deletion-queued: Paging456)', $html ); $this->assertStringContainsString( '(nuke-deletion-queued: Paging456)', $html );
@ -753,6 +754,106 @@ class SpecialNukeTest extends SpecialPageTestBase {
$this->assertStringContainsString( $fauxReason, $deleteLogHtml ); $this->assertStringContainsString( $fauxReason, $deleteLogHtml );
} }
public function testDeleteTarget() {
$pages = [];
$testUser = $this->getTestUser( "user" )->getUser();
$testUserName = $testUser->getName();
$pages[] = $this->uploadTestFile( $testUser )[ 'title' ];
$pages[] = $this->insertPage( 'Page123', 'Test', NS_MAIN, $testUser )[ 'title' ];
$pages[] = $this->insertPage( 'Paging456', 'Test', NS_MAIN, $testUser )[ 'title' ];
$admin = $this->getTestSysop()->getUser();
$fauxReason = "Reason " . wfRandomString();
$request = new FauxRequest( [
'action' => 'delete',
'wpDeleteReasonList' => 'Vandalism',
'wpReason' => $fauxReason,
'target' => $testUserName,
'pages' => $pages,
'wpFormIdentifier' => 'nukelist',
'wpEditToken' => $admin->getEditToken(),
], true );
$performer = new UltimateAuthority( $admin );
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$deleteCount = count( $pages );
$this->assertStringContainsString(
"(nuke-delete-summary-user: $deleteCount, $testUserName)",
$html
);
}
public function testDeleteTargetAnon() {
$pages = [];
$testUser = $this->getTestUser( "user" )->getUser();
$testUserName = $testUser->getName();
$pages[] = $this->uploadTestFile( $testUser )[ 'title' ];
$pages[] = $this->insertPage( 'Page123', 'Test', NS_MAIN, $testUser )[ 'title' ];
$pages[] = $this->insertPage( 'Paging456', 'Test', NS_MAIN, $testUser )[ 'title' ];
$admin = $this->getTestSysop()->getUser();
$fauxReason = "Reason " . wfRandomString();
$request = new FauxRequest( [
'action' => 'delete',
'wpDeleteReasonList' => 'Vandalism',
'wpReason' => $fauxReason,
'pages' => $pages,
'wpFormIdentifier' => 'nukelist',
'wpEditToken' => $admin->getEditToken(),
], true );
$performer = new UltimateAuthority( $admin );
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$deleteCount = count( $pages );
$this->assertStringContainsString(
"(nuke-delete-summary: $deleteCount)",
$html
);
}
public function testDeleteSkipped() {
$pages = [];
$pages[] = $this->insertPage( 'Page123', 'Test', NS_MAIN )[ 'title' ];
$pages[] = $this->insertPage( 'Paging 456', 'Test', NS_MAIN )[ 'title' ];
$skippedPage = $this->insertPage( 'Page 789', 'Test', NS_MAIN )['title'];
$admin = $this->getTestSysop()->getUser();
$request = new FauxRequest( [
'action' => 'delete',
'wpDeleteReasonList' => 'Vandalism',
'originalPageList' => implode( '|', $pages ),
'pages' => [ $skippedPage->getPrefixedDBkey() ],
'wpFormIdentifier' => 'nukelist',
'wpEditToken' => $admin->getEditToken(),
], true );
$performer = new UltimateAuthority( $admin );
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$skippedCount = count( $pages );
$this->assertStringContainsString( '(nuke-delete-summary: 1)', $html );
$this->assertStringContainsString( "(nuke-skipped-summary: $skippedCount)", $html );
// Ensure all delete jobs are run
$this->runJobs();
// Make sure that those pages are not in the deletion log.
$deleteLogHtml = $this->getDeleteLogHtml();
foreach ( $pages as $title ) {
$this->assertStringNotContainsString( $title->getPrefixedText(), $deleteLogHtml );
}
$this->assertStringContainsString( $skippedPage->getPrefixedText(), $deleteLogHtml );
}
public function testDeleteDropdownReason() { public function testDeleteDropdownReason() {
$pages = []; $pages = [];
$pages[] = $this->insertPage( 'Page123', 'Test', NS_MAIN )[ 'title' ]; $pages[] = $this->insertPage( 'Page123', 'Test', NS_MAIN )[ 'title' ];
@ -807,14 +908,14 @@ class SpecialNukeTest extends SpecialPageTestBase {
} }
public function testDeleteFiles() { public function testDeleteFiles() {
$this->uploadTestFile(); $testFileName = $this->uploadTestFile()['title']->getPrefixedText();
$admin = $this->getTestSysop()->getUser(); $admin = $this->getTestSysop()->getUser();
$request = new FauxRequest( [ $request = new FauxRequest( [
'action' => 'delete', 'action' => 'delete',
'wpDeleteReasonList' => 'Reason', 'wpDeleteReasonList' => 'Reason',
'wpReason' => 'Reason', 'wpReason' => 'Reason',
'pages' => [ 'File:Example.png' ], 'pages' => [ $testFileName ],
'wpFormIdentifier' => 'nukelist', 'wpFormIdentifier' => 'nukelist',
'wpEditToken' => $admin->getEditToken(), 'wpEditToken' => $admin->getEditToken(),
], true ); ], true );
@ -822,11 +923,9 @@ class SpecialNukeTest extends SpecialPageTestBase {
[ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer ); [ $html ] = $this->executeSpecialPage( '', $request, 'qqx', $performer );
$expectedTitle = Title::makeTitle( NS_FILE, 'Example.png' );
// Files are deleted instantly // Files are deleted instantly
$this->assertStringContainsString( "nuke-deleted", $html ); $this->assertStringContainsString( "nuke-deleted", $html );
$this->assertStringContainsString( $expectedTitle->getPrefixedText(), $html ); $this->assertStringContainsString( $testFileName, $html );
} }
public function testDeleteHook() { public function testDeleteHook() {
@ -879,26 +978,40 @@ class SpecialNukeTest extends SpecialPageTestBase {
$this->assertCount( 0, $searchResults2 ); $this->assertCount( 0, $searchResults2 );
} }
private function uploadTestFile() { /**
* Upload a test file.
*
* @param ?User|null $user
* @return array Title object and page id
*/
private function uploadTestFile( ?User $user = null ): array {
$exampleFilePath = realpath( __DIR__ . "/../../assets/Example.png" ); $exampleFilePath = realpath( __DIR__ . "/../../assets/Example.png" );
$tempFilePath = $this->getNewTempFile(); $tempFilePath = $this->getNewTempFile();
copy( $exampleFilePath, $tempFilePath ); copy( $exampleFilePath, $tempFilePath );
$title = Title::makeTitle( NS_FILE, "Example " . rand() . ".png" );
$request = new FauxRequest( [], true ); $request = new FauxRequest( [], true );
$request->setUpload( 'wpUploadFile', [ $request->setUpload( 'wpUploadFile', [
'name' => 'Example.png', 'name' => $title->getText(),
'type' => 'image/png', 'type' => 'image/png',
'tmp_name' => $tempFilePath, 'tmp_name' => $tempFilePath,
'size' => filesize( $tempFilePath ), 'size' => filesize( $tempFilePath ),
'error' => UPLOAD_ERR_OK 'error' => UPLOAD_ERR_OK
] ); ] );
$upload = UploadFromFile::createFromRequest( $request ); $upload = UploadFromFile::createFromRequest( $request );
$upload->performUpload( $uploadStatus = $upload->performUpload(
"test", "test",
false, false,
false, false,
$this->getTestUser( "user" )->getUser() $user ?? $this->getTestUser( "user" )->getUser()
); );
$this->assertTrue( $uploadStatus->isOK() );
$this->getServiceContainer()->getJobRunner()->run( [] );
return [
'title' => $title,
'id' => $title->getId()
];
} }
private function getDeleteLogHtml(): string { private function getDeleteLogHtml(): string {