MediaWiki master
ApiStashEdit.php
Go to the documentation of this file.
1<?php
29
43class ApiStashEdit extends ApiBase {
44
45 private IContentHandlerFactory $contentHandlerFactory;
46 private PageEditStash $pageEditStash;
47 private RevisionLookup $revisionLookup;
48 private IBufferingStatsdDataFactory $statsdDataFactory;
49 private WikiPageFactory $wikiPageFactory;
50 private TempUserCreator $tempUserCreator;
51 private UserFactory $userFactory;
52
64 public function __construct(
65 ApiMain $main,
66 $action,
67 IContentHandlerFactory $contentHandlerFactory,
68 PageEditStash $pageEditStash,
69 RevisionLookup $revisionLookup,
70 IBufferingStatsdDataFactory $statsdDataFactory,
71 WikiPageFactory $wikiPageFactory,
72 TempUserCreator $tempUserCreator,
73 UserFactory $userFactory
74 ) {
75 parent::__construct( $main, $action );
76
77 $this->contentHandlerFactory = $contentHandlerFactory;
78 $this->pageEditStash = $pageEditStash;
79 $this->revisionLookup = $revisionLookup;
80 $this->statsdDataFactory = $statsdDataFactory;
81 $this->wikiPageFactory = $wikiPageFactory;
82 $this->tempUserCreator = $tempUserCreator;
83 $this->userFactory = $userFactory;
84 }
85
86 public function execute() {
87 $user = $this->getUser();
89
90 if ( $user->isBot() ) {
91 $this->dieWithError( 'apierror-botsnotsupported' );
92 }
93
94 $page = $this->getTitleOrPageId( $params );
95 $title = $page->getTitle();
96 $this->getErrorFormatter()->setContextTitle( $title );
97
98 if ( !$this->contentHandlerFactory
99 ->getContentHandler( $params['contentmodel'] )
100 ->isSupportedFormat( $params['contentformat'] )
101 ) {
102 $this->dieWithError(
103 [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
104 'badmodelformat'
105 );
106 }
107
108 $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' );
109
110 if ( $params['stashedtexthash'] !== null ) {
111 // Load from cache since the client indicates the text is the same as last stash
112 $textHash = $params['stashedtexthash'];
113 if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
114 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
115 }
116 $text = $this->pageEditStash->fetchInputText( $textHash );
117 if ( !is_string( $text ) ) {
118 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
119 }
120 } else {
121 // 'text' was passed. Trim and fix newlines so the key SHA1's
122 // match (see WebRequest::getText())
123 $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
124 $textHash = sha1( $text );
125 }
126
127 $textContent = $this->contentHandlerFactory
128 ->getContentHandler( $params['contentmodel'] )
129 ->unserializeContent( $text, $params['contentformat'] );
130
131 $page = $this->wikiPageFactory->newFromTitle( $title );
132 if ( $page->exists() ) {
133 // Page exists: get the merged content with the proposed change
134 $baseRev = $this->revisionLookup->getRevisionByPageId(
135 $page->getId(),
136 $params['baserevid']
137 );
138 if ( !$baseRev ) {
139 $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
140 }
141 $currentRev = $page->getRevisionRecord();
142 if ( !$currentRev ) {
143 $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
144 }
145 // Merge in the new version of the section to get the proposed version
146 $editContent = $page->replaceSectionAtRev(
147 $params['section'],
148 $textContent,
149 $params['sectiontitle'],
150 $baseRev->getId()
151 );
152 if ( !$editContent ) {
153 $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
154 }
155 if ( $currentRev->getId() == $baseRev->getId() ) {
156 // Base revision was still the latest; nothing to merge
157 $content = $editContent;
158 } else {
159 // Merge the edit into the current version
160 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
161 $currentContent = $currentRev->getContent( SlotRecord::MAIN );
162 if ( !$baseContent || !$currentContent ) {
163 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
164 }
165
166 $baseModel = $baseContent->getModel();
167 $currentModel = $currentContent->getModel();
168
169 // T255700: Put this in try-block because if the models of these three Contents
170 // happen to not be identical, the ContentHandler may throw exception here.
171 try {
172 $content = $this->contentHandlerFactory
173 ->getContentHandler( $baseModel )
174 ->merge3( $baseContent, $editContent, $currentContent );
175 } catch ( Exception $e ) {
176 $this->dieWithException( $e, [
177 'wrap' => ApiMessage::create(
178 [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
179 )
180 ] );
181 }
182
183 }
184 } else {
185 // New pages: use the user-provided content model
186 $content = $textContent;
187 }
188
189 if ( !$content ) { // merge3() failed
190 $this->getResult()->addValue( null,
191 $this->getModuleName(), [ 'status' => 'editconflict' ] );
192 return;
193 }
194
195 if ( !$user->authorizeWrite( 'stashedit', $title ) ) {
196 $status = 'ratelimited';
197 } else {
198 $user = $this->getUserForPreview();
199 $updater = $page->newPageUpdater( $user );
200 $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] );
201 $this->pageEditStash->stashInputText( $text, $textHash );
202 }
203
204 $this->statsdDataFactory->increment( "editstash.cache_stores.$status" );
205
206 $ret = [ 'status' => $status ];
207 // If we were rate-limited, we still return the pre-existing valid hash if one was passed
208 if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
209 $ret['texthash'] = $textHash;
210 }
211
212 $this->getResult()->addValue( null, $this->getModuleName(), $ret );
213 }
214
215 private function getUserForPreview() {
216 $user = $this->getUser();
217 if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
218 return $this->userFactory->newUnsavedTempUser(
219 $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
220 );
221 }
222 return $user;
223 }
224
225 public function getAllowedParams() {
226 return [
227 'title' => [
228 ParamValidator::PARAM_TYPE => 'string',
229 ParamValidator::PARAM_REQUIRED => true
230 ],
231 'section' => [
232 ParamValidator::PARAM_TYPE => 'string',
233 ],
234 'sectiontitle' => [
235 ParamValidator::PARAM_TYPE => 'string'
236 ],
237 'text' => [
238 ParamValidator::PARAM_TYPE => 'text',
239 ParamValidator::PARAM_DEFAULT => null
240 ],
241 'stashedtexthash' => [
242 ParamValidator::PARAM_TYPE => 'string',
243 ParamValidator::PARAM_DEFAULT => null
244 ],
245 'summary' => [
246 ParamValidator::PARAM_TYPE => 'string',
247 ParamValidator::PARAM_DEFAULT => ''
248 ],
249 'contentmodel' => [
250 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
251 ParamValidator::PARAM_REQUIRED => true
252 ],
253 'contentformat' => [
254 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
255 ParamValidator::PARAM_REQUIRED => true
256 ],
257 'baserevid' => [
258 ParamValidator::PARAM_TYPE => 'integer',
259 ParamValidator::PARAM_REQUIRED => true
260 ]
261 ];
262 }
263
264 public function needsToken() {
265 return 'csrf';
266 }
267
268 public function mustBePosted() {
269 return true;
270 }
271
272 public function isWriteMode() {
273 return true;
274 }
275
276 public function isInternal() {
277 return true;
278 }
279
280 public function getHelpUrls() {
281 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Stashedit';
282 }
283}
array $params
The job parameters.
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:64
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1542
getErrorFormatter()
Definition ApiBase.php:691
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:959
getResult()
Get the result object.
Definition ApiBase.php:680
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:820
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:541
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition ApiBase.php:1107
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1555
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:65
Prepare an edit in shared cache so that it can be reused on edit.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
isInternal()
Indicates whether this module is considered to be "internal".
__construct(ApiMain $main, $action, IContentHandlerFactory $contentHandlerFactory, PageEditStash $pageEditStash, RevisionLookup $revisionLookup, IBufferingStatsdDataFactory $statsdDataFactory, WikiPageFactory $wikiPageFactory, TempUserCreator $tempUserCreator, UserFactory $userFactory)
needsToken()
Returns the token type this module requires in order to execute.
getHelpUrls()
Return links to more detailed help pages about the module.
isWriteMode()
Indicates whether this module requires write mode.
mustBePosted()
Indicates whether this module must be called with a POST request.
Service for creating WikiPage objects.
Value object representing a content slot associated with a page revision.
Manage the pre-emptive page parsing for edits to wiki pages.
Service for temporary user creation.
Creates User objects.
Service for formatting and validating API parameters.
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Service for looking up page revisions.