Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
30.16% |
19 / 63 |
|
25.00% |
2 / 8 |
CRAP | |
0.00% |
0 / 1 |
DirectParsoidClient | |
30.16% |
19 / 63 |
|
25.00% |
2 / 8 |
103.21 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getHtmlOutputRendererHelper | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getHtmlInputTransformHelper | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
getPageHtml | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
makeFakeRevision | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
transformWikitext | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
2.01 | |||
transformHTML | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
fakeRESTbaseHTMLResponse | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Helper functions for using the REST interface to Parsoid. |
4 | * |
5 | * @file |
6 | * @ingroup Extensions |
7 | * @copyright 2022 VisualEditor Team and others; see AUTHORS.txt |
8 | * @license MIT |
9 | */ |
10 | |
11 | namespace MediaWiki\Extension\VisualEditor; |
12 | |
13 | use MediaWiki\MediaWikiServices; |
14 | use MediaWiki\Page\PageIdentity; |
15 | use MediaWiki\Permissions\Authority; |
16 | use MediaWiki\Rest\Handler\Helper\HtmlInputTransformHelper; |
17 | use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper; |
18 | use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory; |
19 | use MediaWiki\Revision\MutableRevisionRecord; |
20 | use MediaWiki\Revision\RevisionRecord; |
21 | use MediaWiki\Revision\SlotRecord; |
22 | use User; |
23 | use Wikimedia\Bcp47Code\Bcp47Code; |
24 | use WikitextContent; |
25 | |
26 | class DirectParsoidClient implements ParsoidClient { |
27 | |
28 | /** |
29 | * Requested Parsoid HTML version. |
30 | * Keep this in sync with the Accept: header in |
31 | * ve.init.mw.ArticleTargetLoader.js |
32 | */ |
33 | public const PARSOID_VERSION = '2.8.0'; |
34 | |
35 | private const FLAVOR_DEFAULT = 'view'; |
36 | |
37 | /** @var PageRestHelperFactory */ |
38 | private $helperFactory; |
39 | |
40 | /** @var Authority */ |
41 | private $performer; |
42 | |
43 | /** |
44 | * @param PageRestHelperFactory $helperFactory |
45 | * @param Authority $performer |
46 | */ |
47 | public function __construct( |
48 | PageRestHelperFactory $helperFactory, |
49 | Authority $performer |
50 | ) { |
51 | $this->performer = $performer; |
52 | $this->helperFactory = $helperFactory; |
53 | } |
54 | |
55 | /** |
56 | * @param PageIdentity $page |
57 | * @param RevisionRecord|null $revision |
58 | * @param Bcp47Code|null $pageLanguage |
59 | * @param bool $stash |
60 | * @param string $flavor |
61 | * |
62 | * @return HtmlOutputRendererHelper |
63 | */ |
64 | private function getHtmlOutputRendererHelper( |
65 | PageIdentity $page, |
66 | ?RevisionRecord $revision = null, |
67 | Bcp47Code $pageLanguage = null, |
68 | bool $stash = false, |
69 | string $flavor = self::FLAVOR_DEFAULT |
70 | ): HtmlOutputRendererHelper { |
71 | $helper = $this->helperFactory->newHtmlOutputRendererHelper(); |
72 | |
73 | // TODO: remove this once we no longer need a User object for rate limiting (T310476). |
74 | if ( $this->performer instanceof User ) { |
75 | $user = $this->performer; |
76 | } else { |
77 | $user = User::newFromIdentity( $this->performer->getUser() ); |
78 | } |
79 | |
80 | $helper->init( $page, [], $user, $revision ); |
81 | |
82 | // Ensure we get a compatible version, not just the default |
83 | $helper->setOutputProfileVersion( self::PARSOID_VERSION ); |
84 | |
85 | $helper->setStashingEnabled( $stash ); |
86 | if ( !$stash ) { |
87 | $helper->setFlavor( $flavor ); |
88 | } |
89 | |
90 | if ( $revision ) { |
91 | $helper->setRevision( $revision ); |
92 | } |
93 | |
94 | if ( $pageLanguage ) { |
95 | $helper->setPageLanguage( $pageLanguage ); |
96 | } |
97 | |
98 | return $helper; |
99 | } |
100 | |
101 | /** |
102 | * @param PageIdentity $page |
103 | * @param string $html |
104 | * @param int|null $oldid |
105 | * @param string|null $etag |
106 | * @param Bcp47Code|null $pageLanguage |
107 | * |
108 | * @return HtmlInputTransformHelper |
109 | */ |
110 | private function getHtmlInputTransformHelper( |
111 | PageIdentity $page, |
112 | string $html, |
113 | int $oldid = null, |
114 | string $etag = null, |
115 | Bcp47Code $pageLanguage = null |
116 | ): HtmlInputTransformHelper { |
117 | $helper = $this->helperFactory->newHtmlInputTransformHelper(); |
118 | |
119 | // Fake REST body |
120 | $body = [ |
121 | 'html' => [ |
122 | 'body' => $html, |
123 | ] |
124 | ]; |
125 | |
126 | $metrics = MediaWikiServices::getInstance()->getParsoidSiteConfig()->metrics(); |
127 | if ( $metrics ) { |
128 | $helper->setMetrics( $metrics ); |
129 | } |
130 | |
131 | if ( $oldid || $etag ) { |
132 | $body['original']['revid'] = $oldid; |
133 | $body['original']['renderid'] = $etag; |
134 | } |
135 | |
136 | $helper->init( $page, $body, [], null, $pageLanguage ); |
137 | |
138 | return $helper; |
139 | } |
140 | |
141 | /** |
142 | * Request page HTML from Parsoid. |
143 | * |
144 | * @param RevisionRecord $revision Page revision |
145 | * @param ?Bcp47Code $targetLanguage Page language (default: `null`) |
146 | * |
147 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
148 | * @phan-return array{body:string,headers:array<string,string>} |
149 | */ |
150 | public function getPageHtml( RevisionRecord $revision, ?Bcp47Code $targetLanguage = null ): array { |
151 | // In the VE client, we always want to stash. |
152 | $page = $revision->getPage(); |
153 | |
154 | $helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, true ); |
155 | $parserOutput = $helper->getHtml(); |
156 | |
157 | return $this->fakeRESTbaseHTMLResponse( $parserOutput->getRawText(), $helper ); |
158 | } |
159 | |
160 | /** |
161 | * @param PageIdentity $page |
162 | * @param string $wikitext |
163 | * |
164 | * @return RevisionRecord |
165 | */ |
166 | private function makeFakeRevision( |
167 | PageIdentity $page, |
168 | string $wikitext |
169 | ): RevisionRecord { |
170 | $rev = new MutableRevisionRecord( $page ); |
171 | $rev->setId( 0 ); |
172 | $rev->setPageId( $page->getId() ); |
173 | |
174 | $rev->setContent( SlotRecord::MAIN, new WikitextContent( $wikitext ) ); |
175 | |
176 | return $rev; |
177 | } |
178 | |
179 | /** |
180 | * Transform wikitext to HTML with Parsoid. |
181 | * |
182 | * @param PageIdentity $page The page the content belongs to use as the parsing context |
183 | * @param Bcp47Code $targetLanguage Page language |
184 | * @param string $wikitext The wikitext fragment to parse |
185 | * @param bool $bodyOnly Whether to provide only the contents of the `<body>` tag |
186 | * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`) |
187 | * @param bool $stash Whether to stash the result in the server-side cache (default: `false`) |
188 | * |
189 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
190 | * @phan-return array{body:string,headers:array<string,string>} |
191 | */ |
192 | public function transformWikitext( |
193 | PageIdentity $page, |
194 | Bcp47Code $targetLanguage, |
195 | string $wikitext, |
196 | bool $bodyOnly, |
197 | ?int $oldid, |
198 | bool $stash |
199 | ): array { |
200 | $revision = $this->makeFakeRevision( $page, $wikitext ); |
201 | |
202 | $helper = $this->getHtmlOutputRendererHelper( $page, $revision, $targetLanguage, $stash ); |
203 | |
204 | if ( $bodyOnly ) { |
205 | $helper->setFlavor( 'fragment' ); |
206 | } |
207 | |
208 | $parserOutput = $helper->getHtml(); |
209 | $html = $parserOutput->getRawText(); |
210 | |
211 | return $this->fakeRESTbaseHTMLResponse( $html, $helper ); |
212 | } |
213 | |
214 | /** |
215 | * Transform HTML to wikitext with Parsoid |
216 | * |
217 | * @param PageIdentity $page The page the content belongs to |
218 | * @param Bcp47Code $targetLanguage The desired output language |
219 | * @param string $html The HTML of the page to be transformed |
220 | * @param ?int $oldid What oldid revision, if any, to base the request from (default: `null`) |
221 | * @param ?string $etag The ETag to set in the HTTP request header |
222 | * |
223 | * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body' |
224 | * @phan-return array{body:string,headers:array<string,string>} |
225 | */ |
226 | public function transformHTML( |
227 | PageIdentity $page, Bcp47Code $targetLanguage, string $html, ?int $oldid, ?string $etag |
228 | ): array { |
229 | $helper = $this->getHtmlInputTransformHelper( $page, $html, $oldid, $etag, $targetLanguage ); |
230 | |
231 | $content = $helper->getContent(); |
232 | $format = $content->getDefaultFormat(); |
233 | |
234 | return [ |
235 | 'headers' => [ |
236 | 'Content-Type' => $format, |
237 | ], |
238 | 'body' => $content->serialize( $format ), |
239 | ]; |
240 | } |
241 | |
242 | /** |
243 | * @param mixed $data |
244 | * @param HtmlOutputRendererHelper $helper |
245 | * |
246 | * @return array |
247 | */ |
248 | private function fakeRESTbaseHTMLResponse( $data, HtmlOutputRendererHelper $helper ): array { |
249 | $contentLanguage = $helper->getHtmlOutputContentLanguage(); |
250 | return [ |
251 | 'headers' => [ |
252 | 'content-language' => $contentLanguage->toBcp47Code(), |
253 | 'etag' => $helper->getETag() |
254 | ], |
255 | 'body' => $data, |
256 | ]; |
257 | } |
258 | |
259 | } |