Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
4.76% |
2 / 42 |
|
18.18% |
2 / 11 |
CRAP | |
0.00% |
0 / 1 |
| ApiParsoidTrait | |
4.76% |
2 / 42 |
|
18.18% |
2 / 11 |
297.88 | |
0.00% |
0 / 1 |
| getLogger | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| setLogger | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getStatsFactory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
| setStatsFactory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| statsGetStartTime | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| statsRecordTiming | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
| dieWithRestHttpException | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| requestRestbasePageHtml | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
| transformHTML | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
| transformWikitext | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
| getPageLanguage | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getParsoidClient | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| dieWithError | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| dieWithException | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| getRequest | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
| 1 | <?php |
| 2 | /** |
| 3 | * Helper functions for contacting Parsoid/RESTBase from the action API. |
| 4 | * |
| 5 | * @file |
| 6 | * @ingroup Extensions |
| 7 | * @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt |
| 8 | * @license MIT |
| 9 | */ |
| 10 | |
| 11 | namespace MediaWiki\Extension\VisualEditor; |
| 12 | |
| 13 | use MediaWiki\Api\ApiUsageException; |
| 14 | use MediaWiki\Language\Language; |
| 15 | use MediaWiki\MediaWikiServices; |
| 16 | use MediaWiki\Request\WebRequest; |
| 17 | use MediaWiki\Rest\HttpException; |
| 18 | use MediaWiki\Rest\LocalizedHttpException; |
| 19 | use MediaWiki\Revision\RevisionRecord; |
| 20 | use MediaWiki\Title\Title; |
| 21 | use Psr\Log\LoggerInterface; |
| 22 | use Psr\Log\NullLogger; |
| 23 | use Throwable; |
| 24 | use Wikimedia\Message\MessageSpecifier; |
| 25 | use Wikimedia\Stats\StatsFactory; |
| 26 | |
| 27 | trait ApiParsoidTrait { |
| 28 | |
| 29 | private ?LoggerInterface $logger = null; |
| 30 | /** @var StatsFactory|null */ |
| 31 | private $stats = null; |
| 32 | |
| 33 | protected function getLogger(): LoggerInterface { |
| 34 | return $this->logger ?: new NullLogger(); |
| 35 | } |
| 36 | |
| 37 | protected function setLogger( LoggerInterface $logger ): void { |
| 38 | $this->logger = $logger; |
| 39 | } |
| 40 | |
| 41 | protected function getStatsFactory(): StatsFactory { |
| 42 | return $this->stats ?: StatsFactory::newNull(); |
| 43 | } |
| 44 | |
| 45 | protected function setStatsFactory( StatsFactory $statsFactory ): void { |
| 46 | $this->stats = $statsFactory; |
| 47 | } |
| 48 | |
| 49 | /** |
| 50 | * @return float Return a start time for use with statsRecordTiming() |
| 51 | */ |
| 52 | private function statsGetStartTime(): float { |
| 53 | return microtime( true ); |
| 54 | } |
| 55 | |
| 56 | /** |
| 57 | * @param string $key |
| 58 | * @param float $startTime from statsGetStartTime() |
| 59 | */ |
| 60 | private function statsRecordTiming( string $key, float $startTime ): void { |
| 61 | $duration = ( microtime( true ) - $startTime ) * 1000; |
| 62 | $this->getStatsFactory()->getTiming( $key )->observe( $duration ); |
| 63 | } |
| 64 | |
| 65 | /** |
| 66 | * @return never |
| 67 | * @throws ApiUsageException |
| 68 | */ |
| 69 | private function dieWithRestHttpException( HttpException $ex ): void { |
| 70 | if ( $ex instanceof LocalizedHttpException ) { |
| 71 | $this->dieWithError( $ex->getMessageValue(), null, $ex->getErrorData() ); |
| 72 | } else { |
| 73 | $this->dieWithException( $ex, [ 'data' => $ex->getErrorData() ] ); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | /** |
| 78 | * Request page HTML from Parsoid. |
| 79 | * |
| 80 | * @param RevisionRecord $revision Page revision |
| 81 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
| 82 | * @phan-return array{body:string,headers:array<string,string>} |
| 83 | * @throws ApiUsageException |
| 84 | */ |
| 85 | protected function requestRestbasePageHtml( RevisionRecord $revision ): array { |
| 86 | $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() ); |
| 87 | $lang = self::getPageLanguage( $title ); |
| 88 | |
| 89 | $startTime = $this->statsGetStartTime(); |
| 90 | try { |
| 91 | $response = $this->getParsoidClient()->getPageHtml( $revision, $lang ); |
| 92 | } catch ( HttpException $ex ) { |
| 93 | $this->dieWithRestHttpException( $ex ); |
| 94 | } |
| 95 | $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_getPageHtml_seconds', $startTime ); |
| 96 | |
| 97 | return $response; |
| 98 | } |
| 99 | |
| 100 | /** |
| 101 | * Transform HTML to wikitext with Parsoid. |
| 102 | * |
| 103 | * @param Title $title The title of the page |
| 104 | * @param string $html The HTML of the page to be transformed |
| 105 | * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`) |
| 106 | * @param string|null $etag The ETag to set in the HTTP request header |
| 107 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
| 108 | * @phan-return array{body:string,headers:array<string,string>} |
| 109 | * @throws ApiUsageException |
| 110 | */ |
| 111 | protected function transformHTML( |
| 112 | Title $title, string $html, ?int $oldid = null, ?string $etag = null |
| 113 | ): array { |
| 114 | $lang = self::getPageLanguage( $title ); |
| 115 | |
| 116 | $startTime = $this->statsGetStartTime(); |
| 117 | try { |
| 118 | $response = $this->getParsoidClient()->transformHTML( $title, $lang, $html, $oldid, $etag ); |
| 119 | } catch ( HttpException $ex ) { |
| 120 | $this->dieWithRestHttpException( $ex ); |
| 121 | } |
| 122 | $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_transformHTML_seconds', $startTime ); |
| 123 | |
| 124 | return $response; |
| 125 | } |
| 126 | |
| 127 | /** |
| 128 | * Transform wikitext to HTML with Parsoid. |
| 129 | * |
| 130 | * @param Title $title The title of the page to use as the parsing context |
| 131 | * @param string $wikitext The wikitext fragment to parse |
| 132 | * @param bool $bodyOnly Whether to provide only the contents of the `<body>` tag |
| 133 | * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`) |
| 134 | * @param bool $stash Whether to stash the result in the server-side cache (default: `false`) |
| 135 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
| 136 | * @phan-return array{body:string,headers:array<string,string>} |
| 137 | * @throws ApiUsageException |
| 138 | */ |
| 139 | protected function transformWikitext( |
| 140 | Title $title, string $wikitext, bool $bodyOnly, ?int $oldid = null, bool $stash = false |
| 141 | ): array { |
| 142 | $lang = self::getPageLanguage( $title ); |
| 143 | |
| 144 | $startTime = $this->statsGetStartTime(); |
| 145 | try { |
| 146 | $response = $this->getParsoidClient()->transformWikitext( |
| 147 | $title, |
| 148 | $lang, |
| 149 | $wikitext, |
| 150 | $bodyOnly, |
| 151 | $oldid, |
| 152 | $stash |
| 153 | ); |
| 154 | } catch ( HttpException $ex ) { |
| 155 | $this->dieWithRestHttpException( $ex ); |
| 156 | } |
| 157 | $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_transformWikitext_seconds', $startTime ); |
| 158 | |
| 159 | return $response; |
| 160 | } |
| 161 | |
| 162 | /** |
| 163 | * Get the page language from a title, using the content language as fallback on special pages |
| 164 | * |
| 165 | * @param Title $title |
| 166 | * @return Language Content language |
| 167 | */ |
| 168 | public static function getPageLanguage( Title $title ): Language { |
| 169 | if ( $title->isSpecial( 'CollabPad' ) ) { |
| 170 | // Use the site language for CollabPad, as getPageLanguage just |
| 171 | // returns the interface language for special pages. |
| 172 | // TODO: Let the user change the document language on multi-lingual sites. |
| 173 | return MediaWikiServices::getInstance()->getContentLanguage(); |
| 174 | } else { |
| 175 | return $title->getPageLanguage(); |
| 176 | } |
| 177 | } |
| 178 | |
| 179 | /** |
| 180 | * @see VisualEditorParsoidClientFactory |
| 181 | */ |
| 182 | abstract protected function getParsoidClient(): ParsoidClient; |
| 183 | |
| 184 | /** |
| 185 | * @see ApiBase |
| 186 | * @param string|array|MessageSpecifier $msg See ApiErrorFormatter::addError() |
| 187 | * @param string|null $code See ApiErrorFormatter::addError() |
| 188 | * @param array|null $data See ApiErrorFormatter::addError() |
| 189 | * @param int $httpCode HTTP error code to use |
| 190 | * @throws ApiUsageException always |
| 191 | * @return never |
| 192 | */ |
| 193 | abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = 0 ); |
| 194 | |
| 195 | /** |
| 196 | * @see ApiBase |
| 197 | * @param Throwable $exception See ApiErrorFormatter::getMessageFromException() |
| 198 | * @param array $options See ApiErrorFormatter::getMessageFromException() |
| 199 | * @throws ApiUsageException always |
| 200 | * @return never |
| 201 | */ |
| 202 | abstract public function dieWithException( Throwable $exception, array $options = [] ); |
| 203 | |
| 204 | /** |
| 205 | * @see ContextSource |
| 206 | * @return WebRequest |
| 207 | */ |
| 208 | abstract public function getRequest(); |
| 209 | } |