aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--autoload.php1
-rw-r--r--includes/DefaultSettings.php7
-rw-r--r--includes/jobqueue/jobs/DeletePageJob.php32
-rw-r--r--includes/page/Article.php34
-rw-r--r--includes/page/WikiPage.php310
-rw-r--r--includes/specials/SpecialMovepage.php10
-rw-r--r--languages/i18n/en.json2
-rw-r--r--languages/i18n/qqq.json2
-rw-r--r--maintenance/deleteBatch.php2
9 files changed, 297 insertions, 103 deletions
diff --git a/autoload.php b/autoload.php
index ac4709377651..6833fea7b6d4 100644
--- a/autoload.php
+++ b/autoload.php
@@ -384,6 +384,7 @@ $wgAutoloadLocalClasses = [
'DeleteLogFormatter' => __DIR__ . '/includes/logging/DeleteLogFormatter.php',
'DeleteOldRevisions' => __DIR__ . '/maintenance/deleteOldRevisions.php',
'DeleteOrphanedRevisions' => __DIR__ . '/maintenance/deleteOrphanedRevisions.php',
+ 'DeletePageJob' => __DIR__ . '/includes/jobqueue/jobs/DeletePageJob.php',
'DeleteSelfExternals' => __DIR__ . '/maintenance/deleteSelfExternals.php',
'DeletedContribsPager' => __DIR__ . '/includes/specials/pagers/DeletedContribsPager.php',
'DeletedContributionsPage' => __DIR__ . '/includes/specials/SpecialDeletedContributions.php',
diff --git a/includes/DefaultSettings.php b/includes/DefaultSettings.php
index 4fee5d7f75f9..4ae3fe543052 100644
--- a/includes/DefaultSettings.php
+++ b/includes/DefaultSettings.php
@@ -5535,6 +5535,12 @@ $wgAvailableRights = [];
$wgDeleteRevisionsLimit = 0;
/**
+ * Page deletions with > this number of revisions will use the job queue.
+ * Revisions will be archived in batches of (at most) this size, one batch per job.
+ */
+$wgDeleteRevisionsBatchSize = 1000;
+
+/**
* The maximum number of edits a user can have and
* can still be hidden by users with the hideuser permission.
* This is limited for performance reason.
@@ -7518,6 +7524,7 @@ $wgServiceWiringFiles = [
* or (since 1.30) a callback to use for creating the job object.
*/
$wgJobClasses = [
+ 'deletePage' => DeletePageJob::class,
'refreshLinks' => RefreshLinksJob::class,
'deleteLinks' => DeleteLinksJob::class,
'htmlCacheUpdate' => HTMLCacheUpdateJob::class,
diff --git a/includes/jobqueue/jobs/DeletePageJob.php b/includes/jobqueue/jobs/DeletePageJob.php
new file mode 100644
index 000000000000..9b5cef44eaa5
--- /dev/null
+++ b/includes/jobqueue/jobs/DeletePageJob.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * Class DeletePageJob
+ */
+class DeletePageJob extends Job {
+ public function __construct( $title, $params ) {
+ parent::__construct( 'deletePage', $title, $params );
+ }
+
+ /**
+ * Execute the job
+ *
+ * @return bool
+ */
+ public function run() {
+ // Failure to load the page is not job failure.
+ // A parallel deletion operation may have already completed the page deletion.
+ $wikiPage = WikiPage::newFromID( $this->params['wikiPageId'] );
+ if ( $wikiPage ) {
+ $wikiPage->doDeleteArticleBatched(
+ $this->params['reason'],
+ $this->params['suppress'],
+ User::newFromId( $this->params['userId'] ),
+ json_decode( $this->params['tags'] ),
+ $this->params['logsubtype'],
+ false,
+ $this->getRequestId() );
+ }
+ return true;
+ }
+}
diff --git a/includes/page/Article.php b/includes/page/Article.php
index 4a689d318a8d..db96cf48b905 100644
--- a/includes/page/Article.php
+++ b/includes/page/Article.php
@@ -2053,25 +2053,31 @@ class Article implements Page {
* Perform a deletion and output success or failure messages
* @param string $reason
* @param bool $suppress
+ * @param bool $immediate false allows deleting over time via the job queue
+ * @throws FatalError
+ * @throws MWException
*/
- public function doDelete( $reason, $suppress = false ) {
+ public function doDelete( $reason, $suppress = false, $immediate = false ) {
$error = '';
$context = $this->getContext();
$outputPage = $context->getOutput();
$user = $context->getUser();
- $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user );
+ $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user,
+ [], 'delete', $immediate );
- if ( $status->isGood() ) {
+ if ( $status->isOK() ) {
$deleted = $this->getTitle()->getPrefixedText();
$outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
$outputPage->setRobotPolicy( 'noindex,nofollow' );
- $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
-
- $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
-
- Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+ if ( $status->isGood() ) {
+ $loglink = '[[Special:Log/delete|' . wfMessage( 'deletionlog' )->text() . ']]';
+ $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
+ Hooks::run( 'ArticleDeleteAfterSuccess', [ $this->getTitle(), $outputPage ] );
+ } else {
+ $outputPage->addWikiMsg( 'delete-scheduled', wfEscapeWikiText( $deleted ) );
+ }
$outputPage->returnToMain( false );
} else {
@@ -2297,10 +2303,10 @@ class Article implements Page {
*/
public function doDeleteArticleReal(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
- $tags = []
+ $tags = [], $immediate = false
) {
return $this->mPage->doDeleteArticleReal(
- $reason, $suppress, $u1, $u2, $error, $user, $tags
+ $reason, $suppress, $u1, $u2, $error, $user, $tags, 'delete', $immediate
);
}
@@ -2826,12 +2832,16 @@ class Article implements Page {
* @param int|null $u1 Unused
* @param bool|null $u2 Unused
* @param string &$error
+ * @param bool $immediate false allows deleting over time via the job queue
* @return bool
+ * @throws FatalError
+ * @throws MWException
*/
public function doDeleteArticle(
- $reason, $suppress = false, $u1 = null, $u2 = null, &$error = ''
+ $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', $immediate = false
) {
- return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error );
+ return $this->mPage->doDeleteArticle( $reason, $suppress, $u1, $u2, $error,
+ null, $immediate );
}
/**
diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php
index 7c97465389bb..7c0450de8ba1 100644
--- a/includes/page/WikiPage.php
+++ b/includes/page/WikiPage.php
@@ -2513,6 +2513,28 @@ class WikiPage implements Page, IDBAccessObject {
}
/**
+ * Determines if deletion of this page would be batched (executed over time by the job queue)
+ * or not (completed in the same request as the delete call).
+ *
+ * It is unlikely but possible that an edit from another request could push the page over the
+ * batching threshold after this function is called, but before the caller acts upon the
+ * return value. Callers must decide for themselves how to deal with this. $safetyMargin
+ * is provided as an unreliable but situationally useful help for some common cases.
+ *
+ * @param int $safetyMargin Added to the revision count when checking for batching
+ * @return bool True if deletion would be batched, false otherwise
+ */
+ public function isBatchedDelete( $safetyMargin = 0 ) {
+ global $wgDeleteRevisionsBatchSize;
+
+ $dbr = wfGetDB( DB_REPLICA );
+ $revCount = $this->getRevisionStore()->countRevisionsByPageId( $dbr, $this->getId() );
+ $revCount += $safetyMargin;
+
+ return $revCount >= $wgDeleteRevisionsBatchSize;
+ }
+
+ /**
* Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
* backwards compatibility, if you care about error reporting you should use
* doDeleteArticleReal() instead.
@@ -2526,13 +2548,20 @@ class WikiPage implements Page, IDBAccessObject {
* @param bool|null $u2 Unused
* @param array|string &$error Array of errors to append to
* @param User|null $user The deleting user
+ * @param bool $immediate false allows deleting over time via the job queue
* @return bool True if successful
+ * @throws FatalError
+ * @throws MWException
*/
public function doDeleteArticle(
- $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
+ $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null,
+ $immediate = false
) {
- $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
- return $status->isGood();
+ $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user,
+ [], 'delete', $immediate );
+
+ // Returns true if the page was actually deleted, or is scheduled for deletion
+ return $status->isOK();
}
/**
@@ -2550,27 +2579,23 @@ class WikiPage implements Page, IDBAccessObject {
* @param User|null $deleter The deleting user
* @param array $tags Tags to apply to the deletion action
* @param string $logsubtype
+ * @param bool $immediate false allows deleting over time via the job queue
* @return Status Status object; if successful, $status->value is the log_id of the
* deletion log entry. If the page couldn't be deleted because it wasn't
* found, $status is a non-fatal 'cannotdelete' error
+ * @throws FatalError
+ * @throws MWException
*/
public function doDeleteArticleReal(
$reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $deleter = null,
- $tags = [], $logsubtype = 'delete'
+ $tags = [], $logsubtype = 'delete', $immediate = false
) {
- global $wgUser, $wgContentHandlerUseDB, $wgCommentTableSchemaMigrationStage,
- $wgActorTableSchemaMigrationStage, $wgMultiContentRevisionSchemaMigrationStage;
+ global $wgUser;
wfDebug( __METHOD__ . "\n" );
$status = Status::newGood();
- if ( $this->mTitle->getDBkey() === '' ) {
- $status->error( 'cannotdelete',
- wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
- return $status;
- }
-
// Avoid PHP 7.1 warning of passing $this by reference
$wikiPage = $this;
@@ -2585,6 +2610,26 @@ class WikiPage implements Page, IDBAccessObject {
return $status;
}
+ return $this->doDeleteArticleBatched( $reason, $suppress, $deleter, $tags,
+ $logsubtype, $immediate );
+ }
+
+ /**
+ * Back-end article deletion
+ *
+ * Only invokes batching via the job queue if necessary per $wgDeleteRevisionsBatchSize.
+ * Deletions can often be completed inline without involving the job queue.
+ *
+ * Potentially called many times per deletion operation for pages with many revisions.
+ */
+ public function doDeleteArticleBatched(
+ $reason, $suppress, User $deleter, $tags,
+ $logsubtype, $immediate = false, $webRequestId = null
+ ) {
+ wfDebug( __METHOD__ . "\n" );
+
+ $status = Status::newGood();
+
$dbw = wfGetDB( DB_MASTER );
$dbw->startAtomic( __METHOD__ );
@@ -2603,11 +2648,7 @@ class WikiPage implements Page, IDBAccessObject {
return $status;
}
- // Given the lock above, we can be confident in the title and page ID values
- $namespace = $this->getTitle()->getNamespace();
- $dbKey = $this->getTitle()->getDBkey();
-
- // At this point we are now comitted to returning an OK
+ // At this point we are now committed to returning an OK
// status unless some DB query error or other exception comes up.
// This way callers don't have to call rollback() if $status is bad
// unless they actually try to catch exceptions (which is rare).
@@ -2623,6 +2664,133 @@ class WikiPage implements Page, IDBAccessObject {
$content = null;
}
+ // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
+ // one batch of revisions and defer archival of any others to the job queue.
+ $explictTrxLogged = false;
+ while ( true ) {
+ $done = $this->archiveRevisions( $dbw, $id, $suppress );
+ if ( $done || !$immediate ) {
+ break;
+ }
+ $dbw->endAtomic( __METHOD__ );
+ if ( $dbw->explicitTrxActive() ) {
+ // Explict transactions may never happen here in practice. Log to be sure.
+ if ( !$explictTrxLogged ) {
+ $explictTrxLogged = true;
+ LoggerFactory::getInstance( 'wfDebug' )->debug(
+ 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
+ 'title' => $this->getTitle()->getText(),
+ ] );
+ }
+ continue;
+ }
+ if ( $dbw->trxLevel() ) {
+ $dbw->commit();
+ }
+ $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory->waitForReplication();
+ $dbw->startAtomic( __METHOD__ );
+ }
+
+ // If done archiving, also delete the article.
+ if ( !$done ) {
+ $dbw->endAtomic( __METHOD__ );
+
+ $jobParams = [
+ 'wikiPageId' => $id,
+ 'requestId' => $webRequestId ?? WebRequest::getRequestId(),
+ 'reason' => $reason,
+ 'suppress' => $suppress,
+ 'userId' => $deleter->getId(),
+ 'tags' => json_encode( $tags ),
+ 'logsubtype' => $logsubtype,
+ ];
+
+ $job = new DeletePageJob( $this->getTitle(), $jobParams );
+ JobQueueGroup::singleton()->push( $job );
+
+ $status->warning( 'delete-scheduled',
+ wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+ } else {
+ // Get archivedRevisionCount by db query, because there's no better alternative.
+ // Jobs cannot pass a count of archived revisions to the next job, because additional
+ // deletion operations can be started while the first is running. Jobs from each
+ // gracefully interleave, but would not know about each other's count. Deduplication
+ // in the job queue to avoid simultaneous deletion operations would add overhead.
+ // Number of archived revisions cannot be known beforehand, because edits can be made
+ // while deletion operations are being processed, changing the number of archivals.
+ $archivedRevisionCount = $dbw->selectRowCount(
+ 'archive', '1', [ 'ar_page_id' => $id ], __METHOD__
+ );
+
+ // Clone the title and wikiPage, so we have the information we need when
+ // we log and run the ArticleDeleteComplete hook.
+ $logTitle = clone $this->mTitle;
+ $wikiPageBeforeDelete = clone $this;
+
+ // Now that it's safely backed up, delete it
+ $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
+
+ // Log the deletion, if the page was suppressed, put it in the suppression log instead
+ $logtype = $suppress ? 'suppress' : 'delete';
+
+ $logEntry = new ManualLogEntry( $logtype, $logsubtype );
+ $logEntry->setPerformer( $deleter );
+ $logEntry->setTarget( $logTitle );
+ $logEntry->setComment( $reason );
+ $logEntry->setTags( $tags );
+ $logid = $logEntry->insert();
+
+ $dbw->onTransactionPreCommitOrIdle(
+ function () use ( $logEntry, $logid ) {
+ // T58776: avoid deadlocks (especially from FileDeleteForm)
+ $logEntry->publish( $logid );
+ },
+ __METHOD__
+ );
+
+ $dbw->endAtomic( __METHOD__ );
+
+ $this->doDeleteUpdates( $id, $content, $revision, $deleter );
+
+ Hooks::run( 'ArticleDeleteComplete', [
+ &$wikiPageBeforeDelete,
+ &$deleter,
+ $reason,
+ $id,
+ $content,
+ $logEntry,
+ $archivedRevisionCount
+ ] );
+ $status->value = $logid;
+
+ // Show log excerpt on 404 pages rather than just a link
+ $cache = MediaWikiServices::getInstance()->getMainObjectStash();
+ $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+ $cache->set( $key, 1, $cache::TTL_DAY );
+ }
+
+ return $status;
+ }
+
+ /**
+ * Archives revisions as part of page deletion.
+ *
+ * @param IDatabase $dbw
+ * @param int $id
+ * @param bool $suppress Suppress all revisions and log the deletion in
+ * the suppression log instead of the deletion log
+ * @return bool
+ */
+ protected function archiveRevisions( $dbw, $id, $suppress ) {
+ global $wgContentHandlerUseDB, $wgMultiContentRevisionSchemaMigrationStage,
+ $wgCommentTableSchemaMigrationStage, $wgActorTableSchemaMigrationStage,
+ $wgDeleteRevisionsBatchSize;
+
+ // Given the lock above, we can be confident in the title and page ID values
+ $namespace = $this->getTitle()->getNamespace();
+ $dbKey = $this->getTitle()->getDBkey();
+
$commentStore = CommentStore::getStore();
$actorMigration = ActorMigration::newMigration();
@@ -2669,13 +2837,14 @@ class WikiPage implements Page, IDBAccessObject {
}
}
- // Get all of the page revisions
+ // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
+ // unusual case where there were exactly $wgDeleteRevisionBatchSize revisions remaining.
$res = $dbw->select(
$revQuery['tables'],
$revQuery['fields'],
[ 'rev_page' => $id ],
__METHOD__,
- [],
+ [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => $wgDeleteRevisionsBatchSize + 1 ],
$revQuery['joins']
);
@@ -2686,16 +2855,22 @@ class WikiPage implements Page, IDBAccessObject {
/** @var int[] Revision IDs of edits that were made by IPs */
$ipRevIds = [];
+ $done = true;
foreach ( $res as $row ) {
+ if ( count( $revids ) >= $wgDeleteRevisionsBatchSize ) {
+ $done = false;
+ break;
+ }
+
$comment = $commentStore->getComment( 'rev_comment', $row );
$user = User::newFromAnyId( $row->rev_user, $row->rev_user_text, $row->rev_actor );
$rowInsert = [
- 'ar_namespace' => $namespace,
- 'ar_title' => $dbKey,
- 'ar_timestamp' => $row->rev_timestamp,
- 'ar_minor_edit' => $row->rev_minor_edit,
- 'ar_rev_id' => $row->rev_id,
- 'ar_parent_id' => $row->rev_parent_id,
+ 'ar_namespace' => $namespace,
+ 'ar_title' => $dbKey,
+ 'ar_timestamp' => $row->rev_timestamp,
+ 'ar_minor_edit' => $row->rev_minor_edit,
+ 'ar_rev_id' => $row->rev_id,
+ 'ar_parent_id' => $row->rev_parent_id,
/**
* ar_text_id should probably not be written to when the multi content schema has
* been migrated to (wgMultiContentRevisionSchemaMigrationStage) however there is no
@@ -2704,11 +2879,11 @@ class WikiPage implements Page, IDBAccessObject {
* Task: https://phabricator.wikimedia.org/T190148
* Copying the value from the revision table should not lead to any issues for now.
*/
- 'ar_len' => $row->rev_len,
- 'ar_page_id' => $id,
- 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
- 'ar_sha1' => $row->rev_sha1,
- ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ 'ar_len' => $row->rev_len,
+ 'ar_page_id' => $id,
+ 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
+ 'ar_sha1' => $row->rev_sha1,
+ ] + $commentStore->insert( $dbw, 'ar_comment', $comment )
+ $actorMigration->getInsertValues( $dbw, 'ar_user', $user );
if ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_OLD ) {
@@ -2729,70 +2904,27 @@ class WikiPage implements Page, IDBAccessObject {
$ipRevIds[] = $row->rev_id;
}
}
- // Copy them into the archive table
- $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
- // Save this so we can pass it to the ArticleDeleteComplete hook.
- $archivedRevisionCount = $dbw->affectedRows();
- // Clone the title and wikiPage, so we have the information we need when
- // we log and run the ArticleDeleteComplete hook.
- $logTitle = clone $this->mTitle;
- $wikiPageBeforeDelete = clone $this;
+ // This conditional is just a sanity check
+ if ( count( $revids ) > 0 ) {
+ // Copy them into the archive table
+ $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
- // Now that it's safely backed up, delete it
- $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
- $dbw->delete( 'revision', [ 'rev_page' => $id ], __METHOD__ );
- if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
- $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
- }
- if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
- $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
- }
+ $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
+ if ( $wgCommentTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
+ }
+ if ( $wgActorTableSchemaMigrationStage > MIGRATION_OLD ) {
+ $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
+ }
- // Also delete records from ip_changes as applicable.
- if ( count( $ipRevIds ) > 0 ) {
- $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+ // Also delete records from ip_changes as applicable.
+ if ( count( $ipRevIds ) > 0 ) {
+ $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
+ }
}
- // Log the deletion, if the page was suppressed, put it in the suppression log instead
- $logtype = $suppress ? 'suppress' : 'delete';
-
- $logEntry = new ManualLogEntry( $logtype, $logsubtype );
- $logEntry->setPerformer( $deleter );
- $logEntry->setTarget( $logTitle );
- $logEntry->setComment( $reason );
- $logEntry->setTags( $tags );
- $logid = $logEntry->insert();
-
- $dbw->onTransactionPreCommitOrIdle(
- function () use ( $logEntry, $logid ) {
- // T58776: avoid deadlocks (especially from FileDeleteForm)
- $logEntry->publish( $logid );
- },
- __METHOD__
- );
-
- $dbw->endAtomic( __METHOD__ );
-
- $this->doDeleteUpdates( $id, $content, $revision, $deleter );
-
- Hooks::run( 'ArticleDeleteComplete', [
- &$wikiPageBeforeDelete,
- &$deleter,
- $reason,
- $id,
- $content,
- $logEntry,
- $archivedRevisionCount
- ] );
- $status->value = $logid;
-
- // Show log excerpt on 404 pages rather than just a link
- $cache = MediaWikiServices::getInstance()->getMainObjectStash();
- $key = $cache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
- $cache->set( $key, 1, $cache::TTL_DAY );
-
- return $status;
+ return $done;
}
/**
diff --git a/includes/specials/SpecialMovepage.php b/includes/specials/SpecialMovepage.php
index 464be4faca9a..2f6dc03b1096 100644
--- a/includes/specials/SpecialMovepage.php
+++ b/includes/specials/SpecialMovepage.php
@@ -547,6 +547,15 @@ class MovePageForm extends UnlistedSpecialPage {
return;
}
+ $page = WikiPage::factory( $nt );
+
+ // Small safety margin to guard against concurrent edits
+ if ( $page->isBatchedDelete( 5 ) ) {
+ $this->showForm( [ [ 'movepage-delete-first' ] ] );
+
+ return;
+ }
+
$reason = $this->msg( 'delete_and_move_reason', $ot )->inContentLanguage()->text();
// Delete an associated image if there is
@@ -559,7 +568,6 @@ class MovePageForm extends UnlistedSpecialPage {
}
$error = ''; // passed by ref
- $page = WikiPage::factory( $nt );
$deleteStatus = $page->doDeleteArticleReal( $reason, false, 0, true, $error, $user );
if ( !$deleteStatus->isGood() ) {
$this->showForm( $deleteStatus->getErrorsArray() );
diff --git a/languages/i18n/en.json b/languages/i18n/en.json
index 1d7f3f53c690..ea6305423b61 100644
--- a/languages/i18n/en.json
+++ b/languages/i18n/en.json
@@ -334,6 +334,7 @@
"badarticleerror": "This action cannot be performed on this page.",
"cannotdelete": "The page or file \"$1\" could not be deleted.\nIt may have already been deleted by someone else.",
"cannotdelete-title": "Cannot delete page \"$1\"",
+ "delete-scheduled": "The page \"$1\" is scheduled for deletion.\nPlease be patient.",
"delete-hook-aborted": "Deletion aborted by hook.\nIt gave no explanation.",
"no-null-revision": "Could not create new null revision for page \"$1\"",
"badtitle": "Bad title",
@@ -2729,6 +2730,7 @@
"movepage-moved": "<strong>\"$1\" has been moved to \"$2\"</strong>",
"movepage-moved-redirect": "A redirect has been created.",
"movepage-moved-noredirect": "The creation of a redirect has been suppressed.",
+ "movepage-delete-first": "The target page has too many revisions to delete as part of a page move. Please first delete the page manually, then try again.",
"articleexists": "A page of that name already exists, or the name you have chosen is not valid.\nPlease choose another name.",
"cantmove-titleprotected": "You cannot move a page to this location because the new title has been protected from creation.",
"movetalk": "Move associated talk page",
diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json
index c4585f39b21e..2ca765fecee2 100644
--- a/languages/i18n/qqq.json
+++ b/languages/i18n/qqq.json
@@ -536,6 +536,7 @@
"badarticleerror": "Used as error message in moving page.\n\nSee also:\n* {{msg-mw|Articleexists}}\n* {{msg-mw|Bad-target-model}}",
"cannotdelete": "Error message in deleting. Parameters:\n* $1 - page name or file name",
"cannotdelete-title": "Title of error page when the user cannot delete a page. Parameters:\n* $1 - the page name",
+ "delete-scheduled": "Warning message shown when page deletion is deferred to the job queue, and therefore is not immediate.",
"delete-hook-aborted": "Error message shown when an extension hook prevents a page deletion, but does not provide an error message.",
"no-null-revision": "Error message shown when no null revision could be created to reflect a protection level change.\n\nAbout \"null revision\":\n* Create a new null-revision for insertion into a page's history. This will not re-save the text, but simply refer to the text from the previous version.\n* Such revisions can for instance identify page rename operations and other such meta-modifications.\n\nParameters:\n* $1 - page title",
"badtitle": "The page title when a user requested a page with invalid page name. The content will be {{msg-mw|badtitletext}}.",
@@ -2931,6 +2932,7 @@
"movepage-moved": "Message displayed after successfully moving a page from source to target name.\n\nParameters:\n* $1 - the source page as a link with display name\n* $2 - the target page as a link with display name\n* $3 - (optional) the source page name without a link\n* $4 - (optional) the target page name without a link\nSee also:\n* {{msg-mw|Movepage-moved-redirect}}\n* {{msg-mw|Movepage-moved-noredirect}}",
"movepage-moved-redirect": "See also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-noredirect}}",
"movepage-moved-noredirect": "The message is shown after pagemove if checkbox \"{{int:move-leave-redirect}}\" was unselected before moving.\n\nSee also:\n* {{msg-mw|Movepage-moved}}\n* {{msg-mw|Movepage-moved-redirect}}",
+ "movepage-delete-first": "Error message shown when trying to move a page and delete the existing page by that name, but the existing page has too many revisions.",
"articleexists": "Used as error message when moving a page.\n\nSee also:\n* {{msg-mw|Badarticleerror}}\n* {{msg-mw|Bad-target-model}}",
"cantmove-titleprotected": "Used as error message when moving a page.",
"movetalk": "The text of the checkbox to watch the associated talk page to the page you are moving. This only appears when the talk page is not empty. Used in [[Special:MovePage]].\n\nSee also:\n* {{msg-mw|Move-page-legend|legend for the form}}\n* {{msg-mw|newtitle|label for new title}}\n* {{msg-mw|Movereason|label for textarea}}\n* {{msg-mw|Move-leave-redirect|label for checkbox}}\n* {{msg-mw|Fix-double-redirects|label for checkbox}}\n* {{msg-mw|Move-subpages|label for checkbox}}\n* {{msg-mw|Move-talk-subpages|label for checkbox}}\n* {{msg-mw|Move-watch|label for checkbox}}",
diff --git a/maintenance/deleteBatch.php b/maintenance/deleteBatch.php
index 0f3c506734e4..9e35687dcf64 100644
--- a/maintenance/deleteBatch.php
+++ b/maintenance/deleteBatch.php
@@ -108,7 +108,7 @@ class DeleteBatch extends Maintenance {
}
$page = WikiPage::factory( $title );
$error = '';
- $success = $page->doDeleteArticle( $reason, false, 0, true, $error, $user );
+ $success = $page->doDeleteArticle( $reason, false, null, null, $error, $user, true );
if ( $success ) {
$this->output( " Deleted!\n" );
} else {