Support diffing deleted revisions, user-supplied text, and additional properties about the diffed revisions such as the user and edit summary. Bug: T20189 Bug: T30047 Bug: T164529 Change-Id: I5565d717d9c2fd19da7cae02890a15e244cc238b
490 lines
14 KiB
PHP
490 lines
14 KiB
PHP
<?php
|
|
/**
|
|
*
|
|
* 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
|
|
*/
|
|
|
|
class ApiComparePages extends ApiBase {
|
|
|
|
private $guessed = false, $guessedTitle, $guessedModel, $props;
|
|
|
|
public function execute() {
|
|
$params = $this->extractRequestParams();
|
|
|
|
// Parameter validation
|
|
$this->requireAtLeastOneParameter( $params, 'fromtitle', 'fromid', 'fromrev', 'fromtext' );
|
|
$this->requireAtLeastOneParameter( $params, 'totitle', 'toid', 'torev', 'totext', 'torelative' );
|
|
|
|
$this->props = array_flip( $params['prop'] );
|
|
|
|
// Cache responses publicly by default. This may be overridden later.
|
|
$this->getMain()->setCacheMode( 'public' );
|
|
|
|
// Get the 'from' Revision and Content
|
|
list( $fromRev, $fromContent, $relRev ) = $this->getDiffContent( 'from', $params );
|
|
|
|
// Get the 'to' Revision and Content
|
|
if ( $params['torelative'] !== null ) {
|
|
if ( !$relRev ) {
|
|
$this->dieWithError( 'apierror-compare-relative-to-nothing' );
|
|
}
|
|
switch ( $params['torelative'] ) {
|
|
case 'prev':
|
|
// Swap 'from' and 'to'
|
|
$toRev = $fromRev;
|
|
$toContent = $fromContent;
|
|
$fromRev = $relRev->getPrevious();
|
|
$fromContent = $fromRev
|
|
? $fromRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
|
|
: $toContent->getContentHandler()->makeEmptyContent();
|
|
if ( !$fromContent ) {
|
|
$this->dieWithError(
|
|
[ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent'
|
|
);
|
|
}
|
|
break;
|
|
|
|
case 'next':
|
|
$toRev = $relRev->getNext();
|
|
$toContent = $toRev
|
|
? $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() )
|
|
: $fromContent;
|
|
if ( !$toContent ) {
|
|
$this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
|
|
}
|
|
break;
|
|
|
|
case 'cur':
|
|
$title = $relRev->getTitle();
|
|
$id = $title->getLatestRevID();
|
|
$toRev = $id ? Revision::newFromId( $id ) : null;
|
|
if ( !$toRev ) {
|
|
$this->dieWithError(
|
|
[ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
|
|
);
|
|
}
|
|
$toContent = $toRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
|
|
if ( !$toContent ) {
|
|
$this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
|
|
}
|
|
break;
|
|
}
|
|
$relRev2 = null;
|
|
} else {
|
|
list( $toRev, $toContent, $relRev2 ) = $this->getDiffContent( 'to', $params );
|
|
}
|
|
|
|
// Should never happen, but just in case...
|
|
if ( !$fromContent || !$toContent ) {
|
|
$this->dieWithError( 'apierror-baddiff' );
|
|
}
|
|
|
|
// Get the diff
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
if ( $relRev && $relRev->getTitle() ) {
|
|
$context->setTitle( $relRev->getTitle() );
|
|
} elseif ( $relRev2 && $relRev2->getTitle() ) {
|
|
$context->setTitle( $relRev2->getTitle() );
|
|
} else {
|
|
$this->guessTitleAndModel();
|
|
if ( $this->guessedTitle ) {
|
|
$context->setTitle( $this->guessedTitle );
|
|
}
|
|
}
|
|
$de = $fromContent->getContentHandler()->createDifferenceEngine(
|
|
$context,
|
|
$fromRev ? $fromRev->getId() : 0,
|
|
$toRev ? $toRev->getId() : 0,
|
|
/* $rcid = */ null,
|
|
/* $refreshCache = */ false,
|
|
/* $unhide = */ true
|
|
);
|
|
$de->setContent( $fromContent, $toContent );
|
|
$difftext = $de->getDiffBody();
|
|
if ( $difftext === false ) {
|
|
$this->dieWithError( 'apierror-baddiff' );
|
|
}
|
|
|
|
// Fill in the response
|
|
$vals = [];
|
|
$this->setVals( $vals, 'from', $fromRev );
|
|
$this->setVals( $vals, 'to', $toRev );
|
|
|
|
if ( isset( $this->props['rel'] ) ) {
|
|
if ( $fromRev ) {
|
|
$rev = $fromRev->getPrevious();
|
|
if ( $rev ) {
|
|
$vals['prev'] = $rev->getId();
|
|
}
|
|
}
|
|
if ( $toRev ) {
|
|
$rev = $toRev->getNext();
|
|
if ( $rev ) {
|
|
$vals['next'] = $rev->getId();
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( isset( $this->props['diffsize'] ) ) {
|
|
$vals['diffsize'] = strlen( $difftext );
|
|
}
|
|
if ( isset( $this->props['diff'] ) ) {
|
|
ApiResult::setContentValue( $vals, 'body', $difftext );
|
|
}
|
|
|
|
$this->getResult()->addValue( null, $this->getModuleName(), $vals );
|
|
}
|
|
|
|
/**
|
|
* Guess an appropriate default Title and content model for this request
|
|
*
|
|
* Fills in $this->guessedTitle based on the first of 'fromrev',
|
|
* 'fromtitle', 'fromid', 'torev', 'totitle', and 'toid' that's present and
|
|
* valid.
|
|
*
|
|
* Fills in $this->guessedModel based on the Revision or Title used to
|
|
* determine $this->guessedTitle, or the 'fromcontentmodel' or
|
|
* 'tocontentmodel' parameters if no title was guessed.
|
|
*/
|
|
private function guessTitleAndModel() {
|
|
if ( $this->guessed ) {
|
|
return;
|
|
}
|
|
|
|
$this->guessed = true;
|
|
$params = $this->extractRequestParams();
|
|
|
|
foreach ( [ 'from', 'to' ] as $prefix ) {
|
|
if ( $params["{$prefix}rev"] !== null ) {
|
|
$revId = $params["{$prefix}rev"];
|
|
$rev = Revision::newFromId( $revId );
|
|
if ( !$rev ) {
|
|
// Titles of deleted revisions aren't secret, per T51088
|
|
$row = $this->getDB()->selectRow(
|
|
'archive',
|
|
array_merge(
|
|
Revision::selectArchiveFields(),
|
|
[ 'ar_namespace', 'ar_title' ]
|
|
),
|
|
[ 'ar_rev_id' => $revId ],
|
|
__METHOD__
|
|
);
|
|
if ( $row ) {
|
|
$rev = Revision::newFromArchiveRow( $row );
|
|
}
|
|
}
|
|
if ( $rev ) {
|
|
$this->guessedTitle = $rev->getTitle();
|
|
$this->guessedModel = $rev->getContentModel();
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( $params["{$prefix}title"] !== null ) {
|
|
$title = Title::newFromText( $params["{$prefix}title"] );
|
|
if ( $title && !$title->isExternal() ) {
|
|
$this->guessedTitle = $title;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( $params["{$prefix}id"] !== null ) {
|
|
$title = Title::newFromID( $params["{$prefix}id"] );
|
|
if ( $title ) {
|
|
$this->guessedTitle = $title;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( !$this->guessedModel ) {
|
|
if ( $this->guessedTitle ) {
|
|
$this->guessedModel = $this->guessedTitle->getContentModel();
|
|
} elseif ( $params['fromcontentmodel'] !== null ) {
|
|
$this->guessedModel = $params['fromcontentmodel'];
|
|
} elseif ( $params['tocontentmodel'] !== null ) {
|
|
$this->guessedModel = $params['tocontentmodel'];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the Revision and Content for one side of the diff
|
|
*
|
|
* This uses the appropriate set of 'rev', 'id', 'title', 'text', 'pst',
|
|
* 'contentmodel', and 'contentformat' parameters to determine what content
|
|
* should be diffed.
|
|
*
|
|
* Returns three values:
|
|
* - The revision used to retrieve the content, if any
|
|
* - The content to be diffed
|
|
* - The revision specified, if any, even if not used to retrieve the
|
|
* Content
|
|
*
|
|
* @param string $prefix 'from' or 'to'
|
|
* @param array $params
|
|
* @return array [ Revision|null, Content, Revision|null ]
|
|
*/
|
|
private function getDiffContent( $prefix, array $params ) {
|
|
$title = null;
|
|
$rev = null;
|
|
$suppliedContent = $params["{$prefix}text"] !== null;
|
|
|
|
// Get the revision and title, if applicable
|
|
$revId = null;
|
|
if ( $params["{$prefix}rev"] !== null ) {
|
|
$revId = $params["{$prefix}rev"];
|
|
} elseif ( $params["{$prefix}title"] !== null || $params["{$prefix}id"] !== null ) {
|
|
if ( $params["{$prefix}title"] !== null ) {
|
|
$title = Title::newFromText( $params["{$prefix}title"] );
|
|
if ( !$title || $title->isExternal() ) {
|
|
$this->dieWithError(
|
|
[ 'apierror-invalidtitle', wfEscapeWikiText( $params["{$prefix}title"] ) ]
|
|
);
|
|
}
|
|
} else {
|
|
$title = Title::newFromID( $params["{$prefix}id"] );
|
|
if ( !$title ) {
|
|
$this->dieWithError( [ 'apierror-nosuchpageid', $params["{$prefix}id"] ] );
|
|
}
|
|
}
|
|
$revId = $title->getLatestRevID();
|
|
if ( !$revId ) {
|
|
$revId = null;
|
|
// Only die here if we're not using supplied text
|
|
if ( !$suppliedContent ) {
|
|
if ( $title->exists() ) {
|
|
$this->dieWithError(
|
|
[ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ], 'nosuchrevid'
|
|
);
|
|
} else {
|
|
$this->dieWithError(
|
|
[ 'apierror-missingtitle-byname', wfEscapeWikiText( $title->getPrefixedText() ) ],
|
|
'missingtitle'
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if ( $revId !== null ) {
|
|
$rev = Revision::newFromId( $revId );
|
|
if ( !$rev && $this->getUser()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
|
|
// Try the 'archive' table
|
|
$row = $this->getDB()->selectRow(
|
|
'archive',
|
|
array_merge(
|
|
Revision::selectArchiveFields(),
|
|
[ 'ar_namespace', 'ar_title' ]
|
|
),
|
|
[ 'ar_rev_id' => $revId ],
|
|
__METHOD__
|
|
);
|
|
if ( $row ) {
|
|
$rev = Revision::newFromArchiveRow( $row );
|
|
$rev->isArchive = true;
|
|
}
|
|
}
|
|
if ( !$rev ) {
|
|
$this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
|
|
}
|
|
$title = $rev->getTitle();
|
|
|
|
// If we don't have supplied content, return here. Otherwise,
|
|
// continue on below with the supplied content.
|
|
if ( !$suppliedContent ) {
|
|
$content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
|
|
if ( !$content ) {
|
|
$this->dieWithError( [ 'apierror-missingcontent-revid', $revId ], 'missingcontent' );
|
|
}
|
|
return [ $rev, $content, $rev ];
|
|
}
|
|
}
|
|
|
|
// Override $content based on supplied text
|
|
$model = $params["{$prefix}contentmodel"];
|
|
$format = $params["{$prefix}contentformat"];
|
|
|
|
if ( !$model && $rev ) {
|
|
$model = $rev->getContentModel();
|
|
}
|
|
if ( !$model && $title ) {
|
|
$model = $title->getContentModel();
|
|
}
|
|
if ( !$model ) {
|
|
$this->guessTitleAndModel();
|
|
$model = $this->guessedModel;
|
|
}
|
|
if ( !$model ) {
|
|
$model = CONTENT_MODEL_WIKITEXT;
|
|
$this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
|
|
}
|
|
|
|
if ( !$title ) {
|
|
$this->guessTitleAndModel();
|
|
$title = $this->guessedTitle;
|
|
}
|
|
|
|
try {
|
|
$content = ContentHandler::makeContent( $params["{$prefix}text"], $title, $model, $format );
|
|
} catch ( MWContentSerializationException $ex ) {
|
|
$this->dieWithException( $ex, [
|
|
'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
|
|
] );
|
|
}
|
|
|
|
if ( $params["{$prefix}pst"] ) {
|
|
if ( !$title ) {
|
|
$this->dieWithError( 'apierror-compare-no-title' );
|
|
}
|
|
$popts = ParserOptions::newFromContext( $this->getContext() );
|
|
$content = $content->preSaveTransform( $title, $this->getUser(), $popts );
|
|
}
|
|
|
|
return [ null, $content, $rev ];
|
|
}
|
|
|
|
/**
|
|
* Set value fields from a Revision object
|
|
* @param array &$vals Result array to set data into
|
|
* @param string $prefix 'from' or 'to'
|
|
* @param Revision|null $rev
|
|
*/
|
|
private function setVals( &$vals, $prefix, $rev ) {
|
|
if ( $rev ) {
|
|
$title = $rev->getTitle();
|
|
if ( isset( $this->props['ids'] ) ) {
|
|
$vals["{$prefix}id"] = $title->getArticleId();
|
|
$vals["{$prefix}revid"] = $rev->getId();
|
|
}
|
|
if ( isset( $this->props['title'] ) ) {
|
|
ApiQueryBase::addTitleInfo( $vals, $title, $prefix );
|
|
}
|
|
if ( isset( $this->props['size'] ) ) {
|
|
$vals["{$prefix}size"] = $rev->getSize();
|
|
}
|
|
|
|
$anyHidden = false;
|
|
if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
|
|
$vals["{$prefix}texthidden"] = true;
|
|
$anyHidden = true;
|
|
}
|
|
|
|
if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
|
|
$vals["{$prefix}userhidden"] = true;
|
|
$anyHidden = true;
|
|
}
|
|
if ( isset( $this->props['user'] ) &&
|
|
$rev->userCan( Revision::DELETED_USER, $this->getUser() )
|
|
) {
|
|
$vals["{$prefix}user"] = $rev->getUserText( Revision::RAW );
|
|
$vals["{$prefix}userid"] = $rev->getUser( Revision::RAW );
|
|
}
|
|
|
|
if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
|
|
$vals["{$prefix}commenthidden"] = true;
|
|
$anyHidden = true;
|
|
}
|
|
if ( $rev->userCan( Revision::DELETED_COMMENT, $this->getUser() ) ) {
|
|
if ( isset( $this->props['comment'] ) ) {
|
|
$vals["{$prefix}comment"] = $rev->getComment( Revision::RAW );
|
|
}
|
|
if ( isset( $this->props['parsedcomment'] ) ) {
|
|
$vals["{$prefix}parsedcomment"] = Linker::formatComment(
|
|
$rev->getComment( Revision::RAW ),
|
|
$rev->getTitle()
|
|
);
|
|
}
|
|
}
|
|
|
|
if ( $anyHidden ) {
|
|
$this->getMain()->setCacheMode( 'private' );
|
|
if ( $rev->isDeleted( Revision::DELETED_RESTRICTED ) ) {
|
|
$vals["{$prefix}suppressed"] = true;
|
|
}
|
|
}
|
|
|
|
if ( !empty( $rev->isArchive ) ) {
|
|
$this->getMain()->setCacheMode( 'private' );
|
|
$vals["{$prefix}archive"] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getAllowedParams() {
|
|
// Parameters for the 'from' and 'to' content
|
|
$fromToParams = [
|
|
'title' => null,
|
|
'id' => [
|
|
ApiBase::PARAM_TYPE => 'integer'
|
|
],
|
|
'rev' => [
|
|
ApiBase::PARAM_TYPE => 'integer'
|
|
],
|
|
'text' => [
|
|
ApiBase::PARAM_TYPE => 'text'
|
|
],
|
|
'pst' => false,
|
|
'contentformat' => [
|
|
ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
|
|
],
|
|
'contentmodel' => [
|
|
ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
|
|
]
|
|
];
|
|
|
|
$ret = [];
|
|
foreach ( $fromToParams as $k => $v ) {
|
|
$ret["from$k"] = $v;
|
|
}
|
|
foreach ( $fromToParams as $k => $v ) {
|
|
$ret["to$k"] = $v;
|
|
}
|
|
|
|
$ret = wfArrayInsertAfter(
|
|
$ret,
|
|
[ 'torelative' => [ ApiBase::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
|
|
'torev'
|
|
);
|
|
|
|
$ret['prop'] = [
|
|
ApiBase::PARAM_DFLT => 'diff|ids|title',
|
|
ApiBase::PARAM_TYPE => [
|
|
'diff',
|
|
'diffsize',
|
|
'rel',
|
|
'ids',
|
|
'title',
|
|
'user',
|
|
'comment',
|
|
'parsedcomment',
|
|
'size',
|
|
],
|
|
ApiBase::PARAM_ISMULTI => true,
|
|
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
|
|
];
|
|
|
|
return $ret;
|
|
}
|
|
|
|
protected function getExamplesMessages() {
|
|
return [
|
|
'action=compare&fromrev=1&torev=2'
|
|
=> 'apihelp-compare-example-1',
|
|
];
|
|
}
|
|
}
|