2020-03-16 23:30:26 +00:00
|
|
|
<?php
|
|
|
|
/**
|
2022-08-09 23:22:02 +00:00
|
|
|
* Helper functions for contacting Parsoid/RESTBase from the action API.
|
2020-03-16 23:30:26 +00:00
|
|
|
*
|
|
|
|
* @file
|
|
|
|
* @ingroup Extensions
|
|
|
|
* @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
|
|
|
|
* @license MIT
|
|
|
|
*/
|
|
|
|
|
2022-03-13 01:38:23 +00:00
|
|
|
namespace MediaWiki\Extension\VisualEditor;
|
|
|
|
|
2023-08-14 15:14:12 +00:00
|
|
|
use ApiUsageException;
|
2022-03-13 01:38:23 +00:00
|
|
|
use Language;
|
2022-11-10 10:00:31 +00:00
|
|
|
use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
|
2020-03-16 23:30:26 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2023-08-14 15:14:12 +00:00
|
|
|
use MediaWiki\Rest\HttpException;
|
|
|
|
use MediaWiki\Rest\LocalizedHttpException;
|
2020-06-10 15:03:26 +00:00
|
|
|
use MediaWiki\Revision\RevisionRecord;
|
2023-08-19 04:21:24 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2022-03-13 01:38:23 +00:00
|
|
|
use Message;
|
2022-11-10 10:00:31 +00:00
|
|
|
use NullStatsdDataFactory;
|
|
|
|
use PrefixingStatsdDataFactoryProxy;
|
2020-05-19 17:53:29 +00:00
|
|
|
use Psr\Log\LoggerInterface;
|
|
|
|
use Psr\Log\NullLogger;
|
2023-08-14 16:27:32 +00:00
|
|
|
use Throwable;
|
2022-03-13 01:38:23 +00:00
|
|
|
use WebRequest;
|
2020-03-16 23:30:26 +00:00
|
|
|
|
|
|
|
trait ApiParsoidTrait {
|
|
|
|
|
2023-04-24 19:10:30 +00:00
|
|
|
private ?LoggerInterface $logger = null;
|
|
|
|
private ?StatsdDataFactoryInterface $stats = null;
|
2022-11-10 10:00:31 +00:00
|
|
|
|
2021-07-21 19:15:31 +00:00
|
|
|
protected function getLogger(): LoggerInterface {
|
2020-05-19 17:53:29 +00:00
|
|
|
return $this->logger ?: new NullLogger();
|
2020-03-16 23:30:26 +00:00
|
|
|
}
|
|
|
|
|
2024-02-20 10:27:15 +00:00
|
|
|
protected function setLogger( LoggerInterface $logger ): void {
|
2020-03-16 23:30:26 +00:00
|
|
|
$this->logger = $logger;
|
|
|
|
}
|
|
|
|
|
2022-11-10 10:00:31 +00:00
|
|
|
protected function getStats(): StatsdDataFactoryInterface {
|
|
|
|
return $this->stats ?: new NullStatsdDataFactory();
|
|
|
|
}
|
|
|
|
|
2024-02-20 10:27:15 +00:00
|
|
|
protected function setStats( StatsdDataFactoryInterface $stats ): void {
|
2023-06-19 17:10:41 +00:00
|
|
|
$this->stats = new PrefixingStatsdDataFactoryProxy( $stats, 'VE' );
|
2022-11-10 10:00:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return float Return a start time for use with statsRecordTiming()
|
|
|
|
*/
|
|
|
|
private function statsGetStartTime(): float {
|
|
|
|
return microtime( true );
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $key
|
|
|
|
* @param float $startTime from statsGetStartTime()
|
|
|
|
*/
|
2024-02-20 10:27:15 +00:00
|
|
|
private function statsRecordTiming( string $key, float $startTime ): void {
|
2022-11-10 10:00:31 +00:00
|
|
|
$duration = ( microtime( true ) - $startTime ) * 1000;
|
|
|
|
$this->getStats()->timing( $key, $duration );
|
|
|
|
}
|
|
|
|
|
2020-06-10 15:03:26 +00:00
|
|
|
/**
|
2023-08-14 15:14:12 +00:00
|
|
|
* @return never
|
|
|
|
* @throws ApiUsageException
|
2022-08-09 23:22:02 +00:00
|
|
|
*/
|
2024-02-20 10:27:15 +00:00
|
|
|
private function dieWithRestHttpException( HttpException $ex ): void {
|
2023-08-14 15:14:12 +00:00
|
|
|
if ( $ex instanceof LocalizedHttpException ) {
|
2023-12-18 21:06:35 +00:00
|
|
|
$converter = new \MediaWiki\Message\Converter();
|
|
|
|
$msg = $converter->convertMessageValue( $ex->getMessageValue() );
|
2024-02-27 17:58:31 +00:00
|
|
|
$this->dieWithError( $msg, null, $ex->getErrorData() );
|
2023-08-14 15:14:12 +00:00
|
|
|
} else {
|
2024-02-27 17:58:31 +00:00
|
|
|
$this->dieWithException( $ex, [ 'data' => $ex->getErrorData() ] );
|
2022-08-09 23:22:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-10 15:03:26 +00:00
|
|
|
/**
|
2023-08-14 15:14:12 +00:00
|
|
|
* Request page HTML from Parsoid.
|
2020-06-10 15:03:26 +00:00
|
|
|
*
|
|
|
|
* @param RevisionRecord $revision Page revision
|
2023-08-14 15:14:12 +00:00
|
|
|
* @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
|
|
|
|
* @phan-return array{body:string,headers:array<string,string>}
|
|
|
|
* @throws ApiUsageException
|
2020-06-10 15:03:26 +00:00
|
|
|
*/
|
2021-07-21 19:15:31 +00:00
|
|
|
protected function requestRestbasePageHtml( RevisionRecord $revision ): array {
|
2020-06-10 15:03:26 +00:00
|
|
|
$title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
|
2022-08-09 23:22:02 +00:00
|
|
|
$lang = self::getPageLanguage( $title );
|
|
|
|
|
2022-11-10 10:00:31 +00:00
|
|
|
$startTime = $this->statsGetStartTime();
|
2023-08-14 15:14:12 +00:00
|
|
|
try {
|
|
|
|
$response = $this->getParsoidClient()->getPageHtml( $revision, $lang );
|
|
|
|
} catch ( HttpException $ex ) {
|
|
|
|
$this->dieWithRestHttpException( $ex );
|
|
|
|
}
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.getPageHtml', $startTime );
|
2022-08-09 23:22:02 +00:00
|
|
|
|
2022-09-05 10:36:16 +00:00
|
|
|
return $response;
|
2020-06-10 15:03:26 +00:00
|
|
|
}
|
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
/**
|
2023-08-14 15:14:12 +00:00
|
|
|
* Transform HTML to wikitext with Parsoid.
|
2020-03-16 23:30:26 +00:00
|
|
|
*
|
|
|
|
* @param Title $title The title of the page
|
2020-07-22 13:35:59 +00:00
|
|
|
* @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`)
|
2020-06-26 15:33:32 +00:00
|
|
|
* @param string|null $etag The ETag to set in the HTTP request header
|
2023-08-14 15:14:12 +00:00
|
|
|
* @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
|
|
|
|
* @phan-return array{body:string,headers:array<string,string>}
|
|
|
|
* @throws ApiUsageException
|
2020-03-16 23:30:26 +00:00
|
|
|
*/
|
2020-07-22 13:38:33 +00:00
|
|
|
protected function transformHTML(
|
2020-07-22 13:35:59 +00:00
|
|
|
Title $title, string $html, int $oldid = null, string $etag = null
|
2021-07-21 19:15:31 +00:00
|
|
|
): array {
|
2022-08-09 23:22:02 +00:00
|
|
|
$lang = self::getPageLanguage( $title );
|
2020-07-22 13:35:59 +00:00
|
|
|
|
2022-11-10 10:00:31 +00:00
|
|
|
$startTime = $this->statsGetStartTime();
|
2023-08-14 15:14:12 +00:00
|
|
|
try {
|
|
|
|
$response = $this->getParsoidClient()->transformHTML( $title, $lang, $html, $oldid, $etag );
|
|
|
|
} catch ( HttpException $ex ) {
|
|
|
|
$this->dieWithRestHttpException( $ex );
|
|
|
|
}
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.transformHTML', $startTime );
|
2022-08-09 23:22:02 +00:00
|
|
|
|
2022-09-05 10:36:16 +00:00
|
|
|
return $response;
|
2020-03-16 23:30:26 +00:00
|
|
|
}
|
|
|
|
|
2020-07-22 13:38:33 +00:00
|
|
|
/**
|
2023-08-14 15:14:12 +00:00
|
|
|
* Transform wikitext to HTML with Parsoid.
|
2020-07-22 13:38:33 +00:00
|
|
|
*
|
|
|
|
* @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`)
|
2023-08-14 15:14:12 +00:00
|
|
|
* @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
|
|
|
|
* @phan-return array{body:string,headers:array<string,string>}
|
|
|
|
* @throws ApiUsageException
|
2020-07-22 13:38:33 +00:00
|
|
|
*/
|
|
|
|
protected function transformWikitext(
|
|
|
|
Title $title, string $wikitext, bool $bodyOnly, int $oldid = null, bool $stash = false
|
2021-07-21 19:15:31 +00:00
|
|
|
): array {
|
2022-08-09 23:22:02 +00:00
|
|
|
$lang = self::getPageLanguage( $title );
|
|
|
|
|
2022-11-10 10:00:31 +00:00
|
|
|
$startTime = $this->statsGetStartTime();
|
2023-08-14 15:14:12 +00:00
|
|
|
try {
|
|
|
|
$response = $this->getParsoidClient()->transformWikitext(
|
|
|
|
$title,
|
|
|
|
$lang,
|
|
|
|
$wikitext,
|
|
|
|
$bodyOnly,
|
|
|
|
$oldid,
|
|
|
|
$stash
|
|
|
|
);
|
|
|
|
} catch ( HttpException $ex ) {
|
|
|
|
$this->dieWithRestHttpException( $ex );
|
|
|
|
}
|
2022-11-10 10:00:31 +00:00
|
|
|
$this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.transformWikitext', $startTime );
|
2022-08-09 23:22:02 +00:00
|
|
|
|
2022-09-05 10:36:16 +00:00
|
|
|
return $response;
|
2020-07-22 13:38:33 +00:00
|
|
|
}
|
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
/**
|
|
|
|
* Get the page language from a title, using the content language as fallback on special pages
|
2020-07-22 13:35:59 +00:00
|
|
|
*
|
2020-10-07 13:24:50 +00:00
|
|
|
* @param Title $title
|
2020-03-16 23:30:26 +00:00
|
|
|
* @return Language Content language
|
|
|
|
*/
|
2021-07-21 19:15:31 +00:00
|
|
|
public static function getPageLanguage( Title $title ): Language {
|
2020-03-16 23:30:26 +00:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-05 10:36:16 +00:00
|
|
|
/**
|
|
|
|
* @see VisualEditorParsoidClientFactory
|
|
|
|
*/
|
|
|
|
abstract protected function getParsoidClient(): ParsoidClient;
|
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
/**
|
|
|
|
* @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
|
2022-02-05 15:49:40 +00:00
|
|
|
* @return never
|
2023-08-14 15:14:12 +00:00
|
|
|
* @throws ApiUsageException
|
2020-03-16 23:30:26 +00:00
|
|
|
*/
|
|
|
|
abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = null );
|
|
|
|
|
2023-08-14 16:27:32 +00:00
|
|
|
/**
|
|
|
|
* @see ApiBase
|
|
|
|
* @param Throwable $exception See ApiErrorFormatter::getMessageFromException()
|
|
|
|
* @param array $options See ApiErrorFormatter::getMessageFromException()
|
|
|
|
* @return never
|
|
|
|
*/
|
|
|
|
abstract public function dieWithException( Throwable $exception, array $options = [] );
|
|
|
|
|
2020-03-16 23:30:26 +00:00
|
|
|
/**
|
|
|
|
* @see ContextSource
|
|
|
|
* @return WebRequest
|
|
|
|
*/
|
|
|
|
abstract public function getRequest();
|
|
|
|
}
|