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