62 public const CONSTRUCTOR_OPTIONS = [
71 private $loadBalancer;
77 private $readOnlyMode;
80 private $titleFormatter;
83 private $revisionStore;
89 private $wikiPageFactory;
92 private $actorMigration;
95 private $actorNormalization;
107 private $summary =
'';
110 private $bot =
false;
147 $this->options = $options;
148 $this->loadBalancer = $loadBalancer;
149 $this->userFactory = $userFactory;
150 $this->readOnlyMode = $readOnlyMode;
151 $this->revisionStore = $revisionStore;
152 $this->titleFormatter = $titleFormatter;
153 $this->hookRunner =
new HookRunner( $hookContainer );
154 $this->wikiPageFactory = $wikiPageFactory;
155 $this->actorMigration = $actorMigration;
156 $this->actorNormalization = $actorNormalization;
159 $this->performer = $performer;
160 $this->byUser = $byUser;
170 $this->summary = $summary ??
'';
181 if ( $bot && $this->performer->isAllowedAny(
'markbotedits',
'bot' ) ) {
198 $this->tags = $tags ?: [];
209 $this->performer->authorizeWrite(
'edit', $this->page, $permissionStatus );
210 $this->performer->authorizeWrite(
'rollback', $this->page, $permissionStatus );
212 if ( $this->readOnlyMode->isReadOnly() ) {
213 $permissionStatus->
fatal(
'readonlytext' );
216 $user = $this->userFactory->newFromAuthority( $this->performer );
217 if ( $user->pingLimiter(
'rollback' ) || $user->pingLimiter() ) {
218 $permissionStatus->fatal(
'actionthrottledtext' );
220 return $permissionStatus;
233 $permissionStatus = $this->authorizeRollback();
234 if ( !$permissionStatus->isGood() ) {
235 return $permissionStatus;
237 return $this->rollback();
259 $updater = $this->wikiPageFactory->newFromTitle( $this->page )->newPageUpdater( $this->performer );
260 $currentRevision = $updater->grabParentRevision();
262 if ( !$currentRevision ) {
264 return StatusValue::newFatal(
'notanarticle' );
267 $currentEditor = $currentRevision->getUser( RevisionRecord::RAW );
268 $currentEditorForPublic = $currentRevision->getUser( RevisionRecord::FOR_PUBLIC );
270 if ( !$this->byUser->equals( $currentEditor ) ) {
271 $result = StatusValue::newGood( [
272 'current-revision-record' => $currentRevision
276 htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
277 htmlspecialchars( $this->byUser->getName() ),
278 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() :
'' )
283 $dbw = $this->loadBalancer->getConnectionRef(
DB_PRIMARY );
288 $actorWhere = $this->actorMigration->getWhere( $dbw,
'rev_user', $currentEditor );
290 [
'revision' ] + $actorWhere[
'tables'],
291 [
'rev_id',
'rev_timestamp',
'rev_deleted' ],
293 'rev_page' => $currentRevision->getPageId(),
294 'NOT(' . $actorWhere[
'conds'] .
')',
298 'USE INDEX' => [
'revision' =>
'rev_page_timestamp' ],
299 'ORDER BY' => [
'rev_timestamp DESC',
'rev_id DESC' ]
304 if ( $targetRevisionRow ===
false ) {
306 return StatusValue::newFatal(
'cantrollback' );
307 } elseif ( $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_TEXT
308 || $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_USER
311 return StatusValue::newFatal(
'notvisiblerev' );
315 $targetRevision = $this->revisionStore
316 ->getRevisionById( $targetRevisionRow->rev_id, RevisionStore::READ_LATEST );
321 if ( $this->performer->isAllowed(
'minoredit' ) ) {
330 $currentContent = $currentRevision->getContent( SlotRecord::MAIN );
331 $targetContent = $targetRevision->getContent( SlotRecord::MAIN );
332 $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
337 foreach ( $targetRevision->getSlots()->getSlots() as $slot ) {
338 $updater->inheritSlot( $slot );
343 foreach ( $currentRevision->getSlotRoles() as $role ) {
344 if ( !$targetRevision->hasSlot( $role ) ) {
345 $updater->removeSlot( $role );
349 $updater->markAsRevert(
350 EditResult::REVERT_ROLLBACK,
351 $currentRevision->getId(),
352 $targetRevision->getId()
358 if ( $this->options->get( MainConfigNames::UseRCPatrol ) &&
359 $this->performer->authorizeWrite(
'autopatrol', $this->page )
361 $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
364 $summary = $this->getSummary( $currentRevision, $targetRevision );
367 $rev = $updater->saveRevision(
368 CommentStoreComment::newUnsavedComment( $summary ),
373 $this->updateRecentChange( $dbw, $currentRevision, $targetRevision );
375 if ( !$updater->wasSuccessful() ) {
376 return $updater->getStatus();
380 if ( !$updater->wasRevisionCreated() ) {
381 $result = StatusValue::newGood( [
382 'current-revision-record' => $currentRevision
386 htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
387 htmlspecialchars( $this->byUser->getName() ),
388 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() :
'' )
393 if ( $changingContentModel ) {
397 $log->setPerformer( $this->performer->getUser() );
398 $log->setTarget(
new TitleValue( $this->page->getNamespace(), $this->page->getDBkey() ) );
399 $log->setComment( $summary );
400 $log->setParameters( [
401 '4::oldmodel' => $currentContent->getModel(),
402 '5::newmodel' => $targetContent->getModel(),
405 $logId = $log->insert( $dbw );
406 $log->publish( $logId );
409 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
411 $this->hookRunner->onRollbackComplete(
413 $this->performer->getUser(),
418 return StatusValue::newGood( [
419 'summary' => $summary,
420 'current-revision-record' => $currentRevision,
421 'target-revision-record' => $targetRevision,
422 'newid' => $rev->getId(),
423 'tags' => array_merge( $this->tags, $updater->getEditResult()->getRevertTags() )
434 private function updateRecentChange(
439 $useRCPatrol = $this->options->get( MainConfigNames::UseRCPatrol );
440 if ( !$this->bot && !$useRCPatrol ) {
444 $actorId = $this->actorNormalization
445 ->acquireActorId( $current->
getUser( RevisionRecord::RAW ), $dbw );
449 [
'rc_id',
'rc_patrolled' ],
453 'rc_timestamp > ' . $dbw->
addQuotes( $timestamp ),
455 'rc_timestamp' => $timestamp,
457 ], IDatabase::LIST_AND ),
458 ], IDatabase::LIST_OR ),
459 'rc_actor' => $actorId,
467 foreach ( $rows as $row ) {
468 $all[] = (int)$row->rc_id;
469 if ( $row->rc_patrolled ) {
470 $patrolled[] = (int)$row->rc_id;
472 $unpatrolled[] = (int)$row->rc_id;
476 if ( $useRCPatrol && $this->bot ) {
479 if ( $unpatrolled ) {
482 [
'rc_bot' => 1,
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
483 [
'rc_id' => $unpatrolled ],
491 [
'rc_id' => $patrolled ],
495 } elseif ( $useRCPatrol ) {
497 if ( $unpatrolled ) {
500 [
'rc_patrolled' => RecentChange::PRC_AUTOPATROLLED ],
501 [
'rc_id' => $unpatrolled ],
525 private function getSummary( RevisionRecord $current, RevisionRecord $target ): string {
526 $currentEditorForPublic = $current->
getUser( RevisionRecord::FOR_PUBLIC );
527 if ( $this->summary ===
'' ) {
528 if ( !$currentEditorForPublic ) {
529 $summary = MessageValue::new(
'revertpage-nouser' );
530 } elseif ( $this->options->get( MainConfigNames::DisableAnonTalk ) &&
531 !$currentEditorForPublic->isRegistered() ) {
532 $summary = MessageValue::new(
'revertpage-anon' );
534 $summary = MessageValue::new(
'revertpage' );
537 $summary = $this->summary;
540 $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
543 $targetEditorForPublic ? $targetEditorForPublic->getName() :
null,
544 $currentEditorForPublic ? $currentEditorForPublic->getName() :
null,
550 if ( $summary instanceof MessageValue ) {
551 $summary = (
new Converter() )->convertMessageValue( $summary );
552 $summary = $summary->params(
$args )->inContentLanguage()->text();
554 $summary = (
new RawMessage( $summary,
$args ) )->inContentLanguage()->plain();
558 return trim( $summary );
if(!defined('MW_SETUP_CALLBACK'))
The persistent session ID (if any) loaded at startup.
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const UseRCPatrol
Name constant for the UseRCPatrol setting, for use with Config::get()
const DisableAnonTalk
Name constant for the DisableAnonTalk setting, for use with Config::get()
Backend logic for performing a page rollback action.
rollback()
Backend implementation of rollbackIfAllowed().
setSummary(?string $summary)
Set custom edit summary.
authorizeRollback()
Authorize the rollback.
setChangeTags(?array $tags)
Change tags to apply to the rollback.
__construct(ServiceOptions $options, ILoadBalancer $loadBalancer, UserFactory $userFactory, ReadOnlyMode $readOnlyMode, RevisionStore $revisionStore, TitleFormatter $titleFormatter, HookContainer $hookContainer, WikiPageFactory $wikiPageFactory, ActorMigration $actorMigration, ActorNormalization $actorNormalization, PageIdentity $page, Authority $performer, UserIdentity $byUser)
rollbackIfAllowed()
Rollback the most recent consecutive set of edits to a page from the same user; fails if there are no...
markAsBot(?bool $bot)
Mark all reverted edits as bot.
Service for creating WikiPage objects.
The Message class deals with fetching and processing of interface message into a variety of formats.
static dateTimeParam(string $dateTime)
Variant of the Message class.
A service class for fetching the wiki's current read-only mode.
Utility class for creating new RC entries.
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.
Represents a page (or page fragment) title within MediaWiki.
Interface for objects (potentially) representing an editable wiki page.