MediaWiki 1.40.4
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 if ( $params['stashedtexthash'] !== null ) {
110 // Load from cache since the client indicates the text is the same as last stash
111 $textHash = $params['stashedtexthash'];
112 if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
113 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
114 }
115 $text = $this->pageEditStash->fetchInputText( $textHash );
116 if ( !is_string( $text ) ) {
117 $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
118 }
119 } else {
120 // 'text' was passed. Trim and fix newlines so the key SHA1's
121 // match (see WebRequest::getText())
122 $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
123 $textHash = sha1( $text );
124 }
125
126 $textContent = $this->contentHandlerFactory
127 ->getContentHandler( $params['contentmodel'] )
128 ->unserializeContent( $text, $params['contentformat'] );
129
130 $page = $this->wikiPageFactory->newFromTitle( $title );
131 if ( $page->exists() ) {
132 // Page exists: get the merged content with the proposed change
133 $baseRev = $this->revisionLookup->getRevisionByPageId(
134 $page->getId(),
135 $params['baserevid']
136 );
137 if ( !$baseRev ) {
138 $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
139 }
140 $currentRev = $page->getRevisionRecord();
141 if ( !$currentRev ) {
142 $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
143 }
144 // Merge in the new version of the section to get the proposed version
145 $editContent = $page->replaceSectionAtRev(
146 $params['section'],
147 $textContent,
148 $params['sectiontitle'],
149 $baseRev->getId()
150 );
151 if ( !$editContent ) {
152 $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
153 }
154 if ( $currentRev->getId() == $baseRev->getId() ) {
155 // Base revision was still the latest; nothing to merge
156 $content = $editContent;
157 } else {
158 // Merge the edit into the current version
159 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
160 $currentContent = $currentRev->getContent( SlotRecord::MAIN );
161 if ( !$baseContent || !$currentContent ) {
162 $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
163 }
164
165 $baseModel = $baseContent->getModel();
166 $currentModel = $currentContent->getModel();
167
168 // T255700: Put this in try-block because if the models of these three Contents
169 // happen to not be identical, the ContentHandler may throw exception here.
170 try {
171 $content = $this->contentHandlerFactory
172 ->getContentHandler( $baseModel )
173 ->merge3( $baseContent, $editContent, $currentContent );
174 } catch ( Exception $e ) {
175 $this->dieWithException( $e, [
176 'wrap' => ApiMessage::create(
177 [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
178 )
179 ] );
180 }
181
182 }
183 } else {
184 // New pages: use the user-provided content model
185 $content = $textContent;
186 }
187
188 if ( !$content ) { // merge3() failed
189 $this->getResult()->addValue( null,
190 $this->getModuleName(), [ 'status' => 'editconflict' ] );
191 return;
192 }
193
194 if ( $user->pingLimiter( 'stashedit' ) ) {
195 $status = 'ratelimited';
196 } else {
197 $updater = $page->newPageUpdater( $user );
198 $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] );
199 $this->pageEditStash->stashInputText( $text, $textHash );
200 }
201
202 $this->statsdDataFactory->increment( "editstash.cache_stores.$status" );
203
204 $ret = [ 'status' => $status ];
205 // If we were rate-limited, we still return the pre-existing valid hash if one was passed
206 if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
207 $ret['texthash'] = $textHash;
208 }
209
210 $this->getResult()->addValue( null, $this->getModuleName(), $ret );
211 }
212
213 public function getAllowedParams() {
214 return [
215 'title' => [
216 ParamValidator::PARAM_TYPE => 'string',
217 ParamValidator::PARAM_REQUIRED => true
218 ],
219 'section' => [
220 ParamValidator::PARAM_TYPE => 'string',
221 ],
222 'sectiontitle' => [
223 ParamValidator::PARAM_TYPE => 'string'
224 ],
225 'text' => [
226 ParamValidator::PARAM_TYPE => 'text',
227 ParamValidator::PARAM_DEFAULT => null
228 ],
229 'stashedtexthash' => [
230 ParamValidator::PARAM_TYPE => 'string',
231 ParamValidator::PARAM_DEFAULT => null
232 ],
233 'summary' => [
234 ParamValidator::PARAM_TYPE => 'string',
235 ParamValidator::PARAM_DEFAULT => ''
236 ],
237 'contentmodel' => [
238 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
239 ParamValidator::PARAM_REQUIRED => true
240 ],
241 'contentformat' => [
242 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
243 ParamValidator::PARAM_REQUIRED => true
244 ],
245 'baserevid' => [
246 ParamValidator::PARAM_TYPE => 'integer',
247 ParamValidator::PARAM_REQUIRED => true
248 ]
249 ];
250 }
251
252 public function needsToken() {
253 return 'csrf';
254 }
255
256 public function mustBePosted() {
257 return true;
258 }
259
260 public function isWriteMode() {
261 return true;
262 }
263
264 public function isInternal() {
265 return true;
266 }
267}
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:59
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1460
getErrorFormatter()
Definition ApiBase.php:648
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:911
getResult()
Get the result object.
Definition ApiBase.php:637
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:773
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:506
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition ApiBase.php:1044
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1473
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:58
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