[ Index ] |
PHP Cross Reference of MediaWiki-1.24.0 |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Implements Special:MergeHistory 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup SpecialPage 22 */ 23 24 /** 25 * Special page allowing users with the appropriate permissions to 26 * merge article histories, with some restrictions 27 * 28 * @ingroup SpecialPage 29 */ 30 class SpecialMergeHistory extends SpecialPage { 31 /** @var string */ 32 protected $mAction; 33 34 /** @var string */ 35 protected $mTarget; 36 37 /** @var string */ 38 protected $mDest; 39 40 /** @var string */ 41 protected $mTimestamp; 42 43 /** @var int */ 44 protected $mTargetID; 45 46 /** @var int */ 47 protected $mDestID; 48 49 /** @var string */ 50 protected $mComment; 51 52 /** @var bool Was posted? */ 53 protected $mMerge; 54 55 /** @var bool Was submitted? */ 56 protected $mSubmitted; 57 58 /** @var Title */ 59 protected $mTargetObj; 60 61 /** @var Title */ 62 protected $mDestObj; 63 64 public function __construct() { 65 parent::__construct( 'MergeHistory', 'mergehistory' ); 66 } 67 68 /** 69 * @return void 70 */ 71 private function loadRequestParams() { 72 $request = $this->getRequest(); 73 $this->mAction = $request->getVal( 'action' ); 74 $this->mTarget = $request->getVal( 'target' ); 75 $this->mDest = $request->getVal( 'dest' ); 76 $this->mSubmitted = $request->getBool( 'submitted' ); 77 78 $this->mTargetID = intval( $request->getVal( 'targetID' ) ); 79 $this->mDestID = intval( $request->getVal( 'destID' ) ); 80 $this->mTimestamp = $request->getVal( 'mergepoint' ); 81 if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) { 82 $this->mTimestamp = ''; 83 } 84 $this->mComment = $request->getText( 'wpComment' ); 85 86 $this->mMerge = $request->wasPosted() 87 && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ); 88 89 // target page 90 if ( $this->mSubmitted ) { 91 $this->mTargetObj = Title::newFromURL( $this->mTarget ); 92 $this->mDestObj = Title::newFromURL( $this->mDest ); 93 } else { 94 $this->mTargetObj = null; 95 $this->mDestObj = null; 96 } 97 $this->preCacheMessages(); 98 } 99 100 /** 101 * As we use the same small set of messages in various methods and that 102 * they are called often, we call them once and save them in $this->message 103 */ 104 function preCacheMessages() { 105 // Precache various messages 106 if ( !isset( $this->message ) ) { 107 $this->message['last'] = $this->msg( 'last' )->escaped(); 108 } 109 } 110 111 public function execute( $par ) { 112 $this->checkPermissions(); 113 $this->checkReadOnly(); 114 115 $this->loadRequestParams(); 116 117 $this->setHeaders(); 118 $this->outputHeader(); 119 120 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) { 121 $this->merge(); 122 123 return; 124 } 125 126 if ( !$this->mSubmitted ) { 127 $this->showMergeForm(); 128 129 return; 130 } 131 132 $errors = array(); 133 if ( !$this->mTargetObj instanceof Title ) { 134 $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock(); 135 } elseif ( !$this->mTargetObj->exists() ) { 136 $errors[] = $this->msg( 'mergehistory-no-source', 137 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) 138 )->parseAsBlock(); 139 } 140 141 if ( !$this->mDestObj instanceof Title ) { 142 $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock(); 143 } elseif ( !$this->mDestObj->exists() ) { 144 $errors[] = $this->msg( 'mergehistory-no-destination', 145 wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) 146 )->parseAsBlock(); 147 } 148 149 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) { 150 $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock(); 151 } 152 153 if ( count( $errors ) ) { 154 $this->showMergeForm(); 155 $this->getOutput()->addHTML( implode( "\n", $errors ) ); 156 } else { 157 $this->showHistory(); 158 } 159 } 160 161 function showMergeForm() { 162 $this->getOutput()->addWikiMsg( 'mergehistory-header' ); 163 164 $this->getOutput()->addHTML( 165 Xml::openElement( 'form', array( 166 'method' => 'get', 167 'action' => wfScript() ) ) . 168 '<fieldset>' . 169 Xml::element( 'legend', array(), 170 $this->msg( 'mergehistory-box' )->text() ) . 171 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . 172 Html::hidden( 'submitted', '1' ) . 173 Html::hidden( 'mergepoint', $this->mTimestamp ) . 174 Xml::openElement( 'table' ) . 175 '<tr> 176 <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td> 177 <td>' . Xml::input( 'target', 30, $this->mTarget, array( 'id' => 'target' ) ) . '</td> 178 </tr><tr> 179 <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td> 180 <td>' . Xml::input( 'dest', 30, $this->mDest, array( 'id' => 'dest' ) ) . '</td> 181 </tr><tr><td>' . 182 Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) . 183 '</td></tr>' . 184 Xml::closeElement( 'table' ) . 185 '</fieldset>' . 186 '</form>' 187 ); 188 } 189 190 private function showHistory() { 191 $this->showMergeForm(); 192 193 # List all stored revisions 194 $revisions = new MergeHistoryPager( 195 $this, array(), $this->mTargetObj, $this->mDestObj 196 ); 197 $haveRevisions = $revisions && $revisions->getNumRows() > 0; 198 199 $out = $this->getOutput(); 200 $titleObj = $this->getPageTitle(); 201 $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) ); 202 # Start the form here 203 $top = Xml::openElement( 204 'form', 205 array( 206 'method' => 'post', 207 'action' => $action, 208 'id' => 'merge' 209 ) 210 ); 211 $out->addHTML( $top ); 212 213 if ( $haveRevisions ) { 214 # Format the user-visible controls (comment field, submission button) 215 # in a nice little table 216 $table = 217 Xml::openElement( 'fieldset' ) . 218 $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(), 219 $this->mDestObj->getPrefixedText() )->parse() . 220 Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) . 221 '<tr> 222 <td class="mw-label">' . 223 Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) . 224 '</td> 225 <td class="mw-input">' . 226 Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) . 227 '</td> 228 </tr> 229 <tr> 230 <td> </td> 231 <td class="mw-submit">' . 232 Xml::submitButton( 233 $this->msg( 'mergehistory-submit' )->text(), 234 array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) 235 ) . 236 '</td> 237 </tr>' . 238 Xml::closeElement( 'table' ) . 239 Xml::closeElement( 'fieldset' ); 240 241 $out->addHTML( $table ); 242 } 243 244 $out->addHTML( 245 '<h2 id="mw-mergehistory">' . 246 $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n" 247 ); 248 249 if ( $haveRevisions ) { 250 $out->addHTML( $revisions->getNavigationBar() ); 251 $out->addHTML( '<ul>' ); 252 $out->addHTML( $revisions->getBody() ); 253 $out->addHTML( '</ul>' ); 254 $out->addHTML( $revisions->getNavigationBar() ); 255 } else { 256 $out->addWikiMsg( 'mergehistory-empty' ); 257 } 258 259 # Show relevant lines from the merge log: 260 $mergeLogPage = new LogPage( 'merge' ); 261 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" ); 262 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj ); 263 264 # When we submit, go by page ID to avoid some nasty but unlikely collisions. 265 # Such would happen if a page was renamed after the form loaded, but before submit 266 $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() ); 267 $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() ); 268 $misc .= Html::hidden( 'target', $this->mTarget ); 269 $misc .= Html::hidden( 'dest', $this->mDest ); 270 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); 271 $misc .= Xml::closeElement( 'form' ); 272 $out->addHTML( $misc ); 273 274 return true; 275 } 276 277 function formatRevisionRow( $row ) { 278 $rev = new Revision( $row ); 279 280 $stxt = ''; 281 $last = $this->message['last']; 282 283 $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); 284 $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) ); 285 286 $user = $this->getUser(); 287 288 $pageLink = Linker::linkKnown( 289 $rev->getTitle(), 290 htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ), 291 array(), 292 array( 'oldid' => $rev->getId() ) 293 ); 294 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 295 $pageLink = '<span class="history-deleted">' . $pageLink . '</span>'; 296 } 297 298 # Last link 299 if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { 300 $last = $this->message['last']; 301 } elseif ( isset( $this->prevId[$row->rev_id] ) ) { 302 $last = Linker::linkKnown( 303 $rev->getTitle(), 304 $this->message['last'], 305 array(), 306 array( 307 'diff' => $row->rev_id, 308 'oldid' => $this->prevId[$row->rev_id] 309 ) 310 ); 311 } 312 313 $userLink = Linker::revUserTools( $rev ); 314 315 $size = $row->rev_len; 316 if ( !is_null( $size ) ) { 317 $stxt = Linker::formatRevisionSize( $size ); 318 } 319 $comment = Linker::revComment( $rev ); 320 321 return Html::rawElement( 'li', array(), 322 $this->msg( 'mergehistory-revisionrow' ) 323 ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); 324 } 325 326 /** 327 * Actually attempt the history move 328 * 329 * @todo if all versions of page A are moved to B and then a user 330 * tries to do a reverse-merge via the "unmerge" log link, then page 331 * A will still be a redirect (as it was after the original merge), 332 * though it will have the old revisions back from before (as expected). 333 * The user may have to "undo" the redirect manually to finish the "unmerge". 334 * Maybe this should delete redirects at the target page of merges? 335 * 336 * @return bool Success 337 */ 338 function merge() { 339 # Get the titles directly from the IDs, in case the target page params 340 # were spoofed. The queries are done based on the IDs, so it's best to 341 # keep it consistent... 342 $targetTitle = Title::newFromID( $this->mTargetID ); 343 $destTitle = Title::newFromID( $this->mDestID ); 344 if ( is_null( $targetTitle ) || is_null( $destTitle ) ) { 345 return false; // validate these 346 } 347 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) { 348 return false; 349 } 350 # Verify that this timestamp is valid 351 # Must be older than the destination page 352 $dbw = wfGetDB( DB_MASTER ); 353 # Get timestamp into DB format 354 $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp( $this->mTimestamp ) : ''; 355 # Max timestamp should be min of destination page 356 $maxtimestamp = $dbw->selectField( 357 'revision', 358 'MIN(rev_timestamp)', 359 array( 'rev_page' => $this->mDestID ), 360 __METHOD__ 361 ); 362 # Destination page must exist with revisions 363 if ( !$maxtimestamp ) { 364 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 365 366 return false; 367 } 368 # Get the latest timestamp of the source 369 $lasttimestamp = $dbw->selectField( 370 array( 'page', 'revision' ), 371 'rev_timestamp', 372 array( 'page_id' => $this->mTargetID, 'page_latest = rev_id' ), 373 __METHOD__ 374 ); 375 # $this->mTimestamp must be older than $maxtimestamp 376 if ( $this->mTimestamp >= $maxtimestamp ) { 377 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 378 379 return false; 380 } 381 # Get the timestamp pivot condition 382 if ( $this->mTimestamp ) { 383 $timewhere = "rev_timestamp <= {$this->mTimestamp}"; 384 $timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp ); 385 } else { 386 $timewhere = "rev_timestamp <= {$maxtimestamp}"; 387 $timestampLimit = wfTimestamp( TS_MW, $lasttimestamp ); 388 } 389 # Check that there are not too many revisions to move 390 $limit = 5000; // avoid too much slave lag 391 $count = $dbw->selectRowCount( 'revision', '1', 392 array( 'rev_page' => $this->mTargetID, $timewhere ), 393 __METHOD__, 394 array( 'LIMIT' => $limit + 1 ) 395 ); 396 if ( $count > $limit ) { 397 $this->getOutput()->addWikiMsg( 'mergehistory-fail-toobig' ); 398 399 return false; 400 } 401 # Do the moving... 402 $dbw->update( 403 'revision', 404 array( 'rev_page' => $this->mDestID ), 405 array( 'rev_page' => $this->mTargetID, $timewhere ), 406 __METHOD__ 407 ); 408 409 $count = $dbw->affectedRows(); 410 # Make the source page a redirect if no revisions are left 411 $haveRevisions = $dbw->selectField( 412 'revision', 413 'rev_timestamp', 414 array( 'rev_page' => $this->mTargetID ), 415 __METHOD__, 416 array( 'FOR UPDATE' ) 417 ); 418 if ( !$haveRevisions ) { 419 if ( $this->mComment ) { 420 $comment = $this->msg( 421 'mergehistory-comment', 422 $targetTitle->getPrefixedText(), 423 $destTitle->getPrefixedText(), 424 $this->mComment 425 )->inContentLanguage()->text(); 426 } else { 427 $comment = $this->msg( 428 'mergehistory-autocomment', 429 $targetTitle->getPrefixedText(), 430 $destTitle->getPrefixedText() 431 )->inContentLanguage()->text(); 432 } 433 434 $contentHandler = ContentHandler::getForTitle( $targetTitle ); 435 $redirectContent = $contentHandler->makeRedirectContent( $destTitle ); 436 437 if ( $redirectContent ) { 438 $redirectPage = WikiPage::factory( $targetTitle ); 439 $redirectRevision = new Revision( array( 440 'title' => $targetTitle, 441 'page' => $this->mTargetID, 442 'comment' => $comment, 443 'content' => $redirectContent ) ); 444 $redirectRevision->insertOn( $dbw ); 445 $redirectPage->updateRevisionOn( $dbw, $redirectRevision ); 446 447 # Now, we record the link from the redirect to the new title. 448 # It should have no other outgoing links... 449 $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ ); 450 $dbw->insert( 'pagelinks', 451 array( 452 'pl_from' => $this->mDestID, 453 'pl_from_namespace' => $destTitle->getNamespace(), 454 'pl_namespace' => $destTitle->getNamespace(), 455 'pl_title' => $destTitle->getDBkey() ), 456 __METHOD__ 457 ); 458 } else { 459 // would be nice to show a warning if we couldn't create a redirect 460 } 461 } else { 462 $targetTitle->invalidateCache(); // update histories 463 } 464 $destTitle->invalidateCache(); // update histories 465 # Check if this did anything 466 if ( !$count ) { 467 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 468 469 return false; 470 } 471 # Update our logs 472 $log = new LogPage( 'merge' ); 473 $log->addEntry( 474 'merge', $targetTitle, $this->mComment, 475 array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser() 476 ); 477 478 # @todo message should use redirect=no 479 $this->getOutput()->addWikiText( $this->msg( 'mergehistory-success', 480 $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() )->numParams( 481 $count )->text() ); 482 483 wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); 484 485 return true; 486 } 487 488 protected function getGroupName() { 489 return 'pagetools'; 490 } 491 } 492 493 class MergeHistoryPager extends ReverseChronologicalPager { 494 /** @var IContextSource */ 495 public $mForm; 496 497 /** @var array */ 498 public $mConds; 499 500 function __construct( $form, $conds, $source, $dest ) { 501 $this->mForm = $form; 502 $this->mConds = $conds; 503 $this->title = $source; 504 $this->articleID = $source->getArticleID(); 505 506 $dbr = wfGetDB( DB_SLAVE ); 507 $maxtimestamp = $dbr->selectField( 508 'revision', 509 'MIN(rev_timestamp)', 510 array( 'rev_page' => $dest->getArticleID() ), 511 __METHOD__ 512 ); 513 $this->maxTimestamp = $maxtimestamp; 514 515 parent::__construct( $form->getContext() ); 516 } 517 518 function getStartBody() { 519 wfProfileIn( __METHOD__ ); 520 # Do a link batch query 521 $this->mResult->seek( 0 ); 522 $batch = new LinkBatch(); 523 # Give some pointers to make (last) links 524 $this->mForm->prevId = array(); 525 foreach ( $this->mResult as $row ) { 526 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); 527 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); 528 529 $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id; 530 if ( $rev_id > $row->rev_id ) { 531 $this->mForm->prevId[$rev_id] = $row->rev_id; 532 } elseif ( $rev_id < $row->rev_id ) { 533 $this->mForm->prevId[$row->rev_id] = $rev_id; 534 } 535 536 $rev_id = $row->rev_id; 537 } 538 539 $batch->execute(); 540 $this->mResult->seek( 0 ); 541 542 wfProfileOut( __METHOD__ ); 543 544 return ''; 545 } 546 547 function formatRow( $row ) { 548 return $this->mForm->formatRevisionRow( $row ); 549 } 550 551 function getQueryInfo() { 552 $conds = $this->mConds; 553 $conds['rev_page'] = $this->articleID; 554 $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp ); 555 556 return array( 557 'tables' => array( 'revision', 'page', 'user' ), 558 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), 559 'conds' => $conds, 560 'join_conds' => array( 561 'page' => Revision::pageJoinCond(), 562 'user' => Revision::userJoinCond() ) 563 ); 564 } 565 566 function getIndexField() { 567 return 'rev_timestamp'; 568 } 569 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Fri Nov 28 14:03:12 2014 | Cross-referenced by PHPXref 0.7.1 |