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