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