Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
20.44% covered (danger)
20.44%
28 / 137
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
TransformHandler
20.44% covered (danger)
20.44%
28 / 137
0.00% covered (danger)
0.00%
0 / 10
483.27
0.00% covered (danger)
0.00%
0 / 1
 getParamSettings
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
6
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkPreconditions
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOpts
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getRequestAttributes
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getTargetFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFromFormat
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 generateResponseSpec
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getHeaderParamSettings
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 execute
71.79% covered (warning)
71.79%
28 / 39
0.00% covered (danger)
0.00%
0 / 1
20.05
1<?php
2
3/**
4 * Copyright (C) 2011-2020 Wikimedia Foundation and others.
5 *
6 * @license GPL-2.0-or-later
7 */
8
9namespace MediaWiki\Rest\Handler;
10
11use MediaWiki\Rest\Handler;
12use MediaWiki\Rest\Handler\Helper\ParsoidFormatHelper;
13use MediaWiki\Rest\HttpException;
14use MediaWiki\Rest\LocalizedHttpException;
15use MediaWiki\Rest\RequestInterface;
16use MediaWiki\Rest\Response;
17use Wikimedia\Message\MessageValue;
18use Wikimedia\ParamValidator\ParamValidator;
19
20/**
21 * Handler for transforming content given in the request.
22 *
23 * This handler can provide the intended APIs of restbase V1 routes, such as:
24 * - POST /v1/transform/wikitext/to/html
25 * - POST /v1/transform/html/to/wikitext
26 * - POST /v1/transform/wikitext/to/lint
27 * - POST /v1/transform/wikitext/to/html/{title}
28 * - POST /v1/transform/html/to/wikitext/{title}
29 * - POST /v1/transform/wikitext/to/lint/{title}
30 * - POST /v1/transform/wikitext/to/html/{title}/{revision}
31 * - POST /v1/transform/html/to/wikitext/{title}/{revision}
32 * - POST /v1/transform/wikitext/to/lint/{title}/{revision}
33 *
34 * This class is extended by the Parsoid extension, as CoreTransformHandler.
35 * Be careful with changes, in order to not break Parsoid.
36 *
37 * This handler can also provide the intended APIs of Parsoid V3 routes.
38 * These routes are mentioned in the relevant links below.
39 *
40 * @see https://www.mediawiki.org/wiki/Parsoid/API#POST
41 */
42class TransformHandler extends ParsoidHandler {
43
44    /** @inheritDoc */
45    public function getParamSettings() {
46        $params = [
47            'title' => [
48                self::PARAM_SOURCE => 'path',
49                ParamValidator::PARAM_TYPE => 'string',
50                ParamValidator::PARAM_REQUIRED => false,
51                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-transform-title' ),
52            ],
53            'revision' => [
54                self::PARAM_SOURCE => 'path',
55                ParamValidator::PARAM_TYPE => 'string',
56                ParamValidator::PARAM_REQUIRED => false,
57                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-transform-revision' ),
58            ],
59        ];
60
61        if ( !isset( $this->getConfig()['from'] ) ) {
62            $params['from'] = [
63                self::PARAM_SOURCE => 'path',
64                ParamValidator::PARAM_TYPE => 'string',
65                ParamValidator::PARAM_REQUIRED => true,
66                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-param-desc-transform-from' ),
67            ];
68        }
69
70        return $params;
71    }
72
73    /**
74     * @inheritDoc
75     */
76    public function needsWriteAccess() {
77        return false;
78    }
79
80    public function checkPreconditions() {
81        // NOTE: disable all precondition checks.
82        // If-(not)-Modified-Since is not supported by the /transform/ handler.
83        // If-None-Match is not supported by the /transform/ handler.
84        // If-Match for wt2html is handled in getRequestAttributes.
85    }
86
87    protected function getOpts( array $body, RequestInterface $request ): array {
88        return array_merge(
89            $body,
90            [
91                'format' => $this->getTargetFormat(),
92                'from' => $this->getFromFormat(),
93            ]
94        );
95    }
96
97    protected function &getRequestAttributes(): array {
98        $attribs =& parent::getRequestAttributes();
99
100        $request = $this->getRequest();
101
102        // NOTE: If there is more than one ETag, this will break.
103        //       We don't have a good way to test multiple ETag to see if one of them is a working stash key.
104        $ifMatch = $request->getHeaderLine( 'If-Match' );
105
106        if ( $ifMatch ) {
107            $attribs['opts']['original']['etag'] = $ifMatch;
108        }
109
110        return $attribs;
111    }
112
113    private function getTargetFormat(): string {
114        return $this->getConfig()['format'];
115    }
116
117    private function getFromFormat(): string {
118        $request = $this->getRequest();
119        return $this->getConfig()['from'] ?? $request->getPathParam( 'from' );
120    }
121
122    protected function generateResponseSpec( string $method ): array {
123        // TODO: Consider if we prefer something like (for html and wikitext):
124        //    text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0"
125        //    text/plain; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/wikitext/1.0.0"
126        //  Those would be more specific, but fragile when the profile version changes.
127        switch ( $this->getTargetFormat() ) {
128            case 'html':
129                $spec = parent::generateResponseSpec( $method );
130                $spec['200']['content']['text/html']['schema']['type'] = 'string';
131                return $spec;
132
133            case 'wikitext':
134                $spec = parent::generateResponseSpec( $method );
135                $spec['200']['content']['text/plain']['schema']['type'] = 'string';
136                return $spec;
137
138            case 'lint':
139                $spec = parent::generateResponseSpec( $method );
140
141                // TODO: define a schema for lint responses
142                $spec['200']['content']['application/json']['schema']['type'] = 'array';
143                return $spec;
144
145            default:
146                // Additional formats may be supported by subclasses, just do nothing.
147                return parent::generateResponseSpec( $method );
148        }
149    }
150
151    /**
152     * @inheritDoc
153     * @return array
154     */
155    public function getHeaderParamSettings(): array {
156        return [
157            'Content-Type' => [
158                self::PARAM_SOURCE => 'header',
159                ParamValidator::PARAM_TYPE => 'string',
160                // RFC 7231 Â§ 3.1.1.5 allows no content-type, but
161                // a 400 still gets returned so request will error out
162                ParamValidator::PARAM_REQUIRED => false,
163                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-contenttype' ),
164            ],
165            'Accept-Language' => [
166                self::PARAM_SOURCE => 'header',
167                ParamValidator::PARAM_TYPE => 'string',
168                ParamValidator::PARAM_REQUIRED => false,
169                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-acceptlanguage' ),
170            ],
171            'Cookie' => [
172                self::PARAM_SOURCE => 'header',
173                ParamValidator::PARAM_TYPE => 'string',
174                ParamValidator::PARAM_REQUIRED => false,
175                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-cookie' ),
176            ],
177            'Authorization' => [
178                self::PARAM_SOURCE => 'header',
179                ParamValidator::PARAM_TYPE => 'string',
180                ParamValidator::PARAM_REQUIRED => false,
181                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-authorization' ),
182            ],
183            'X-Request-Id' => [
184                self::PARAM_SOURCE => 'header',
185                ParamValidator::PARAM_TYPE => 'string',
186                ParamValidator::PARAM_REQUIRED => false,
187                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-xrequestid' ),
188            ],
189            'User-Agent' => [
190                self::PARAM_SOURCE => 'header',
191                ParamValidator::PARAM_TYPE => 'string',
192                ParamValidator::PARAM_REQUIRED => false,
193                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-useragent' ),
194            ],
195            'If-Match' => [
196                self::PARAM_SOURCE => 'header',
197                ParamValidator::PARAM_TYPE => 'string',
198                ParamValidator::PARAM_REQUIRED => false,
199                Handler::PARAM_DESCRIPTION => new MessageValue( 'rest-requestheader-desc-ifmatch' ),
200            ]
201        ];
202    }
203
204    /**
205     * Transform content given in the request from or to wikitext.
206     *
207     * @return Response
208     * @throws HttpException
209     */
210    public function execute(): Response {
211        $from = $this->getFromFormat();
212        $format = $this->getTargetFormat();
213
214        // XXX: Fallback to the default valid transforms in case the request is
215        //      coming from a legacy client (restbase) that supports everything
216        //      in the default valid transforms.
217        $validTransformations = $this->getConfig()['transformations'] ?? ParsoidFormatHelper::VALID_TRANSFORM;
218
219        if ( !isset( $validTransformations[$from] ) || !in_array( $format,
220                $validTransformations[$from],
221                true ) ) {
222            throw new LocalizedHttpException( new MessageValue( "rest-invalid-transform", [ $from, $format ] ), 404 );
223        }
224        $attribs = &$this->getRequestAttributes();
225        if ( !$this->acceptable( $attribs ) ) { // mutates $attribs
226            throw new LocalizedHttpException( new MessageValue( "rest-unsupported-target-format" ), 406 );
227        }
228        if ( $from === ParsoidFormatHelper::FORMAT_WIKITEXT ) {
229            // Accept wikitext as a string or object{body,headers}
230            $wikitext = $attribs['opts']['wikitext'] ?? null;
231            if ( is_array( $wikitext ) ) {
232                $wikitext = $wikitext['body'];
233                // We've been given a pagelanguage for this page.
234                if ( isset( $attribs['opts']['wikitext']['headers']['content-language'] ) ) {
235                    $attribs['pagelanguage'] = $attribs['opts']['wikitext']['headers']['content-language'];
236                }
237            }
238            // We've been given source for this page
239            if ( $wikitext === null && isset( $attribs['opts']['original']['wikitext'] ) ) {
240                $wikitext = $attribs['opts']['original']['wikitext']['body'];
241                // We've been given a pagelanguage for this page.
242                if ( isset( $attribs['opts']['original']['wikitext']['headers']['content-language'] ) ) {
243                    $attribs['pagelanguage'] = $attribs['opts']['original']['wikitext']['headers']['content-language'];
244                }
245            }
246            // Abort if no wikitext or title.
247            if ( $wikitext === null && ( $attribs['pageName'] ?? '' ) === '' ) {
248                throw new LocalizedHttpException( new MessageValue( "rest-transform-missing-title" ), 400 );
249            }
250            $pageConfig = $this->tryToCreatePageConfig( $attribs, $wikitext );
251
252            return $this->wt2html( $pageConfig,
253                $attribs,
254                $wikitext );
255        } elseif ( $format === ParsoidFormatHelper::FORMAT_WIKITEXT ) {
256            $html = $attribs['opts']['html'] ?? null;
257            // Accept html as a string or object{body,headers}
258            if ( is_array( $html ) ) {
259                $html = $html['body'];
260            }
261            if ( $html === null ) {
262                throw new LocalizedHttpException( new MessageValue( "rest-transform-missing-html" ), 400 );
263            }
264
265            // TODO: use ETag from If-Match header, for compat!
266
267            $page = $this->tryToCreatePageIdentity( $attribs );
268
269            return $this->html2wt(
270                $page,
271                $attribs,
272                $html
273            );
274        } else {
275            return $this->pb2pb( $attribs );
276        }
277    }
278}