includes/api/ApiComparePages.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
*/
use MediaWiki\CommentFormatter\CommentFormatter;
use MediaWiki\Content\IContentHandlerFactory;
use MediaWiki\Content\Transform\ContentTransformer;
use MediaWiki\Revision\ArchivedRevisionLookup;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionArchiveRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\RevisionStore;
use MediaWiki\Revision\SlotRecord;
use MediaWiki\Revision\SlotRoleRegistry;
use MediaWiki\Title\Title;
use MediaWiki\User\TempUser\TempUserCreator;
use MediaWiki\User\UserFactory;
use Wikimedia\ParamValidator\ParamValidator;
use Wikimedia\RequestTimeout\TimeoutException;
/**
* @ingroup API
*/
class ApiComparePages extends ApiBase {
private RevisionStore $revisionStore;
private ArchivedRevisionLookup $archivedRevisionLookup;
private SlotRoleRegistry $slotRoleRegistry;
/** @var Title|null|false */
private $guessedTitle = false;
private $props;
private IContentHandlerFactory $contentHandlerFactory;
private ContentTransformer $contentTransformer;
private CommentFormatter $commentFormatter;
private TempUserCreator $tempUserCreator;
private UserFactory $userFactory;
private DifferenceEngine $differenceEngine;
/**
* @param ApiMain $mainModule
* @param string $moduleName
* @param RevisionStore $revisionStore
* @param ArchivedRevisionLookup $archivedRevisionLookup
* @param SlotRoleRegistry $slotRoleRegistry
* @param IContentHandlerFactory $contentHandlerFactory
* @param ContentTransformer $contentTransformer
* @param CommentFormatter $commentFormatter
* @param TempUserCreator $tempUserCreator
* @param UserFactory $userFactory
*/
public function __construct(
ApiMain $mainModule,
$moduleName,
RevisionStore $revisionStore,
ArchivedRevisionLookup $archivedRevisionLookup,
SlotRoleRegistry $slotRoleRegistry,
IContentHandlerFactory $contentHandlerFactory,
ContentTransformer $contentTransformer,
CommentFormatter $commentFormatter,
TempUserCreator $tempUserCreator,
UserFactory $userFactory
) {
parent::__construct( $mainModule, $moduleName );
$this->revisionStore = $revisionStore;
$this->archivedRevisionLookup = $archivedRevisionLookup;
$this->slotRoleRegistry = $slotRoleRegistry;
$this->contentHandlerFactory = $contentHandlerFactory;
$this->contentTransformer = $contentTransformer;
$this->commentFormatter = $commentFormatter;
$this->tempUserCreator = $tempUserCreator;
$this->userFactory = $userFactory;
$this->differenceEngine = new DifferenceEngine;
}
public function execute() {
$params = $this->extractRequestParams();
// Parameter validation
$this->requireAtLeastOneParameter(
$params, 'fromtitle', 'fromid', 'fromrev', 'fromtext', 'fromslots'
);
$this->requireAtLeastOneParameter(
$params, 'totitle', 'toid', 'torev', 'totext', 'torelative', 'toslots'
);
$this->props = array_fill_keys( $params['prop'], true );
// Cache responses publicly by default. This may be overridden later.
$this->getMain()->setCacheMode( 'public' );
// Get the 'from' RevisionRecord
[ $fromRev, $fromRelRev, $fromValsRev ] = $this->getDiffRevision( 'from', $params );
// Get the 'to' RevisionRecord
if ( $params['torelative'] !== null ) {
if ( !$fromRelRev ) {
$this->dieWithError( 'apierror-compare-relative-to-nothing' );
}
if ( $params['torelative'] !== 'cur' && $fromRelRev instanceof RevisionArchiveRecord ) {
// RevisionStore's getPreviousRevision/getNextRevision blow up
// when passed an RevisionArchiveRecord for a deleted page
$this->dieWithError( [ 'apierror-compare-relative-to-deleted', $params['torelative'] ] );
}
switch ( $params['torelative'] ) {
case 'prev':
// Swap 'from' and 'to'
[ $toRev, $toRelRev, $toValsRev ] = [ $fromRev, $fromRelRev, $fromValsRev ];
$fromRev = $this->revisionStore->getPreviousRevision( $toRelRev );
$fromRelRev = $fromRev;
$fromValsRev = $fromRev;
if ( !$fromRev ) {
$title = Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() );
$this->addWarning( [
'apiwarn-compare-no-prev',
wfEscapeWikiText( $title->getPrefixedText() ),
$toRelRev->getId()
] );
// (T203433) Create an empty dummy revision as the "previous".
// The main slot has to exist, the rest will be handled by DifferenceEngine.
$fromRev = new MutableRevisionRecord(
$title ?: $toRev->getPage()
);
$fromRev->setContent(
SlotRecord::MAIN,
$toRelRev->getContent( SlotRecord::MAIN, RevisionRecord::RAW )
->getContentHandler()
->makeEmptyContent()
);
}
break;
case 'next':
$toRev = $this->revisionStore->getNextRevision( $fromRelRev );
$toRelRev = $toRev;
$toValsRev = $toRev;
if ( !$toRev ) {
$title = Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() );
$this->addWarning( [
'apiwarn-compare-no-next',
wfEscapeWikiText( $title->getPrefixedText() ),
$fromRelRev->getId()
] );
// (T203433) The web UI treats "next" as "cur" in this case.
// Avoid repeating metadata by making a MutableRevisionRecord with no changes.
$toRev = MutableRevisionRecord::newFromParentRevision( $fromRelRev );
}
break;
case 'cur':
$title = $fromRelRev->getPageAsLinkTarget();
$toRev = $this->revisionStore->getRevisionByTitle( $title );
if ( !$toRev ) {
$title = Title::newFromLinkTarget( $title );
$this->dieWithError(
[ 'apierror-missingrev-title', wfEscapeWikiText( $title->getPrefixedText() ) ],
'nosuchrevid'
);
}
$toRelRev = $toRev;
$toValsRev = $toRev;
break;
}
} else {
[ $toRev, $toRelRev, $toValsRev ] = $this->getDiffRevision( 'to', $params );
}
// Handle missing from or to revisions (should never happen)
// @codeCoverageIgnoreStart
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
if ( !$fromRev || !$toRev ) {
$this->dieWithError( 'apierror-baddiff' );
}
// @codeCoverageIgnoreEnd
// Handle revdel
if ( !$fromRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
$this->dieWithError( [ 'apierror-missingcontent-revid', $fromRev->getId() ], 'missingcontent' );
}
if ( !$toRev->userCan( RevisionRecord::DELETED_TEXT, $this->getAuthority() ) ) {
$this->dieWithError( [ 'apierror-missingcontent-revid', $toRev->getId() ], 'missingcontent' );
}
// Get the diff
$context = new DerivativeContext( $this->getContext() );
if ( $fromRelRev && $fromRelRev->getPageAsLinkTarget() ) {
$context->setTitle( Title::newFromLinkTarget( $fromRelRev->getPageAsLinkTarget() ) );
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
} elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
$context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
} else {
$guessedTitle = $this->guessTitle();
if ( $guessedTitle ) {
$context->setTitle( $guessedTitle );
}
}
$this->differenceEngine->setContext( $context );
$this->differenceEngine->setSlotDiffOptions( [ 'diff-type' => $params['difftype'] ] );
$this->differenceEngine->setRevisions( $fromRev, $toRev );
if ( $params['slots'] === null ) {
$difftext = $this->differenceEngine->getDiffBody();
if ( $difftext === false ) {
$this->dieWithError( 'apierror-baddiff' );
}
} else {
$difftext = [];
foreach ( $params['slots'] as $role ) {
$difftext[$role] = $this->differenceEngine->getDiffBodyForRole( $role );
}
}
foreach ( $this->differenceEngine->getRevisionLoadErrors() as $msg ) {
$this->addWarning( $msg );
}
// Fill in the response
$vals = [];
$this->setVals( $vals, 'from', $fromValsRev );
// @phan-suppress-next-line PhanPossiblyUndeclaredVariable T240141
$this->setVals( $vals, 'to', $toValsRev );
if ( isset( $this->props['rel'] ) ) {
if ( !$fromRev instanceof MutableRevisionRecord ) {
$rev = $this->revisionStore->getPreviousRevision( $fromRev );
if ( $rev ) {
$vals['prev'] = $rev->getId();
}
}
if ( !$toRev instanceof MutableRevisionRecord ) {
$rev = $this->revisionStore->getNextRevision( $toRev );
if ( $rev ) {
$vals['next'] = $rev->getId();
}
}
}
if ( isset( $this->props['diffsize'] ) ) {
$vals['diffsize'] = 0;
foreach ( (array)$difftext as $text ) {
$vals['diffsize'] += strlen( $text );
}
}
if ( isset( $this->props['diff'] ) ) {
if ( is_array( $difftext ) ) {
ApiResult::setArrayType( $difftext, 'kvp', 'diff' );
$vals['bodies'] = $difftext;
} else {
ApiResult::setContentValue( $vals, 'body', $difftext );
}
}
// Diffs can be really big and there's little point in having
// ApiResult truncate it to an empty response since the diff is the
// whole reason this module exists. So pass NO_SIZE_CHECK here.
$this->getResult()->addValue( null, $this->getModuleName(), $vals, ApiResult::NO_SIZE_CHECK );
}
/**
* Load a revision by ID
*
* Falls back to checking the archive table if appropriate.
*
* @param int $id
* @return RevisionRecord|null
*/
private function getRevisionById( $id ) {
$rev = $this->revisionStore->getRevisionById( $id );
if ( !$rev && $this->getAuthority()->isAllowedAny( 'deletedtext', 'undelete' ) ) {
// Try the 'archive' table
$rev = $this->archivedRevisionLookup->getArchivedRevisionRecord( null, $id );
}
return $rev;
}
/**
* Guess an appropriate default Title for this request
*
* @return Title|null
*/
private function guessTitle() {
if ( $this->guessedTitle !== false ) {
return $this->guessedTitle;
}
$this->guessedTitle = null;
$params = $this->extractRequestParams();
foreach ( [ 'from', 'to' ] as $prefix ) {
if ( $params["{$prefix}rev"] !== null ) {
$rev = $this->getRevisionById( $params["{$prefix}rev"] );
if ( $rev ) {
$this->guessedTitle = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
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;
}
}
}
return $this->guessedTitle;
}
/**
* Guess an appropriate default content model for this request
* @param string $role Slot for which to guess the model
* @return string|null Guessed content model
*/
private function guessModel( $role ) {
$params = $this->extractRequestParams();
foreach ( [ 'from', 'to' ] as $prefix ) {
if ( $params["{$prefix}rev"] !== null ) {
$rev = $this->getRevisionById( $params["{$prefix}rev"] );
if ( $rev && $rev->hasSlot( $role ) ) {
return $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
}
}
}
$guessedTitle = $this->guessTitle();
if ( $guessedTitle ) {
return $this->slotRoleRegistry->getRoleHandler( $role )->getDefaultModel( $guessedTitle );
}
if ( isset( $params["fromcontentmodel-$role"] ) ) {
return $params["fromcontentmodel-$role"];
}
if ( isset( $params["tocontentmodel-$role"] ) ) {
return $params["tocontentmodel-$role"];
}
if ( $role === SlotRecord::MAIN ) {
if ( isset( $params['fromcontentmodel'] ) ) {
return $params['fromcontentmodel'];
}
if ( isset( $params['tocontentmodel'] ) ) {
return $params['tocontentmodel'];
}
}
return null;
}
/**
* Get the RevisionRecord for one side of the diff
*
* This uses the appropriate set of parameters to determine what content
* should be diffed.
*
* Returns three values:
* - A RevisionRecord holding the content
* - The revision specified, if any, even if content was supplied
* - The revision to pass to setVals(), if any
*
* @param string $prefix 'from' or 'to'
* @param array $params
* @return array [ RevisionRecord|null, RevisionRecord|null, RevisionRecord|null ]
*/
private function getDiffRevision( $prefix, array $params ) {
// Back compat params
$this->requireMaxOneParameter( $params, "{$prefix}text", "{$prefix}slots" );
$this->requireMaxOneParameter( $params, "{$prefix}section", "{$prefix}slots" );
if ( $params["{$prefix}text"] !== null ) {
$params["{$prefix}slots"] = [ SlotRecord::MAIN ];
$params["{$prefix}text-main"] = $params["{$prefix}text"];
$params["{$prefix}section-main"] = null;
$params["{$prefix}contentmodel-main"] = $params["{$prefix}contentmodel"];
$params["{$prefix}contentformat-main"] = $params["{$prefix}contentformat"];
}
$title = null;
$rev = null;
$suppliedContent = $params["{$prefix}slots"] !== 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 = $this->getRevisionById( $revId );
if ( !$rev ) {
$this->dieWithError( [ 'apierror-nosuchrevid', $revId ] );
}
$title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
// If we don't have supplied content, return here. Otherwise,
// continue on below with the supplied content.
if ( !$suppliedContent ) {
$newRev = $rev;
// Deprecated 'fromsection'/'tosection'
if ( isset( $params["{$prefix}section"] ) ) {
$section = $params["{$prefix}section"];
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
$newRev = MutableRevisionRecord::newFromParentRevision( $rev );
$content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
$this->getAuthority() );
if ( !$content ) {
$this->dieWithError(
[ 'apierror-missingcontent-revid-role', $rev->getId(), SlotRecord::MAIN ], 'missingcontent'
);
}
$content = $content->getSection( $section );
if ( !$content ) {
$this->dieWithError(
[ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
"nosuch{$prefix}section"
);
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
$newRev->setContent( SlotRecord::MAIN, $content );
}
return [ $newRev, $rev, $rev ];
}
}
// Override $content based on supplied text
if ( !$title ) {
$title = $this->guessTitle();
}
if ( $rev ) {
$newRev = MutableRevisionRecord::newFromParentRevision( $rev );
} else {
$newRev = new MutableRevisionRecord( $title ?: Title::newMainPage() );
}
foreach ( $params["{$prefix}slots"] as $role ) {
$text = $params["{$prefix}text-{$role}"];
if ( $text === null ) {
// The SlotRecord::MAIN role can't be deleted
if ( $role === SlotRecord::MAIN ) {
$this->dieWithError( [ 'apierror-compare-maintextrequired', $prefix ] );
}
// These parameters make no sense without text. Reject them to avoid
// confusion.
foreach ( [ 'section', 'contentmodel', 'contentformat' ] as $param ) {
if ( isset( $params["{$prefix}{$param}-{$role}"] ) ) {
$this->dieWithError( [
'apierror-compare-notext',
wfEscapeWikiText( "{$prefix}{$param}-{$role}" ),
wfEscapeWikiText( "{$prefix}text-{$role}" ),
] );
}
}
$newRev->removeSlot( $role );
continue;
}
$model = $params["{$prefix}contentmodel-{$role}"];
$format = $params["{$prefix}contentformat-{$role}"];
if ( !$model && $rev && $rev->hasSlot( $role ) ) {
$model = $rev->getSlot( $role, RevisionRecord::RAW )->getModel();
}
if ( !$model && $title && $role === SlotRecord::MAIN ) {
// @todo: Use SlotRoleRegistry and do this for all slots
$model = $title->getContentModel();
}
if ( !$model ) {
$model = $this->guessModel( $role );
}
if ( !$model ) {
$model = CONTENT_MODEL_WIKITEXT;
$this->addWarning( [ 'apiwarn-compare-nocontentmodel', $model ] );
}
try {
$content = $this->contentHandlerFactory
->getContentHandler( $model )
->unserializeContent( $text, $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 = $this->contentTransformer->preSaveTransform(
$content,
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
$title,
$this->getUserForPreview(),
$popts
);
}
$section = $params["{$prefix}section-{$role}"];
if ( $section !== null && $section !== '' ) {
if ( !$rev ) {
$this->dieWithError( "apierror-compare-no{$prefix}revision" );
}
$oldContent = $rev->getContent( $role, RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
if ( !$oldContent ) {
$this->dieWithError(
[ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
'missingcontent'
);
}
if ( !$oldContent->getContentHandler()->supportsSections() ) {
$this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
}
try {
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
$content = $oldContent->replaceSection( $section, $content, '' );
} catch ( TimeoutException $e ) {
throw $e;
} catch ( Exception $ex ) {
// Probably a content model mismatch.
$content = null;
}
if ( !$content ) {
$this->dieWithError( [ 'apierror-sectionreplacefailed' ] );
}
}
// Deprecated 'fromsection'/'tosection'
if ( $role === SlotRecord::MAIN && isset( $params["{$prefix}section"] ) ) {
$section = $params["{$prefix}section"];
$content = $content->getSection( $section );
if ( !$content ) {
$this->dieWithError(
[ "apierror-compare-nosuch{$prefix}section", wfEscapeWikiText( $section ) ],
"nosuch{$prefix}section"
);
}
}
// @phan-suppress-next-line PhanTypeMismatchArgumentNullable T240141
$newRev->setContent( $role, $content );
}
return [ $newRev, $rev, null ];
}
/**
* Set value fields from a RevisionRecord object
*
* @param array &$vals Result array to set data into
* @param string $prefix 'from' or 'to'
* @param RevisionRecord|null $rev
*/
private function setVals( &$vals, $prefix, $rev ) {
if ( $rev ) {
$title = Title::newFromLinkTarget( $rev->getPageAsLinkTarget() );
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();
}
if ( isset( $this->props['timestamp'] ) ) {
$revTimestamp = $rev->getTimestamp();
if ( $revTimestamp ) {
$vals["{$prefix}timestamp"] = wfTimestamp( TS_ISO_8601, $revTimestamp );
}
}
$anyHidden = false;
if ( $rev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
$vals["{$prefix}texthidden"] = true;
$anyHidden = true;
}
if ( $rev->isDeleted( RevisionRecord::DELETED_USER ) ) {
$vals["{$prefix}userhidden"] = true;
$anyHidden = true;
}
if ( isset( $this->props['user'] ) ) {
$user = $rev->getUser( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
if ( $user ) {
$vals["{$prefix}user"] = $user->getName();
$vals["{$prefix}userid"] = $user->getId();
}
}
if ( $rev->isDeleted( RevisionRecord::DELETED_COMMENT ) ) {
$vals["{$prefix}commenthidden"] = true;
$anyHidden = true;
}
if ( isset( $this->props['comment'] ) || isset( $this->props['parsedcomment'] ) ) {
$comment = $rev->getComment( RevisionRecord::FOR_THIS_USER, $this->getAuthority() );
if ( $comment !== null ) {
if ( isset( $this->props['comment'] ) ) {
$vals["{$prefix}comment"] = $comment->text;
}
$vals["{$prefix}parsedcomment"] = $this->commentFormatter->format(
$comment->text, $title
);
}
}
if ( $anyHidden ) {
$this->getMain()->setCacheMode( 'private' );
if ( $rev->isDeleted( RevisionRecord::DELETED_RESTRICTED ) ) {
$vals["{$prefix}suppressed"] = true;
}
}
if ( $rev instanceof RevisionArchiveRecord ) {
$this->getMain()->setCacheMode( 'private' );
$vals["{$prefix}archive"] = true;
}
}
}
private function getUserForPreview() {
$user = $this->getUser();
if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
return $this->userFactory->newUnsavedTempUser(
$this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
);
}
return $user;
}
public function getAllowedParams() {
$slotRoles = $this->slotRoleRegistry->getKnownRoles();
sort( $slotRoles, SORT_STRING );
// Parameters for the 'from' and 'to' content
$fromToParams = [
'title' => null,
'id' => [
ParamValidator::PARAM_TYPE => 'integer'
],
'rev' => [
ParamValidator::PARAM_TYPE => 'integer'
],
'slots' => [
ParamValidator::PARAM_TYPE => $slotRoles,
ParamValidator::PARAM_ISMULTI => true,
],
'text-{slot}' => [
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
ParamValidator::PARAM_TYPE => 'text',
],
'section-{slot}' => [
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
ParamValidator::PARAM_TYPE => 'string',
],
'contentformat-{slot}' => [
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
],
'contentmodel-{slot}' => [
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
],
'pst' => false,
'text' => [
ParamValidator::PARAM_TYPE => 'text',
ParamValidator::PARAM_DEPRECATED => true,
],
'contentformat' => [
ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
ParamValidator::PARAM_DEPRECATED => true,
],
'contentmodel' => [
ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
ParamValidator::PARAM_DEPRECATED => true,
],
'section' => [
ParamValidator::PARAM_DEFAULT => null,
ParamValidator::PARAM_DEPRECATED => true,
],
];
$ret = [];
foreach ( $fromToParams as $k => $v ) {
if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
$v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'fromslots';
}
$ret["from$k"] = $v;
}
foreach ( $fromToParams as $k => $v ) {
if ( isset( $v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] ) ) {
$v[ApiBase::PARAM_TEMPLATE_VARS]['slot'] = 'toslots';
}
$ret["to$k"] = $v;
}
$ret = wfArrayInsertAfter(
$ret,
[ 'torelative' => [ ParamValidator::PARAM_TYPE => [ 'prev', 'next', 'cur' ], ] ],
'torev'
);
$ret['prop'] = [
ParamValidator::PARAM_DEFAULT => 'diff|ids|title',
ParamValidator::PARAM_TYPE => [
'diff',
'diffsize',
'rel',
'ids',
'title',
'user',
'comment',
'parsedcomment',
'size',
'timestamp',
],
ParamValidator::PARAM_ISMULTI => true,
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
];
$ret['slots'] = [
ParamValidator::PARAM_TYPE => $slotRoles,
ParamValidator::PARAM_ISMULTI => true,
ParamValidator::PARAM_ALL => true,
];
$ret['difftype'] = [
ParamValidator::PARAM_TYPE => $this->differenceEngine->getSupportedFormats(),
ParamValidator::PARAM_DEFAULT => 'table',
];
return $ret;
}
protected function getExamplesMessages() {
return [
'action=compare&fromrev=1&torev=2'
=> 'apihelp-compare-example-1',
];
}
public function getHelpUrls() {
return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Compare';
}
}