MediaWiki REL1_37
UpdateHandler.php
Go to the documentation of this file.
1<?php
2
4
5use FormatJson;
12use MWTimestamp;
13use TextContent;
16
21
26
30 protected function getTitleParameter() {
31 return $this->getValidatedParams()['title'];
32 }
33
39 public function setJsonDiffFunction( callable $jsonDiffFunction ) {
40 $this->jsonDiffFunction = $jsonDiffFunction;
41 }
42
46 public function getParamSettings() {
47 return [
48 'title' => [
49 self::PARAM_SOURCE => 'path',
50 ParamValidator::PARAM_TYPE => 'string',
51 ParamValidator::PARAM_REQUIRED => true,
52 ],
53 ];
54 }
55
59 public function getBodyValidator( $contentType ) {
60 if ( $contentType !== 'application/json' ) {
61 throw new HttpException( "Unsupported Content-Type",
62 415,
63 [ 'content_type' => $contentType ]
64 );
65 }
66
67 return new JsonBodyValidator( [
68 'source' => [
69 self::PARAM_SOURCE => 'body',
70 ParamValidator::PARAM_TYPE => 'string',
71 ParamValidator::PARAM_REQUIRED => true,
72 ],
73 'comment' => [
74 self::PARAM_SOURCE => 'body',
75 ParamValidator::PARAM_TYPE => 'string',
76 ParamValidator::PARAM_REQUIRED => true,
77 ],
78 'content_model' => [
79 self::PARAM_SOURCE => 'body',
80 ParamValidator::PARAM_TYPE => 'string',
81 ParamValidator::PARAM_REQUIRED => false,
82 ],
83 'latest' => [
84 self::PARAM_SOURCE => 'body',
85 ParamValidator::PARAM_TYPE => 'array',
86 ParamValidator::PARAM_REQUIRED => false,
87 ],
88 'token' => [
89 self::PARAM_SOURCE => 'body',
90 ParamValidator::PARAM_TYPE => 'string',
91 ParamValidator::PARAM_REQUIRED => false,
92 ParamValidator::PARAM_DEFAULT => '',
93 ],
94 ] );
95 }
96
100 protected function getActionModuleParameters() {
101 $body = $this->getValidatedBody();
102
103 $title = $this->getTitleParameter();
104 $baseRevId = $body['latest']['id'] ?? 0;
105
106 $contentmodel = $body['content_model'] ?: null;
107
108 if ( $contentmodel !== null && !$this->contentHandlerFactory->isDefinedModel( $contentmodel ) ) {
109 throw new LocalizedHttpException(
110 new MessageValue( 'rest-bad-content-model', [ $contentmodel ] ), 400
111 );
112 }
113
114 $token = $this->getActionModuleToken();
115
116 $params = [
117 'action' => 'edit',
118 'title' => $title,
119 'text' => $body['source'],
120 'summary' => $body['comment'],
121 'token' => $token
122 ];
123
124 if ( $contentmodel !== null ) {
125 $params['contentmodel'] = $contentmodel;
126 }
127
128 if ( $baseRevId > 0 ) {
129 $params['baserevid'] = $baseRevId;
130 $params['nocreate'] = true;
131 } else {
132 $params['createonly'] = true;
133 }
134
135 return $params;
136 }
137
141 protected function mapActionModuleResult( array $data ) {
142 if ( isset( $data['edit']['nochange'] ) ) {
143 // Null-edit, no new revision was created. The new revision is the same as the old.
144 // We may want to signal this more explicitly to the client in the future.
145
146 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
147 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
148
149 $data['edit']['newrevid'] = $currentRev->getId();
150 $data['edit']['newtimestamp']
151 = MWTimestamp::convert( TS_ISO_8601, $currentRev->getTimestamp() );
152 }
153
154 return parent::mapActionModuleResult( $data );
155 }
156
160 protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
161 $code = $msg->getApiCode();
162
163 // Provide a message instructing the client to provide the base revision ID for updates.
164 if ( $code === 'articleexists' ) {
165 $title = $this->getTitleParameter();
166 throw new LocalizedHttpException(
167 new MessageValue( 'rest-update-cannot-create-page', [ $title ] ),
168 409
169 );
170 }
171
172 if ( $code === 'editconflict' ) {
173 $data = $this->getConflictData();
174 throw new LocalizedHttpException( $this->makeMessageValue( $msg ), 409, $data );
175 }
176
177 parent::throwHttpExceptionForActionModuleError( $msg, $statusCode );
178 }
179
193 private function getConflictData() {
194 $body = $this->getValidatedBody();
195 $baseRevId = $body['latest']['id'] ?? 0;
196 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
197
198 $baseRev = $this->revisionLookup->getRevisionById( $baseRevId );
199 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
200
201 if ( !$baseRev || !$currentRev ) {
202 return [];
203 }
204
205 $baseContent = $baseRev->getContent(
206 SlotRecord::MAIN,
207 RevisionRecord::FOR_THIS_USER,
208 $this->getAuthority()
209 );
210 $currentContent = $currentRev->getContent(
211 SlotRecord::MAIN,
212 RevisionRecord::FOR_THIS_USER,
213 $this->getAuthority()
214 );
215
216 if ( !$baseContent || !$currentContent ) {
217 return [];
218 }
219
220 $model = $body['content_model'] ?: $baseContent->getModel();
221 $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
222 $newContent = $contentHandler->unserializeContent( $body['source'] );
223
224 if ( !$baseContent instanceof TextContent
225 || !$currentContent instanceof TextContent
226 || !$newContent instanceof TextContent
227 ) {
228 return [];
229 }
230
231 $localDiff = $this->getDiff( $baseContent, $newContent );
232 $remoteDiff = $this->getDiff( $baseContent, $currentContent );
233
234 if ( !$localDiff || !$remoteDiff ) {
235 return [];
236 }
237
238 return [
239 'base' => $baseRev->getId(),
240 'current' => $currentRev->getId(),
241 'local' => $localDiff,
242 'remote' => $remoteDiff,
243 ];
244 }
245
254 private function getDiff( TextContent $from, TextContent $to ) {
255 if ( !is_callable( $this->jsonDiffFunction ) ) {
256 return null;
257 }
258
259 $json = ( $this->jsonDiffFunction )( $from->getText(), $to->getText(), 2 );
260 return FormatJson::decode( $json, FormatJson::FORCE_ASSOC );
261 }
262}
JSON formatter wrapper class.
Library for creating and parsing MW-style timestamps.
makeMessageValue(IApiMessage $msg)
Constructs a MessageValue from an IApiMessage.
Base class for REST API handlers that perform page edits (main slot only).
getActionModuleToken()
Determines the CSRF token to be passed to the action module.
Core REST API endpoint that handles page updates (main slot only)
mapActionModuleResult(array $data)
Maps an action API result to a REST API result.mixed Data structure to be converted to JSON and wrapp...
setJsonDiffFunction(callable $jsonDiffFunction)
Sets the function to use for JSON diffs, for testing.
getTitleParameter()
Returns the requested title.string
getConflictData()
Returns an associative array to be used in the response in the event of edit conflicts.
getActionModuleParameters()
Maps a REST API request to an action API request.Implementations typically use information returned b...
getDiff(TextContent $from, TextContent $to)
Returns a text diff encoded as an array, to be included in the response data.
throwHttpExceptionForActionModuleError(IApiMessage $msg, $statusCode=400)
Throws a HttpException for a given IApiMessage that represents an error.Never returns normally....
getBodyValidator( $contentType)
Fetch the BodyValidator.to overrideBodyValidator
getParamSettings()
Fetch ParamValidator settings for parameters.Every setting must include self::PARAM_SOURCE to specify...
getValidatedBody()
Fetch the validated body.
Definition Handler.php:294
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:282
getAuthority()
Get the current acting authority.
Definition Handler.php:148
This is the base exception class for non-fatal exceptions thrown from REST handlers.
Page revision base class.
Value object representing a content slot associated with a page revision.
Content object implementation for representing flat text.
getText()
Returns the text represented by this Content object, as a string.
Value object representing a message for i18n.
Service for formatting and validating API parameters.
Interface for messages with machine-readable data for use by the API.
getApiCode()
Returns a machine-readable code for use by the API.