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