MediaWiki  master
ApiStashEdit.php
Go to the documentation of this file.
1 <?php
27 
41 class ApiStashEdit extends ApiBase {
42 
44  private $contentHandlerFactory;
45 
47  private $pageEditStash;
48 
50  private $revisionLookup;
51 
53  private $statsdDataFactory;
54 
56  private $wikiPageFactory;
57 
67  public function __construct(
68  ApiMain $main,
69  $action,
70  IContentHandlerFactory $contentHandlerFactory,
71  PageEditStash $pageEditStash,
72  RevisionLookup $revisionLookup,
73  IBufferingStatsdDataFactory $statsdDataFactory,
74  WikiPageFactory $wikiPageFactory
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() ) {
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 = $this->contentHandlerFactory
129  ->getContentHandler( $params['contentmodel'] )
130  ->unserializeContent( $text, $params['contentformat'] );
131 
132  $page = $this->wikiPageFactory->newFromTitle( $title );
133  if ( $page->exists() ) {
134  // Page exists: get the merged content with the proposed change
135  $baseRev = $this->revisionLookup->getRevisionByPageId(
136  $page->getId(),
137  $params['baserevid']
138  );
139  if ( !$baseRev ) {
140  $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
141  }
142  $currentRev = $page->getRevisionRecord();
143  if ( !$currentRev ) {
144  $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
145  }
146  // Merge in the new version of the section to get the proposed version
147  $editContent = $page->replaceSectionAtRev(
148  $params['section'],
149  $textContent,
150  $params['sectiontitle'],
151  $baseRev->getId()
152  );
153  if ( !$editContent ) {
154  $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
155  }
156  if ( $currentRev->getId() == $baseRev->getId() ) {
157  // Base revision was still the latest; nothing to merge
158  $content = $editContent;
159  } else {
160  // Merge the edit into the current version
161  $baseContent = $baseRev->getContent( SlotRecord::MAIN );
162  $currentContent = $currentRev->getContent( SlotRecord::MAIN );
163  if ( !$baseContent || !$currentContent ) {
164  $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
165  }
166 
167  $baseModel = $baseContent->getModel();
168  $currentModel = $currentContent->getModel();
169 
170  // T255700: Put this in try-block because if the models of these three Contents
171  // happen to not be identical, the ContentHandler may throw exception here.
172  try {
173  $content = $this->contentHandlerFactory
174  ->getContentHandler( $baseModel )
175  ->merge3( $baseContent, $editContent, $currentContent );
176  } catch ( Exception $e ) {
177  $this->dieWithException( $e, [
178  'wrap' => ApiMessage::create(
179  [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
180  )
181  ] );
182  }
183 
184  }
185  } else {
186  // New pages: use the user-provided content model
187  $content = $textContent;
188  }
189 
190  if ( !$content ) { // merge3() failed
191  $this->getResult()->addValue( null,
192  $this->getModuleName(), [ 'status' => 'editconflict' ] );
193  return;
194  }
195 
196  if ( $user->pingLimiter( 'stashedit' ) ) {
197  $status = 'ratelimited';
198  } else {
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  public function getAllowedParams() {
216  return [
217  'title' => [
218  ParamValidator::PARAM_TYPE => 'string',
219  ParamValidator::PARAM_REQUIRED => true
220  ],
221  'section' => [
222  ParamValidator::PARAM_TYPE => 'string',
223  ],
224  'sectiontitle' => [
225  ParamValidator::PARAM_TYPE => 'string'
226  ],
227  'text' => [
228  ParamValidator::PARAM_TYPE => 'text',
229  ParamValidator::PARAM_DEFAULT => null
230  ],
231  'stashedtexthash' => [
232  ParamValidator::PARAM_TYPE => 'string',
233  ParamValidator::PARAM_DEFAULT => null
234  ],
235  'summary' => [
236  ParamValidator::PARAM_TYPE => 'string',
237  ParamValidator::PARAM_DEFAULT => ''
238  ],
239  'contentmodel' => [
240  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
241  ParamValidator::PARAM_REQUIRED => true
242  ],
243  'contentformat' => [
244  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
245  ParamValidator::PARAM_REQUIRED => true
246  ],
247  'baserevid' => [
248  ParamValidator::PARAM_TYPE => 'integer',
249  ParamValidator::PARAM_REQUIRED => true
250  ]
251  ];
252  }
253 
254  public function needsToken() {
255  return 'csrf';
256  }
257 
258  public function mustBePosted() {
259  return true;
260  }
261 
262  public function isWriteMode() {
263  return true;
264  }
265 
266  public function isInternal() {
267  return true;
268  }
269 }
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:56
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1458
getErrorFormatter()
Definition: ApiBase.php:640
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:903
getResult()
Get the result object.
Definition: ApiBase.php:629
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:765
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:498
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition: ApiBase.php:1036
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1471
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:52
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:43
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 "internal" Internal API modules are not (yet) intended for 3rd party...
needsToken()
Returns the token type this module requires in order to execute.
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.
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 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