Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
30.16% covered (danger)
30.16%
19 / 63
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
DirectParsoidClient
30.16% covered (danger)
30.16%
19 / 63
25.00% covered (danger)
25.00%
2 / 8
103.21
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getHtmlOutputRendererHelper
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getHtmlInputTransformHelper
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 getPageHtml
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 makeFakeRevision
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 transformWikitext
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
2.01
 transformHTML
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 fakeRESTbaseHTMLResponse
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
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
11namespace MediaWiki\Extension\VisualEditor;
12
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Page\PageIdentity;
15use MediaWiki\Permissions\Authority;
16use MediaWiki\Rest\Handler\Helper\HtmlInputTransformHelper;
17use MediaWiki\Rest\Handler\Helper\HtmlOutputRendererHelper;
18use MediaWiki\Rest\Handler\Helper\PageRestHelperFactory;
19use MediaWiki\Revision\MutableRevisionRecord;
20use MediaWiki\Revision\RevisionRecord;
21use MediaWiki\Revision\SlotRecord;
22use User;
23use Wikimedia\Bcp47Code\Bcp47Code;
24use WikitextContent;
25
26class 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}