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