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