[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/specials/ -> SpecialMergeHistory.php (source)

   1  <?php
   2  /**
   3   * Implements Special:MergeHistory
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   * @ingroup SpecialPage
  22   */
  23  
  24  /**
  25   * Special page allowing users with the appropriate permissions to
  26   * merge article histories, with some restrictions
  27   *
  28   * @ingroup SpecialPage
  29   */
  30  class SpecialMergeHistory extends SpecialPage {
  31      /** @var string */
  32      protected $mAction;
  33  
  34      /** @var string */
  35      protected $mTarget;
  36  
  37      /** @var string */
  38      protected $mDest;
  39  
  40      /** @var string */
  41      protected $mTimestamp;
  42  
  43      /** @var int */
  44      protected $mTargetID;
  45  
  46      /** @var int */
  47      protected $mDestID;
  48  
  49      /** @var string */
  50      protected $mComment;
  51  
  52      /** @var bool Was posted? */
  53      protected $mMerge;
  54  
  55      /** @var bool Was submitted? */
  56      protected $mSubmitted;
  57  
  58      /** @var Title */
  59      protected $mTargetObj;
  60  
  61      /** @var Title */
  62      protected $mDestObj;
  63  
  64  	public function __construct() {
  65          parent::__construct( 'MergeHistory', 'mergehistory' );
  66      }
  67  
  68      /**
  69       * @return void
  70       */
  71  	private function loadRequestParams() {
  72          $request = $this->getRequest();
  73          $this->mAction = $request->getVal( 'action' );
  74          $this->mTarget = $request->getVal( 'target' );
  75          $this->mDest = $request->getVal( 'dest' );
  76          $this->mSubmitted = $request->getBool( 'submitted' );
  77  
  78          $this->mTargetID = intval( $request->getVal( 'targetID' ) );
  79          $this->mDestID = intval( $request->getVal( 'destID' ) );
  80          $this->mTimestamp = $request->getVal( 'mergepoint' );
  81          if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
  82              $this->mTimestamp = '';
  83          }
  84          $this->mComment = $request->getText( 'wpComment' );
  85  
  86          $this->mMerge = $request->wasPosted()
  87              && $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
  88  
  89          // target page
  90          if ( $this->mSubmitted ) {
  91              $this->mTargetObj = Title::newFromURL( $this->mTarget );
  92              $this->mDestObj = Title::newFromURL( $this->mDest );
  93          } else {
  94              $this->mTargetObj = null;
  95              $this->mDestObj = null;
  96          }
  97          $this->preCacheMessages();
  98      }
  99  
 100      /**
 101       * As we use the same small set of messages in various methods and that
 102       * they are called often, we call them once and save them in $this->message
 103       */
 104  	function preCacheMessages() {
 105          // Precache various messages
 106          if ( !isset( $this->message ) ) {
 107              $this->message['last'] = $this->msg( 'last' )->escaped();
 108          }
 109      }
 110  
 111  	public function execute( $par ) {
 112          $this->checkPermissions();
 113          $this->checkReadOnly();
 114  
 115          $this->loadRequestParams();
 116  
 117          $this->setHeaders();
 118          $this->outputHeader();
 119  
 120          if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
 121              $this->merge();
 122  
 123              return;
 124          }
 125  
 126          if ( !$this->mSubmitted ) {
 127              $this->showMergeForm();
 128  
 129              return;
 130          }
 131  
 132          $errors = array();
 133          if ( !$this->mTargetObj instanceof Title ) {
 134              $errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
 135          } elseif ( !$this->mTargetObj->exists() ) {
 136              $errors[] = $this->msg( 'mergehistory-no-source',
 137                  wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
 138              )->parseAsBlock();
 139          }
 140  
 141          if ( !$this->mDestObj instanceof Title ) {
 142              $errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
 143          } elseif ( !$this->mDestObj->exists() ) {
 144              $errors[] = $this->msg( 'mergehistory-no-destination',
 145                  wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
 146              )->parseAsBlock();
 147          }
 148  
 149          if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
 150              $errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
 151          }
 152  
 153          if ( count( $errors ) ) {
 154              $this->showMergeForm();
 155              $this->getOutput()->addHTML( implode( "\n", $errors ) );
 156          } else {
 157              $this->showHistory();
 158          }
 159      }
 160  
 161  	function showMergeForm() {
 162          $this->getOutput()->addWikiMsg( 'mergehistory-header' );
 163  
 164          $this->getOutput()->addHTML(
 165              Xml::openElement( 'form', array(
 166                  'method' => 'get',
 167                  'action' => wfScript() ) ) .
 168                  '<fieldset>' .
 169                  Xml::element( 'legend', array(),
 170                      $this->msg( 'mergehistory-box' )->text() ) .
 171                  Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
 172                  Html::hidden( 'submitted', '1' ) .
 173                  Html::hidden( 'mergepoint', $this->mTimestamp ) .
 174                  Xml::openElement( 'table' ) .
 175                  '<tr>
 176                  <td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
 177                  <td>' . Xml::input( 'target', 30, $this->mTarget, array( 'id' => 'target' ) ) . '</td>
 178              </tr><tr>
 179                  <td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
 180                  <td>' . Xml::input( 'dest', 30, $this->mDest, array( 'id' => 'dest' ) ) . '</td>
 181              </tr><tr><td>' .
 182                  Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
 183                  '</td></tr>' .
 184                  Xml::closeElement( 'table' ) .
 185                  '</fieldset>' .
 186                  '</form>'
 187          );
 188      }
 189  
 190  	private function showHistory() {
 191          $this->showMergeForm();
 192  
 193          # List all stored revisions
 194          $revisions = new MergeHistoryPager(
 195              $this, array(), $this->mTargetObj, $this->mDestObj
 196          );
 197          $haveRevisions = $revisions && $revisions->getNumRows() > 0;
 198  
 199          $out = $this->getOutput();
 200          $titleObj = $this->getPageTitle();
 201          $action = $titleObj->getLocalURL( array( 'action' => 'submit' ) );
 202          # Start the form here
 203          $top = Xml::openElement(
 204              'form',
 205              array(
 206                  'method' => 'post',
 207                  'action' => $action,
 208                  'id' => 'merge'
 209              )
 210          );
 211          $out->addHTML( $top );
 212  
 213          if ( $haveRevisions ) {
 214              # Format the user-visible controls (comment field, submission button)
 215              # in a nice little table
 216              $table =
 217                  Xml::openElement( 'fieldset' ) .
 218                      $this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
 219                          $this->mDestObj->getPrefixedText() )->parse() .
 220                      Xml::openElement( 'table', array( 'id' => 'mw-mergehistory-table' ) ) .
 221                      '<tr>
 222                          <td class="mw-label">' .
 223                      Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
 224                      '</td>
 225                      <td class="mw-input">' .
 226                      Xml::input( 'wpComment', 50, $this->mComment, array( 'id' => 'wpComment' ) ) .
 227                      '</td>
 228                      </tr>
 229                      <tr>
 230                          <td>&#160;</td>
 231                          <td class="mw-submit">' .
 232                      Xml::submitButton(
 233                          $this->msg( 'mergehistory-submit' )->text(),
 234                          array( 'name' => 'merge', 'id' => 'mw-merge-submit' )
 235                      ) .
 236                      '</td>
 237                      </tr>' .
 238                      Xml::closeElement( 'table' ) .
 239                      Xml::closeElement( 'fieldset' );
 240  
 241              $out->addHTML( $table );
 242          }
 243  
 244          $out->addHTML(
 245              '<h2 id="mw-mergehistory">' .
 246                  $this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
 247          );
 248  
 249          if ( $haveRevisions ) {
 250              $out->addHTML( $revisions->getNavigationBar() );
 251              $out->addHTML( '<ul>' );
 252              $out->addHTML( $revisions->getBody() );
 253              $out->addHTML( '</ul>' );
 254              $out->addHTML( $revisions->getNavigationBar() );
 255          } else {
 256              $out->addWikiMsg( 'mergehistory-empty' );
 257          }
 258  
 259          # Show relevant lines from the merge log:
 260          $mergeLogPage = new LogPage( 'merge' );
 261          $out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
 262          LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
 263  
 264          # When we submit, go by page ID to avoid some nasty but unlikely collisions.
 265          # Such would happen if a page was renamed after the form loaded, but before submit
 266          $misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
 267          $misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
 268          $misc .= Html::hidden( 'target', $this->mTarget );
 269          $misc .= Html::hidden( 'dest', $this->mDest );
 270          $misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
 271          $misc .= Xml::closeElement( 'form' );
 272          $out->addHTML( $misc );
 273  
 274          return true;
 275      }
 276  
 277  	function formatRevisionRow( $row ) {
 278          $rev = new Revision( $row );
 279  
 280          $stxt = '';
 281          $last = $this->message['last'];
 282  
 283          $ts = wfTimestamp( TS_MW, $row->rev_timestamp );
 284          $checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
 285  
 286          $user = $this->getUser();
 287  
 288          $pageLink = Linker::linkKnown(
 289              $rev->getTitle(),
 290              htmlspecialchars( $this->getLanguage()->userTimeAndDate( $ts, $user ) ),
 291              array(),
 292              array( 'oldid' => $rev->getId() )
 293          );
 294          if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
 295              $pageLink = '<span class="history-deleted">' . $pageLink . '</span>';
 296          }
 297  
 298          # Last link
 299          if ( !$rev->userCan( Revision::DELETED_TEXT, $user ) ) {
 300              $last = $this->message['last'];
 301          } elseif ( isset( $this->prevId[$row->rev_id] ) ) {
 302              $last = Linker::linkKnown(
 303                  $rev->getTitle(),
 304                  $this->message['last'],
 305                  array(),
 306                  array(
 307                      'diff' => $row->rev_id,
 308                      'oldid' => $this->prevId[$row->rev_id]
 309                  )
 310              );
 311          }
 312  
 313          $userLink = Linker::revUserTools( $rev );
 314  
 315          $size = $row->rev_len;
 316          if ( !is_null( $size ) ) {
 317              $stxt = Linker::formatRevisionSize( $size );
 318          }
 319          $comment = Linker::revComment( $rev );
 320  
 321          return Html::rawElement( 'li', array(),
 322              $this->msg( 'mergehistory-revisionrow' )
 323                  ->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
 324      }
 325  
 326      /**
 327       * Actually attempt the history move
 328       *
 329       * @todo if all versions of page A are moved to B and then a user
 330       * tries to do a reverse-merge via the "unmerge" log link, then page
 331       * A will still be a redirect (as it was after the original merge),
 332       * though it will have the old revisions back from before (as expected).
 333       * The user may have to "undo" the redirect manually to finish the "unmerge".
 334       * Maybe this should delete redirects at the target page of merges?
 335       *
 336       * @return bool Success
 337       */
 338  	function merge() {
 339          # Get the titles directly from the IDs, in case the target page params
 340          # were spoofed. The queries are done based on the IDs, so it's best to
 341          # keep it consistent...
 342          $targetTitle = Title::newFromID( $this->mTargetID );
 343          $destTitle = Title::newFromID( $this->mDestID );
 344          if ( is_null( $targetTitle ) || is_null( $destTitle ) ) {
 345              return false; // validate these
 346          }
 347          if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
 348              return false;
 349          }
 350          # Verify that this timestamp is valid
 351          # Must be older than the destination page
 352          $dbw = wfGetDB( DB_MASTER );
 353          # Get timestamp into DB format
 354          $this->mTimestamp = $this->mTimestamp ? $dbw->timestamp( $this->mTimestamp ) : '';
 355          # Max timestamp should be min of destination page
 356          $maxtimestamp = $dbw->selectField(
 357              'revision',
 358              'MIN(rev_timestamp)',
 359              array( 'rev_page' => $this->mDestID ),
 360              __METHOD__
 361          );
 362          # Destination page must exist with revisions
 363          if ( !$maxtimestamp ) {
 364              $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
 365  
 366              return false;
 367          }
 368          # Get the latest timestamp of the source
 369          $lasttimestamp = $dbw->selectField(
 370              array( 'page', 'revision' ),
 371              'rev_timestamp',
 372              array( 'page_id' => $this->mTargetID, 'page_latest = rev_id' ),
 373              __METHOD__
 374          );
 375          # $this->mTimestamp must be older than $maxtimestamp
 376          if ( $this->mTimestamp >= $maxtimestamp ) {
 377              $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
 378  
 379              return false;
 380          }
 381          # Get the timestamp pivot condition
 382          if ( $this->mTimestamp ) {
 383              $timewhere = "rev_timestamp <= {$this->mTimestamp}";
 384              $timestampLimit = wfTimestamp( TS_MW, $this->mTimestamp );
 385          } else {
 386              $timewhere = "rev_timestamp <= {$maxtimestamp}";
 387              $timestampLimit = wfTimestamp( TS_MW, $lasttimestamp );
 388          }
 389          # Check that there are not too many revisions to move
 390          $limit = 5000; // avoid too much slave lag
 391          $count = $dbw->selectRowCount( 'revision', '1',
 392              array( 'rev_page' => $this->mTargetID, $timewhere ),
 393              __METHOD__,
 394              array( 'LIMIT' => $limit + 1 )
 395          );
 396          if ( $count > $limit ) {
 397              $this->getOutput()->addWikiMsg( 'mergehistory-fail-toobig' );
 398  
 399              return false;
 400          }
 401          # Do the moving...
 402          $dbw->update(
 403              'revision',
 404              array( 'rev_page' => $this->mDestID ),
 405              array( 'rev_page' => $this->mTargetID, $timewhere ),
 406              __METHOD__
 407          );
 408  
 409          $count = $dbw->affectedRows();
 410          # Make the source page a redirect if no revisions are left
 411          $haveRevisions = $dbw->selectField(
 412              'revision',
 413              'rev_timestamp',
 414              array( 'rev_page' => $this->mTargetID ),
 415              __METHOD__,
 416              array( 'FOR UPDATE' )
 417          );
 418          if ( !$haveRevisions ) {
 419              if ( $this->mComment ) {
 420                  $comment = $this->msg(
 421                      'mergehistory-comment',
 422                      $targetTitle->getPrefixedText(),
 423                      $destTitle->getPrefixedText(),
 424                      $this->mComment
 425                  )->inContentLanguage()->text();
 426              } else {
 427                  $comment = $this->msg(
 428                      'mergehistory-autocomment',
 429                      $targetTitle->getPrefixedText(),
 430                      $destTitle->getPrefixedText()
 431                  )->inContentLanguage()->text();
 432              }
 433  
 434              $contentHandler = ContentHandler::getForTitle( $targetTitle );
 435              $redirectContent = $contentHandler->makeRedirectContent( $destTitle );
 436  
 437              if ( $redirectContent ) {
 438                  $redirectPage = WikiPage::factory( $targetTitle );
 439                  $redirectRevision = new Revision( array(
 440                      'title' => $targetTitle,
 441                      'page' => $this->mTargetID,
 442                      'comment' => $comment,
 443                      'content' => $redirectContent ) );
 444                  $redirectRevision->insertOn( $dbw );
 445                  $redirectPage->updateRevisionOn( $dbw, $redirectRevision );
 446  
 447                  # Now, we record the link from the redirect to the new title.
 448                  # It should have no other outgoing links...
 449                  $dbw->delete( 'pagelinks', array( 'pl_from' => $this->mDestID ), __METHOD__ );
 450                  $dbw->insert( 'pagelinks',
 451                      array(
 452                          'pl_from' => $this->mDestID,
 453                          'pl_from_namespace' => $destTitle->getNamespace(),
 454                          'pl_namespace' => $destTitle->getNamespace(),
 455                          'pl_title' => $destTitle->getDBkey() ),
 456                      __METHOD__
 457                  );
 458              } else {
 459                  // would be nice to show a warning if we couldn't create a redirect
 460              }
 461          } else {
 462              $targetTitle->invalidateCache(); // update histories
 463          }
 464          $destTitle->invalidateCache(); // update histories
 465          # Check if this did anything
 466          if ( !$count ) {
 467              $this->getOutput()->addWikiMsg( 'mergehistory-fail' );
 468  
 469              return false;
 470          }
 471          # Update our logs
 472          $log = new LogPage( 'merge' );
 473          $log->addEntry(
 474              'merge', $targetTitle, $this->mComment,
 475              array( $destTitle->getPrefixedText(), $timestampLimit ), $this->getUser()
 476          );
 477  
 478          # @todo message should use redirect=no
 479          $this->getOutput()->addWikiText( $this->msg( 'mergehistory-success',
 480              $targetTitle->getPrefixedText(), $destTitle->getPrefixedText() )->numParams(
 481              $count )->text() );
 482  
 483          wfRunHooks( 'ArticleMergeComplete', array( $targetTitle, $destTitle ) );
 484  
 485          return true;
 486      }
 487  
 488  	protected function getGroupName() {
 489          return 'pagetools';
 490      }
 491  }
 492  
 493  class MergeHistoryPager extends ReverseChronologicalPager {
 494      /** @var IContextSource */
 495      public $mForm;
 496  
 497      /** @var array */
 498      public $mConds;
 499  
 500  	function __construct( $form, $conds, $source, $dest ) {
 501          $this->mForm = $form;
 502          $this->mConds = $conds;
 503          $this->title = $source;
 504          $this->articleID = $source->getArticleID();
 505  
 506          $dbr = wfGetDB( DB_SLAVE );
 507          $maxtimestamp = $dbr->selectField(
 508              'revision',
 509              'MIN(rev_timestamp)',
 510              array( 'rev_page' => $dest->getArticleID() ),
 511              __METHOD__
 512          );
 513          $this->maxTimestamp = $maxtimestamp;
 514  
 515          parent::__construct( $form->getContext() );
 516      }
 517  
 518  	function getStartBody() {
 519          wfProfileIn( __METHOD__ );
 520          # Do a link batch query
 521          $this->mResult->seek( 0 );
 522          $batch = new LinkBatch();
 523          # Give some pointers to make (last) links
 524          $this->mForm->prevId = array();
 525          foreach ( $this->mResult as $row ) {
 526              $batch->addObj( Title::makeTitleSafe( NS_USER, $row->user_name ) );
 527              $batch->addObj( Title::makeTitleSafe( NS_USER_TALK, $row->user_name ) );
 528  
 529              $rev_id = isset( $rev_id ) ? $rev_id : $row->rev_id;
 530              if ( $rev_id > $row->rev_id ) {
 531                  $this->mForm->prevId[$rev_id] = $row->rev_id;
 532              } elseif ( $rev_id < $row->rev_id ) {
 533                  $this->mForm->prevId[$row->rev_id] = $rev_id;
 534              }
 535  
 536              $rev_id = $row->rev_id;
 537          }
 538  
 539          $batch->execute();
 540          $this->mResult->seek( 0 );
 541  
 542          wfProfileOut( __METHOD__ );
 543  
 544          return '';
 545      }
 546  
 547  	function formatRow( $row ) {
 548          return $this->mForm->formatRevisionRow( $row );
 549      }
 550  
 551  	function getQueryInfo() {
 552          $conds = $this->mConds;
 553          $conds['rev_page'] = $this->articleID;
 554          $conds[] = "rev_timestamp < " . $this->mDb->addQuotes( $this->maxTimestamp );
 555  
 556          return array(
 557              'tables' => array( 'revision', 'page', 'user' ),
 558              'fields' => array_merge( Revision::selectFields(), Revision::selectUserFields() ),
 559              'conds' => $conds,
 560              'join_conds' => array(
 561                  'page' => Revision::pageJoinCond(),
 562                  'user' => Revision::userJoinCond() )
 563          );
 564      }
 565  
 566  	function getIndexField() {
 567          return 'rev_timestamp';
 568      }
 569  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1