63 public const CONSTRUCTOR_OPTIONS = [
78 private $readOnlyMode;
81 private $titleFormatter;
84 private $revisionStore;
90 private $wikiPageFactory;
93 private $actorMigration;
96 private $actorNormalization;
108 private $summary =
'';
111 private $bot =
false;
148 $this->options = $options;
149 $this->dbProvider = $dbProvider;
150 $this->userFactory = $userFactory;
151 $this->readOnlyMode = $readOnlyMode;
152 $this->revisionStore = $revisionStore;
153 $this->titleFormatter = $titleFormatter;
154 $this->hookRunner =
new HookRunner( $hookContainer );
155 $this->wikiPageFactory = $wikiPageFactory;
156 $this->actorMigration = $actorMigration;
157 $this->actorNormalization = $actorNormalization;
160 $this->performer = $performer;
161 $this->byUser = $byUser;
171 $this->summary = $summary ??
'';
182 if ( $bot && $this->performer->isAllowedAny(
'markbotedits',
'bot' ) ) {
199 $this->tags = $tags ?: [];
210 $this->performer->authorizeWrite(
'edit', $this->page, $permissionStatus );
211 $this->performer->authorizeWrite(
'rollback', $this->page, $permissionStatus );
213 if ( $this->readOnlyMode->isReadOnly() ) {
214 $permissionStatus->
fatal(
'readonlytext' );
217 $user = $this->userFactory->newFromAuthority( $this->performer );
218 if ( $user->pingLimiter(
'rollback' ) || $user->pingLimiter() ) {
219 $permissionStatus->fatal(
'actionthrottledtext' );
221 return $permissionStatus;
234 $permissionStatus = $this->authorizeRollback();
235 if ( !$permissionStatus->isGood() ) {
236 return $permissionStatus;
238 return $this->rollback();
260 $updater = $this->wikiPageFactory->newFromTitle( $this->page )->newPageUpdater( $this->performer );
261 $currentRevision = $updater->grabParentRevision();
263 if ( !$currentRevision ) {
268 $currentEditor = $currentRevision->getUser( RevisionRecord::RAW );
269 $currentEditorForPublic = $currentRevision->getUser( RevisionRecord::FOR_PUBLIC );
271 if ( !$this->byUser->equals( $currentEditor ) ) {
273 'current-revision-record' => $currentRevision
277 htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
278 htmlspecialchars( $this->byUser->getName() ),
279 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() :
'' )
284 $dbw = $this->dbProvider->getPrimaryDatabase();
288 $actorWhere = $this->actorMigration->getWhere( $dbw,
'rev_user', $currentEditor );
290 ->where( [
'rev_page' => $currentRevision->getPageId(),
'NOT(' . $actorWhere[
'conds'] .
')' ] )
291 ->useIndex( [
'revision' =>
'rev_page_timestamp' ] )
292 ->orderBy( [
'rev_timestamp',
'rev_id' ], SelectQueryBuilder::SORT_DESC );
293 $targetRevisionRow = $queryBuilder->caller( __METHOD__ )->fetchRow();
295 if ( $targetRevisionRow ===
false ) {
298 } elseif ( $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_TEXT
299 || $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_USER
306 $targetRevision = $this->revisionStore
307 ->getRevisionById( $targetRevisionRow->rev_id, RevisionStore::READ_LATEST );
312 if ( $this->performer->isAllowed(
'minoredit' ) ) {
321 $currentContent = $currentRevision->getContent( SlotRecord::MAIN );
322 $targetContent = $targetRevision->getContent( SlotRecord::MAIN );
323 $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
328 foreach ( $targetRevision->getSlots()->getSlots() as $slot ) {
329 $updater->inheritSlot( $slot );
334 foreach ( $currentRevision->getSlotRoles() as $role ) {
335 if ( !$targetRevision->hasSlot( $role ) ) {
336 $updater->removeSlot( $role );
340 $updater->markAsRevert(
341 EditResult::REVERT_ROLLBACK,
342 $currentRevision->getId(),
343 $targetRevision->getId()
349 if ( $this->options->get( MainConfigNames::UseRCPatrol ) &&
350 $this->performer->authorizeWrite(
'autopatrol', $this->page )
355 $summary = $this->getSummary( $currentRevision, $targetRevision );
358 $rev = $updater->addTags( $this->tags )->saveRevision(
359 CommentStoreComment::newUnsavedComment( $summary ),
364 $this->updateRecentChange( $dbw, $currentRevision, $targetRevision );
366 if ( !$updater->wasSuccessful() ) {
367 return $updater->getStatus();
371 if ( !$updater->wasRevisionCreated() ) {
373 'current-revision-record' => $currentRevision
377 htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
378 htmlspecialchars( $this->byUser->getName() ),
379 htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() :
'' )
384 if ( $changingContentModel ) {
388 $log->setPerformer( $this->performer->getUser() );
389 $log->setTarget(
new TitleValue( $this->page->getNamespace(), $this->page->getDBkey() ) );
390 $log->setComment( $summary );
391 $log->setParameters( [
392 '4::oldmodel' => $currentContent->getModel(),
393 '5::newmodel' => $targetContent->getModel(),
396 $logId = $log->insert( $dbw );
397 $log->publish( $logId );
400 $wikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
402 $this->hookRunner->onRollbackComplete(
404 $this->performer->getUser(),
410 'summary' => $summary,
411 'current-revision-record' => $currentRevision,
412 'target-revision-record' => $targetRevision,
413 'newid' => $rev->getId(),
414 'tags' => array_merge( $this->tags, $updater->getEditResult()->getRevertTags() )
425 private function updateRecentChange(
430 $useRCPatrol = $this->options->get( MainConfigNames::UseRCPatrol );
431 if ( !$this->bot && !$useRCPatrol ) {
435 $actorId = $this->actorNormalization
436 ->acquireActorId( $current->
getUser( RevisionRecord::RAW ), $dbw );
439 ->select( [
'rc_id',
'rc_patrolled' ] )
440 ->from(
'recentchanges' )
441 ->where( [
'rc_cur_id' => $current->
getPageId(),
'rc_actor' => $actorId, ] )
443 'rc_timestamp' => $timestamp,
444 'rc_this_oldid' => $target->
getId(),
446 ->caller( __METHOD__ )->fetchResultSet();
451 foreach ( $rows as $row ) {
452 $all[] = (int)$row->rc_id;
453 if ( $row->rc_patrolled ) {
454 $patrolled[] = (int)$row->rc_id;
456 $unpatrolled[] = (int)$row->rc_id;
460 if ( $useRCPatrol && $this->bot ) {
463 if ( $unpatrolled ) {
465 ->update(
'recentchanges' )
467 ->where( [
'rc_id' => $unpatrolled ] )
468 ->caller( __METHOD__ )->execute();
472 ->update(
'recentchanges' )
473 ->set( [
'rc_bot' => 1 ] )
474 ->where( [
'rc_id' => $patrolled ] )
475 ->caller( __METHOD__ )->execute();
477 } elseif ( $useRCPatrol ) {
479 if ( $unpatrolled ) {
481 ->update(
'recentchanges' )
483 ->where( [
'rc_id' => $unpatrolled ] )
484 ->caller( __METHOD__ )->execute();
490 ->update(
'recentchanges' )
491 ->set( [
'rc_bot' => 1 ] )
492 ->where( [
'rc_id' => $all ] )
493 ->caller( __METHOD__ )->execute();
505 private function getSummary( RevisionRecord $current, RevisionRecord $target ): string {
506 $revisionsBetween = $this->revisionStore->countRevisionsBetween(
507 $current->getPageId(),
511 RevisionStore::INCLUDE_NEW
513 $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
514 if ( $this->summary ===
'' ) {
515 if ( !$currentEditorForPublic ) {
516 $summary = MessageValue::new(
'revertpage-nouser' );
517 } elseif ( $this->options->get( MainConfigNames::DisableAnonTalk ) &&
518 !$currentEditorForPublic->isRegistered() ) {
519 $summary = MessageValue::new(
'revertpage-anon' );
521 $summary = MessageValue::new(
'revertpage' );
524 $summary = $this->summary;
527 $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
530 $targetEditorForPublic ? $targetEditorForPublic->getName() :
null,
531 $currentEditorForPublic ? $currentEditorForPublic->getName() :
null,
538 if ( $summary instanceof MessageValue ) {
539 $summary = (
new Converter() )->convertMessageValue( $summary );
540 $summary = $summary->params( $args )->inContentLanguage()->text();
542 $summary = (
new RawMessage( $summary, $args ) )->inContentLanguage()->plain();
546 return trim( $summary );
if(!defined('MW_SETUP_CALLBACK'))
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.
__construct(ServiceOptions $options, IConnectionProvider $dbProvider, UserFactory $userFactory, ReadOnlyMode $readOnlyMode, RevisionStore $revisionStore, TitleFormatter $titleFormatter, HookContainer $hookContainer, WikiPageFactory $wikiPageFactory, ActorMigration $actorMigration, ActorNormalization $actorNormalization, PageIdentity $page, Authority $performer, UserIdentity $byUser)
authorizeRollback()
Authorize the rollback.
setChangeTags(?array $tags)
Change tags to apply to the rollback.
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)
Utility class for creating new RC entries.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
static newFatal( $message,... $parameters)
Factory function for fatal errors.
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.
Interface for objects (potentially) representing an editable wiki page.