MediaWiki  master
RevertedTagUpdate.php
Go to the documentation of this file.
1 <?php
23 namespace MediaWiki\Storage;
24 
25 use ChangeTags;
27 use FormatJson;
31 use Psr\Log\LoggerInterface;
33 
42 
46  public const CONSTRUCTOR_OPTIONS = [ 'RevertedTagMaxDepth' ];
47 
49  private $revisionStore;
50 
52  private $logger;
53 
55  private $softwareTags;
56 
58  private $loadBalancer;
59 
61  private $options;
62 
64  private $revertId;
65 
67  private $editResult;
68 
70  private $revertRevision;
71 
74 
77 
88  public function __construct(
90  LoggerInterface $logger,
91  array $softwareTags,
93  ServiceOptions $serviceOptions,
94  int $revertId,
96  ) {
97  $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
98 
99  $this->revisionStore = $revisionStore;
100  $this->logger = $logger;
101  $this->softwareTags = $softwareTags;
102  $this->loadBalancer = $loadBalancer;
103  $this->options = $serviceOptions;
104  $this->revertId = $revertId;
105  $this->editResult = $editResult;
106  }
107 
111  public function doUpdate() {
112  // Do extensive checks, as the update may be carried out even months after the edit
113  if ( !$this->shouldExecute() ) {
114  return;
115  }
116 
117  // Skip some of the DB code and just tag it if only one edit was reverted
118  if ( $this->handleSingleRevertedEdit() ) {
119  return;
120  }
121 
122  $maxDepth = $this->options->get( 'RevertedTagMaxDepth' );
123  $extraParams = $this->getTagExtraParams();
124  $revertedRevisionIds = $this->revisionStore->getRevisionIdsBetween(
125  $this->getOldestRevertedRevision()->getPageId(),
126  $this->getOldestRevertedRevision(),
127  $this->getNewestRevertedRevision(),
128  $maxDepth,
130  );
131 
132  if ( count( $revertedRevisionIds ) > $maxDepth ) {
133  // This revert exceeds the depth limit
134  $this->logger->notice(
135  'The revert is deeper than $wgRevertedTagMaxDepth. Skipping...',
136  $extraParams
137  );
138  return;
139  }
140 
141  $revertedRevision = null;
142  foreach ( $revertedRevisionIds as $revId ) {
143  $previousRevision = $revertedRevision;
144 
145  $revertedRevision = $this->revisionStore->getRevisionById( $revId );
146  if ( $revertedRevision === null ) {
147  // Shouldn't happen, but necessary for static analysis
148  continue;
149  }
150 
151  if ( $previousRevision === null ) {
152  $previousRevision = $this->revisionStore->getPreviousRevision(
153  $revertedRevision
154  );
155  }
156  if ( $previousRevision !== null &&
157  $revertedRevision->hasSameContent( $previousRevision )
158  ) {
159  // This is a null revision (e.g. a page move or protection record)
160  // See: T265312
161  continue;
162  }
163 
164  $this->markAsReverted(
165  $revId,
166  $extraParams
167  );
168  }
169  }
170 
176  private function shouldExecute(): bool {
177  $maxDepth = $this->options->get( 'RevertedTagMaxDepth' );
178  if ( !in_array( ChangeTags::TAG_REVERTED, $this->softwareTags ) || $maxDepth <= 0 ) {
179  return false;
180  }
181 
182  $extraParams = $this->getTagExtraParams();
183  if ( !$this->editResult->isRevert() ||
184  $this->editResult->getOldestRevertedRevisionId() === null ||
185  $this->editResult->getNewestRevertedRevisionId() === null
186  ) {
187  $this->logger->error( 'Invalid EditResult specified.', $extraParams );
188  return false;
189  }
190 
191  if ( !$this->getOldestRevertedRevision() || !$this->getNewestRevertedRevision() ) {
192  $this->logger->error(
193  'Could not find the newest or oldest reverted revision in the database.',
194  $extraParams
195  );
196  return false;
197  }
198  if ( !$this->getRevertRevision() ) {
199  $this->logger->error(
200  'Could not find the revert revision in the database.',
201  $extraParams
202  );
203  return false;
204  }
205 
206  if ( $this->getNewestRevertedRevision()->getPageId() !==
207  $this->getOldestRevertedRevision()->getPageId()
208  ||
209  $this->getOldestRevertedRevision()->getPageId() !==
210  $this->getRevertRevision()->getPageId()
211  ) {
212  $this->logger->error(
213  'The revert and reverted revisions belong to different pages.',
214  $extraParams
215  );
216  return false;
217  }
218 
219  if ( $this->getRevertRevision()->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
220  // The revert's text is marked as deleted, which probably means the update
221  // shouldn't be executed.
222  $this->logger->notice(
223  'The revert\'s text had been marked as deleted before the update was ' .
224  'executed. Skipping...',
225  $extraParams
226  );
227  return false;
228  }
229 
230  $changeTagsOnRevert = $this->getChangeTags( $this->revertId );
231  if ( in_array( ChangeTags::TAG_REVERTED, $changeTagsOnRevert ) ) {
232  // This is already marked as reverted, which means the update was delayed
233  // until the edit is approved. Apparently, the edit was not approved, as
234  // it was reverted, so the update should not be performed.
235  $this->logger->notice(
236  'The revert had been reverted before the update was executed. Skipping...',
237  $extraParams
238  );
239  return false;
240  }
241 
242  return true;
243  }
244 
254  private function handleSingleRevertedEdit(): bool {
255  if ( $this->editResult->getOldestRevertedRevisionId() !==
256  $this->editResult->getNewestRevertedRevisionId()
257  ) {
258  return false;
259  }
260 
261  $revertedRevision = $this->getOldestRevertedRevision();
262  if ( $revertedRevision === null ||
263  $revertedRevision->isDeleted( RevisionRecord::DELETED_TEXT )
264  ) {
265  return true;
266  }
267 
268  $previousRevision = $this->revisionStore->getPreviousRevision(
269  $revertedRevision
270  );
271  if ( $previousRevision !== null &&
272  $revertedRevision->hasSameContent( $previousRevision )
273  ) {
274  // Ignore the very rare case of a null edit. This should not occur unless
275  // someone does something weird with the page's history before the update
276  // is executed.
277  return true;
278  }
279 
280  $this->markAsReverted(
281  $this->editResult->getOldestRevertedRevisionId(),
282  $this->getTagExtraParams()
283  );
284  return true;
285  }
286 
299  protected function markAsReverted( int $revisionId, array $extraParams ) {
302  null,
303  $revisionId,
304  null,
305  FormatJson::encode( $extraParams )
306  );
307  }
308 
322  protected function getChangeTags( int $revisionId ) {
323  return ChangeTags::getTags(
324  $this->loadBalancer->getConnection( DB_REPLICA ),
325  null,
326  $revisionId
327  );
328  }
329 
337  private function getTagExtraParams(): array {
338  return array_merge(
339  [ 'revertId' => $this->revertId ],
340  $this->editResult->jsonSerialize()
341  );
342  }
343 
349  private function getRevertRevision(): ?RevisionRecord {
350  if ( !isset( $this->revertRevision ) ) {
351  $this->revertRevision = $this->revisionStore->getRevisionById(
352  $this->revertId
353  );
354  }
355  return $this->revertRevision;
356  }
357 
364  if ( !isset( $this->newestRevertedRevision ) ) {
365  $this->newestRevertedRevision = $this->revisionStore->getRevisionById(
366  $this->editResult->getNewestRevertedRevisionId()
367  );
368  }
370  }
371 
378  if ( !isset( $this->oldestRevertedRevision ) ) {
379  $this->oldestRevertedRevision = $this->revisionStore->getRevisionById(
380  $this->editResult->getOldestRevertedRevisionId()
381  );
382  }
384  }
385 }
MediaWiki\Storage\RevertedTagUpdate\__construct
__construct(RevisionStore $revisionStore, LoggerInterface $logger, array $softwareTags, ILoadBalancer $loadBalancer, ServiceOptions $serviceOptions, int $revertId, EditResult $editResult)
Definition: RevertedTagUpdate.php:88
MediaWiki\Storage\RevertedTagUpdate\$options
ServiceOptions $options
Definition: RevertedTagUpdate.php:61
MediaWiki\Storage\RevertedTagUpdate\$oldestRevertedRevision
RevisionRecord null $oldestRevertedRevision
Definition: RevertedTagUpdate.php:76
MediaWiki\Storage\RevertedTagUpdate\getChangeTags
getChangeTags(int $revisionId)
Protected function calling static ChangeTags class to allow for unit testing of this deferrable updat...
Definition: RevertedTagUpdate.php:322
MediaWiki\Revision\RevisionRecord
Page revision base class.
Definition: RevisionRecord.php:47
MediaWiki\Revision\RevisionStore
Service for looking up page revisions.
Definition: RevisionStore.php:88
MediaWiki\Storage\RevertedTagUpdate\getTagExtraParams
getTagExtraParams()
Returns additional data to be saved in ct_params field of table 'change_tag'.
Definition: RevertedTagUpdate.php:337
MediaWiki\Storage\RevertedTagUpdate\CONSTRUCTOR_OPTIONS
const CONSTRUCTOR_OPTIONS
Definition: RevertedTagUpdate.php:46
MediaWiki\Storage\RevertedTagUpdate\$revertRevision
RevisionRecord null $revertRevision
Definition: RevertedTagUpdate.php:70
MediaWiki\Storage\RevertedTagUpdate\handleSingleRevertedEdit
handleSingleRevertedEdit()
Handles the case where only one edit was reverted.
Definition: RevertedTagUpdate.php:254
MediaWiki\Storage\RevertedTagUpdate
Adds the mw-reverted tag to reverted edits after a revert is made.
Definition: RevertedTagUpdate.php:41
MediaWiki\Revision\RevisionRecord\DELETED_TEXT
const DELETED_TEXT
Definition: RevisionRecord.php:53
ChangeTags\getTags
static getTags(IDatabase $db, $rc_id=null, $rev_id=null, $log_id=null)
Return all the tags associated with the given recent change ID, revision ID, and/or log entry ID.
Definition: ChangeTags.php:588
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:96
FormatJson
JSON formatter wrapper class.
Definition: FormatJson.php:26
MediaWiki\Config\ServiceOptions
A class for passing options to services.
Definition: ServiceOptions.php:27
MediaWiki\Storage\RevertedTagUpdate\$revisionStore
RevisionStore $revisionStore
Definition: RevertedTagUpdate.php:49
ChangeTags
Definition: ChangeTags.php:32
MediaWiki\Storage\RevertedTagUpdate\$logger
LoggerInterface $logger
Definition: RevertedTagUpdate.php:52
MediaWiki\Storage\RevertedTagUpdate\getNewestRevertedRevision
getNewestRevertedRevision()
Returns the newest revision record that was reverted.
Definition: RevertedTagUpdate.php:363
DB_REPLICA
const DB_REPLICA
Definition: defines.php:25
MediaWiki\Storage\EditResult
Object for storing information about the effects of an edit.
Definition: EditResult.php:38
MediaWiki\Storage\RevertedTagUpdate\$editResult
EditResult $editResult
Definition: RevertedTagUpdate.php:67
MediaWiki\Storage\RevertedTagUpdate\markAsReverted
markAsReverted(int $revisionId, array $extraParams)
Protected function calling static ChangeTags class to allow for unit testing of this deferrable updat...
Definition: RevertedTagUpdate.php:299
MediaWiki\Storage\RevertedTagUpdate\getRevertRevision
getRevertRevision()
Returns the revision that performed the revert.
Definition: RevertedTagUpdate.php:349
MediaWiki\Storage\RevertedTagUpdate\shouldExecute
shouldExecute()
Performs checks to determine whether the update should execute.
Definition: RevertedTagUpdate.php:176
MediaWiki\Storage\RevertedTagUpdate\$newestRevertedRevision
RevisionRecord null $newestRevertedRevision
Definition: RevertedTagUpdate.php:73
MediaWiki\Storage
Definition: BlobAccessException.php:23
MediaWiki\Storage\RevertedTagUpdate\doUpdate
doUpdate()
Marks reverted edits with mw-reverted tag.
Definition: RevertedTagUpdate.php:111
MediaWiki\Storage\RevertedTagUpdate\$softwareTags
string[] $softwareTags
Definition: RevertedTagUpdate.php:55
MediaWiki\Revision\RevisionStore\INCLUDE_BOTH
const INCLUDE_BOTH
Definition: RevisionStore.php:100
MediaWiki\Storage\RevertedTagUpdate\getOldestRevertedRevision
getOldestRevertedRevision()
Returns the oldest revision record that was reverted.
Definition: RevertedTagUpdate.php:377
MediaWiki\Storage\RevertedTagUpdate\$revertId
int $revertId
Definition: RevertedTagUpdate.php:64
ChangeTags\TAG_REVERTED
const TAG_REVERTED
The tagged edit is reverted by a subsequent edit (which is tagged by one of TAG_ROLLBACK,...
Definition: ChangeTags.php:86
DeferrableUpdate
Interface that deferrable updates should implement.
Definition: DeferrableUpdate.php:11
MediaWiki\Storage\RevertedTagUpdate\$loadBalancer
ILoadBalancer $loadBalancer
Definition: RevertedTagUpdate.php:58
Wikimedia\Rdbms\ILoadBalancer
Database cluster connection, tracking, load balancing, and transaction manager interface.
Definition: ILoadBalancer.php:81
ChangeTags\addTags
static addTags( $tags, $rc_id=null, $rev_id=null, $log_id=null, $params=null, RecentChange $rc=null)
Add tags to a change given its rc_id, rev_id and/or log_id.
Definition: ChangeTags.php:328
MediaWiki\Config\ServiceOptions\assertRequiredOptions
assertRequiredOptions(array $expectedKeys)
Assert that the list of options provided in this instance exactly match $expectedKeys,...
Definition: ServiceOptions.php:71