Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
24.19% covered (danger)
24.19%
15 / 62
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
DirectParsoidClient
24.19% covered (danger)
24.19%
15 / 62
0.00% covered (danger)
0.00%
0 / 8
99.38
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHtmlOutputRendererHelper
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getHtmlInputTransformHelper
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 makeFakeRevision
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 convertWikitextToHtml
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
2.09
 convertHtmlToWikitext
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
2.02
 fakeRESTbaseHTMLResponse
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 fakeRESTbaseError
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Helper functions for using the REST interface to Parsoid.
4 *
5 * @copyright See AUTHORS.txt
6 * @license GPL-2.0-or-later
7 */
8
9namespace ContentTranslation;
10
11use Exception;
12use MediaWiki\Content\WikitextContent;
13use MediaWiki\Exception\LocalizedException;
14use MediaWiki\Language\RawMessage;
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\Rest\HttpException;
21use MediaWiki\Rest\LocalizedHttpException;
22use MediaWiki\Revision\MutableRevisionRecord;
23use MediaWiki\Revision\RevisionRecord;
24use MediaWiki\Revision\SlotRecord;
25use MediaWiki\User\User;
26
27class DirectParsoidClient implements ParsoidClient {
28
29    /**
30     * Requested Parsoid HTML version.
31     * Keep this in sync with the Accept: header in
32     * ve.init.mw.ArticleTargetLoader.js
33     */
34    public const PARSOID_VERSION = '2.6.0';
35
36    public function __construct(
37        private readonly PageRestHelperFactory $helperFactory,
38        private readonly Authority $performer
39    ) {
40    }
41
42    /**
43     * @param PageIdentity $page
44     * @param RevisionRecord|null $revision
45     *
46     * @return HtmlOutputRendererHelper
47     */
48    private function getHtmlOutputRendererHelper(
49        PageIdentity $page,
50        ?RevisionRecord $revision = null
51    ): HtmlOutputRendererHelper {
52        // TODO: remove this once we no longer need a User object for rate limiting (T310476).
53        if ( $this->performer instanceof User ) {
54            $user = $this->performer;
55        } else {
56            $user = User::newFromIdentity( $this->performer->getUser() );
57        }
58        $helper = $this->helperFactory->newHtmlOutputRendererHelper( $page, [], $user, null );
59
60        if ( $revision ) {
61            $helper->setRevision( $revision );
62        }
63
64        return $helper;
65    }
66
67    /**
68     * @param PageIdentity $page
69     * @param string $html
70     *
71     * @return HtmlInputTransformHelper
72     */
73    private function getHtmlInputTransformHelper(
74        PageIdentity $page,
75        string $html
76    ): HtmlInputTransformHelper {
77        // Fake REST body
78        $body = [
79            'html' => [
80                'body' => $html,
81            ]
82        ];
83
84        $helper = $this->helperFactory->newHtmlInputTransformHelper(
85            [], $page, $body, [], null, null
86        );
87
88        return $helper;
89    }
90
91    /**
92     * @param PageIdentity $page
93     * @param string $wikitext
94     *
95     * @return RevisionRecord
96     */
97    private function makeFakeRevision(
98        PageIdentity $page,
99        string $wikitext
100    ): RevisionRecord {
101        $rev = new MutableRevisionRecord( $page );
102        $rev->setId( 0 );
103        $rev->setPageId( $page->getId() );
104
105        $rev->setContent( SlotRecord::MAIN, new WikitextContent( $wikitext ) );
106
107        return $rev;
108    }
109
110    /**
111     * Transform wikitext to HTML with Parsoid. Wrapper for ::postData().
112     *
113     * @param PageIdentity $page The page the content belongs to use as the parsing context
114     * @param string $wikitext The wikitext fragment to parse
115     *
116     * @return array An array mimicking a RESTbase server's response,
117     *   with keys 'code', 'reason', 'headers' and 'body'
118     */
119    public function convertWikitextToHtml(
120        PageIdentity $page,
121        string $wikitext
122    ): array {
123        $revision = $this->makeFakeRevision( $page, $wikitext );
124        $helper = $this->getHtmlOutputRendererHelper( $page, $revision );
125
126        try {
127            $parserOutput = $helper->getHtml();
128            $html = $parserOutput->getRawText();
129
130            return $this->fakeRESTbaseHTMLResponse( $html, $helper );
131        } catch ( HttpException $ex ) {
132            return $this->fakeRESTbaseError( $ex );
133        }
134    }
135
136    /**
137     * Transform HTML to wikitext with Parsoid
138     *
139     * @param PageIdentity $page The page the content belongs to
140     * @param string $html The HTML of the page to be transformed
141     *
142     * @return array The response, 'code', 'reason', 'headers' and 'body'
143     */
144    public function convertHtmlToWikitext(
145        PageIdentity $page, string $html
146    ): array {
147        $helper = $this->getHtmlInputTransformHelper( $page, $html );
148
149        try {
150            $content = $helper->getContent();
151            $format = $content->getDefaultFormat();
152
153            return [
154                'code' => 200,
155                'headers' => [
156                    'Content-Type' => $format,
157                ],
158                'body' => $content->serialize( $format ),
159            ];
160        } catch ( HttpException $ex ) {
161            return $this->fakeRESTbaseError( $ex );
162        }
163    }
164
165    /**
166     * @param mixed $data
167     * @param HtmlOutputRendererHelper $helper
168     *
169     * @return array
170     */
171    private function fakeRESTbaseHTMLResponse( $data, HtmlOutputRendererHelper $helper ): array {
172        return [
173            'code' => 200,
174            'headers' => [
175                'content-language' => $helper->getHtmlOutputContentLanguage(),
176                'etag' => $helper->getETag()
177            ],
178            'body' => $data,
179        ];
180    }
181
182    private function fakeRESTbaseError( Exception $ex ): array {
183        if ( $ex instanceof LocalizedHttpException ) {
184            $msg = $ex->getMessageValue();
185        } elseif ( $ex instanceof LocalizedException ) {
186            $msg = $ex->getMessageObject();
187        } else {
188            $msg = new RawMessage( $ex->getMessage() );
189        }
190
191        return [
192            'error' => [
193                'message' => $msg->getKey(),
194                'params' => $msg->getParams(),
195            ],
196            'headers' => [],
197            'body' => $ex->getMessage(),
198        ];
199    }
200}