68 private $isDeletePageUnitTest =
false;
70 private $suppress =
false;
74 private $logSubtype =
'delete';
76 private $forceImmediate =
false;
78 private $associatedTalk;
81 private $legacyHookErrors =
'';
83 private $mergeLegacyHookErrors =
true;
89 private $successfulDeletionsIDs;
94 private $wasScheduled;
96 private $attemptedDeletion =
false;
105 private string $localWikiID;
106 private string $webRequestID;
128 string $webRequestID,
138 $this->hookRunner =
new HookRunner( $hookContainer );
139 $this->revisionStore = $revisionStore;
140 $this->lbFactory = $lbFactory;
141 $this->jobQueueGroup = $jobQueueGroup;
142 $this->commentStore = $commentStore;
144 $this->options = $serviceOptions;
145 $this->recentDeletesCache = $recentDeletesCache;
146 $this->localWikiID = $localWikiID;
147 $this->webRequestID = $webRequestID;
148 $this->wikiPageFactory = $wikiPageFactory;
149 $this->userFactory = $userFactory;
150 $this->backlinkCacheFactory = $backlinkCacheFactory;
151 $this->namespaceInfo = $namespaceInfo;
152 $this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
155 $this->deleter = $deleter;
156 $this->redirectStore = $redirectStore;
164 return $this->legacyHookErrors;
172 $this->mergeLegacyHookErrors = false;
184 $this->suppress = $suppress;
194 public function setTags( array $tags ): self {
206 $this->logSubtype = $logSubtype;
217 $this->forceImmediate = $forceImmediate;
226 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
227 return StatusValue::newFatal(
'delete-error-associated-alreadytalk' );
230 $talkPage = $this->wikiPageFactory->newFromLinkTarget(
231 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
233 if ( !$talkPage->exists() ) {
234 return StatusValue::newFatal(
'delete-error-associated-doesnotexist' );
236 return StatusValue::newGood();
252 $this->associatedTalk =
null;
256 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
257 throw new BadMethodCallException(
"Cannot delete associated talk page of a talk page! ($this->page)" );
260 $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
261 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
272 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
273 throw new LogicException( __METHOD__ .
' can only be used in tests!' );
275 $this->isDeletePageUnitTest = $test;
284 $this->attemptedDeletion = true;
285 $this->successfulDeletionsIDs = [ self::PAGE_BASE => null ];
286 $this->wasScheduled = [ self::PAGE_BASE => null ];
287 if ( $this->associatedTalk ) {
288 $this->successfulDeletionsIDs[self::PAGE_TALK] =
null;
289 $this->wasScheduled[self::PAGE_TALK] =
null;
298 private function assertDeletionAttempted(): void {
299 if ( !$this->attemptedDeletion ) {
300 throw new BadMethodCallException(
'No deletion was attempted' );
309 $this->assertDeletionAttempted();
310 return $this->successfulDeletionsIDs;
318 $this->assertDeletionAttempted();
319 return $this->wasScheduled;
329 $this->setDeletionAttempted();
330 $status = $this->authorizeDeletion();
331 if ( !$status->isGood() ) {
335 return $this->deleteUnsafe( $reason );
338 private function authorizeDeletion(): PermissionStatus {
339 $status = PermissionStatus::newEmpty();
340 $this->deleter->authorizeWrite(
'delete', $this->page, $status );
341 if ( $this->associatedTalk ) {
342 $this->deleter->authorizeWrite(
'delete', $this->associatedTalk, $status );
344 if ( !$this->deleter->isAllowed(
'bigdelete' ) && $this->isBigDeletion() ) {
346 'delete-toomanyrevisions',
347 Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
356 private function isBigDeletion(): bool {
357 $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
362 $dbr = $this->lbFactory->getReplicaDatabase();
363 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
364 if ( $this->associatedTalk ) {
365 $revCount += $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
368 return $revCount > $revLimit;
384 $dbr = $this->lbFactory->getReplicaDatabase();
385 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
386 $revCount += $safetyMargin;
388 if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
390 } elseif ( !$this->associatedTalk ) {
394 $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
395 $talkRevCount += $safetyMargin;
397 return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
411 $this->setDeletionAttempted();
412 $origReason = $reason;
413 $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
414 if ( !$hookStatus->isGood() ) {
417 if ( $this->associatedTalk ) {
418 $talkReason = $this->contLangMsgTextFormatter->format(
419 MessageValue::new(
'delete-talk-summary-prefix' )->plaintextParams( $origReason )
421 $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
422 if ( !$talkHookStatus->isGood() ) {
423 return $talkHookStatus;
427 $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
428 if ( !$this->associatedTalk || !$status->isGood() ) {
435 $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
444 private function runPreDeleteHooks(
WikiPage $page,
string &$reason ): Status {
445 $status = Status::newGood();
447 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
448 if ( !$this->hookRunner->onArticleDelete(
449 $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
451 if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !==
'' ) {
452 if ( is_string( $this->legacyHookErrors ) ) {
453 $this->legacyHookErrors = [ $this->legacyHookErrors ];
455 foreach ( $this->legacyHookErrors as $legacyError ) {
456 $status->fatal(
new RawMessage( $legacyError ) );
459 if ( $status->isOK() ) {
461 $status->fatal(
'delete-hook-aborted' );
467 $status = Status::newGood();
468 $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
469 if ( !$hookRes && !$status->isGood() ) {
473 return Status::newGood();
495 ?
string $webRequestId =
null,
499 $status = Status::newGood();
501 $dbw = $this->lbFactory->getPrimaryDatabase();
502 $dbw->startAtomic( __METHOD__ );
505 $id = $page->
getId();
511 if ( $id === 0 || $page->
getLatest() !== $lockedLatest ) {
512 $dbw->endAtomic( __METHOD__ );
514 $status->error(
'cannotdelete',
wfEscapeWikiText( $title->getPrefixedText() ) );
525 if ( !$revisionRecord ) {
526 throw new LogicException(
"No revisions for $page?" );
529 $content = $page->
getContent( RevisionRecord::RAW );
530 }
catch ( TimeoutException $e ) {
532 }
catch ( Exception $ex ) {
533 wfLogWarning( __METHOD__ .
': failed to load content during deletion! '
534 . $ex->getMessage() );
542 $done = $this->archiveRevisions( $page, $id );
543 if ( $done || !$this->forceImmediate ) {
546 $dbw->endAtomic( __METHOD__ );
547 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
548 $dbw->startAtomic( __METHOD__ );
552 $dbw->endAtomic( __METHOD__ );
555 'namespace' => $title->getNamespace(),
556 'title' => $title->getDBkey(),
558 'requestId' => $webRequestId ?? $this->webRequestID,
560 'suppress' => $this->suppress,
561 'userId' => $this->deleter->getUser()->getId(),
562 'tags' => json_encode( $this->tags ),
563 'logsubtype' => $this->logSubtype,
564 'pageRole' => $pageRole,
568 $this->jobQueueGroup->push(
$job );
569 $this->wasScheduled[$pageRole] =
true;
572 $this->wasScheduled[$pageRole] =
false;
581 $archivedRevisionCount = $dbw->newSelectQueryBuilder()
585 'ar_namespace' => $title->getNamespace(),
586 'ar_title' => $title->getDBkey(),
589 ->caller( __METHOD__ )->fetchRowCount();
597 $logTitle = clone $title;
598 $wikiPageBeforeDelete = clone $page;
601 $dbw->newDeleteQueryBuilder()
602 ->deleteFrom(
'page' )
603 ->where( [
'page_id' => $id ] )
604 ->caller( __METHOD__ )->execute();
607 $logtype = $this->suppress ?
'suppress' :
'delete';
610 $logEntry->setPerformer( $this->deleter->getUser() );
611 $logEntry->setTarget( $logTitle );
612 $logEntry->setComment( $reason );
613 $logEntry->addTags( $this->tags );
614 if ( !$this->isDeletePageUnitTest ) {
616 $logid = $logEntry->insert();
618 $dbw->onTransactionPreCommitOrIdle(
619 static function () use ( $logEntry, $logid ) {
621 $logEntry->publish( $logid );
629 $dbw->endAtomic( __METHOD__ );
631 $this->doDeleteUpdates( $page, $revisionRecord );
634 $page->loadFromRow(
false, IDBAccessObject::READ_LATEST );
637 Title::clearCaches();
639 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
640 $this->hookRunner->onArticleDeleteComplete(
641 $wikiPageBeforeDelete,
647 $archivedRevisionCount
649 $this->hookRunner->onPageDeleteComplete(
650 $wikiPageBeforeDelete,
656 $archivedRevisionCount
658 $this->successfulDeletionsIDs[$pageRole] = $logid;
661 $this->redirectStore->clearCache( $page );
664 $key = $this->recentDeletesCache->makeKey(
'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
665 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
677 private function archiveRevisions(
WikiPage $page,
int $id ): bool {
679 $namespace = $page->
getTitle()->getNamespace();
680 $dbKey = $page->
getTitle()->getDBkey();
682 $dbw = $this->lbFactory->getPrimaryDatabase();
684 $revQuery = $this->revisionStore->getQueryInfo();
688 if ( $this->suppress ) {
689 $bitfield = RevisionRecord::SUPPRESSED_ALL;
690 $revQuery[
'fields'] = array_diff( $revQuery[
'fields'], [
'rev_deleted' ] );
704 $lockQuery = $revQuery;
705 $lockQuery[
'tables'] = array_intersect(
707 [
'revision',
'revision_comment_temp' ]
709 unset( $lockQuery[
'fields'] );
710 $dbw->newSelectQueryBuilder()
711 ->queryInfo( $lockQuery )
712 ->where( [
'rev_page' => $id ] )
714 ->caller( __METHOD__ )
717 $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
720 $res = $dbw->newSelectQueryBuilder()
721 ->queryInfo( $revQuery )
722 ->where( [
'rev_page' => $id ] )
723 ->orderBy( [
'rev_timestamp',
'rev_id' ] )
724 ->limit( $deleteBatchSize + 1 )
725 ->caller( __METHOD__ )
736 foreach ( $res as $row ) {
737 if ( count( $revids ) >= $deleteBatchSize ) {
742 $comment = $this->commentStore->getComment(
'rev_comment', $row );
744 'ar_namespace' => $namespace,
745 'ar_title' => $dbKey,
746 'ar_actor' => $row->rev_actor,
747 'ar_timestamp' => $row->rev_timestamp,
748 'ar_minor_edit' => $row->rev_minor_edit,
749 'ar_rev_id' => $row->rev_id,
750 'ar_parent_id' => $row->rev_parent_id,
751 'ar_len' => $row->rev_len,
753 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
754 'ar_sha1' => $row->rev_sha1,
755 ] + $this->commentStore->insert( $dbw,
'ar_comment', $comment );
757 $rowsInsert[] = $rowInsert;
758 $revids[] = $row->rev_id;
762 if ( (
int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
763 $ipRevIds[] = $row->rev_id;
767 if ( count( $revids ) > 0 ) {
769 $dbw->newInsertQueryBuilder()
770 ->insertInto(
'archive' )
771 ->rows( $rowsInsert )
772 ->caller( __METHOD__ )->execute();
774 $dbw->newDeleteQueryBuilder()
775 ->deleteFrom(
'revision' )
776 ->where( [
'rev_id' => $revids ] )
777 ->caller( __METHOD__ )->execute();
779 if ( count( $ipRevIds ) > 0 ) {
780 $dbw->newDeleteQueryBuilder()
781 ->deleteFrom(
'ip_changes' )
782 ->where( [
'ipc_rev_id' => $ipRevIds ] )
783 ->caller( __METHOD__ )->execute();
798 private function doDeleteUpdates(
WikiPage $page, RevisionRecord $revRecord ): void {
800 $countable = $page->isCountable();
801 }
catch ( TimeoutException $e ) {
803 }
catch ( Exception $ex ) {
810 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
811 [
'edits' => 1,
'articles' => $countable ? -1 : 0,
'pages' => -1 ]
815 $updates = $this->getDeletionUpdates( $page, $revRecord );
816 foreach ( $updates as $update ) {
817 DeferredUpdates::addUpdate( $update );
821 LinksUpdate::queueRecursiveJobsForTable(
825 $this->deleter->getUser()->getName(),
826 $this->backlinkCacheFactory->getBacklinkCache( $page->
getTitle() )
830 LinksUpdate::queueRecursiveJobsForTable(
834 $this->deleter->getUser()->getName(),
835 $this->backlinkCacheFactory->getBacklinkCache( $page->
getTitle() )
839 if ( !$this->isDeletePageUnitTest ) {
845 WikiModule::invalidateModuleCache(
853 $page->
loadFromRow(
false, IDBAccessObject::READ_LATEST );
856 DeferredUpdates::addUpdate(
new SearchUpdate( $page->
getId(), $page->
getTitle() ) );
868 private function getDeletionUpdates(
WikiPage $page, RevisionRecord $rev ): array {
869 if ( $this->isDeletePageUnitTest ) {
873 $slotContent = array_map(
static function ( SlotRecord $slot ) {
874 return $slot->getContent();
875 }, $rev->getSlots()->getSlots() );
877 $allUpdates = [
new LinksDeletionUpdate( $page ) ];
884 foreach ( $slotContent as $role => $content ) {
885 $handler = $content->getContentHandler();
887 $updates = $handler->getDeletionUpdates(
892 $allUpdates = array_merge( $allUpdates, $updates );
895 $this->hookRunner->onPageDeletionDataUpdates(
896 $page->
getTitle(), $rev, $allUpdates );
899 $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
Class for creating new log entries and inserting them into the database.
__construct(HookContainer $hookContainer, RevisionStore $revisionStore, LBFactory $lbFactory, JobQueueGroup $jobQueueGroup, CommentStore $commentStore, ServiceOptions $serviceOptions, BagOStuff $recentDeletesCache, string $localWikiID, string $webRequestID, WikiPageFactory $wikiPageFactory, UserFactory $userFactory, BacklinkCacheFactory $backlinkCacheFactory, NamespaceInfo $namespaceInfo, ITextFormatter $contLangMsgTextFormatter, RedirectStore $redirectStore, ProperPageIdentity $page, Authority $deleter)
Base representation for an editable wiki page.
loadFromRow( $data, $from)
Load the object from a database row.
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
static onArticleDelete(Title $title)
Clears caches when article is deleted.
getContent( $audience=RevisionRecord::FOR_PUBLIC, ?Authority $performer=null)
Get the content of the current revision.
loadPageData( $from='fromdb')
Load the object from a given source by title.