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  if ( $params['stashedtexthash'] !== null ) {
110  // Load from cache since the client indicates the text is the same as last stash
111  $textHash = $params['stashedtexthash'];
112  if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
113  $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
114  }
115  $text = $this->pageEditStash->fetchInputText( $textHash );
116  if ( !is_string( $text ) ) {
117  $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
118  }
119  } else {
120  // 'text' was passed. Trim and fix newlines so the key SHA1's
121  // match (see WebRequest::getText())
122  $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
123  $textHash = sha1( $text );
124  }
125 
126  $textContent = $this->contentHandlerFactory
127  ->getContentHandler( $params['contentmodel'] )
128  ->unserializeContent( $text, $params['contentformat'] );
129 
130  $page = $this->wikiPageFactory->newFromTitle( $title );
131  if ( $page->exists() ) {
132  // Page exists: get the merged content with the proposed change
133  $baseRev = $this->revisionLookup->getRevisionByPageId(
134  $page->getId(),
135  $params['baserevid']
136  );
137  if ( !$baseRev ) {
138  $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] );
139  }
140  $currentRev = $page->getRevisionRecord();
141  if ( !$currentRev ) {
142  $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' );
143  }
144  // Merge in the new version of the section to get the proposed version
145  $editContent = $page->replaceSectionAtRev(
146  $params['section'],
147  $textContent,
148  $params['sectiontitle'],
149  $baseRev->getId()
150  );
151  if ( !$editContent ) {
152  $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' );
153  }
154  if ( $currentRev->getId() == $baseRev->getId() ) {
155  // Base revision was still the latest; nothing to merge
156  $content = $editContent;
157  } else {
158  // Merge the edit into the current version
159  $baseContent = $baseRev->getContent( SlotRecord::MAIN );
160  $currentContent = $currentRev->getContent( SlotRecord::MAIN );
161  if ( !$baseContent || !$currentContent ) {
162  $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' );
163  }
164 
165  $baseModel = $baseContent->getModel();
166  $currentModel = $currentContent->getModel();
167 
168  // T255700: Put this in try-block because if the models of these three Contents
169  // happen to not be identical, the ContentHandler may throw exception here.
170  try {
171  $content = $this->contentHandlerFactory
172  ->getContentHandler( $baseModel )
173  ->merge3( $baseContent, $editContent, $currentContent );
174  } catch ( Exception $e ) {
175  $this->dieWithException( $e, [
176  'wrap' => ApiMessage::create(
177  [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ]
178  )
179  ] );
180  }
181 
182  }
183  } else {
184  // New pages: use the user-provided content model
185  $content = $textContent;
186  }
187 
188  if ( !$content ) { // merge3() failed
189  $this->getResult()->addValue( null,
190  $this->getModuleName(), [ 'status' => 'editconflict' ] );
191  return;
192  }
193 
194  if ( $user->pingLimiter( 'stashedit' ) ) {
195  $status = 'ratelimited';
196  } else {
197  $updater = $page->newPageUpdater( $user );
198  $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] );
199  $this->pageEditStash->stashInputText( $text, $textHash );
200  }
201 
202  $this->statsdDataFactory->increment( "editstash.cache_stores.$status" );
203 
204  $ret = [ 'status' => $status ];
205  // If we were rate-limited, we still return the pre-existing valid hash if one was passed
206  if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) {
207  $ret['texthash'] = $textHash;
208  }
209 
210  $this->getResult()->addValue( null, $this->getModuleName(), $ret );
211  }
212 
213  public function getAllowedParams() {
214  return [
215  'title' => [
216  ParamValidator::PARAM_TYPE => 'string',
217  ParamValidator::PARAM_REQUIRED => true
218  ],
219  'section' => [
220  ParamValidator::PARAM_TYPE => 'string',
221  ],
222  'sectiontitle' => [
223  ParamValidator::PARAM_TYPE => 'string'
224  ],
225  'text' => [
226  ParamValidator::PARAM_TYPE => 'text',
227  ParamValidator::PARAM_DEFAULT => null
228  ],
229  'stashedtexthash' => [
230  ParamValidator::PARAM_TYPE => 'string',
231  ParamValidator::PARAM_DEFAULT => null
232  ],
233  'summary' => [
234  ParamValidator::PARAM_TYPE => 'string',
235  ParamValidator::PARAM_DEFAULT => ''
236  ],
237  'contentmodel' => [
238  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
239  ParamValidator::PARAM_REQUIRED => true
240  ],
241  'contentformat' => [
242  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
243  ParamValidator::PARAM_REQUIRED => true
244  ],
245  'baserevid' => [
246  ParamValidator::PARAM_TYPE => 'integer',
247  ParamValidator::PARAM_REQUIRED => true
248  ]
249  ];
250  }
251 
252  public function needsToken() {
253  return 'csrf';
254  }
255 
256  public function mustBePosted() {
257  return true;
258  }
259 
260  public function isWriteMode() {
261  return true;
262  }
263 
264  public function isInternal() {
265  return true;
266  }
267 }
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:57
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1455
getErrorFormatter()
Definition: ApiBase.php:641
requireOnlyOneParameter( $params,... $required)
Die if none or more than one of a certain set of parameters is set and not false.
Definition: ApiBase.php:904
getResult()
Get the result object.
Definition: ApiBase.php:630
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:766
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:499
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition: ApiBase.php:1037
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1468
This is the main API class, used for both external and internal processing.
Definition: ApiMain.php:55
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