MediaWiki REL1_39
DeletePage.php
Go to the documentation of this file.
1<?php
2
3namespace MediaWiki\Page;
4
5use BadMethodCallException;
6use BagOStuff;
7use ChangeTags;
9use Content;
13use Exception;
15use LogicException;
32use Message;
34use RawMessage;
35use SearchUpdate;
37use Status;
38use StatusValue;
39use Wikimedia\IPUtils;
44use Wikimedia\RequestTimeout\TimeoutException;
45use WikiPage;
46
56 public const CONSTRUCTOR_OPTIONS = [
59 ];
60
64 public const PAGE_BASE = 'base';
65 public const PAGE_TALK = 'talk';
66
68 private $hookRunner;
70 private $revisionStore;
72 private $lbFactory;
74 private $loadBalancer;
76 private $jobQueueGroup;
78 private $commentStore;
80 private $options;
82 private $recentDeletesCache;
84 private $localWikiID;
86 private $webRequestID;
88 private $userFactory;
90 private $backlinkCacheFactory;
92 private $wikiPageFactory;
94 private $namespaceInfo;
96 private $contLangMsgTextFormatter;
97
99 private $isDeletePageUnitTest = false;
100
102 private $page;
104 private $deleter;
105
107 private $suppress = false;
109 private $tags = [];
111 private $logSubtype = 'delete';
113 private $forceImmediate = false;
115 private $associatedTalk;
116
118 private $legacyHookErrors = '';
120 private $mergeLegacyHookErrors = true;
121
126 private $successfulDeletionsIDs;
131 private $wasScheduled;
133 private $attemptedDeletion = false;
134
154 public function __construct(
155 HookContainer $hookContainer,
156 RevisionStore $revisionStore,
157 LBFactory $lbFactory,
158 JobQueueGroup $jobQueueGroup,
159 CommentStore $commentStore,
160 ServiceOptions $serviceOptions,
161 BagOStuff $recentDeletesCache,
162 string $localWikiID,
163 string $webRequestID,
164 WikiPageFactory $wikiPageFactory,
165 UserFactory $userFactory,
166 BacklinkCacheFactory $backlinkCacheFactory,
167 NamespaceInfo $namespaceInfo,
168 ITextFormatter $contLangMsgTextFormatter,
169 ProperPageIdentity $page,
170 Authority $deleter
171 ) {
172 $this->hookRunner = new HookRunner( $hookContainer );
173 $this->revisionStore = $revisionStore;
174 $this->lbFactory = $lbFactory;
175 $this->loadBalancer = $this->lbFactory->getMainLB();
176 $this->jobQueueGroup = $jobQueueGroup;
177 $this->commentStore = $commentStore;
178 $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
179 $this->options = $serviceOptions;
180 $this->recentDeletesCache = $recentDeletesCache;
181 $this->localWikiID = $localWikiID;
182 $this->webRequestID = $webRequestID;
183 $this->wikiPageFactory = $wikiPageFactory;
184 $this->userFactory = $userFactory;
185 $this->backlinkCacheFactory = $backlinkCacheFactory;
186 $this->namespaceInfo = $namespaceInfo;
187 $this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
188
189 $this->page = $wikiPageFactory->newFromTitle( $page );
190 $this->deleter = $deleter;
191 }
192
197 public function getLegacyHookErrors() {
198 return $this->legacyHookErrors;
199 }
200
205 public function keepLegacyHookErrorsSeparate(): self {
206 $this->mergeLegacyHookErrors = false;
207 return $this;
208 }
209
217 public function setSuppress( bool $suppress ): self {
218 $this->suppress = $suppress;
219 return $this;
220 }
221
228 public function setTags( array $tags ): self {
229 $this->tags = $tags;
230 return $this;
231 }
232
239 public function setLogSubtype( string $logSubtype ): self {
240 $this->logSubtype = $logSubtype;
241 return $this;
242 }
243
250 public function forceImmediate( bool $forceImmediate ): self {
251 $this->forceImmediate = $forceImmediate;
252 return $this;
253 }
254
262 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
263 return StatusValue::newFatal( 'delete-error-associated-alreadytalk' );
264 }
265 // FIXME NamespaceInfo should work with PageIdentity
266 $talkPage = $this->wikiPageFactory->newFromLinkTarget(
267 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
268 );
269 if ( !$talkPage->exists() ) {
270 return StatusValue::newFatal( 'delete-error-associated-doesnotexist' );
271 }
272 return StatusValue::newGood();
273 }
274
286 public function setDeleteAssociatedTalk( bool $delete ): self {
287 if ( !$delete ) {
288 $this->associatedTalk = null;
289 return $this;
290 }
291
292 if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
293 throw new BadMethodCallException( "Cannot delete associated talk page of a talk page! ($this->page)" );
294 }
295 // FIXME NamespaceInfo should work with PageIdentity
296 $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
297 $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
298 );
299 return $this;
300 }
301
307 public function setIsDeletePageUnitTest( bool $test ): void {
308 if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
309 throw new BadMethodCallException( __METHOD__ . ' can only be used in tests!' );
310 }
311 $this->isDeletePageUnitTest = $test;
312 }
313
319 public function setDeletionAttempted(): self {
320 $this->attemptedDeletion = true;
321 $this->successfulDeletionsIDs = [ self::PAGE_BASE => null ];
322 $this->wasScheduled = [ self::PAGE_BASE => null ];
323 if ( $this->associatedTalk ) {
324 $this->successfulDeletionsIDs[self::PAGE_TALK] = null;
325 $this->wasScheduled[self::PAGE_TALK] = null;
326 }
327 return $this;
328 }
329
334 private function assertDeletionAttempted(): void {
335 if ( !$this->attemptedDeletion ) {
336 throw new BadMethodCallException( 'No deletion was attempted' );
337 }
338 }
339
344 public function getSuccessfulDeletionsIDs(): array {
345 $this->assertDeletionAttempted();
346 return $this->successfulDeletionsIDs;
347 }
348
354 public function deletionWasScheduled(): bool {
355 wfDeprecated( __METHOD__, '1.38' );
356 $this->assertDeletionAttempted();
357 // @phan-suppress-next-line PhanTypeArraySuspiciousNullable,PhanTypeMismatchReturnNullable
358 return $this->wasScheduled[self::PAGE_BASE];
359 }
360
365 public function deletionsWereScheduled(): array {
366 $this->assertDeletionAttempted();
367 return $this->wasScheduled;
368 }
369
376 public function deleteIfAllowed( string $reason ): StatusValue {
377 $this->setDeletionAttempted();
378 $status = $this->authorizeDeletion();
379 if ( !$status->isGood() ) {
380 return $status;
381 }
382
383 return $this->deleteUnsafe( $reason );
384 }
385
389 private function authorizeDeletion(): PermissionStatus {
390 $status = PermissionStatus::newEmpty();
391 $this->deleter->authorizeWrite( 'delete', $this->page, $status );
392 if ( $this->associatedTalk ) {
393 $this->deleter->authorizeWrite( 'delete', $this->associatedTalk, $status );
394 }
395 if ( !$this->deleter->isAllowed( 'bigdelete' ) && $this->isBigDeletion() ) {
396 $status->fatal(
397 'delete-toomanyrevisions',
398 Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
399 );
400 }
401 if ( $this->tags ) {
402 $status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->deleter ) );
403 }
404 return $status;
405 }
406
410 private function isBigDeletion(): bool {
411 $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
412 if ( !$revLimit ) {
413 return false;
414 }
415
416 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
417 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
418 if ( $this->associatedTalk ) {
419 $revCount += $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
420 }
421
422 return $revCount > $revLimit;
423 }
424
437 public function isBatchedDelete( int $safetyMargin = 0 ): bool {
438 $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
439 $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
440 $revCount += $safetyMargin;
441
442 if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
443 return true;
444 } elseif ( !$this->associatedTalk ) {
445 return false;
446 }
447
448 $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
449 $talkRevCount += $safetyMargin;
450
451 return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
452 }
453
464 public function deleteUnsafe( string $reason ): Status {
465 $this->setDeletionAttempted();
466 $origReason = $reason;
467 $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
468 if ( !$hookStatus->isGood() ) {
469 return $hookStatus;
470 }
471 if ( $this->associatedTalk ) {
472 $talkReason = $this->contLangMsgTextFormatter->format(
473 MessageValue::new( 'delete-talk-summary-prefix' )->plaintextParams( $origReason )
474 );
475 $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
476 if ( !$talkHookStatus->isGood() ) {
477 return $talkHookStatus;
478 }
479 }
480
481 $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
482 if ( !$this->associatedTalk || !$status->isGood() ) {
483 return $status;
484 }
485 // NOTE: If the page deletion above failed because the page is no longer there (e.g. race condition) we'll
486 // still try to delete the talk page, since it was the user's intention anyway.
487 // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable talkReason is set when used
488 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable talkReason is set when used
489 $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
490 return $status;
491 }
492
498 private function runPreDeleteHooks( WikiPage $page, string &$reason ): Status {
499 $status = Status::newGood();
500
501 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
502 if ( !$this->hookRunner->onArticleDelete(
503 $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
504 ) {
505 if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !== '' ) {
506 if ( is_string( $this->legacyHookErrors ) ) {
507 $this->legacyHookErrors = [ $this->legacyHookErrors ];
508 }
509 foreach ( $this->legacyHookErrors as $legacyError ) {
510 $status->fatal( new RawMessage( $legacyError ) );
511 }
512 }
513 if ( $status->isOK() ) {
514 // Hook aborted but didn't set a fatal status
515 $status->fatal( 'delete-hook-aborted' );
516 }
517 return $status;
518 }
519
520 // Use a new Status in case a hook handler put something here without aborting.
521 $status = Status::newGood();
522 $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
523 if ( !$hookRes && !$status->isGood() ) {
524 // Note: as per the PageDeleteHook documentation, `return false` is ignored if $status is good.
525 return $status;
526 }
527 return Status::newGood();
528 }
529
544 public function deleteInternal(
545 WikiPage $page,
546 string $pageRole,
547 string $reason,
548 ?string $webRequestId = null
549 ): Status {
550 $title = $page->getTitle();
551 $status = Status::newGood();
552
553 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
554 $dbw->startAtomic( __METHOD__ );
555
556 $page->loadPageData( WikiPage::READ_LATEST );
557 $id = $page->getId();
558 // T98706: lock the page from various other updates but avoid using
559 // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
560 // the revisions queries (which also JOIN on user). Only lock the page
561 // row and CAS check on page_latest to see if the trx snapshot matches.
562 $lockedLatest = $page->lockAndGetLatest();
563 if ( $id === 0 || $page->getLatest() !== $lockedLatest ) {
564 $dbw->endAtomic( __METHOD__ );
565 // Page not there or trx snapshot is stale
566 $status->error( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) );
567 return $status;
568 }
569
570 // At this point we are now committed to returning an OK
571 // status unless some DB query error or other exception comes up.
572 // This way callers don't have to call rollback() if $status is bad
573 // unless they actually try to catch exceptions (which is rare).
574
575 // we need to remember the old content so we can use it to generate all deletion updates.
576 $revisionRecord = $page->getRevisionRecord();
577 if ( !$revisionRecord ) {
578 throw new LogicException( "No revisions for $page?" );
579 }
580 try {
581 $content = $page->getContent( RevisionRecord::RAW );
582 } catch ( TimeoutException $e ) {
583 throw $e;
584 } catch ( Exception $ex ) {
585 wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
586 . $ex->getMessage() );
587
588 $content = null;
589 }
590
591 // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
592 // one batch of revisions and defer archival of any others to the job queue.
593 $explictTrxLogged = false;
594 while ( true ) {
595 $done = $this->archiveRevisions( $page, $id );
596 if ( $done || !$this->forceImmediate ) {
597 break;
598 }
599 $dbw->endAtomic( __METHOD__ );
600 if ( $dbw->explicitTrxActive() ) {
601 // Explicit transactions may never happen here in practice. Log to be sure.
602 if ( !$explictTrxLogged ) {
603 $explictTrxLogged = true;
604 LoggerFactory::getInstance( 'wfDebug' )->debug(
605 'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
606 'title' => $title->getText(),
607 ] );
608 }
609 continue;
610 }
611 if ( $dbw->trxLevel() ) {
612 $dbw->commit( __METHOD__ );
613 }
614 $this->lbFactory->waitForReplication();
615 $dbw->startAtomic( __METHOD__ );
616 }
617
618 if ( !$done ) {
619 $dbw->endAtomic( __METHOD__ );
620
621 $jobParams = [
622 'namespace' => $title->getNamespace(),
623 'title' => $title->getDBkey(),
624 'wikiPageId' => $id,
625 'requestId' => $webRequestId ?? $this->webRequestID,
626 'reason' => $reason,
627 'suppress' => $this->suppress,
628 'userId' => $this->deleter->getUser()->getId(),
629 'tags' => json_encode( $this->tags ),
630 'logsubtype' => $this->logSubtype,
631 'pageRole' => $pageRole,
632 ];
633
634 $job = new DeletePageJob( $jobParams );
635 $this->jobQueueGroup->push( $job );
636 $this->wasScheduled[$pageRole] = true;
637 return $status;
638 }
639 $this->wasScheduled[$pageRole] = false;
640
641 // Get archivedRevisionCount by db query, because there's no better alternative.
642 // Jobs cannot pass a count of archived revisions to the next job, because additional
643 // deletion operations can be started while the first is running. Jobs from each
644 // gracefully interleave, but would not know about each other's count. Deduplication
645 // in the job queue to avoid simultaneous deletion operations would add overhead.
646 // Number of archived revisions cannot be known beforehand, because edits can be made
647 // while deletion operations are being processed, changing the number of archivals.
648 $archivedRevisionCount = $dbw->selectRowCount(
649 'archive',
650 '*',
651 [
652 'ar_namespace' => $title->getNamespace(),
653 'ar_title' => $title->getDBkey(),
654 'ar_page_id' => $id
655 ], __METHOD__
656 );
657
658 // Clone the title and wikiPage, so we have the information we need when
659 // we log and run the ArticleDeleteComplete hook.
660 $logTitle = clone $title;
661 $wikiPageBeforeDelete = clone $page;
662
663 // Now that it's safely backed up, delete it
664 $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
665
666 // Log the deletion, if the page was suppressed, put it in the suppression log instead
667 $logtype = $this->suppress ? 'suppress' : 'delete';
668
669 $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
670 $logEntry->setPerformer( $this->deleter->getUser() );
671 $logEntry->setTarget( $logTitle );
672 $logEntry->setComment( $reason );
673 $logEntry->addTags( $this->tags );
674 if ( !$this->isDeletePageUnitTest ) {
675 // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
676 $logid = $logEntry->insert();
677
678 $dbw->onTransactionPreCommitOrIdle(
679 static function () use ( $logEntry, $logid ) {
680 // T58776: avoid deadlocks (especially from FileDeleteForm)
681 $logEntry->publish( $logid );
682 },
683 __METHOD__
684 );
685 } else {
686 $logid = 42;
687 }
688
689 $dbw->endAtomic( __METHOD__ );
690
691 $this->doDeleteUpdates( $page, $revisionRecord );
692
693 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
694 $this->hookRunner->onArticleDeleteComplete(
695 $wikiPageBeforeDelete,
696 $legacyDeleter,
697 $reason,
698 $id,
699 $content,
700 $logEntry,
701 $archivedRevisionCount
702 );
703 $this->hookRunner->onPageDeleteComplete(
704 $wikiPageBeforeDelete,
705 $this->deleter,
706 $reason,
707 $id,
708 $revisionRecord,
709 $logEntry,
710 $archivedRevisionCount
711 );
712 $this->successfulDeletionsIDs[$pageRole] = $logid;
713
714 // Show log excerpt on 404 pages rather than just a link
715 $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
716 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
717
718 return $status;
719 }
720
728 private function archiveRevisions( WikiPage $page, int $id ): bool {
729 // Given the lock above, we can be confident in the title and page ID values
730 $namespace = $page->getTitle()->getNamespace();
731 $dbKey = $page->getTitle()->getDBkey();
732
733 $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
734
735 $revQuery = $this->revisionStore->getQueryInfo();
736 $bitfield = false;
737
738 // Bitfields to further suppress the content
739 if ( $this->suppress ) {
740 $bitfield = RevisionRecord::SUPPRESSED_ALL;
741 $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
742 }
743
744 // For now, shunt the revision data into the archive table.
745 // Text is *not* removed from the text table; bulk storage
746 // is left intact to avoid breaking block-compression or
747 // immutable storage schemes.
748 // In the future, we may keep revisions and mark them with
749 // the rev_deleted field, which is reserved for this purpose.
750
751 // Lock rows in `revision` and its temp tables, but not any others.
752 // Note array_intersect() preserves keys from the first arg, and we're
753 // assuming $revQuery has `revision` primary and isn't using subtables
754 // for anything we care about.
755 $dbw->lockForUpdate(
756 array_intersect(
757 $revQuery['tables'],
758 [ 'revision', 'revision_comment_temp' ]
759 ),
760 [ 'rev_page' => $id ],
761 __METHOD__,
762 [],
763 $revQuery['joins']
764 );
765
766 $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
767 // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
768 // unusual case where there were exactly $deleteBatchSize revisions remaining.
769 $res = $dbw->select(
770 $revQuery['tables'],
771 $revQuery['fields'],
772 [ 'rev_page' => $id ],
773 __METHOD__,
774 [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
775 $revQuery['joins']
776 );
777
778 // Build their equivalent archive rows
779 $rowsInsert = [];
780 $revids = [];
781
783 $ipRevIds = [];
784
785 $done = true;
786 foreach ( $res as $row ) {
787 if ( count( $revids ) >= $deleteBatchSize ) {
788 $done = false;
789 break;
790 }
791
792 $comment = $this->commentStore->getComment( 'rev_comment', $row );
793 $rowInsert = [
794 'ar_namespace' => $namespace,
795 'ar_title' => $dbKey,
796 'ar_actor' => $row->rev_actor,
797 'ar_timestamp' => $row->rev_timestamp,
798 'ar_minor_edit' => $row->rev_minor_edit,
799 'ar_rev_id' => $row->rev_id,
800 'ar_parent_id' => $row->rev_parent_id,
801 'ar_len' => $row->rev_len,
802 'ar_page_id' => $id,
803 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
804 'ar_sha1' => $row->rev_sha1,
805 ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
806
807 $rowsInsert[] = $rowInsert;
808 $revids[] = $row->rev_id;
809
810 // Keep track of IP edits, so that the corresponding rows can
811 // be deleted in the ip_changes table.
812 if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
813 $ipRevIds[] = $row->rev_id;
814 }
815 }
816
817 if ( count( $revids ) > 0 ) {
818 // Copy them into the archive table
819 $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
820
821 $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
822 $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
823 // Also delete records from ip_changes as applicable.
824 if ( count( $ipRevIds ) > 0 ) {
825 $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
826 }
827 }
828
829 return $done;
830 }
831
841 public 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 if ( !$this->isDeletePageUnitTest ) {
854 // TODO Remove conditional once DeferredUpdates is servicified (T265749)
855 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
856 [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
857 ) );
858
859 // Delete pagelinks, update secondary indexes, etc
860 $updates = $this->getDeletionUpdates( $page, $revRecord );
861 foreach ( $updates as $update ) {
862 DeferredUpdates::addUpdate( $update );
863 }
864 }
865
866 // Reparse any pages transcluding this page
867 LinksUpdate::queueRecursiveJobsForTable(
868 $page->getTitle(),
869 'templatelinks',
870 'delete-page',
871 $this->deleter->getUser()->getName(),
872 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
873 );
874 // Reparse any pages including this image
875 if ( $page->getTitle()->getNamespace() === NS_FILE ) {
876 LinksUpdate::queueRecursiveJobsForTable(
877 $page->getTitle(),
878 'imagelinks',
879 'delete-page',
880 $this->deleter->getUser()->getName(),
881 $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
882 );
883 }
884
885 if ( !$this->isDeletePageUnitTest ) {
886 // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
887 // Clear caches
889 }
890
891 WikiModule::invalidateModuleCache(
892 $page->getTitle(),
893 $revRecord,
894 null,
895 $this->localWikiID
896 );
897
898 // Reset the page object and the Title object
899 $page->loadFromRow( false, WikiPage::READ_LATEST );
900
901 if ( !$this->isDeletePageUnitTest ) {
902 // TODO Remove conditional once DeferredUpdates is servicified (T265749)
903 // Search engine
904 DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
905 }
906 }
907
918 public function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
919 $slotContent = array_map( static function ( SlotRecord $slot ) {
920 return $slot->getContent();
921 }, $rev->getSlots()->getSlots() );
922
923 $allUpdates = [ new LinksDeletionUpdate( $page ) ];
924
925 // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
926 // model here, not the content object!
927 // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
929 $content = null; // in case $slotContent is zero-length
930 foreach ( $slotContent as $role => $content ) {
931 $handler = $content->getContentHandler();
932
933 $updates = $handler->getDeletionUpdates(
934 $page->getTitle(),
935 $role
936 );
937
938 $allUpdates = array_merge( $allUpdates, $updates );
939 }
940
941 $this->hookRunner->onPageDeletionDataUpdates(
942 $page->getTitle(), $rev, $allUpdates );
943
944 // TODO: hard deprecate old hook in 1.33
945 $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
946 return $allUpdates;
947 }
948}
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,...
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
Definition WebStart.php:82
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Handle database storage of comments such as edit summaries and log reasons.
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.
Class DeletePageJob.
Class to handle enqueueing of background jobs.
Class for creating new log entries and inserting them into the database.
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...
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:140
static numParam( $num)
Definition Message.php:1145
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Variant of the Message class.
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:44
Base representation for an editable wiki page.
Definition WikiPage.php:62
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition WikiPage.php:826
loadFromRow( $data, $from)
Load the object from a database row.
Definition WikiPage.php:530
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:741
static onArticleDelete(Title $title)
Clears caches when article is deleted.
getId( $wikiId=self::LOCAL)
Definition WikiPage.php:573
getTitle()
Get the title object of the article.
Definition WikiPage.php:303
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition WikiPage.php:459
getRevisionRecord()
Get the latest revision.
Definition WikiPage.php:805
Value object representing a message for i18n.
Base interface for content objects.
Definition Content.php:35
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
getMainLB( $domain=false)
Get the tracked load balancer instance for the main cluster that handles the given domain.
Create and track the database connections and transactions for a given database cluster.
const DB_REPLICA
Definition defines.php:26
const DB_PRIMARY
Definition defines.php:28
if(count( $args)< 1) $job
$content
Definition router.php:76
return true
Definition router.php:92