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