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;
32 use Message;
33 use NamespaceInfo;
34 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 = [
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)
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...
Definition: ChangeTags.php:635
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 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:563
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.
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:217
forceImmediate(bool $forceImmediate)
If false, allows deleting over time via the job queue.
Definition: DeletePage.php:250
deleteIfAllowed(string $reason)
Same as deleteUnsafe, but checks permissions.
Definition: DeletePage.php:376
setTags(array $tags)
Change tags to apply to the deletion action.
Definition: DeletePage.php:228
setLogSubtype(string $logSubtype)
Set a specific log subtype for the deletion log entry.
Definition: DeletePage.php:239
deleteUnsafe(string $reason)
Back-end article deletion: deletes the article with database consistency, writes logs,...
Definition: DeletePage.php:464
const CONSTRUCTOR_OPTIONS
Definition: DeletePage.php:56
const PAGE_BASE
Constants used for the return value of getSuccessfulDeletionsIDs() and deletionsWereScheduled()
Definition: DeletePage.php:64
doDeleteUpdates(WikiPage $page, RevisionRecord $revRecord)
Definition: DeletePage.php:841
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
Definition: DeletePage.php:437
setDeleteAssociatedTalk(bool $delete)
If set to true and the page has a talk page, delete that one too.
Definition: DeletePage.php:286
keepLegacyHookErrorsSeparate()
Definition: DeletePage.php:205
__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:154
getDeletionUpdates(WikiPage $page, RevisionRecord $rev)
Definition: DeletePage.php:918
setIsDeletePageUnitTest(bool $test)
Definition: DeletePage.php:307
canProbablyDeleteAssociatedTalk()
Tests whether it's probably possible to delete the associated talk page.
Definition: DeletePage.php:261
deleteInternal(WikiPage $page, string $pageRole, string $reason, ?string $webRequestId=null)
Definition: DeletePage.php:544
setDeletionAttempted()
Called before attempting a deletion, allows the result getters to be used.
Definition: DeletePage.php:319
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.
Definition: WikiModule.php:65
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:1146
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
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:46
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
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:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:45
Base representation for an editable wiki page.
Definition: WikiPage.php:69
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:833
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:537
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2725
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:748
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2827
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:580
getTitle()
Get the title object of the article.
Definition: WikiPage.php:310
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:466
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:812
Value object representing a message for i18n.
Base interface for representing page content.
Definition: Content.php:34
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
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:90
$revQuery