Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.55% covered (danger)
4.55%
2 / 44
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiParsoidTrait
4.55% covered (danger)
4.55%
2 / 44
18.18% covered (danger)
18.18%
2 / 11
299.80
0.00% covered (danger)
0.00%
0 / 1
 getLogger
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 setLogger
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getStats
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 setStats
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 statsGetStartTime
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 statsRecordTiming
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 dieWithRestHttpException
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 requestRestbasePageHtml
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 transformHTML
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 transformWikitext
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
6
 getPageLanguage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getParsoidClient
n/a
0 / 0
n/a
0 / 0
0
 dieWithError
n/a
0 / 0
n/a
0 / 0
0
 dieWithException
n/a
0 / 0
n/a
0 / 0
0
 getRequest
n/a
0 / 0
n/a
0 / 0
0
1<?php
2/**
3 * Helper functions for contacting Parsoid/RESTBase from the action API.
4 *
5 * @file
6 * @ingroup Extensions
7 * @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
8 * @license MIT
9 */
10
11namespace MediaWiki\Extension\VisualEditor;
12
13use ApiUsageException;
14use Language;
15use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
16use MediaWiki\MediaWikiServices;
17use MediaWiki\Rest\HttpException;
18use MediaWiki\Rest\LocalizedHttpException;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Title\Title;
21use Message;
22use NullStatsdDataFactory;
23use PrefixingStatsdDataFactoryProxy;
24use Psr\Log\LoggerInterface;
25use Psr\Log\NullLogger;
26use Throwable;
27use WebRequest;
28
29trait ApiParsoidTrait {
30
31    private ?LoggerInterface $logger = null;
32    private ?StatsdDataFactoryInterface $stats = null;
33
34    /**
35     * @return LoggerInterface
36     */
37    protected function getLogger(): LoggerInterface {
38        return $this->logger ?: new NullLogger();
39    }
40
41    /**
42     * @param LoggerInterface $logger
43     */
44    protected function setLogger( LoggerInterface $logger ) {
45        $this->logger = $logger;
46    }
47
48    /**
49     * @return StatsdDataFactoryInterface
50     */
51    protected function getStats(): StatsdDataFactoryInterface {
52        return $this->stats ?: new NullStatsdDataFactory();
53    }
54
55    /**
56     * @param StatsdDataFactoryInterface $stats
57     */
58    protected function setStats( StatsdDataFactoryInterface $stats ) {
59        $this->stats = new PrefixingStatsdDataFactoryProxy( $stats, 'VE' );
60    }
61
62    /**
63     * @return float Return a start time for use with statsRecordTiming()
64     */
65    private function statsGetStartTime(): float {
66        return microtime( true );
67    }
68
69    /**
70     * @param string $key
71     * @param float $startTime from statsGetStartTime()
72     */
73    private function statsRecordTiming( string $key, float $startTime ) {
74        $duration = ( microtime( true ) - $startTime ) * 1000;
75        $this->getStats()->timing( $key, $duration );
76    }
77
78    /**
79     * @param HttpException $ex
80     * @return never
81     * @throws ApiUsageException
82     */
83    private function dieWithRestHttpException( HttpException $ex ) {
84        if ( $ex instanceof LocalizedHttpException ) {
85            $converter = new \MediaWiki\Message\Converter();
86            $msg = $converter->convertMessageValue( $ex->getMessageValue() );
87            $this->dieWithError( $msg, null, $ex->getErrorData() );
88        } else {
89            $this->dieWithException( $ex, [ 'data' => $ex->getErrorData() ] );
90        }
91    }
92
93    /**
94     * Request page HTML from Parsoid.
95     *
96     * @param RevisionRecord $revision Page revision
97     * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
98     * @phan-return array{body:string,headers:array<string,string>}
99     * @throws ApiUsageException
100     */
101    protected function requestRestbasePageHtml( RevisionRecord $revision ): array {
102        $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
103        $lang = self::getPageLanguage( $title );
104
105        $startTime = $this->statsGetStartTime();
106        try {
107            $response = $this->getParsoidClient()->getPageHtml( $revision, $lang );
108        } catch ( HttpException $ex ) {
109            $this->dieWithRestHttpException( $ex );
110        }
111        $this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.getPageHtml', $startTime );
112
113        return $response;
114    }
115
116    /**
117     * Transform HTML to wikitext with Parsoid.
118     *
119     * @param Title $title The title of the page
120     * @param string $html The HTML of the page to be transformed
121     * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`)
122     * @param string|null $etag The ETag to set in the HTTP request header
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     * @throws ApiUsageException
126     */
127    protected function transformHTML(
128        Title $title, string $html, int $oldid = null, string $etag = null
129    ): array {
130        $lang = self::getPageLanguage( $title );
131
132        $startTime = $this->statsGetStartTime();
133        try {
134            $response = $this->getParsoidClient()->transformHTML( $title, $lang, $html, $oldid, $etag );
135        } catch ( HttpException $ex ) {
136            $this->dieWithRestHttpException( $ex );
137        }
138        $this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.transformHTML', $startTime );
139
140        return $response;
141    }
142
143    /**
144     * Transform wikitext to HTML with Parsoid.
145     *
146     * @param Title $title The title of the page to use as the parsing context
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     * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
152     * @phan-return array{body:string,headers:array<string,string>}
153     * @throws ApiUsageException
154     */
155    protected function transformWikitext(
156        Title $title, string $wikitext, bool $bodyOnly, int $oldid = null, bool $stash = false
157    ): array {
158        $lang = self::getPageLanguage( $title );
159
160        $startTime = $this->statsGetStartTime();
161        try {
162            $response = $this->getParsoidClient()->transformWikitext(
163                $title,
164                $lang,
165                $wikitext,
166                $bodyOnly,
167                $oldid,
168                $stash
169            );
170        } catch ( HttpException $ex ) {
171            $this->dieWithRestHttpException( $ex );
172        }
173        $this->statsRecordTiming( 'ApiVisualEditor.ParsoidClient.transformWikitext', $startTime );
174
175        return $response;
176    }
177
178    /**
179     * Get the page language from a title, using the content language as fallback on special pages
180     *
181     * @param Title $title
182     * @return Language Content language
183     */
184    public static function getPageLanguage( Title $title ): Language {
185        if ( $title->isSpecial( 'CollabPad' ) ) {
186            // Use the site language for CollabPad, as getPageLanguage just
187            // returns the interface language for special pages.
188            // TODO: Let the user change the document language on multi-lingual sites.
189            return MediaWikiServices::getInstance()->getContentLanguage();
190        } else {
191            return $title->getPageLanguage();
192        }
193    }
194
195    /**
196     * @see VisualEditorParsoidClientFactory
197     * @return ParsoidClient
198     */
199    abstract protected function getParsoidClient(): ParsoidClient;
200
201    /**
202     * @see ApiBase
203     * @param string|array|Message $msg See ApiErrorFormatter::addError()
204     * @param string|null $code See ApiErrorFormatter::addError()
205     * @param array|null $data See ApiErrorFormatter::addError()
206     * @param int|null $httpCode HTTP error code to use
207     * @return never
208     * @throws ApiUsageException
209     */
210    abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = null );
211
212    /**
213     * @see ApiBase
214     * @param Throwable $exception See ApiErrorFormatter::getMessageFromException()
215     * @param array $options See ApiErrorFormatter::getMessageFromException()
216     * @return never
217     */
218    abstract public function dieWithException( Throwable $exception, array $options = [] );
219
220    /**
221     * @see ContextSource
222     * @return WebRequest
223     */
224    abstract public function getRequest();
225}