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;
35 use Message;
36 use SearchUpdate;
37 use SiteStatsUpdate;
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 LogicException( __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->newSelectQueryBuilder()
633  ->select( '*' )
634  ->from( 'archive' )
635  ->where( [
636  'ar_namespace' => $title->getNamespace(),
637  'ar_title' => $title->getDBkey(),
638  'ar_page_id' => $id
639  ] )
640  ->caller( __METHOD__ )->fetchRowCount();
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->newDeleteQueryBuilder()
649  ->deleteFrom( 'page' )
650  ->where( [ 'page_id' => $id ] )
651  ->caller( __METHOD__ )->execute();
652 
653  // Log the deletion, if the page was suppressed, put it in the suppression log instead
654  $logtype = $this->suppress ? 'suppress' : 'delete';
655 
656  $logEntry = new ManualLogEntry( $logtype, $this->logSubtype );
657  $logEntry->setPerformer( $this->deleter->getUser() );
658  $logEntry->setTarget( $logTitle );
659  $logEntry->setComment( $reason );
660  $logEntry->addTags( $this->tags );
661  if ( !$this->isDeletePageUnitTest ) {
662  // TODO: Remove conditional once ManualLogEntry is servicified (T253717)
663  $logid = $logEntry->insert();
664 
665  $dbw->onTransactionPreCommitOrIdle(
666  static function () use ( $logEntry, $logid ) {
667  // T58776: avoid deadlocks (especially from FileDeleteForm)
668  $logEntry->publish( $logid );
669  },
670  __METHOD__
671  );
672  } else {
673  $logid = 42;
674  }
675 
676  $dbw->endAtomic( __METHOD__ );
677 
678  $this->doDeleteUpdates( $page, $revisionRecord );
679 
680  $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
681  $this->hookRunner->onArticleDeleteComplete(
682  $wikiPageBeforeDelete,
683  $legacyDeleter,
684  $reason,
685  $id,
686  $content,
687  $logEntry,
688  $archivedRevisionCount
689  );
690  $this->hookRunner->onPageDeleteComplete(
691  $wikiPageBeforeDelete,
692  $this->deleter,
693  $reason,
694  $id,
695  $revisionRecord,
696  $logEntry,
697  $archivedRevisionCount
698  );
699  $this->successfulDeletionsIDs[$pageRole] = $logid;
700 
701  // Show log excerpt on 404 pages rather than just a link
702  $key = $this->recentDeletesCache->makeKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
703  $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
704 
705  return $status;
706  }
707 
715  private function archiveRevisions( WikiPage $page, int $id ): bool {
716  // Given the lock above, we can be confident in the title and page ID values
717  $namespace = $page->getTitle()->getNamespace();
718  $dbKey = $page->getTitle()->getDBkey();
719 
720  $dbw = $this->lbFactory->getPrimaryDatabase();
721 
722  $revQuery = $this->revisionStore->getQueryInfo();
723  $bitfield = false;
724 
725  // Bitfields to further suppress the content
726  if ( $this->suppress ) {
727  $bitfield = RevisionRecord::SUPPRESSED_ALL;
728  $revQuery['fields'] = array_diff( $revQuery['fields'], [ 'rev_deleted' ] );
729  }
730 
731  // For now, shunt the revision data into the archive table.
732  // Text is *not* removed from the text table; bulk storage
733  // is left intact to avoid breaking block-compression or
734  // immutable storage schemes.
735  // In the future, we may keep revisions and mark them with
736  // the rev_deleted field, which is reserved for this purpose.
737 
738  // Lock rows in `revision` and its temp tables, but not any others.
739  // Note array_intersect() preserves keys from the first arg, and we're
740  // assuming $revQuery has `revision` primary and isn't using subtables
741  // for anything we care about.
742  $lockQuery = $revQuery;
743  $lockQuery['tables'] = array_intersect(
744  $revQuery['tables'],
745  [ 'revision', 'revision_comment_temp' ]
746  );
747  unset( $lockQuery['fields'] );
748  $dbw->newSelectQueryBuilder()
749  ->queryInfo( $lockQuery )
750  ->where( [ 'rev_page' => $id ] )
751  ->forUpdate()
752  ->caller( __METHOD__ )
753  ->acquireRowLocks();
754 
755  $deleteBatchSize = $this->options->get( MainConfigNames::DeleteRevisionsBatchSize );
756  // Get as many of the page revisions as we are allowed to. The +1 lets us recognize the
757  // unusual case where there were exactly $deleteBatchSize revisions remaining.
758  $res = $dbw->select(
759  $revQuery['tables'],
760  $revQuery['fields'],
761  [ 'rev_page' => $id ],
762  __METHOD__,
763  [ 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC', 'LIMIT' => $deleteBatchSize + 1 ],
764  $revQuery['joins']
765  );
766 
767  // Build their equivalent archive rows
768  $rowsInsert = [];
769  $revids = [];
770 
772  $ipRevIds = [];
773 
774  $done = true;
775  foreach ( $res as $row ) {
776  if ( count( $revids ) >= $deleteBatchSize ) {
777  $done = false;
778  break;
779  }
780 
781  $comment = $this->commentStore->getComment( 'rev_comment', $row );
782  $rowInsert = [
783  'ar_namespace' => $namespace,
784  'ar_title' => $dbKey,
785  'ar_actor' => $row->rev_actor,
786  'ar_timestamp' => $row->rev_timestamp,
787  'ar_minor_edit' => $row->rev_minor_edit,
788  'ar_rev_id' => $row->rev_id,
789  'ar_parent_id' => $row->rev_parent_id,
790  'ar_len' => $row->rev_len,
791  'ar_page_id' => $id,
792  'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
793  'ar_sha1' => $row->rev_sha1,
794  ] + $this->commentStore->insert( $dbw, 'ar_comment', $comment );
795 
796  $rowsInsert[] = $rowInsert;
797  $revids[] = $row->rev_id;
798 
799  // Keep track of IP edits, so that the corresponding rows can
800  // be deleted in the ip_changes table.
801  if ( (int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
802  $ipRevIds[] = $row->rev_id;
803  }
804  }
805 
806  if ( count( $revids ) > 0 ) {
807  // Copy them into the archive table
808  $dbw->newInsertQueryBuilder()
809  ->insertInto( 'archive' )
810  ->rows( $rowsInsert )
811  ->caller( __METHOD__ )->execute();
812 
813  $dbw->newDeleteQueryBuilder()
814  ->deleteFrom( 'revision' )
815  ->where( [ 'rev_id' => $revids ] )
816  ->caller( __METHOD__ )->execute();
817  // Also delete records from ip_changes as applicable.
818  if ( count( $ipRevIds ) > 0 ) {
819  $dbw->newDeleteQueryBuilder()
820  ->deleteFrom( 'ip_changes' )
821  ->where( [ 'ipc_rev_id' => $ipRevIds ] )
822  ->caller( __METHOD__ )->execute();
823  }
824  }
825 
826  return $done;
827  }
828 
837  private function doDeleteUpdates( WikiPage $page, RevisionRecord $revRecord ): void {
838  try {
839  $countable = $page->isCountable();
840  } catch ( TimeoutException $e ) {
841  throw $e;
842  } catch ( Exception $ex ) {
843  // fallback for deleting broken pages for which we cannot load the content for
844  // some reason. Note that doDeleteArticleReal() already logged this problem.
845  $countable = false;
846  }
847 
848  // Update site status
850  [ 'edits' => 1, 'articles' => $countable ? -1 : 0, 'pages' => -1 ]
851  ) );
852 
853  // Delete pagelinks, update secondary indexes, etc
854  $updates = $this->getDeletionUpdates( $page, $revRecord );
855  foreach ( $updates as $update ) {
856  DeferredUpdates::addUpdate( $update );
857  }
858 
859  // Reparse any pages transcluding this page
860  LinksUpdate::queueRecursiveJobsForTable(
861  $page->getTitle(),
862  'templatelinks',
863  'delete-page',
864  $this->deleter->getUser()->getName(),
865  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
866  );
867  // Reparse any pages including this image
868  if ( $page->getTitle()->getNamespace() === NS_FILE ) {
869  LinksUpdate::queueRecursiveJobsForTable(
870  $page->getTitle(),
871  'imagelinks',
872  'delete-page',
873  $this->deleter->getUser()->getName(),
874  $this->backlinkCacheFactory->getBacklinkCache( $page->getTitle() )
875  );
876  }
877 
878  if ( !$this->isDeletePageUnitTest ) {
879  // TODO Remove conditional once WikiPage::onArticleDelete is moved to a proper service
880  // Clear caches
882  }
883 
884  WikiModule::invalidateModuleCache(
885  $page->getTitle(),
886  $revRecord,
887  null,
888  $this->localWikiID
889  );
890 
891  // Reset the page object and the Title object
892  $page->loadFromRow( false, WikiPage::READ_LATEST );
893 
894  // Search engine
895  DeferredUpdates::addUpdate( new SearchUpdate( $page->getId(), $page->getTitle() ) );
896  }
897 
907  private function getDeletionUpdates( WikiPage $page, RevisionRecord $rev ): array {
908  if ( $this->isDeletePageUnitTest ) {
909  // Hack: LinksDeletionUpdate reads from the global state in the constructor
910  return [];
911  }
912  $slotContent = array_map( static function ( SlotRecord $slot ) {
913  return $slot->getContent();
914  }, $rev->getSlots()->getSlots() );
915 
916  $allUpdates = [ new LinksDeletionUpdate( $page ) ];
917 
918  // NOTE: once Content::getDeletionUpdates() is removed, we only need the content
919  // model here, not the content object!
920  // TODO: consolidate with similar logic in DerivedPageDataUpdater::getSecondaryDataUpdates()
922  $content = null; // in case $slotContent is zero-length
923  foreach ( $slotContent as $role => $content ) {
924  $handler = $content->getContentHandler();
925 
926  $updates = $handler->getDeletionUpdates(
927  $page->getTitle(),
928  $role
929  );
930 
931  $allUpdates = array_merge( $allUpdates, $updates );
932  }
933 
934  $this->hookRunner->onPageDeletionDataUpdates(
935  $page->getTitle(), $rev, $allUpdates );
936 
937  // TODO: hard deprecate old hook in 1.33
938  $this->hookRunner->onWikiPageDeletionUpdates( $page, $content, $allUpdates );
939  return $allUpdates;
940  }
941 }
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:396
Defer callable updates to run later in the PHP process.
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:54
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:568
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
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: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
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:58
This is a utility class for dealing with namespaces that encodes all the "magic" behaviors of them ba...
Creates User objects.
Definition: UserFactory.php:41
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:1154
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
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Base representation for an editable wiki page.
Definition: WikiPage.php:77
getContent( $audience=RevisionRecord::FOR_PUBLIC, Authority $performer=null)
Get the content of the current revision.
Definition: WikiPage.php:776
loadFromRow( $data, $from)
Load the object from a database row.
Definition: WikiPage.php:486
lockAndGetLatest()
Lock the page row for this title+id and return page_latest (or 0)
Definition: WikiPage.php:2667
getLatest( $wikiId=self::LOCAL)
Get the page_latest field.
Definition: WikiPage.php:694
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Definition: WikiPage.php:2742
getId( $wikiId=self::LOCAL)
Definition: WikiPage.php:528
getTitle()
Get the title object of the article.
Definition: WikiPage.php:258
loadPageData( $from='fromdb')
Load the object from a given source by title.
Definition: WikiPage.php:415
getRevisionRecord()
Get the latest revision.
Definition: WikiPage.php:758
Value object representing a message for i18n.
Base interface for representing page content.
Definition: Content.php:39
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