mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-23 18:28:51 +00:00
Local implementation of ParsoidClient (DirectParsoidClient)
* DirectParsoidClient makes use of parsoid directly for performing transformations on both wikitext and/or HTML contents. * Also, it's used to fetch HTML from parsoid's parser cache. Before, this operation was done via RESTBase but now it's being fetched in core's parsoid parser cache. * This patch also enables VE clients to transform HTML to Wikitext when switching from HTML to source mode on. It makes use of the HtmlInputTransformHelper to perform this transformation. * Now, VE client can make use of core code for switching between HTML to source mode and back without RESTBase. Change-Id: I5c7cfcc4086d8da7905897194d8601aa07418b59
This commit is contained in:
parent
80504b2c03
commit
35cb550747
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper functions for using the REST interface to Parsoid/RESTBase.
|
||||
* Helper functions for using the REST interface to Parsoid.
|
||||
*
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
|
@ -10,22 +10,24 @@
|
|||
|
||||
namespace MediaWiki\Extension\VisualEditor;
|
||||
|
||||
use Exception;
|
||||
use IBufferingStatsdDataFactory;
|
||||
use Language;
|
||||
use LocalizedException;
|
||||
use MediaWiki\Edit\ParsoidOutputStash;
|
||||
use MediaWiki\Page\PageIdentity;
|
||||
use MediaWiki\Parser\Parsoid\Config\PageConfigFactory;
|
||||
use MediaWiki\Parser\Parsoid\HTMLTransformFactory;
|
||||
use MediaWiki\Parser\Parsoid\ParsoidOutputAccess;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use MediaWiki\Rest\Handler\HtmlInputTransformHelper;
|
||||
use MediaWiki\Rest\Handler\HtmlOutputRendererHelper;
|
||||
use MediaWiki\Rest\HttpException;
|
||||
use MediaWiki\Rest\LocalizedHttpException;
|
||||
use MediaWiki\Revision\MutableRevisionRecord;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWiki\Revision\SlotRecord;
|
||||
use ParserOutput;
|
||||
use RequestContext;
|
||||
use Title;
|
||||
use WikiMap;
|
||||
use Wikimedia\Parsoid\Config\DataAccess;
|
||||
use Wikimedia\Parsoid\Config\SiteConfig;
|
||||
use Wikimedia\Parsoid\Core\SelserData;
|
||||
use Wikimedia\Parsoid\Parsoid;
|
||||
use Wikimedia\Parsoid\Utils\DOMUtils;
|
||||
use Wikimedia\UUID\GlobalIdGenerator;
|
||||
use RawMessage;
|
||||
use User;
|
||||
use WikitextContent;
|
||||
|
||||
class DirectParsoidClient implements ParsoidClient {
|
||||
|
@ -35,103 +37,198 @@ class DirectParsoidClient implements ParsoidClient {
|
|||
* ve.init.mw.ArticleTargetLoader.js
|
||||
*/
|
||||
public const PARSOID_VERSION = '2.4.0';
|
||||
private const FLAVOR_DEFAULT = 'view';
|
||||
|
||||
/** @var array Parsoid-specific settings array from $config */
|
||||
private $parsoidSettings;
|
||||
/** @var ParsoidOutputStash */
|
||||
private $parsoidOutputStash;
|
||||
|
||||
/** @var SiteConfig */
|
||||
protected $siteConfig;
|
||||
/** @var IBufferingStatsdDataFactory */
|
||||
private $stats;
|
||||
|
||||
/** @var PageConfigFactory */
|
||||
protected $pageConfigFactory;
|
||||
/** @var ParsoidOutputAccess */
|
||||
private $parsoidOutputAccess;
|
||||
|
||||
/** @var DataAccess */
|
||||
protected $dataAccess;
|
||||
/** @var Authority */
|
||||
private $performer;
|
||||
|
||||
/** @var GlobalIdGenerator */
|
||||
private $globalIdGenerator;
|
||||
/** @var HTMLTransformFactory */
|
||||
private $htmlTransformFactory;
|
||||
|
||||
/**
|
||||
* @param array $parsoidSettings
|
||||
* @param SiteConfig $siteConfig
|
||||
* @param PageConfigFactory $pageConfigFactory
|
||||
* @param DataAccess $dataAccess
|
||||
* @param GlobalIdGenerator $globalIdGenerator
|
||||
* @param ParsoidOutputStash $parsoidOutputStash
|
||||
* @param IBufferingStatsdDataFactory $statsDataFactory
|
||||
* @param ParsoidOutputAccess $parsoidOutputAccess
|
||||
* @param HTMLTransformFactory $htmlTransformFactory
|
||||
* @param Authority $performer
|
||||
*/
|
||||
public function __construct(
|
||||
array $parsoidSettings,
|
||||
SiteConfig $siteConfig,
|
||||
PageConfigFactory $pageConfigFactory,
|
||||
DataAccess $dataAccess,
|
||||
GlobalIdGenerator $globalIdGenerator
|
||||
ParsoidOutputStash $parsoidOutputStash,
|
||||
IBufferingStatsdDataFactory $statsDataFactory,
|
||||
ParsoidOutputAccess $parsoidOutputAccess,
|
||||
HTMLTransformFactory $htmlTransformFactory,
|
||||
Authority $performer
|
||||
) {
|
||||
$this->parsoidSettings = $parsoidSettings;
|
||||
$this->siteConfig = $siteConfig;
|
||||
$this->pageConfigFactory = $pageConfigFactory;
|
||||
$this->dataAccess = $dataAccess;
|
||||
$this->globalIdGenerator = $globalIdGenerator;
|
||||
$this->parsoidOutputStash = $parsoidOutputStash;
|
||||
$this->stats = $statsDataFactory;
|
||||
$this->parsoidOutputAccess = $parsoidOutputAccess;
|
||||
$this->htmlTransformFactory = $htmlTransformFactory;
|
||||
$this->performer = $performer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request page HTML
|
||||
* @param PageIdentity $page
|
||||
* @param RevisionRecord|null $revision
|
||||
* @param Language|null $pageLanguage
|
||||
* @param bool $stash
|
||||
* @param string $flavor
|
||||
*
|
||||
* @return HtmlOutputRendererHelper
|
||||
*/
|
||||
private function getHtmlOutputRendererHelper(
|
||||
PageIdentity $page,
|
||||
?RevisionRecord $revision = null,
|
||||
Language $pageLanguage = null,
|
||||
bool $stash = false,
|
||||
string $flavor = self::FLAVOR_DEFAULT
|
||||
): HtmlOutputRendererHelper {
|
||||
$helper = new HtmlOutputRendererHelper(
|
||||
$this->parsoidOutputStash,
|
||||
$this->stats,
|
||||
$this->parsoidOutputAccess
|
||||
);
|
||||
|
||||
// Fake REST params
|
||||
$params = [
|
||||
'stash' => $stash,
|
||||
'flavor' => $flavor,
|
||||
];
|
||||
|
||||
$user = User::newFromIdentity( $this->performer->getUser() );
|
||||
$helper->init( $page, $params, $user, $revision, $pageLanguage );
|
||||
return $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PageIdentity $page
|
||||
* @param string $html
|
||||
* @param int|null $oldid
|
||||
* @param string|null $etag
|
||||
* @param Language|null $pageLanguage
|
||||
*
|
||||
* @return HtmlInputTransformHelper
|
||||
*/
|
||||
private function getHtmlInputTransformHelper(
|
||||
PageIdentity $page,
|
||||
string $html,
|
||||
int $oldid = null,
|
||||
string $etag = null,
|
||||
Language $pageLanguage = null
|
||||
): HtmlInputTransformHelper {
|
||||
$helper = new HtmlInputTransformHelper(
|
||||
$this->stats,
|
||||
$this->htmlTransformFactory,
|
||||
$this->parsoidOutputStash,
|
||||
$this->parsoidOutputAccess
|
||||
);
|
||||
|
||||
// Fake REST body
|
||||
$body = [
|
||||
'html' => [
|
||||
'body' => $html,
|
||||
],
|
||||
'original' => [
|
||||
'revid' => $oldid,
|
||||
'etag' => $etag,
|
||||
]
|
||||
];
|
||||
|
||||
$helper->init( $page, $body, [], null, $pageLanguage );
|
||||
|
||||
return $helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request page HTML from Parsoid.
|
||||
*
|
||||
* @param RevisionRecord $revision Page revision
|
||||
* @param ?Language $targetLanguage Desired output language
|
||||
* @param ?Language $targetLanguage Page language (default: `null`)
|
||||
*
|
||||
* @return array The response
|
||||
* @return array An array mimicking a RESTbase server's response,
|
||||
* with keys: 'error', 'headers' and 'body'
|
||||
*/
|
||||
public function getPageHtml( RevisionRecord $revision, ?Language $targetLanguage ): array {
|
||||
public function getPageHtml( RevisionRecord $revision, ?Language $targetLanguage = null ): array {
|
||||
// In the VE client, we always want to stash.
|
||||
$page = $revision->getPage();
|
||||
$title = Title::castFromPageIdentity( $page );
|
||||
$targetLanguage = $targetLanguage ?: $title->getPageLanguage();
|
||||
$oldid = $revision->getId();
|
||||
$lang = $targetLanguage->getCode();
|
||||
// This is /page/html/$title/$revision?redirect=false&stash=true
|
||||
// With Accept-Language: $lang
|
||||
$envOptions = [
|
||||
// $attribs['envOptions'] is created in ParsoidHandler::getRequestAttributes()
|
||||
'prefix' => WikiMap::getCurrentWikiId(),
|
||||
'pageName' => $title->getPrefixedDBkey(),
|
||||
'htmlVariantLanguage' => $lang,
|
||||
'outputContentVersion' => Parsoid::resolveContentVersion(
|
||||
self::PARSOID_VERSION
|
||||
) ?? Parsoid::defaultHTMLVersion(),
|
||||
];
|
||||
// $pageConfig originally created in
|
||||
// ParsoidHandler::tryToCreatePageConfig
|
||||
$user = RequestContext::getMain()->getUser();
|
||||
// Note: Parsoid by design isn't supposed to use the user
|
||||
// context right now, and all user state is expected to be
|
||||
// introduced as a post-parse transform. So although we pass a
|
||||
// User here, it only currently affects the output in obscure
|
||||
// corner cases; see PageConfigFactory::create() for more.
|
||||
$pageConfig = $this->pageConfigFactory->create(
|
||||
$page, $user, $revision, null, $lang, $this->parsoidSettings
|
||||
);
|
||||
if ( $pageConfig->getRevisionContent() === null ) {
|
||||
throw new \LogicException( "Specified revision does not exist" );
|
||||
$helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, true );
|
||||
|
||||
try {
|
||||
$parserOutput = $helper->getHtml();
|
||||
|
||||
return $this->fakeRESTbaseHTMLResponse( $parserOutput->getRawText(), $helper );
|
||||
} catch ( HttpException $ex ) {
|
||||
return $this->fakeRESTbaseError( $ex );
|
||||
}
|
||||
$parsoid = new Parsoid( $this->siteConfig, $this->dataAccess );
|
||||
$parserOutput = new ParserOutput();
|
||||
// Note that $headers is an out parameter
|
||||
// $envOptions originally included $opts['contentmodel'] here as well
|
||||
$out = $parsoid->wikitext2html(
|
||||
$pageConfig, $envOptions, $headers, $parserOutput
|
||||
);
|
||||
$tid = $this->globalIdGenerator->newUUIDv1();
|
||||
$etag = "W/\"{$oldid}/{$tid}\"";
|
||||
# XXX: we could cache this locally using the $etag as a key,
|
||||
# then reuse it when transforming back to wikitext below.
|
||||
return [
|
||||
'body' => $out,
|
||||
'headers' => $headers + [
|
||||
'etag' => $etag,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform HTML to wikitext via Parsoid
|
||||
* @param PageIdentity $page
|
||||
* @param string $wikitext
|
||||
*
|
||||
* @return RevisionRecord
|
||||
*/
|
||||
private function makeFakeRevision(
|
||||
PageIdentity $page,
|
||||
string $wikitext
|
||||
): RevisionRecord {
|
||||
$rev = new MutableRevisionRecord( $page );
|
||||
$rev->setId( 0 );
|
||||
$rev->setPageId( $page->getId() );
|
||||
|
||||
$rev->setContent( SlotRecord::MAIN, new WikitextContent( $wikitext ) );
|
||||
|
||||
return $rev;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform wikitext to HTML with Parsoid. Wrapper for ::postData().
|
||||
*
|
||||
* @param PageIdentity $page The page the content belongs to use as the parsing context
|
||||
* @param Language $targetLanguage Page language
|
||||
* @param string $wikitext The wikitext fragment to parse
|
||||
* @param bool $bodyOnly Whether to provide only the contents of the `<body>` tag
|
||||
* @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`)
|
||||
* @param bool $stash Whether to stash the result in the server-side cache (default: `false`)
|
||||
*
|
||||
* @return array An array mimicking a RESTbase server's response,
|
||||
* with keys 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
public function transformWikitext(
|
||||
PageIdentity $page,
|
||||
Language $targetLanguage,
|
||||
string $wikitext,
|
||||
bool $bodyOnly,
|
||||
?int $oldid,
|
||||
bool $stash
|
||||
): array {
|
||||
$revision = $this->makeFakeRevision( $page, $wikitext );
|
||||
$helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, $stash );
|
||||
|
||||
if ( $bodyOnly ) {
|
||||
$helper->setFlavor( 'fragment' );
|
||||
}
|
||||
|
||||
try {
|
||||
$parserOutput = $helper->getHtml();
|
||||
$html = $parserOutput->getRawText();
|
||||
|
||||
return $this->fakeRESTbaseHTMLResponse( $html, $helper );
|
||||
} catch ( HttpException $ex ) {
|
||||
return $this->fakeRESTbaseError( $ex );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform HTML to wikitext with Parsoid
|
||||
*
|
||||
* @param PageIdentity $page The page the content belongs to
|
||||
* @param Language $targetLanguage The desired output language
|
||||
|
@ -144,120 +241,63 @@ class DirectParsoidClient implements ParsoidClient {
|
|||
public function transformHTML(
|
||||
PageIdentity $page, Language $targetLanguage, string $html, ?int $oldid, ?string $etag
|
||||
): array {
|
||||
// This is POST /transform/html/to/wikitext/$title/$oldid
|
||||
// with header If-Match: $etag
|
||||
// and data: [ 'html' => $html ]
|
||||
$lang = $targetLanguage->getCode();
|
||||
// $pageConfig originally created in
|
||||
// ParsoidHandler::tryToCreatePageConfig
|
||||
$user = RequestContext::getMain()->getUser();
|
||||
// Note: Parsoid by design isn't supposed to use the user
|
||||
// context right now, and all user state is expected to be
|
||||
// introduced as a post-parse transform. So although we pass a
|
||||
// User here, it only currently affects the output in obscure
|
||||
// corner cases; see PageConfigFactory::create() for more.
|
||||
$pageConfig = $this->pageConfigFactory->create(
|
||||
$page, $user, $oldid, null, $lang, $this->parsoidSettings
|
||||
);
|
||||
$doc = DOMUtils::parseHTML( $html, true );
|
||||
$vEdited = DOMUtils::extractInlinedContentVersion( $doc ) ??
|
||||
Parsoid::defaultHTMLVersion();
|
||||
// T267990: This should be replaced by PET's ParserCache/stash
|
||||
// mechanism. (RESTBase did the fetch based on the etag, and then
|
||||
// compared vEdited to vOriginal to determine if it was usable.)
|
||||
$oldHtml = null;
|
||||
$selserData = ( $oldid === null ) ? null : new SelserData(
|
||||
$pageConfig->getPageMainContent(), $oldHtml
|
||||
);
|
||||
$parsoid = new Parsoid( $this->siteConfig, $this->dataAccess );
|
||||
$wikitext = $parsoid->dom2wikitext( $pageConfig, $doc, [
|
||||
'inputContentVersion' => $vEdited,
|
||||
'htmlSize' => mb_strlen( $html ),
|
||||
], $selserData );
|
||||
$helper = $this->getHtmlInputTransformHelper( $page, $html, $oldid, $etag, $targetLanguage );
|
||||
|
||||
try {
|
||||
$content = $helper->getContent();
|
||||
$format = $content->getDefaultFormat();
|
||||
|
||||
return [
|
||||
'code' => 200,
|
||||
'headers' => [
|
||||
'Content-Type' => $format,
|
||||
],
|
||||
'body' => $content->serialize( $format ),
|
||||
];
|
||||
} catch ( HttpException $ex ) {
|
||||
return $this->fakeRESTbaseError( $ex );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $data
|
||||
* @param HtmlOutputRendererHelper $helper
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private function fakeRESTbaseHTMLResponse( $data, HtmlOutputRendererHelper $helper ): array {
|
||||
return [
|
||||
'body' => $wikitext,
|
||||
'headers' => [],
|
||||
'code' => 200,
|
||||
'headers' => [
|
||||
'content-language' => $helper->getHtmlOutputContentLanguage(),
|
||||
'etag' => $helper->getETag()
|
||||
],
|
||||
'body' => $data,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform wikitext to HTML via Parsoid.
|
||||
* @param Exception $ex
|
||||
*
|
||||
* @param PageIdentity $page The page the content belongs to
|
||||
* @param Language $targetLanguage The desired output language
|
||||
* @param string $wikitext The wikitext fragment to parse
|
||||
* @param bool $bodyOnly Whether to provide only the contents of the `<body>` tag
|
||||
* @param ?int $oldid What oldid revision, if any, to base the request from (default: `null`)
|
||||
* @param bool $stash Whether to stash the result in the server-side cache (default: `false`)
|
||||
*
|
||||
* @return array The response, 'code', 'reason', 'headers' and 'body'
|
||||
* @return array
|
||||
*/
|
||||
public function transformWikitext(
|
||||
PageIdentity $page, Language $targetLanguage, string $wikitext,
|
||||
bool $bodyOnly, ?int $oldid, bool $stash
|
||||
): array {
|
||||
$title = Title::castFromPageIdentity( $page );
|
||||
private function fakeRESTbaseError( Exception $ex ): array {
|
||||
if ( $ex instanceof LocalizedHttpException ) {
|
||||
$msg = $ex->getMessageValue();
|
||||
} elseif ( $ex instanceof LocalizedException ) {
|
||||
$msg = $ex->getMessageObject();
|
||||
} else {
|
||||
$msg = new RawMessage( $ex->getMessage() );
|
||||
}
|
||||
|
||||
// This is POST /transform/wikitext/to/html/$title/$oldid
|
||||
// with data: [
|
||||
// 'wikitext' => $wikitext,
|
||||
// 'body_only' => $bodyOnly,
|
||||
// 'stash' => $stash,
|
||||
// ]
|
||||
// T267990: Stashing features are not implemented in zero-conf mode;
|
||||
// they will eventually be replaced by PET's ParserCache/stash mechanism
|
||||
$lang = $targetLanguage->getCode();
|
||||
$envOptions = [
|
||||
// $attribs['envOptions'] is created in ParsoidHandler::getRequestAttributes()
|
||||
'prefix' => WikiMap::getCurrentWikiId(),
|
||||
'pageName' => $title->getPrefixedDBkey(),
|
||||
'htmlVariantLanguage' => $lang,
|
||||
'outputContentVersion' => Parsoid::resolveContentVersion(
|
||||
self::PARSOID_VERSION
|
||||
) ?? Parsoid::defaultHTMLVersion(),
|
||||
'body_only' => $bodyOnly,
|
||||
// When VE does a fragment expansion (for example when
|
||||
// template arguments are edited and it wants an updated
|
||||
// render of the template) it's not going to want section
|
||||
// tags; in this case bodyOnly=true and wrapSections=false.
|
||||
// (T181226)
|
||||
// But on the other hand, VE doesn't do anything with section
|
||||
// tags right now other than strip them, so we'll just always
|
||||
// pass wrapSections=false for now.
|
||||
'wrapSections' => false,
|
||||
];
|
||||
// $pageConfig originally created in
|
||||
// ParsoidHandler::tryToCreatePageConfig
|
||||
$user = RequestContext::getMain()->getUser();
|
||||
// Note: Parsoid by design isn't supposed to use the user
|
||||
// context right now, and all user state is expected to be
|
||||
// introduced as a post-parse transform. So although we pass a
|
||||
// User here, it only currently affects the output in obscure
|
||||
// corner cases; see PageConfigFactory::create() for more.
|
||||
|
||||
// Create a mutable revision record and set to the desired wikitext.
|
||||
$tmpRevision = new MutableRevisionRecord( $page );
|
||||
$tmpRevision->setSlot(
|
||||
SlotRecord::newUnsaved(
|
||||
SlotRecord::MAIN,
|
||||
new WikitextContent( $wikitext )
|
||||
)
|
||||
);
|
||||
|
||||
$pageConfig = $this->pageConfigFactory->create(
|
||||
$page, $user, $tmpRevision, null, $lang, $this->parsoidSettings
|
||||
);
|
||||
$parsoid = new Parsoid( $this->siteConfig, $this->dataAccess );
|
||||
$parserOutput = new ParserOutput();
|
||||
// Note that $headers is an out parameter
|
||||
$out = $parsoid->wikitext2html(
|
||||
$pageConfig, $envOptions, $headers, $parserOutput
|
||||
);
|
||||
// No etag generation in this pathway, and no caching
|
||||
// This is just used to update previews when you edit a template
|
||||
return [
|
||||
'body' => $out,
|
||||
'headers' => $headers,
|
||||
'error' => [
|
||||
'message' => $msg->getKey() ?? '',
|
||||
'params' => $msg->getParams() ?? []
|
||||
],
|
||||
'headers' => [],
|
||||
'body' => $ex->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -33,12 +33,12 @@ return [
|
|||
VisualEditorParsoidClientFactory::ENABLE_COOKIE_FORWARDING => $isPrivateWiki
|
||||
]
|
||||
),
|
||||
$services->getParsoidSiteConfig(),
|
||||
$services->getParsoidPageConfigFactory(),
|
||||
$services->getParsoidDataAccess(),
|
||||
$services->getGlobalIdGenerator(),
|
||||
$services->getHttpRequestFactory(),
|
||||
LoggerFactory::getInstance( 'VisualEditor' )
|
||||
LoggerFactory::getInstance( 'VisualEditor' ),
|
||||
$services->getParsoidOutputStash(),
|
||||
$services->getStatsdDataFactory(),
|
||||
$services->getParsoidOutputAccess(),
|
||||
$services->getHTMLTransformFactory()
|
||||
);
|
||||
},
|
||||
];
|
||||
|
|
|
@ -3,19 +3,20 @@
|
|||
namespace MediaWiki\Extension\VisualEditor;
|
||||
|
||||
use ConfigException;
|
||||
use IBufferingStatsdDataFactory;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Edit\ParsoidOutputStash;
|
||||
use MediaWiki\Http\HttpRequestFactory;
|
||||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\Parser\Parsoid\Config\PageConfigFactory;
|
||||
use MediaWiki\Parser\Parsoid\HTMLTransformFactory;
|
||||
use MediaWiki\Parser\Parsoid\ParsoidOutputAccess;
|
||||
use MediaWiki\Permissions\Authority;
|
||||
use ParsoidVirtualRESTService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RequestContext;
|
||||
use RestbaseVirtualRESTService;
|
||||
use VirtualRESTService;
|
||||
use VirtualRESTServiceClient;
|
||||
use Wikimedia\Assert\Assert;
|
||||
use Wikimedia\Parsoid\Config\DataAccess;
|
||||
use Wikimedia\Parsoid\Config\SiteConfig;
|
||||
use Wikimedia\UUID\GlobalIdGenerator;
|
||||
|
||||
/**
|
||||
* @since 1.40
|
||||
|
@ -40,24 +41,11 @@ class VisualEditorParsoidClientFactory {
|
|||
* @var array
|
||||
*/
|
||||
public const CONSTRUCTOR_OPTIONS = [
|
||||
MainConfigNames::ParsoidSettings,
|
||||
MainConfigNames::VirtualRestConfig,
|
||||
self::USE_AUTO_CONFIG,
|
||||
self::ENABLE_COOKIE_FORWARDING
|
||||
];
|
||||
|
||||
/** @var SiteConfig */
|
||||
private $siteConfig;
|
||||
|
||||
/** @var PageConfigFactory */
|
||||
private $pageConfigFactory;
|
||||
|
||||
/** @var DataAccess */
|
||||
private $dataAccess;
|
||||
|
||||
/** @var GlobalIdGenerator */
|
||||
private $globalIdGenerator;
|
||||
|
||||
/** @var HttpRequestFactory */
|
||||
private $httpRequestFactory;
|
||||
|
||||
|
@ -70,45 +58,61 @@ class VisualEditorParsoidClientFactory {
|
|||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/** @var ParsoidOutputStash */
|
||||
private $parsoidOutputStash;
|
||||
|
||||
/** @var IBufferingStatsdDataFactory */
|
||||
private $statsDataFactory;
|
||||
|
||||
/** @var ParsoidOutputAccess */
|
||||
private $parsoidOutputAccess;
|
||||
|
||||
/** @var HTMLTransformFactory */
|
||||
private $htmlTransformFactory;
|
||||
|
||||
/**
|
||||
* @param ServiceOptions $options
|
||||
* @param SiteConfig $siteConfig
|
||||
* @param PageConfigFactory $pageConfigFactory
|
||||
* @param DataAccess $dataAccess
|
||||
* @param GlobalIdGenerator $globalIdGenerator
|
||||
* @param HttpRequestFactory $httpRequestFactory
|
||||
* @param LoggerInterface $logger
|
||||
* @param ParsoidOutputStash $parsoidOutputStash
|
||||
* @param IBufferingStatsdDataFactory $statsDataFactory
|
||||
* @param ParsoidOutputAccess $parsoidOutputAccess
|
||||
* @param HTMLTransformFactory $htmlTransformFactory
|
||||
*/
|
||||
public function __construct(
|
||||
ServiceOptions $options,
|
||||
SiteConfig $siteConfig,
|
||||
PageConfigFactory $pageConfigFactory,
|
||||
DataAccess $dataAccess,
|
||||
GlobalIdGenerator $globalIdGenerator,
|
||||
HttpRequestFactory $httpRequestFactory,
|
||||
LoggerInterface $logger
|
||||
LoggerInterface $logger,
|
||||
ParsoidOutputStash $parsoidOutputStash,
|
||||
IBufferingStatsdDataFactory $statsDataFactory,
|
||||
ParsoidOutputAccess $parsoidOutputAccess,
|
||||
HTMLTransformFactory $htmlTransformFactory
|
||||
) {
|
||||
$this->options = $options;
|
||||
$this->options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
|
||||
|
||||
$this->siteConfig = $siteConfig;
|
||||
$this->pageConfigFactory = $pageConfigFactory;
|
||||
$this->dataAccess = $dataAccess;
|
||||
$this->globalIdGenerator = $globalIdGenerator;
|
||||
$this->httpRequestFactory = $httpRequestFactory;
|
||||
$this->logger = $logger;
|
||||
$this->parsoidOutputStash = $parsoidOutputStash;
|
||||
$this->statsDataFactory = $statsDataFactory;
|
||||
$this->parsoidOutputAccess = $parsoidOutputAccess;
|
||||
$this->htmlTransformFactory = $htmlTransformFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ParsoidClient for accessing Parsoid.
|
||||
*
|
||||
* @param string|string[]|false $cookiesToForward
|
||||
* @param Authority|null $performer
|
||||
*
|
||||
* @return ParsoidClient
|
||||
*/
|
||||
public function createParsoidClient( $cookiesToForward ): ParsoidClient {
|
||||
public function createParsoidClient( $cookiesToForward, ?Authority $performer = null ): ParsoidClient {
|
||||
if ( $performer === null ) {
|
||||
$performer = RequestContext::getMain()->getAuthority();
|
||||
}
|
||||
// Default to using the direct client.
|
||||
$client = $this->createDirectClient();
|
||||
$client = $this->createDirectClient( $performer );
|
||||
|
||||
if ( !$client ) {
|
||||
// Default to using the direct client.
|
||||
|
@ -122,9 +126,13 @@ class VisualEditorParsoidClientFactory {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create a ParsoidClient for accessing Parsoid.
|
||||
*
|
||||
* @param Authority $performer
|
||||
*
|
||||
* @return ?ParsoidClient
|
||||
*/
|
||||
private function createDirectClient(): ?ParsoidClient {
|
||||
private function createDirectClient( Authority $performer ): ?ParsoidClient {
|
||||
// We haven't checked configuration yet.
|
||||
// Check to see if any of the restbase-related configuration
|
||||
// variables are set, and bail if so:
|
||||
|
@ -142,11 +150,11 @@ class VisualEditorParsoidClientFactory {
|
|||
}
|
||||
|
||||
return new DirectParsoidClient(
|
||||
$this->options->get( MainConfigNames::ParsoidSettings ),
|
||||
$this->siteConfig,
|
||||
$this->pageConfigFactory,
|
||||
$this->dataAccess,
|
||||
$this->globalIdGenerator
|
||||
$this->parsoidOutputStash,
|
||||
$this->statsDataFactory,
|
||||
$this->parsoidOutputAccess,
|
||||
$this->htmlTransformFactory,
|
||||
$performer
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -165,11 +173,6 @@ class VisualEditorParsoidClientFactory {
|
|||
* @return VirtualRESTService the VirtualRESTService object to use
|
||||
*/
|
||||
private function createVRSObject( $forwardCookies ): VirtualRESTService {
|
||||
Assert::precondition(
|
||||
!$this->createDirectClient(),
|
||||
"Direct Parsoid access is configured but the VirtualRESTService was used"
|
||||
);
|
||||
|
||||
// the params array to create the service object with
|
||||
$params = [];
|
||||
// the VRS class to use, defaults to Parsoid
|
||||
|
|
|
@ -66,63 +66,105 @@ describe( 'Visual Editor API', function () {
|
|||
// VisualEditor edit: 'visualeditoredit' action API ///
|
||||
const page = utils.title( 'VisualEditorNew' );
|
||||
|
||||
it( 'Should create page, edit and save page', async () => {
|
||||
const token = await alice.token();
|
||||
const html = '<p>save paction</p>';
|
||||
const summary = 'save test workflow';
|
||||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
paction: 'save',
|
||||
token: token,
|
||||
html: html,
|
||||
summary: summary
|
||||
},
|
||||
'post'
|
||||
);
|
||||
describe( 'Editing', function () {
|
||||
it( 'Should create page, edit and save page with HTML', async () => {
|
||||
const token = await alice.token();
|
||||
const html = '<p>save paction</p>';
|
||||
const summary = 'save test workflow';
|
||||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
paction: 'save',
|
||||
token: token,
|
||||
html: html,
|
||||
summary: summary
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
assert.equal( result.visualeditoredit.result, 'success' );
|
||||
} );
|
||||
assert.equal( result.visualeditoredit.result, 'success' );
|
||||
} );
|
||||
|
||||
it( 'Should refuse to edit with a bad token', async () => {
|
||||
const token = 'dshfkjdsakf';
|
||||
const html = '<p>save paction</p>';
|
||||
const summary = 'save test workflow';
|
||||
const error = await alice.actionError(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
paction: 'save',
|
||||
token: token,
|
||||
html: html,
|
||||
summary: summary
|
||||
},
|
||||
'post'
|
||||
);
|
||||
it( 'Should refuse to edit with a bad token', async () => {
|
||||
const token = 'dshfkjdsakf';
|
||||
const html = '<p>save paction</p>';
|
||||
const summary = 'save test workflow';
|
||||
const error = await alice.actionError(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
paction: 'save',
|
||||
token: token,
|
||||
html: html,
|
||||
summary: summary
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
assert.equal( error.code, 'badtoken' );
|
||||
} );
|
||||
assert.equal( error.code, 'badtoken' );
|
||||
} );
|
||||
|
||||
it( 'Should edit page and save with Wikitext', async () => {
|
||||
const token = await alice.token();
|
||||
const html = '<p>save paction</p>';
|
||||
const summary = 'save test workflow';
|
||||
const wikitext = 'wikitext string in page test';
|
||||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
paction: 'save',
|
||||
token: token,
|
||||
html: html,
|
||||
wikitext: wikitext,
|
||||
summary: summary
|
||||
},
|
||||
'post'
|
||||
);
|
||||
assert.equal( result.visualeditoredit.result, 'success' );
|
||||
assert.include( result.visualeditoredit.content, wikitext );
|
||||
it( 'Should use selser when editing', async () => {
|
||||
const token = await alice.token();
|
||||
let result;
|
||||
|
||||
// Create a page with messy wikitext
|
||||
const originalWikitext = '*a\n* b\n* <i>c</I>';
|
||||
|
||||
result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page,
|
||||
paction: 'save',
|
||||
token,
|
||||
wikitext: originalWikitext,
|
||||
summary: 'editing wikitext'
|
||||
},
|
||||
'post'
|
||||
);
|
||||
assert.equal( result.visualeditoredit.result, 'success' );
|
||||
|
||||
// Fetch HTML for editing
|
||||
result = await alice.action( 'visualeditor', { page, paction: 'parse' } );
|
||||
assert.equal( result.visualeditor.result, 'success' );
|
||||
|
||||
let html = result.visualeditor.content;
|
||||
const etag = result.visualeditor.etag;
|
||||
const oldid = result.visualeditor.oldid;
|
||||
|
||||
// Append to HTML
|
||||
html = html.replace( '</body>', '<p>More Text</p></body>' );
|
||||
result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page,
|
||||
paction: 'save',
|
||||
token,
|
||||
html,
|
||||
etag,
|
||||
oldid,
|
||||
summary: 'appending html'
|
||||
},
|
||||
'post'
|
||||
);
|
||||
|
||||
// TODO: Make a test that will fail if the etag is not used to look up stashed HTML.
|
||||
// This test will pass even if stashing is not used, because in that case
|
||||
// the base revision will be re-rendered, and the HTML will still match.
|
||||
|
||||
assert.equal( result.visualeditoredit.result, 'success' );
|
||||
|
||||
// Fetch wikitext to check
|
||||
result = await alice.action( 'visualeditor', { page, paction: 'wikitext' } );
|
||||
assert.equal( result.visualeditor.result, 'success' );
|
||||
|
||||
// Make sure the new content was appended, but the wikitext was kept
|
||||
// in its original messy state.
|
||||
const newWikitext = result.visualeditor.content;
|
||||
assert.include( newWikitext, originalWikitext );
|
||||
assert.include( newWikitext, 'More Text' );
|
||||
} );
|
||||
} );
|
||||
|
||||
it( 'Should show page diff', async () => {
|
||||
|
@ -132,7 +174,7 @@ describe( 'Visual Editor API', function () {
|
|||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
page: title,
|
||||
paction: 'diff',
|
||||
token: token,
|
||||
html: html,
|
||||
|
@ -150,7 +192,7 @@ describe( 'Visual Editor API', function () {
|
|||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
page: title,
|
||||
paction: 'serialize',
|
||||
token: token,
|
||||
html: html,
|
||||
|
@ -171,7 +213,7 @@ describe( 'Visual Editor API', function () {
|
|||
const result = await alice.action(
|
||||
'visualeditoredit',
|
||||
{
|
||||
page: page,
|
||||
page: title,
|
||||
paction: 'serializeforcache',
|
||||
token: token,
|
||||
html: html,
|
||||
|
|
|
@ -6,6 +6,8 @@ use Generator;
|
|||
use Language;
|
||||
use MediaWiki\Extension\VisualEditor\DirectParsoidClient;
|
||||
use MediaWiki\Page\PageIdentityValue;
|
||||
use MediaWiki\Parser\Parsoid\ParsoidOutputAccess;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
|
||||
/**
|
||||
|
@ -20,11 +22,11 @@ class DirectParsoidClientTest extends MediaWikiIntegrationTestCase {
|
|||
private function createDirectClient(): DirectParsoidClient {
|
||||
$services = $this->getServiceContainer();
|
||||
$directClient = new DirectParsoidClient(
|
||||
[],
|
||||
$services->getParsoidSiteConfig(),
|
||||
$services->getParsoidPageConfigFactory(),
|
||||
$services->getParsoidDataAccess(),
|
||||
$services->getGlobalIdGenerator()
|
||||
$services->getParsoidOutputStash(),
|
||||
$services->getStatsdDataFactory(),
|
||||
$services->getParsoidOutputAccess(),
|
||||
$services->getHTMLTransformFactory(),
|
||||
$services->getUserFactory()->newAnonymous()
|
||||
);
|
||||
|
||||
return $directClient;
|
||||
|
@ -81,7 +83,7 @@ class DirectParsoidClientTest extends MediaWikiIntegrationTestCase {
|
|||
$this->assertStringContainsString( 'lang="' . $langCode . '"', $pageHtml );
|
||||
|
||||
$this->assertArrayHasKey( 'etag', $headers );
|
||||
$this->assertStringContainsString( 'W/"' . $revision->getId(), $headers['etag'] );
|
||||
$this->assertStringContainsString( (string)$revision->getId(), $headers['etag'] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -100,19 +102,19 @@ class DirectParsoidClientTest extends MediaWikiIntegrationTestCase {
|
|||
|
||||
$html = '<h2>Hello World</h2>';
|
||||
$oldid = $pageIdentity->getId();
|
||||
$etag = 'W/"' . $oldid . '/abc-123"';
|
||||
|
||||
$response = $directClient->transformHTML(
|
||||
$pageIdentity,
|
||||
$language,
|
||||
$html,
|
||||
$oldid,
|
||||
$etag
|
||||
// Supplying "null" will use the $oldid and look at recent rendering in ParserCache.
|
||||
null
|
||||
);
|
||||
|
||||
$this->assertIsArray( $response );
|
||||
$this->assertArrayHasKey( 'headers', $response );
|
||||
$this->assertSame( [], $response['headers'] );
|
||||
$this->assertArrayHasKey( 'Content-Type', $response['headers'] );
|
||||
|
||||
$this->assertArrayHasKey( 'body', $response );
|
||||
// Trim to remove trailing newline
|
||||
|
@ -128,15 +130,16 @@ class DirectParsoidClientTest extends MediaWikiIntegrationTestCase {
|
|||
$directClient = $this->createDirectClient();
|
||||
|
||||
$page = $this->getExistingTestPage( 'DirectParsoidClient' );
|
||||
$pageRecord = $page->toPageRecord();
|
||||
$wikitext = '== Hello World ==';
|
||||
[ $language, $langCode ] = $this->createLanguage( $langCode );
|
||||
|
||||
$response = $directClient->transformWikitext(
|
||||
$page,
|
||||
$pageRecord,
|
||||
$language,
|
||||
$wikitext,
|
||||
false,
|
||||
$page->getId(),
|
||||
$pageRecord->getLatest(),
|
||||
false
|
||||
);
|
||||
|
||||
|
@ -152,4 +155,75 @@ class DirectParsoidClientTest extends MediaWikiIntegrationTestCase {
|
|||
$this->assertStringContainsString( '>Hello World</h2>', $html );
|
||||
}
|
||||
|
||||
/** @covers ::transformHTML */
|
||||
public function testRoundTripSelserWithETag() {
|
||||
$directClient = $this->createDirectClient();
|
||||
|
||||
// Nasty wikitext that would be reformated without selser.
|
||||
$originalWikitext = '*a\n* b\n* <i>c</I>';
|
||||
|
||||
/** @var RevisionRecord $revision */
|
||||
$revision = $this->editPage( 'RoundTripSelserWithETag', $originalWikitext )
|
||||
->getValue()['revision-record'];
|
||||
|
||||
$pageHtmlResponse = $directClient->getPageHtml( $revision );
|
||||
$eTag = $pageHtmlResponse['headers']['etag'];
|
||||
$oldHtml = $pageHtmlResponse['body'];
|
||||
$updatedHtml = str_replace( '</body>', '<p>More Text</p></body>', $oldHtml );
|
||||
|
||||
// Now make a new client object, so we can mock the ParsoidOutputAccess.
|
||||
$parsoidOutputAccess = $this->createNoOpMock( ParsoidOutputAccess::class );
|
||||
$services = $this->getServiceContainer();
|
||||
$directClient = new DirectParsoidClient(
|
||||
$services->getParsoidOutputStash(),
|
||||
$services->getStatsdDataFactory(),
|
||||
$parsoidOutputAccess,
|
||||
$services->getHTMLTransformFactory(),
|
||||
$services->getUserFactory()->newAnonymous()
|
||||
);
|
||||
|
||||
[ $targetLanguage, ] = $this->createLanguage( 'en' );
|
||||
$transformHtmlResponse = $directClient->transformHTML(
|
||||
$revision->getPage(),
|
||||
$targetLanguage,
|
||||
$updatedHtml,
|
||||
$revision->getId(),
|
||||
$eTag
|
||||
);
|
||||
|
||||
$updatedWikitext = $transformHtmlResponse['body'];
|
||||
$this->assertStringContainsString( $originalWikitext, $updatedWikitext );
|
||||
$this->assertStringContainsString( 'More Text', $updatedWikitext );
|
||||
}
|
||||
|
||||
/** @covers ::transformHTML */
|
||||
public function testRoundTripSelserWithoutETag() {
|
||||
$directClient = $this->createDirectClient();
|
||||
|
||||
// Nasty wikitext that would be reformated without selser.
|
||||
$originalWikitext = '*a\n* b\n* <i>c</I>';
|
||||
|
||||
/** @var RevisionRecord $revision */
|
||||
$revision = $this->editPage( 'RoundTripSelserWithoutETag', $originalWikitext )
|
||||
->getValue()['revision-record'];
|
||||
|
||||
$pageHtmlResponse = $directClient->getPageHtml( $revision );
|
||||
$oldHtml = $pageHtmlResponse['body'];
|
||||
$updatedHtml = str_replace( '</body>', '<p>More Text</p></body>', $oldHtml );
|
||||
|
||||
[ $targetLanguage, ] = $this->createLanguage( 'en' );
|
||||
$transformHtmlResponse = $directClient->transformHTML(
|
||||
$revision->getPage(),
|
||||
$targetLanguage,
|
||||
$updatedHtml,
|
||||
$revision->getId(),
|
||||
null
|
||||
);
|
||||
|
||||
// Selser should still work, because the current rendering of the page still matches.
|
||||
$updatedWikitext = $transformHtmlResponse['body'];
|
||||
$this->assertStringContainsString( $originalWikitext, $updatedWikitext );
|
||||
$this->assertStringContainsString( 'More Text', $updatedWikitext );
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,21 +2,21 @@
|
|||
|
||||
namespace MediaWiki\Extension\VisualEditor\Tests;
|
||||
|
||||
use IBufferingStatsdDataFactory;
|
||||
use MediaWiki\Config\ServiceOptions;
|
||||
use MediaWiki\Edit\ParsoidOutputStash;
|
||||
use MediaWiki\Extension\VisualEditor\DirectParsoidClient;
|
||||
use MediaWiki\Extension\VisualEditor\VisualEditorParsoidClientFactory;
|
||||
use MediaWiki\Extension\VisualEditor\VRSParsoidClient;
|
||||
use MediaWiki\Http\HttpRequestFactory;
|
||||
use MediaWiki\MainConfigNames;
|
||||
use MediaWiki\Parser\Parsoid\Config\DataAccess;
|
||||
use MediaWiki\Parser\Parsoid\Config\PageConfigFactory;
|
||||
use MediaWiki\Parser\Parsoid\Config\SiteConfig;
|
||||
use MediaWiki\Parser\Parsoid\HTMLTransformFactory;
|
||||
use MediaWiki\Parser\Parsoid\ParsoidOutputAccess;
|
||||
use MediaWikiIntegrationTestCase;
|
||||
use MultiHttpClient;
|
||||
use ParsoidVirtualRESTService;
|
||||
use Psr\Log\NullLogger;
|
||||
use Wikimedia\TestingAccessWrapper;
|
||||
use Wikimedia\UUID\GlobalIdGenerator;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \MediaWiki\Extension\VisualEditor\VisualEditorParsoidClientFactory
|
||||
|
@ -43,12 +43,12 @@ class VisualEditorParsoidClientFactoryTest extends MediaWikiIntegrationTestCase
|
|||
|
||||
return new VisualEditorParsoidClientFactory(
|
||||
$options,
|
||||
$this->createNoOpMock( SiteConfig::class ),
|
||||
$this->createNoOpMock( PageConfigFactory::class ),
|
||||
$this->createNoOpMock( DataAccess::class ),
|
||||
$this->createNoOpMock( GlobalIdGenerator::class ),
|
||||
$httpRequestFactory,
|
||||
new NullLogger()
|
||||
new NullLogger(),
|
||||
$this->createNoOpMock( ParsoidOutputStash::class ),
|
||||
$this->createNoOpMock( IBufferingStatsdDataFactory::class ),
|
||||
$this->createNoOpMock( ParsoidOutputAccess::class ),
|
||||
$this->createNoOpMock( HTMLTransformFactory::class )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue