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