MediaWiki master
DeletePage.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Page;
4
5use BadMethodCallException;
6use BagOStuff;
7use ChangeTags;
8use Content;
10use Exception;
13use LogicException;
39use StatusValue;
40use Wikimedia\IPUtils;
44use Wikimedia\RequestTimeout\TimeoutException;
45use WikiPage;
46
56 public const CONSTRUCTOR_OPTIONS = [
59 ];
60
64 public const PAGE_BASE = 'base';
65 public const PAGE_TALK = 'talk';
66
68 private $hookRunner;
70 private $revisionStore;
72 private $lbFactory;
74 private $jobQueueGroup;
76 private $commentStore;
78 private $options;
80 private $recentDeletesCache;
82 private $localWikiID;
84 private $webRequestID;
86 private $userFactory;
88 private $backlinkCacheFactory;
90 private $wikiPageFactory;
92 private $namespaceInfo;
94 private $contLangMsgTextFormatter;
95
97 private $isDeletePageUnitTest = false;
98
100 private $page;
102 private $deleter;
103
105 private $suppress = false;
107 private $tags = [];
109 private $logSubtype = 'delete';
111 private $forceImmediate = false;
113 private $associatedTalk;
114
116 private $legacyHookErrors = '';
118 private $mergeLegacyHookErrors = true;
119
124 private $successfulDeletionsIDs;
129 private $wasScheduled;
131 private $attemptedDeletion = false;
132
152 public function __construct(
153 HookContainer $hookContainer,
154 RevisionStore $revisionStore,
155 LBFactory $lbFactory,
156 JobQueueGroup $jobQueueGroup,
157 CommentStore $commentStore,
158 ServiceOptions $serviceOptions,
159 BagOStuff $recentDeletesCache,
160 string $localWikiID,
161 string $webRequestID,
162 WikiPageFactory $wikiPageFactory,
163 UserFactory $userFactory,
164 BacklinkCacheFactory $backlinkCacheFactory,
165 NamespaceInfo $namespaceInfo,
166 ITextFormatter $contLangMsgTextFormatter,
167 ProperPageIdentity $page,
168 Authority $deleter
169 ) {
170 $this->hookRunner = new HookRunner( $hookContainer );
171 $this->revisionStore = $revisionStore;
172 $this->lbFactory = $lbFactory;
173 $this->jobQueueGroup = $jobQueueGroup;
174 $this->commentStore = $commentStore;
175 $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
176 $this->options = $serviceOptions;
177 $this->recentDeletesCache = $recentDeletesCache;
178 $this->localWikiID = $localWikiID;
179 $this->webRequestID = $webRequestID;
180 $this->wikiPageFactory = $wikiPageFactory;
181 $this->userFactory = $userFactory;
182 $this->backlinkCacheFactory = $backlinkCacheFactory;
183 $this->namespaceInfo = $namespaceInfo;
184 $this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
185
186 $this->page = $wikiPageFactory->newFromTitle( $page );
187 $this->deleter = $deleter;
188 }
189
194 public function getLegacyHookErrors() {
195 return $this->legacyHookErrors;
196 }
197
202 public function keepLegacyHookErrorsSeparate(): self {
203 $this->mergeLegacyHookErrors = false;
204 return $this;
205 }
206
214 public function setSuppress( bool $suppress ): self {
215 $this->suppress = $suppress;
216 return $this;
217 }
218
225 public function setTags( array $tags ): self {
226 $this->tags = $tags;
227 return $this;
228 }
229
236 public function setLogSubtype( string $logSubtype ): self {
237 $this->logSubtype = $logSubtype;
238 return $this;
239 }
240
247 public function forceImmediate( bool $forceImmediate ): self {
248 $this->forceImmediate = $forceImmediate;
249 return $this;
250 }
251
259 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
260 return StatusValue::newFatal( 'delete-error-associated-alreadytalk' );
261 }
262 // FIXME NamespaceInfo should work with PageIdentity
263 $talkPage = $this->wikiPageFactory->newFromLinkTarget(
264 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
265 );
266 if ( !$talkPage->exists() ) {
267 return StatusValue::newFatal( 'delete-error-associated-doesnotexist' );
268 }
269 return StatusValue::newGood();
270 }
271
283 public function setDeleteAssociatedTalk( bool $delete ): self {
284 if ( !$delete ) {
285 $this->associatedTalk = null;
286 return $this;
287 }
288
289 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
290 throw new BadMethodCallException( "Cannot delete associated talk page of a talk page! ($this->page)" );
291 }
292 // FIXME NamespaceInfo should work with PageIdentity
293 $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
294 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
295 );
296 return $this;
297 }
298
304 public function setIsDeletePageUnitTest( bool $test ): void {
305 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
306 throw new LogicException( __METHOD__ . ' can only be used in tests!' );
307 }
308 $this->isDeletePageUnitTest = $test;
309 }
310
316 public function setDeletionAttempted(): self {
317 $this->attemptedDeletion = true;
318 $this->successfulDeletionsIDs = [ self::PAGE_BASE => null ];
319 $this->wasScheduled = [ self::PAGE_BASE => null ];
320 if ( $this->associatedTalk ) {
321 $this->successfulDeletionsIDs[self::PAGE_TALK] = null;
322 $this->wasScheduled[self::PAGE_TALK] = null;
323 }
324 return $this;
325 }
326
331 private function assertDeletionAttempted(): void {
332 if ( !$this->attemptedDeletion ) {
333 throw new BadMethodCallException( 'No deletion was attempted' );
334 }
335 }
336
341 public function getSuccessfulDeletionsIDs(): array {
342 $this->assertDeletionAttempted();
343 return $this->successfulDeletionsIDs;
344 }
345
350 public function deletionsWereScheduled(): array {
351 $this->assertDeletionAttempted();
352 return $this->wasScheduled;
353 }
354
361 public function deleteIfAllowed( string $reason ): StatusValue {
362 $this->setDeletionAttempted();
363 $status = $this->authorizeDeletion();
364 if ( !$status->isGood() ) {
365 return $status;
366 }
367
368 return $this->deleteUnsafe( $reason );
369 }
370
374 private function authorizeDeletion(): PermissionStatus {
375 $status = PermissionStatus::newEmpty();
376 $this->deleter->authorizeWrite( 'delete', $this->page, $status );
377 if ( $this->associatedTalk ) {
378 $this->deleter->authorizeWrite( 'delete', $this->associatedTalk, $status );
379 }
380 if ( !$this->deleter->isAllowed( 'bigdelete' ) && $this->isBigDeletion() ) {
381 $status->fatal(
382 'delete-toomanyrevisions',
383 Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
384 );
385 }
386 if ( $this->tags ) {
387 $status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->deleter ) );
388 }
389 return $status;
390 }
391
395 private function isBigDeletion(): bool {
396 $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
397 if ( !$revLimit ) {
398 return false;
399 }
400
401 $dbr = $this->lbFactory->getReplicaDatabase();
402 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
403 if ( $this->associatedTalk ) {
404 $revCount += $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
405 }
406
407 return $revCount > $revLimit;
408 }
409
422 public function isBatchedDelete( int $safetyMargin = 0 ): bool {
423 $dbr = $this->lbFactory->getReplicaDatabase();
424 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
425 $revCount += $safetyMargin;
426
427 if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
428 return true;
429 } elseif ( !$this->associatedTalk ) {
430 return false;
431 }
432
433 $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
434 $talkRevCount += $safetyMargin;
435
436 return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
437 }
438
449 public function deleteUnsafe( string $reason ): Status {
450 $this->setDeletionAttempted();
451 $origReason = $reason;
452 $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
453 if ( !$hookStatus->isGood() ) {
454 return $hookStatus;
455 }
456 if ( $this->associatedTalk ) {
457 $talkReason = $this->contLangMsgTextFormatter->format(
458 MessageValue::new( 'delete-talk-summary-prefix' )->plaintextParams( $origReason )
459 );
460 $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
461 if ( !$talkHookStatus->isGood() ) {
462 return $talkHookStatus;
463 }
464 }
465
466 $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
467 if ( !$this->associatedTalk || !$status->isGood() ) {
468 return $status;
469 }
470 // NOTE: If the page deletion above failed because the page is no longer there (e.g. race condition) we'll
471 // still try to delete the talk page, since it was the user's intention anyway.
472 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable talkReason is set when used
473 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable talkReason is set when used
474 $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
475 return $status;
476 }
477
483 private function runPreDeleteHooks( WikiPage $page, string &$reason ): Status {
484 $status = Status::newGood();
485
486 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
487 if ( !$this->hookRunner->onArticleDelete(
488 $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
489 ) {
490 if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !== '' ) {
491 if ( is_string( $this->legacyHookErrors ) ) {
492 $this->legacyHookErrors = [ $this->legacyHookErrors ];
493 }
494 foreach ( $this->legacyHookErrors as $legacyError ) {
495 $status->fatal( new RawMessage( $legacyError ) );
496 }
497 }
498 if ( $status->isOK() ) {
499 // Hook aborted but didn't set a fatal status
500 $status->fatal( 'delete-hook-aborted' );
501 }
502 return $status;
503 }
504
505 // Use a new Status in case a hook handler put something here without aborting.
506 $status = Status::newGood();
507 $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
508 if ( !$hookRes && !$status->isGood() ) {
509 // Note: as per the PageDeleteHook documentation, `return false` is ignored if $status is good.
510 return $status;
511 }
512 return Status::newGood();
513 }
514
529 public function deleteInternal(
530 WikiPage $page,
531 string $pageRole,
532 string $reason,
533 ?string $webRequestId = null
534 ): Status {
535 $title = $page->getTitle();
536 $status = Status::newGood();
537
538 $dbw = $this->lbFactory->getPrimaryDatabase();
539 $dbw->startAtomic( __METHOD__ );
540
541 $page->loadPageData( IDBAccessObject::READ_LATEST );
542 $id = $page->getId();
543 // T98706: lock the page from various other updates but avoid using
544 // IDBAccessObject::READ_LOCKING as that will carry over the FOR UPDATE to
545 // the revisions queries (which also JOIN on user). Only lock the page
546 // row and CAS check on page_latest to see if the trx snapshot matches.
547 $lockedLatest = $page->lockAndGetLatest();
548 if ( $id === 0 || $page->getLatest() !== $lockedLatest ) {
549 $dbw->endAtomic( __METHOD__ );
550 // Page not there or trx snapshot is stale
551 $status->error( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) );
552 return $status;
553 }
554
555 // At this point we are now committed to returning an OK
556 // status unless some DB query error or other exception comes up.
557 // This way callers don't have to call rollback() if $status is bad
558 // unless they actually try to catch exceptions (which is rare).
559
560 // we need to remember the old content so we can use it to generate all deletion updates.
561 $revisionRecord = $page->getRevisionRecord();
562 if ( !$revisionRecord ) {
563 throw new LogicException( "No revisions for $page?" );
564 }
565 try {
566 $content = $page->getContent( RevisionRecord::RAW );
567 } catch ( TimeoutException $e ) {
568 throw $e;
569 } catch ( Exception $ex ) {
570 wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
571 . $ex->getMessage() );
572
573 $content = null;
574 }
575
576 // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
577 // one batch of revisions and defer archival of any others to the job queue.
578 $explictTrxLogged = false;
579 while ( true ) {
580 $done = $this->archiveRevisions( $page, $id );
581 if ( $done || !$this->forceImmediate ) {
582 break;
583 }
584 $dbw->endAtomic( __METHOD__ );
585 if ( $dbw->explicitTrxActive() ) {
586 // Explicit transactions may never happen here in practice. Log to be sure.
587 if ( !$explictTrxLogged ) {
588 $explictTrxLogged = true;
589 LoggerFactory::getInstance( 'wfDebug' )->debug(
590 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
591 'title' => $title->getText(),
592 ] );
593 }
594 continue;
595 }
596 if ( $dbw->trxLevel() ) {
597 $dbw->commit( __METHOD__ );
598 }
599 $this->lbFactory->waitForReplication();
600 $dbw->startAtomic( __METHOD__ );
601 }
602
603 if ( !$done ) {
604 $dbw->endAtomic( __METHOD__ );
605
606 $jobParams = [
607 'namespace' => $title->getNamespace(),
608 'title' => $title->getDBkey(),
609 'wikiPageId' => $id,
610 'requestId' => $webRequestId ?? $this->webRequestID,
611 'reason' => $reason,
612 'suppress' => $this->suppress,
613 'userId' => $this->deleter->getUser()->getId(),
614 'tags' => json_encode( $this->tags ),
615 'logsubtype' => $this->logSubtype,
616 'pageRole' => $pageRole,
617 ];
618
619 $job = new DeletePageJob( $jobParams );
620 $this->jobQueueGroup->push( $job );
621 $this->wasScheduled[$pageRole] = true;
622 return $status;
623 }
624 $this->wasScheduled[$pageRole] = false;
625
626 // Get archivedRevisionCount by db query, because there's no better alternative.
627 // Jobs cannot pass a count of archived revisions to the next job, because additional
628 // deletion operations can be started while the first is running. Jobs from each
629 // gracefully interleave, but would not know about each other's count. Deduplication
630 // in the job queue to avoid simultaneous deletion operations would add overhead.
631 // Number of archived revisions cannot be known beforehand, because edits can be made
632 // while deletion operations are being processed, changing the number of archivals.
633 $archivedRevisionCount = $dbw->newSelectQueryBuilder()
634 ->select( '*' )
635 ->from( 'archive' )
636 ->where( [
637 'ar_namespace' => $title->getNamespace(),
638 'ar_title' => $title->getDBkey(),
639 'ar_page_id' => $id
640 ] )
641 ->caller( __METHOD__ )->fetchRowCount();
642
643 // Look up the redirect target before deleting the page to avoid inconsistent state (T348881).
644 // The cloning business below is specifically to allow hook handlers to check the redirect
645 // status before the deletion (see I715046dc8157047aff4d5bd03ea6b5a47aee58bb).
646 $page->getRedirectTarget();
647 // Clone the title and wikiPage, so we have the information we need when
648 // we log and run the ArticleDeleteComplete hook.
649 $logTitle = clone $title;
650 $wikiPageBeforeDelete = clone $page;
651
652 // Now that it's safely backed up, delete it
653 $dbw->newDeleteQueryBuilder()
654 ->deleteFrom( 'page' )
655 ->where( [ 'page_id' => $id ] )
656 ->caller( __METHOD__ )->execute();
657
658 // Log the deletion, if the page was suppressed, put it in the suppression log instead
659 $logtype = $this->suppress ? 'suppress' : 'delete';
660
661 $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
662 $logEntry->setPerformer( $this->deleter->getUser() );
663 $logEntry->setTarget( $logTitle );
664 $logEntry->setComment( $reason );
665 $logEntry->addTags( $this->tags );
666 if ( !$this->isDeletePageUnitTest ) {
667 // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
668 $logid = $logEntry->insert();
669
670 $dbw->onTransactionPreCommitOrIdle(
671 static function () use ( $logEntry, $logid ) {
672 // T58776: avoid deadlocks (especially from FileDeleteForm)
673 $logEntry->publish( $logid );
674 },
675 __METHOD__
676 );
677 } else {
678 $logid = 42;
679 }
680
681 $dbw->endAtomic( __METHOD__ );
682
683 $this->doDeleteUpdates( $page, $revisionRecord );
684
685 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
686 $this->hookRunner->onArticleDeleteComplete(
687 $wikiPageBeforeDelete,
688 $legacyDeleter,
689 $reason,
690 $id,
691 $content,
692 $logEntry,
693 $archivedRevisionCount
694 );
695 $this->hookRunner->onPageDeleteComplete(
696 $wikiPageBeforeDelete,
697 $this->deleter,
698 $reason,
699 $id,
700 $revisionRecord,
701 $logEntry,
702 $archivedRevisionCount
703 );
704 $this->successfulDeletionsIDs[$pageRole] = $logid;
705
706 // Show log excerpt on 404 pages rather than just a link
707 $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
708 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
709
710 return $status;
711 }
712
720 private function archiveRevisions( WikiPage $page, int $id ): bool {
721 // Given the lock above, we can be confident in the title and page ID values
722 $namespace = $page->getTitle()->getNamespace();
723 $dbKey = $page->getTitle()->getDBkey();
724
725 $dbw = $this->lbFactory->getPrimaryDatabase();
726
727 $revQuery = $this->revisionStore->getQueryInfo();
728 $bitfield = false;
729
730 // Bitfields to further suppress the content
731 if ( $this->suppress ) {
732 $bitfield = RevisionRecord::SUPPRESSED_ALL;
733 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
734 }
735
736 // For now, shunt the revision data into the archive table.
737 // Text is *not* removed from the text table; bulk storage
738 // is left intact to avoid breaking block-compression or
739 // immutable storage schemes.
740 // In the future, we may keep revisions and mark them with
741 // the rev_deleted field, which is reserved for this purpose.
742
743 // Lock rows in `revision` and its temp tables, but not any others.
744 // Note array_intersect() preserves keys from the first arg, and we're
745 // assuming $revQuery has `revision` primary and isn't using subtables
746 // for anything we care about.
747 $lockQuery = $revQuery;
748 $lockQuery['tables'] = array_intersect(
749 $revQuery['tables'],
750 [ 'revision', 'revision_comment_temp' ]
751 );
752 unset( $lockQuery['fields'] );
753 $dbw->newSelectQueryBuilder()
754 ->queryInfo( $lockQuery )
755 ->where( [ 'rev_page' => $id ] )
756 ->forUpdate()
757 ->caller( __METHOD__ )
758 ->acquireRowLocks();
759
760 $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
761 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
762 // unusual case where there were exactly $deleteBatchSize revisions remaining.
763 $res = $dbw->select(
764 $revQuery['tables'],
765 $revQuery['fields'],
766 [ 'rev_page' => $id ],
767 __METHOD__,
768 [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
769 $revQuery['joins']
770 );
771
772 // Build their equivalent archive rows
773 $rowsInsert = [];
774 $revids = [];
775
777 $ipRevIds = [];
778
779 $done = true;
780 foreach ( $res as $row ) {
781 if ( count( $revids ) >= $deleteBatchSize ) {
782 $done = false;
783 break;
784 }
785
786 $comment = $this->commentStore->getComment( 'rev_comment', $row );
787 $rowInsert = [
788 'ar_namespace' => $namespace,
789 'ar_title' => $dbKey,
790 'ar_actor' => $row->rev_actor,
791 'ar_timestamp' => $row->rev_timestamp,
792 'ar_minor_edit' => $row->rev_minor_edit,
793 'ar_rev_id' => $row->rev_id,
794 'ar_parent_id' => $row->rev_parent_id,
795 'ar_len' => $row->rev_len,
796 'ar_page_id' => $id,
797 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
798 'ar_sha1' => $row->rev_sha1,
799 ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
800
801 $rowsInsert[] = $rowInsert;
802 $revids[] = $row->rev_id;
803
804 // Keep track of IP edits, so that the corresponding rows can
805 // be deleted in the ip_changes table.
806 if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
807 $ipRevIds[] = $row->rev_id;
808 }
809 }
810
811 if ( count( $revids ) > 0 ) {
812 // Copy them into the archive table
813 $dbw->newInsertQueryBuilder()
814 ->insertInto( 'archive' )
815 ->rows( $rowsInsert )
816 ->caller( __METHOD__ )->execute();
817
818 $dbw->newDeleteQueryBuilder()
819 ->deleteFrom( 'revision' )
820 ->where( [ 'rev_id' => $revids ] )
821 ->caller( __METHOD__ )->execute();
822 // Also delete records from ip_changes as applicable.
823 if ( count( $ipRevIds ) > 0 ) {
824 $dbw->newDeleteQueryBuilder()
825 ->deleteFrom( 'ip_changes' )
826 ->where( [ 'ipc_rev_id' => $ipRevIds ] )
827 ->caller( __METHOD__ )->execute();
828 }
829 }
830
831 return $done;
832 }
833
842 private function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
843 try {
844 $countable = $page->isCountable();
845 } catch ( TimeoutException $e ) {
846 throw $e;
847 } catch ( Exception $ex ) {
848 // fallback for deleting broken pages for which we cannot load the content for
849 // some reason. Note that doDeleteArticleReal() already logged this problem.
850 $countable = false;
851 }
852
853 // Update site status
854 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
855 [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
856 ) );
857
858 // Delete pagelinks, update secondary indexes, etc
859 $updates = $this->getDeletionUpdates( $page, $revRecord );
860 foreach ( $updates as $update ) {
861 DeferredUpdates::addUpdate( $update );
862 }
863
864 // Reparse any pages transcluding this page
865 LinksUpdate::queueRecursiveJobsForTable(
866 $page->getTitle(),
867 'templatelinks',
868 'delete-page',
869 $this->deleter->getUser()->getName(),
870 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
871 );
872 // Reparse any pages including this image
873 if ( $page->getTitle()->getNamespace() === NS_FILE ) {
874 LinksUpdate::queueRecursiveJobsForTable(
875 $page->getTitle(),
876 'imagelinks',
877 'delete-page',
878 $this->deleter->getUser()->getName(),
879 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
880 );
881 }
882
883 if ( !$this->isDeletePageUnitTest ) {
884 // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
885 // Clear caches
887 }
888
889 WikiModule::invalidateModuleCache(
890 $page->getTitle(),
891 $revRecord,
892 null,
893 $this->localWikiID
894 );
895
896 // Reset the page object and the Title object
897 $page->loadFromRow( false, IDBAccessObject::READ_LATEST );
898
899 // Search engine
900 DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
901 }
902
912 private function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
913 if ( $this->isDeletePageUnitTest ) {
914 // Hack: LinksDeletionUpdate reads from the global state in the constructor
915 return [];
916 }
917 $slotContent = array_map( static function ( SlotRecord $slot ) {
918 return $slot->getContent();
919 }, $rev->getSlots()->getSlots() );
920
921 $allUpdates = [ new LinksDeletionUpdate( $page ) ];
922
923 // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
924 // model here, not the content object!
925 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
927 $content = null; // in case $slotContent is zero-length
928 foreach ( $slotContent as $role => $content ) {
929 $handler = $content->getContentHandler();
930
931 $updates = $handler->getDeletionUpdates(
932 $page->getTitle(),
933 $role
934 );
935
936 $allUpdates = array_merge( $allUpdates, $updates );
937 }
938
939 $this->hookRunner->onPageDeletionDataUpdates(
940 $page->getTitle(), $rev, $allUpdates );
941
942 // TODO: hard deprecate old hook in 1.33
943 $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
944 return $allUpdates;
945 }
946}
const NS_FILE
Definition Defines.php:70
wfEscapeWikiText( $input)
Escapes the given text so that it may be output using addWikiText() without any linking,...
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:81
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Handle enqueueing of background jobs.
Class for creating new log entries and inserting them into the database.
Handle database storage of comments such as edit summaries and log reasons.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Defer callable updates to run later in the PHP process.
Update object handling the cleanup of links tables after a page was deleted.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Database independent search index updater.
Class for handling updates to the site_stats table.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Variant of the Message class.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
const DeleteRevisionsLimit
Name constant for the DeleteRevisionsLimit setting, for use with Config::get()
const DeleteRevisionsBatchSize
Name constant for the DeleteRevisionsBatchSize setting, for use with Config::get()
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:157
Backend logic for performing a page delete action.
setDeletionAttempted()
Called before attempting a deletion, allows the result getters to be used.
canProbablyDeleteAssociatedTalk()
Tests whether it's probably possible to delete the associated talk page.
__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, ProperPageIdentity $page, Authority $deleter)
deleteInternal(WikiPage $page, string $pageRole, string $reason, ?string $webRequestId=null)
setTags(array $tags)
Change tags to apply to the deletion action.
deleteIfAllowed(string $reason)
Same as deleteUnsafe, but checks permissions.
setLogSubtype(string $logSubtype)
Set a specific log subtype for the deletion log entry.
const PAGE_BASE
Constants used for the return value of getSuccessfulDeletionsIDs() and deletionsWereScheduled()
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
setIsDeletePageUnitTest(bool $test)
deleteUnsafe(string $reason)
Back-end article deletion: deletes the article with database consistency, writes logs,...
forceImmediate(bool $forceImmediate)
If false, allows deleting over time via the job queue.
setDeleteAssociatedTalk(bool $delete)
If set to true and the page has a talk page, delete that one too.
setSuppress(bool $suppress)
If true, suppress all revisions and log the deletion in the suppression log instead of the deletion l...
Service for creating WikiPage objects.
newFromTitle(PageIdentity $pageIdentity)
Create a WikiPage object from a title.
A StatusValue for permission errors.
Abstraction for ResourceLoader modules which pull from wiki pages.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates User objects.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Base representation for an editable wiki page.
Definition WikiPage.php:79
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition WikiPage.php:760
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:488
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:957
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition WikiPage.php:678
static onArticleDelete(Title $title)
Clears caches when article is deleted.
getId( $wikiId=self::LOCAL)
Definition WikiPage.php:530
getTitle()
Get the title object of the article.
Definition WikiPage.php:260
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:417
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:742
Value object representing a message for i18n.
Base interface for representing page content.
Definition Content.php:37
Interface for database access objects.
Interface that deferrable updates should implement.
Interface for a page that is (or could be, or used to be) an editable wiki page.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
if(count( $args)< 1) $job