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