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