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