Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
EditHandler
0.00% covered (danger)
0.00%
0 / 63
0.00% covered (danger)
0.00%
0 / 7
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 needsWriteAccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleParameter
n/a
0 / 0
n/a
0 / 0
0
 validate
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 mapActionModuleResult
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 throwHttpExceptionForActionModuleError
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 mapActionModuleResponse
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 generateResponseSpec
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Rest\Handler;
4
5use MediaWiki\Api\IApiMessage;
6use MediaWiki\Config\Config;
7use MediaWiki\Content\IContentHandlerFactory;
8use MediaWiki\MainConfigNames;
9use MediaWiki\Request\WebResponse;
10use MediaWiki\Rest\LocalizedHttpException;
11use MediaWiki\Rest\Response;
12use MediaWiki\Rest\TokenAwareHandlerTrait;
13use MediaWiki\Rest\Validator\Validator;
14use MediaWiki\Revision\RevisionLookup;
15use MediaWiki\Revision\SlotRecord;
16use MediaWiki\Title\TitleFormatter;
17use MediaWiki\Title\TitleParser;
18use RuntimeException;
19use Wikimedia\Message\MessageValue;
20
21/**
22 * Base class for REST API handlers that perform page edits (main slot only).
23 */
24abstract class EditHandler extends ActionModuleBasedHandler {
25    use TokenAwareHandlerTrait;
26
27    protected Config $config;
28    protected IContentHandlerFactory $contentHandlerFactory;
29    protected TitleParser $titleParser;
30    protected TitleFormatter $titleFormatter;
31    protected RevisionLookup $revisionLookup;
32
33    public function __construct(
34        Config $config,
35        IContentHandlerFactory $contentHandlerFactory,
36        TitleParser $titleParser,
37        TitleFormatter $titleFormatter,
38        RevisionLookup $revisionLookup
39    ) {
40        $this->config = $config;
41        $this->contentHandlerFactory = $contentHandlerFactory;
42        $this->titleParser = $titleParser;
43        $this->titleFormatter = $titleFormatter;
44        $this->revisionLookup = $revisionLookup;
45    }
46
47    public function needsWriteAccess() {
48        return true;
49    }
50
51    /**
52     * Returns the requested title.
53     *
54     * @return string
55     */
56    abstract protected function getTitleParameter();
57
58    /**
59     * @inheritDoc
60     */
61    public function validate( Validator $restValidator ) {
62            parent::validate( $restValidator );
63            $this->validateToken( true );
64    }
65
66    /**
67     * @inheritDoc
68     */
69    protected function mapActionModuleResult( array $data ) {
70        if ( isset( $data['error'] ) ) {
71            throw new LocalizedHttpException( new MessageValue( 'apierror-' . $data['error'] ), 400 );
72        }
73
74        if ( !isset( $data['edit'] ) || !$data['edit']['result'] ) {
75            throw new RuntimeException( 'Bad result structure received from ApiEditPage' );
76        }
77
78        if ( $data['edit']['result'] !== 'Success' ) {
79            // Probably an edit conflict
80            // TODO: which code for null edits?
81            throw new LocalizedHttpException(
82                new MessageValue( "rest-edit-conflict", [ $data['edit']['result'] ] ),
83                409
84            );
85        }
86
87        $title = $this->titleParser->parseTitle( $data['edit']['title'] );
88
89        // This seems wasteful. This is the downside of delegating to the action API module:
90        // if we need additional data in the response, we have to load it.
91        $revision = $this->revisionLookup->getRevisionById( (int)$data['edit']['newrevid'] );
92        $content = $revision->getContent( SlotRecord::MAIN );
93
94        return [
95            'id' => $data['edit']['pageid'],
96            'title' => $this->titleFormatter->getPrefixedText( $title ),
97            'key' => $this->titleFormatter->getPrefixedDBkey( $title ),
98            'latest' => [
99                'id' => $data['edit']['newrevid'],
100                'timestamp' => $data['edit']['newtimestamp'],
101            ],
102            'license' => [
103                'url' => $this->config->get( MainConfigNames::RightsUrl ),
104                'title' => $this->config->get( MainConfigNames::RightsText )
105            ],
106            'content_model' => $data['edit']['contentmodel'],
107            'source' => $content->serialize(),
108        ];
109    }
110
111    /**
112     * @inheritDoc
113     */
114    protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
115        $code = $msg->getApiCode();
116
117        if ( $code === 'protectedpage' ) {
118            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 403 );
119        }
120
121        if ( $code === 'badtoken' ) {
122            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 403 );
123        }
124
125        if ( $code === 'missingtitle' ) {
126            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 404 );
127        }
128
129        if ( $code === 'articleexists' ) {
130            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 409 );
131        }
132
133        if ( $code === 'editconflict' ) {
134            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 409 );
135        }
136
137        if ( $code === 'ratelimited' ) {
138            throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 429 );
139        }
140
141        // Fall through to generic handling of the error (status 400).
142        parent::throwHttpExceptionForActionModuleError( $msg, $statusCode );
143    }
144
145    protected function mapActionModuleResponse(
146        WebResponse $actionModuleResponse,
147        array $actionModuleResult,
148        Response $response
149    ) {
150        parent::mapActionModuleResponse(
151            $actionModuleResponse,
152            $actionModuleResult,
153            $response
154        );
155
156        if ( $actionModuleResult['edit']['new'] ?? false ) {
157            $response->setStatus( 201 );
158        }
159    }
160
161    protected function generateResponseSpec( string $method ): array {
162        $spec = parent::generateResponseSpec( $method );
163
164        $spec['201'][parent::OPENAPI_DESCRIPTION_KEY] = 'OK';
165        $spec['201']['content']['application/json']['schema'] =
166            $spec['200']['content']['application/json']['schema'];
167        $spec['403'] = [ '$ref' => '#/components/responses/GenericErrorResponse' ];
168        $spec['409'] = [ '$ref' => '#/components/responses/GenericErrorResponse' ];
169
170        return $spec;
171    }
172
173}