2013-07-11 17:09:28 +00:00
|
|
|
<?php
|
|
|
|
/**
|
2015-09-14 16:13:31 +00:00
|
|
|
* Parsoid/RESTBase+MediaWiki API wrapper.
|
2013-07-11 17:09:28 +00:00
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
2021-04-22 19:27:47 +00:00
|
|
|
* @copyright 2011-2021 VisualEditor Team and others; see AUTHORS.txt
|
2018-03-28 19:47:04 +00:00
|
|
|
* @license MIT
|
2013-07-11 17:09:28 +00:00
|
|
|
*/
|
|
|
|
|
2022-03-13 01:38:23 +00:00
|
|
|
namespace MediaWiki\Extension\VisualEditor;
|
|
|
|
|
|
|
|
use ApiBase;
|
|
|
|
use ApiMain;
|
|
|
|
use BagOStuff;
|
|
|
|
use ContentHandler;
|
|
|
|
use Deflate;
|
|
|
|
use DerivativeContext;
|
|
|
|
use DifferenceEngine;
|
|
|
|
use ExtensionRegistry;
|
|
|
|
use FlaggablePageView;
|
|
|
|
use IBufferingStatsdDataFactory;
|
2023-06-03 01:19:38 +00:00
|
|
|
use IDBAccessObject;
|
2024-01-30 22:36:55 +00:00
|
|
|
use MediaWiki\HookContainer\HookContainer;
|
2019-10-11 19:42:23 +00:00
|
|
|
use MediaWiki\Logger\LoggerFactory;
|
2022-06-24 20:50:56 +00:00
|
|
|
use MediaWiki\Page\WikiPageFactory;
|
2023-08-19 23:40:59 +00:00
|
|
|
use MediaWiki\Request\DerivativeRequest;
|
2023-11-15 16:28:37 +00:00
|
|
|
use MediaWiki\Revision\SlotRecord;
|
2023-08-15 18:39:10 +00:00
|
|
|
use MediaWiki\SpecialPage\SpecialPageFactory;
|
2021-08-10 15:57:00 +00:00
|
|
|
use MediaWiki\Storage\PageEditStash;
|
2023-08-19 04:21:24 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2021-01-07 15:57:57 +00:00
|
|
|
use MediaWiki\User\UserIdentity;
|
2022-03-13 01:38:23 +00:00
|
|
|
use ObjectCache;
|
|
|
|
use RequestContext;
|
|
|
|
use Sanitizer;
|
|
|
|
use SkinFactory;
|
2020-07-20 20:43:59 +00:00
|
|
|
use Wikimedia\ParamValidator\ParamValidator;
|
2016-09-08 04:28:59 +00:00
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
class ApiVisualEditorEdit extends ApiBase {
|
|
|
|
use ApiParsoidTrait;
|
|
|
|
|
2020-09-09 14:59:39 +00:00
|
|
|
private const MAX_CACHE_RECENT = 2;
|
|
|
|
private const MAX_CACHE_TTL = 900;
|
2013-07-11 17:09:28 +00:00
|
|
|
|
2023-04-24 19:10:30 +00:00
|
|
|
private VisualEditorHookRunner $hookRunner;
|
|
|
|
private PageEditStash $pageEditStash;
|
|
|
|
private SkinFactory $skinFactory;
|
|
|
|
private WikiPageFactory $wikiPageFactory;
|
2023-08-15 18:39:10 +00:00
|
|
|
private SpecialPageFactory $specialPageFactory;
|
2023-04-24 19:10:30 +00:00
|
|
|
private VisualEditorParsoidClientFactory $parsoidClientFactory;
|
2021-04-22 19:27:47 +00:00
|
|
|
|
2021-08-10 15:57:00 +00:00
|
|
|
public function __construct(
|
|
|
|
ApiMain $main,
|
|
|
|
string $name,
|
2024-01-30 22:36:55 +00:00
|
|
|
HookContainer $hookContainer,
|
2021-08-10 15:57:00 +00:00
|
|
|
IBufferingStatsdDataFactory $statsdDataFactory,
|
2022-02-18 15:52:47 +00:00
|
|
|
PageEditStash $pageEditStash,
|
2022-06-24 20:50:56 +00:00
|
|
|
SkinFactory $skinFactory,
|
2022-09-05 10:36:16 +00:00
|
|
|
WikiPageFactory $wikiPageFactory,
|
2023-08-15 18:39:10 +00:00
|
|
|
SpecialPageFactory $specialPageFactory,
|
2022-09-05 10:36:16 +00:00
|
|
|
VisualEditorParsoidClientFactory $parsoidClientFactory
|
2021-08-10 15:57:00 +00:00
|
|
|
) {
|
2020-05-12 16:53:31 +00:00
|
|
|
parent::__construct( $main, $name );
|
2020-03-16 23:30:26 +00:00
|
|
|
$this->setLogger( LoggerFactory::getInstance( 'VisualEditor' ) );
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->setStats( $statsdDataFactory );
|
2024-01-30 22:36:55 +00:00
|
|
|
$this->hookRunner = new VisualEditorHookRunner( $hookContainer );
|
2021-08-10 15:57:00 +00:00
|
|
|
$this->pageEditStash = $pageEditStash;
|
2022-02-18 15:52:47 +00:00
|
|
|
$this->skinFactory = $skinFactory;
|
2022-06-24 20:50:56 +00:00
|
|
|
$this->wikiPageFactory = $wikiPageFactory;
|
2023-08-15 18:39:10 +00:00
|
|
|
$this->specialPageFactory = $specialPageFactory;
|
2022-09-05 10:36:16 +00:00
|
|
|
$this->parsoidClientFactory = $parsoidClientFactory;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
protected function getParsoidClient(): ParsoidClient {
|
|
|
|
return $this->parsoidClientFactory->createParsoidClient(
|
|
|
|
$this->getRequest()->getHeader( 'Cookie' )
|
|
|
|
);
|
2014-08-13 08:15:42 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Attempt to save a given page's wikitext to MediaWiki's storage layer via its API
|
|
|
|
*
|
2018-06-26 15:35:09 +00:00
|
|
|
* @param Title $title The title of the page to write
|
2018-03-28 19:47:04 +00:00
|
|
|
* @param string $wikitext The wikitext to write
|
|
|
|
* @param array $params The edit parameters
|
2019-03-01 22:49:26 +00:00
|
|
|
* @return mixed The result of the save attempt
|
2018-03-28 19:47:04 +00:00
|
|
|
*/
|
2018-06-26 15:35:09 +00:00
|
|
|
protected function saveWikitext( Title $title, $wikitext, $params ) {
|
2016-02-17 16:18:02 +00:00
|
|
|
$apiParams = [
|
2013-07-11 17:09:28 +00:00
|
|
|
'action' => 'edit',
|
|
|
|
'title' => $title->getPrefixedDBkey(),
|
|
|
|
'text' => $wikitext,
|
|
|
|
'summary' => $params['summary'],
|
|
|
|
'basetimestamp' => $params['basetimestamp'],
|
|
|
|
'starttimestamp' => $params['starttimestamp'],
|
|
|
|
'token' => $params['token'],
|
2020-02-29 17:35:29 +00:00
|
|
|
'watchlist' => $params['watchlist'],
|
2023-08-02 15:52:11 +00:00
|
|
|
// NOTE: Must use getText() to work; PHP array from $params['tags'] is not understood
|
|
|
|
// by the edit API.
|
|
|
|
'tags' => $this->getRequest()->getText( 'tags' ),
|
2020-02-29 17:38:12 +00:00
|
|
|
'section' => $params['section'],
|
|
|
|
'sectiontitle' => $params['sectiontitle'],
|
|
|
|
'captchaid' => $params['captchaid'],
|
|
|
|
'captchaword' => $params['captchaword'],
|
2023-06-15 00:06:42 +00:00
|
|
|
'returnto' => $params['returnto'],
|
|
|
|
'returntoquery' => $params['returntoquery'],
|
|
|
|
'returntoanchor' => $params['returntoanchor'],
|
2017-01-12 00:30:01 +00:00
|
|
|
'errorformat' => 'html',
|
2021-08-26 09:37:13 +00:00
|
|
|
( $params['minor'] !== null ? 'minor' : 'notminor' ) => true,
|
2016-02-17 16:18:02 +00:00
|
|
|
];
|
2013-07-11 17:09:28 +00:00
|
|
|
|
2020-02-29 17:38:12 +00:00
|
|
|
// Pass any unrecognized query parameters to the internal action=edit API request. This is
|
|
|
|
// necessary to support extensions that add extra stuff to the edit form (e.g. FlaggedRevs)
|
|
|
|
// and allows passing any other query parameters to be used for edit tagging (e.g. T209132).
|
|
|
|
// Exclude other known params from here and ApiMain.
|
|
|
|
// TODO: This doesn't exclude params from the formatter
|
|
|
|
$allParams = $this->getRequest()->getValues();
|
|
|
|
$knownParams = array_keys( $this->getAllowedParams() + $this->getMain()->getAllowedParams() );
|
|
|
|
foreach ( $knownParams as $knownParam ) {
|
|
|
|
unset( $allParams[ $knownParam ] );
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
|
2021-04-28 23:58:28 +00:00
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setRequest(
|
2013-07-11 17:09:28 +00:00
|
|
|
new DerivativeRequest(
|
2021-04-28 23:58:28 +00:00
|
|
|
$context->getRequest(),
|
2020-02-29 17:38:12 +00:00
|
|
|
$apiParams + $allParams,
|
2017-05-04 22:27:27 +00:00
|
|
|
/* was posted? */ true
|
2021-04-28 23:58:28 +00:00
|
|
|
)
|
|
|
|
);
|
|
|
|
$api = new ApiMain(
|
|
|
|
$context,
|
2017-05-04 22:27:27 +00:00
|
|
|
/* enable write? */ true
|
2013-07-11 17:09:28 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
$api->execute();
|
|
|
|
|
2016-09-20 19:28:15 +00:00
|
|
|
return $api->getResult()->getResultData();
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Load into an array the output of MediaWiki's parser for a given revision
|
|
|
|
*
|
|
|
|
* @param int $newRevId The revision to load
|
2022-02-18 15:52:47 +00:00
|
|
|
* @param array $params Original request params
|
2023-06-02 21:23:06 +00:00
|
|
|
* @return array Some properties haphazardly extracted from an action=parse API response
|
2018-03-28 19:47:04 +00:00
|
|
|
*/
|
2022-02-18 15:52:47 +00:00
|
|
|
protected function parseWikitext( $newRevId, array $params ) {
|
2016-02-17 16:18:02 +00:00
|
|
|
$apiParams = [
|
2015-03-13 20:10:49 +00:00
|
|
|
'action' => 'parse',
|
2015-04-08 20:22:48 +00:00
|
|
|
'oldid' => $newRevId,
|
2022-06-03 01:37:55 +00:00
|
|
|
'prop' => 'text|revid|categorieshtml|sections|displaytitle|subtitle|modules|jsconfigvars',
|
2022-02-18 15:52:47 +00:00
|
|
|
'useskin' => $params['useskin'],
|
2016-02-17 16:18:02 +00:00
|
|
|
];
|
2022-03-08 13:00:15 +00:00
|
|
|
// Boolean parameters must be omitted completely to be treated as false.
|
|
|
|
// Param is added by hook in MobileFrontend, so it may be unset.
|
|
|
|
if ( isset( $params['mobileformat'] ) && $params['mobileformat'] ) {
|
|
|
|
$apiParams['mobileformat'] = '1';
|
|
|
|
}
|
2021-04-28 23:58:28 +00:00
|
|
|
|
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setRequest(
|
2015-03-13 20:10:49 +00:00
|
|
|
new DerivativeRequest(
|
2021-04-28 23:58:28 +00:00
|
|
|
$context->getRequest(),
|
2015-03-13 20:10:49 +00:00
|
|
|
$apiParams,
|
2021-04-28 23:58:28 +00:00
|
|
|
/* was posted? */ true
|
|
|
|
)
|
|
|
|
);
|
|
|
|
$api = new ApiMain(
|
|
|
|
$context,
|
2017-05-04 22:27:27 +00:00
|
|
|
/* enable write? */ true
|
2015-03-13 20:10:49 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
$api->execute();
|
2016-09-20 19:28:15 +00:00
|
|
|
$result = $api->getResult()->getResultData( null, [
|
2017-05-04 22:27:27 +00:00
|
|
|
/* Transform content nodes to '*' */ 'BC' => [],
|
|
|
|
/* Add back-compat subelements */ 'Types' => [],
|
|
|
|
/* Remove any metadata keys from the links array */ 'Strip' => 'all',
|
2016-09-20 19:28:15 +00:00
|
|
|
] );
|
2019-03-12 20:27:47 +00:00
|
|
|
$content = $result['parse']['text']['*'] ?? false;
|
|
|
|
$categorieshtml = $result['parse']['categorieshtml']['*'] ?? false;
|
2022-07-27 22:38:27 +00:00
|
|
|
$sections = isset( $result['parse']['showtoc'] ) ? $result['parse']['sections'] : [];
|
2019-03-12 20:27:47 +00:00
|
|
|
$displaytitle = $result['parse']['displaytitle'] ?? false;
|
2020-10-28 21:12:01 +00:00
|
|
|
$subtitle = $result['parse']['subtitle'] ?? false;
|
2017-02-14 20:39:34 +00:00
|
|
|
$modules = array_merge(
|
2019-03-12 20:27:47 +00:00
|
|
|
$result['parse']['modules'] ?? [],
|
|
|
|
$result['parse']['modulestyles'] ?? []
|
2017-02-14 20:39:34 +00:00
|
|
|
);
|
2019-03-12 20:27:47 +00:00
|
|
|
$jsconfigvars = $result['parse']['jsconfigvars'] ?? [];
|
2015-03-13 20:10:49 +00:00
|
|
|
|
|
|
|
if ( $displaytitle !== false ) {
|
|
|
|
// Escape entities as in OutputPage::setPageTitle()
|
2022-03-04 19:29:47 +00:00
|
|
|
$displaytitle = Sanitizer::removeSomeTags( $displaytitle );
|
2015-03-13 20:10:49 +00:00
|
|
|
}
|
|
|
|
|
2016-02-17 16:18:02 +00:00
|
|
|
return [
|
2015-03-13 20:10:49 +00:00
|
|
|
'content' => $content,
|
|
|
|
'categorieshtml' => $categorieshtml,
|
2022-06-03 01:37:55 +00:00
|
|
|
'sections' => $sections,
|
2015-07-27 19:27:03 +00:00
|
|
|
'displayTitleHtml' => $displaytitle,
|
2020-10-28 21:12:01 +00:00
|
|
|
'contentSub' => $subtitle,
|
2015-07-27 19:27:03 +00:00
|
|
|
'modules' => $modules,
|
|
|
|
'jsconfigvars' => $jsconfigvars
|
2016-02-17 16:18:02 +00:00
|
|
|
];
|
2015-03-13 20:10:49 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Create and load the parsed wikitext of an edit, or from the serialisation cache if available.
|
|
|
|
*
|
2018-06-26 15:35:09 +00:00
|
|
|
* @param Title $title The title of the page
|
2018-03-28 19:47:04 +00:00
|
|
|
* @param array $params The edit parameters
|
|
|
|
* @param array $parserParams The parser parameters
|
|
|
|
* @return string The wikitext of the edit
|
|
|
|
*/
|
2018-06-26 15:35:09 +00:00
|
|
|
protected function getWikitext( Title $title, $params, $parserParams ) {
|
2016-10-31 17:45:49 +00:00
|
|
|
if ( $params['cachekey'] !== null ) {
|
|
|
|
$wikitext = $this->trySerializationCache( $params['cachekey'] );
|
|
|
|
if ( !is_string( $wikitext ) ) {
|
2016-11-03 19:16:57 +00:00
|
|
|
$this->dieWithError( 'apierror-visualeditor-badcachekey', 'badcachekey' );
|
2016-10-31 17:45:49 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
$wikitext = $this->getWikitextNoCache( $title, $params, $parserParams );
|
|
|
|
}
|
2019-12-15 04:30:43 +00:00
|
|
|
'@phan-var string $wikitext';
|
2016-10-31 17:45:49 +00:00
|
|
|
return $wikitext;
|
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Create and load the parsed wikitext of an edit, ignoring the serialisation cache.
|
|
|
|
*
|
2018-06-26 15:35:09 +00:00
|
|
|
* @param Title $title The title of the page
|
2018-03-28 19:47:04 +00:00
|
|
|
* @param array $params The edit parameters
|
|
|
|
* @param array $parserParams The parser parameters
|
|
|
|
* @return string The wikitext of the edit
|
|
|
|
*/
|
2018-06-26 15:35:09 +00:00
|
|
|
protected function getWikitextNoCache( Title $title, $params, $parserParams ) {
|
2016-10-31 17:45:49 +00:00
|
|
|
$this->requireOnlyOneParameter( $params, 'html' );
|
2019-10-30 19:13:47 +00:00
|
|
|
if ( Deflate::isDeflated( $params['html'] ) ) {
|
|
|
|
$status = Deflate::inflate( $params['html'] );
|
2018-08-02 19:38:55 +00:00
|
|
|
if ( !$status->isGood() ) {
|
2019-10-30 19:13:47 +00:00
|
|
|
$this->dieWithError( 'deflate-invaliddeflate', 'invaliddeflate' );
|
2018-08-02 19:38:55 +00:00
|
|
|
}
|
|
|
|
$html = $status->getValue();
|
|
|
|
} else {
|
|
|
|
$html = $params['html'];
|
2018-06-10 14:57:42 +00:00
|
|
|
}
|
2020-07-22 13:38:33 +00:00
|
|
|
$wikitext = $this->transformHTML(
|
2020-07-22 13:35:59 +00:00
|
|
|
$title, $html, $parserParams['oldid'] ?? null, $params['etag'] ?? null
|
|
|
|
)['body'];
|
2016-10-31 17:45:49 +00:00
|
|
|
return $wikitext;
|
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Load the parsed wikitext of an edit into the serialisation cache.
|
|
|
|
*
|
2018-06-26 15:35:09 +00:00
|
|
|
* @param Title $title The title of the page
|
2018-03-28 19:47:04 +00:00
|
|
|
* @param string $wikitext The wikitext of the edit
|
2019-12-15 04:30:43 +00:00
|
|
|
* @return string|false The key of the wikitext in the serialisation cache
|
2018-03-28 19:47:04 +00:00
|
|
|
*/
|
2018-06-26 15:35:09 +00:00
|
|
|
protected function storeInSerializationCache( Title $title, $wikitext ) {
|
2016-09-08 04:28:59 +00:00
|
|
|
if ( $wikitext === false ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2019-03-27 19:04:57 +00:00
|
|
|
$cache = ObjectCache::getLocalClusterInstance();
|
2019-04-23 19:45:17 +00:00
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
// Store the corresponding wikitext, referenceable by a new key
|
|
|
|
$hash = md5( $wikitext );
|
2019-03-27 19:04:57 +00:00
|
|
|
$key = $cache->makeKey( 'visualeditor', 'serialization', $hash );
|
2019-03-28 17:42:45 +00:00
|
|
|
$ok = $cache->set( $key, $wikitext, self::MAX_CACHE_TTL );
|
|
|
|
if ( $ok ) {
|
|
|
|
$this->pruneExcessStashedEntries( $cache, $this->getUser(), $key );
|
|
|
|
}
|
|
|
|
|
2019-03-27 19:04:57 +00:00
|
|
|
$status = $ok ? 'ok' : 'failed';
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->getStats()->increment( "editstash.ve_serialization_cache.set_" . $status );
|
2016-09-08 04:28:59 +00:00
|
|
|
|
|
|
|
// Also parse and prepare the edit in case it might be saved later
|
2022-06-24 20:50:56 +00:00
|
|
|
$pageUpdater = $this->wikiPageFactory->newFromTitle( $title )->newPageUpdater( $this->getUser() );
|
2016-09-08 04:28:59 +00:00
|
|
|
$content = ContentHandler::makeContent( $wikitext, $title, CONTENT_MODEL_WIKITEXT );
|
|
|
|
|
2021-12-20 22:26:44 +00:00
|
|
|
$status = $this->pageEditStash->parseAndCache( $pageUpdater, $content, $this->getUser(), '' );
|
2021-08-10 15:57:00 +00:00
|
|
|
if ( $status === $this->pageEditStash::ERROR_NONE ) {
|
2016-09-08 04:28:59 +00:00
|
|
|
$logger = LoggerFactory::getInstance( 'StashEdit' );
|
|
|
|
$logger->debug( "Cached parser output for VE content key '$key'." );
|
|
|
|
}
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->getStats()->increment( "editstash.ve_cache_stores.$status" );
|
2016-09-08 04:28:59 +00:00
|
|
|
|
|
|
|
return $hash;
|
|
|
|
}
|
|
|
|
|
2024-02-20 10:27:15 +00:00
|
|
|
private function pruneExcessStashedEntries( BagOStuff $cache, UserIdentity $user, string $newKey ): void {
|
2019-03-28 17:42:45 +00:00
|
|
|
$key = $cache->makeKey( 'visualeditor-serialization-recent', $user->getName() );
|
|
|
|
|
|
|
|
$keyList = $cache->get( $key ) ?: [];
|
|
|
|
if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
|
|
|
|
$oldestKey = array_shift( $keyList );
|
|
|
|
$cache->delete( $oldestKey );
|
|
|
|
}
|
|
|
|
|
|
|
|
$keyList[] = $newKey;
|
|
|
|
$cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
|
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Load some parsed wikitext of an edit from the serialisation cache.
|
|
|
|
*
|
|
|
|
* @param string $hash The key of the wikitext in the serialisation cache
|
2024-02-20 10:23:33 +00:00
|
|
|
* @return string|false The wikitext
|
2018-03-28 19:47:04 +00:00
|
|
|
*/
|
2016-09-08 04:28:59 +00:00
|
|
|
protected function trySerializationCache( $hash ) {
|
2019-03-27 19:04:57 +00:00
|
|
|
$cache = ObjectCache::getLocalClusterInstance();
|
|
|
|
$key = $cache->makeKey( 'visualeditor', 'serialization', $hash );
|
|
|
|
$value = $cache->get( $key );
|
|
|
|
|
|
|
|
$status = ( $value !== false ) ? 'hit' : 'miss';
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->getStats()->increment( "editstash.ve_serialization_cache.get_$status" );
|
2019-03-27 19:04:57 +00:00
|
|
|
|
|
|
|
return $value;
|
2016-09-08 04:28:59 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* Calculate the different between the wikitext of an edit and an existing revision.
|
|
|
|
*
|
2018-06-26 15:35:09 +00:00
|
|
|
* @param Title $title The title of the page
|
2022-09-02 00:58:15 +00:00
|
|
|
* @param int|null $fromId The existing revision of the page to compare with
|
2018-03-28 19:47:04 +00:00
|
|
|
* @param string $wikitext The wikitext to compare against
|
|
|
|
* @param int|null $section Whether the wikitext refers to a given section or the whole page
|
|
|
|
* @return array The comparison, or `[ 'result' => 'nochanges' ]` if there are none
|
|
|
|
*/
|
2022-09-02 00:58:15 +00:00
|
|
|
protected function diffWikitext( Title $title, ?int $fromId, $wikitext, $section = null ) {
|
2016-09-08 04:28:59 +00:00
|
|
|
$apiParams = [
|
2018-03-10 22:32:02 +00:00
|
|
|
'action' => 'compare',
|
|
|
|
'prop' => 'diff',
|
2023-11-15 16:28:37 +00:00
|
|
|
// Because we're just providing wikitext, we only care about the main slot
|
|
|
|
'slots' => SlotRecord::MAIN,
|
2018-03-10 22:32:02 +00:00
|
|
|
'fromtitle' => $title->getPrefixedDBkey(),
|
|
|
|
'fromrev' => $fromId,
|
|
|
|
'fromsection' => $section,
|
2023-11-15 16:28:37 +00:00
|
|
|
'toslots' => SlotRecord::MAIN,
|
|
|
|
'totext-main' => $wikitext,
|
2020-05-12 18:55:57 +00:00
|
|
|
'topst' => true,
|
2016-09-08 04:28:59 +00:00
|
|
|
];
|
|
|
|
|
2021-04-28 23:58:28 +00:00
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setRequest(
|
2016-09-08 04:28:59 +00:00
|
|
|
new DerivativeRequest(
|
2021-04-28 23:58:28 +00:00
|
|
|
$context->getRequest(),
|
2016-09-08 04:28:59 +00:00
|
|
|
$apiParams,
|
2021-04-28 23:58:28 +00:00
|
|
|
/* was posted? */ true
|
|
|
|
)
|
|
|
|
);
|
|
|
|
$api = new ApiMain(
|
|
|
|
$context,
|
2017-05-04 22:27:27 +00:00
|
|
|
/* enable write? */ false
|
2016-09-08 04:28:59 +00:00
|
|
|
);
|
|
|
|
$api->execute();
|
2023-11-15 16:28:37 +00:00
|
|
|
$result = $api->getResult()->getResultData();
|
2018-03-10 22:32:02 +00:00
|
|
|
|
2023-11-15 16:28:37 +00:00
|
|
|
if ( !isset( $result['compare']['bodies'][SlotRecord::MAIN] ) ) {
|
2019-11-18 20:57:36 +00:00
|
|
|
$this->dieWithError( 'apierror-visualeditor-difffailed', 'difffailed' );
|
2016-09-08 04:28:59 +00:00
|
|
|
}
|
2023-11-15 16:28:37 +00:00
|
|
|
$diffRows = $result['compare']['bodies'][SlotRecord::MAIN];
|
2016-09-08 04:28:59 +00:00
|
|
|
|
2019-11-19 19:38:15 +00:00
|
|
|
$context = new DerivativeContext( $this->getContext() );
|
|
|
|
$context->setTitle( $title );
|
|
|
|
$engine = new DifferenceEngine( $context );
|
|
|
|
return [
|
|
|
|
'result' => 'success',
|
|
|
|
'diff' => $diffRows ? $engine->addHeader(
|
|
|
|
$diffRows,
|
|
|
|
$context->msg( 'currentrev' )->parse(),
|
|
|
|
$context->msg( 'yourtext' )->parse()
|
|
|
|
) : ''
|
|
|
|
];
|
2016-09-08 04:28:59 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2013-07-11 17:09:28 +00:00
|
|
|
public function execute() {
|
|
|
|
$user = $this->getUser();
|
|
|
|
$params = $this->extractRequestParams();
|
2021-06-11 12:34:21 +00:00
|
|
|
|
|
|
|
$result = [];
|
2016-04-29 16:00:57 +00:00
|
|
|
$title = Title::newFromText( $params['page'] );
|
2023-08-15 18:39:10 +00:00
|
|
|
if ( $title && $title->isSpecialPage() ) {
|
2019-12-20 15:50:48 +00:00
|
|
|
// Convert Special:CollabPad/MyPage to MyPage so we can serialize properly
|
2023-08-15 18:39:10 +00:00
|
|
|
[ $special, $subPage ] = $this->specialPageFactory->resolveAlias( $title->getDBkey() );
|
|
|
|
if ( $special === 'CollabPad' ) {
|
|
|
|
$title = Title::newFromText( $subPage );
|
|
|
|
}
|
2019-12-20 15:50:48 +00:00
|
|
|
}
|
2016-04-29 16:00:57 +00:00
|
|
|
if ( !$title ) {
|
2016-11-03 19:16:57 +00:00
|
|
|
$this->dieWithError( [ 'apierror-invalidtitle', wfEscapeWikiText( $params['page'] ) ] );
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
2021-04-14 14:11:18 +00:00
|
|
|
if ( !$title->canExist() ) {
|
|
|
|
$this->dieWithError( 'apierror-pagecannotexist' );
|
|
|
|
}
|
2021-05-21 16:02:44 +00:00
|
|
|
$this->getErrorFormatter()->setContextTitle( $title );
|
2016-04-29 16:00:57 +00:00
|
|
|
|
2016-02-17 16:18:02 +00:00
|
|
|
$parserParams = [];
|
2013-07-11 17:09:28 +00:00
|
|
|
if ( isset( $params['oldid'] ) ) {
|
|
|
|
$parserParams['oldid'] = $params['oldid'];
|
|
|
|
}
|
|
|
|
|
2016-11-19 14:09:16 +00:00
|
|
|
if ( isset( $params['wikitext'] ) ) {
|
2019-11-13 22:24:06 +00:00
|
|
|
$wikitext = str_replace( "\r\n", "\n", $params['wikitext'] );
|
2016-11-19 14:09:16 +00:00
|
|
|
} else {
|
2016-10-31 17:45:49 +00:00
|
|
|
$wikitext = $this->getWikitext( $title, $params, $parserParams );
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
if ( $params['paction'] === 'serialize' ) {
|
|
|
|
$result = [ 'result' => 'success', 'content' => $wikitext ];
|
|
|
|
} elseif ( $params['paction'] === 'serializeforcache' ) {
|
|
|
|
$key = $this->storeInSerializationCache(
|
|
|
|
$title,
|
|
|
|
$wikitext
|
|
|
|
);
|
|
|
|
$result = [ 'result' => 'success', 'cachekey' => $key ];
|
|
|
|
} elseif ( $params['paction'] === 'diff' ) {
|
2019-03-12 20:27:47 +00:00
|
|
|
$section = $params['section'] ?? null;
|
2019-11-18 20:57:36 +00:00
|
|
|
$result = $this->diffWikitext( $title, $params['oldid'], $wikitext, $section );
|
2016-09-08 04:28:59 +00:00
|
|
|
} elseif ( $params['paction'] === 'save' ) {
|
2021-06-11 12:34:21 +00:00
|
|
|
$pluginData = [];
|
|
|
|
foreach ( $params['plugins'] ?? [] as $plugin ) {
|
|
|
|
$pluginData[$plugin] = $params['data-' . $plugin];
|
|
|
|
}
|
|
|
|
$presaveHook = $this->hookRunner->onVisualEditorApiVisualEditorEditPreSave(
|
|
|
|
$title->toPageIdentity(),
|
|
|
|
$user,
|
|
|
|
$wikitext,
|
|
|
|
$params,
|
|
|
|
$pluginData,
|
|
|
|
$result
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( $presaveHook === false ) {
|
|
|
|
$this->dieWithError( $result['message'], 'hookaborted', $result );
|
|
|
|
}
|
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
$saveresult = $this->saveWikitext( $title, $wikitext, $params );
|
|
|
|
$editStatus = $saveresult['edit']['result'];
|
|
|
|
|
|
|
|
// Error
|
|
|
|
if ( $editStatus !== 'Success' ) {
|
2021-06-11 12:34:21 +00:00
|
|
|
$result['result'] = 'error';
|
|
|
|
$result['edit'] = $saveresult['edit'];
|
2015-04-08 20:22:48 +00:00
|
|
|
} else {
|
2021-06-11 12:34:21 +00:00
|
|
|
// Success
|
|
|
|
$result['result'] = 'success';
|
2023-06-03 01:19:38 +00:00
|
|
|
|
|
|
|
if ( $params['nocontent'] ) {
|
|
|
|
$result['nocontent'] = true;
|
2016-09-08 04:28:59 +00:00
|
|
|
} else {
|
2023-06-03 01:19:38 +00:00
|
|
|
if ( isset( $saveresult['edit']['newrevid'] ) ) {
|
|
|
|
$newRevId = intval( $saveresult['edit']['newrevid'] );
|
|
|
|
} else {
|
|
|
|
$newRevId = $title->getLatestRevID();
|
|
|
|
}
|
2013-07-11 17:09:28 +00:00
|
|
|
|
2023-06-03 01:19:38 +00:00
|
|
|
// Return result of parseWikitext instead of saveWikitext so that the
|
|
|
|
// frontend can update the page rendering without a refresh.
|
|
|
|
$parseWikitextResult = $this->parseWikitext( $newRevId, $params );
|
2014-11-07 00:31:34 +00:00
|
|
|
|
2023-06-03 01:19:38 +00:00
|
|
|
$result = array_merge( $result, $parseWikitextResult );
|
|
|
|
}
|
2021-06-11 12:34:21 +00:00
|
|
|
|
2017-05-04 22:07:10 +00:00
|
|
|
$result['isRedirect'] = (string)$title->isRedirect();
|
2016-09-08 04:28:59 +00:00
|
|
|
|
2020-04-20 22:07:01 +00:00
|
|
|
if ( ExtensionRegistry::getInstance()->isLoaded( 'FlaggedRevs' ) ) {
|
2022-08-08 11:03:45 +00:00
|
|
|
$newContext = new DerivativeContext( RequestContext::getMain() );
|
2016-09-08 04:28:59 +00:00
|
|
|
// Defeat !$this->isPageView( $request ) || $request->getVal( 'oldid' ) check in setPageContent
|
|
|
|
$newRequest = new DerivativeRequest(
|
|
|
|
$this->getRequest(),
|
|
|
|
[
|
|
|
|
'diff' => null,
|
|
|
|
'oldid' => '',
|
|
|
|
'title' => $title->getPrefixedText(),
|
|
|
|
'action' => 'view'
|
|
|
|
] + $this->getRequest()->getValues()
|
|
|
|
);
|
2019-01-30 21:03:09 +00:00
|
|
|
$newContext->setRequest( $newRequest );
|
|
|
|
$newContext->setTitle( $title );
|
2022-08-01 23:23:10 +00:00
|
|
|
|
|
|
|
// Must be after $globalContext->setTitle since FlaggedRevs constructor
|
|
|
|
// inspects global Title
|
2022-08-08 11:03:45 +00:00
|
|
|
$view = FlaggablePageView::newFromTitle( $title );
|
2022-08-01 23:23:10 +00:00
|
|
|
// Most likely identical to $globalState, but not our concern
|
|
|
|
$originalContext = $view->getContext();
|
2019-01-30 21:03:09 +00:00
|
|
|
$view->setContext( $newContext );
|
2016-09-08 04:28:59 +00:00
|
|
|
|
|
|
|
// The two parameters here are references but we don't care
|
|
|
|
// about what FlaggedRevs does with them.
|
|
|
|
$outputDone = null;
|
|
|
|
$useParserCache = null;
|
2019-12-15 04:30:43 +00:00
|
|
|
// @phan-suppress-next-line PhanTypeMismatchArgument
|
2016-09-08 04:28:59 +00:00
|
|
|
$view->setPageContent( $outputDone, $useParserCache );
|
|
|
|
$view->displayTag();
|
2019-01-30 21:03:09 +00:00
|
|
|
$view->setContext( $originalContext );
|
2016-09-08 04:28:59 +00:00
|
|
|
}
|
2016-04-04 16:27:46 +00:00
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
$lang = $this->getLanguage();
|
2014-10-31 00:26:32 +00:00
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
if ( isset( $saveresult['edit']['newtimestamp'] ) ) {
|
|
|
|
$ts = $saveresult['edit']['newtimestamp'];
|
2014-11-17 01:00:06 +00:00
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
$result['lastModified'] = [
|
|
|
|
'date' => $lang->userDate( $ts, $user ),
|
|
|
|
'time' => $lang->userTime( $ts, $user )
|
|
|
|
];
|
|
|
|
}
|
2014-10-31 00:26:32 +00:00
|
|
|
|
2016-09-08 04:28:59 +00:00
|
|
|
if ( isset( $saveresult['edit']['newrevid'] ) ) {
|
|
|
|
$result['newrevid'] = intval( $saveresult['edit']['newrevid'] );
|
|
|
|
}
|
2013-07-11 17:09:28 +00:00
|
|
|
|
2023-06-15 00:06:42 +00:00
|
|
|
if ( isset( $saveresult['edit']['tempusercreated'] ) ) {
|
|
|
|
$result['tempusercreated'] = $saveresult['edit']['tempusercreated'];
|
|
|
|
}
|
|
|
|
if ( isset( $saveresult['edit']['tempusercreatedredirect'] ) ) {
|
|
|
|
$result['tempusercreatedredirect'] = $saveresult['edit']['tempusercreatedredirect'];
|
|
|
|
}
|
|
|
|
|
2020-08-24 20:45:04 +00:00
|
|
|
$result['watched'] = $saveresult['edit']['watched'] ?? false;
|
|
|
|
$result['watchlistexpiry'] = $saveresult['edit']['watchlistexpiry'] ?? null;
|
2021-04-22 19:27:47 +00:00
|
|
|
}
|
2021-06-11 12:34:21 +00:00
|
|
|
|
2023-06-28 19:58:31 +00:00
|
|
|
// Refresh article ID (which is used by toPageIdentity()) in case we just created the page.
|
|
|
|
// Maybe it's not great to rely on this side-effect…
|
|
|
|
$title->getArticleID( IDBAccessObject::READ_LATEST );
|
|
|
|
|
2021-04-22 19:27:47 +00:00
|
|
|
$this->hookRunner->onVisualEditorApiVisualEditorEditPostSave(
|
2023-06-28 19:58:31 +00:00
|
|
|
$title->toPageIdentity(),
|
2021-04-22 19:27:47 +00:00
|
|
|
$user,
|
|
|
|
$wikitext,
|
|
|
|
$params,
|
|
|
|
$pluginData,
|
|
|
|
$saveresult,
|
|
|
|
$result
|
|
|
|
);
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
$this->getResult()->addValue( null, $this->getModuleName(), $result );
|
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2013-07-11 17:09:28 +00:00
|
|
|
public function getAllowedParams() {
|
2016-02-17 16:18:02 +00:00
|
|
|
return [
|
2016-09-08 04:28:59 +00:00
|
|
|
'paction' => [
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
|
|
|
ParamValidator::PARAM_TYPE => [
|
2016-09-08 04:28:59 +00:00
|
|
|
'serialize',
|
|
|
|
'serializeforcache',
|
|
|
|
'diff',
|
|
|
|
'save',
|
|
|
|
],
|
|
|
|
],
|
2016-02-17 16:18:02 +00:00
|
|
|
'page' => [
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
2016-02-17 16:18:02 +00:00
|
|
|
],
|
|
|
|
'token' => [
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_REQUIRED => true,
|
2016-02-17 16:18:02 +00:00
|
|
|
],
|
2020-06-10 16:08:23 +00:00
|
|
|
'wikitext' => [
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_TYPE => 'text',
|
|
|
|
ParamValidator::PARAM_DEFAULT => null,
|
2020-06-10 16:08:23 +00:00
|
|
|
],
|
2016-09-06 19:16:55 +00:00
|
|
|
'section' => null,
|
2016-12-08 19:24:10 +00:00
|
|
|
'sectiontitle' => null,
|
2022-09-02 00:58:15 +00:00
|
|
|
'basetimestamp' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'timestamp',
|
|
|
|
],
|
|
|
|
'starttimestamp' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'timestamp',
|
|
|
|
],
|
|
|
|
'oldid' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'integer',
|
|
|
|
],
|
2013-07-11 17:09:28 +00:00
|
|
|
'minor' => null,
|
2020-02-29 17:35:29 +00:00
|
|
|
'watchlist' => null,
|
2020-06-10 16:08:23 +00:00
|
|
|
'html' => [
|
2020-12-15 23:58:48 +00:00
|
|
|
// Use the 'raw' type to avoid Unicode NFC normalization.
|
|
|
|
// This makes the parameter binary safe, so that (a) if
|
|
|
|
// we use client-side compression it is not mangled, and/or
|
|
|
|
// (b) deprecated Unicode sequences explicitly encoded in
|
|
|
|
// wikitext (ie,  ) are not mangled. Wikitext is
|
|
|
|
// in Unicode Normal Form C, but because of explicit entities
|
|
|
|
// the output HTML is not guaranteed to be.
|
|
|
|
ParamValidator::PARAM_TYPE => 'raw',
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_DEFAULT => null,
|
2020-06-10 16:08:23 +00:00
|
|
|
],
|
2015-10-08 22:16:56 +00:00
|
|
|
'etag' => null,
|
2013-07-11 17:09:28 +00:00
|
|
|
'summary' => null,
|
|
|
|
'captchaid' => null,
|
|
|
|
'captchaword' => null,
|
2013-11-01 21:30:22 +00:00
|
|
|
'cachekey' => null,
|
2023-06-03 01:19:38 +00:00
|
|
|
'nocontent' => false,
|
2023-06-15 00:06:42 +00:00
|
|
|
'returnto' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'title',
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returnto',
|
|
|
|
],
|
|
|
|
'returntoquery' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'string',
|
|
|
|
ParamValidator::PARAM_DEFAULT => '',
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoquery',
|
|
|
|
],
|
|
|
|
'returntoanchor' => [
|
|
|
|
ParamValidator::PARAM_TYPE => 'string',
|
|
|
|
ParamValidator::PARAM_DEFAULT => '',
|
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-edit-param-returntoanchor',
|
|
|
|
],
|
2022-02-18 15:52:47 +00:00
|
|
|
'useskin' => [
|
2022-04-03 23:30:46 +00:00
|
|
|
ParamValidator::PARAM_TYPE => array_keys( $this->skinFactory->getInstalledSkins() ),
|
2022-02-18 15:52:47 +00:00
|
|
|
ApiBase::PARAM_HELP_MSG => 'apihelp-parse-param-useskin',
|
|
|
|
],
|
2020-01-24 19:14:32 +00:00
|
|
|
'tags' => [
|
2020-07-20 20:43:59 +00:00
|
|
|
ParamValidator::PARAM_ISMULTI => true,
|
2020-01-24 19:14:32 +00:00
|
|
|
],
|
2021-04-22 19:27:47 +00:00
|
|
|
'plugins' => [
|
|
|
|
ParamValidator::PARAM_ISMULTI => true,
|
|
|
|
ParamValidator::PARAM_TYPE => 'string',
|
|
|
|
],
|
|
|
|
// Additional data sent by the client. Not used directly in the ApiVisualEditorEdit workflows, but
|
2021-08-25 08:44:00 +00:00
|
|
|
// is passed alongside the other parameters to implementations of onApiVisualEditorEditPostSave and
|
|
|
|
// onApiVisualEditorEditPreSave
|
2021-04-22 19:27:47 +00:00
|
|
|
'data-{plugin}' => [
|
|
|
|
ApiBase::PARAM_TEMPLATE_VARS => [ 'plugin' => 'plugins' ]
|
|
|
|
]
|
2016-02-17 16:18:02 +00:00
|
|
|
];
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2013-07-11 17:09:28 +00:00
|
|
|
public function needsToken() {
|
2014-08-09 13:08:14 +00:00
|
|
|
return 'csrf';
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
|
|
|
public function isInternal() {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2018-03-28 19:47:04 +00:00
|
|
|
/**
|
|
|
|
* @inheritDoc
|
|
|
|
*/
|
2013-07-11 17:09:28 +00:00
|
|
|
public function isWriteMode() {
|
|
|
|
return true;
|
|
|
|
}
|
2022-09-05 10:36:16 +00:00
|
|
|
|
2013-07-11 17:09:28 +00:00
|
|
|
}
|