mediawiki-extensions-Visual.../includes/DirectParsoidClient.php
thiemowmde ab2cbb3b70 Change almost all code to use Bcp47Code instead of Language
This patch makes the following assumptions:

1. There is no class outside of this codebase that implements the
ParsoidClient interface. According to CodeSearch this appears to be
correct.

2. It is fine to use the Bcp47Code instead of the internal MediaWiki
language code in the "Accept-Language" HTTP header. As far as I
understand this might even qualify as a bugfix.

Depends-On: If059674597e261039df7f4613a89cb08120c3262
Change-Id: Icd160da2d5555891b9a91a0cb966bd36a55f6fdd
2023-07-12 09:38:16 +02:00

298 lines
7.7 KiB
PHP

<?php
/**
* Helper functions for using the REST interface to Parsoid.
*
* @file
* @ingroup Extensions
* @copyright 2022 VisualEditor Team and others; see AUTHORS.txt
* @license MIT
*/
namespace MediaWiki\Extension\VisualEditor;
use Exception;
use LocalizedException;
use MediaWiki\Page\PageIdentity;
use MediaWiki\Parser\Parsoid\ParsoidRenderID;
use MediaWiki\Permissions\Authority;
use MediaWiki\Rest\Handler\Helper\HtmlInputTransformHelper;
use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper;
use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
use MediaWiki\Rest\HttpException;
use MediaWiki\Rest\LocalizedHttpException;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use RawMessage;
use User;
use Wikimedia\Bcp47Code\Bcp47Code;
use WikitextContent;
class DirectParsoidClient implements ParsoidClient {
/**
* Requested Parsoid HTML version.
* Keep this in sync with the Accept: header in
* ve.init.mw.ArticleTargetLoader.js
*/
public const PARSOID_VERSION = '2.8.0';
private const FLAVOR_DEFAULT = 'view';
/** @var PageRestHelperFactory */
private $helperFactory;
/** @var Authority */
private $performer;
/**
* @param PageRestHelperFactory $helperFactory
* @param Authority $performer
*/
public function __construct(
PageRestHelperFactory $helperFactory,
Authority $performer
) {
$this->performer = $performer;
$this->helperFactory = $helperFactory;
}
/**
* @param PageIdentity $page
* @param RevisionRecord|null $revision
* @param Bcp47Code|null $pageLanguage
* @param bool $stash
* @param string $flavor
*
* @return HtmlOutputRendererHelper
*/
private function getHtmlOutputRendererHelper(
PageIdentity $page,
?RevisionRecord $revision = null,
Bcp47Code $pageLanguage = null,
bool $stash = false,
string $flavor = self::FLAVOR_DEFAULT
): HtmlOutputRendererHelper {
$helper = $this->helperFactory->newHtmlOutputRendererHelper();
// TODO: remove this once we no longer need a User object for rate limiting (T310476).
if ( $this->performer instanceof User ) {
$user = $this->performer;
} else {
$user = User::newFromIdentity( $this->performer->getUser() );
}
$helper->init( $page, [], $user, $revision );
// Ensure we get a compatible version, not just the default
$helper->setOutputProfileVersion( self::PARSOID_VERSION );
$helper->setStashingEnabled( $stash );
if ( !$stash ) {
$helper->setFlavor( $flavor );
}
if ( $revision ) {
$helper->setRevision( $revision );
}
if ( $pageLanguage ) {
$helper->setPageLanguage( $pageLanguage );
}
return $helper;
}
/**
* @param PageIdentity $page
* @param string $html
* @param int|null $oldid
* @param string|null $etag
* @param Bcp47Code|null $pageLanguage
*
* @return HtmlInputTransformHelper
*/
private function getHtmlInputTransformHelper(
PageIdentity $page,
string $html,
int $oldid = null,
string $etag = null,
Bcp47Code $pageLanguage = null
): HtmlInputTransformHelper {
$helper = $this->helperFactory->newHtmlInputTransformHelper();
// Fake REST body
$body = [
'html' => [
'body' => $html,
]
];
$renderId = $etag ? ParsoidRenderID::newFromETag( $etag ) : null;
$helper->init( $page, $body, [], null, $pageLanguage );
if ( $oldid || $renderId ) {
$helper->setOriginal( $oldid, $renderId );
}
return $helper;
}
/**
* Request page HTML from Parsoid.
*
* @param RevisionRecord $revision Page revision
* @param ?Bcp47Code $targetLanguage Page language (default: `null`)
*
* @return array An array mimicking a RESTbase server's response,
* with keys: 'error', 'headers' and 'body'
*/
public function getPageHtml( RevisionRecord $revision, ?Bcp47Code $targetLanguage = null ): array {
// In the VE client, we always want to stash.
$page = $revision->getPage();
try {
$helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, true );
$parserOutput = $helper->getHtml();
return $this->fakeRESTbaseHTMLResponse( $parserOutput->getRawText(), $helper );
} catch ( HttpException $ex ) {
return $this->fakeRESTbaseError( $ex );
}
}
/**
* @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 Bcp47Code $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,
Bcp47Code $targetLanguage,
string $wikitext,
bool $bodyOnly,
?int $oldid,
bool $stash
): array {
$revision = $this->makeFakeRevision( $page, $wikitext );
try {
$helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, $stash );
if ( $bodyOnly ) {
$helper->setFlavor( 'fragment' );
}
$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 Bcp47Code $targetLanguage The desired output language
* @param string $html The HTML of the page to be transformed
* @param ?int $oldid What oldid revision, if any, to base the request from (default: `null`)
* @param ?string $etag The ETag to set in the HTTP request header
*
* @return array The response, 'code', 'reason', 'headers' and 'body'
*/
public function transformHTML(
PageIdentity $page, Bcp47Code $targetLanguage, string $html, ?int $oldid, ?string $etag
): array {
try {
$helper = $this->getHtmlInputTransformHelper( $page, $html, $oldid, $etag, $targetLanguage );
$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 {
$contentLanguage = $helper->getHtmlOutputContentLanguage();
return [
'code' => 200,
'headers' => [
'content-language' => $contentLanguage->toBcp47Code(),
'etag' => $helper->getETag()
],
'body' => $data,
];
}
/**
* @param Exception $ex
*
* @return array
*/
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() );
}
return [
'error' => [
'message' => $msg->getKey() ?? '',
'params' => $msg->getParams() ?? []
],
'headers' => [],
'body' => $ex->getMessage(),
];
}
}