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