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