MediaWiki REL1_40
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;
33use Message;
35use SearchUpdate;
37use Status;
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 BadMethodCallException( __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->selectRowCount(
633 'archive',
634 '*',
635 [
636 'ar_namespace' => $title->getNamespace(),
637 'ar_title' => $title->getDBkey(),
638 'ar_page_id' => $id
639 ], __METHOD__
640 );
641
642 // Clone the title and wikiPage, so we have the information we need when
643 // we log and run the ArticleDeleteComplete hook.
644 $logTitle = clone $title;
645 $wikiPageBeforeDelete = clone $page;
646
647 // Now that it's safely backed up, delete it
648 $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
649
650 // Log the deletion, if the page was suppressed, put it in the suppression log instead
651 $logtype = $this->suppress ? 'suppress' : 'delete';
652
653 $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
654 $logEntry->setPerformer( $this->deleter->getUser() );
655 $logEntry->setTarget( $logTitle );
656 $logEntry->setComment( $reason );
657 $logEntry->addTags( $this->tags );
658 if ( !$this->isDeletePageUnitTest ) {
659 // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
660 $logid = $logEntry->insert();
661
662 $dbw->onTransactionPreCommitOrIdle(
663 static function () use ( $logEntry, $logid ) {
664 // T58776: avoid deadlocks (especially from FileDeleteForm)
665 $logEntry->publish( $logid );
666 },
667 __METHOD__
668 );
669 } else {
670 $logid = 42;
671 }
672
673 $dbw->endAtomic( __METHOD__ );
674
675 $this->doDeleteUpdates( $page, $revisionRecord );
676
677 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
678 $this->hookRunner->onArticleDeleteComplete(
679 $wikiPageBeforeDelete,
680 $legacyDeleter,
681 $reason,
682 $id,
683 $content,
684 $logEntry,
685 $archivedRevisionCount
686 );
687 $this->hookRunner->onPageDeleteComplete(
688 $wikiPageBeforeDelete,
689 $this->deleter,
690 $reason,
691 $id,
692 $revisionRecord,
693 $logEntry,
694 $archivedRevisionCount
695 );
696 $this->successfulDeletionsIDs[$pageRole] = $logid;
697
698 // Show log excerpt on 404 pages rather than just a link
699 $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
700 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
701
702 return $status;
703 }
704
712 private function archiveRevisions( WikiPage $page, int $id ): bool {
713 // Given the lock above, we can be confident in the title and page ID values
714 $namespace = $page->getTitle()->getNamespace();
715 $dbKey = $page->getTitle()->getDBkey();
716
717 $dbw = $this->lbFactory->getPrimaryDatabase();
718
719 $revQuery = $this->revisionStore->getQueryInfo();
720 $bitfield = false;
721
722 // Bitfields to further suppress the content
723 if ( $this->suppress ) {
724 $bitfield = RevisionRecord::SUPPRESSED_ALL;
725 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
726 }
727
728 // For now, shunt the revision data into the archive table.
729 // Text is *not* removed from the text table; bulk storage
730 // is left intact to avoid breaking block-compression or
731 // immutable storage schemes.
732 // In the future, we may keep revisions and mark them with
733 // the rev_deleted field, which is reserved for this purpose.
734
735 // Lock rows in `revision` and its temp tables, but not any others.
736 // Note array_intersect() preserves keys from the first arg, and we're
737 // assuming $revQuery has `revision` primary and isn't using subtables
738 // for anything we care about.
739 $lockQuery = $revQuery;
740 $lockQuery['tables'] = array_intersect(
741 $revQuery['tables'],
742 [ 'revision', 'revision_comment_temp' ]
743 );
744 unset( $lockQuery['fields'] );
745 $dbw->newSelectQueryBuilder()
746 ->queryInfo( $lockQuery )
747 ->where( [ 'rev_page' => $id ] )
748 ->forUpdate()
749 ->caller( __METHOD__ )
750 ->acquireRowLocks();
751
752 $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
753 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
754 // unusual case where there were exactly $deleteBatchSize revisions remaining.
755 $res = $dbw->select(
756 $revQuery['tables'],
757 $revQuery['fields'],
758 [ 'rev_page' => $id ],
759 __METHOD__,
760 [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
761 $revQuery['joins']
762 );
763
764 // Build their equivalent archive rows
765 $rowsInsert = [];
766 $revids = [];
767
769 $ipRevIds = [];
770
771 $done = true;
772 foreach ( $res as $row ) {
773 if ( count( $revids ) >= $deleteBatchSize ) {
774 $done = false;
775 break;
776 }
777
778 $comment = $this->commentStore->getComment( 'rev_comment', $row );
779 $rowInsert = [
780 'ar_namespace' => $namespace,
781 'ar_title' => $dbKey,
782 'ar_actor' => $row->rev_actor,
783 'ar_timestamp' => $row->rev_timestamp,
784 'ar_minor_edit' => $row->rev_minor_edit,
785 'ar_rev_id' => $row->rev_id,
786 'ar_parent_id' => $row->rev_parent_id,
787 'ar_len' => $row->rev_len,
788 'ar_page_id' => $id,
789 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
790 'ar_sha1' => $row->rev_sha1,
791 ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
792
793 $rowsInsert[] = $rowInsert;
794 $revids[] = $row->rev_id;
795
796 // Keep track of IP edits, so that the corresponding rows can
797 // be deleted in the ip_changes table.
798 if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
799 $ipRevIds[] = $row->rev_id;
800 }
801 }
802
803 if ( count( $revids ) > 0 ) {
804 // Copy them into the archive table
805 $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
806
807 $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
808 if ( $this->commentStore->getTempTableMigrationStage( 'rev_comment' ) & SCHEMA_COMPAT_WRITE_OLD ) {
809 $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
810 }
811 // Also delete records from ip_changes as applicable.
812 if ( count( $ipRevIds ) > 0 ) {
813 $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
814 }
815 }
816
817 return $done;
818 }
819
829 public function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
830 try {
831 $countable = $page->isCountable();
832 } catch ( TimeoutException $e ) {
833 throw $e;
834 } catch ( Exception $ex ) {
835 // fallback for deleting broken pages for which we cannot load the content for
836 // some reason. Note that doDeleteArticleReal() already logged this problem.
837 $countable = false;
838 }
839
840 // Update site status
841 if ( !$this->isDeletePageUnitTest ) {
842 // TODO Remove conditional once DeferredUpdates is servicified (T265749)
843 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
844 [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
845 ) );
846
847 // Delete pagelinks, update secondary indexes, etc
848 $updates = $this->getDeletionUpdates( $page, $revRecord );
849 foreach ( $updates as $update ) {
850 DeferredUpdates::addUpdate( $update );
851 }
852 }
853
854 // Reparse any pages transcluding this page
855 LinksUpdate::queueRecursiveJobsForTable(
856 $page->getTitle(),
857 'templatelinks',
858 'delete-page',
859 $this->deleter->getUser()->getName(),
860 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
861 );
862 // Reparse any pages including this image
863 if ( $page->getTitle()->getNamespace() === NS_FILE ) {
864 LinksUpdate::queueRecursiveJobsForTable(
865 $page->getTitle(),
866 'imagelinks',
867 'delete-page',
868 $this->deleter->getUser()->getName(),
869 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
870 );
871 }
872
873 if ( !$this->isDeletePageUnitTest ) {
874 // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
875 // Clear caches
877 }
878
879 WikiModule::invalidateModuleCache(
880 $page->getTitle(),
881 $revRecord,
882 null,
883 $this->localWikiID
884 );
885
886 // Reset the page object and the Title object
887 $page->loadFromRow( false, WikiPage::READ_LATEST );
888
889 if ( !$this->isDeletePageUnitTest ) {
890 // TODO Remove conditional once DeferredUpdates is servicified (T265749)
891 // Search engine
892 DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
893 }
894 }
895
906 public function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
907 $slotContent = array_map( static function ( SlotRecord $slot ) {
908 return $slot->getContent();
909 }, $rev->getSlots()->getSlots() );
910
911 $allUpdates = [ new LinksDeletionUpdate( $page ) ];
912
913 // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
914 // model here, not the content object!
915 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
917 $content = null; // in case $slotContent is zero-length
918 foreach ( $slotContent as $role => $content ) {
919 $handler = $content->getContentHandler();
920
921 $updates = $handler->getDeletionUpdates(
922 $page->getTitle(),
923 $role
924 );
925
926 $allUpdates = array_merge( $allUpdates, $updates );
927 }
928
929 $this->hookRunner->onPageDeletionDataUpdates(
930 $page->getTitle(), $rev, $allUpdates );
931
932 // TODO: hard deprecate old hook in 1.33
933 $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
934 return $allUpdates;
935 }
936}
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:265
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'))
The persistent session ID (if any) loaded at startup.
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...
Class for managing the deferral of updates within the scope of a PHP script invocation.
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.
PSR-3 logger instance factory.
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)
doDeleteUpdates(WikiPage $page, RevisionRecord $revRecord)
getDeletionUpdates(WikiPage $page, RevisionRecord $rev)
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.
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:1146
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
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.
fatal( $message,... $parameters)
Add an error and set OK to false, indicating that the operation as a whole was fatal.
static newGood( $value=null)
Factory function for good results.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:46
Base representation for an editable wiki page.
Definition WikiPage.php:75
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition WikiPage.php:838
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:545
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:756
static onArticleDelete(Title $title)
Clears caches when article is deleted.
getId( $wikiId=self::LOCAL)
Definition WikiPage.php:588
getTitle()
Get the title object of the article.
Definition WikiPage.php:318
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:474
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:820
Value object representing a message for i18n.
Base interface for representing page content.
Definition Content.php:37
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