MediaWiki REL1_41
UpdateHandler.php
Go to the documentation of this file.
1<?php
2
4
5use FormatJson;
13use TextContent;
16
21
25 private $jsonDiffFunction;
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 ] + $this->getTokenParamDefinition() );
89 }
90
94 protected function getActionModuleParameters() {
95 $body = $this->getValidatedBody();
96
97 $title = $this->getTitleParameter();
98 $baseRevId = $body['latest']['id'] ?? 0;
99
100 $contentmodel = $body['content_model'] ?: null;
101
102 if ( $contentmodel !== null && !$this->contentHandlerFactory->isDefinedModel( $contentmodel ) ) {
103 throw new LocalizedHttpException(
104 new MessageValue( 'rest-bad-content-model', [ $contentmodel ] ), 400
105 );
106 }
107
108 $token = $this->getToken() ?? $this->getUser()->getEditToken();
109
110 $params = [
111 'action' => 'edit',
112 'title' => $title,
113 'text' => $body['source'],
114 'summary' => $body['comment'],
115 'token' => $token
116 ];
117
118 if ( $contentmodel !== null ) {
119 $params['contentmodel'] = $contentmodel;
120 }
121
122 if ( $baseRevId > 0 ) {
123 $params['baserevid'] = $baseRevId;
124 $params['nocreate'] = true;
125 } else {
126 $params['createonly'] = true;
127 }
128
129 return $params;
130 }
131
135 protected function mapActionModuleResult( array $data ) {
136 if ( isset( $data['edit']['nochange'] ) ) {
137 // Null-edit, no new revision was created. The new revision is the same as the old.
138 // We may want to signal this more explicitly to the client in the future.
139
140 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
141 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
142
143 $data['edit']['newrevid'] = $currentRev->getId();
144 $data['edit']['newtimestamp']
145 = MWTimestamp::convert( TS_ISO_8601, $currentRev->getTimestamp() );
146 }
147
148 return parent::mapActionModuleResult( $data );
149 }
150
154 protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
155 $code = $msg->getApiCode();
156
157 // Provide a message instructing the client to provide the base revision ID for updates.
158 if ( $code === 'articleexists' ) {
159 $title = $this->getTitleParameter();
160 throw new LocalizedHttpException(
161 new MessageValue( 'rest-update-cannot-create-page', [ $title ] ),
162 409
163 );
164 }
165
166 if ( $code === 'editconflict' ) {
167 $data = $this->getConflictData();
168 throw new LocalizedHttpException( $this->makeMessageValue( $msg ), 409, $data );
169 }
170
171 parent::throwHttpExceptionForActionModuleError( $msg, $statusCode );
172 }
173
187 private function getConflictData() {
188 $body = $this->getValidatedBody();
189 $baseRevId = $body['latest']['id'] ?? 0;
190 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
191
192 $baseRev = $this->revisionLookup->getRevisionById( $baseRevId );
193 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
194
195 if ( !$baseRev || !$currentRev ) {
196 return [];
197 }
198
199 $baseContent = $baseRev->getContent(
200 SlotRecord::MAIN,
201 RevisionRecord::FOR_THIS_USER,
202 $this->getAuthority()
203 );
204 $currentContent = $currentRev->getContent(
205 SlotRecord::MAIN,
206 RevisionRecord::FOR_THIS_USER,
207 $this->getAuthority()
208 );
209
210 if ( !$baseContent || !$currentContent ) {
211 return [];
212 }
213
214 $model = $body['content_model'] ?: $baseContent->getModel();
215 $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
216 $newContent = $contentHandler->unserializeContent( $body['source'] );
217
218 if ( !$baseContent instanceof TextContent
219 || !$currentContent instanceof TextContent
220 || !$newContent instanceof TextContent
221 ) {
222 return [];
223 }
224
225 $localDiff = $this->getDiff( $baseContent, $newContent );
226 $remoteDiff = $this->getDiff( $baseContent, $currentContent );
227
228 if ( !$localDiff || !$remoteDiff ) {
229 return [];
230 }
231
232 return [
233 'base' => $baseRev->getId(),
234 'current' => $currentRev->getId(),
235 'local' => $localDiff,
236 'remote' => $remoteDiff,
237 ];
238 }
239
248 private function getDiff( TextContent $from, TextContent $to ) {
249 if ( !is_callable( $this->jsonDiffFunction ) ) {
250 return null;
251 }
252
253 $json = ( $this->jsonDiffFunction )( $from->getText(), $to->getText(), 2 );
254 return FormatJson::decode( $json, true );
255 }
256}
JSON formatter wrapper class.
static decode( $value, $assoc=false)
Decodes a JSON string.
makeMessageValue(IApiMessage $msg)
Constructs a MessageValue from an IApiMessage.
Base class for REST API handlers that perform page edits (main slot only).
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
getActionModuleParameters()
Maps a REST API request to an action API request.Implementations typically use information returned b...
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:383
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:371
getAuthority()
Get the current acting authority.
Definition Handler.php:166
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.
Library for creating and parsing MW-style timestamps.
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.
Copyright (C) 2011-2020 Wikimedia Foundation and others.
getTokenParamDefinition()
Returns the definition for the token parameter, to be used in getBodyValidator().
getToken()
Determines the CSRF token to be used, possibly taking it from a request parameter.