Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 63 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
EditHandler | |
0.00% |
0 / 63 |
|
0.00% |
0 / 7 |
342 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
needsWriteAccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitleParameter | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
validate | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
mapActionModuleResult | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
throwHttpExceptionForActionModuleError | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
56 | |||
mapActionModuleResponse | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
generateResponseSpec | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Rest\Handler; |
4 | |
5 | use MediaWiki\Api\IApiMessage; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Content\IContentHandlerFactory; |
8 | use MediaWiki\MainConfigNames; |
9 | use MediaWiki\Request\WebResponse; |
10 | use MediaWiki\Rest\LocalizedHttpException; |
11 | use MediaWiki\Rest\Response; |
12 | use MediaWiki\Rest\TokenAwareHandlerTrait; |
13 | use MediaWiki\Rest\Validator\Validator; |
14 | use MediaWiki\Revision\RevisionLookup; |
15 | use MediaWiki\Revision\SlotRecord; |
16 | use MediaWiki\Title\TitleFormatter; |
17 | use MediaWiki\Title\TitleParser; |
18 | use RuntimeException; |
19 | use Wikimedia\Message\MessageValue; |
20 | |
21 | /** |
22 | * Base class for REST API handlers that perform page edits (main slot only). |
23 | */ |
24 | abstract 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 | } |