MediaWiki  master
ApiEditPage.php
Go to the documentation of this file.
1 <?php
36 
53 class ApiEditPage extends ApiBase {
55 
57  private $contentHandlerFactory;
58 
60  private $revisionLookup;
61 
63  private $watchedItemStore;
64 
66  private $wikiPageFactory;
67 
69  private $userOptionsLookup;
70 
72  private $redirectLookup;
73 
77  private function persistGlobalSession() {
79  }
80 
92  public function __construct(
93  ApiMain $mainModule,
94  $moduleName,
95  IContentHandlerFactory $contentHandlerFactory = null,
96  RevisionLookup $revisionLookup = null,
97  WatchedItemStoreInterface $watchedItemStore = null,
98  WikiPageFactory $wikiPageFactory = null,
99  WatchlistManager $watchlistManager = null,
100  UserOptionsLookup $userOptionsLookup = null,
101  RedirectLookup $redirectLookup = null
102  ) {
103  parent::__construct( $mainModule, $moduleName );
104 
105  // This class is extended and therefor fallback to global state - T264213
106  $services = MediaWikiServices::getInstance();
107  $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
108  $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
109  $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
110  $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
111 
112  // Variables needed in ApiWatchlistTrait trait
113  $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
114  $this->watchlistMaxDuration =
115  $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
116  $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
117  $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
118  $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
119  }
120 
121  public function execute() {
122  $this->useTransactionalTimeLimit();
123 
124  $user = $this->getUser();
125  $params = $this->extractRequestParams();
126 
127  $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
128 
129  $pageObj = $this->getTitleOrPageId( $params );
130  $titleObj = $pageObj->getTitle();
131  $this->getErrorFormatter()->setContextTitle( $titleObj );
132  $apiResult = $this->getResult();
133 
134  if ( $params['redirect'] ) {
135  if ( $params['prependtext'] === null
136  && $params['appendtext'] === null
137  && $params['section'] !== 'new'
138  ) {
139  $this->dieWithError( 'apierror-redirect-appendonly' );
140  }
141  if ( $titleObj->isRedirect() ) {
142  $oldTarget = $titleObj;
143  $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
144  $redirTarget = Title::castFromLinkTarget( $redirTarget );
145 
146  $redirValues = [
147  'from' => $titleObj->getPrefixedText(),
148  'to' => $redirTarget->getPrefixedText()
149  ];
150 
151  // T239428: Check whether the new title is valid
152  if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
153  $redirValues['to'] = $redirTarget->getFullText();
154  $this->dieWithError(
155  [
156  'apierror-edit-invalidredirect',
157  Message::plaintextParam( $oldTarget->getPrefixedText() ),
158  Message::plaintextParam( $redirTarget->getFullText() ),
159  ],
160  'edit-invalidredirect',
161  [ 'redirects' => $redirValues ]
162  );
163  }
164 
165  ApiResult::setIndexedTagName( $redirValues, 'r' );
166  $apiResult->addValue( null, 'redirects', $redirValues );
167 
168  // Since the page changed, update $pageObj and $titleObj
169  $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
170  $titleObj = $pageObj->getTitle();
171 
172  $this->getErrorFormatter()->setContextTitle( $redirTarget );
173  }
174  }
175 
176  if ( $params['contentmodel'] ) {
177  $contentHandler = $this->contentHandlerFactory->getContentHandler( $params['contentmodel'] );
178  } else {
179  $contentHandler = $pageObj->getContentHandler();
180  }
181  $contentModel = $contentHandler->getModelID();
182 
183  $name = $titleObj->getPrefixedDBkey();
184 
185  if ( $params['undo'] > 0 ) {
186  // allow undo via api
187  } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
188  $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
189  }
190 
191  $contentFormat = $params['contentformat'] ?: $contentHandler->getDefaultFormat();
192 
193  if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
194  $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
195  }
196 
197  if ( $params['createonly'] && $titleObj->exists() ) {
198  $this->dieWithError( 'apierror-articleexists' );
199  }
200  if ( $params['nocreate'] && !$titleObj->exists() ) {
201  $this->dieWithError( 'apierror-missingtitle' );
202  }
203 
204  // Now let's check whether we're even allowed to do this
206  $titleObj,
207  'edit',
208  [ 'autoblock' => true ]
209  );
210 
211  $toMD5 = $params['text'];
212  if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
213  $content = $pageObj->getContent();
214 
215  if ( !$content ) {
216  if ( $titleObj->getNamespace() === NS_MEDIAWIKI ) {
217  # If this is a MediaWiki:x message, then load the messages
218  # and return the message value for x.
219  $text = $titleObj->getDefaultMessageText();
220  if ( $text === false ) {
221  $text = '';
222  }
223 
224  try {
225  $content = ContentHandler::makeContent( $text, $titleObj );
226  } catch ( MWContentSerializationException $ex ) {
227  $this->dieWithException( $ex, [
228  'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
229  ] );
230  }
231  } else {
232  # Otherwise, make a new empty content.
233  $content = $contentHandler->makeEmptyContent();
234  }
235  }
236 
237  // @todo Add support for appending/prepending to the Content interface
238 
239  if ( !( $content instanceof TextContent ) ) {
240  $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
241  }
242 
243  if ( $params['section'] !== null ) {
244  if ( !$contentHandler->supportsSections() ) {
245  $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
246  }
247 
248  if ( $params['section'] == 'new' ) {
249  // DWIM if they're trying to prepend/append to a new section.
250  $content = null;
251  } else {
252  // Process the content for section edits
253  $section = $params['section'];
254  $content = $content->getSection( $section );
255 
256  if ( !$content ) {
257  $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
258  }
259  }
260  }
261 
262  if ( !$content ) {
263  $text = '';
264  } else {
265  $text = $content->serialize( $contentFormat );
266  }
267 
268  $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
269  $toMD5 = $params['prependtext'] . $params['appendtext'];
270  }
271 
272  if ( $params['undo'] > 0 ) {
273  $undoRev = $this->revisionLookup->getRevisionById( $params['undo'] );
274  if ( $undoRev === null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
275  $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
276  }
277 
278  if ( $params['undoafter'] > 0 ) {
279  $undoafterRev = $this->revisionLookup->getRevisionById( $params['undoafter'] );
280  } else {
281  // undoafter=0 or null
282  $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
283  }
284  if ( $undoafterRev === null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
285  $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
286  }
287 
288  if ( $undoRev->getPageId() != $pageObj->getId() ) {
289  $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
290  $titleObj->getPrefixedText() ] );
291  }
292  if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
293  $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
294  $titleObj->getPrefixedText() ] );
295  }
296 
297  $newContent = $contentHandler->getUndoContent(
298  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
299  $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
300  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
301  $undoRev->getContent( SlotRecord::MAIN ),
302  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
303  $undoafterRev->getContent( SlotRecord::MAIN ),
304  $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
305  );
306 
307  if ( !$newContent ) {
308  $this->dieWithError( 'undo-failure', 'undofailure' );
309  }
310  if ( !$params['contentmodel'] && !$params['contentformat'] ) {
311  // If we are reverting content model, the new content model
312  // might not support the current serialization format, in
313  // which case go back to the old serialization format,
314  // but only if the user hasn't specified a format/model
315  // parameter.
316  if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
317  $undoafterRevMainSlot = $undoafterRev->getSlot(
318  SlotRecord::MAIN,
319  RevisionRecord::RAW
320  );
321  $contentFormat = $undoafterRevMainSlot->getFormat();
322  if ( !$contentFormat ) {
323  // fall back to default content format for the model
324  // of $undoafterRev
325  $contentFormat = $this->contentHandlerFactory
326  ->getContentHandler( $undoafterRevMainSlot->getModel() )
327  ->getDefaultFormat();
328  }
329  }
330  // Override content model with model of undid revision.
331  $contentModel = $newContent->getModel();
332  $undoContentModel = true;
333  }
334  $params['text'] = $newContent->serialize( $contentFormat );
335  // If no summary was given and we only undid one rev,
336  // use an autosummary
337 
338  if ( $params['summary'] === null ) {
339  $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
340  if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
341  $undoRevUser = $undoRev->getUser();
342  $params['summary'] = $this->msg( 'undo-summary' )
343  ->params( $params['undo'], $undoRevUser ? $undoRevUser->getName() : '' )
344  ->inContentLanguage()->text();
345  }
346  }
347  }
348 
349  // See if the MD5 hash checks out
350  if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
351  $this->dieWithError( 'apierror-badmd5' );
352  }
353 
354  // EditPage wants to parse its stuff from a WebRequest
355  // That interface kind of sucks, but it's workable
356  $requestArray = [
357  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
358  'wpTextbox1' => $params['text'],
359  'format' => $contentFormat,
360  'model' => $contentModel,
361  'wpEditToken' => $params['token'],
362  'wpIgnoreBlankSummary' => true,
363  'wpIgnoreBlankArticle' => true,
364  'wpIgnoreSelfRedirect' => true,
365  'bot' => $params['bot'],
366  'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
367  ];
368 
369  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
370  if ( $params['summary'] !== null ) {
371  $requestArray['wpSummary'] = $params['summary'];
372  }
373 
374  if ( $params['sectiontitle'] !== null ) {
375  $requestArray['wpSectionTitle'] = $params['sectiontitle'];
376  }
377 
378  if ( $params['undo'] > 0 ) {
379  $requestArray['wpUndidRevision'] = $params['undo'];
380  }
381  if ( $params['undoafter'] > 0 ) {
382  $requestArray['wpUndoAfter'] = $params['undoafter'];
383  }
384 
385  // Skip for baserevid == null or '' or '0' or 0
386  if ( !empty( $params['baserevid'] ) ) {
387  $requestArray['editRevId'] = $params['baserevid'];
388  }
389 
390  // Watch out for basetimestamp == '' or '0'
391  // It gets treated as NOW, almost certainly causing an edit conflict
392  if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
393  $requestArray['wpEdittime'] = $params['basetimestamp'];
394  } elseif ( empty( $params['baserevid'] ) ) {
395  // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
396  // due to the way userWasLastToEdit() works.
397  $requestArray['wpEdittime'] = $pageObj->getTimestamp();
398  }
399 
400  if ( $params['starttimestamp'] !== null ) {
401  $requestArray['wpStarttime'] = $params['starttimestamp'];
402  } else {
403  $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
404  }
405 
406  if ( $params['minor'] || ( !$params['notminor'] &&
407  $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
408  ) {
409  $requestArray['wpMinoredit'] = '';
410  }
411 
412  if ( $params['recreate'] ) {
413  $requestArray['wpRecreate'] = '';
414  }
415 
416  if ( $params['section'] !== null ) {
417  $section = $params['section'];
418  if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
419  $this->dieWithError( 'apierror-invalidsection' );
420  }
421  $content = $pageObj->getContent();
422  if ( $section !== '0'
423  && $section != 'new'
424  && ( !$content || !$content->getSection( $section ) )
425  ) {
426  $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
427  }
428  $requestArray['wpSection'] = $params['section'];
429  } else {
430  $requestArray['wpSection'] = '';
431  }
432 
433  $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
434 
435  // Deprecated parameters
436  if ( $params['watch'] ) {
437  $watch = true;
438  } elseif ( $params['unwatch'] ) {
439  $watch = false;
440  }
441 
442  if ( $watch ) {
443  $requestArray['wpWatchthis'] = true;
444  $watchlistExpiry = $this->getExpiryFromParams( $params );
445 
446  if ( $watchlistExpiry ) {
447  $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
448  }
449  }
450 
451  // Apply change tags
452  if ( $params['tags'] ) {
453  $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
454  if ( $tagStatus->isOK() ) {
455  $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
456  } else {
457  $this->dieStatus( $tagStatus );
458  }
459  }
460 
461  // Pass through anything else we might have been given, to support extensions
462  // This is kind of a hack but it's the best we can do to make extensions work
463  $requestArray += $this->getRequest()->getValues();
464 
465  global $wgTitle, $wgRequest;
466 
467  $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
468 
469  // Some functions depend on $wgTitle == $ep->mTitle
470  // TODO: Make them not or check if they still do
471  $wgTitle = $titleObj;
472 
473  $articleContext = new RequestContext;
474  $articleContext->setRequest( $req );
475  $articleContext->setWikiPage( $pageObj );
476  $articleContext->setUser( $this->getUser() );
477 
479  $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
480 
481  $ep = new EditPage( $articleObject );
482 
483  $ep->setApiEditOverride( true );
484  $ep->setContextTitle( $titleObj );
485  $ep->importFormData( $req );
486  $ep->maybeActivateTempUserCreate( true );
487 
488  // T255700: Ensure content models of the base content
489  // and fetched revision remain the same before attempting to save.
490  $editRevId = $requestArray['editRevId'] ?? false;
491  $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
492  $baseContentModel = null;
493 
494  if ( $baseRev ) {
495  $baseContent = $baseRev->getContent( SlotRecord::MAIN );
496  $baseContentModel = $baseContent ? $baseContent->getModel() : null;
497  }
498 
499  if ( $baseContentModel === null ) {
500  $baseContentModel = $pageObj->getContentModel();
501  }
502 
503  // However, allow the content models to possibly differ if we are intentionally
504  // changing them or we are doing an undo edit that is reverting content model change.
505  $contentModelsCanDiffer = $params['contentmodel'] || isset( $undoContentModel );
506 
507  if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
508  $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
509  }
510 
511  // Do the actual save
512  $oldRevId = $articleObject->getRevIdFetched();
513  $result = null;
514 
515  // Fake $wgRequest for some hooks inside EditPage
516  // @todo FIXME: This interface SUCKS
517  $oldRequest = $wgRequest;
518  $wgRequest = $req;
519 
520  $status = $ep->attemptSave( $result );
521  $statusValue = is_int( $status->value ) ? $status->value : 0;
522  $wgRequest = $oldRequest;
523 
524  $r = [];
525  switch ( $statusValue ) {
528  if ( isset( $status->apiHookResult ) ) {
529  $r = $status->apiHookResult;
530  $r['result'] = 'Failure';
531  $apiResult->addValue( null, $this->getModuleName(), $r );
532  return;
533  }
534  if ( !$status->getErrors() ) {
535  // This appears to be unreachable right now, because all
536  // code paths will set an error. Could change, though.
537  $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
538  }
539  $this->dieStatus( $status );
540 
541  // These two cases will normally have been caught earlier, and will
542  // only occur if something blocks the user between the earlier
543  // check and the check in EditPage (presumably a hook). It's not
544  // obvious that this is even possible.
545  // @codeCoverageIgnoreStart
547  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
548  $this->dieBlocked( $user->getBlock() );
549  // dieBlocked prevents continuation
550 
552  $this->dieReadOnly();
553  // @codeCoverageIgnoreEnd
554 
556  $r['new'] = true;
557  // fall-through
558 
560  $r['result'] = 'Success';
561  $r['pageid'] = (int)$titleObj->getArticleID();
562  $r['title'] = $titleObj->getPrefixedText();
563  $r['contentmodel'] = $articleObject->getPage()->getContentModel();
564  $newRevId = $articleObject->getPage()->getLatest();
565  if ( $newRevId == $oldRevId ) {
566  $r['nochange'] = true;
567  } else {
568  $r['oldrevid'] = (int)$oldRevId;
569  $r['newrevid'] = (int)$newRevId;
570  $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
571  $pageObj->getTimestamp() );
572  }
573 
574  if ( $watch ) {
575  $r['watched'] = true;
576 
577  $watchlistExpiry = $this->getWatchlistExpiry(
578  $this->watchedItemStore,
579  $titleObj,
580  $user
581  );
582 
583  if ( $watchlistExpiry ) {
584  $r['watchlistexpiry'] = $watchlistExpiry;
585  }
586  }
587  $this->persistGlobalSession();
588  break;
589 
590  default:
591  if ( !$status->getErrors() ) {
592  // EditPage sometimes only sets the status code without setting
593  // any actual error messages. Supply defaults for those cases.
594  switch ( $statusValue ) {
595  // Currently needed
597  $status->fatal( 'apierror-noimageredirect-anon' );
598  break;
600  $status->fatal( 'apierror-noimageredirect' );
601  break;
604  $status->fatal( 'apierror-contenttoobig',
605  $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
606  break;
608  $status->fatal( 'apierror-noedit-anon' );
609  break;
611  $status->fatal( 'apierror-cantchangecontentmodel' );
612  break;
614  $status->fatal( 'apierror-pagedeleted' );
615  break;
617  $status->fatal( 'edit-conflict' );
618  break;
619 
620  // Currently shouldn't be needed, but here in case
621  // hooks use them without setting appropriate
622  // errors on the status.
623  // @codeCoverageIgnoreStart
625  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
626  $status->fatal( 'apierror-spamdetected', $result['spam'] );
627  break;
629  $status->fatal( 'apierror-noedit' );
630  break;
632  $status->fatal( 'apierror-ratelimited' );
633  break;
635  $status->fatal( 'nocreate-loggedin' );
636  break;
638  $status->fatal( 'apierror-emptypage' );
639  break;
641  $status->fatal( 'apierror-emptynewsection' );
642  break;
644  $status->fatal( 'apierror-summaryrequired' );
645  break;
646  default:
647  wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
648  $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
649  break;
650  // @codeCoverageIgnoreEnd
651  }
652  }
653  $this->dieStatus( $status );
654  }
655  $apiResult->addValue( null, $this->getModuleName(), $r );
656  }
657 
658  public function mustBePosted() {
659  return true;
660  }
661 
662  public function isWriteMode() {
663  return true;
664  }
665 
666  public function getAllowedParams() {
667  $params = [
668  'title' => [
669  ParamValidator::PARAM_TYPE => 'string',
670  ],
671  'pageid' => [
672  ParamValidator::PARAM_TYPE => 'integer',
673  ],
674  'section' => null,
675  'sectiontitle' => [
676  ParamValidator::PARAM_TYPE => 'string',
677  ],
678  'text' => [
679  ParamValidator::PARAM_TYPE => 'text',
680  ],
681  'summary' => null,
682  'tags' => [
683  ParamValidator::PARAM_TYPE => 'tags',
684  ParamValidator::PARAM_ISMULTI => true,
685  ],
686  'minor' => false,
687  'notminor' => false,
688  'bot' => false,
689  'baserevid' => [
690  ParamValidator::PARAM_TYPE => 'integer',
691  ],
692  'basetimestamp' => [
693  ParamValidator::PARAM_TYPE => 'timestamp',
694  ],
695  'starttimestamp' => [
696  ParamValidator::PARAM_TYPE => 'timestamp',
697  ],
698  'recreate' => false,
699  'createonly' => false,
700  'nocreate' => false,
701  'watch' => [
702  ParamValidator::PARAM_DEFAULT => false,
703  ParamValidator::PARAM_DEPRECATED => true,
704  ],
705  'unwatch' => [
706  ParamValidator::PARAM_DEFAULT => false,
707  ParamValidator::PARAM_DEPRECATED => true,
708  ],
709  ];
710 
711  // Params appear in the docs in the order they are defined,
712  // which is why this is here and not at the bottom.
713  $params += $this->getWatchlistParams();
714 
715  return $params + [
716  'md5' => null,
717  'prependtext' => [
718  ParamValidator::PARAM_TYPE => 'text',
719  ],
720  'appendtext' => [
721  ParamValidator::PARAM_TYPE => 'text',
722  ],
723  'undo' => [
724  ParamValidator::PARAM_TYPE => 'integer',
725  IntegerDef::PARAM_MIN => 0,
727  ],
728  'undoafter' => [
729  ParamValidator::PARAM_TYPE => 'integer',
730  IntegerDef::PARAM_MIN => 0,
732  ],
733  'redirect' => [
734  ParamValidator::PARAM_TYPE => 'boolean',
735  ParamValidator::PARAM_DEFAULT => false,
736  ],
737  'contentformat' => [
738  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
739  ],
740  'contentmodel' => [
741  ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
742  ],
743  'token' => [
744  // Standard definition automatically inserted
745  ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
746  ],
747  ];
748  }
749 
750  public function needsToken() {
751  return 'csrf';
752  }
753 
754  protected function getExamplesMessages() {
755  return [
756  'action=edit&title=Test&summary=test%20summary&' .
757  'text=article%20content&baserevid=1234567&token=123ABC'
758  => 'apihelp-edit-example-edit',
759  'action=edit&title=Test&summary=NOTOC&minor=&' .
760  'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
761  => 'apihelp-edit-example-prepend',
762  'action=edit&title=Test&undo=13585&undoafter=13579&' .
763  'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
764  => 'apihelp-edit-example-undo',
765  ];
766  }
767 
768  public function getHelpUrls() {
769  return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
770  }
771 }
getWatchlistValue(string $watchlist, Title $title, User $user, ?string $userOption=null)
Return true if we're to watch the page, false if not.
getExpiryFromParams(array $params)
Get formatted expiry from the given parameters, or null if no expiry was provided.
getWatchlistExpiry(WatchedItemStoreInterface $store, Title $title, UserIdentity $user)
Get existing expiry from the database.
getWatchlistParams(array $watchOptions=[])
Get additional allow params specific to watchlisting.
const NS_MEDIAWIKI
Definition: Defines.php:72
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
global $wgRequest
Definition: Setup.php:373
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:493
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
getMain()
Get the main module.
Definition: ApiBase.php:515
getErrorFormatter()
Definition: ApiBase.php:641
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition: ApiBase.php:171
dieReadOnly()
Helper function for readonly errors.
Definition: ApiBase.php:1545
requireAtLeastOneParameter( $params,... $required)
Die if none of a certain set of parameters is set and not false.
Definition: ApiBase.php:964
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
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:154
checkTitleUserPermissions( $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition: ApiBase.php:1587
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
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1516
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition: ApiBase.php:1300
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1484
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1468
A module that allows for editing and creating pages.
Definition: ApiEditPage.php:53
__construct(ApiMain $mainModule, $moduleName, IContentHandlerFactory $contentHandlerFactory=null, RevisionLookup $revisionLookup=null, WatchedItemStoreInterface $watchedItemStore=null, WikiPageFactory $wikiPageFactory=null, WatchlistManager $watchlistManager=null, UserOptionsLookup $userOptionsLookup=null, RedirectLookup $redirectLookup=null)
Definition: ApiEditPage.php:92
execute()
Evaluates the parameters, performs the requested query, and sets up the result.
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.
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
getExamplesMessages()
Returns usage examples for this module.
getHelpUrls()
Return links to more detailed help pages about the module.
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
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
Definition: ApiResult.php:604
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
Definition: Article.php:201
static canAddTagsAccompanyingChange(array $tags, Authority $performer=null, $checkBlock=true)
Is it OK to allow the user to apply all the specified tags at the same time as they edit/make the cha...
Definition: ChangeTags.php:635
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
The edit page/HTML interface (split from Article) The actual database and text munging is still in Ar...
Definition: EditPage.php:102
const UNICODE_CHECK
Used for Unicode support checks.
Definition: EditPage.php:109
Exception representing a failure to serialize or unserialize a content object.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Service for creating WikiPage objects.
Similar to MediaWiki\Request\FauxRequest, but only fakes URL parameters and method (POST or GET) and ...
Page revision base class.
Value object representing a content slot associated with a page revision.
Definition: SlotRecord.php:40
static getGlobalSession()
If PHP's session_id() has been set, returns that session.
Provides access to user options.
static plaintextParam( $plaintext)
Definition: Message.php:1265
Group all the pieces relevant to the context of a request into one instance.
setRequest(WebRequest $request)
Content object implementation for representing flat text.
Definition: TextContent.php:40
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:309
Service for formatting and validating API parameters.
Type definition for integer types.
Definition: IntegerDef.php:23
trait ApiWatchlistTrait
An ApiWatchlistTrait adds class properties and convenience methods for APIs that allow you to watch a...
const AS_RATE_LIMITED
Status: rate limiter for action 'edit' was tripped.
Definition: IEditObject.php:59
const AS_NO_CHANGE_CONTENT_MODEL
Status: user tried to modify the content model, but is not allowed to do that ( User::isAllowed('edit...
const AS_READ_ONLY_PAGE_LOGGED
Status: this logged in user is not allowed to edit this page.
Definition: IEditObject.php:53
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
Definition: IEditObject.php:50
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
Definition: IEditObject.php:71
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and wpRecreate == false or form was not posted.
Definition: IEditObject.php:62
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
Definition: IEditObject.php:80
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
Definition: IEditObject.php:41
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
Definition: IEditObject.php:47
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
Definition: IEditObject.php:89
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
Definition: IEditObject.php:32
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
Definition: IEditObject.php:92
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
Definition: IEditObject.php:35
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that.
Definition: IEditObject.php:65
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
Definition: IEditObject.php:95
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
Definition: IEditObject.php:83
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
Definition: IEditObject.php:44
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
Definition: IEditObject.php:77
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
Definition: IEditObject.php:38
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
Definition: IEditObject.php:68
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (ReadOnlyMode::isReadOnly() == true)
Definition: IEditObject.php:56
Service for resolving a wiki page redirect.
Service for looking up page revisions.
$content
Definition: router.php:76