2020-03-16 23:30:26 +00:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Helper functions for contacting Parsoid/RESTBase.
|
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
|
|
|
use MediaWiki\MediaWikiServices;
|
2020-05-19 17:53:29 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
use Psr\Log\NullLogger;
|
2020-03-16 23:30:26 +00:00
|
|
|
|
|
|
|
trait ApiParsoidTrait {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var VirtualRESTServiceClient
|
|
|
|
*/
|
|
|
|
protected $serviceClient = null;
|
|
|
|
|
|
|
|
/**
|
2020-05-19 17:53:29 +00:00
|
|
|
* @var LoggerInterface
|
2020-03-16 23:30:26 +00:00
|
|
|
*/
|
|
|
|
protected $logger = null;
|
|
|
|
|
|
|
|
/**
|
2020-05-19 17:53:29 +00:00
|
|
|
* @return LoggerInterface
|
2020-03-16 23:30:26 +00:00
|
|
|
*/
|
|
|
|
protected function getLogger() {
|
2020-05-19 17:53:29 +00:00
|
|
|
return $this->logger ?: new NullLogger();
|
2020-03-16 23:30:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2020-05-19 17:53:29 +00:00
|
|
|
* @param LoggerInterface $logger
|
2020-03-16 23:30:26 +00:00
|
|
|
*/
|
|
|
|
protected function setLogger( $logger ) {
|
|
|
|
$this->logger = $logger;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
*/
|
|
|
|
protected function getVRSObject() {
|
|
|
|
// 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;
|
|
|
|
} 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
|
|
|
|
*/
|
|
|
|
protected function getVRSClient() {
|
|
|
|
if ( !$this->serviceClient ) {
|
2020-05-21 04:54:55 +00:00
|
|
|
$this->serviceClient = new VirtualRESTServiceClient(
|
|
|
|
MediaWikiServices::getInstance()->getHttpRequestFactory()->createMultiClient() );
|
2020-03-16 23:30:26 +00:00
|
|
|
$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'
|
|
|
|
*/
|
|
|
|
protected function requestRestbase( Title $title, $method, $path, $params, $reqheaders = [] ) {
|
|
|
|
$request = [
|
|
|
|
'method' => $method,
|
|
|
|
'url' => '/restbase/local/v1/' . $path
|
|
|
|
];
|
|
|
|
if ( $method === 'GET' ) {
|
|
|
|
$request['query'] = $params;
|
|
|
|
} else {
|
|
|
|
$request['body'] = $params;
|
|
|
|
}
|
|
|
|
// Should be synchronised with modules/ve-mw/init/ve.init.mw.ArticleTargetLoader.js
|
|
|
|
$defaultReqHeaders = [
|
|
|
|
'Accept' =>
|
|
|
|
'text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.0.0"',
|
|
|
|
'Accept-Language' => self::getPageLanguage( $title )->getCode(),
|
2020-05-15 19:09:24 +00:00
|
|
|
'User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
|
|
|
'Api-User-Agent' => 'VisualEditor-MediaWiki/' . MW_VERSION,
|
2020-03-16 23:30:26 +00:00
|
|
|
];
|
|
|
|
// $reqheaders take precedence over $defaultReqHeaders
|
|
|
|
$request['headers'] = $reqheaders + $defaultReqHeaders;
|
|
|
|
$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'];
|
|
|
|
$rp = null;
|
|
|
|
if ( isset( $headers['x-cache'] ) && strpos( $headers['x-cache'], 'hit' ) !== false ) {
|
|
|
|
$rp = 'cached-response=true';
|
|
|
|
}
|
|
|
|
if ( $rp !== null ) {
|
|
|
|
$resp = $this->getRequest()->response();
|
|
|
|
$resp->header( 'X-Cache: ' . $rp );
|
|
|
|
}
|
|
|
|
} 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'],
|
|
|
|
'trace' => ( new Exception )->getTraceAsString(),
|
|
|
|
'response' => $response['body'],
|
|
|
|
'requestPath' => $path,
|
|
|
|
'requestIfMatch' => $reqheaders['If-Match'] ?? '',
|
|
|
|
]
|
|
|
|
);
|
|
|
|
$this->dieWithError(
|
|
|
|
[ 'apierror-visualeditor-docserver-http', $response['code'] ],
|
|
|
|
'apierror-visualeditor-docserver-http'
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $response;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transform HTML to wikitext via Parsoid through RESTbase.
|
|
|
|
*
|
|
|
|
* @param string $path The RESTbase path of the transform endpoint
|
|
|
|
* @param Title $title The title of the page
|
|
|
|
* @param array $data An array of the HTML and the 'scrub_wikitext' option
|
|
|
|
* @param array $parserParams Parsoid parser parameters to pass in
|
|
|
|
* @param string $etag The ETag to set in the HTTP request header
|
|
|
|
* @return string Body of the RESTbase server's response
|
|
|
|
*/
|
|
|
|
protected function postData( $path, Title $title, $data, $parserParams, $etag ) {
|
|
|
|
$path .= urlencode( $title->getPrefixedDBkey() );
|
|
|
|
if ( isset( $parserParams['oldid'] ) && $parserParams['oldid'] ) {
|
|
|
|
$path .= '/' . $parserParams['oldid'];
|
|
|
|
}
|
|
|
|
// Adapted from RESTBase mwUtil.parseETag()
|
|
|
|
// ETag is not expected when creating a new page (oldid=0)
|
|
|
|
if ( isset( $parserParams['oldid'] ) && $parserParams['oldid'] && !preg_match( '/
|
|
|
|
^(?:W\\/)?"?
|
|
|
|
([^"\\/]+)
|
|
|
|
(?:\\/([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,
|
|
|
|
'requestPath' => $path,
|
|
|
|
]
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return $this->requestRestbase(
|
|
|
|
$title,
|
|
|
|
'POST', $path, $data,
|
|
|
|
[ 'If-Match' => $etag ]
|
|
|
|
)['body'];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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 array $parserParams Parsoid parser parameters to pass in
|
|
|
|
* @param string $etag The ETag to set in the HTTP request header
|
|
|
|
* @return string Body of the RESTbase server's response
|
|
|
|
*/
|
|
|
|
protected function postHTML( Title $title, $html, $parserParams, $etag ) {
|
|
|
|
return $this->postData(
|
|
|
|
'transform/html/to/wikitext/', $title,
|
|
|
|
[ 'html' => $html, 'scrub_wikitext' => 1 ], $parserParams, $etag
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the page language from a title, using the content language as fallback on special pages
|
|
|
|
* @param Title $title Title
|
|
|
|
* @return Language Content language
|
|
|
|
*/
|
|
|
|
public static function getPageLanguage( Title $title ) {
|
|
|
|
if ( $title->isSpecial( 'CollabPad' ) ) {
|
|
|
|
// Use the site language for CollabPad, as getPageLanguage just
|
|
|
|
// returns the interface language for special pages.
|
|
|
|
// TODO: Let the user change the document language on multi-lingual sites.
|
|
|
|
return MediaWikiServices::getInstance()->getContentLanguage();
|
|
|
|
} else {
|
|
|
|
return $title->getPageLanguage();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see ApiBase
|
|
|
|
* @param string|array|Message $msg See ApiErrorFormatter::addError()
|
|
|
|
* @param string|null $code See ApiErrorFormatter::addError()
|
|
|
|
* @param array|null $data See ApiErrorFormatter::addError()
|
|
|
|
* @param int|null $httpCode HTTP error code to use
|
|
|
|
*/
|
|
|
|
abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = null );
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see ContextSource
|
|
|
|
* @return Config
|
|
|
|
*/
|
|
|
|
abstract public function getConfig();
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @see ContextSource
|
|
|
|
* @return WebRequest
|
|
|
|
*/
|
|
|
|
abstract public function getRequest();
|
|
|
|
}
|