MediaWiki master
DeletePage.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Page;
4
5use BadMethodCallException;
6use ChangeTags;
8use Exception;
10use LogicException;
37use StatusValue;
38use 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 $isDeletePageUnitTest = false;
70 private $suppress = false;
72 private $tags = [];
74 private $logSubtype = 'delete';
76 private $forceImmediate = false;
78 private $associatedTalk;
79
81 private $legacyHookErrors = '';
83 private $mergeLegacyHookErrors = true;
84
89 private $successfulDeletionsIDs;
94 private $wasScheduled;
96 private $attemptedDeletion = false;
97
98 private HookRunner $hookRunner;
99 private RevisionStore $revisionStore;
100 private LBFactory $lbFactory;
101 private JobQueueGroup $jobQueueGroup;
102 private CommentStore $commentStore;
103 private ServiceOptions $options;
104 private BagOStuff $recentDeletesCache;
105 private string $localWikiID;
106 private string $webRequestID;
107 private WikiPageFactory $wikiPageFactory;
108 private UserFactory $userFactory;
109 private BacklinkCacheFactory $backlinkCacheFactory;
110 private NamespaceInfo $namespaceInfo;
111 private ITextFormatter $contLangMsgTextFormatter;
112 private RedirectStore $redirectStore;
113 private WikiPage $page;
114 private Authority $deleter;
115
119 public function __construct(
120 HookContainer $hookContainer,
121 RevisionStore $revisionStore,
122 LBFactory $lbFactory,
123 JobQueueGroup $jobQueueGroup,
124 CommentStore $commentStore,
125 ServiceOptions $serviceOptions,
126 BagOStuff $recentDeletesCache,
127 string $localWikiID,
128 string $webRequestID,
129 WikiPageFactory $wikiPageFactory,
130 UserFactory $userFactory,
131 BacklinkCacheFactory $backlinkCacheFactory,
132 NamespaceInfo $namespaceInfo,
133 ITextFormatter $contLangMsgTextFormatter,
134 RedirectStore $redirectStore,
135 ProperPageIdentity $page,
136 Authority $deleter
137 ) {
138 $this->hookRunner = new HookRunner( $hookContainer );
139 $this->revisionStore = $revisionStore;
140 $this->lbFactory = $lbFactory;
141 $this->jobQueueGroup = $jobQueueGroup;
142 $this->commentStore = $commentStore;
143 $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
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;
153
154 $this->page = $wikiPageFactory->newFromTitle( $page );
155 $this->deleter = $deleter;
156 $this->redirectStore = $redirectStore;
157 }
158
163 public function getLegacyHookErrors() {
164 return $this->legacyHookErrors;
165 }
166
171 public function keepLegacyHookErrorsSeparate(): self {
172 $this->mergeLegacyHookErrors = false;
173 return $this;
174 }
175
183 public function setSuppress( bool $suppress ): self {
184 $this->suppress = $suppress;
185 return $this;
186 }
187
194 public function setTags( array $tags ): self {
195 $this->tags = $tags;
196 return $this;
197 }
198
205 public function setLogSubtype( string $logSubtype ): self {
206 $this->logSubtype = $logSubtype;
207 return $this;
208 }
209
216 public function forceImmediate( bool $forceImmediate ): self {
217 $this->forceImmediate = $forceImmediate;
218 return $this;
219 }
220
226 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
227 return StatusValue::newFatal( 'delete-error-associated-alreadytalk' );
228 }
229 // FIXME NamespaceInfo should work with PageIdentity
230 $talkPage = $this->wikiPageFactory->newFromLinkTarget(
231 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
232 );
233 if ( !$talkPage->exists() ) {
234 return StatusValue::newFatal( 'delete-error-associated-doesnotexist' );
235 }
236 return StatusValue::newGood();
237 }
238
250 public function setDeleteAssociatedTalk( bool $delete ): self {
251 if ( !$delete ) {
252 $this->associatedTalk = null;
253 return $this;
254 }
255
256 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
257 throw new BadMethodCallException( "Cannot delete associated talk page of a talk page! ($this->page)" );
258 }
259 // FIXME NamespaceInfo should work with PageIdentity
260 $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
261 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
262 );
263 return $this;
264 }
265
271 public function setIsDeletePageUnitTest( bool $test ): void {
272 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
273 throw new LogicException( __METHOD__ . ' can only be used in tests!' );
274 }
275 $this->isDeletePageUnitTest = $test;
276 }
277
283 public function setDeletionAttempted(): self {
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;
290 }
291 return $this;
292 }
293
298 private function assertDeletionAttempted(): void {
299 if ( !$this->attemptedDeletion ) {
300 throw new BadMethodCallException( 'No deletion was attempted' );
301 }
302 }
303
308 public function getSuccessfulDeletionsIDs(): array {
309 $this->assertDeletionAttempted();
310 return $this->successfulDeletionsIDs;
311 }
312
317 public function deletionsWereScheduled(): array {
318 $this->assertDeletionAttempted();
319 return $this->wasScheduled;
320 }
321
328 public function deleteIfAllowed( string $reason ): StatusValue {
329 $this->setDeletionAttempted();
330 $status = $this->authorizeDeletion();
331 if ( !$status->isGood() ) {
332 return $status;
333 }
334
335 return $this->deleteUnsafe( $reason );
336 }
337
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 );
343 }
344 if ( !$this->deleter->isAllowed( 'bigdelete' ) && $this->isBigDeletion() ) {
345 $status->fatal(
346 'delete-toomanyrevisions',
347 Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
348 );
349 }
350 if ( $this->tags ) {
351 $status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->deleter ) );
352 }
353 return $status;
354 }
355
356 private function isBigDeletion(): bool {
357 $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
358 if ( !$revLimit ) {
359 return false;
360 }
361
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() );
366 }
367
368 return $revCount > $revLimit;
369 }
370
383 public function isBatchedDelete( int $safetyMargin = 0 ): bool {
384 $dbr = $this->lbFactory->getReplicaDatabase();
385 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
386 $revCount += $safetyMargin;
387
388 if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
389 return true;
390 } elseif ( !$this->associatedTalk ) {
391 return false;
392 }
393
394 $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
395 $talkRevCount += $safetyMargin;
396
397 return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
398 }
399
410 public function deleteUnsafe( string $reason ): Status {
411 $this->setDeletionAttempted();
412 $origReason = $reason;
413 $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
414 if ( !$hookStatus->isGood() ) {
415 return $hookStatus;
416 }
417 if ( $this->associatedTalk ) {
418 $talkReason = $this->contLangMsgTextFormatter->format(
419 MessageValue::new( 'delete-talk-summary-prefix' )->plaintextParams( $origReason )
420 );
421 $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
422 if ( !$talkHookStatus->isGood() ) {
423 return $talkHookStatus;
424 }
425 }
426
427 $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
428 if ( !$this->associatedTalk || !$status->isGood() ) {
429 return $status;
430 }
431 // NOTE: If the page deletion above failed because the page is no longer there (e.g. race condition) we'll
432 // still try to delete the talk page, since it was the user's intention anyway.
433 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable talkReason is set when used
434 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable talkReason is set when used
435 $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
436 return $status;
437 }
438
444 private function runPreDeleteHooks( WikiPage $page, string &$reason ): Status {
445 $status = Status::newGood();
446
447 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
448 if ( !$this->hookRunner->onArticleDelete(
449 $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
450 ) {
451 if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !== '' ) {
452 if ( is_string( $this->legacyHookErrors ) ) {
453 $this->legacyHookErrors = [ $this->legacyHookErrors ];
454 }
455 foreach ( $this->legacyHookErrors as $legacyError ) {
456 $status->fatal( new RawMessage( $legacyError ) );
457 }
458 }
459 if ( $status->isOK() ) {
460 // Hook aborted but didn't set a fatal status
461 $status->fatal( 'delete-hook-aborted' );
462 }
463 return $status;
464 }
465
466 // Use a new Status in case a hook handler put something here without aborting.
467 $status = Status::newGood();
468 $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
469 if ( !$hookRes && !$status->isGood() ) {
470 // Note: as per the PageDeleteHook documentation, `return false` is ignored if $status is good.
471 return $status;
472 }
473 return Status::newGood();
474 }
475
491 public function deleteInternal(
492 WikiPage $page,
493 string $pageRole,
494 string $reason,
495 ?string $webRequestId = null,
496 $ticket = null
497 ): Status {
498 $title = $page->getTitle();
499 $status = Status::newGood();
500
501 $dbw = $this->lbFactory->getPrimaryDatabase();
502 $dbw->startAtomic( __METHOD__ );
503
504 $page->loadPageData( IDBAccessObject::READ_LATEST );
505 $id = $page->getId();
506 // T98706: lock the page from various other updates but avoid using
507 // IDBAccessObject::READ_LOCKING as that will carry over the FOR UPDATE to
508 // the revisions queries (which also JOIN on user). Only lock the page
509 // row and CAS check on page_latest to see if the trx snapshot matches.
510 $lockedLatest = $page->lockAndGetLatest();
511 if ( $id === 0 || $page->getLatest() !== $lockedLatest ) {
512 $dbw->endAtomic( __METHOD__ );
513 // Page not there or trx snapshot is stale
514 $status->error( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) );
515 return $status;
516 }
517
518 // At this point we are now committed to returning an OK
519 // status unless some DB query error or other exception comes up.
520 // This way callers don't have to call rollback() if $status is bad
521 // unless they actually try to catch exceptions (which is rare).
522
523 // we need to remember the old content so we can use it to generate all deletion updates.
524 $revisionRecord = $page->getRevisionRecord();
525 if ( !$revisionRecord ) {
526 throw new LogicException( "No revisions for $page?" );
527 }
528 try {
529 $content = $page->getContent( RevisionRecord::RAW );
530 } catch ( TimeoutException $e ) {
531 throw $e;
532 } catch ( Exception $ex ) {
533 wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
534 . $ex->getMessage() );
535
536 $content = null;
537 }
538
539 // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
540 // one batch of revisions and defer archival of any others to the job queue.
541 while ( true ) {
542 $done = $this->archiveRevisions( $page, $id );
543 if ( $done || !$this->forceImmediate ) {
544 break;
545 }
546 $dbw->endAtomic( __METHOD__ );
547 $this->lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
548 $dbw->startAtomic( __METHOD__ );
549 }
550
551 if ( !$done ) {
552 $dbw->endAtomic( __METHOD__ );
553
554 $jobParams = [
555 'namespace' => $title->getNamespace(),
556 'title' => $title->getDBkey(),
557 'wikiPageId' => $id,
558 'requestId' => $webRequestId ?? $this->webRequestID,
559 'reason' => $reason,
560 'suppress' => $this->suppress,
561 'userId' => $this->deleter->getUser()->getId(),
562 'tags' => json_encode( $this->tags ),
563 'logsubtype' => $this->logSubtype,
564 'pageRole' => $pageRole,
565 ];
566
567 $job = new DeletePageJob( $jobParams );
568 $this->jobQueueGroup->push( $job );
569 $this->wasScheduled[$pageRole] = true;
570 return $status;
571 }
572 $this->wasScheduled[$pageRole] = false;
573
574 // Get archivedRevisionCount by db query, because there's no better alternative.
575 // Jobs cannot pass a count of archived revisions to the next job, because additional
576 // deletion operations can be started while the first is running. Jobs from each
577 // gracefully interleave, but would not know about each other's count. Deduplication
578 // in the job queue to avoid simultaneous deletion operations would add overhead.
579 // Number of archived revisions cannot be known beforehand, because edits can be made
580 // while deletion operations are being processed, changing the number of archivals.
581 $archivedRevisionCount = $dbw->newSelectQueryBuilder()
582 ->select( '*' )
583 ->from( 'archive' )
584 ->where( [
585 'ar_namespace' => $title->getNamespace(),
586 'ar_title' => $title->getDBkey(),
587 'ar_page_id' => $id
588 ] )
589 ->caller( __METHOD__ )->fetchRowCount();
590
591 // Look up the redirect target before deleting the page to avoid inconsistent state (T348881).
592 // The cloning business below is specifically to allow hook handlers to check the redirect
593 // status before the deletion (see I715046dc8157047aff4d5bd03ea6b5a47aee58bb).
594 $page->getRedirectTarget();
595 // Clone the title and wikiPage, so we have the information we need when
596 // we log and run the ArticleDeleteComplete hook.
597 $logTitle = clone $title;
598 $wikiPageBeforeDelete = clone $page;
599
600 // Now that it's safely backed up, delete it
601 $dbw->newDeleteQueryBuilder()
602 ->deleteFrom( 'page' )
603 ->where( [ 'page_id' => $id ] )
604 ->caller( __METHOD__ )->execute();
605
606 // Log the deletion, if the page was suppressed, put it in the suppression log instead
607 $logtype = $this->suppress ? 'suppress' : 'delete';
608
609 $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
610 $logEntry->setPerformer( $this->deleter->getUser() );
611 $logEntry->setTarget( $logTitle );
612 $logEntry->setComment( $reason );
613 $logEntry->addTags( $this->tags );
614 if ( !$this->isDeletePageUnitTest ) {
615 // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
616 $logid = $logEntry->insert();
617
618 $dbw->onTransactionPreCommitOrIdle(
619 static function () use ( $logEntry, $logid ) {
620 // T58776: avoid deadlocks (especially from FileDeleteForm)
621 $logEntry->publish( $logid );
622 },
623 __METHOD__
624 );
625 } else {
626 $logid = 42;
627 }
628
629 $dbw->endAtomic( __METHOD__ );
630
631 $this->doDeleteUpdates( $page, $revisionRecord );
632
633 // Reset the page object and the Title object
634 $page->loadFromRow( false, IDBAccessObject::READ_LATEST );
635
636 // Make sure there are no cached title instances that refer to the same page.
637 Title::clearCaches();
638
639 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
640 $this->hookRunner->onArticleDeleteComplete(
641 $wikiPageBeforeDelete,
642 $legacyDeleter,
643 $reason,
644 $id,
645 $content,
646 $logEntry,
647 $archivedRevisionCount
648 );
649 $this->hookRunner->onPageDeleteComplete(
650 $wikiPageBeforeDelete,
651 $this->deleter,
652 $reason,
653 $id,
654 $revisionRecord,
655 $logEntry,
656 $archivedRevisionCount
657 );
658 $this->successfulDeletionsIDs[$pageRole] = $logid;
659
660 // Clear any cached redirect status for the now-deleted page.
661 $this->redirectStore->clearCache( $page );
662
663 // Show log excerpt on 404 pages rather than just a link
664 $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
665 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
666
667 return $status;
668 }
669
677 private function archiveRevisions( WikiPage $page, int $id ): bool {
678 // Given the lock above, we can be confident in the title and page ID values
679 $namespace = $page->getTitle()->getNamespace();
680 $dbKey = $page->getTitle()->getDBkey();
681
682 $dbw = $this->lbFactory->getPrimaryDatabase();
683
684 $revQuery = $this->revisionStore->getQueryInfo();
685 $bitfield = false;
686
687 // Bitfields to further suppress the content
688 if ( $this->suppress ) {
689 $bitfield = RevisionRecord::SUPPRESSED_ALL;
690 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
691 }
692
693 // For now, shunt the revision data into the archive table.
694 // Text is *not* removed from the text table; bulk storage
695 // is left intact to avoid breaking block-compression or
696 // immutable storage schemes.
697 // In the future, we may keep revisions and mark them with
698 // the rev_deleted field, which is reserved for this purpose.
699
700 // Lock rows in `revision` and its temp tables, but not any others.
701 // Note array_intersect() preserves keys from the first arg, and we're
702 // assuming $revQuery has `revision` primary and isn't using subtables
703 // for anything we care about.
704 $lockQuery = $revQuery;
705 $lockQuery['tables'] = array_intersect(
706 $revQuery['tables'],
707 [ 'revision', 'revision_comment_temp' ]
708 );
709 unset( $lockQuery['fields'] );
710 $dbw->newSelectQueryBuilder()
711 ->queryInfo( $lockQuery )
712 ->where( [ 'rev_page' => $id ] )
713 ->forUpdate()
714 ->caller( __METHOD__ )
715 ->acquireRowLocks();
716
717 $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
718 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
719 // unusual case where there were exactly $deleteBatchSize revisions remaining.
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__ )
726 ->fetchResultSet();
727
728 // Build their equivalent archive rows
729 $rowsInsert = [];
730 $revids = [];
731
733 $ipRevIds = [];
734
735 $done = true;
736 foreach ( $res as $row ) {
737 if ( count( $revids ) >= $deleteBatchSize ) {
738 $done = false;
739 break;
740 }
741
742 $comment = $this->commentStore->getComment( 'rev_comment', $row );
743 $rowInsert = [
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,
752 'ar_page_id' => $id,
753 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
754 'ar_sha1' => $row->rev_sha1,
755 ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
756
757 $rowsInsert[] = $rowInsert;
758 $revids[] = $row->rev_id;
759
760 // Keep track of IP edits, so that the corresponding rows can
761 // be deleted in the ip_changes table.
762 if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
763 $ipRevIds[] = $row->rev_id;
764 }
765 }
766
767 if ( count( $revids ) > 0 ) {
768 // Copy them into the archive table
769 $dbw->newInsertQueryBuilder()
770 ->insertInto( 'archive' )
771 ->rows( $rowsInsert )
772 ->caller( __METHOD__ )->execute();
773
774 $dbw->newDeleteQueryBuilder()
775 ->deleteFrom( 'revision' )
776 ->where( [ 'rev_id' => $revids ] )
777 ->caller( __METHOD__ )->execute();
778 // Also delete records from ip_changes as applicable.
779 if ( count( $ipRevIds ) > 0 ) {
780 $dbw->newDeleteQueryBuilder()
781 ->deleteFrom( 'ip_changes' )
782 ->where( [ 'ipc_rev_id' => $ipRevIds ] )
783 ->caller( __METHOD__ )->execute();
784 }
785 }
786
787 return $done;
788 }
789
798 private function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
799 try {
800 $countable = $page->isCountable();
801 } catch ( TimeoutException $e ) {
802 throw $e;
803 } catch ( Exception $ex ) {
804 // fallback for deleting broken pages for which we cannot load the content for
805 // some reason. Note that doDeleteArticleReal() already logged this problem.
806 $countable = false;
807 }
808
809 // Update site status
810 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
811 [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
812 ) );
813
814 // Delete pagelinks, update secondary indexes, etc
815 $updates = $this->getDeletionUpdates( $page, $revRecord );
816 foreach ( $updates as $update ) {
817 DeferredUpdates::addUpdate( $update );
818 }
819
820 // Reparse any pages transcluding this page
821 LinksUpdate::queueRecursiveJobsForTable(
822 $page->getTitle(),
823 'templatelinks',
824 'delete-page',
825 $this->deleter->getUser()->getName(),
826 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
827 );
828 // Reparse any pages including this image
829 if ( $page->getTitle()->getNamespace() === NS_FILE ) {
830 LinksUpdate::queueRecursiveJobsForTable(
831 $page->getTitle(),
832 'imagelinks',
833 'delete-page',
834 $this->deleter->getUser()->getName(),
835 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
836 );
837 }
838
839 if ( !$this->isDeletePageUnitTest ) {
840 // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
841 // Clear caches
843 }
844
845 WikiModule::invalidateModuleCache(
846 $page->getTitle(),
847 $revRecord,
848 null,
849 $this->localWikiID
850 );
851
852 // Reset the page object and the Title object
853 $page->loadFromRow( false, IDBAccessObject::READ_LATEST );
854
855 // Search engine
856 DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
857 }
858
868 private function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
869 if ( $this->isDeletePageUnitTest ) {
870 // Hack: LinksDeletionUpdate reads from the global state in the constructor
871 return [];
872 }
873 $slotContent = array_map( static function ( SlotRecord $slot ) {
874 return $slot->getContent();
875 }, $rev->getSlots()->getSlots() );
876
877 $allUpdates = [ new LinksDeletionUpdate( $page ) ];
878
879 // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
880 // model here, not the content object!
881 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
883 $content = null; // in case $slotContent is zero-length
884 foreach ( $slotContent as $role => $content ) {
885 $handler = $content->getContentHandler();
886
887 $updates = $handler->getDeletionUpdates(
888 $page->getTitle(),
889 $role
890 );
891
892 $allUpdates = array_merge( $allUpdates, $updates );
893 }
894
895 $this->hookRunner->onPageDeletionDataUpdates(
896 $page->getTitle(), $rev, $allUpdates );
897
898 // TODO: hard deprecate old hook in 1.33
899 $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
900 return $allUpdates;
901 }
902}
const NS_FILE
Definition Defines.php:71
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
Recent changes tagging.
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.
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.
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:155
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.
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()
__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)
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
deleteInternal(WikiPage $page, string $pageRole, string $reason, ?string $webRequestId=null, $ticket=null)
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 storing and retrieving page redirect information.
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.
Database independent search index updater.
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...
Represents a title within MediaWiki.
Definition Title.php:78
Create 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:84
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:486
getRedirectTarget()
If this page is a redirect, get its target.
Definition WikiPage.php:956
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:682
static onArticleDelete(Title $title)
Clears caches when article is deleted.
getId( $wikiId=self::LOCAL)
Definition WikiPage.php:528
getTitle()
Get the title object of the article.
Definition WikiPage.php:252
getContent( $audience=RevisionRecord::FOR_PUBLIC, ?Authority $performer=null)
Get the content of the current revision.
Definition WikiPage.php:763
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:412
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:745
Value object representing a message for i18n.
Abstract class for any ephemeral data store.
Definition BagOStuff.php:88
Base interface for representing page content.
Definition Content.php:39
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
Interface for database access objects.
if(count( $args)< 1) $job