MediaWiki  REL1_19
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'] = 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>&#160;</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 }