mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/VisualEditor
synced 2024-09-23 18:28:51 +00:00
Merge "Create Parsoid helper for use outside of action API"
This commit is contained in:
commit
efdde38f85
|
@ -23,10 +23,8 @@
|
|||
},
|
||||
"apierror-visualeditor-badcachekey": "No cached serialization found with that key",
|
||||
"apierror-visualeditor-difffailed": "Diff failed",
|
||||
"apierror-visualeditor-docserver-bad-config": "Direct Parsoid access is configured but the VirtualRESTService was used",
|
||||
"apierror-visualeditor-docserver-http-error": "Error contacting the Parsoid/RESTBase server: $1",
|
||||
"apierror-visualeditor-docserver-http": "Error contacting the Parsoid/RESTBase server (HTTP $1)",
|
||||
"apierror-visualeditor-docserver-unconfigured": "The VirtualRESTService for the document server is not defined; see https://www.mediawiki.org/wiki/Extension:VisualEditor",
|
||||
"apierror-visualeditor-docserver": "Error contacting the Parsoid/RESTBase server (no response)",
|
||||
"apierror-visualeditor-latestnotfound": "Could not find latest revision for title",
|
||||
"apihelp-visualeditor-description": "Returns HTML5 for a page from the Parsoid service.",
|
||||
|
|
|
@ -32,10 +32,8 @@
|
|||
},
|
||||
"apierror-visualeditor-badcachekey": "{{doc-apierror}}",
|
||||
"apierror-visualeditor-difffailed": "{{doc-apierror}}",
|
||||
"apierror-visualeditor-docserver-bad-config": "{{doc-apierror}}",
|
||||
"apierror-visualeditor-docserver-http-error": "{{Related|apierror-visualeditor-docserver}}\n{{doc-apierror}}\n\nParameters:\n* $1 - Error message, probably in English",
|
||||
"apierror-visualeditor-docserver-http": "{{Related|apierror-visualeditor-docserver}}\n{{doc-apierror}}\n\nParameters:\n* $1 - HTTP status code (numeric)",
|
||||
"apierror-visualeditor-docserver-unconfigured": "{{doc-apierror}}",
|
||||
"apierror-visualeditor-docserver": "{{Related|apierror-visualeditor-docserver}}\n{{doc-apierror}}",
|
||||
"apierror-visualeditor-latestnotfound": "{{doc-apierror}}",
|
||||
"apihelp-visualeditor-description": "{{doc-apihelp-description|visualeditor}}",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper functions for contacting Parsoid/RESTBase.
|
||||
* Helper functions for contacting Parsoid/RESTBase from the action API.
|
||||
*
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
|
@ -15,36 +15,38 @@ use Language;
|
|||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use Message;
|
||||
use ParsoidVirtualRESTService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Psr\Log\NullLogger;
|
||||
use RestbaseVirtualRESTService;
|
||||
use RuntimeException;
|
||||
use StatusValue;
|
||||
use Title;
|
||||
use VirtualRESTService;
|
||||
use VirtualRESTServiceClient;
|
||||
use WebRequest;
|
||||
|
||||
trait ApiParsoidTrait {
|
||||
|
||||
/**
|
||||
* A direct Parsoid client for zero-configuration mode.
|
||||
* Initially `false`, then once we determine whether we're using zeroconf
|
||||
* mode or not then it will be a ?VisualEditorParsoidClient.
|
||||
* @var VisualEditorParsoidClient|null|false
|
||||
* @var ParsoidHelper
|
||||
*/
|
||||
private $directClient = false;
|
||||
|
||||
/**
|
||||
* @var VirtualRESTServiceClient
|
||||
*/
|
||||
private $serviceClient = null;
|
||||
private $helper = null;
|
||||
|
||||
/**
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger = null;
|
||||
|
||||
/**
|
||||
* @return ParsoidHelper
|
||||
*/
|
||||
protected function getHelper(): ParsoidHelper {
|
||||
if ( !$this->helper ) {
|
||||
$this->helper = new ParsoidHelper(
|
||||
$this->getConfig(),
|
||||
$this->getLogger(),
|
||||
$this->getRequest()->getHeader( 'Cookie' )
|
||||
);
|
||||
}
|
||||
return $this->helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LoggerInterface
|
||||
*/
|
||||
|
@ -59,174 +61,6 @@ trait ApiParsoidTrait {
|
|||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the VisualEditorParsoidClient used for direct access to
|
||||
* Parsoid.
|
||||
* @return ?VisualEditorParsoidClient null if a VirtualRESTService is
|
||||
* to be used.
|
||||
*/
|
||||
private function getDirectClient(): ?VisualEditorParsoidClient {
|
||||
if ( $this->directClient === false ) {
|
||||
// We haven't checked configuration yet.
|
||||
// Check to see if any of the restbase-related configuration
|
||||
// variables are set, and bail if so:
|
||||
$vrs = $this->getConfig()->get( 'VirtualRestConfig' );
|
||||
if ( isset( $vrs['modules'] ) &&
|
||||
( isset( $vrs['modules']['restbase'] ) ||
|
||||
isset( $vrs['modules']['parsoid'] ) )
|
||||
) {
|
||||
$this->directClient = null;
|
||||
return null;
|
||||
}
|
||||
// Eventually we'll do something fancy, but I'm hacking here...
|
||||
global $wgVisualEditorParsoidAutoConfig;
|
||||
if ( !$wgVisualEditorParsoidAutoConfig ) {
|
||||
// explicit opt out
|
||||
$this->directClient = null;
|
||||
return null;
|
||||
}
|
||||
// Default to using the direct client.
|
||||
$this->directClient = VisualEditorParsoidClient::factory();
|
||||
}
|
||||
return $this->directClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the virtual REST service object to be used in VE's API calls. The
|
||||
* method determines whether to instantiate a ParsoidVirtualRESTService or a
|
||||
* RestbaseVirtualRESTService object based on configuration directives: if
|
||||
* $wgVirtualRestConfig['modules']['restbase'] is defined, RESTBase is chosen,
|
||||
* otherwise Parsoid is used (either by using the MW Core config, or the
|
||||
* VE-local one).
|
||||
*
|
||||
* @return VirtualRESTService the VirtualRESTService object to use
|
||||
*/
|
||||
private function getVRSObject(): VirtualRESTService {
|
||||
if ( $this->getDirectClient() ) {
|
||||
$this->dieWithError(
|
||||
'apierror-visualeditor-docserver-bad-config',
|
||||
'apierror-visualeditor-docserver-bad-config'
|
||||
);
|
||||
}
|
||||
global $wgVisualEditorParsoidAutoConfig;
|
||||
// the params array to create the service object with
|
||||
$params = [];
|
||||
// the VRS class to use, defaults to Parsoid
|
||||
$class = ParsoidVirtualRESTService::class;
|
||||
// The global virtual rest service config object, if any
|
||||
$vrs = $this->getConfig()->get( 'VirtualRestConfig' );
|
||||
if ( isset( $vrs['modules'] ) && isset( $vrs['modules']['restbase'] ) ) {
|
||||
// if restbase is available, use it
|
||||
$params = $vrs['modules']['restbase'];
|
||||
// backward compatibility
|
||||
$params['parsoidCompat'] = false;
|
||||
$class = RestbaseVirtualRESTService::class;
|
||||
} elseif ( isset( $vrs['modules'] ) && isset( $vrs['modules']['parsoid'] ) ) {
|
||||
// there's a global parsoid config, use it next
|
||||
$params = $vrs['modules']['parsoid'];
|
||||
$params['restbaseCompat'] = true;
|
||||
} elseif ( $wgVisualEditorParsoidAutoConfig ) {
|
||||
$params = $vrs['modules']['parsoid'] ?? [];
|
||||
$params['restbaseCompat'] = true;
|
||||
// forward cookies on private wikis
|
||||
$params['forwardCookies'] = !MediaWikiServices::getInstance()
|
||||
->getPermissionManager()->isEveryoneAllowed( 'read' );
|
||||
} else {
|
||||
// No global modules defined, so no way to contact the document server.
|
||||
$this->dieWithError( 'apierror-visualeditor-docserver-unconfigured', 'no_vrs' );
|
||||
}
|
||||
// merge the global and service-specific params
|
||||
if ( isset( $vrs['global'] ) ) {
|
||||
$params = array_merge( $vrs['global'], $params );
|
||||
}
|
||||
// set up cookie forwarding
|
||||
if ( $params['forwardCookies'] ) {
|
||||
$params['forwardCookies'] = $this->getRequest()->getHeader( 'Cookie' );
|
||||
} else {
|
||||
$params['forwardCookies'] = false;
|
||||
}
|
||||
// create the VRS object and return it
|
||||
return new $class( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the object which directs queries to the virtual REST service, depending on the path.
|
||||
*
|
||||
* @return VirtualRESTServiceClient
|
||||
*/
|
||||
private function getVRSClient(): VirtualRESTServiceClient {
|
||||
if ( !$this->serviceClient ) {
|
||||
$this->serviceClient = new VirtualRESTServiceClient(
|
||||
MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient() );
|
||||
$this->serviceClient->mount( '/restbase/', $this->getVRSObject() );
|
||||
}
|
||||
return $this->serviceClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor function for all RESTbase requests
|
||||
*
|
||||
* @param Title $title The title of the page to use as the parsing context
|
||||
* @param string $method The HTTP method, either 'GET' or 'POST'
|
||||
* @param string $path The RESTbase api path
|
||||
* @param array $params Request parameters
|
||||
* @param array $reqheaders Request headers
|
||||
* @return array The RESTbase server's response, 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
private function requestRestbase(
|
||||
Title $title, string $method, string $path, array $params, array $reqheaders = []
|
||||
): array {
|
||||
// Should be synchronised with requestParsoidData() in
|
||||
// modules/ve-mw/preinit/ve.init.mw.ArticleTargetLoader.js
|
||||
$profile = 'https://www.mediawiki.org/wiki/Specs/HTML/' .
|
||||
VisualEditorParsoidClient::PARSOID_VERSION;
|
||||
$reqheaders += [
|
||||
'Accept' =>
|
||||
"text/html; charset=utf-8; profile=\"$profile\"",
|
||||
'Accept-Language' => self::getPageLanguage( $title )->getCode(),
|
||||
'User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
||||
'Api-User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
||||
'Promise-Non-Write-API-Action' => 'true',
|
||||
];
|
||||
$request = [
|
||||
'method' => $method,
|
||||
'url' => '/restbase/local/v1/' . $path,
|
||||
( $method === 'GET' ? 'query' : 'body' ) => $params,
|
||||
'headers' => $reqheaders,
|
||||
];
|
||||
$response = $this->getVRSClient()->run( $request );
|
||||
if ( $response['code'] === 200 && $response['error'] === "" ) {
|
||||
// If response was served directly from Varnish, use the response
|
||||
// (RP) header to declare the cache hit and pass the data to the client.
|
||||
$headers = $response['headers'];
|
||||
if ( isset( $headers['x-cache'] ) && strpos( $headers['x-cache'], 'hit' ) !== false ) {
|
||||
$this->getRequest()->response()->header( 'X-Cache: cached-response=true' );
|
||||
}
|
||||
} elseif ( $response['error'] !== '' ) {
|
||||
$this->dieWithError(
|
||||
[ 'apierror-visualeditor-docserver-http-error', wfEscapeWikiText( $response['error'] ) ],
|
||||
'apierror-visualeditor-docserver-http-error'
|
||||
);
|
||||
} else {
|
||||
// error null, code not 200
|
||||
$this->getLogger()->warning(
|
||||
__METHOD__ . ": Received HTTP {code} from RESTBase",
|
||||
[
|
||||
'code' => $response['code'],
|
||||
'exception' => new RuntimeException(),
|
||||
'response' => [ 'body' => $response['body'] ],
|
||||
'requestPath' => $path,
|
||||
'requestIfMatch' => $reqheaders['If-Match'] ?? '',
|
||||
]
|
||||
);
|
||||
$this->dieWithError(
|
||||
[ 'apierror-visualeditor-docserver-http', $response['code'] ],
|
||||
'apierror-visualeditor-docserver-http'
|
||||
);
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest revision of a title
|
||||
*
|
||||
|
@ -267,6 +101,26 @@ trait ApiParsoidTrait {
|
|||
$this->dieWithError( [ 'apierror-nosuchrevid', $oldid ], 'oldidnotfound' );
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatusValue $status
|
||||
*/
|
||||
private function forwardErrorsAndCacheHeaders( StatusValue $status ) {
|
||||
if ( !$status->isOK() ) {
|
||||
$this->dieStatus( $status );
|
||||
}
|
||||
|
||||
$response = $status->getValue();
|
||||
// Only set when using RESTBase
|
||||
if ( isset( $response['code'] ) && $response['code'] === 200 ) {
|
||||
// If response was served directly from Varnish, use the response
|
||||
// (RP) header to declare the cache hit and pass the data to the client.
|
||||
$headers = $response['headers'];
|
||||
if ( isset( $headers['x-cache'] ) && strpos( $headers['x-cache'], 'hit' ) !== false ) {
|
||||
$this->getRequest()->response()->header( 'X-Cache: cached-response=true' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request page HTML from RESTBase
|
||||
*
|
||||
|
@ -275,20 +129,13 @@ trait ApiParsoidTrait {
|
|||
*/
|
||||
protected function requestRestbasePageHtml( RevisionRecord $revision ): array {
|
||||
$title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return $client->getPageHtml(
|
||||
$revision, $this->getPageLanguage( $title )
|
||||
);
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'GET',
|
||||
'page/html/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
'/' . $revision->getId() .
|
||||
'?redirect=false&stash=true',
|
||||
[]
|
||||
);
|
||||
$lang = self::getPageLanguage( $title );
|
||||
|
||||
$status = $this->getHelper()->requestRestbasePageHtml( $revision, $lang );
|
||||
|
||||
$this->forwardErrorsAndCacheHeaders( $status );
|
||||
|
||||
return $status->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -303,39 +150,13 @@ trait ApiParsoidTrait {
|
|||
protected function transformHTML(
|
||||
Title $title, string $html, int $oldid = null, string $etag = null
|
||||
): array {
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return $client->transformHtml(
|
||||
$title, $this->getPageLanguage( $title ), $html, $oldid, $etag
|
||||
);
|
||||
}
|
||||
$data = [ 'html' => $html ];
|
||||
$path = 'transform/html/to/wikitext/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
( $oldid === null ? '' : '/' . $oldid );
|
||||
$lang = self::getPageLanguage( $title );
|
||||
|
||||
// Adapted from RESTBase mwUtil.parseETag()
|
||||
// ETag is not expected when creating a new page (oldid=0)
|
||||
if ( $oldid && !( preg_match( '/
|
||||
^(?:W\\/)?"?
|
||||
' . preg_quote( "$oldid", '/' ) . '
|
||||
(?:\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))
|
||||
(?:\\/([^"]+))?
|
||||
"?$
|
||||
/x', $etag ) ) ) {
|
||||
$this->getLogger()->info(
|
||||
__METHOD__ . ": Received funny ETag from client: '{etag}'",
|
||||
[
|
||||
'etag' => $etag,
|
||||
'oldid' => $oldid,
|
||||
'requestPath' => $path,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'POST', $path, $data,
|
||||
[ 'If-Match' => $etag ]
|
||||
);
|
||||
$status = $this->getHelper()->transformHTML( $title, $html, $oldid, $etag, $lang );
|
||||
|
||||
$this->forwardErrorsAndCacheHeaders( $status );
|
||||
|
||||
return $status->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -351,24 +172,13 @@ trait ApiParsoidTrait {
|
|||
protected function transformWikitext(
|
||||
Title $title, string $wikitext, bool $bodyOnly, int $oldid = null, bool $stash = false
|
||||
): array {
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return $client->transformWikitext(
|
||||
$title, $this->getPageLanguage( $title ),
|
||||
$wikitext, $bodyOnly, $oldid, $stash
|
||||
);
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'POST',
|
||||
'transform/wikitext/to/html/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
( $oldid === null ? '' : '/' . $oldid ),
|
||||
[
|
||||
'wikitext' => $wikitext,
|
||||
'body_only' => $bodyOnly ? 1 : 0,
|
||||
'stash' => $stash ? 1 : 0
|
||||
]
|
||||
);
|
||||
$lang = self::getPageLanguage( $title );
|
||||
|
||||
$status = $this->getHelper()->transformWikitext( $title, $wikitext, $bodyOnly, $oldid, $stash, $lang );
|
||||
|
||||
$this->forwardErrorsAndCacheHeaders( $status );
|
||||
|
||||
return $status->getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -398,6 +208,13 @@ trait ApiParsoidTrait {
|
|||
*/
|
||||
abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = null );
|
||||
|
||||
/**
|
||||
* @see ApiBase
|
||||
* @param StatusValue $status
|
||||
* @return never
|
||||
*/
|
||||
abstract public function dieStatus( StatusValue $status );
|
||||
|
||||
/**
|
||||
* @see ContextSource
|
||||
* @return Config
|
||||
|
|
340
includes/ParsoidHelper.php
Normal file
340
includes/ParsoidHelper.php
Normal file
|
@ -0,0 +1,340 @@
|
|||
<?php
|
||||
/**
|
||||
* Helper functions for contacting Parsoid/RESTBase.
|
||||
*
|
||||
* Because clearly we don't have enough APIs yet for accomplishing this Herculean task.
|
||||
*
|
||||
* @file
|
||||
* @ingroup Extensions
|
||||
* @copyright 2022 VisualEditor Team and others; see AUTHORS.txt
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
namespace MediaWiki\Extension\VisualEditor;
|
||||
|
||||
use Config;
|
||||
use ConfigException;
|
||||
use Language;
|
||||
use MediaWiki\MediaWikiServices;
|
||||
use MediaWiki\Revision\RevisionRecord;
|
||||
use ParsoidVirtualRESTService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RestbaseVirtualRESTService;
|
||||
use RuntimeException;
|
||||
use StatusValue;
|
||||
use Title;
|
||||
use VirtualRESTService;
|
||||
use VirtualRESTServiceClient;
|
||||
use Wikimedia\Assert\Assert;
|
||||
|
||||
class ParsoidHelper {
|
||||
|
||||
/**
|
||||
* A direct Parsoid client for zero-configuration mode.
|
||||
* Initially `false`, then once we determine whether we're using zeroconf
|
||||
* mode or not then it will be a ?VisualEditorParsoidClient.
|
||||
* @var VisualEditorParsoidClient|null|false
|
||||
*/
|
||||
private $directClient = false;
|
||||
|
||||
/** @var VirtualRESTServiceClient */
|
||||
private $serviceClient = null;
|
||||
|
||||
/** @var Config */
|
||||
private $config;
|
||||
|
||||
/** @var LoggerInterface */
|
||||
private $logger;
|
||||
|
||||
/** @var string|false */
|
||||
private $forwardCookies;
|
||||
|
||||
/**
|
||||
* @param Config $config
|
||||
* @param LoggerInterface $logger
|
||||
* @param string|false $forwardCookies
|
||||
*/
|
||||
public function __construct( Config $config, LoggerInterface $logger, $forwardCookies ) {
|
||||
$this->config = $config;
|
||||
$this->logger = $logger;
|
||||
$this->forwardCookies = $forwardCookies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the VisualEditorParsoidClient used for direct access to
|
||||
* Parsoid.
|
||||
* @return ?VisualEditorParsoidClient null if a VirtualRESTService is
|
||||
* to be used.
|
||||
*/
|
||||
private function getDirectClient(): ?VisualEditorParsoidClient {
|
||||
if ( $this->directClient === false ) {
|
||||
// We haven't checked configuration yet.
|
||||
// Check to see if any of the restbase-related configuration
|
||||
// variables are set, and bail if so:
|
||||
$vrs = $this->config->get( 'VirtualRestConfig' );
|
||||
if ( isset( $vrs['modules'] ) &&
|
||||
( isset( $vrs['modules']['restbase'] ) ||
|
||||
isset( $vrs['modules']['parsoid'] ) )
|
||||
) {
|
||||
$this->directClient = null;
|
||||
return null;
|
||||
}
|
||||
// Eventually we'll do something fancy, but I'm hacking here...
|
||||
global $wgVisualEditorParsoidAutoConfig;
|
||||
if ( !$wgVisualEditorParsoidAutoConfig ) {
|
||||
// explicit opt out
|
||||
$this->directClient = null;
|
||||
return null;
|
||||
}
|
||||
// Default to using the direct client.
|
||||
$this->directClient = VisualEditorParsoidClient::factory();
|
||||
}
|
||||
return $this->directClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the virtual REST service object to be used in VE's API calls. The
|
||||
* method determines whether to instantiate a ParsoidVirtualRESTService or a
|
||||
* RestbaseVirtualRESTService object based on configuration directives: if
|
||||
* $wgVirtualRestConfig['modules']['restbase'] is defined, RESTBase is chosen,
|
||||
* otherwise Parsoid is used (either by using the MW Core config, or the
|
||||
* VE-local one).
|
||||
*
|
||||
* @return VirtualRESTService the VirtualRESTService object to use
|
||||
*/
|
||||
private function getVRSObject(): VirtualRESTService {
|
||||
Assert::precondition(
|
||||
!$this->getDirectClient(),
|
||||
"Direct Parsoid access is configured but the VirtualRESTService was used"
|
||||
);
|
||||
|
||||
global $wgVisualEditorParsoidAutoConfig;
|
||||
// the params array to create the service object with
|
||||
$params = [];
|
||||
// the VRS class to use, defaults to Parsoid
|
||||
$class = ParsoidVirtualRESTService::class;
|
||||
// The global virtual rest service config object, if any
|
||||
$vrs = $this->config->get( 'VirtualRestConfig' );
|
||||
if ( isset( $vrs['modules'] ) && isset( $vrs['modules']['restbase'] ) ) {
|
||||
// if restbase is available, use it
|
||||
$params = $vrs['modules']['restbase'];
|
||||
// backward compatibility
|
||||
$params['parsoidCompat'] = false;
|
||||
$class = RestbaseVirtualRESTService::class;
|
||||
} elseif ( isset( $vrs['modules'] ) && isset( $vrs['modules']['parsoid'] ) ) {
|
||||
// there's a global parsoid config, use it next
|
||||
$params = $vrs['modules']['parsoid'];
|
||||
$params['restbaseCompat'] = true;
|
||||
} elseif ( $wgVisualEditorParsoidAutoConfig ) {
|
||||
$params = $vrs['modules']['parsoid'] ?? [];
|
||||
$params['restbaseCompat'] = true;
|
||||
// forward cookies on private wikis
|
||||
$params['forwardCookies'] = !MediaWikiServices::getInstance()
|
||||
->getPermissionManager()->isEveryoneAllowed( 'read' );
|
||||
} else {
|
||||
// No global modules defined, so no way to contact the document server.
|
||||
throw new ConfigException( "The VirtualRESTService for the document server is not defined;" .
|
||||
" see https://www.mediawiki.org/wiki/Extension:VisualEditor" );
|
||||
}
|
||||
// merge the global and service-specific params
|
||||
if ( isset( $vrs['global'] ) ) {
|
||||
$params = array_merge( $vrs['global'], $params );
|
||||
}
|
||||
// set up cookie forwarding
|
||||
if ( $params['forwardCookies'] ) {
|
||||
$params['forwardCookies'] = $this->forwardCookies;
|
||||
} else {
|
||||
$params['forwardCookies'] = false;
|
||||
}
|
||||
// create the VRS object and return it
|
||||
return new $class( $params );
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the object which directs queries to the virtual REST service, depending on the path.
|
||||
*
|
||||
* @return VirtualRESTServiceClient
|
||||
*/
|
||||
private function getVRSClient(): VirtualRESTServiceClient {
|
||||
if ( !$this->serviceClient ) {
|
||||
$this->serviceClient = new VirtualRESTServiceClient(
|
||||
MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient() );
|
||||
$this->serviceClient->mount( '/restbase/', $this->getVRSObject() );
|
||||
}
|
||||
return $this->serviceClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor function for all RESTbase requests
|
||||
*
|
||||
* @param Title $title The title of the page to use as the parsing context
|
||||
* @param string $method The HTTP method, either 'GET' or 'POST'
|
||||
* @param string $path The RESTbase api path
|
||||
* @param array $params Request parameters
|
||||
* @param array $reqheaders Request headers
|
||||
* @return StatusValue If successful, the value is the RESTbase server's response as an array
|
||||
* with keys 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
private function requestRestbase(
|
||||
Title $title, string $method, string $path, array $params, array $reqheaders = []
|
||||
): StatusValue {
|
||||
// Should be synchronised with requestParsoidData() in
|
||||
// modules/ve-mw/preinit/ve.init.mw.ArticleTargetLoader.js
|
||||
$profile = 'https://www.mediawiki.org/wiki/Specs/HTML/' .
|
||||
VisualEditorParsoidClient::PARSOID_VERSION;
|
||||
$reqheaders += [
|
||||
'Accept' =>
|
||||
"text/html; charset=utf-8; profile=\"$profile\"",
|
||||
'Accept-Language' => $title->getPageLanguage()->getCode(),
|
||||
'User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
||||
'Api-User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
||||
'Promise-Non-Write-API-Action' => 'true',
|
||||
];
|
||||
$request = [
|
||||
'method' => $method,
|
||||
'url' => '/restbase/local/v1/' . $path,
|
||||
( $method === 'GET' ? 'query' : 'body' ) => $params,
|
||||
'headers' => $reqheaders,
|
||||
];
|
||||
$response = $this->getVRSClient()->run( $request );
|
||||
if ( $response['error'] !== '' ) {
|
||||
return StatusValue::newFatal( 'apierror-visualeditor-docserver-http-error',
|
||||
wfEscapeWikiText( $response['error'] ) );
|
||||
} elseif ( $response['code'] !== 200 ) {
|
||||
// error null, code not 200
|
||||
$this->logger->warning(
|
||||
__METHOD__ . ": Received HTTP {code} from RESTBase",
|
||||
[
|
||||
'code' => $response['code'],
|
||||
'exception' => new RuntimeException(),
|
||||
'response' => [ 'body' => $response['body'] ],
|
||||
'requestPath' => $path,
|
||||
'requestIfMatch' => $reqheaders['If-Match'] ?? '',
|
||||
]
|
||||
);
|
||||
return StatusValue::newFatal( 'apierror-visualeditor-docserver-http', $response['code'] );
|
||||
}
|
||||
return StatusValue::newGood( $response );
|
||||
}
|
||||
|
||||
/**
|
||||
* Request page HTML from RESTBase
|
||||
*
|
||||
* @param RevisionRecord $revision Page revision
|
||||
* @param ?Language $pageLanguage Page language (default: `null`)
|
||||
* @return StatusValue If successful, the value is the RESTbase server's response as an array
|
||||
* with keys 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
public function requestRestbasePageHtml( RevisionRecord $revision, ?Language $pageLanguage = null ): StatusValue {
|
||||
$title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return StatusValue::newGood( $client->getPageHtml(
|
||||
$revision, $pageLanguage ?: $title->getPageLanguage()
|
||||
) );
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'GET',
|
||||
'page/html/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
'/' . $revision->getId() .
|
||||
'?redirect=false&stash=true',
|
||||
[],
|
||||
[
|
||||
'Accept-Language' => ( $pageLanguage ?: $title->getPageLanguage() )->getCode(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform HTML to wikitext via Parsoid through RESTbase. Wrapper for ::postData().
|
||||
*
|
||||
* @param Title $title The title of the page
|
||||
* @param string $html The HTML of the page to be transformed
|
||||
* @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`)
|
||||
* @param string|null $etag The ETag to set in the HTTP request header
|
||||
* @param ?Language $pageLanguage Page language (default: `null`)
|
||||
* @return StatusValue If successful, the value is the RESTbase server's response as an array
|
||||
* with keys 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
public function transformHTML(
|
||||
Title $title, string $html, int $oldid = null, string $etag = null, ?Language $pageLanguage = null
|
||||
): StatusValue {
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return StatusValue::newGood( $client->transformHtml(
|
||||
$title, $pageLanguage ?: $title->getPageLanguage(), $html, $oldid, $etag
|
||||
) );
|
||||
}
|
||||
$data = [ 'html' => $html ];
|
||||
$path = 'transform/html/to/wikitext/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
( $oldid === null ? '' : '/' . $oldid );
|
||||
|
||||
// Adapted from RESTBase mwUtil.parseETag()
|
||||
// ETag is not expected when creating a new page (oldid=0)
|
||||
if ( $oldid && !( preg_match( '/
|
||||
^(?:W\\/)?"?
|
||||
' . preg_quote( "$oldid", '/' ) . '
|
||||
(?:\\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))
|
||||
(?:\\/([^"]+))?
|
||||
"?$
|
||||
/x', $etag ) ) ) {
|
||||
$this->logger->info(
|
||||
__METHOD__ . ": Received funny ETag from client: '{etag}'",
|
||||
[
|
||||
'etag' => $etag,
|
||||
'oldid' => $oldid,
|
||||
'requestPath' => $path,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'POST', $path, $data,
|
||||
[
|
||||
'If-Match' => $etag,
|
||||
'Accept-Language' => ( $pageLanguage ?: $title->getPageLanguage() )->getCode(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform wikitext to HTML via Parsoid through RESTbase. Wrapper for ::postData().
|
||||
*
|
||||
* @param Title $title The title of the page to use as the parsing context
|
||||
* @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`)
|
||||
* @param ?Language $pageLanguage Page language (default: `null`)
|
||||
* @return StatusValue If successful, the value is the RESTbase server's response as an array
|
||||
* with keys 'code', 'reason', 'headers' and 'body'
|
||||
*/
|
||||
public function transformWikitext(
|
||||
Title $title, string $wikitext, bool $bodyOnly, int $oldid = null, bool $stash = false,
|
||||
?Language $pageLanguage = null
|
||||
): StatusValue {
|
||||
$client = $this->getDirectClient();
|
||||
if ( $client ) {
|
||||
return StatusValue::newGood( $client->transformWikitext(
|
||||
$title, $pageLanguage ?: $title->getPageLanguage(),
|
||||
$wikitext, $bodyOnly, $oldid, $stash
|
||||
) );
|
||||
}
|
||||
return $this->requestRestbase(
|
||||
$title,
|
||||
'POST',
|
||||
'transform/wikitext/to/html/' . urlencode( $title->getPrefixedDBkey() ) .
|
||||
( $oldid === null ? '' : '/' . $oldid ),
|
||||
[
|
||||
'wikitext' => $wikitext,
|
||||
'body_only' => $bodyOnly ? 1 : 0,
|
||||
'stash' => $stash ? 1 : 0
|
||||
],
|
||||
[
|
||||
'Accept-Language' => ( $pageLanguage ?: $title->getPageLanguage() )->getCode(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue