MediaWiki  master
MergeHistory.php
Go to the documentation of this file.
1 <?php
2 
24 use Wikimedia\Timestamp\TimestampException;
26 
33 class MergeHistory {
34 
36  const REVISION_LIMIT = 5000;
37 
39  protected $source;
40 
42  protected $dest;
43 
45  protected $dbw;
46 
48  protected $maxTimestamp;
49 
51  protected $timeWhere;
52 
54  protected $timestampLimit;
55 
57  protected $revisionsMerged;
58 
64  public function __construct( Title $source, Title $dest, $timestamp = false ) {
65  // Save the parameters
66  $this->source = $source;
67  $this->dest = $dest;
68 
69  // Get the database
70  $this->dbw = wfGetDB( DB_MASTER );
71 
72  // Max timestamp should be min of destination page
73  $firstDestTimestamp = $this->dbw->selectField(
74  'revision',
75  'MIN(rev_timestamp)',
76  [ 'rev_page' => $this->dest->getArticleID() ],
77  __METHOD__
78  );
79  $this->maxTimestamp = new MWTimestamp( $firstDestTimestamp );
80 
81  // Get the timestamp pivot condition
82  try {
83  if ( $timestamp ) {
84  // If we have a requested timestamp, use the
85  // latest revision up to that point as the insertion point
86  $mwTimestamp = new MWTimestamp( $timestamp );
87  $lastWorkingTimestamp = $this->dbw->selectField(
88  'revision',
89  'MAX(rev_timestamp)',
90  [
91  'rev_timestamp <= ' .
92  $this->dbw->addQuotes( $this->dbw->timestamp( $mwTimestamp ) ),
93  'rev_page' => $this->source->getArticleID()
94  ],
95  __METHOD__
96  );
97  $mwLastWorkingTimestamp = new MWTimestamp( $lastWorkingTimestamp );
98 
99  $timeInsert = $mwLastWorkingTimestamp;
100  $this->timestampLimit = $mwLastWorkingTimestamp;
101  } else {
102  // If we don't, merge entire source page history into the
103  // beginning of destination page history
104 
105  // Get the latest timestamp of the source
106  $lastSourceTimestamp = $this->dbw->selectField(
107  [ 'page', 'revision' ],
108  'rev_timestamp',
109  [ 'page_id' => $this->source->getArticleID(),
110  'page_latest = rev_id'
111  ],
112  __METHOD__
113  );
114  $lasttimestamp = new MWTimestamp( $lastSourceTimestamp );
115 
116  $timeInsert = $this->maxTimestamp;
117  $this->timestampLimit = $lasttimestamp;
118  }
119 
120  $this->timeWhere = "rev_timestamp <= " .
121  $this->dbw->addQuotes( $this->dbw->timestamp( $timeInsert ) );
122  } catch ( TimestampException $ex ) {
123  // The timestamp we got is screwed up and merge cannot continue
124  // This should be detected by $this->isValidMerge()
125  $this->timestampLimit = false;
126  }
127  }
128 
133  public function getRevisionCount() {
134  $count = $this->dbw->selectRowCount( 'revision', '1',
135  [ 'rev_page' => $this->source->getArticleID(), $this->timeWhere ],
136  __METHOD__,
137  [ 'LIMIT' => self::REVISION_LIMIT + 1 ]
138  );
139 
140  return $count;
141  }
142 
148  public function getMergedRevisionCount() {
149  return $this->revisionsMerged;
150  }
151 
158  public function checkPermissions( User $user, $reason ) {
159  $status = new Status();
160 
161  // Check if user can edit both pages
162  $errors = wfMergeErrorArrays(
163  $this->source->getUserPermissionsErrors( 'edit', $user ),
164  $this->dest->getUserPermissionsErrors( 'edit', $user )
165  );
166 
167  // Convert into a Status object
168  if ( $errors ) {
169  foreach ( $errors as $error ) {
170  $status->fatal( ...$error );
171  }
172  }
173 
174  // Anti-spam
175  if ( EditPage::matchSummarySpamRegex( $reason ) !== false ) {
176  // This is kind of lame, won't display nice
177  $status->fatal( 'spamprotectiontext' );
178  }
179 
180  // Check mergehistory permission
181  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
182  if ( !$permissionManager->userHasRight( $user, 'mergehistory' ) ) {
183  // User doesn't have the right to merge histories
184  $status->fatal( 'mergehistory-fail-permission' );
185  }
186 
187  return $status;
188  }
189 
197  public function isValidMerge() {
198  $status = new Status();
199 
200  // If either article ID is 0, then revisions cannot be reliably selected
201  if ( $this->source->getArticleID() === 0 ) {
202  $status->fatal( 'mergehistory-fail-invalid-source' );
203  }
204  if ( $this->dest->getArticleID() === 0 ) {
205  $status->fatal( 'mergehistory-fail-invalid-dest' );
206  }
207 
208  // Make sure page aren't the same
209  if ( $this->source->equals( $this->dest ) ) {
210  $status->fatal( 'mergehistory-fail-self-merge' );
211  }
212 
213  // Make sure the timestamp is valid
214  if ( !$this->timestampLimit ) {
215  $status->fatal( 'mergehistory-fail-bad-timestamp' );
216  }
217 
218  // $this->timestampLimit must be older than $this->maxTimestamp
219  if ( $this->timestampLimit > $this->maxTimestamp ) {
220  $status->fatal( 'mergehistory-fail-timestamps-overlap' );
221  }
222 
223  // Check that there are not too many revisions to move
224  if ( $this->timestampLimit && $this->getRevisionCount() > self::REVISION_LIMIT ) {
225  $status->fatal( 'mergehistory-fail-toobig', Message::numParam( self::REVISION_LIMIT ) );
226  }
227 
228  return $status;
229  }
230 
245  public function merge( User $user, $reason = '' ) {
246  $status = new Status();
247 
248  // Check validity and permissions required for merge
249  $validCheck = $this->isValidMerge(); // Check this first to check for null pages
250  if ( !$validCheck->isOK() ) {
251  return $validCheck;
252  }
253  $permCheck = $this->checkPermissions( $user, $reason );
254  if ( !$permCheck->isOK() ) {
255  return $permCheck;
256  }
257 
258  $this->dbw->startAtomic( __METHOD__ );
259 
260  $this->dbw->update(
261  'revision',
262  [ 'rev_page' => $this->dest->getArticleID() ],
263  [ 'rev_page' => $this->source->getArticleID(), $this->timeWhere ],
264  __METHOD__
265  );
266 
267  // Check if this did anything
268  $this->revisionsMerged = $this->dbw->affectedRows();
269  if ( $this->revisionsMerged < 1 ) {
270  $this->dbw->endAtomic( __METHOD__ );
271  $status->fatal( 'mergehistory-fail-no-change' );
272 
273  return $status;
274  }
275 
276  // Update denormalized revactor_page too
277  $this->dbw->update(
278  'revision_actor_temp',
279  [ 'revactor_page' => $this->dest->getArticleID() ],
280  [
281  'revactor_page' => $this->source->getArticleID(),
282  // Slightly hacky, but should work given the values assigned in this class
283  str_replace( 'rev_timestamp', 'revactor_timestamp', $this->timeWhere )
284  ],
285  __METHOD__
286  );
287 
288  // Make the source page a redirect if no revisions are left
289  $haveRevisions = $this->dbw->lockForUpdate(
290  'revision',
291  [ 'rev_page' => $this->source->getArticleID() ],
292  __METHOD__
293  );
294  if ( !$haveRevisions ) {
295  if ( $reason ) {
296  $reason = wfMessage(
297  'mergehistory-comment',
298  $this->source->getPrefixedText(),
299  $this->dest->getPrefixedText(),
300  $reason
301  )->inContentLanguage()->text();
302  } else {
303  $reason = wfMessage(
304  'mergehistory-autocomment',
305  $this->source->getPrefixedText(),
306  $this->dest->getPrefixedText()
307  )->inContentLanguage()->text();
308  }
309 
310  $contentHandler = ContentHandler::getForTitle( $this->source );
311  $redirectContent = $contentHandler->makeRedirectContent(
312  $this->dest,
313  wfMessage( 'mergehistory-redirect-text' )->inContentLanguage()->plain()
314  );
315 
316  if ( $redirectContent ) {
317  $redirectPage = WikiPage::factory( $this->source );
318  $redirectRevision = new Revision( [
319  'title' => $this->source,
320  'page' => $this->source->getArticleID(),
321  'comment' => $reason,
322  'content' => $redirectContent ] );
323  $redirectRevision->insertOn( $this->dbw );
324  $redirectPage->updateRevisionOn( $this->dbw, $redirectRevision );
325 
326  // Now, we record the link from the redirect to the new title.
327  // It should have no other outgoing links...
328  $this->dbw->delete(
329  'pagelinks',
330  [ 'pl_from' => $this->dest->getArticleID() ],
331  __METHOD__
332  );
333  $this->dbw->insert( 'pagelinks',
334  [
335  'pl_from' => $this->dest->getArticleID(),
336  'pl_from_namespace' => $this->dest->getNamespace(),
337  'pl_namespace' => $this->dest->getNamespace(),
338  'pl_title' => $this->dest->getDBkey() ],
339  __METHOD__
340  );
341  } else {
342  // Warning if we couldn't create the redirect
343  $status->warning( 'mergehistory-warning-redirect-not-created' );
344  }
345  } else {
346  $this->source->invalidateCache(); // update histories
347  }
348  $this->dest->invalidateCache(); // update histories
349 
350  // Duplicate watchers of the old article to the new article on history merge
351  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
352  $store->duplicateAllAssociatedEntries( $this->source, $this->dest );
353 
354  // Update our logs
355  $logEntry = new ManualLogEntry( 'merge', 'merge' );
356  $logEntry->setPerformer( $user );
357  $logEntry->setComment( $reason );
358  $logEntry->setTarget( $this->source );
359  $logEntry->setParameters( [
360  '4::dest' => $this->dest->getPrefixedText(),
361  '5::mergepoint' => $this->timestampLimit->getTimestamp( TS_MW )
362  ] );
363  $logId = $logEntry->insert();
364  $logEntry->publish( $logId );
365 
366  Hooks::run( 'ArticleMergeComplete', [ $this->source, $this->dest ] );
367 
368  $this->dbw->endAtomic( __METHOD__ );
369 
370  return $status;
371  }
372 }
MWTimestamp
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:32
wfMergeErrorArrays
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
Definition: GlobalFunctions.php:181
MergeHistory\checkPermissions
checkPermissions(User $user, $reason)
Check if the merge is possible.
Definition: MergeHistory.php:158
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:129
MergeHistory\$revisionsMerged
int $revisionsMerged
Number of revisions merged (for Special:MergeHistory success message)
Definition: MergeHistory.php:57
MergeHistory\$dbw
IDatabase $dbw
Database that we are using.
Definition: MergeHistory.php:45
MergeHistory\getMergedRevisionCount
getMergedRevisionCount()
Get the number of revisions that were moved Used in the SpecialMergeHistory success message.
Definition: MergeHistory.php:148
wfMessage
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
Definition: GlobalFunctions.php:1263
ContentHandler\getForTitle
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
Definition: ContentHandler.php:201
Revision\insertOn
insertOn( $dbw)
Insert a new revision into the database, returning the new revision ID number on success and dies hor...
Definition: Revision.php:948
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Status
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:40
Revision
Definition: Revision.php:40
WikiPage\factory
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:140
wfGetDB
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
Definition: GlobalFunctions.php:2579
MergeHistory\$timestampLimit
MWTimestamp bool $timestampLimit
Timestamp upto which history from the source will be merged.
Definition: MergeHistory.php:54
DB_MASTER
const DB_MASTER
Definition: defines.php:26
MergeHistory\$maxTimestamp
MWTimestamp $maxTimestamp
Maximum timestamp that we can use (oldest timestamp of dest)
Definition: MergeHistory.php:48
EditPage\matchSummarySpamRegex
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
Definition: EditPage.php:2520
MergeHistory\REVISION_LIMIT
const REVISION_LIMIT
Maximum number of revisions that can be merged at once.
Definition: MergeHistory.php:36
MergeHistory\merge
merge(User $user, $reason='')
Actually attempt the history move.
Definition: MergeHistory.php:245
MergeHistory\$timeWhere
string $timeWhere
SQL WHERE condition that selects source revisions to insert into destination.
Definition: MergeHistory.php:51
MergeHistory
Handles the backend logic of merging the histories of two pages.
Definition: MergeHistory.php:33
MergeHistory\$source
Title $source
Page from which history will be merged.
Definition: MergeHistory.php:39
MergeHistory\isValidMerge
isValidMerge()
Does various sanity checks that the merge is valid.
Definition: MergeHistory.php:197
Title
Represents a title within MediaWiki.
Definition: Title.php:42
MergeHistory\__construct
__construct(Title $source, Title $dest, $timestamp=false)
Definition: MergeHistory.php:64
MergeHistory\$dest
Title $dest
Page to which history will be merged.
Definition: MergeHistory.php:42
ManualLogEntry
Class for creating new log entries and inserting them into the database.
Definition: ManualLogEntry.php:37
User
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:51
Hooks\run
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
MergeHistory\getRevisionCount
getRevisionCount()
Get the number of revisions that will be moved.
Definition: MergeHistory.php:133