Don't catch and discard exceptions from the RequestTimeout library, except when the exception is properly handled and the code seems to be trying to wrap things up. In most cases the exception is rethrown. Ideally it should instead be done by narrowing the catch, and this was feasible in a few cases. But sometimes the exception being caught is an instance of the base class (notably DateTime::__construct()). Often Exception is the root of the hierarchy of exceptions being thrown and so is the obvious catch-all. Notes on specific callers: * In the case of ResourceLoader::respond(), exceptions were caught for API correctness, but processing continued. I added an outer try block for timeout handling so that termination would be more prompt. * In LCStoreCDB the Exception being caught was Cdb\Exception not \Exception. I added an alias to avoid confusion. * In ImageGallery I added a special exception class. * In Message::__toString() the rationale for catching disappears in PHP 7.4.0+, so I added a PHP version check. * In PoolCounterRedis, let the shutdown function do its thing, but rethrow the exception for logging. Change-Id: I4c3770b9efc76a1ce42ed9f59329c36de04d657c
780 lines
24 KiB
PHP
780 lines
24 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
|
|
*/
|
|
|
|
use MediaWiki\CommentFormatter\CommentFormatter;
|
|
use MediaWiki\Content\IContentHandlerFactory;
|
|
use MediaWiki\Content\Transform\ContentTransformer;
|
|
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 Wikimedia\RequestTimeout\TimeoutException;
|
|
|
|
/**
|
|
* @ingroup API
|
|
*/
|
|
class ApiComparePages extends ApiBase {
|
|
|
|
/** @var RevisionStore */
|
|
private $revisionStore;
|
|
|
|
/** @var SlotRoleRegistry */
|
|
private $slotRoleRegistry;
|
|
|
|
/** @var Title|false */
|
|
private $guessedTitle = false;
|
|
private $props;
|
|
|
|
/** @var IContentHandlerFactory */
|
|
private $contentHandlerFactory;
|
|
|
|
/** @var ContentTransformer */
|
|
private $contentTransformer;
|
|
|
|
/** @var CommentFormatter */
|
|
private $commentFormatter;
|
|
|
|
/**
|
|
* @param ApiMain $mainModule
|
|
* @param string $moduleName
|
|
* @param RevisionStore $revisionStore
|
|
* @param SlotRoleRegistry $slotRoleRegistry
|
|
* @param IContentHandlerFactory $contentHandlerFactory
|
|
* @param ContentTransformer $contentTransformer
|
|
* @param CommentFormatter $commentFormatter
|
|
*/
|
|
public function __construct(
|
|
ApiMain $mainModule,
|
|
$moduleName,
|
|
RevisionStore $revisionStore,
|
|
SlotRoleRegistry $slotRoleRegistry,
|
|
IContentHandlerFactory $contentHandlerFactory,
|
|
ContentTransformer $contentTransformer,
|
|
CommentFormatter $commentFormatter
|
|
) {
|
|
parent::__construct( $mainModule, $moduleName );
|
|
$this->revisionStore = $revisionStore;
|
|
$this->slotRoleRegistry = $slotRoleRegistry;
|
|
$this->contentHandlerFactory = $contentHandlerFactory;
|
|
$this->contentTransformer = $contentTransformer;
|
|
$this->commentFormatter = $commentFormatter;
|
|
}
|
|
|
|
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
|
|
list( $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'
|
|
list( $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 {
|
|
list( $toRev, $toRelRev, $toValsRev ) = $this->getDiffRevision( 'to', $params );
|
|
}
|
|
|
|
// Handle missing from or to revisions (should never happen)
|
|
// @codeCoverageIgnoreStart
|
|
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() ) );
|
|
} elseif ( $toRelRev && $toRelRev->getPageAsLinkTarget() ) {
|
|
$context->setTitle( Title::newFromLinkTarget( $toRelRev->getPageAsLinkTarget() ) );
|
|
} else {
|
|
$guessedTitle = $this->guessTitle();
|
|
if ( $guessedTitle ) {
|
|
$context->setTitle( $guessedTitle );
|
|
}
|
|
}
|
|
$de = new DifferenceEngine( $context );
|
|
$de->setRevisions( $fromRev, $toRev );
|
|
if ( $params['slots'] === null ) {
|
|
$difftext = $de->getDiffBody();
|
|
if ( $difftext === false ) {
|
|
$this->dieWithError( 'apierror-baddiff' );
|
|
}
|
|
} else {
|
|
$difftext = [];
|
|
foreach ( $params['slots'] as $role ) {
|
|
$difftext[$role] = $de->getDiffBodyForRole( $role );
|
|
}
|
|
}
|
|
|
|
// Fill in the response
|
|
$vals = [];
|
|
$this->setVals( $vals, 'from', $fromValsRev );
|
|
$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
|
|
$arQuery = $this->revisionStore->getArchiveQueryInfo();
|
|
$row = $this->getDB()->selectRow(
|
|
$arQuery['tables'],
|
|
array_merge(
|
|
$arQuery['fields'],
|
|
[ 'ar_namespace', 'ar_title' ]
|
|
),
|
|
[ 'ar_rev_id' => $id ],
|
|
__METHOD__,
|
|
[],
|
|
$arQuery['joins']
|
|
);
|
|
if ( $row ) {
|
|
$rev = $this->revisionStore->newRevisionFromArchiveRow( $row );
|
|
// @phan-suppress-next-line PhanUndeclaredProperty
|
|
$rev->isArchive = true;
|
|
}
|
|
}
|
|
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();
|
|
|
|
$title = null;
|
|
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"];
|
|
$newRev = MutableRevisionRecord::newFromParentRevision( $rev );
|
|
$content = $rev->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER,
|
|
$this->getUser() );
|
|
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"
|
|
);
|
|
}
|
|
$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 = ContentHandler::makeContent( $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 = $this->contentTransformer->preSaveTransform(
|
|
$content,
|
|
$title,
|
|
$this->getUser(),
|
|
$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->getUser() );
|
|
if ( !$oldContent ) {
|
|
$this->dieWithError(
|
|
[ 'apierror-missingcontent-revid-role', $rev->getId(), wfEscapeWikiText( $role ) ],
|
|
'missingcontent'
|
|
);
|
|
}
|
|
if ( !$oldContent->getContentHandler()->supportsSections() ) {
|
|
$this->dieWithError( [ 'apierror-sectionsnotsupported', $content->getModel() ] );
|
|
}
|
|
try {
|
|
$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"
|
|
);
|
|
}
|
|
}
|
|
|
|
$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->getUser() );
|
|
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->getUser() );
|
|
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;
|
|
}
|
|
}
|
|
|
|
// @phan-suppress-next-line PhanUndeclaredProperty
|
|
if ( !empty( $rev->isArchive ) ) {
|
|
$this->getMain()->setCacheMode( 'private' );
|
|
$vals["{$prefix}archive"] = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
public function getAllowedParams() {
|
|
$slotRoles = $this->slotRoleRegistry->getKnownRoles();
|
|
sort( $slotRoles, SORT_STRING );
|
|
|
|
// Parameters for the 'from' and 'to' content
|
|
$fromToParams = [
|
|
'title' => null,
|
|
'id' => [
|
|
ApiBase::PARAM_TYPE => 'integer'
|
|
],
|
|
'rev' => [
|
|
ApiBase::PARAM_TYPE => 'integer'
|
|
],
|
|
|
|
'slots' => [
|
|
ApiBase::PARAM_TYPE => $slotRoles,
|
|
ApiBase::PARAM_ISMULTI => true,
|
|
],
|
|
'text-{slot}' => [
|
|
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
|
|
ApiBase::PARAM_TYPE => 'text',
|
|
],
|
|
'section-{slot}' => [
|
|
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
|
|
ApiBase::PARAM_TYPE => 'string',
|
|
],
|
|
'contentformat-{slot}' => [
|
|
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
|
|
ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
|
|
],
|
|
'contentmodel-{slot}' => [
|
|
ApiBase::PARAM_TEMPLATE_VARS => [ 'slot' => 'slots' ], // fixed below
|
|
ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
|
|
],
|
|
'pst' => false,
|
|
|
|
'text' => [
|
|
ApiBase::PARAM_TYPE => 'text',
|
|
ApiBase::PARAM_DEPRECATED => true,
|
|
],
|
|
'contentformat' => [
|
|
ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
|
|
ApiBase::PARAM_DEPRECATED => true,
|
|
],
|
|
'contentmodel' => [
|
|
ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
|
|
ApiBase::PARAM_DEPRECATED => true,
|
|
],
|
|
'section' => [
|
|
ApiBase::PARAM_DFLT => null,
|
|
ApiBase::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' => [ 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',
|
|
'timestamp',
|
|
],
|
|
ApiBase::PARAM_ISMULTI => true,
|
|
ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
|
|
];
|
|
|
|
$ret['slots'] = [
|
|
ApiBase::PARAM_TYPE => $slotRoles,
|
|
ApiBase::PARAM_ISMULTI => true,
|
|
ApiBase::PARAM_ALL => true,
|
|
];
|
|
|
|
return $ret;
|
|
}
|
|
|
|
protected function getExamplesMessages() {
|
|
return [
|
|
'action=compare&fromrev=1&torev=2'
|
|
=> 'apihelp-compare-example-1',
|
|
];
|
|
}
|
|
}
|