MediaWiki  master
UpdateHandler.php
Go to the documentation of this file.
1 <?php
2 
3 namespace MediaWiki\Rest\Handler;
4 
5 use FormatJson;
6 use IApiMessage;
13 use TextContent;
16 
20 class UpdateHandler extends EditHandler {
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',
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',
72  ],
73  'comment' => [
74  self::PARAM_SOURCE => 'body',
75  ParamValidator::PARAM_TYPE => 'string',
77  ],
78  'content_model' => [
79  self::PARAM_SOURCE => 'body',
80  ParamValidator::PARAM_TYPE => 'string',
82  ],
83  'latest' => [
84  self::PARAM_SOURCE => 'body',
85  ParamValidator::PARAM_TYPE => 'array',
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,
202  $this->getAuthority()
203  );
204  $currentContent = $currentRev->getContent(
205  SlotRecord::MAIN,
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 }
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.
JSON formatter wrapper class.
Definition: FormatJson.php:28
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:148
makeMessageValue(IApiMessage $msg)
Constructs a MessageValue from an IApiMessage.
Base class for REST API handlers that perform page edits (main slot only).
Definition: EditHandler.php:23
Core REST API endpoint that handles page updates (main slot only)
mapActionModuleResult(array $data)
Maps an action API result to a REST API result.Data structure retrieved from the ApiResult returned b...
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.Stability: stableto overrideContent type of the request. BodyValidator
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.
Definition: SlotRecord.php:40
Library for creating and parsing MW-style timestamps.
Definition: MWTimestamp.php:48
Content object implementation for representing flat text.
Definition: TextContent.php:41
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.
const PARAM_TYPE
(string|array) Type of the parameter.
const PARAM_REQUIRED
(bool) Indicate that the parameter is required.
Interface for messages with machine-readable data for use by the API.
Definition: IApiMessage.php:39
getApiCode()
Returns a machine-readable code for use by the API.
Copyright (C) 2011-2020 Wikimedia Foundation and others.