MediaWiki master
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
60 public function getBodyValidator( $contentType ) {
61 if ( $contentType !== 'application/json' ) {
62 throw new HttpException( "Unsupported Content-Type",
63 415,
64 [ 'content_type' => $contentType ]
65 );
66 }
67
68 return new JsonBodyValidator( [
69 'source' => [
70 self::PARAM_SOURCE => 'body',
71 ParamValidator::PARAM_TYPE => 'string',
72 ParamValidator::PARAM_REQUIRED => true,
73 ],
74 'comment' => [
75 self::PARAM_SOURCE => 'body',
76 ParamValidator::PARAM_TYPE => 'string',
77 ParamValidator::PARAM_REQUIRED => true,
78 ],
79 'content_model' => [
80 self::PARAM_SOURCE => 'body',
81 ParamValidator::PARAM_TYPE => 'string',
82 ParamValidator::PARAM_REQUIRED => false,
83 ],
84 'latest' => [
85 self::PARAM_SOURCE => 'body',
86 ParamValidator::PARAM_TYPE => 'array',
87 ParamValidator::PARAM_REQUIRED => false,
88 ],
89 ] + $this->getTokenParamDefinition() );
90 }
91
95 protected function getActionModuleParameters() {
96 $body = $this->getValidatedBody();
97 '@phan-var array $body';
98
99 $title = $this->getTitleParameter();
100 $baseRevId = $body['latest']['id'] ?? 0;
101
102 $contentmodel = $body['content_model'] ?: null;
103
104 if ( $contentmodel !== null && !$this->contentHandlerFactory->isDefinedModel( $contentmodel ) ) {
105 throw new LocalizedHttpException(
106 new MessageValue( 'rest-bad-content-model', [ $contentmodel ] ), 400
107 );
108 }
109
110 // Use a known good CSRF token if a token is not needed because we are
111 // using a method of authentication that protects against CSRF, like OAuth.
112 $token = $this->needsToken() ? $this->getToken() : $this->getUser()->getEditToken();
113
114 $params = [
115 'action' => 'edit',
116 'title' => $title,
117 'text' => $body['source'],
118 'summary' => $body['comment'],
119 'token' => $token
120 ];
121
122 if ( $contentmodel !== null ) {
123 $params['contentmodel'] = $contentmodel;
124 }
125
126 if ( $baseRevId > 0 ) {
127 $params['baserevid'] = $baseRevId;
128 $params['nocreate'] = true;
129 } else {
130 $params['createonly'] = true;
131 }
132
133 return $params;
134 }
135
139 protected function mapActionModuleResult( array $data ) {
140 if ( isset( $data['edit']['nochange'] ) ) {
141 // Null-edit, no new revision was created. The new revision is the same as the old.
142 // We may want to signal this more explicitly to the client in the future.
143
144 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
145 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
146
147 $data['edit']['newrevid'] = $currentRev->getId();
148 $data['edit']['newtimestamp']
149 = MWTimestamp::convert( TS_ISO_8601, $currentRev->getTimestamp() );
150 }
151
152 return parent::mapActionModuleResult( $data );
153 }
154
158 protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
159 $code = $msg->getApiCode();
160
161 // Provide a message instructing the client to provide the base revision ID for updates.
162 if ( $code === 'articleexists' ) {
163 $title = $this->getTitleParameter();
164 throw new LocalizedHttpException(
165 new MessageValue( 'rest-update-cannot-create-page', [ $title ] ),
166 409
167 );
168 }
169
170 if ( $code === 'editconflict' ) {
171 $data = $this->getConflictData();
172 throw new LocalizedHttpException( $this->makeMessageValue( $msg ), 409, $data );
173 }
174
175 parent::throwHttpExceptionForActionModuleError( $msg, $statusCode );
176 }
177
191 private function getConflictData() {
192 $body = $this->getValidatedBody();
193 '@phan-var array $body';
194 $baseRevId = $body['latest']['id'] ?? 0;
195 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
196
197 $baseRev = $this->revisionLookup->getRevisionById( $baseRevId );
198 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
199
200 if ( !$baseRev || !$currentRev ) {
201 return [];
202 }
203
204 $baseContent = $baseRev->getContent(
205 SlotRecord::MAIN,
206 RevisionRecord::FOR_THIS_USER,
207 $this->getAuthority()
208 );
209 $currentContent = $currentRev->getContent(
210 SlotRecord::MAIN,
211 RevisionRecord::FOR_THIS_USER,
212 $this->getAuthority()
213 );
214
215 if ( !$baseContent || !$currentContent ) {
216 return [];
217 }
218
219 $model = $body['content_model'] ?: $baseContent->getModel();
220 $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
221 $newContent = $contentHandler->unserializeContent( $body['source'] );
222
223 if ( !$baseContent instanceof TextContent
224 || !$currentContent instanceof TextContent
225 || !$newContent instanceof TextContent
226 ) {
227 return [];
228 }
229
230 $localDiff = $this->getDiff( $baseContent, $newContent );
231 $remoteDiff = $this->getDiff( $baseContent, $currentContent );
232
233 if ( !$localDiff || !$remoteDiff ) {
234 return [];
235 }
236
237 return [
238 'base' => $baseRev->getId(),
239 'current' => $currentRev->getId(),
240 'local' => $localDiff,
241 'remote' => $remoteDiff,
242 ];
243 }
244
253 private function getDiff( TextContent $from, TextContent $to ) {
254 if ( !is_callable( $this->jsonDiffFunction ) ) {
255 return null;
256 }
257
258 $json = ( $this->jsonDiffFunction )( $from->getText(), $to->getText(), 2 );
259 return FormatJson::decode( $json, true );
260 }
261}
array $params
The job parameters.
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 A {NullBodyValidator} in this default implementation...
getParamSettings()
Fetch ParamValidator settings for parameters.Every setting must include self::PARAM_SOURCE to specify...
getValidatedBody()
Fetch the validated body.
Definition Handler.php:517
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:505
getAuthority()
Get the current acting authority.
Definition Handler.php:168
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.
needsToken()
Determines whether a CSRF token is needed.