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