MediaWiki  master
DeletePage.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Page;
4 
5 use BadMethodCallException;
6 use BagOStuff;
7 use ChangeTags;
8 use CommentStore;
9 use Content;
11 use DeferredUpdates;
12 use DeletePageJob;
13 use Exception;
14 use JobQueueGroup;
15 use LogicException;
16 use ManualLogEntry;
31 use Message;
32 use NamespaceInfo;
33 use RawMessage;
35 use SearchUpdate;
36 use SiteStatsUpdate;
37 use Status;
38 use StatusValue;
39 use Wikimedia\IPUtils;
44 use Wikimedia\RequestTimeout\TimeoutException;
45 use WikiPage;
46 
52 class DeletePage {
56  public const CONSTRUCTOR_OPTIONS = [
60  ];
61 
65  public const PAGE_BASE = 'base';
66  public const PAGE_TALK = 'talk';
67 
69  private $hookRunner;
71  private $revisionStore;
73  private $lbFactory;
75  private $loadBalancer;
77  private $jobQueueGroup;
79  private $commentStore;
81  private $options;
85  private $localWikiID;
87  private $webRequestID;
89  private $userFactory;
95  private $namespaceInfo;
98 
100  private $isDeletePageUnitTest = false;
101 
103  private $page;
105  private $deleter;
106 
108  private $suppress = false;
110  private $tags = [];
112  private $logSubtype = 'delete';
114  private $forceImmediate = false;
117 
119  private $legacyHookErrors = '';
121  private $mergeLegacyHookErrors = true;
122 
132  private $wasScheduled;
134  private $attemptedDeletion = false;
135 
155  public function __construct(
156  HookContainer $hookContainer,
157  RevisionStore $revisionStore,
158  LBFactory $lbFactory,
161  ServiceOptions $serviceOptions,
163  string $localWikiID,
164  string $webRequestID,
166  UserFactory $userFactory,
167  BacklinkCacheFactory $backlinkCacheFactory,
169  ITextFormatter $contLangMsgTextFormatter,
171  Authority $deleter
172  ) {
173  $this->hookRunner = new HookRunner( $hookContainer );
174  $this->revisionStore = $revisionStore;
175  $this->lbFactory = $lbFactory;
176  $this->loadBalancer = $this->lbFactory->getMainLB();
177  $this->jobQueueGroup = $jobQueueGroup;
178  $this->commentStore = $commentStore;
179  $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
180  $this->options = $serviceOptions;
181  $this->recentDeletesCache = $recentDeletesCache;
182  $this->localWikiID = $localWikiID;
183  $this->webRequestID = $webRequestID;
184  $this->wikiPageFactory = $wikiPageFactory;
185  $this->userFactory = $userFactory;
186  $this->backlinkCacheFactory = $backlinkCacheFactory;
187  $this->namespaceInfo = $namespaceInfo;
188  $this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
189 
190  $this->page = $wikiPageFactory->newFromTitle( $page );
191  $this->deleter = $deleter;
192  }
193 
198  public function getLegacyHookErrors() {
200  }
201 
206  public function keepLegacyHookErrorsSeparate(): self {
207  $this->mergeLegacyHookErrors = false;
208  return $this;
209  }
210 
218  public function setSuppress( bool $suppress ): self {
219  $this->suppress = $suppress;
220  return $this;
221  }
222 
229  public function setTags( array $tags ): self {
230  $this->tags = $tags;
231  return $this;
232  }
233 
240  public function setLogSubtype( string $logSubtype ): self {
241  $this->logSubtype = $logSubtype;
242  return $this;
243  }
244 
251  public function forceImmediate( bool $forceImmediate ): self {
252  $this->forceImmediate = $forceImmediate;
253  return $this;
254  }
255 
263  if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
264  return StatusValue::newFatal( 'delete-error-associated-alreadytalk' );
265  }
266  // FIXME NamespaceInfo should work with PageIdentity
267  $talkPage = $this->wikiPageFactory->newFromLinkTarget(
268  $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
269  );
270  if ( !$talkPage->exists() ) {
271  return StatusValue::newFatal( 'delete-error-associated-doesnotexist' );
272  }
273  return StatusValue::newGood();
274  }
275 
287  public function setDeleteAssociatedTalk( bool $delete ): self {
288  if ( !$delete ) {
289  $this->associatedTalk = null;
290  return $this;
291  }
292 
293  if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
294  throw new BadMethodCallException( "Cannot delete associated talk page of a talk page! ($this->page)" );
295  }
296  // FIXME NamespaceInfo should work with PageIdentity
297  $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
298  $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
299  );
300  return $this;
301  }
302 
308  public function setIsDeletePageUnitTest( bool $test ): void {
309  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
310  throw new BadMethodCallException( __METHOD__ . ' can only be used in tests!' );
311  }
312  $this->isDeletePageUnitTest = $test;
313  }
314 
320  public function setDeletionAttempted(): self {
321  $this->attemptedDeletion = true;
322  $this->successfulDeletionsIDs = [ self::PAGE_BASE => null ];
323  $this->wasScheduled = [ self::PAGE_BASE => null ];
324  if ( $this->associatedTalk ) {
325  $this->successfulDeletionsIDs[self::PAGE_TALK] = null;
326  $this->wasScheduled[self::PAGE_TALK] = null;
327  }
328  return $this;
329  }
330 
335  private function assertDeletionAttempted(): void {
336  if ( !$this->attemptedDeletion ) {
337  throw new BadMethodCallException( 'No deletion was attempted' );
338  }
339  }
340 
345  public function getSuccessfulDeletionsIDs(): array {
346  $this->assertDeletionAttempted();
347  return $this->successfulDeletionsIDs;
348  }
349 
355  public function deletionWasScheduled(): bool {
356  wfDeprecated( __METHOD__, '1.38' );
357  $this->assertDeletionAttempted();
358  // @phan-suppress-next-line PhanTypeArraySuspiciousNullable,PhanTypeMismatchReturnNullable
359  return $this->wasScheduled[self::PAGE_BASE];
360  }
361 
366  public function deletionsWereScheduled(): array {
367  $this->assertDeletionAttempted();
368  return $this->wasScheduled;
369  }
370 
377  public function deleteIfAllowed( string $reason ): StatusValue {
378  $this->setDeletionAttempted();
379  $status = $this->authorizeDeletion();
380  if ( !$status->isGood() ) {
381  return $status;
382  }
383 
384  return $this->deleteUnsafe( $reason );
385  }
386 
390  private function authorizeDeletion(): PermissionStatus {
391  $status = PermissionStatus::newEmpty();
392  $this->deleter->authorizeWrite( 'delete', $this->page, $status );
393  if ( $this->associatedTalk ) {
394  $this->deleter->authorizeWrite( 'delete', $this->associatedTalk, $status );
395  }
396  if ( !$this->deleter->isAllowed( 'bigdelete' ) && $this->isBigDeletion() ) {
397  $status->fatal(
398  'delete-toomanyrevisions',
399  Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
400  );
401  }
402  if ( $this->tags ) {
403  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->deleter ) );
404  }
405  return $status;
406  }
407 
411  private function isBigDeletion(): bool {
412  $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
413  if ( !$revLimit ) {
414  return false;
415  }
416 
417  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
418  $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
419  if ( $this->associatedTalk ) {
420  $revCount += $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
421  }
422 
423  return $revCount > $revLimit;
424  }
425 
438  public function isBatchedDelete( int $safetyMargin = 0 ): bool {
439  $dbr = $this->loadBalancer->getConnectionRef( DB_REPLICA );
440  $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
441  $revCount += $safetyMargin;
442 
443  if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
444  return true;
445  } elseif ( !$this->associatedTalk ) {
446  return false;
447  }
448 
449  $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
450  $talkRevCount += $safetyMargin;
451 
452  return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
453  }
454 
465  public function deleteUnsafe( string $reason ): Status {
466  $this->setDeletionAttempted();
467  $origReason = $reason;
468  $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
469  if ( !$hookStatus->isGood() ) {
470  return $hookStatus;
471  }
472  if ( $this->associatedTalk ) {
473  $talkReason = $this->contLangMsgTextFormatter->format(
474  MessageValue::new( 'delete-talk-summary-prefix' )->plaintextParams( $origReason )
475  );
476  $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
477  if ( !$talkHookStatus->isGood() ) {
478  return $talkHookStatus;
479  }
480  }
481 
482  $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
483  if ( !$this->associatedTalk || !$status->isGood() ) {
484  return $status;
485  }
486  // NOTE: If the page deletion above failed because the page is no longer there (e.g. race condition) we'll
487  // still try to delete the talk page, since it was the user's intention anyway.
488  // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable talkReason is set when used
489  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable talkReason is set when used
490  $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
491  return $status;
492  }
493 
499  private function runPreDeleteHooks( WikiPage $page, string &$reason ): Status {
500  $status = Status::newGood();
501 
502  $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
503  if ( !$this->hookRunner->onArticleDelete(
504  $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
505  ) {
506  if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !== '' ) {
507  if ( is_string( $this->legacyHookErrors ) ) {
508  $this->legacyHookErrors = [ $this->legacyHookErrors ];
509  }
510  foreach ( $this->legacyHookErrors as $legacyError ) {
511  $status->fatal( new RawMessage( $legacyError ) );
512  }
513  }
514  if ( $status->isOK() ) {
515  // Hook aborted but didn't set a fatal status
516  $status->fatal( 'delete-hook-aborted' );
517  }
518  return $status;
519  }
520 
521  // Use a new Status in case a hook handler put something here without aborting.
522  $status = Status::newGood();
523  $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
524  if ( !$hookRes && !$status->isGood() ) {
525  // Note: as per the PageDeleteHook documentation, `return false` is ignored if $status is good.
526  return $status;
527  }
528  return Status::newGood();
529  }
530 
545  public function deleteInternal(
546  WikiPage $page,
547  string $pageRole,
548  string $reason,
549  ?string $webRequestId = null
550  ): Status {
551  $title = $page->getTitle();
552  $status = Status::newGood();
553 
554  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
555  $dbw->startAtomic( __METHOD__ );
556 
557  $page->loadPageData( WikiPage::READ_LATEST );
558  $id = $page->getId();
559  // T98706: lock the page from various other updates but avoid using
560  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
561  // the revisions queries (which also JOIN on user). Only lock the page
562  // row and CAS check on page_latest to see if the trx snapshot matches.
563  $lockedLatest = $page->lockAndGetLatest();
564  if ( $id === 0 || $page->getLatest() !== $lockedLatest ) {
565  $dbw->endAtomic( __METHOD__ );
566  // Page not there or trx snapshot is stale
567  $status->error( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) );
568  return $status;
569  }
570 
571  // At this point we are now committed to returning an OK
572  // status unless some DB query error or other exception comes up.
573  // This way callers don't have to call rollback() if $status is bad
574  // unless they actually try to catch exceptions (which is rare).
575 
576  // we need to remember the old content so we can use it to generate all deletion updates.
577  $revisionRecord = $page->getRevisionRecord();
578  if ( !$revisionRecord ) {
579  throw new LogicException( "No revisions for $page?" );
580  }
581  try {
582  $content = $page->getContent( RevisionRecord::RAW );
583  } catch ( TimeoutException $e ) {
584  throw $e;
585  } catch ( Exception $ex ) {
586  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
587  . $ex->getMessage() );
588 
589  $content = null;
590  }
591 
592  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
593  // one batch of revisions and defer archival of any others to the job queue.
594  $explictTrxLogged = false;
595  while ( true ) {
596  $done = $this->archiveRevisions( $page, $id );
597  if ( $done || !$this->forceImmediate ) {
598  break;
599  }
600  $dbw->endAtomic( __METHOD__ );
601  if ( $dbw->explicitTrxActive() ) {
602  // Explicit transactions may never happen here in practice. Log to be sure.
603  if ( !$explictTrxLogged ) {
604  $explictTrxLogged = true;
605  LoggerFactory::getInstance( 'wfDebug' )->debug(
606  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
607  'title' => $title->getText(),
608  ] );
609  }
610  continue;
611  }
612  if ( $dbw->trxLevel() ) {
613  $dbw->commit( __METHOD__ );
614  }
615  $this->lbFactory->waitForReplication();
616  $dbw->startAtomic( __METHOD__ );
617  }
618 
619  if ( !$done ) {
620  $dbw->endAtomic( __METHOD__ );
621 
622  $jobParams = [
623  'namespace' => $title->getNamespace(),
624  'title' => $title->getDBkey(),
625  'wikiPageId' => $id,
626  'requestId' => $webRequestId ?? $this->webRequestID,
627  'reason' => $reason,
628  'suppress' => $this->suppress,
629  'userId' => $this->deleter->getUser()->getId(),
630  'tags' => json_encode( $this->tags ),
631  'logsubtype' => $this->logSubtype,
632  'pageRole' => $pageRole,
633  ];
634 
635  $job = new DeletePageJob( $jobParams );
636  $this->jobQueueGroup->push( $job );
637  $this->wasScheduled[$pageRole] = true;
638  return $status;
639  }
640  $this->wasScheduled[$pageRole] = false;
641 
642  // Get archivedRevisionCount by db query, because there's no better alternative.
643  // Jobs cannot pass a count of archived revisions to the next job, because additional
644  // deletion operations can be started while the first is running. Jobs from each
645  // gracefully interleave, but would not know about each other's count. Deduplication
646  // in the job queue to avoid simultaneous deletion operations would add overhead.
647  // Number of archived revisions cannot be known beforehand, because edits can be made
648  // while deletion operations are being processed, changing the number of archivals.
649  $archivedRevisionCount = $dbw->selectRowCount(
650  'archive',
651  '*',
652  [
653  'ar_namespace' => $title->getNamespace(),
654  'ar_title' => $title->getDBkey(),
655  'ar_page_id' => $id
656  ], __METHOD__
657  );
658 
659  // Clone the title and wikiPage, so we have the information we need when
660  // we log and run the ArticleDeleteComplete hook.
661  $logTitle = clone $title;
662  $wikiPageBeforeDelete = clone $page;
663 
664  // Now that it's safely backed up, delete it
665  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
666 
667  // Log the deletion, if the page was suppressed, put it in the suppression log instead
668  $logtype = $this->suppress ? 'suppress' : 'delete';
669 
670  $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
671  $logEntry->setPerformer( $this->deleter->getUser() );
672  $logEntry->setTarget( $logTitle );
673  $logEntry->setComment( $reason );
674  $logEntry->addTags( $this->tags );
675  if ( !$this->isDeletePageUnitTest ) {
676  // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
677  $logid = $logEntry->insert();
678 
679  $dbw->onTransactionPreCommitOrIdle(
680  static function () use ( $logEntry, $logid ) {
681  // T58776: avoid deadlocks (especially from FileDeleteForm)
682  $logEntry->publish( $logid );
683  },
684  __METHOD__
685  );
686  } else {
687  $logid = 42;
688  }
689 
690  $dbw->endAtomic( __METHOD__ );
691 
692  $this->doDeleteUpdates( $page, $revisionRecord );
693 
694  $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
695  $this->hookRunner->onArticleDeleteComplete(
696  $wikiPageBeforeDelete,
697  $legacyDeleter,
698  $reason,
699  $id,
700  $content,
701  $logEntry,
702  $archivedRevisionCount
703  );
704  $this->hookRunner->onPageDeleteComplete(
705  $wikiPageBeforeDelete,
706  $this->deleter,
707  $reason,
708  $id,
709  $revisionRecord,
710  $logEntry,
711  $archivedRevisionCount
712  );
713  $this->successfulDeletionsIDs[$pageRole] = $logid;
714 
715  // Show log excerpt on 404 pages rather than just a link
716  $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
717  $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
718 
719  return $status;
720  }
721 
729  private function archiveRevisions( WikiPage $page, int $id ): bool {
730  // Given the lock above, we can be confident in the title and page ID values
731  $namespace = $page->getTitle()->getNamespace();
732  $dbKey = $page->getTitle()->getDBkey();
733 
734  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
735 
736  $revQuery = $this->revisionStore->getQueryInfo();
737  $bitfield = false;
738 
739  // Bitfields to further suppress the content
740  if ( $this->suppress ) {
741  $bitfield = RevisionRecord::SUPPRESSED_ALL;
742  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
743  }
744 
745  // For now, shunt the revision data into the archive table.
746  // Text is *not* removed from the text table; bulk storage
747  // is left intact to avoid breaking block-compression or
748  // immutable storage schemes.
749  // In the future, we may keep revisions and mark them with
750  // the rev_deleted field, which is reserved for this purpose.
751 
752  // Lock rows in `revision` and its temp tables, but not any others.
753  // Note array_intersect() preserves keys from the first arg, and we're
754  // assuming $revQuery has `revision` primary and isn't using subtables
755  // for anything we care about.
756  $dbw->lockForUpdate(
757  array_intersect(
758  $revQuery['tables'],
759  [ 'revision', 'revision_comment_temp', 'revision_actor_temp' ]
760  ),
761  [ 'rev_page' => $id ],
762  __METHOD__,
763  [],
764  $revQuery['joins']
765  );
766 
767  $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
768  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
769  // unusual case where there were exactly $deleteBatchSize revisions remaining.
770  $res = $dbw->select(
771  $revQuery['tables'],
772  $revQuery['fields'],
773  [ 'rev_page' => $id ],
774  __METHOD__,
775  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
776  $revQuery['joins']
777  );
778 
779  // Build their equivalent archive rows
780  $rowsInsert = [];
781  $revids = [];
782 
784  $ipRevIds = [];
785 
786  $done = true;
787  foreach ( $res as $row ) {
788  if ( count( $revids ) >= $deleteBatchSize ) {
789  $done = false;
790  break;
791  }
792 
793  $comment = $this->commentStore->getComment( 'rev_comment', $row );
794  $rowInsert = [
795  'ar_namespace' => $namespace,
796  'ar_title' => $dbKey,
797  'ar_actor' => $row->rev_actor,
798  'ar_timestamp' => $row->rev_timestamp,
799  'ar_minor_edit' => $row->rev_minor_edit,
800  'ar_rev_id' => $row->rev_id,
801  'ar_parent_id' => $row->rev_parent_id,
802  'ar_len' => $row->rev_len,
803  'ar_page_id' => $id,
804  'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
805  'ar_sha1' => $row->rev_sha1,
806  ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
807 
808  $rowsInsert[] = $rowInsert;
809  $revids[] = $row->rev_id;
810 
811  // Keep track of IP edits, so that the corresponding rows can
812  // be deleted in the ip_changes table.
813  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
814  $ipRevIds[] = $row->rev_id;
815  }
816  }
817 
818  if ( count( $revids ) > 0 ) {
819  // Copy them into the archive table
820  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
821 
822  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
823  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
824  if ( $this->options->get( MainConfigNames::ActorTableSchemaMigrationStage )
826  $dbw->delete( 'revision_actor_temp', [ 'revactor_rev' => $revids ], __METHOD__ );
827  }
828 
829  // Also delete records from ip_changes as applicable.
830  if ( count( $ipRevIds ) > 0 ) {
831  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
832  }
833  }
834 
835  return $done;
836  }
837 
847  public function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
848  try {
849  $countable = $page->isCountable();
850  } catch ( TimeoutException $e ) {
851  throw $e;
852  } catch ( Exception $ex ) {
853  // fallback for deleting broken pages for which we cannot load the content for
854  // some reason. Note that doDeleteArticleReal() already logged this problem.
855  $countable = false;
856  }
857 
858  // Update site status
859  if ( !$this->isDeletePageUnitTest ) {
860  // TODO Remove conditional once DeferredUpdates is servicified (T265749)
862  [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
863  ) );
864 
865  // Delete pagelinks, update secondary indexes, etc
866  $updates = $this->getDeletionUpdates( $page, $revRecord );
867  foreach ( $updates as $update ) {
868  DeferredUpdates::addUpdate( $update );
869  }
870  }
871 
872  // Reparse any pages transcluding this page
873  LinksUpdate::queueRecursiveJobsForTable(
874  $page->getTitle(),
875  'templatelinks',
876  'delete-page',
877  $this->deleter->getUser()->getName(),
878  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
879  );
880  // Reparse any pages including this image
881  if ( $page->getTitle()->getNamespace() === NS_FILE ) {
882  LinksUpdate::queueRecursiveJobsForTable(
883  $page->getTitle(),
884  'imagelinks',
885  'delete-page',
886  $this->deleter->getUser()->getName(),
887  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
888  );
889  }
890 
891  if ( !$this->isDeletePageUnitTest ) {
892  // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
893  // Clear caches
895  }
896 
898  $page->getTitle(),
899  $revRecord,
900  null,
901  $this->localWikiID
902  );
903 
904  // Reset the page object and the Title object
905  $page->loadFromRow( false, WikiPage::READ_LATEST );
906 
907  if ( !$this->isDeletePageUnitTest ) {
908  // TODO Remove conditional once DeferredUpdates is servicified (T265749)
909  // Search engine
910  DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
911  }
912  }
913 
924  public function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
925  $slotContent = array_map( static function ( SlotRecord $slot ) {
926  return $slot->getContent();
927  }, $rev->getSlots()->getSlots() );
928 
929  $allUpdates = [ new LinksDeletionUpdate( $page ) ];
930 
931  // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
932  // model here, not the content object!
933  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
935  $content = null; // in case $slotContent is zero-length
936  foreach ( $slotContent as $role => $content ) {
937  $handler = $content->getContentHandler();
938 
939  $updates = $handler->getDeletionUpdates(
940  $page->getTitle(),
941  $role
942  );
943 
944  $allUpdates = array_merge( $allUpdates, $updates );
945  }
946 
947  $this->hookRunner->onPageDeletionDataUpdates(
948  $page->getTitle(), $rev, $allUpdates );
949 
950  // TODO: hard deprecate old hook in 1.33
951  $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
952  return $allUpdates;
953  }
954 }
const SCHEMA_COMPAT_WRITE_TEMP
Definition: Defines.php:266
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:87
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...
Definition: ChangeTags.php:629
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.
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.
Definition: LinksUpdate.php:55
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:562
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
const ActorTableSchemaMigrationStage
Name constant for the ActorTableSchemaMigrationStage setting, for use with Config::get()
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.
Definition: DeletePage.php:52
setSuppress(bool $suppress)
If true, suppress all revisions and log the deletion in the suppression log instead of the deletion l...
Definition: DeletePage.php:218
forceImmediate(bool $forceImmediate)
If false, allows deleting over time via the job queue.
Definition: DeletePage.php:251
deleteIfAllowed(string $reason)
Same as deleteUnsafe, but checks permissions.
Definition: DeletePage.php:377
BagOStuff $recentDeletesCache
Definition: DeletePage.php:83
string[] $tags
Definition: DeletePage.php:110
WikiPage $page
Definition: DeletePage.php:103
CommentStore $commentStore
Definition: DeletePage.php:79
setTags(array $tags)
Change tags to apply to the deletion action.
Definition: DeletePage.php:229
setLogSubtype(string $logSubtype)
Set a specific log subtype for the deletion log entry.
Definition: DeletePage.php:240
deleteUnsafe(string $reason)
Back-end article deletion: deletes the article with database consistency, writes logs,...
Definition: DeletePage.php:465
const CONSTRUCTOR_OPTIONS
Definition: DeletePage.php:56
const PAGE_BASE
Constants used for the return value of getSuccessfulDeletionsIDs() and deletionsWereScheduled()
Definition: DeletePage.php:65
JobQueueGroup $jobQueueGroup
Definition: DeletePage.php:77
string $logSubtype
Definition: DeletePage.php:112
HookRunner $hookRunner
Definition: DeletePage.php:69
doDeleteUpdates(WikiPage $page, RevisionRecord $revRecord)
Definition: DeletePage.php:847
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
Definition: DeletePage.php:438
Authority $deleter
Definition: DeletePage.php:105
setDeleteAssociatedTalk(bool $delete)
If set to true and the page has a talk page, delete that one too.
Definition: DeletePage.php:287
string $localWikiID
Definition: DeletePage.php:85
runPreDeleteHooks(WikiPage $page, string &$reason)
Definition: DeletePage.php:499
keepLegacyHookErrorsSeparate()
Definition: DeletePage.php:206
RevisionStore $revisionStore
Definition: DeletePage.php:71
__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)
Definition: DeletePage.php:155
getDeletionUpdates(WikiPage $page, RevisionRecord $rev)
Definition: DeletePage.php:924
setIsDeletePageUnitTest(bool $test)
Definition: DeletePage.php:308
bool $attemptedDeletion
Whether a deletion was attempted.
Definition: DeletePage.php:134
canProbablyDeleteAssociatedTalk()
Tests whether it's probably possible to delete the associated talk page.
Definition: DeletePage.php:262
NamespaceInfo $namespaceInfo
Definition: DeletePage.php:95
LBFactory $lbFactory
Definition: DeletePage.php:73
archiveRevisions(WikiPage $page, int $id)
Archives revisions as part of page deletion.
Definition: DeletePage.php:729
WikiPageFactory $wikiPageFactory
Definition: DeletePage.php:93
string array $legacyHookErrors
Definition: DeletePage.php:119
assertDeletionAttempted()
Asserts that a deletion operation was attempted.
Definition: DeletePage.php:335
array< int|null > null $successfulDeletionsIDs
Keys are the self::PAGE_* constants.
Definition: DeletePage.php:127
ILoadBalancer $loadBalancer
Definition: DeletePage.php:75
bool $isDeletePageUnitTest
Definition: DeletePage.php:100
WikiPage null $associatedTalk
If not null, it means that we have to delete it.
Definition: DeletePage.php:116
array< bool|null > null $wasScheduled
Keys are the self::PAGE_* constants.
Definition: DeletePage.php:132
ITextFormatter $contLangMsgTextFormatter
Definition: DeletePage.php:97
BacklinkCacheFactory $backlinkCacheFactory
Definition: DeletePage.php:91
deleteInternal(WikiPage $page, string $pageRole, string $reason, ?string $webRequestId=null)
Definition: DeletePage.php:545
setDeletionAttempted()
Called before attempting a deletion, allows the result getters to be used.
Definition: DeletePage.php:320
string $webRequestID
Definition: DeletePage.php:87
UserFactory $userFactory
Definition: DeletePage.php:89
ServiceOptions $options
Definition: DeletePage.php:81
bool $mergeLegacyHookErrors
Definition: DeletePage.php:121
Service for creating WikiPage objects.
newFromTitle(PageIdentity $pageIdentity)
Create a WikiPage object from a title.
A StatusValue for permission errors.
Page revision base class.
Service for looking up page revisions.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
Creates User objects.
Definition: UserFactory.php:38
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:141
static numParam( $num)
Definition: Message.php:1166
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Variant of the Message class.
Definition: RawMessage.php:35
Abstraction for ResourceLoader modules which pull from wiki pages.
static invalidateModuleCache(PageIdentity $page, ?RevisionRecord $old, ?RevisionRecord $new, string $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Database independent search index updater.
Class for handling updates to the site_stats table.
static factory(array $deltas)
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
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.
Definition: StatusValue.php:82
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:829
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:533
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2782
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:744
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2884
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:576
getTitle()
Get the title object of the article.
Definition: WikiPage.php:307
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:462
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:808
Value object representing a message for i18n.
An interface for generating database load balancers.
Definition: LBFactory.php:43
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
Database cluster connection, tracking, load balancing, and transaction manager interface.
const DB_REPLICA
Definition: defines.php:25
const DB_PRIMARY
Definition: defines.php:27
if(count( $args)< 1) $job
$content
Definition: router.php:76
return true
Definition: router.php:90
$revQuery