diff options
-rw-r--r-- | autoload.php | 1 | ||||
-rw-r--r-- | includes/DefaultSettings.php | 7 | ||||
-rw-r--r-- | includes/jobqueue/jobs/DeletePageJob.php | 32 | ||||
-rw-r--r-- | includes/page/Article.php | 34 | ||||
-rw-r--r-- | includes/page/WikiPage.php | 310 | ||||
-rw-r--r-- | includes/specials/SpecialMovepage.php | 10 | ||||
-rw-r--r-- | languages/i18n/en.json | 2 | ||||
-rw-r--r-- | languages/i18n/qqq.json | 2 | ||||
-rw-r--r-- | maintenance/deleteBatch.php | 2 |
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 { |