MediaWiki  REL1_24
SpecialMergeHistory.php
Go to the documentation of this file.
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>&#160;</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 }