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