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 Content;
10 use DeferredUpdates;
11 use DeletePageJob;
12 use Exception;
13 use JobQueueGroup;
14 use LogicException;
15 use ManualLogEntry;
33 use Message;
34 use NamespaceInfo;
35 use SearchUpdate;
36 use SiteStatsUpdate;
37 use Status;
38 use StatusValue;
39 use Wikimedia\IPUtils;
43 use Wikimedia\RequestTimeout\TimeoutException;
44 use WikiPage;
45 
51 class DeletePage {
55  public const CONSTRUCTOR_OPTIONS = [
58  ];
59 
63  public const PAGE_BASE = 'base';
64  public const PAGE_TALK = 'talk';
65 
67  private $hookRunner;
69  private $revisionStore;
71  private $lbFactory;
73  private $jobQueueGroup;
75  private $commentStore;
77  private $options;
79  private $recentDeletesCache;
81  private $localWikiID;
83  private $webRequestID;
85  private $userFactory;
87  private $backlinkCacheFactory;
89  private $wikiPageFactory;
91  private $namespaceInfo;
93  private $contLangMsgTextFormatter;
94 
96  private $isDeletePageUnitTest = false;
97 
99  private $page;
101  private $deleter;
102 
104  private $suppress = false;
106  private $tags = [];
108  private $logSubtype = 'delete';
110  private $forceImmediate = false;
112  private $associatedTalk;
113 
115  private $legacyHookErrors = '';
117  private $mergeLegacyHookErrors = true;
118 
123  private $successfulDeletionsIDs;
128  private $wasScheduled;
130  private $attemptedDeletion = false;
131 
151  public function __construct(
152  HookContainer $hookContainer,
153  RevisionStore $revisionStore,
154  LBFactory $lbFactory,
155  JobQueueGroup $jobQueueGroup,
156  CommentStore $commentStore,
157  ServiceOptions $serviceOptions,
158  BagOStuff $recentDeletesCache,
159  string $localWikiID,
160  string $webRequestID,
161  WikiPageFactory $wikiPageFactory,
162  UserFactory $userFactory,
163  BacklinkCacheFactory $backlinkCacheFactory,
164  NamespaceInfo $namespaceInfo,
165  ITextFormatter $contLangMsgTextFormatter,
166  ProperPageIdentity $page,
167  Authority $deleter
168  ) {
169  $this->hookRunner = new HookRunner( $hookContainer );
170  $this->revisionStore = $revisionStore;
171  $this->lbFactory = $lbFactory;
172  $this->jobQueueGroup = $jobQueueGroup;
173  $this->commentStore = $commentStore;
174  $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
175  $this->options = $serviceOptions;
176  $this->recentDeletesCache = $recentDeletesCache;
177  $this->localWikiID = $localWikiID;
178  $this->webRequestID = $webRequestID;
179  $this->wikiPageFactory = $wikiPageFactory;
180  $this->userFactory = $userFactory;
181  $this->backlinkCacheFactory = $backlinkCacheFactory;
182  $this->namespaceInfo = $namespaceInfo;
183  $this->contLangMsgTextFormatter = $contLangMsgTextFormatter;
184 
185  $this->page = $wikiPageFactory->newFromTitle( $page );
186  $this->deleter = $deleter;
187  }
188 
193  public function getLegacyHookErrors() {
194  return $this->legacyHookErrors;
195  }
196 
201  public function keepLegacyHookErrorsSeparate(): self {
202  $this->mergeLegacyHookErrors = false;
203  return $this;
204  }
205 
213  public function setSuppress( bool $suppress ): self {
214  $this->suppress = $suppress;
215  return $this;
216  }
217 
224  public function setTags( array $tags ): self {
225  $this->tags = $tags;
226  return $this;
227  }
228 
235  public function setLogSubtype( string $logSubtype ): self {
236  $this->logSubtype = $logSubtype;
237  return $this;
238  }
239 
246  public function forceImmediate( bool $forceImmediate ): self {
247  $this->forceImmediate = $forceImmediate;
248  return $this;
249  }
250 
258  if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
259  return StatusValue::newFatal( 'delete-error-associated-alreadytalk' );
260  }
261  // FIXME NamespaceInfo should work with PageIdentity
262  $talkPage = $this->wikiPageFactory->newFromLinkTarget(
263  $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
264  );
265  if ( !$talkPage->exists() ) {
266  return StatusValue::newFatal( 'delete-error-associated-doesnotexist' );
267  }
268  return StatusValue::newGood();
269  }
270 
282  public function setDeleteAssociatedTalk( bool $delete ): self {
283  if ( !$delete ) {
284  $this->associatedTalk = null;
285  return $this;
286  }
287 
288  if ( $this->namespaceInfo->isTalk( $this->page->getNamespace() ) ) {
289  throw new BadMethodCallException( "Cannot delete associated talk page of a talk page! ($this->page)" );
290  }
291  // FIXME NamespaceInfo should work with PageIdentity
292  $this->associatedTalk = $this->wikiPageFactory->newFromLinkTarget(
293  $this->namespaceInfo->getTalkPage( $this->page->getTitle() )
294  );
295  return $this;
296  }
297 
303  public function setIsDeletePageUnitTest( bool $test ): void {
304  if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
305  throw new BadMethodCallException( __METHOD__ . ' can only be used in tests!' );
306  }
307  $this->isDeletePageUnitTest = $test;
308  }
309 
315  public function setDeletionAttempted(): self {
316  $this->attemptedDeletion = true;
317  $this->successfulDeletionsIDs = [ self::PAGE_BASE => null ];
318  $this->wasScheduled = [ self::PAGE_BASE => null ];
319  if ( $this->associatedTalk ) {
320  $this->successfulDeletionsIDs[self::PAGE_TALK] = null;
321  $this->wasScheduled[self::PAGE_TALK] = null;
322  }
323  return $this;
324  }
325 
330  private function assertDeletionAttempted(): void {
331  if ( !$this->attemptedDeletion ) {
332  throw new BadMethodCallException( 'No deletion was attempted' );
333  }
334  }
335 
340  public function getSuccessfulDeletionsIDs(): array {
341  $this->assertDeletionAttempted();
342  return $this->successfulDeletionsIDs;
343  }
344 
349  public function deletionsWereScheduled(): array {
350  $this->assertDeletionAttempted();
351  return $this->wasScheduled;
352  }
353 
360  public function deleteIfAllowed( string $reason ): StatusValue {
361  $this->setDeletionAttempted();
362  $status = $this->authorizeDeletion();
363  if ( !$status->isGood() ) {
364  return $status;
365  }
366 
367  return $this->deleteUnsafe( $reason );
368  }
369 
373  private function authorizeDeletion(): PermissionStatus {
374  $status = PermissionStatus::newEmpty();
375  $this->deleter->authorizeWrite( 'delete', $this->page, $status );
376  if ( $this->associatedTalk ) {
377  $this->deleter->authorizeWrite( 'delete', $this->associatedTalk, $status );
378  }
379  if ( !$this->deleter->isAllowed( 'bigdelete' ) && $this->isBigDeletion() ) {
380  $status->fatal(
381  'delete-toomanyrevisions',
382  Message::numParam( $this->options->get( MainConfigNames::DeleteRevisionsLimit ) )
383  );
384  }
385  if ( $this->tags ) {
386  $status->merge( ChangeTags::canAddTagsAccompanyingChange( $this->tags, $this->deleter ) );
387  }
388  return $status;
389  }
390 
394  private function isBigDeletion(): bool {
395  $revLimit = $this->options->get( MainConfigNames::DeleteRevisionsLimit );
396  if ( !$revLimit ) {
397  return false;
398  }
399 
400  $dbr = $this->lbFactory->getReplicaDatabase();
401  $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
402  if ( $this->associatedTalk ) {
403  $revCount += $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
404  }
405 
406  return $revCount > $revLimit;
407  }
408 
421  public function isBatchedDelete( int $safetyMargin = 0 ): bool {
422  $dbr = $this->lbFactory->getReplicaDatabase();
423  $revCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->page->getId() );
424  $revCount += $safetyMargin;
425 
426  if ( $revCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize ) ) {
427  return true;
428  } elseif ( !$this->associatedTalk ) {
429  return false;
430  }
431 
432  $talkRevCount = $this->revisionStore->countRevisionsByPageId( $dbr, $this->associatedTalk->getId() );
433  $talkRevCount += $safetyMargin;
434 
435  return $talkRevCount >= $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
436  }
437 
448  public function deleteUnsafe( string $reason ): Status {
449  $this->setDeletionAttempted();
450  $origReason = $reason;
451  $hookStatus = $this->runPreDeleteHooks( $this->page, $reason );
452  if ( !$hookStatus->isGood() ) {
453  return $hookStatus;
454  }
455  if ( $this->associatedTalk ) {
456  $talkReason = $this->contLangMsgTextFormatter->format(
457  MessageValue::new( 'delete-talk-summary-prefix' )->plaintextParams( $origReason )
458  );
459  $talkHookStatus = $this->runPreDeleteHooks( $this->associatedTalk, $talkReason );
460  if ( !$talkHookStatus->isGood() ) {
461  return $talkHookStatus;
462  }
463  }
464 
465  $status = $this->deleteInternal( $this->page, self::PAGE_BASE, $reason );
466  if ( !$this->associatedTalk || !$status->isGood() ) {
467  return $status;
468  }
469  // NOTE: If the page deletion above failed because the page is no longer there (e.g. race condition) we'll
470  // still try to delete the talk page, since it was the user's intention anyway.
471  // @phan-suppress-next-next-line PhanPossiblyUndeclaredVariable talkReason is set when used
472  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable talkReason is set when used
473  $status->merge( $this->deleteInternal( $this->associatedTalk, self::PAGE_TALK, $talkReason ) );
474  return $status;
475  }
476 
482  private function runPreDeleteHooks( WikiPage $page, string &$reason ): Status {
483  $status = Status::newGood();
484 
485  $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
486  if ( !$this->hookRunner->onArticleDelete(
487  $page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
488  ) {
489  if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !== '' ) {
490  if ( is_string( $this->legacyHookErrors ) ) {
491  $this->legacyHookErrors = [ $this->legacyHookErrors ];
492  }
493  foreach ( $this->legacyHookErrors as $legacyError ) {
494  $status->fatal( new RawMessage( $legacyError ) );
495  }
496  }
497  if ( $status->isOK() ) {
498  // Hook aborted but didn't set a fatal status
499  $status->fatal( 'delete-hook-aborted' );
500  }
501  return $status;
502  }
503 
504  // Use a new Status in case a hook handler put something here without aborting.
505  $status = Status::newGood();
506  $hookRes = $this->hookRunner->onPageDelete( $page, $this->deleter, $reason, $status, $this->suppress );
507  if ( !$hookRes && !$status->isGood() ) {
508  // Note: as per the PageDeleteHook documentation, `return false` is ignored if $status is good.
509  return $status;
510  }
511  return Status::newGood();
512  }
513 
528  public function deleteInternal(
529  WikiPage $page,
530  string $pageRole,
531  string $reason,
532  ?string $webRequestId = null
533  ): Status {
534  $title = $page->getTitle();
535  $status = Status::newGood();
536 
537  $dbw = $this->lbFactory->getPrimaryDatabase();
538  $dbw->startAtomic( __METHOD__ );
539 
540  $page->loadPageData( WikiPage::READ_LATEST );
541  $id = $page->getId();
542  // T98706: lock the page from various other updates but avoid using
543  // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
544  // the revisions queries (which also JOIN on user). Only lock the page
545  // row and CAS check on page_latest to see if the trx snapshot matches.
546  $lockedLatest = $page->lockAndGetLatest();
547  if ( $id === 0 || $page->getLatest() !== $lockedLatest ) {
548  $dbw->endAtomic( __METHOD__ );
549  // Page not there or trx snapshot is stale
550  $status->error( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) );
551  return $status;
552  }
553 
554  // At this point we are now committed to returning an OK
555  // status unless some DB query error or other exception comes up.
556  // This way callers don't have to call rollback() if $status is bad
557  // unless they actually try to catch exceptions (which is rare).
558 
559  // we need to remember the old content so we can use it to generate all deletion updates.
560  $revisionRecord = $page->getRevisionRecord();
561  if ( !$revisionRecord ) {
562  throw new LogicException( "No revisions for $page?" );
563  }
564  try {
565  $content = $page->getContent( RevisionRecord::RAW );
566  } catch ( TimeoutException $e ) {
567  throw $e;
568  } catch ( Exception $ex ) {
569  wfLogWarning( __METHOD__ . ': failed to load content during deletion! '
570  . $ex->getMessage() );
571 
572  $content = null;
573  }
574 
575  // Archive revisions. In immediate mode, archive all revisions. Otherwise, archive
576  // one batch of revisions and defer archival of any others to the job queue.
577  $explictTrxLogged = false;
578  while ( true ) {
579  $done = $this->archiveRevisions( $page, $id );
580  if ( $done || !$this->forceImmediate ) {
581  break;
582  }
583  $dbw->endAtomic( __METHOD__ );
584  if ( $dbw->explicitTrxActive() ) {
585  // Explicit transactions may never happen here in practice. Log to be sure.
586  if ( !$explictTrxLogged ) {
587  $explictTrxLogged = true;
588  LoggerFactory::getInstance( 'wfDebug' )->debug(
589  'explicit transaction active in ' . __METHOD__ . ' while deleting {title}', [
590  'title' => $title->getText(),
591  ] );
592  }
593  continue;
594  }
595  if ( $dbw->trxLevel() ) {
596  $dbw->commit( __METHOD__ );
597  }
598  $this->lbFactory->waitForReplication();
599  $dbw->startAtomic( __METHOD__ );
600  }
601 
602  if ( !$done ) {
603  $dbw->endAtomic( __METHOD__ );
604 
605  $jobParams = [
606  'namespace' => $title->getNamespace(),
607  'title' => $title->getDBkey(),
608  'wikiPageId' => $id,
609  'requestId' => $webRequestId ?? $this->webRequestID,
610  'reason' => $reason,
611  'suppress' => $this->suppress,
612  'userId' => $this->deleter->getUser()->getId(),
613  'tags' => json_encode( $this->tags ),
614  'logsubtype' => $this->logSubtype,
615  'pageRole' => $pageRole,
616  ];
617 
618  $job = new DeletePageJob( $jobParams );
619  $this->jobQueueGroup->push( $job );
620  $this->wasScheduled[$pageRole] = true;
621  return $status;
622  }
623  $this->wasScheduled[$pageRole] = false;
624 
625  // Get archivedRevisionCount by db query, because there's no better alternative.
626  // Jobs cannot pass a count of archived revisions to the next job, because additional
627  // deletion operations can be started while the first is running. Jobs from each
628  // gracefully interleave, but would not know about each other's count. Deduplication
629  // in the job queue to avoid simultaneous deletion operations would add overhead.
630  // Number of archived revisions cannot be known beforehand, because edits can be made
631  // while deletion operations are being processed, changing the number of archivals.
632  $archivedRevisionCount = $dbw->selectRowCount(
633  'archive',
634  '*',
635  [
636  'ar_namespace' => $title->getNamespace(),
637  'ar_title' => $title->getDBkey(),
638  'ar_page_id' => $id
639  ], __METHOD__
640  );
641 
642  // Clone the title and wikiPage, so we have the information we need when
643  // we log and run the ArticleDeleteComplete hook.
644  $logTitle = clone $title;
645  $wikiPageBeforeDelete = clone $page;
646 
647  // Now that it's safely backed up, delete it
648  $dbw->delete( 'page', [ 'page_id' => $id ], __METHOD__ );
649 
650  // Log the deletion, if the page was suppressed, put it in the suppression log instead
651  $logtype = $this->suppress ? 'suppress' : 'delete';
652 
653  $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
654  $logEntry->setPerformer( $this->deleter->getUser() );
655  $logEntry->setTarget( $logTitle );
656  $logEntry->setComment( $reason );
657  $logEntry->addTags( $this->tags );
658  if ( !$this->isDeletePageUnitTest ) {
659  // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
660  $logid = $logEntry->insert();
661 
662  $dbw->onTransactionPreCommitOrIdle(
663  static function () use ( $logEntry, $logid ) {
664  // T58776: avoid deadlocks (especially from FileDeleteForm)
665  $logEntry->publish( $logid );
666  },
667  __METHOD__
668  );
669  } else {
670  $logid = 42;
671  }
672 
673  $dbw->endAtomic( __METHOD__ );
674 
675  $this->doDeleteUpdates( $page, $revisionRecord );
676 
677  $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
678  $this->hookRunner->onArticleDeleteComplete(
679  $wikiPageBeforeDelete,
680  $legacyDeleter,
681  $reason,
682  $id,
683  $content,
684  $logEntry,
685  $archivedRevisionCount
686  );
687  $this->hookRunner->onPageDeleteComplete(
688  $wikiPageBeforeDelete,
689  $this->deleter,
690  $reason,
691  $id,
692  $revisionRecord,
693  $logEntry,
694  $archivedRevisionCount
695  );
696  $this->successfulDeletionsIDs[$pageRole] = $logid;
697 
698  // Show log excerpt on 404 pages rather than just a link
699  $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
700  $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
701 
702  return $status;
703  }
704 
712  private function archiveRevisions( WikiPage $page, int $id ): bool {
713  // Given the lock above, we can be confident in the title and page ID values
714  $namespace = $page->getTitle()->getNamespace();
715  $dbKey = $page->getTitle()->getDBkey();
716 
717  $dbw = $this->lbFactory->getPrimaryDatabase();
718 
719  $revQuery = $this->revisionStore->getQueryInfo();
720  $bitfield = false;
721 
722  // Bitfields to further suppress the content
723  if ( $this->suppress ) {
724  $bitfield = RevisionRecord::SUPPRESSED_ALL;
725  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
726  }
727 
728  // For now, shunt the revision data into the archive table.
729  // Text is *not* removed from the text table; bulk storage
730  // is left intact to avoid breaking block-compression or
731  // immutable storage schemes.
732  // In the future, we may keep revisions and mark them with
733  // the rev_deleted field, which is reserved for this purpose.
734 
735  // Lock rows in `revision` and its temp tables, but not any others.
736  // Note array_intersect() preserves keys from the first arg, and we're
737  // assuming $revQuery has `revision` primary and isn't using subtables
738  // for anything we care about.
739  $lockQuery = $revQuery;
740  $lockQuery['tables'] = array_intersect(
741  $revQuery['tables'],
742  [ 'revision', 'revision_comment_temp' ]
743  );
744  unset( $lockQuery['fields'] );
745  $dbw->newSelectQueryBuilder()
746  ->queryInfo( $lockQuery )
747  ->where( [ 'rev_page' => $id ] )
748  ->forUpdate()
749  ->caller( __METHOD__ )
750  ->acquireRowLocks();
751 
752  $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
753  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
754  // unusual case where there were exactly $deleteBatchSize revisions remaining.
755  $res = $dbw->select(
756  $revQuery['tables'],
757  $revQuery['fields'],
758  [ 'rev_page' => $id ],
759  __METHOD__,
760  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
761  $revQuery['joins']
762  );
763 
764  // Build their equivalent archive rows
765  $rowsInsert = [];
766  $revids = [];
767 
769  $ipRevIds = [];
770 
771  $done = true;
772  foreach ( $res as $row ) {
773  if ( count( $revids ) >= $deleteBatchSize ) {
774  $done = false;
775  break;
776  }
777 
778  $comment = $this->commentStore->getComment( 'rev_comment', $row );
779  $rowInsert = [
780  'ar_namespace' => $namespace,
781  'ar_title' => $dbKey,
782  'ar_actor' => $row->rev_actor,
783  'ar_timestamp' => $row->rev_timestamp,
784  'ar_minor_edit' => $row->rev_minor_edit,
785  'ar_rev_id' => $row->rev_id,
786  'ar_parent_id' => $row->rev_parent_id,
787  'ar_len' => $row->rev_len,
788  'ar_page_id' => $id,
789  'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
790  'ar_sha1' => $row->rev_sha1,
791  ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
792 
793  $rowsInsert[] = $rowInsert;
794  $revids[] = $row->rev_id;
795 
796  // Keep track of IP edits, so that the corresponding rows can
797  // be deleted in the ip_changes table.
798  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
799  $ipRevIds[] = $row->rev_id;
800  }
801  }
802 
803  if ( count( $revids ) > 0 ) {
804  // Copy them into the archive table
805  $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
806 
807  $dbw->delete( 'revision', [ 'rev_id' => $revids ], __METHOD__ );
808  if ( $this->commentStore->getTempTableMigrationStage( 'rev_comment' ) & SCHEMA_COMPAT_WRITE_OLD ) {
809  $dbw->delete( 'revision_comment_temp', [ 'revcomment_rev' => $revids ], __METHOD__ );
810  }
811  // Also delete records from ip_changes as applicable.
812  if ( count( $ipRevIds ) > 0 ) {
813  $dbw->delete( 'ip_changes', [ 'ipc_rev_id' => $ipRevIds ], __METHOD__ );
814  }
815  }
816 
817  return $done;
818  }
819 
829  public function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
830  try {
831  $countable = $page->isCountable();
832  } catch ( TimeoutException $e ) {
833  throw $e;
834  } catch ( Exception $ex ) {
835  // fallback for deleting broken pages for which we cannot load the content for
836  // some reason. Note that doDeleteArticleReal() already logged this problem.
837  $countable = false;
838  }
839 
840  // Update site status
841  if ( !$this->isDeletePageUnitTest ) {
842  // TODO Remove conditional once DeferredUpdates is servicified (T265749)
844  [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
845  ) );
846 
847  // Delete pagelinks, update secondary indexes, etc
848  $updates = $this->getDeletionUpdates( $page, $revRecord );
849  foreach ( $updates as $update ) {
850  DeferredUpdates::addUpdate( $update );
851  }
852  }
853 
854  // Reparse any pages transcluding this page
855  LinksUpdate::queueRecursiveJobsForTable(
856  $page->getTitle(),
857  'templatelinks',
858  'delete-page',
859  $this->deleter->getUser()->getName(),
860  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
861  );
862  // Reparse any pages including this image
863  if ( $page->getTitle()->getNamespace() === NS_FILE ) {
864  LinksUpdate::queueRecursiveJobsForTable(
865  $page->getTitle(),
866  'imagelinks',
867  'delete-page',
868  $this->deleter->getUser()->getName(),
869  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
870  );
871  }
872 
873  if ( !$this->isDeletePageUnitTest ) {
874  // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
875  // Clear caches
877  }
878 
879  WikiModule::invalidateModuleCache(
880  $page->getTitle(),
881  $revRecord,
882  null,
883  $this->localWikiID
884  );
885 
886  // Reset the page object and the Title object
887  $page->loadFromRow( false, WikiPage::READ_LATEST );
888 
889  if ( !$this->isDeletePageUnitTest ) {
890  // TODO Remove conditional once DeferredUpdates is servicified (T265749)
891  // Search engine
892  DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
893  }
894  }
895 
906  public function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
907  $slotContent = array_map( static function ( SlotRecord $slot ) {
908  return $slot->getContent();
909  }, $rev->getSlots()->getSlots() );
910 
911  $allUpdates = [ new LinksDeletionUpdate( $page ) ];
912 
913  // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
914  // model here, not the content object!
915  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
917  $content = null; // in case $slotContent is zero-length
918  foreach ( $slotContent as $role => $content ) {
919  $handler = $content->getContentHandler();
920 
921  $updates = $handler->getDeletionUpdates(
922  $page->getTitle(),
923  $role
924  );
925 
926  $allUpdates = array_merge( $allUpdates, $updates );
927  }
928 
929  $this->hookRunner->onPageDeletionDataUpdates(
930  $page->getTitle(), $rev, $allUpdates );
931 
932  // TODO: hard deprecate old hook in 1.33
933  $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
934  return $allUpdates;
935  }
936 }
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:265
const NS_FILE
Definition: Defines.php:70
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
if(!defined('MW_SETUP_CALLBACK'))
Definition: WebStart.php:88
Class representing a cache/ephemeral data store.
Definition: BagOStuff.php:85
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:577
Class for managing the deferral of updates within the scope of a PHP script invocation.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
Handle enqueueing of background jobs.
Class for creating new log entries and inserting them into the database.
Handle database storage of comments such as edit summaries and log reasons.
A class for passing options to services.
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Update object handling the cleanup of links tables after a page was deleted.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:55
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
Variant of the Message class.
Definition: RawMessage.php:40
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:51
setDeletionAttempted()
Called before attempting a deletion, allows the result getters to be used.
Definition: DeletePage.php:315
canProbablyDeleteAssociatedTalk()
Tests whether it's probably possible to delete the associated talk page.
Definition: DeletePage.php:257
__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:151
deleteInternal(WikiPage $page, string $pageRole, string $reason, ?string $webRequestId=null)
Definition: DeletePage.php:528
setTags(array $tags)
Change tags to apply to the deletion action.
Definition: DeletePage.php:224
deleteIfAllowed(string $reason)
Same as deleteUnsafe, but checks permissions.
Definition: DeletePage.php:360
setLogSubtype(string $logSubtype)
Set a specific log subtype for the deletion log entry.
Definition: DeletePage.php:235
const PAGE_BASE
Constants used for the return value of getSuccessfulDeletionsIDs() and deletionsWereScheduled()
Definition: DeletePage.php:63
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
Definition: DeletePage.php:421
setIsDeletePageUnitTest(bool $test)
Definition: DeletePage.php:303
doDeleteUpdates(WikiPage $page, RevisionRecord $revRecord)
Definition: DeletePage.php:829
getDeletionUpdates(WikiPage $page, RevisionRecord $rev)
Definition: DeletePage.php:906
deleteUnsafe(string $reason)
Back-end article deletion: deletes the article with database consistency, writes logs,...
Definition: DeletePage.php:448
forceImmediate(bool $forceImmediate)
If false, allows deleting over time via the job queue.
Definition: DeletePage.php:246
setDeleteAssociatedTalk(bool $delete)
If set to true and the page has a talk page, delete that one too.
Definition: DeletePage.php:282
setSuppress(bool $suppress)
If true, suppress all revisions and log the deletion in the suppression log instead of the deletion l...
Definition: DeletePage.php:213
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:66
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:42
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:144
static numParam( $num)
Definition: Message.php:1146
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Database independent search index updater.
Class for handling updates to the site_stats table.
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:46
Base representation for an editable wiki page.
Definition: WikiPage.php:75
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:837
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:544
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:755
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2827
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:587
getTitle()
Get the title object of the article.
Definition: WikiPage.php:317
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:473
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:819
Value object representing a message for i18n.
Base interface for representing page content.
Definition: Content.php:37
Interface that deferrable updates should implement.
Interface for a page that is (or could be, or used to be) an editable wiki page.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
if(count( $args)< 1) $job
$content
Definition: router.php:76
return true
Definition: router.php:90
$revQuery