MediaWiki REL1_37
ApiStashEdit.php
Go to the documentation of this file.
1<?php
27
41class ApiStashEdit extends ApiBase {
42
45
48
51
54
57
67 public function __construct(
68 ApiMain $main,
69 $action,
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() ) { // sanity
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 = ContentHandler::makeContent(
129 $text, $title, $params['contentmodel'], $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->pingLimiter( 'stashedit' ) ) {
196 $status = 'ratelimited';
197 } else {
198 $status = $this->pageEditStash->parseAndCache( $page, $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
222 public function parseAndStash( WikiPage $page, Content $content, UserIdentity $user, $summary ) {
223 return $this->pageEditStash->parseAndCache( $page, $content, $user, $summary ?? '' );
224 }
225
226 public function getAllowedParams() {
227 return [
228 'title' => [
229 ApiBase::PARAM_TYPE => 'string',
231 ],
232 'section' => [
233 ApiBase::PARAM_TYPE => 'string',
234 ],
235 'sectiontitle' => [
236 ApiBase::PARAM_TYPE => 'string'
237 ],
238 'text' => [
239 ApiBase::PARAM_TYPE => 'text',
240 ApiBase::PARAM_DFLT => null
241 ],
242 'stashedtexthash' => [
243 ApiBase::PARAM_TYPE => 'string',
244 ApiBase::PARAM_DFLT => null
245 ],
246 'summary' => [
247 ApiBase::PARAM_TYPE => 'string',
249 ],
250 'contentmodel' => [
251 ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
253 ],
254 'contentformat' => [
255 ApiBase::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
257 ],
258 'baserevid' => [
259 ApiBase::PARAM_TYPE => 'integer',
261 ]
262 ];
263 }
264
265 public function needsToken() {
266 return 'csrf';
267 }
268
269 public function mustBePosted() {
270 return true;
271 }
272
273 public function isWriteMode() {
274 return true;
275 }
276
277 public function isInternal() {
278 return true;
279 }
280}
This abstract class implements many basic API functions, and is the base of all API classes.
Definition ApiBase.php:55
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition ApiBase.php:1436
const PARAM_REQUIRED
Definition ApiBase.php:105
const PARAM_TYPE
Definition ApiBase.php:81
getErrorFormatter()
Definition ApiBase.php:639
const PARAM_DFLT
Definition ApiBase.php:73
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition ApiBase.php:901
getResult()
Get the result object.
Definition ApiBase.php:628
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition ApiBase.php:764
getModuleName()
Get the name of the module being executed by this instance.
Definition ApiBase.php:497
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition ApiBase.php:1033
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition ApiBase.php:1448
This is the main API class, used for both external and internal processing.
Definition ApiMain.php:49
Prepare an edit in shared cache so that it can be reused on edit.
PageEditStash $pageEditStash
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
WikiPageFactory $wikiPageFactory
RevisionLookup $revisionLookup
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
IContentHandlerFactory $contentHandlerFactory
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.
IBufferingStatsdDataFactory $statsdDataFactory
parseAndStash(WikiPage $page, Content $content, UserIdentity $user, $summary)
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.
Value object representing a content slot associated with a page revision.
Class for managing stashed edits used by the page updater classes.
Class representing a MediaWiki article and history.
Definition WikiPage.php:60
Base interface for content objects.
Definition Content.php:35
MediaWiki adaptation of StatsdDataFactory that provides buffering functionality.
Service for looking up page revisions.
Interface for objects representing user identity.
$content
Definition router.php:76
return true
Definition router.php:92