MediaWiki  REL1_20
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                         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>&#160;</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 }