MediaWiki master
UpdateHandler.php
Go to the documentation of this file.
1<?php
2
4
5use FormatJson;
11use TextContent;
14
19
23 private $jsonDiffFunction;
24
28 protected function getTitleParameter() {
29 return $this->getValidatedParams()['title'];
30 }
31
37 public function setJsonDiffFunction( callable $jsonDiffFunction ) {
38 $this->jsonDiffFunction = $jsonDiffFunction;
39 }
40
44 public function getParamSettings() {
45 return [
46 'title' => [
47 self::PARAM_SOURCE => 'path',
48 ParamValidator::PARAM_TYPE => 'string',
49 ParamValidator::PARAM_REQUIRED => true,
50 ],
51 'source' => [
52 self::PARAM_SOURCE => 'body',
53 ParamValidator::PARAM_TYPE => 'string',
54 ParamValidator::PARAM_REQUIRED => true,
55 ],
56 'comment' => [
57 self::PARAM_SOURCE => 'body',
58 ParamValidator::PARAM_TYPE => 'string',
59 ParamValidator::PARAM_REQUIRED => true,
60 ],
61 'content_model' => [
62 self::PARAM_SOURCE => 'body',
63 ParamValidator::PARAM_TYPE => 'string',
64 ParamValidator::PARAM_REQUIRED => false,
65 ],
66 'latest' => [
67 self::PARAM_SOURCE => 'body',
68 ParamValidator::PARAM_TYPE => 'array',
69 ParamValidator::PARAM_REQUIRED => false,
70 ],
71
72 ]
74 + parent::getParamSettings();
75 }
76
80 protected function getActionModuleParameters() {
81 $body = $this->getValidatedBody();
82 '@phan-var array $body';
83
84 $title = $this->getTitleParameter();
85 $baseRevId = $body['latest']['id'] ?? 0;
86
87 $contentmodel = $body['content_model'] ?: null;
88
89 if ( $contentmodel !== null && !$this->contentHandlerFactory->isDefinedModel( $contentmodel ) ) {
90 throw new LocalizedHttpException(
91 new MessageValue( 'rest-bad-content-model', [ $contentmodel ] ), 400
92 );
93 }
94
95 // Use a known good CSRF token if a token is not needed because we are
96 // using a method of authentication that protects against CSRF, like OAuth.
97 $token = $this->needsToken() ? $this->getToken() : $this->getUser()->getEditToken();
98
99 $params = [
100 'action' => 'edit',
101 'title' => $title,
102 'text' => $body['source'],
103 'summary' => $body['comment'],
104 'token' => $token
105 ];
106
107 if ( $contentmodel !== null ) {
108 $params['contentmodel'] = $contentmodel;
109 }
110
111 if ( $baseRevId > 0 ) {
112 $params['baserevid'] = $baseRevId;
113 $params['nocreate'] = true;
114 } else {
115 $params['createonly'] = true;
116 }
117
118 return $params;
119 }
120
124 protected function mapActionModuleResult( array $data ) {
125 if ( isset( $data['edit']['nochange'] ) ) {
126 // Null-edit, no new revision was created. The new revision is the same as the old.
127 // We may want to signal this more explicitly to the client in the future.
128
129 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
130 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
131
132 $data['edit']['newrevid'] = $currentRev->getId();
133 $data['edit']['newtimestamp']
134 = MWTimestamp::convert( TS_ISO_8601, $currentRev->getTimestamp() );
135 }
136
137 return parent::mapActionModuleResult( $data );
138 }
139
143 protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) {
144 $code = $msg->getApiCode();
145
146 // Provide a message instructing the client to provide the base revision ID for updates.
147 if ( $code === 'articleexists' ) {
148 $title = $this->getTitleParameter();
149 throw new LocalizedHttpException(
150 new MessageValue( 'rest-update-cannot-create-page', [ $title ] ),
151 409
152 );
153 }
154
155 if ( $code === 'editconflict' ) {
156 $data = $this->getConflictData();
157 throw new LocalizedHttpException( $this->makeMessageValue( $msg ), 409, $data );
158 }
159
160 parent::throwHttpExceptionForActionModuleError( $msg, $statusCode );
161 }
162
176 private function getConflictData() {
177 $body = $this->getValidatedBody();
178 '@phan-var array $body';
179 $baseRevId = $body['latest']['id'] ?? 0;
180 $title = $this->titleParser->parseTitle( $this->getValidatedParams()['title'] );
181
182 $baseRev = $this->revisionLookup->getRevisionById( $baseRevId );
183 $currentRev = $this->revisionLookup->getRevisionByTitle( $title );
184
185 if ( !$baseRev || !$currentRev ) {
186 return [];
187 }
188
189 $baseContent = $baseRev->getContent(
190 SlotRecord::MAIN,
191 RevisionRecord::FOR_THIS_USER,
192 $this->getAuthority()
193 );
194 $currentContent = $currentRev->getContent(
195 SlotRecord::MAIN,
196 RevisionRecord::FOR_THIS_USER,
197 $this->getAuthority()
198 );
199
200 if ( !$baseContent || !$currentContent ) {
201 return [];
202 }
203
204 $model = $body['content_model'] ?: $baseContent->getModel();
205 $contentHandler = $this->contentHandlerFactory->getContentHandler( $model );
206 $newContent = $contentHandler->unserializeContent( $body['source'] );
207
208 if ( !$baseContent instanceof TextContent
209 || !$currentContent instanceof TextContent
210 || !$newContent instanceof TextContent
211 ) {
212 return [];
213 }
214
215 $localDiff = $this->getDiff( $baseContent, $newContent );
216 $remoteDiff = $this->getDiff( $baseContent, $currentContent );
217
218 if ( !$localDiff || !$remoteDiff ) {
219 return [];
220 }
221
222 return [
223 'base' => $baseRev->getId(),
224 'current' => $currentRev->getId(),
225 'local' => $localDiff,
226 'remote' => $remoteDiff,
227 ];
228 }
229
238 private function getDiff( TextContent $from, TextContent $to ) {
239 if ( !is_callable( $this->jsonDiffFunction ) ) {
240 return null;
241 }
242
243 $json = ( $this->jsonDiffFunction )( $from->getText(), $to->getText(), 2 );
244 return FormatJson::decode( $json, true );
245 }
246}
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....
getParamSettings()
Fetch ParamValidator settings for parameters.Every setting must include self::PARAM_SOURCE to specify...
getValidatedBody()
Fetch the validated body.
Definition Handler.php:609
getValidatedParams()
Fetch the validated parameters.
Definition Handler.php:597
getAuthority()
Get the current acting authority.
Definition Handler.php:205
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.