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