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