MediaWiki
REL1_24
|
00001 <?php 00030 class SpecialMergeHistory extends SpecialPage { 00032 protected $mAction; 00033 00035 protected $mTarget; 00036 00038 protected $mDest; 00039 00041 protected $mTimestamp; 00042 00044 protected $mTargetID; 00045 00047 protected $mDestID; 00048 00050 protected $mComment; 00051 00053 protected $mMerge; 00054 00056 protected $mSubmitted; 00057 00059 protected $mTargetObj; 00060 00062 protected $mDestObj; 00063 00064 public function __construct() { 00065 parent::__construct( 'MergeHistory', 'mergehistory' ); 00066 } 00067 00071 private function loadRequestParams() { 00072 $request = $this->getRequest(); 00073 $this->mAction = $request->getVal( 'action' ); 00074 $this->mTarget = $request->getVal( 'target' ); 00075 $this->mDest = $request->getVal( 'dest' ); 00076 $this->mSubmitted = $request->getBool( 'submitted' ); 00077 00078 $this->mTargetID = intval( $request->getVal( 'targetID' ) ); 00079 $this->mDestID = intval( $request->getVal( 'destID' ) ); 00080 $this->mTimestamp = $request->getVal( 'mergepoint' ); 00081 if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) { 00082 $this->mTimestamp = ''; 00083 } 00084 $this->mComment = $request->getText( 'wpComment' ); 00085 00086 $this->mMerge = $request->wasPosted() 00087 && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) ); 00088 00089 // target page 00090 if ( $this->mSubmitted ) { 00091 $this->mTargetObj = Title::newFromURL( $this->mTarget ); 00092 $this->mDestObj = Title::newFromURL( $this->mDest ); 00093 } else { 00094 $this->mTargetObj = null; 00095 $this->mDestObj = null; 00096 } 00097 $this->preCacheMessages(); 00098 } 00099 00104 function preCacheMessages() { 00105 // Precache various messages 00106 if ( !isset( $this->message ) ) { 00107 $this->message['last'] = $this->msg( 'last' )->escaped(); 00108 } 00109 } 00110 00111 public function execute( $par ) { 00112 $this->checkPermissions(); 00113 $this->checkReadOnly(); 00114 00115 $this->loadRequestParams(); 00116 00117 $this->setHeaders(); 00118 $this->outputHeader(); 00119 00120 if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) { 00121 $this->merge(); 00122 00123 return; 00124 } 00125 00126 if ( !$this->mSubmitted ) { 00127 $this->showMergeForm(); 00128 00129 return; 00130 } 00131 00132 $errors = array(); 00133 if ( !$this->mTargetObj instanceof Title ) { 00134 $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock(); 00135 } elseif ( !$this->mTargetObj->exists() ) { 00136 $errors[] = $this->msg( 'mergehistory-no-source', 00137 wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) 00138 )->parseAsBlock(); 00139 } 00140 00141 if ( !$this->mDestObj instanceof Title ) { 00142 $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock(); 00143 } elseif ( !$this->mDestObj->exists() ) { 00144 $errors[] = $this->msg( 'mergehistory-no-destination', 00145 wfEscapeWikiText( $this->mDestObj->getPrefixedText() ) 00146 )->parseAsBlock(); 00147 } 00148 00149 if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) { 00150 $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock(); 00151 } 00152 00153 if ( count( $errors ) ) { 00154 $this->showMergeForm(); 00155 $this->getOutput()->addHTML( implode( "\n", $errors ) ); 00156 } else { 00157 $this->showHistory(); 00158 } 00159 } 00160 00161 function showMergeForm() { 00162 $this->getOutput()->addWikiMsg( 'mergehistory-header' ); 00163 00164 $this->getOutput()->addHTML( 00165 Xml::openElement( 'form', array( 00166 'method' => 'get', 00167 'action' => wfScript() ) ) . 00168 '<fieldset>' . 00169 Xml::element( 'legend', array(), 00170 $this->msg( 'mergehistory-box' )->text() ) . 00171 Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) . 00172 Html::hidden( 'submitted', '1' ) . 00173 Html::hidden( 'mergepoint', $this->mTimestamp ) . 00174 Xml::openElement( 'table' ) . 00175 '<tr> 00176 <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td> 00177 <td>' . Xml::input( 'target', 30, $this->mTarget, array( 'id' => 'target' ) ) . '</td> 00178 </tr><tr> 00179 <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td> 00180 <td>' . Xml::input( 'dest', 30, $this->mDest, array( 'id' => 'dest' ) ) . '</td> 00181 </tr><tr><td>' . 00182 Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) . 00183 '</td></tr>' . 00184 Xml::closeElement( 'table' ) . 00185 '</fieldset>' . 00186 '</form>' 00187 ); 00188 } 00189 00190 private function showHistory() { 00191 $this->showMergeForm(); 00192 00193 # List all stored revisions 00194 $revisions = new MergeHistoryPager( 00195 $this, array(), $this->mTargetObj, $this->mDestObj 00196 ); 00197 $haveRevisions = $revisions && $revisions->getNumRows() > 0; 00198 00199 $out = $this->getOutput(); 00200 $titleObj = $this->getPageTitle(); 00201 $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) ); 00202 # Start the form here 00203 $top = Xml::openElement( 00204 'form', 00205 array( 00206 'method' => 'post', 00207 'action' => $action, 00208 'id' => 'merge' 00209 ) 00210 ); 00211 $out->addHTML( $top ); 00212 00213 if ( $haveRevisions ) { 00214 # Format the user-visible controls (comment field, submission button) 00215 # in a nice little table 00216 $table = 00217 Xml::openElement( 'fieldset' ) . 00218 $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(), 00219 $this->mDestObj->getPrefixedText() )->parse() . 00220 Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) . 00221 '<tr> 00222 <td class="mw-label">' . 00223 Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) . 00224 '</td> 00225 <td class="mw-input">' . 00226 Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) . 00227 '</td> 00228 </tr> 00229 <tr> 00230 <td> </td> 00231 <td class="mw-submit">' . 00232 Xml::submitButton( 00233 $this->msg( 'mergehistory-submit' )->text(), 00234 array( 'name' => 'merge', 'id' => 'mw-merge-submit' ) 00235 ) . 00236 '</td> 00237 </tr>' . 00238 Xml::closeElement( 'table' ) . 00239 Xml::closeElement( 'fieldset' ); 00240 00241 $out->addHTML( $table ); 00242 } 00243 00244 $out->addHTML( 00245 '<h2 id="mw-mergehistory">' . 00246 $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n" 00247 ); 00248 00249 if ( $haveRevisions ) { 00250 $out->addHTML( $revisions->getNavigationBar() ); 00251 $out->addHTML( '<ul>' ); 00252 $out->addHTML( $revisions->getBody() ); 00253 $out->addHTML( '</ul>' ); 00254 $out->addHTML( $revisions->getNavigationBar() ); 00255 } else { 00256 $out->addWikiMsg( 'mergehistory-empty' ); 00257 } 00258 00259 # Show relevant lines from the merge log: 00260 $mergeLogPage = new LogPage( 'merge' ); 00261 $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" ); 00262 LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj ); 00263 00264 # When we submit, go by page ID to avoid some nasty but unlikely collisions. 00265 # Such would happen if a page was renamed after the form loaded, but before submit 00266 $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() ); 00267 $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() ); 00268 $misc .= Html::hidden( 'target', $this->mTarget ); 00269 $misc .= Html::hidden( 'dest', $this->mDest ); 00270 $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() ); 00271 $misc .= Xml::closeElement( 'form' ); 00272 $out->addHTML( $misc ); 00273 00274 return true; 00275 } 00276 00277 function formatRevisionRow( $row ) { 00278 $rev = new Revision( $row ); 00279 00280 $stxt = ''; 00281 $last = $this->message['last']; 00282 00283 $ts = wfTimestamp( TS_MW, $row->rev_timestamp ); 00284 $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) ); 00285 00286 $user = $this->getUser(); 00287 00288 $pageLink = Linker::linkKnown( 00289 $rev->getTitle(), 00290 htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ), 00291 array(), 00292 array( 'oldid' => $rev->getId() ) 00293 ); 00294 if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) { 00295 $pageLink = '<span class="history-deleted">' . $pageLink . '</span>'; 00296 } 00297 00298 # Last link 00299 if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) { 00300 $last = $this->message['last']; 00301 } elseif ( isset( $this->prevId[$row->rev_id] ) ) { 00302 $last = Linker::linkKnown( 00303 $rev->getTitle(), 00304 $this->message['last'], 00305 array(), 00306 array( 00307 'diff' => $row->rev_id, 00308 'oldid' => $this->prevId[$row->rev_id] 00309 ) 00310 ); 00311 } 00312 00313 $userLink = Linker::revUserTools( $rev ); 00314 00315 $size = $row->rev_len; 00316 if ( !is_null( $size ) ) { 00317 $stxt = Linker::formatRevisionSize( $size ); 00318 } 00319 $comment = Linker::revComment( $rev ); 00320 00321 return Html::rawElement( 'li', array(), 00322 $this->msg( 'mergehistory-revisionrow' ) 00323 ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() ); 00324 } 00325 00338 function merge() { 00339 # Get the titles directly from the IDs, in case the target page params 00340 # were spoofed. The queries are done based on the IDs, so it's best to 00341 # keep it consistent... 00342 $targetTitle = Title::newFromID( $this->mTargetID ); 00343 $destTitle = Title::newFromID( $this->mDestID ); 00344 if ( is_null( $targetTitle ) || is_null( $destTitle ) ) { 00345 return false; // validate these 00346 } 00347 if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) { 00348 return false; 00349 } 00350 # Verify that this timestamp is valid 00351 # Must be older than the destination page 00352 $dbw = wfGetDB( DB_MASTER ); 00353 # Get timestamp into DB format 00354 $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp( $this->mTimestamp ) : ''; 00355 # Max timestamp should be min of destination page 00356 $maxtimestamp = $dbw->selectField( 00357 'revision', 00358 'MIN(rev_timestamp)', 00359 array( 'rev_page' => $this->mDestID ), 00360 __METHOD__ 00361 ); 00362 # Destination page must exist with revisions 00363 if ( !$maxtimestamp ) { 00364 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 00365 00366 return false; 00367 } 00368 # Get the latest timestamp of the source 00369 $lasttimestamp = $dbw->selectField( 00370 array( 'page', 'revision' ), 00371 'rev_timestamp', 00372 array( 'page_id' => $this->mTargetID, 'page_latest = rev_id' ), 00373 __METHOD__ 00374 ); 00375 # $this->mTimestamp must be older than $maxtimestamp 00376 if ( $this->mTimestamp >= $maxtimestamp ) { 00377 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 00378 00379 return false; 00380 } 00381 # Get the timestamp pivot condition 00382 if ( $this->mTimestamp ) { 00383 $timewhere = "rev_timestamp <= {$this->mTimestamp}"; 00384 $timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp ); 00385 } else { 00386 $timewhere = "rev_timestamp <= {$maxtimestamp}"; 00387 $timestampLimit = wfTimestamp( TS_MW, $lasttimestamp ); 00388 } 00389 # Check that there are not too many revisions to move 00390 $limit = 5000; // avoid too much slave lag 00391 $count = $dbw->selectRowCount( 'revision', '1', 00392 array( 'rev_page' => $this->mTargetID, $timewhere ), 00393 __METHOD__, 00394 array( 'LIMIT' => $limit + 1 ) 00395 ); 00396 if ( $count > $limit ) { 00397 $this->getOutput()->addWikiMsg( 'mergehistory-fail-toobig' ); 00398 00399 return false; 00400 } 00401 # Do the moving... 00402 $dbw->update( 00403 'revision', 00404 array( 'rev_page' => $this->mDestID ), 00405 array( 'rev_page' => $this->mTargetID, $timewhere ), 00406 __METHOD__ 00407 ); 00408 00409 $count = $dbw->affectedRows(); 00410 # Make the source page a redirect if no revisions are left 00411 $haveRevisions = $dbw->selectField( 00412 'revision', 00413 'rev_timestamp', 00414 array( 'rev_page' => $this->mTargetID ), 00415 __METHOD__, 00416 array( 'FOR UPDATE' ) 00417 ); 00418 if ( !$haveRevisions ) { 00419 if ( $this->mComment ) { 00420 $comment = $this->msg( 00421 'mergehistory-comment', 00422 $targetTitle->getPrefixedText(), 00423 $destTitle->getPrefixedText(), 00424 $this->mComment 00425 )->inContentLanguage()->text(); 00426 } else { 00427 $comment = $this->msg( 00428 'mergehistory-autocomment', 00429 $targetTitle->getPrefixedText(), 00430 $destTitle->getPrefixedText() 00431 )->inContentLanguage()->text(); 00432 } 00433 00434 $contentHandler = ContentHandler::getForTitle( $targetTitle ); 00435 $redirectContent = $contentHandler->makeRedirectContent( $destTitle ); 00436 00437 if ( $redirectContent ) { 00438 $redirectPage = WikiPage::factory( $targetTitle ); 00439 $redirectRevision = new Revision( array( 00440 'title' => $targetTitle, 00441 'page' => $this->mTargetID, 00442 'comment' => $comment, 00443 'content' => $redirectContent ) ); 00444 $redirectRevision->insertOn( $dbw ); 00445 $redirectPage->updateRevisionOn( $dbw, $redirectRevision ); 00446 00447 # Now, we record the link from the redirect to the new title. 00448 # It should have no other outgoing links... 00449 $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ ); 00450 $dbw->insert( 'pagelinks', 00451 array( 00452 'pl_from' => $this->mDestID, 00453 'pl_from_namespace' => $destTitle->getNamespace(), 00454 'pl_namespace' => $destTitle->getNamespace(), 00455 'pl_title' => $destTitle->getDBkey() ), 00456 __METHOD__ 00457 ); 00458 } else { 00459 // would be nice to show a warning if we couldn't create a redirect 00460 } 00461 } else { 00462 $targetTitle->invalidateCache(); // update histories 00463 } 00464 $destTitle->invalidateCache(); // update histories 00465 # Check if this did anything 00466 if ( !$count ) { 00467 $this->getOutput()->addWikiMsg( 'mergehistory-fail' ); 00468 00469 return false; 00470 } 00471 # Update our logs 00472 $log = new LogPage( 'merge' ); 00473 $log->addEntry( 00474 'merge', $targetTitle, $this->mComment, 00475 array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser() 00476 ); 00477 00478 # @todo message should use redirect=no 00479 $this->getOutput()->addWikiText( $this->msg( 'mergehistory-success', 00480 $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() )->numParams( 00481 $count )->text() ); 00482 00483 wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) ); 00484 00485 return true; 00486 } 00487 00488 protected function getGroupName() { 00489 return 'pagetools'; 00490 } 00491 } 00492 00493 class MergeHistoryPager extends ReverseChronologicalPager { 00495 public $mForm; 00496 00498 public $mConds; 00499 00500 function __construct( $form, $conds, $source, $dest ) { 00501 $this->mForm = $form; 00502 $this->mConds = $conds; 00503 $this->title = $source; 00504 $this->articleID = $source->getArticleID(); 00505 00506 $dbr = wfGetDB( DB_SLAVE ); 00507 $maxtimestamp = $dbr->selectField( 00508 'revision', 00509 'MIN(rev_timestamp)', 00510 array( 'rev_page' => $dest->getArticleID() ), 00511 __METHOD__ 00512 ); 00513 $this->maxTimestamp = $maxtimestamp; 00514 00515 parent::__construct( $form->getContext() ); 00516 } 00517 00518 function getStartBody() { 00519 wfProfileIn( __METHOD__ ); 00520 # Do a link batch query 00521 $this->mResult->seek( 0 ); 00522 $batch = new LinkBatch(); 00523 # Give some pointers to make (last) links 00524 $this->mForm->prevId = array(); 00525 foreach ( $this->mResult as $row ) { 00526 $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) ); 00527 $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) ); 00528 00529 $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id; 00530 if ( $rev_id > $row->rev_id ) { 00531 $this->mForm->prevId[$rev_id] = $row->rev_id; 00532 } elseif ( $rev_id < $row->rev_id ) { 00533 $this->mForm->prevId[$row->rev_id] = $rev_id; 00534 } 00535 00536 $rev_id = $row->rev_id; 00537 } 00538 00539 $batch->execute(); 00540 $this->mResult->seek( 0 ); 00541 00542 wfProfileOut( __METHOD__ ); 00543 00544 return ''; 00545 } 00546 00547 function formatRow( $row ) { 00548 return $this->mForm->formatRevisionRow( $row ); 00549 } 00550 00551 function getQueryInfo() { 00552 $conds = $this->mConds; 00553 $conds['rev_page'] = $this->articleID; 00554 $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp ); 00555 00556 return array( 00557 'tables' => array( 'revision', 'page', 'user' ), 00558 'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ), 00559 'conds' => $conds, 00560 'join_conds' => array( 00561 'page' => Revision::pageJoinCond(), 00562 'user' => Revision::userJoinCond() ) 00563 ); 00564 } 00565 00566 function getIndexField() { 00567 return 'rev_timestamp'; 00568 } 00569 }