MediaWiki  master
ApiEditPage.php
Go to the documentation of this file.
1 <?php
38 
55 class ApiEditPage extends ApiBase {
57 
59  private $contentHandlerFactory;
60 
62  private $revisionLookup;
63 
65  private $watchedItemStore;
66 
68  private $wikiPageFactory;
69 
71  private $userOptionsLookup;
72 
74  private $redirectLookup;
75 
79  private function persistGlobalSession() {
81  }
82 
94  public function __construct(
95  ApiMain $mainModule,
96  $moduleName,
97  IContentHandlerFactory $contentHandlerFactory = null,
98  RevisionLookup $revisionLookup = null,
99  WatchedItemStoreInterface $watchedItemStore = null,
100  WikiPageFactory $wikiPageFactory = null,
101  WatchlistManager $watchlistManager = null,
102  UserOptionsLookup $userOptionsLookup = null,
103  RedirectLookup $redirectLookup = null
104  ) {
105  parent::__construct( $mainModule, $moduleName );
106 
107  // This class is extended and therefor fallback to global state - T264213
108  $services = MediaWikiServices::getInstance();
109  $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
110  $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
111  $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
112  $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
113 
114  // Variables needed in ApiWatchlistTrait trait
115  $this->watchlistExpiryEnabled = $this->getConfig()->get( MainConfigNames::WatchlistExpiry );
116  $this->watchlistMaxDuration =
117  $this->getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
118  $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
119  $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
120  $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
121  }
122 
123  public function execute() {
124  $this->useTransactionalTimeLimit();
125 
126  $user = $this->getUser();
127  $params = $this->extractRequestParams();
128 
129  $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
130 
131  $pageObj = $this->getTitleOrPageId( $params );
132  $titleObj = $pageObj->getTitle();
133  $this->getErrorFormatter()->setContextTitle( $titleObj );
134  $apiResult = $this->getResult();
135 
136  if ( $params['redirect'] ) {
137  if ( $params['prependtext'] === null
138  && $params['appendtext'] === null
139  && $params['section'] !== 'new'
140  ) {
141  $this->dieWithError( 'apierror-redirect-appendonly' );
142  }
143  if ( $titleObj->isRedirect() ) {
144  $oldTarget = $titleObj;
145  $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
146  $redirTarget = Title::castFromLinkTarget( $redirTarget );
147 
148  $redirValues = [
149  'from' => $titleObj->getPrefixedText(),
150  'to' => $redirTarget->getPrefixedText()
151  ];
152 
153  // T239428: Check whether the new title is valid
154  if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
155  $redirValues['to'] = $redirTarget->getFullText();
156  $this->dieWithError(
157  [
158  'apierror-edit-invalidredirect',
159  Message::plaintextParam( $oldTarget->getPrefixedText() ),
160  Message::plaintextParam( $redirTarget->getFullText() ),
161  ],
162  'edit-invalidredirect',
163  [ 'redirects' => $redirValues ]
164  );
165  }
166 
167  ApiResult::setIndexedTagName( $redirValues, 'r' );
168  $apiResult->addValue( null, 'redirects', $redirValues );
169 
170  // Since the page changed, update $pageObj and $titleObj
171  $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
172  $titleObj = $pageObj->getTitle();
173 
174  $this->getErrorFormatter()->setContextTitle( $redirTarget );
175  }
176  }
177 
178  if ( $params['contentmodel'] ) {
179  $contentHandler = $this->contentHandlerFactory->getContentHandler( $params['contentmodel'] );
180  } else {
181  $contentHandler = $pageObj->getContentHandler();
182  }
183  $contentModel = $contentHandler->getModelID();
184 
185  $name = $titleObj->getPrefixedDBkey();
186 
187  if ( $params['undo'] > 0 ) {
188  // allow undo via api
189  } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
190  $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
191  }
192 
193  $contentFormat = $params['contentformat'] ?: $contentHandler->getDefaultFormat();
194 
195  if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
196  $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
197  }
198 
199  if ( $params['createonly'] && $titleObj->exists() ) {
200  $this->dieWithError( 'apierror-articleexists' );
201  }
202  if ( $params['nocreate'] && !$titleObj->exists() ) {
203  $this->dieWithError( 'apierror-missingtitle' );
204  }
205 
206  // Now let's check whether we're even allowed to do this
208  $titleObj,
209  'edit',
210  [ 'autoblock' => true ]
211  );
212 
213  $toMD5 = $params['text'];
214  if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
215  $content = $pageObj->getContent();
216 
217  if ( !$content ) {
218  if ( $titleObj->getNamespace() === NS_MEDIAWIKI ) {
219  # If this is a MediaWiki:x message, then load the messages
220  # and return the message value for x.
221  $text = $titleObj->getDefaultMessageText();
222  if ( $text === false ) {
223  $text = '';
224  }
225 
226  try {
227  $content = ContentHandler::makeContent( $text, $titleObj );
228  } catch ( MWContentSerializationException $ex ) {
229  $this->dieWithException( $ex, [
230  'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
231  ] );
232  }
233  } else {
234  # Otherwise, make a new empty content.
235  $content = $contentHandler->makeEmptyContent();
236  }
237  }
238 
239  // @todo Add support for appending/prepending to the Content interface
240 
241  if ( !( $content instanceof TextContent ) ) {
242  $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
243  }
244 
245  if ( $params['section'] !== null ) {
246  if ( !$contentHandler->supportsSections() ) {
247  $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
248  }
249 
250  if ( $params['section'] == 'new' ) {
251  // DWIM if they're trying to prepend/append to a new section.
252  $content = null;
253  } else {
254  // Process the content for section edits
255  $section = $params['section'];
256  $content = $content->getSection( $section );
257 
258  if ( !$content ) {
259  $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
260  }
261  }
262  }
263 
264  if ( !$content ) {
265  $text = '';
266  } else {
267  $text = $content->serialize( $contentFormat );
268  }
269 
270  $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
271  $toMD5 = $params['prependtext'] . $params['appendtext'];
272  }
273 
274  if ( $params['undo'] > 0 ) {
275  $undoRev = $this->revisionLookup->getRevisionById( $params['undo'] );
276  if ( $undoRev === null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
277  $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
278  }
279 
280  if ( $params['undoafter'] > 0 ) {
281  $undoafterRev = $this->revisionLookup->getRevisionById( $params['undoafter'] );
282  } else {
283  // undoafter=0 or null
284  $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
285  }
286  if ( $undoafterRev === null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
287  $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
288  }
289 
290  if ( $undoRev->getPageId() != $pageObj->getId() ) {
291  $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
292  $titleObj->getPrefixedText() ] );
293  }
294  if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
295  $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
296  $titleObj->getPrefixedText() ] );
297  }
298 
299  $newContent = $contentHandler->getUndoContent(
300  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
301  $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
302  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
303  $undoRev->getContent( SlotRecord::MAIN ),
304  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
305  $undoafterRev->getContent( SlotRecord::MAIN ),
306  $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
307  );
308 
309  if ( !$newContent ) {
310  $this->dieWithError( 'undo-failure', 'undofailure' );
311  }
312  if ( !$params['contentmodel'] && !$params['contentformat'] ) {
313  // If we are reverting content model, the new content model
314  // might not support the current serialization format, in
315  // which case go back to the old serialization format,
316  // but only if the user hasn't specified a format/model
317  // parameter.
318  if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
319  $undoafterRevMainSlot = $undoafterRev->getSlot(
320  SlotRecord::MAIN,
321  RevisionRecord::RAW
322  );
323  $contentFormat = $undoafterRevMainSlot->getFormat();
324  if ( !$contentFormat ) {
325  // fall back to default content format for the model
326  // of $undoafterRev
327  $contentFormat = $this->contentHandlerFactory
328  ->getContentHandler( $undoafterRevMainSlot->getModel() )
329  ->getDefaultFormat();
330  }
331  }
332  // Override content model with model of undid revision.
333  $contentModel = $newContent->getModel();
334  $undoContentModel = true;
335  }
336  $params['text'] = $newContent->serialize( $contentFormat );
337  // If no summary was given and we only undid one rev,
338  // use an autosummary
339 
340  if ( $params['summary'] === null ) {
341  $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
342  if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
343  $undoRevUser = $undoRev->getUser();
344  $params['summary'] = $this->msg( 'undo-summary' )
345  ->params( $params['undo'], $undoRevUser ? $undoRevUser->getName() : '' )
346  ->inContentLanguage()->text();
347  }
348  }
349  }
350 
351  // See if the MD5 hash checks out
352  if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
353  $this->dieWithError( 'apierror-badmd5' );
354  }
355 
356  // EditPage wants to parse its stuff from a WebRequest
357  // That interface kind of sucks, but it's workable
358  $requestArray = [
359  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
360  'wpTextbox1' => $params['text'],
361  'format' => $contentFormat,
362  'model' => $contentModel,
363  'wpEditToken' => $params['token'],
364  'wpIgnoreBlankSummary' => true,
365  'wpIgnoreBlankArticle' => true,
366  'wpIgnoreSelfRedirect' => true,
367  'bot' => $params['bot'],
368  'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
369  ];
370 
371  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
372  if ( $params['summary'] !== null ) {
373  $requestArray['wpSummary'] = $params['summary'];
374  }
375 
376  if ( $params['sectiontitle'] !== null ) {
377  $requestArray['wpSectionTitle'] = $params['sectiontitle'];
378  }
379 
380  if ( $params['undo'] > 0 ) {
381  $requestArray['wpUndidRevision'] = $params['undo'];
382  }
383  if ( $params['undoafter'] > 0 ) {
384  $requestArray['wpUndoAfter'] = $params['undoafter'];
385  }
386 
387  // Skip for baserevid == null or '' or '0' or 0
388  if ( !empty( $params['baserevid'] ) ) {
389  $requestArray['editRevId'] = $params['baserevid'];
390  }
391 
392  // Watch out for basetimestamp == '' or '0'
393  // It gets treated as NOW, almost certainly causing an edit conflict
394  if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
395  $requestArray['wpEdittime'] = $params['basetimestamp'];
396  } elseif ( empty( $params['baserevid'] ) ) {
397  // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
398  // due to the way userWasLastToEdit() works.
399  $requestArray['wpEdittime'] = $pageObj->getTimestamp();
400  }
401 
402  if ( $params['starttimestamp'] !== null ) {
403  $requestArray['wpStarttime'] = $params['starttimestamp'];
404  } else {
405  $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
406  }
407 
408  if ( $params['minor'] || ( !$params['notminor'] &&
409  $this->userOptionsLookup->getOption( $user, 'minordefault' ) )
410  ) {
411  $requestArray['wpMinoredit'] = '';
412  }
413 
414  if ( $params['recreate'] ) {
415  $requestArray['wpRecreate'] = '';
416  }
417 
418  if ( $params['section'] !== null ) {
419  $section = $params['section'];
420  if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
421  $this->dieWithError( 'apierror-invalidsection' );
422  }
423  $content = $pageObj->getContent();
424  if ( $section !== '0'
425  && $section != 'new'
426  && ( !$content || !$content->getSection( $section ) )
427  ) {
428  $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
429  }
430  $requestArray['wpSection'] = $params['section'];
431  } else {
432  $requestArray['wpSection'] = '';
433  }
434 
435  $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
436 
437  // Deprecated parameters
438  if ( $params['watch'] ) {
439  $watch = true;
440  } elseif ( $params['unwatch'] ) {
441  $watch = false;
442  }
443 
444  if ( $watch ) {
445  $requestArray['wpWatchthis'] = true;
446  $watchlistExpiry = $this->getExpiryFromParams( $params );
447 
448  if ( $watchlistExpiry ) {
449  $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
450  }
451  }
452 
453  // Apply change tags
454  if ( $params['tags'] ) {
455  $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
456  if ( $tagStatus->isOK() ) {
457  $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
458  } else {
459  $this->dieStatus( $tagStatus );
460  }
461  }
462 
463  // Pass through anything else we might have been given, to support extensions
464  // This is kind of a hack but it's the best we can do to make extensions work
465  $requestArray += $this->getRequest()->getValues();
466 
467  global $wgTitle, $wgRequest;
468 
469  $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
470 
471  // Some functions depend on $wgTitle == $ep->mTitle
472  // TODO: Make them not or check if they still do
473  $wgTitle = $titleObj;
474 
475  $articleContext = new RequestContext;
476  $articleContext->setRequest( $req );
477  $articleContext->setWikiPage( $pageObj );
478  $articleContext->setUser( $this->getUser() );
479 
481  $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
482 
483  $ep = new EditPage( $articleObject );
484 
485  $ep->setApiEditOverride( true );
486  $ep->setContextTitle( $titleObj );
487  $ep->importFormData( $req );
488  $ep->maybeActivateTempUserCreate( true );
489 
490  // T255700: Ensure content models of the base content
491  // and fetched revision remain the same before attempting to save.
492  $editRevId = $requestArray['editRevId'] ?? false;
493  $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
494  $baseContentModel = null;
495 
496  if ( $baseRev ) {
497  $baseContent = $baseRev->getContent( SlotRecord::MAIN );
498  $baseContentModel = $baseContent ? $baseContent->getModel() : null;
499  }
500 
501  $baseContentModel ??= $pageObj->getContentModel();
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 ) {
526  case EditPage::AS_HOOK_ERROR:
527  case EditPage::AS_HOOK_ERROR_EXPECTED:
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
546  case EditPage::AS_BLOCKED_PAGE_FOR_USER:
547  // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
548  $this->dieBlocked( $user->getBlock() );
549  // dieBlocked prevents continuation
550 
551  case EditPage::AS_READ_ONLY_PAGE:
552  $this->dieReadOnly();
553  // @codeCoverageIgnoreEnd
554 
555  case EditPage::AS_SUCCESS_NEW_ARTICLE:
556  $r['new'] = true;
557  // fall-through
558 
559  case EditPage::AS_SUCCESS_UPDATE:
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
596  case EditPage::AS_IMAGE_REDIRECT_ANON:
597  $status->fatal( 'apierror-noimageredirect-anon' );
598  break;
599  case EditPage::AS_IMAGE_REDIRECT_LOGGED:
600  $status->fatal( 'apierror-noimageredirect' );
601  break;
602  case EditPage::AS_CONTENT_TOO_BIG:
603  case EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED:
604  $status->fatal( 'apierror-contenttoobig',
605  $this->getConfig()->get( MainConfigNames::MaxArticleSize ) );
606  break;
607  case EditPage::AS_READ_ONLY_PAGE_ANON:
608  $status->fatal( 'apierror-noedit-anon' );
609  break;
610  case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
611  $status->fatal( 'apierror-cantchangecontentmodel' );
612  break;
613  case EditPage::AS_ARTICLE_WAS_DELETED:
614  $status->fatal( 'apierror-pagedeleted' );
615  break;
616  case EditPage::AS_CONFLICT_DETECTED:
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
624  case EditPage::AS_SPAM_ERROR:
625  // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
626  $status->fatal( 'apierror-spamdetected', $result['spam'] );
627  break;
628  case EditPage::AS_READ_ONLY_PAGE_LOGGED:
629  $status->fatal( 'apierror-noedit' );
630  break;
631  case EditPage::AS_RATE_LIMITED:
632  $status->fatal( 'apierror-ratelimited' );
633  break;
634  case EditPage::AS_NO_CREATE_PERMISSION:
635  $status->fatal( 'nocreate-loggedin' );
636  break;
637  case EditPage::AS_BLANK_ARTICLE:
638  $status->fatal( 'apierror-emptypage' );
639  break;
640  case EditPage::AS_TEXTBOX_EMPTY:
641  $status->fatal( 'apierror-emptynewsection' );
642  break;
643  case EditPage::AS_SUMMARY_NEEDED:
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:399
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgTitle
Definition: Setup.php:519
This abstract class implements many basic API functions, and is the base of all API classes.
Definition: ApiBase.php:59
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
Definition: ApiBase.php:1460
getMain()
Get the main module.
Definition: ApiBase.php:522
getErrorFormatter()
Definition: ApiBase.php:648
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
Definition: ApiBase.php:173
dieReadOnly()
Helper function for readonly errors.
Definition: ApiBase.php:1550
requireAtLeastOneParameter( $params,... $required)
Die if none of a certain set of parameters is set and not false.
Definition: ApiBase.php:971
getResult()
Get the result object.
Definition: ApiBase.php:637
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
Definition: ApiBase.php:773
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
Definition: ApiBase.php:156
checkTitleUserPermissions( $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
Definition: ApiBase.php:1592
getModuleName()
Get the name of the module being executed by this instance.
Definition: ApiBase.php:506
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
Definition: ApiBase.php:1044
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
Definition: ApiBase.php:1521
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
Definition: ApiBase.php:1305
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
Definition: ApiBase.php:1489
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
Definition: ApiBase.php:1473
A module that allows for editing and creating pages.
Definition: ApiEditPage.php:55
__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:94
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:59
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
Definition: ApiMessage.php:45
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:214
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:637
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()
Exception representing a failure to serialize or unserialize a content object.
The HTML user interface for page editing.
Definition: EditPage.php:148
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.
Represents a title within MediaWiki.
Definition: Title.php:82
Provides access to user options.
static plaintextParam( $plaintext)
Definition: Message.php:1267
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
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...
Service for resolving a wiki page redirect.
Service for looking up page revisions.
$content
Definition: router.php:76