MediaWiki REL1_34
MergeHistory.php
Go to the documentation of this file.
1<?php
2
24use Wikimedia\Timestamp\TimestampException;
26
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
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() {
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}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static matchSummarySpamRegex( $text)
Check given input text against $wgSummarySpamRegex, and return the text of the first match.
Library for creating and parsing MW-style timestamps.
Class for creating new log entries and inserting them into the database.
MediaWikiServices is the service locator for the application scope of MediaWiki.
Handles the backend logic of merging the histories of two pages.
getMergedRevisionCount()
Get the number of revisions that were moved Used in the SpecialMergeHistory success message.
Title $source
Page from which history will be merged.
MWTimestamp $maxTimestamp
Maximum timestamp that we can use (oldest timestamp of dest)
merge(User $user, $reason='')
Actually attempt the history move.
int $revisionsMerged
Number of revisions merged (for Special:MergeHistory success message)
Title $dest
Page to which history will be merged.
MWTimestamp bool $timestampLimit
Timestamp upto which history from the source will be merged.
IDatabase $dbw
Database that we are using.
getRevisionCount()
Get the number of revisions that will be moved.
string $timeWhere
SQL WHERE condition that selects source revisions to insert into destination.
const REVISION_LIMIT
Maximum number of revisions that can be merged at once.
__construct(Title $source, Title $dest, $timestamp=false)
isValidMerge()
Does various sanity checks that the merge is valid.
checkPermissions(User $user, $reason)
Check if the merge is possible.
static numParam( $num)
Definition Message.php:1038
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
Represents a title within MediaWiki.
Definition Title.php:42
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:51
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
const DB_MASTER
Definition defines.php:26