MediaWiki  master
ApiStashEdit.php
Go to the documentation of this file.
1 <?php
29 
43 class ApiStashEdit extends ApiBase {
44 
45  private IContentHandlerFactory $contentHandlerFactory;
46  private PageEditStash $pageEditStash;
47  private RevisionLookup $revisionLookup;
48  private IBufferingStatsdDataFactory $statsdDataFactory;
49  private WikiPageFactory $wikiPageFactory;
50  private TempUserCreator $tempUserCreator;
51  private UserFactory $userFactory;
52 
64  public function __construct(
65  ApiMain $main,
66  $action,
67  IContentHandlerFactory $contentHandlerFactory,
68  PageEditStash $pageEditStash,
69  RevisionLookup $revisionLookup,
70  IBufferingStatsdDataFactory $statsdDataFactory,
71  WikiPageFactory $wikiPageFactory,
72  TempUserCreator $tempUserCreator,
73  UserFactory $userFactory
74  ) {
75  parent::__construct( $main, $action );
76 
77  $this->contentHandlerFactory = $contentHandlerFactory;
78  $this->pageEditStash = $pageEditStash;
79  $this->revisionLookup = $revisionLookup;
80  $this->statsdDataFactory = $statsdDataFactory;
81  $this->wikiPageFactory = $wikiPageFactory;
82  $this->tempUserCreator = $tempUserCreator;
83  $this->userFactory = $userFactory;
84  }
85 
86  public function execute() {
87  $user = $this->getUser();
88  $params = $this->extractRequestParams();
89 
90  if ( $user->isBot() ) {
91  $this->dieWithError( 'apierror-botsnotsupported' );
92  }
93 
94  $page = $this->getTitleOrPageId( $params );
95  $title = $page->getTitle();
96  $this->getErrorFormatter()->setContextTitle( $title );
97 
98  if ( !$this->contentHandlerFactory
99  ->getContentHandler( $params['contentmodel'] )
100  ->isSupportedFormat( $params['contentformat'] )
101  ) {
102  $this->dieWithError(
103  [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ],
104  'badmodelformat'
105  );
106  }
107 
108  $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' );
109 
110  if ( $params['stashedtexthash'] !== null ) {
111  // Load from cache since the client indicates the text is the same as last stash
112  $textHash = $params['stashedtexthash'];
113  if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
114  $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
115  }
116  $text = $this->pageEditStash->fetchInputText( $textHash );
117  if ( !is_string( $text ) ) {
118  $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
119  }
120  } else {
121  // 'text' was passed. Trim and fix newlines so the key SHA1's
122  // match (see WebRequest::getText())
123  $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
124  $textHash = sha1( $text );
125  }
126 
127  $textContent = $this->contentHandlerFactory
128  ->getContentHandler( $params['contentmodel'] )
129  ->unserializeContent( $text, $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->authorizeWrite( 'stashedit', $title ) ) {
196  $status = 'ratelimited';
197  } else {
198  $user = $this->getUserForPreview();
199  $updater = $page->newPageUpdater( $user );
200  $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] );
201  $this->pageEditStash->stashInputText( $text, $textHash );
202  }
203 
204  $this->statsdDataFactory->increment( "editstash.cache_stores.$status" );
205 
206  $ret = [ 'status' => $status ];
207  // If we were rate-limited, we still return the pre-existing valid hash if one was passed
208  if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
209  $ret['texthash'] = $textHash;
210  }
211 
212  $this->getResult()->addValue( null, $this->getModuleName(), $ret );
213  }
214 
215  private function getUserForPreview() {
216  $user = $this->getUser();
217  if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) {
218  return $this->userFactory->newUnsavedTempUser(
219  $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() )
220  );
221  }
222  return $user;
223  }
224 
225  public function getAllowedParams() {
226  return [
227  'title' => [
228  ParamValidator::PARAM_TYPE => 'string',
229  ParamValidator::PARAM_REQUIRED => true
230  ],
231  'section' => [
232  ParamValidator::PARAM_TYPE => 'string',
233  ],
234  'sectiontitle' => [
235  ParamValidator::PARAM_TYPE => 'string'
236  ],
237  'text' => [
238  ParamValidator::PARAM_TYPE => 'text',
239  ParamValidator::PARAM_DEFAULT => null
240  ],
241  'stashedtexthash' => [
242  ParamValidator::PARAM_TYPE => 'string',
243  ParamValidator::PARAM_DEFAULT => null
244  ],
245  'summary' => [
246  ParamValidator::PARAM_TYPE => 'string',
247  ParamValidator::PARAM_DEFAULT => ''
248  ],
249  'contentmodel' => [
250  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
251  ParamValidator::PARAM_REQUIRED => true
252  ],
253  'contentformat' => [
254  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
255  ParamValidator::PARAM_REQUIRED => true
256  ],
257  'baserevid' => [
258  ParamValidator::PARAM_TYPE => 'integer',
259  ParamValidator::PARAM_REQUIRED => true
260  ]
261  ];
262  }
263 
264  public function needsToken() {
265  return 'csrf';
266  }
267 
268  public function mustBePosted() {
269  return true;
270  }
271 
272  public function isWriteMode() {
273  return true;
274  }
275 
276  public function isInternal() {
277  return true;
278  }
279 }
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:62
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1515
getErrorFormatter()
Definition: ApiBase.php:678
requireOnlyOneParameter( $params,... $required)
Die if 0 or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:946
getResult()
Get the result object.
Definition: ApiBase.php:667
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:807
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:528
getTitleOrPageId( $params, $load=false)
Attempts to load a WikiPage object from a title or pageid parameter, if possible.
Definition: ApiBase.php:1080
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1528
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:64
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:45
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 considered to be "internal".
__construct(ApiMain $main, $action, IContentHandlerFactory $contentHandlerFactory, PageEditStash $pageEditStash, RevisionLookup $revisionLookup, IBufferingStatsdDataFactory $statsdDataFactory, WikiPageFactory $wikiPageFactory, TempUserCreator $tempUserCreator, UserFactory $userFactory)
needsToken()
Returns the token type this module requires in order to execute.
isWriteMode()
Indicates whether this module requires write mode.
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.
Definition: SlotRecord.php:40
Manage the pre-emptive page parsing for edits to wiki pages.
Service for temporary user creation.
Creates User objects.
Definition: UserFactory.php:41
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:90