5use BadMethodCallException;
51 'DeleteRevisionsBatchSize',
52 'ActorTableSchemaMigrationStage',
53 'DeleteRevisionsLimit',
142 $this->hookRunner =
new HookRunner( $hookContainer );
145 $this->loadBalancer = $this->lbFactory->
getMainLB();
149 $this->options = $serviceOptions;
173 $this->mergeLegacyHookErrors = false;
185 $this->suppress = $suppress;
195 public function setTags( array $tags ): self {
207 $this->logSubtype = $logSubtype;
218 $this->forceImmediate = $forceImmediate;
228 if ( !defined(
'MW_PHPUNIT_TEST' ) ) {
229 throw new BadMethodCallException( __METHOD__ .
' can only be used in tests!' );
231 $this->isDeletePageUnitTest = $test;
238 $this->attemptedDeletion =
true;
239 $this->successfulDeletionsIDs = [];
240 $this->wasScheduled =
false;
248 if ( !$this->attemptedDeletion ) {
249 throw new BadMethodCallException(
'No deletion was attempted' );
258 $this->assertDeletionAttempted();
259 return $this->successfulDeletionsIDs;
267 $this->assertDeletionAttempted();
268 return $this->wasScheduled;
278 $this->setDeletionAttempted();
279 $status = $this->authorizeDeletion();
280 if ( !$status->isGood() ) {
284 return $this->deleteUnsafe( $reason );
292 $this->deleter->authorizeWrite(
'delete', $this->page, $status );
294 !$this->deleter->authorizeWrite(
'bigdelete', $this->page ) &&
295 $this->isBigDeletion()
309 $revLimit = $this->options->get(
'DeleteRevisionsLimit' );
314 $revCount = $this->revisionStore->countRevisionsByPageId(
315 $this->loadBalancer->getConnectionRef(
DB_REPLICA ),
319 return $revCount > $revLimit;
335 $revCount = $this->revisionStore->countRevisionsByPageId(
336 $this->loadBalancer->getConnectionRef(
DB_REPLICA ),
339 $revCount += $safetyMargin;
341 return $revCount >= $this->options->get(
'DeleteRevisionsBatchSize' );
355 $this->setDeletionAttempted();
356 $status = Status::newGood();
358 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
359 if ( !$this->hookRunner->onArticleDelete(
360 $this->page, $legacyDeleter, $reason, $this->legacyHookErrors, $status, $this->suppress )
362 if ( $this->mergeLegacyHookErrors && $this->legacyHookErrors !==
'' ) {
363 if ( is_string( $this->legacyHookErrors ) ) {
364 $this->legacyHookErrors = [ $this->legacyHookErrors ];
366 foreach ( $this->legacyHookErrors as $legacyError ) {
367 $status->fatal(
new RawMessage( $legacyError ) );
370 if ( $status->isOK() ) {
372 $status->fatal(
'delete-hook-aborted' );
379 $hookRes = $this->hookRunner->onPageDelete( $this->page, $this->deleter, $reason, $status, $this->suppress );
380 if ( !$hookRes && !$status->isGood() ) {
385 return $this->deleteInternal( $reason );
402 $this->setDeletionAttempted();
404 $title = $this->page->getTitle();
405 $status = Status::newGood();
407 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
408 $dbw->startAtomic( __METHOD__ );
410 $this->page->loadPageData( WikiPage::READ_LATEST );
411 $id = $this->page->getId();
416 $lockedLatest = $this->page->lockAndGetLatest();
417 if ( $id === 0 || $this->page->getLatest() !== $lockedLatest ) {
418 $dbw->endAtomic( __METHOD__ );
430 $revisionRecord = $this->page->getRevisionRecord();
431 if ( !$revisionRecord ) {
432 throw new LogicException(
"No revisions for $this->page?" );
435 $content = $this->page->getContent( RevisionRecord::RAW );
436 }
catch ( Exception $ex ) {
437 wfLogWarning( __METHOD__ .
': failed to load content during deletion! '
438 . $ex->getMessage() );
445 $explictTrxLogged =
false;
447 $done = $this->archiveRevisions( $id );
448 if ( $done || !$this->forceImmediate ) {
451 $dbw->endAtomic( __METHOD__ );
452 if ( $dbw->explicitTrxActive() ) {
454 if ( !$explictTrxLogged ) {
455 $explictTrxLogged =
true;
456 LoggerFactory::getInstance(
'wfDebug' )->debug(
457 'explicit transaction active in ' . __METHOD__ .
' while deleting {title}', [
458 'title' =>
$title->getText(),
463 if ( $dbw->trxLevel() ) {
464 $dbw->commit( __METHOD__ );
466 $this->lbFactory->waitForReplication();
467 $dbw->startAtomic( __METHOD__ );
471 $dbw->endAtomic( __METHOD__ );
474 'namespace' =>
$title->getNamespace(),
475 'title' =>
$title->getDBkey(),
477 'requestId' => $webRequestId ?? $this->webRequestID,
479 'suppress' => $this->suppress,
480 'userId' => $this->deleter->getUser()->getId(),
481 'tags' => json_encode( $this->tags ),
482 'logsubtype' => $this->logSubtype,
486 $this->jobQueueGroup->push(
$job );
487 $this->wasScheduled =
true;
498 $archivedRevisionCount = $dbw->selectRowCount(
502 'ar_namespace' =>
$title->getNamespace(),
503 'ar_title' =>
$title->getDBkey(),
511 $wikiPageBeforeDelete = clone $this->page;
514 $dbw->delete(
'page', [
'page_id' => $id ], __METHOD__ );
517 $logtype = $this->suppress ?
'suppress' :
'delete';
520 $logEntry->setPerformer( $this->deleter->getUser() );
521 $logEntry->setTarget( $logTitle );
522 $logEntry->setComment( $reason );
523 $logEntry->addTags( $this->tags );
524 if ( !$this->isDeletePageUnitTest ) {
526 $logid = $logEntry->insert();
528 $dbw->onTransactionPreCommitOrIdle(
529 static function () use ( $logEntry, $logid ) {
531 $logEntry->publish( $logid );
539 $dbw->endAtomic( __METHOD__ );
541 $this->doDeleteUpdates( $revisionRecord );
543 $legacyDeleter = $this->userFactory->newFromAuthority( $this->deleter );
544 $this->hookRunner->onArticleDeleteComplete(
545 $wikiPageBeforeDelete,
551 $archivedRevisionCount
553 $this->hookRunner->onPageDeleteComplete(
554 $wikiPageBeforeDelete,
560 $archivedRevisionCount
562 $this->successfulDeletionsIDs[] = $logid;
565 $key = $this->recentDeletesCache->makeKey(
'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
566 $this->recentDeletesCache->set( $key, 1, BagOStuff::TTL_DAY );
579 $namespace = $this->page->
getTitle()->getNamespace();
580 $dbKey = $this->page->getTitle()->getDBkey();
582 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
584 $revQuery = $this->revisionStore->getQueryInfo();
588 if ( $this->suppress ) {
589 $bitfield = RevisionRecord::SUPPRESSED_ALL;
607 [
'revision',
'revision_comment_temp',
'revision_actor_temp' ]
609 [
'rev_page' => $id ],
615 $deleteBatchSize = $this->options->get(
'DeleteRevisionsBatchSize' );
621 [
'rev_page' => $id ],
623 [
'ORDER BY' =>
'rev_timestamp ASC, rev_id ASC',
'LIMIT' => $deleteBatchSize + 1 ],
635 foreach (
$res as $row ) {
636 if ( count( $revids ) >= $deleteBatchSize ) {
641 $comment = $this->commentStore->getComment(
'rev_comment', $row );
643 'ar_namespace' => $namespace,
644 'ar_title' => $dbKey,
645 'ar_actor' => $row->rev_actor,
646 'ar_timestamp' => $row->rev_timestamp,
647 'ar_minor_edit' => $row->rev_minor_edit,
648 'ar_rev_id' => $row->rev_id,
649 'ar_parent_id' => $row->rev_parent_id,
650 'ar_len' => $row->rev_len,
652 'ar_deleted' => $this->suppress ? $bitfield : $row->rev_deleted,
653 'ar_sha1' => $row->rev_sha1,
654 ] + $this->commentStore->insert( $dbw,
'ar_comment', $comment );
656 $rowsInsert[] = $rowInsert;
657 $revids[] = $row->rev_id;
661 if ( (
int)$row->rev_user === 0 && IPUtils::isValid( $row->rev_user_text ) ) {
662 $ipRevIds[] = $row->rev_id;
667 if ( count( $revids ) > 0 ) {
669 $dbw->insert(
'archive', $rowsInsert, __METHOD__ );
671 $dbw->delete(
'revision', [
'rev_id' => $revids ], __METHOD__ );
672 $dbw->delete(
'revision_comment_temp', [
'revcomment_rev' => $revids ], __METHOD__ );
674 $dbw->delete(
'revision_actor_temp', [
'revactor_rev' => $revids ], __METHOD__ );
678 if ( count( $ipRevIds ) > 0 ) {
679 $dbw->delete(
'ip_changes', [
'ipc_rev_id' => $ipRevIds ], __METHOD__ );
696 $countable = $this->page->isCountable();
697 }
catch ( Exception $ex ) {
704 if ( !$this->isDeletePageUnitTest ) {
706 DeferredUpdates::addUpdate( SiteStatsUpdate::factory(
707 [
'edits' => 1,
'articles' => -$countable,
'pages' => -1 ]
711 $updates = $this->getDeletionUpdates( $revRecord );
712 foreach ( $updates as $update ) {
713 DeferredUpdates::addUpdate( $update );
719 $this->page->getTitle(),
722 $this->deleter->getUser()->getName(),
723 $this->backlinkCacheFactory->getBacklinkCache( $this->page->getTitle() )
726 if ( $this->page->getTitle()->getNamespace() ===
NS_FILE ) {
728 $this->page->getTitle(),
731 $this->deleter->getUser()->getName(),
732 $this->backlinkCacheFactory->getBacklinkCache( $this->page->getTitle() )
736 if ( !$this->isDeletePageUnitTest ) {
743 $this->page->getTitle(),
750 $this->page->loadFromRow(
false, WikiPage::READ_LATEST );
752 if ( !$this->isDeletePageUnitTest ) {
769 $slotContent = array_map( static function (
SlotRecord $slot ) {
770 return $slot->getContent();
771 }, $rev->getSlots()->getSlots() );
780 foreach ( $slotContent as $role =>
$content ) {
781 $handler =
$content->getContentHandler();
783 $updates = $handler->getDeletionUpdates(
784 $this->page->getTitle(),
788 $allUpdates = array_merge( $allUpdates, $updates );
791 $this->hookRunner->onPageDeletionDataUpdates(
792 $this->page->getTitle(), $rev, $allUpdates );
795 $this->hookRunner->onWikiPageDeletionUpdates( $this->page,
$content, $allUpdates );
const SCHEMA_COMPAT_WRITE_TEMP
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(ini_get('mbstring.func_overload')) if(!defined('MW_ENTRY_POINT'))
Pre-config setup: Before loading LocalSettings.php.
Class representing a cache/ephemeral data store.
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.
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.
static queueRecursiveJobsForTable(PageIdentity $page, $table, $action='unknown', $userName='unknown', ?BacklinkCache $backlinkCache=null)
Queue a RefreshLinks job for any table.
Class for creating new log entries and inserting them into the database.
__construct(HookContainer $hookContainer, RevisionStore $revisionStore, LBFactory $lbFactory, JobQueueGroup $jobQueueGroup, CommentStore $commentStore, ServiceOptions $serviceOptions, BagOStuff $recentDeletesCache, string $localWikiID, string $webRequestID, WikiPageFactory $wikiPageFactory, UserFactory $userFactory, ProperPageIdentity $page, Authority $deleter, BacklinkCacheFactory $backlinkCacheFactory)
setDeletionAttempted()
Called before attempting a deletion, allows the result getters to be used.
doDeleteUpdates(RevisionRecord $revRecord)
getDeletionUpdates(RevisionRecord $rev)
setTags(array $tags)
Change tags to apply to the deletion action.
getSuccessfulDeletionsIDs()
deleteIfAllowed(string $reason)
Same as deleteUnsafe, but checks permissions.
setLogSubtype(string $logSubtype)
Set a specific log subtype for the deletion log entry.
keepLegacyHookErrorsSeparate()
BagOStuff $recentDeletesCache
bool $mergeLegacyHookErrors
assertDeletionAttempted()
Asserts that a deletion operation was attempted.
const CONSTRUCTOR_OPTIONS
archiveRevisions(int $id)
Archives revisions as part of page deletion.
deleteInternal(string $reason, ?string $webRequestId=null)
isBatchedDelete(int $safetyMargin=0)
Determines if this deletion would be batched (executed over time by the job queue) or not (completed ...
string array $legacyHookErrors
setIsDeletePageUnitTest(bool $test)
RevisionStore $revisionStore
ILoadBalancer $loadBalancer
bool $isDeletePageUnitTest
int[] null $successfulDeletionsIDs
BacklinkCacheFactory $backlinkCacheFactory
CommentStore $commentStore
deleteUnsafe(string $reason)
Back-end article deletion: deletes the article with database consistency, writes logs,...
bool $attemptedDeletion
Whether a deletion was attempted.
JobQueueGroup $jobQueueGroup
forceImmediate(bool $forceImmediate)
If false, allows deleting over time via the job queue.
setSuppress(bool $suppress)
If true, suppress all revisions and log the deletion in the suppression log instead of the deletion l...
newFromTitle(PageIdentity $pageIdentity)
Create a WikiPage object from a title.
The Message class deals with fetching and processing of interface message into a variety of formats.
Variant of the Message class.
Abstraction for ResourceLoader modules which pull from wiki pages.
static invalidateModuleCache(PageIdentity $page, ?RevisionRecord $old, ?RevisionRecord $new, string $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
Database independent search index updater.
Class for handling updates to the site_stats table.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
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.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Class representing a MediaWiki article and history.
static onArticleDelete(Title $title)
Clears caches when article is deleted.
Base interface for content objects.
Interface that deferrable updates should implement.
Interface for objects representing a page that is (or could be, or used to be) an editable page on a ...
if(count( $args)< 1) $job