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