Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
4.76% covered (danger)
4.76%
2 / 42
18.18% covered (danger)
18.18%
2 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiParsoidTrait
4.76% covered (danger)
4.76%
2 / 42
18.18% covered (danger)
18.18%
2 / 11
297.88
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
 getStatsFactory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 setStatsFactory
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 / 3
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 MediaWiki\Api\ApiUsageException;
14use MediaWiki\Language\Language;
15use MediaWiki\MediaWikiServices;
16use MediaWiki\Request\WebRequest;
17use MediaWiki\Rest\HttpException;
18use MediaWiki\Rest\LocalizedHttpException;
19use MediaWiki\Revision\RevisionRecord;
20use MediaWiki\Title\Title;
21use Psr\Log\LoggerInterface;
22use Psr\Log\NullLogger;
23use Throwable;
24use Wikimedia\Message\MessageSpecifier;
25use Wikimedia\Stats\StatsFactory;
26
27trait ApiParsoidTrait {
28
29    private ?LoggerInterface $logger = null;
30    /** @var StatsFactory|null */
31    private $stats = null;
32
33    protected function getLogger(): LoggerInterface {
34        return $this->logger ?: new NullLogger();
35    }
36
37    protected function setLogger( LoggerInterface $logger ): void {
38        $this->logger = $logger;
39    }
40
41    protected function getStatsFactory(): StatsFactory {
42        return $this->stats ?: StatsFactory::newNull();
43    }
44
45    protected function setStatsFactory( StatsFactory $statsFactory ): void {
46        $this->stats = $statsFactory;
47    }
48
49    /**
50     * @return float Return a start time for use with statsRecordTiming()
51     */
52    private function statsGetStartTime(): float {
53        return microtime( true );
54    }
55
56    /**
57     * @param string $key
58     * @param float $startTime from statsGetStartTime()
59     */
60    private function statsRecordTiming( string $key, float $startTime ): void {
61        $duration = ( microtime( true ) - $startTime ) * 1000;
62        $this->getStatsFactory()->getTiming( $key )->observe( $duration );
63    }
64
65    /**
66     * @return never
67     * @throws ApiUsageException
68     */
69    private function dieWithRestHttpException( HttpException $ex ): void {
70        if ( $ex instanceof LocalizedHttpException ) {
71            $this->dieWithError( $ex->getMessageValue(), null, $ex->getErrorData() );
72        } else {
73            $this->dieWithException( $ex, [ 'data' => $ex->getErrorData() ] );
74        }
75    }
76
77    /**
78     * Request page HTML from Parsoid.
79     *
80     * @param RevisionRecord $revision Page revision
81     * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
82     * @phan-return array{body:string,headers:array<string,string>}
83     * @throws ApiUsageException
84     */
85    protected function requestRestbasePageHtml( RevisionRecord $revision ): array {
86        $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
87        $lang = self::getPageLanguage( $title );
88
89        $startTime = $this->statsGetStartTime();
90        try {
91            $response = $this->getParsoidClient()->getPageHtml( $revision, $lang );
92        } catch ( HttpException $ex ) {
93            $this->dieWithRestHttpException( $ex );
94        }
95        $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_getPageHtml_seconds', $startTime );
96
97        return $response;
98    }
99
100    /**
101     * Transform HTML to wikitext with Parsoid.
102     *
103     * @param Title $title The title of the page
104     * @param string $html The HTML of the page to be transformed
105     * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`)
106     * @param string|null $etag The ETag to set in the HTTP request header
107     * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
108     * @phan-return array{body:string,headers:array<string,string>}
109     * @throws ApiUsageException
110     */
111    protected function transformHTML(
112        Title $title, string $html, ?int $oldid = null, ?string $etag = null
113    ): array {
114        $lang = self::getPageLanguage( $title );
115
116        $startTime = $this->statsGetStartTime();
117        try {
118            $response = $this->getParsoidClient()->transformHTML( $title, $lang, $html, $oldid, $etag );
119        } catch ( HttpException $ex ) {
120            $this->dieWithRestHttpException( $ex );
121        }
122        $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_transformHTML_seconds', $startTime );
123
124        return $response;
125    }
126
127    /**
128     * Transform wikitext to HTML with Parsoid.
129     *
130     * @param Title $title The title of the page to use as the parsing context
131     * @param string $wikitext The wikitext fragment to parse
132     * @param bool $bodyOnly Whether to provide only the contents of the `<body>` tag
133     * @param int|null $oldid What oldid revision, if any, to base the request from (default: `null`)
134     * @param bool $stash Whether to stash the result in the server-side cache (default: `false`)
135     * @return array An array mimicking a RESTbase server's response, with keys: 'headers' and 'body'
136     * @phan-return array{body:string,headers:array<string,string>}
137     * @throws ApiUsageException
138     */
139    protected function transformWikitext(
140        Title $title, string $wikitext, bool $bodyOnly, ?int $oldid = null, bool $stash = false
141    ): array {
142        $lang = self::getPageLanguage( $title );
143
144        $startTime = $this->statsGetStartTime();
145        try {
146            $response = $this->getParsoidClient()->transformWikitext(
147                $title,
148                $lang,
149                $wikitext,
150                $bodyOnly,
151                $oldid,
152                $stash
153            );
154        } catch ( HttpException $ex ) {
155            $this->dieWithRestHttpException( $ex );
156        }
157        $this->statsRecordTiming( 'ApiVisualEditor_ParsoidClient_transformWikitext_seconds', $startTime );
158
159        return $response;
160    }
161
162    /**
163     * Get the page language from a title, using the content language as fallback on special pages
164     *
165     * @param Title $title
166     * @return Language Content language
167     */
168    public static function getPageLanguage( Title $title ): Language {
169        if ( $title->isSpecial( 'CollabPad' ) ) {
170            // Use the site language for CollabPad, as getPageLanguage just
171            // returns the interface language for special pages.
172            // TODO: Let the user change the document language on multi-lingual sites.
173            return MediaWikiServices::getInstance()->getContentLanguage();
174        } else {
175            return $title->getPageLanguage();
176        }
177    }
178
179    /**
180     * @see VisualEditorParsoidClientFactory
181     */
182    abstract protected function getParsoidClient(): ParsoidClient;
183
184    /**
185     * @see ApiBase
186     * @param string|array|MessageSpecifier $msg See ApiErrorFormatter::addError()
187     * @param string|null $code See ApiErrorFormatter::addError()
188     * @param array|null $data See ApiErrorFormatter::addError()
189     * @param int $httpCode HTTP error code to use
190     * @throws ApiUsageException always
191     * @return never
192     */
193    abstract public function dieWithError( $msg, $code = null, $data = null, $httpCode = 0 );
194
195    /**
196     * @see ApiBase
197     * @param Throwable $exception See ApiErrorFormatter::getMessageFromException()
198     * @param array $options See ApiErrorFormatter::getMessageFromException()
199     * @throws ApiUsageException always
200     * @return never
201     */
202    abstract public function dieWithException( Throwable $exception, array $options = [] );
203
204    /**
205     * @see ContextSource
206     * @return WebRequest
207     */
208    abstract public function getRequest();
209}