MediaWiki REL1_39
ApiStashEdit.php
Go to the documentation of this file.
1<?php
27
41class ApiStashEdit extends ApiBase {
42
44 private $contentHandlerFactory;
45
47 private $pageEditStash;
48
50 private $revisionLookup;
51
53 private $statsdDataFactory;
54
56 private $wikiPageFactory;
57
67 public function __construct(
68 ApiMain $main,
69 $action,
70 IContentHandlerFactory $contentHandlerFactory,
71 PageEditStash $pageEditStash,
72 RevisionLookup $revisionLookup,
73 IBufferingStatsdDataFactory $statsdDataFactory,
74 WikiPageFactory $wikiPageFactory
75 ) {
76 parent::__construct( $main, $action );
77
78 $this->contentHandlerFactory = $contentHandlerFactory;
79 $this->pageEditStash = $pageEditStash;
80 $this->revisionLookup = $revisionLookup;
81 $this->statsdDataFactory = $statsdDataFactory;
82 $this->wikiPageFactory = $wikiPageFactory;
83 }
84
85 public function execute() {
86 $user = $this->getUser();
87 $params = $this->extractRequestParams();
88
89 if ( $user->isBot() ) {
90 $this->dieWithError( 'apierror-botsnotsupported' );
91 }
92
93 $page = $this->getTitleOrPageId( $params );
94 $title = $page->getTitle();
95 $this->getErrorFormatter()->setContextTitle( $title );
96
97 if ( !$this->contentHandlerFactory
98 ->getContentHandler( $params['contentmodel'] )
99 ->isSupportedFormat( $params['contentformat'] )
100 ) {
101 $this->dieWithError(
102 [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
103 'badmodelformat'
104 );
105 }
106
107 $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' );
108
109 $text = null;
110 $textHash = null;
111 if ( $params['stashedtexthash'] !== null ) {
112 // Load from cache since the client indicates the text is the same as last stash
113 $textHash = $params['stashedtexthash'];
114 if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
115 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
116 }
117 $text = $this->pageEditStash->fetchInputText( $textHash );
118 if ( !is_string( $text ) ) {
119 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
120 }
121 } else {
122 // 'text' was passed. Trim and fix newlines so the key SHA1's
123 // match (see WebRequest::getText())
124 $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
125 $textHash = sha1( $text );
126 }
127
128 $textContent = $this->contentHandlerFactory
129 ->getContentHandler( $params['contentmodel'] )
130 ->unserializeContent( $text, $params['contentformat'] );
131
132 $page = $this->wikiPageFactory->newFromTitle( $title );
133 if ( $page->exists() ) {
134 // Page exists: get the merged content with the proposed change
135 $baseRev = $this->revisionLookup->getRevisionByPageId(
136 $page->getId(),
137 $params['baserevid']
138 );
139 if ( !$baseRev ) {
140 $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
141 }
142 $currentRev = $page->getRevisionRecord();
143 if ( !$currentRev ) {
144 $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
145 }
146 // Merge in the new version of the section to get the proposed version
147 $editContent = $page->replaceSectionAtRev(
148 $params['section'],
149 $textContent,
150 $params['sectiontitle'],
151 $baseRev->getId()
152 );
153 if ( !$editContent ) {
154 $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
155 }
156 if ( $currentRev->getId() == $baseRev->getId() ) {
157 // Base revision was still the latest; nothing to merge
158 $content = $editContent;
159 } else {
160 // Merge the edit into the current version
161 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
162 $currentContent = $currentRev->getContent( SlotRecord::MAIN );
163 if ( !$baseContent || !$currentContent ) {
164 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
165 }
166
167 $baseModel = $baseContent->getModel();
168 $currentModel = $currentContent->getModel();
169
170 // T255700: Put this in try-block because if the models of these three Contents
171 // happen to not be identical, the ContentHandler may throw exception here.
172 try {
173 $content = $this->contentHandlerFactory
174 ->getContentHandler( $baseModel )
175 ->merge3( $baseContent, $editContent, $currentContent );
176 } catch ( Exception $e ) {
177 $this->dieWithException( $e, [
178 'wrap' => ApiMessage::create(
179 [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
180 )
181 ] );
182 }
183
184 }
185 } else {
186 // New pages: use the user-provided content model
187 $content = $textContent;
188 }
189
190 if ( !$content ) { // merge3() failed
191 $this->getResult()->addValue( null,
192 $this->getModuleName(), [ 'status' => 'editconflict' ] );
193 return;
194 }
195
196 if ( $user->pingLimiter( 'stashedit' ) ) {
197 $status = 'ratelimited';
198 } else {
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 public function getAllowedParams() {
216 return [
217 'title' => [
218 ParamValidator::PARAM_TYPE => 'string',
219 ParamValidator::PARAM_REQUIRED => true
220 ],
221 'section' => [
222 ParamValidator::PARAM_TYPE => 'string',
223 ],
224 'sectiontitle' => [
225 ParamValidator::PARAM_TYPE => 'string'
226 ],
227 'text' => [
228 ParamValidator::PARAM_TYPE => 'text',
229 ParamValidator::PARAM_DEFAULT => null
230 ],
231 'stashedtexthash' => [
232 ParamValidator::PARAM_TYPE => 'string',
233 ParamValidator::PARAM_DEFAULT => null
234 ],
235 'summary' => [
236 ParamValidator::PARAM_TYPE => 'string',
237 ParamValidator::PARAM_DEFAULT => ''
238 ],
239 'contentmodel' => [
240 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
241 ParamValidator::PARAM_REQUIRED => true
242 ],
243 'contentformat' => [
244 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
245 ParamValidator::PARAM_REQUIRED => true
246 ],
247 'baserevid' => [
248 ParamValidator::PARAM_TYPE => 'integer',
249 ParamValidator::PARAM_REQUIRED => true
250 ]
251 ];
252 }
253
254 public function needsToken() {
255 return 'csrf';
256 }
257
258 public function mustBePosted() {
259 return true;
260 }
261
262 public function isWriteMode() {
263 return true;
264 }
265
266 public function isInternal() {
267 return true;
268 }
269}
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:56
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1454
getErrorFormatter()
Definition ApiBase.php:640
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:903
getResult()
Get the result object.
Definition ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:765
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:498
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition ApiBase.php:1036
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1467
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:52
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 "internal" Internal API modules are not (yet) intended for 3rd party...
needsToken()
Returns the token type this module requires in order to execute.
isWriteMode()
Indicates whether this module requires write mode.
__construct(ApiMain $main, $action, IContentHandlerFactory $contentHandlerFactory, PageEditStash $pageEditStash, RevisionLookup $revisionLookup, IBufferingStatsdDataFactory $statsdDataFactory, WikiPageFactory $wikiPageFactory)
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 formatting and validating API parameters.
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Service for looking up page revisions.
$content
Definition router.php:76
return true
Definition router.php:92