wiki.techinc.nl/includes/specials/SpecialMergeHistory.php
Timo Tijhof e387cd9c35 Change trivial use of getVal('action') to getRawVal
Per docs added in I18767cd809f67b, these don't need normalization
as they are only compared against predefined strings, and besides
are generally entered manually in a form, and even then would not
require the kinds of Unicode chars that have multiple/non-normalized
forms.

In nearby areas to also fix some trivial cases:

* getVal('title') obviously needs normalization.
  Use getText() to make this more obvious.

* getVal() compared against simple string literals within the code
  obviously don't need normalization (e.g. printable === 'no').

* Change hot code in MediaWiki checking for whether 'diff' or 'oldid'
  are set to getCheck (which uses getRawVal) instead of getVal.
  As a bonus this means it now handles values like "0" correctly,
  which could theoretically have caused bad behaviour before.

Change-Id: Ied721cfdf59c7ba11d1afa6f4cc59ede1381238e
2021-08-26 22:11:58 +01:00

429 lines
12 KiB
PHP

<?php
/**
* Implements Special:MergeHistory
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup SpecialPage
*/
use MediaWiki\Cache\LinkBatchFactory;
use MediaWiki\Page\MergeHistoryFactory;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use Wikimedia\Rdbms\ILoadBalancer;
/**
* Special page allowing users with the appropriate permissions to
* merge article histories, with some restrictions
*
* @ingroup SpecialPage
*/
class SpecialMergeHistory extends SpecialPage {
/** @var string */
protected $mAction;
/** @var string */
protected $mTarget;
/** @var string */
protected $mDest;
/** @var string */
protected $mTimestamp;
/** @var int */
protected $mTargetID;
/** @var int */
protected $mDestID;
/** @var string */
protected $mComment;
/** @var bool Was posted? */
protected $mMerge;
/** @var bool Was submitted? */
protected $mSubmitted;
/** @var Title */
protected $mTargetObj;
/** @var Title */
protected $mDestObj;
/** @var int[] */
public $prevId;
/** @var MergeHistoryFactory */
private $mergeHistoryFactory;
/** @var LinkBatchFactory */
private $linkBatchFactory;
/** @var ILoadBalancer */
private $loadBalancer;
/** @var RevisionStore */
private $revisionStore;
/**
* @param MergeHistoryFactory $mergeHistoryFactory
* @param LinkBatchFactory $linkBatchFactory
* @param ILoadBalancer $loadBalancer
* @param RevisionStore $revisionStore
*/
public function __construct(
MergeHistoryFactory $mergeHistoryFactory,
LinkBatchFactory $linkBatchFactory,
ILoadBalancer $loadBalancer,
RevisionStore $revisionStore
) {
parent::__construct( 'MergeHistory', 'mergehistory' );
$this->mergeHistoryFactory = $mergeHistoryFactory;
$this->linkBatchFactory = $linkBatchFactory;
$this->loadBalancer = $loadBalancer;
$this->revisionStore = $revisionStore;
}
public function doesWrites() {
return true;
}
/**
* @return void
*/
private function loadRequestParams() {
$request = $this->getRequest();
$this->mAction = $request->getRawVal( 'action' );
$this->mTarget = $request->getVal( 'target' );
$this->mDest = $request->getVal( 'dest' );
$this->mSubmitted = $request->getBool( 'submitted' );
$this->mTargetID = intval( $request->getVal( 'targetID' ) );
$this->mDestID = intval( $request->getVal( 'destID' ) );
$this->mTimestamp = $request->getVal( 'mergepoint' );
if ( !preg_match( '/[0-9]{14}/', $this->mTimestamp ) ) {
$this->mTimestamp = '';
}
$this->mComment = $request->getText( 'wpComment' );
$this->mMerge = $request->wasPosted()
&& $this->getUser()->matchEditToken( $request->getVal( 'wpEditToken' ) );
// target page
if ( $this->mSubmitted ) {
$this->mTargetObj = Title::newFromText( $this->mTarget );
$this->mDestObj = Title::newFromText( $this->mDest );
} else {
$this->mTargetObj = null;
$this->mDestObj = null;
}
}
public function execute( $par ) {
$this->useTransactionalTimeLimit();
$this->checkPermissions();
$this->checkReadOnly();
$this->loadRequestParams();
$this->setHeaders();
$this->outputHeader();
if ( $this->mTargetID && $this->mDestID && $this->mAction == 'submit' && $this->mMerge ) {
$this->merge();
return;
}
if ( !$this->mSubmitted ) {
$this->showMergeForm();
return;
}
$errors = [];
if ( !$this->mTargetObj instanceof Title ) {
$errors[] = $this->msg( 'mergehistory-invalid-source' )->parseAsBlock();
} elseif ( !$this->mTargetObj->exists() ) {
$errors[] = $this->msg( 'mergehistory-no-source',
wfEscapeWikiText( $this->mTargetObj->getPrefixedText() )
)->parseAsBlock();
}
if ( !$this->mDestObj instanceof Title ) {
$errors[] = $this->msg( 'mergehistory-invalid-destination' )->parseAsBlock();
} elseif ( !$this->mDestObj->exists() ) {
$errors[] = $this->msg( 'mergehistory-no-destination',
wfEscapeWikiText( $this->mDestObj->getPrefixedText() )
)->parseAsBlock();
}
if ( $this->mTargetObj && $this->mDestObj && $this->mTargetObj->equals( $this->mDestObj ) ) {
$errors[] = $this->msg( 'mergehistory-same-destination' )->parseAsBlock();
}
if ( count( $errors ) ) {
$this->showMergeForm();
$this->getOutput()->addHTML( implode( "\n", $errors ) );
} else {
$this->showHistory();
}
}
private function showMergeForm() {
$out = $this->getOutput();
$out->addWikiMsg( 'mergehistory-header' );
$out->addHTML(
Xml::openElement( 'form', [
'method' => 'get',
'action' => wfScript() ] ) .
'<fieldset>' .
Xml::element( 'legend', [],
$this->msg( 'mergehistory-box' )->text() ) .
Html::hidden( 'title', $this->getPageTitle()->getPrefixedDBkey() ) .
Html::hidden( 'submitted', '1' ) .
Html::hidden( 'mergepoint', $this->mTimestamp ) .
Xml::openElement( 'table' ) .
'<tr>
<td>' . Xml::label( $this->msg( 'mergehistory-from' )->text(), 'target' ) . '</td>
<td>' . Xml::input( 'target', 30, $this->mTarget, [ 'id' => 'target' ] ) . '</td>
</tr><tr>
<td>' . Xml::label( $this->msg( 'mergehistory-into' )->text(), 'dest' ) . '</td>
<td>' . Xml::input( 'dest', 30, $this->mDest, [ 'id' => 'dest' ] ) . '</td>
</tr><tr><td>' .
Xml::submitButton( $this->msg( 'mergehistory-go' )->text() ) .
'</td></tr>' .
Xml::closeElement( 'table' ) .
'</fieldset>' .
'</form>'
);
$this->addHelpLink( 'Help:Merge history' );
}
private function showHistory() {
$this->showMergeForm();
# List all stored revisions
$revisions = new MergeHistoryPager(
$this,
[],
$this->mTargetObj,
$this->mDestObj,
$this->linkBatchFactory,
$this->loadBalancer,
$this->revisionStore
);
$haveRevisions = $revisions->getNumRows() > 0;
$out = $this->getOutput();
$titleObj = $this->getPageTitle();
$action = $titleObj->getLocalURL( [ 'action' => 'submit' ] );
# Start the form here
$top = Xml::openElement(
'form',
[
'method' => 'post',
'action' => $action,
'id' => 'merge'
]
);
$out->addHTML( $top );
if ( $haveRevisions ) {
# Format the user-visible controls (comment field, submission button)
# in a nice little table
$table =
Xml::openElement( 'fieldset' ) .
$this->msg( 'mergehistory-merge', $this->mTargetObj->getPrefixedText(),
$this->mDestObj->getPrefixedText() )->parse() .
Xml::openElement( 'table', [ 'id' => 'mw-mergehistory-table' ] ) .
'<tr>
<td class="mw-label">' .
Xml::label( $this->msg( 'mergehistory-reason' )->text(), 'wpComment' ) .
'</td>
<td class="mw-input">' .
Xml::input( 'wpComment', 50, $this->mComment, [ 'id' => 'wpComment' ] ) .
"</td>
</tr>
<tr>
<td>\u{00A0}</td>
<td class=\"mw-submit\">" .
Xml::submitButton(
$this->msg( 'mergehistory-submit' )->text(),
[ 'name' => 'merge', 'id' => 'mw-merge-submit' ]
) .
'</td>
</tr>' .
Xml::closeElement( 'table' ) .
Xml::closeElement( 'fieldset' );
$out->addHTML( $table );
}
$out->addHTML(
'<h2 id="mw-mergehistory">' .
$this->msg( 'mergehistory-list' )->escaped() . "</h2>\n"
);
if ( $haveRevisions ) {
$out->addHTML( $revisions->getNavigationBar() );
$out->addHTML( '<ul>' );
$out->addHTML( $revisions->getBody() );
$out->addHTML( '</ul>' );
$out->addHTML( $revisions->getNavigationBar() );
} else {
$out->addWikiMsg( 'mergehistory-empty' );
}
# Show relevant lines from the merge log:
$mergeLogPage = new LogPage( 'merge' );
$out->addHTML( '<h2>' . $mergeLogPage->getName()->escaped() . "</h2>\n" );
LogEventsList::showLogExtract( $out, 'merge', $this->mTargetObj );
# When we submit, go by page ID to avoid some nasty but unlikely collisions.
# Such would happen if a page was renamed after the form loaded, but before submit
$misc = Html::hidden( 'targetID', $this->mTargetObj->getArticleID() );
$misc .= Html::hidden( 'destID', $this->mDestObj->getArticleID() );
$misc .= Html::hidden( 'target', $this->mTarget );
$misc .= Html::hidden( 'dest', $this->mDest );
$misc .= Html::hidden( 'wpEditToken', $this->getUser()->getEditToken() );
$misc .= Xml::closeElement( 'form' );
$out->addHTML( $misc );
return true;
}
public function formatRevisionRow( $row ) {
$revRecord = $this->revisionStore->newRevisionFromRow( $row );
$linkRenderer = $this->getLinkRenderer();
$stxt = '';
$last = $this->msg( 'last' )->escaped();
$ts = wfTimestamp( TS_MW, $row->rev_timestamp );
$checkBox = Xml::radio( 'mergepoint', $ts, ( $this->mTimestamp === $ts ) );
$user = $this->getUser();
$pageLink = $linkRenderer->makeKnownLink(
$revRecord->getPageAsLinkTarget(),
$this->getLanguage()->userTimeAndDate( $ts, $user ),
[],
[ 'oldid' => $revRecord->getId() ]
);
if ( $revRecord->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
$class = Linker::getRevisionDeletedClass( $revRecord );
$pageLink = '<span class=" ' . $class . '">' . $pageLink . '</span>';
}
# Last link
if ( !$revRecord->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
$last = $this->msg( 'last' )->escaped();
} elseif ( isset( $this->prevId[$row->rev_id] ) ) {
$last = $linkRenderer->makeKnownLink(
$revRecord->getPageAsLinkTarget(),
$this->msg( 'last' )->text(),
[],
[
'diff' => $row->rev_id,
'oldid' => $this->prevId[$row->rev_id]
]
);
}
$userLink = Linker::revUserTools( $revRecord );
$size = $row->rev_len;
if ( $size !== null ) {
$stxt = Linker::formatRevisionSize( $size );
}
$comment = Linker::revComment( $revRecord );
return Html::rawElement( 'li', [],
$this->msg( 'mergehistory-revisionrow' )
->rawParams( $checkBox, $last, $pageLink, $userLink, $stxt, $comment )->escaped() );
}
/**
* Actually attempt the history move
*
* @todo if all versions of page A are moved to B and then a user
* tries to do a reverse-merge via the "unmerge" log link, then page
* A will still be a redirect (as it was after the original merge),
* though it will have the old revisions back from before (as expected).
* The user may have to "undo" the redirect manually to finish the "unmerge".
* Maybe this should delete redirects at the target page of merges?
*
* @return bool Success
*/
private function merge() {
# Get the titles directly from the IDs, in case the target page params
# were spoofed. The queries are done based on the IDs, so it's best to
# keep it consistent...
$targetTitle = Title::newFromID( $this->mTargetID );
$destTitle = Title::newFromID( $this->mDestID );
if ( $targetTitle === null || $destTitle === null ) {
return false; // validate these
}
if ( $targetTitle->getArticleID() == $destTitle->getArticleID() ) {
return false;
}
// MergeHistory object
$mh = $this->mergeHistoryFactory->newMergeHistory( $targetTitle, $destTitle, $this->mTimestamp );
// Merge!
$mergeStatus = $mh->merge( $this->getUser(), $this->mComment );
if ( !$mergeStatus->isOK() ) {
// Failed merge
$this->getOutput()->addWikiMsg( $mergeStatus->getMessage() );
return false;
}
$linkRenderer = $this->getLinkRenderer();
$targetLink = $linkRenderer->makeLink(
$targetTitle,
null,
[],
[ 'redirect' => 'no' ]
);
// In some cases the target page will be deleted
$append = ( $mergeStatus->getValue() === 'source-deleted' )
? $this->msg( 'mergehistory-source-deleted', $targetTitle->getPrefixedText() ) : '';
$this->getOutput()->addWikiMsg( $this->msg( 'mergehistory-done' )
->rawParams( $targetLink )
->params( $destTitle->getPrefixedText(), $append )
->numParams( $mh->getMergedRevisionCount() )
);
return true;
}
protected function getGroupName() {
return 'pagetools';
}
}