MediaWiki  master
RollbackPage.php
Go to the documentation of this file.
1 <?php
21 namespace MediaWiki\Page;
22 
23 use ActorMigration;
25 use ManualLogEntry;
39 use Message;
40 use RawMessage;
41 use ReadOnlyMode;
42 use RecentChange;
43 use StatusValue;
44 use TitleFormatter;
45 use TitleValue;
49 
55 class RollbackPage {
56 
61  public const CONSTRUCTOR_OPTIONS = [
62  'UseRCPatrol',
63  'DisableAnonTalk',
64  ];
65 
67  private $options;
68 
70  private $loadBalancer;
71 
73  private $userFactory;
74 
76  private $readOnlyMode;
77 
79  private $titleFormatter;
80 
82  private $revisionStore;
83 
85  private $hookRunner;
86 
89 
91  private $actorMigration;
92 
95 
97  private $page;
98 
100  private $performer;
101 
103  private $byUser;
104 
106  private $summary = '';
107 
109  private $bot = false;
110 
112  private $tags = [];
113 
129  public function __construct(
130  ServiceOptions $options,
131  ILoadBalancer $loadBalancer,
132  UserFactory $userFactory,
134  RevisionStore $revisionStore,
136  HookContainer $hookContainer,
139  ActorNormalization $actorNormalization,
141  Authority $performer,
142  UserIdentity $byUser
143  ) {
144  $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
145  $this->options = $options;
146  $this->loadBalancer = $loadBalancer;
147  $this->userFactory = $userFactory;
148  $this->readOnlyMode = $readOnlyMode;
149  $this->revisionStore = $revisionStore;
150  $this->titleFormatter = $titleFormatter;
151  $this->hookRunner = new HookRunner( $hookContainer );
152  $this->wikiPageFactory = $wikiPageFactory;
153  $this->actorMigration = $actorMigration;
154  $this->actorNormalization = $actorNormalization;
155 
156  $this->page = $page;
157  $this->performer = $performer;
158  $this->byUser = $byUser;
159  }
160 
167  public function setSummary( ?string $summary ): self {
168  $this->summary = $summary ?: '';
169  return $this;
170  }
171 
178  public function markAsBot( ?bool $bot ): self {
179  if ( $bot && $this->performer->isAllowedAny( 'markbotedits', 'bot' ) ) {
180  $this->bot = true;
181  } elseif ( !$bot ) {
182  $this->bot = false;
183  }
184  return $this;
185  }
186 
195  public function setChangeTags( ?array $tags ): self {
196  $this->tags = $tags ?: [];
197  return $this;
198  }
199 
205  public function authorizeRollback(): PermissionStatus {
206  $permissionStatus = PermissionStatus::newEmpty();
207  $this->performer->authorizeWrite( 'edit', $this->page, $permissionStatus );
208  $this->performer->authorizeWrite( 'rollback', $this->page, $permissionStatus );
209 
210  if ( $this->readOnlyMode->isReadOnly() ) {
211  $permissionStatus->fatal( 'readonlytext' );
212  }
213 
214  $user = $this->userFactory->newFromAuthority( $this->performer );
215  if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
216  $permissionStatus->fatal( 'actionthrottledtext' );
217  }
218  return $permissionStatus;
219  }
220 
230  public function rollbackIfAllowed(): StatusValue {
231  $permissionStatus = $this->authorizeRollback();
232  if ( !$permissionStatus->isGood() ) {
233  return $permissionStatus;
234  }
235  return $this->rollback();
236  }
237 
253  public function rollback() {
254  // Begin revision creation cycle by creating a PageUpdater.
255  // If the page is changed concurrently after grabParentRevision(), the rollback will fail.
256  // TODO: move PageUpdater to PageStore or PageUpdaterFactory or something?
257  $updater = $this->wikiPageFactory->newFromTitle( $this->page )->newPageUpdater( $this->performer );
258  $currentRevision = $updater->grabParentRevision();
259 
260  if ( !$currentRevision ) {
261  // Something wrong... no page?
262  return StatusValue::newFatal( 'notanarticle' );
263  }
264 
265  $currentEditor = $currentRevision->getUser( RevisionRecord::RAW );
266  $currentEditorForPublic = $currentRevision->getUser( RevisionRecord::FOR_PUBLIC );
267  // User name given should match up with the top revision.
268  if ( !$this->byUser->equals( $currentEditor ) ) {
269  $result = StatusValue::newGood( [
270  'current-revision-record' => $currentRevision
271  ] );
272  $result->fatal(
273  'alreadyrolled',
274  htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
275  htmlspecialchars( $this->byUser->getName() ),
276  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
277  );
278  return $result;
279  }
280 
281  $dbw = $this->loadBalancer->getConnectionRef( DB_PRIMARY );
282  // T270033 Index renaming
283  $revIndex = $dbw->indexExists( 'revision', 'page_timestamp', __METHOD__ )
284  ? 'page_timestamp'
285  : 'rev_page_timestamp';
286 
287  // TODO: move this query to RevisionSelectQueryBuilder when it's available
288  // Get the last edit not by this person...
289  // Note: these may not be public values
290  $actorWhere = $this->actorMigration->getWhere( $dbw, 'rev_user', $currentEditor );
291  $targetRevisionRow = $dbw->selectRow(
292  [ 'revision' ] + $actorWhere['tables'],
293  [ 'rev_id', 'rev_timestamp', 'rev_deleted' ],
294  [
295  'rev_page' => $currentRevision->getPageId(),
296  'NOT(' . $actorWhere['conds'] . ')',
297  ],
298  __METHOD__,
299  [
300  'USE INDEX' => [ 'revision' => $revIndex ],
301  'ORDER BY' => [ 'rev_timestamp DESC', 'rev_id DESC' ]
302  ],
303  $actorWhere['joins']
304  );
305 
306  if ( $targetRevisionRow === false ) {
307  // No one else ever edited this page
308  return StatusValue::newFatal( 'cantrollback' );
309  } elseif ( $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_TEXT
310  || $targetRevisionRow->rev_deleted & RevisionRecord::DELETED_USER
311  ) {
312  // Only admins can see this text
313  return StatusValue::newFatal( 'notvisiblerev' );
314  }
315 
316  // Generate the edit summary if necessary
317  $targetRevision = $this->revisionStore
318  ->getRevisionById( $targetRevisionRow->rev_id, RevisionStore::READ_LATEST );
319 
320  // Save
321  $flags = EDIT_UPDATE | EDIT_INTERNAL;
322 
323  if ( $this->performer->isAllowed( 'minoredit' ) ) {
324  $flags |= EDIT_MINOR;
325  }
326 
327  if ( $this->bot ) {
328  $flags |= EDIT_FORCE_BOT;
329  }
330 
331  // TODO: MCR: also log model changes in other slots, in case that becomes possible!
332  $currentContent = $currentRevision->getContent( SlotRecord::MAIN );
333  $targetContent = $targetRevision->getContent( SlotRecord::MAIN );
334  $changingContentModel = $targetContent->getModel() !== $currentContent->getModel();
335 
336  // Build rollback revision:
337  // Restore old content
338  // TODO: MCR: test this once we can store multiple slots
339  foreach ( $targetRevision->getSlots()->getSlots() as $slot ) {
340  $updater->inheritSlot( $slot );
341  }
342 
343  // Remove extra slots
344  // TODO: MCR: test this once we can store multiple slots
345  foreach ( $currentRevision->getSlotRoles() as $role ) {
346  if ( !$targetRevision->hasSlot( $role ) ) {
347  $updater->removeSlot( $role );
348  }
349  }
350 
351  $updater->markAsRevert(
353  $currentRevision->getId(),
354  $targetRevision->getId()
355  );
356 
357  // TODO: this logic should not be in the storage layer, it's here for compatibility
358  // with 1.31 behavior. Applying the 'autopatrol' right should be done in the same
359  // place the 'bot' right is handled, which is currently in EditPage::attemptSave.
360  if ( $this->options->get( 'UseRCPatrol' ) &&
361  $this->performer->authorizeWrite( 'autopatrol', $this->page )
362  ) {
363  $updater->setRcPatrolStatus( RecentChange::PRC_AUTOPATROLLED );
364  }
365 
366  $summary = $this->getSummary( $currentRevision, $targetRevision );
367 
368  // Actually store the rollback
369  $rev = $updater->saveRevision(
371  $flags
372  );
373 
374  // This is done even on edit failure to have patrolling in that case (T64157).
375  $this->updateRecentChange( $dbw, $currentRevision, $targetRevision );
376 
377  if ( !$updater->wasSuccessful() ) {
378  return $updater->getStatus();
379  }
380 
381  // Report if the edit was not created because it did not change the content.
382  if ( $updater->isUnchanged() ) {
383  $result = StatusValue::newGood( [
384  'current-revision-record' => $currentRevision
385  ] );
386  $result->fatal(
387  'alreadyrolled',
388  htmlspecialchars( $this->titleFormatter->getPrefixedText( $this->page ) ),
389  htmlspecialchars( $this->byUser->getName() ),
390  htmlspecialchars( $currentEditorForPublic ? $currentEditorForPublic->getName() : '' )
391  );
392  return $result;
393  }
394 
395  if ( $changingContentModel ) {
396  // If the content model changed during the rollback,
397  // make sure it gets logged to Special:Log/contentmodel
398  $log = new ManualLogEntry( 'contentmodel', 'change' );
399  $log->setPerformer( $this->performer->getUser() );
400  $log->setTarget( new TitleValue( $this->page->getNamespace(), $this->page->getDBkey() ) );
401  $log->setComment( $summary );
402  $log->setParameters( [
403  '4::oldmodel' => $currentContent->getModel(),
404  '5::newmodel' => $targetContent->getModel(),
405  ] );
406 
407  $logId = $log->insert( $dbw );
408  $log->publish( $logId );
409  }
410 
411  $wikiPage = $this->wikiPageFactory->newFromTitle( $this->page );
412 
413  $this->hookRunner->onRollbackComplete(
414  $wikiPage,
415  $this->performer->getUser(),
416  $targetRevision,
417  $currentRevision
418  );
419 
420  return StatusValue::newGood( [
421  'summary' => $summary,
422  'current-revision-record' => $currentRevision,
423  'target-revision-record' => $targetRevision,
424  'newid' => $rev->getId(),
425  'tags' => array_merge( $this->tags, $updater->getEditResult()->getRevertTags() )
426  ] );
427  }
428 
436  private function updateRecentChange(
437  IDatabase $dbw,
438  RevisionRecord $current,
439  RevisionRecord $target
440  ) {
441  $set = [];
442  if ( $this->bot ) {
443  // Mark all reverted edits as bot
444  $set['rc_bot'] = 1;
445  }
446 
447  if ( $this->options->get( 'UseRCPatrol' ) ) {
448  // Mark all reverted edits as patrolled
449  $set['rc_patrolled'] = RecentChange::PRC_AUTOPATROLLED;
450  }
451 
452  if ( $set ) {
453  $actorId = $this->actorNormalization
454  ->acquireActorId( $current->getUser( RevisionRecord::RAW ), $dbw );
455  $dbw->update(
456  'recentchanges',
457  $set,
458  [ /* WHERE */
459  'rc_cur_id' => $current->getPageId(),
460  'rc_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $target->getTimestamp() ) ),
461  'rc_actor' => $actorId
462  ],
463  __METHOD__
464  );
465  }
466  }
467 
475  private function getSummary( RevisionRecord $current, RevisionRecord $target ): string {
476  $currentEditorForPublic = $current->getUser( RevisionRecord::FOR_PUBLIC );
477  if ( !$this->summary ) {
478  if ( !$currentEditorForPublic ) { // no public user name
479  $summary = MessageValue::new( 'revertpage-nouser' );
480  } elseif ( $this->options->get( 'DisableAnonTalk' ) && !$currentEditorForPublic->isRegistered() ) {
481  $summary = MessageValue::new( 'revertpage-anon' );
482  } else {
483  $summary = MessageValue::new( 'revertpage' );
484  }
485  } else {
487  }
488 
489  $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
490  // Allow the custom summary to use the same args as the default message
491  $args = [
492  $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
493  $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
494  $target->getId(),
495  Message::dateTimeParam( $target->getTimestamp() ),
496  $current->getId(),
497  Message::dateTimeParam( $current->getTimestamp() ),
498  ];
499  if ( $summary instanceof MessageValue ) {
500  $summary = ( new Converter() )->convertMessageValue( $summary );
501  $summary = $summary->params( $args )->inContentLanguage()->text();
502  } else {
503  $summary = ( new RawMessage( $summary, $args ) )->inContentLanguage()->plain();
504  }
505 
506  // Trim spaces on user supplied text
507  return trim( $summary );
508  }
509 }
MediaWiki\Revision\RevisionRecord\RAW
const RAW
Definition: RevisionRecord.php:64
Page\PageIdentity
Interface for objects (potentially) representing an editable wiki page.
Definition: PageIdentity.php:64
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
StatusValue
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: StatusValue.php:43
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
MediaWiki\Revision\RevisionRecord\DELETED_USER
const DELETED_USER
Definition: RevisionRecord.php:55
StatusValue\newFatal
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
Page\RollbackPage\$page
PageIdentity $page
Definition: RollbackPage.php:97
Page\RollbackPage\setChangeTags
setChangeTags(?array $tags)
Change tags to apply to the rollback.
Definition: RollbackPage.php:195
MediaWiki\Storage\EditResult\REVERT_ROLLBACK
const REVERT_ROLLBACK
Definition: EditResult.php:42
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
RecentChange
Utility class for creating new RC entries.
Definition: RecentChange.php:80
ReadOnlyMode
A service class for fetching the wiki's current read-only mode.
Definition: ReadOnlyMode.php:11
MediaWiki\Permissions\PermissionStatus\newEmpty
static newEmpty()
Definition: PermissionStatus.php:67
Page\RollbackPage\$readOnlyMode
ReadOnlyMode $readOnlyMode
Definition: RollbackPage.php:76
ActorMigration
This is not intended to be a long-term part of MediaWiki; it will be deprecated and removed once acto...
Definition: ActorMigration.php:15
Page\RollbackPage\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RollbackPage.php:70
Page\RollbackPage\$actorMigration
ActorMigration $actorMigration
Definition: RollbackPage.php:91
MediaWiki\User\UserIdentity
Interface for objects representing user identity.
Definition: UserIdentity.php:39
Wikimedia\Message\MessageValue
Value object representing a message for i18n.
Definition: MessageValue.php:16
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Page\RollbackPage\getSummary
getSummary(RevisionRecord $current, RevisionRecord $target)
Generate and format summary for the rollback.
Definition: RollbackPage.php:475
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
MediaWiki\Revision\SlotRecord\MAIN
const MAIN
Definition: SlotRecord.php:43
Page\RollbackPage\$tags
string[] $tags
Definition: RollbackPage.php:112
Page\RollbackPage\markAsBot
markAsBot(?bool $bot)
Mark all reverted edits as bot.
Definition: RollbackPage.php:178
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
Page\RollbackPage\$wikiPageFactory
WikiPageFactory $wikiPageFactory
Definition: RollbackPage.php:88
Page\RollbackPage\$bot
bool $bot
Definition: RollbackPage.php:109
Page\RollbackPage\$userFactory
UserFactory $userFactory
Definition: RollbackPage.php:73
Page\RollbackPage\rollback
rollback()
Backend implementation of rollbackIfAllowed().
Definition: RollbackPage.php:253
Page\RollbackPage\__construct
__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)
Definition: RollbackPage.php:129
$args
if( $line===false) $args
Definition: mcc.php:124
Page\WikiPageFactory
Definition: WikiPageFactory.php:20
Page\RollbackPage
Definition: RollbackPage.php:55
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
Message\Converter
Converter between Message and MessageValue.
Definition: Converter.php:18
MediaWiki\Permissions\Authority
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
StatusValue\newGood
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
DB_PRIMARY
const DB_PRIMARY
Definition: defines.php:27
Page\RollbackPage\$titleFormatter
TitleFormatter $titleFormatter
Definition: RollbackPage.php:79
Page\RollbackPage\$actorNormalization
ActorNormalization $actorNormalization
Definition: RollbackPage.php:94
RecentChange\PRC_AUTOPATROLLED
const PRC_AUTOPATROLLED
Definition: RecentChange.php:93
Page\RollbackPage\$revisionStore
RevisionStore $revisionStore
Definition: RollbackPage.php:82
MediaWiki\Permissions\PermissionStatus
A StatusValue for permission errors.
Definition: PermissionStatus.php:35
EDIT_UPDATE
const EDIT_UPDATE
Definition: Defines.php:126
Page\RollbackPage\setSummary
setSummary(?string $summary)
Set custom edit summary.
Definition: RollbackPage.php:167
Page\RollbackPage\$hookRunner
HookRunner $hookRunner
Definition: RollbackPage.php:85
Page\RollbackPage\updateRecentChange
updateRecentChange(IDatabase $dbw, RevisionRecord $current, RevisionRecord $target)
Set patrolling and bot flag on the edits, which gets rolled back.
Definition: RollbackPage.php:436
Message\dateTimeParam
static dateTimeParam(string $dateTime)
Definition: Message.php:1139
TitleFormatter
A title formatter service for MediaWiki.
Definition: TitleFormatter.php:35
Page\RollbackPage\rollbackIfAllowed
rollbackIfAllowed()
Rollback the most recent consecutive set of edits to a page from the same user; fails if there are no...
Definition: RollbackPage.php:230
Page\RollbackPage\authorizeRollback
authorizeRollback()
Authorize the rollback.
Definition: RollbackPage.php:205
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:138
Wikimedia\Message\MessageValue\new
static new( $key, $params=[])
Static constructor for easier chaining of ->params() methods.
Definition: MessageValue.php:42
MediaWiki\Page
Definition: ContentModelChangeFactory.php:23
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:44
Page\RollbackPage\$performer
Authority $performer
Definition: RollbackPage.php:100
MediaWiki\HookContainer\HookContainer
HookContainer class.
Definition: HookContainer.php:45
MediaWiki\HookContainer\HookRunner
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:557
EDIT_FORCE_BOT
const EDIT_FORCE_BOT
Definition: Defines.php:129
RawMessage
Variant of the Message class.
Definition: RawMessage.php:35
EDIT_MINOR
const EDIT_MINOR
Definition: Defines.php:127
Page\RollbackPage\$byUser
UserIdentity $byUser
who made the edits we are rolling back
Definition: RollbackPage.php:103
MediaWiki\Revision\RevisionRecord\FOR_PUBLIC
const FOR_PUBLIC
Definition: RevisionRecord.php:62
MediaWiki\User\UserFactory
Creates User objects.
Definition: UserFactory.php:41
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
Page\RollbackPage\$options
ServiceOptions $options
Definition: RollbackPage.php:57
Page\RollbackPage\$summary
string $summary
Definition: RollbackPage.php:106
MediaWiki\Revision\SlotRecord
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
MediaWiki\User\ActorNormalization
Service for dealing with the actor table.
Definition: ActorNormalization.php:32
TitleValue
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:40
EDIT_INTERNAL
const EDIT_INTERNAL
Definition: Defines.php:132