56 private $contentHandlerFactory;
59 private $revisionLookup;
62 private $watchedItemStore;
65 private $wikiPageFactory;
68 private $userOptionsLookup;
71 private $redirectLookup;
76 private function persistGlobalSession() {
77 MediaWiki\Session\SessionManager::getGlobalSession()->persist();
102 parent::__construct( $mainModule, $moduleName );
105 $services = MediaWikiServices::getInstance();
106 $this->contentHandlerFactory = $contentHandlerFactory ?? $services->getContentHandlerFactory();
107 $this->revisionLookup = $revisionLookup ?? $services->getRevisionLookup();
108 $this->watchedItemStore = $watchedItemStore ?? $services->getWatchedItemStore();
109 $this->wikiPageFactory = $wikiPageFactory ?? $services->getWikiPageFactory();
112 $this->watchlistExpiryEnabled = $this->
getConfig()->get( MainConfigNames::WatchlistExpiry );
113 $this->watchlistMaxDuration =
114 $this->
getConfig()->get( MainConfigNames::WatchlistExpiryMaxDuration );
115 $this->watchlistManager = $watchlistManager ?? $services->getWatchlistManager();
116 $this->userOptionsLookup = $userOptionsLookup ?? $services->getUserOptionsLookup();
117 $this->redirectLookup = $redirectLookup ?? $services->getRedirectLookup();
129 $titleObj = $pageObj->getTitle();
133 if ( $params[
'redirect'] ) {
134 if ( $params[
'prependtext'] ===
null
135 && $params[
'appendtext'] ===
null
136 && $params[
'section'] !==
'new'
140 if ( $titleObj->isRedirect() ) {
141 $oldTarget = $titleObj;
142 $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
143 $redirTarget = Title::castFromLinkTarget( $redirTarget );
146 'from' => $titleObj->getPrefixedText(),
147 'to' => $redirTarget->getPrefixedText()
151 if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
152 $redirValues[
'to'] = $redirTarget->getFullText();
155 'apierror-edit-invalidredirect',
159 'edit-invalidredirect',
160 [
'redirects' => $redirValues ]
164 ApiResult::setIndexedTagName( $redirValues,
'r' );
165 $apiResult->addValue(
null,
'redirects', $redirValues );
168 $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
169 $titleObj = $pageObj->getTitle();
175 if ( $params[
'contentmodel'] ) {
176 $contentHandler = $this->contentHandlerFactory->getContentHandler( $params[
'contentmodel'] );
178 $contentHandler = $pageObj->getContentHandler();
180 $contentModel = $contentHandler->getModelID();
182 $name = $titleObj->getPrefixedDBkey();
184 if ( $params[
'undo'] > 0 ) {
186 } elseif ( $contentHandler->supportsDirectApiEditing() ===
false ) {
187 $this->
dieWithError( [
'apierror-no-direct-editing', $contentModel, $name ] );
190 $contentFormat = $params[
'contentformat'] ?: $contentHandler->getDefaultFormat();
192 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
193 $this->
dieWithError( [
'apierror-badformat', $contentFormat, $contentModel, $name ] );
196 if ( $params[
'createonly'] && $titleObj->exists() ) {
199 if ( $params[
'nocreate'] && !$titleObj->exists() ) {
207 [
'autoblock' =>
true ]
210 $toMD5 = $params[
'text'];
211 if ( $params[
'appendtext'] !==
null || $params[
'prependtext'] !==
null ) {
216 # If this is a MediaWiki:x message, then load the messages
217 # and return the message value for x.
218 $text = $titleObj->getDefaultMessageText();
219 if ( $text ===
false ) {
224 $content = ContentHandler::makeContent( $text, $titleObj );
227 'wrap' => ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
231 # Otherwise, make a new empty content.
232 $content = $contentHandler->makeEmptyContent();
239 $this->
dieWithError( [
'apierror-appendnotsupported', $contentModel ] );
242 if ( $params[
'section'] !==
null ) {
243 if ( !$contentHandler->supportsSections() ) {
244 $this->
dieWithError( [
'apierror-sectionsnotsupported', $contentModel ] );
247 if ( $params[
'section'] ==
'new' ) {
252 $section = $params[
'section'];
264 $text =
$content->serialize( $contentFormat );
267 $params[
'text'] = $params[
'prependtext'] . $text . $params[
'appendtext'];
268 $toMD5 = $params[
'prependtext'] . $params[
'appendtext'];
271 if ( $params[
'undo'] > 0 ) {
272 $undoRev = $this->revisionLookup->getRevisionById( $params[
'undo'] );
273 if ( $undoRev ===
null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
274 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'undo'] ] );
277 if ( $params[
'undoafter'] > 0 ) {
278 $undoafterRev = $this->revisionLookup->getRevisionById( $params[
'undoafter'] );
281 $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
283 if ( $undoafterRev ===
null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
284 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'undoafter'] ] );
287 if ( $undoRev->getPageId() != $pageObj->getId() ) {
288 $this->
dieWithError( [
'apierror-revwrongpage', $undoRev->getId(),
289 $titleObj->getPrefixedText() ] );
291 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
292 $this->
dieWithError( [
'apierror-revwrongpage', $undoafterRev->getId(),
293 $titleObj->getPrefixedText() ] );
296 $newContent = $contentHandler->getUndoContent(
298 $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
300 $undoRev->getContent( SlotRecord::MAIN ),
302 $undoafterRev->getContent( SlotRecord::MAIN ),
303 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
306 if ( !$newContent ) {
309 if ( !$params[
'contentmodel'] && !$params[
'contentformat'] ) {
315 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
316 $undoafterRevMainSlot = $undoafterRev->getSlot(
320 $contentFormat = $undoafterRevMainSlot->getFormat();
321 if ( !$contentFormat ) {
324 $contentFormat = $this->contentHandlerFactory
325 ->getContentHandler( $undoafterRevMainSlot->getModel() )
326 ->getDefaultFormat();
330 $contentModel = $newContent->getModel();
331 $undoContentModel =
true;
333 $params[
'text'] = $newContent->serialize( $contentFormat );
337 if ( $params[
'summary'] ===
null ) {
338 $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
339 if ( $nextRev && $nextRev->getId() == $params[
'undo'] ) {
340 $undoRevUser = $undoRev->getUser();
341 $params[
'summary'] = $this->
msg(
'undo-summary' )
342 ->params( $params[
'undo'], $undoRevUser ? $undoRevUser->getName() :
'' )
343 ->inContentLanguage()->text();
349 if ( $params[
'md5'] !==
null && md5( $toMD5 ) !== $params[
'md5'] ) {
357 'wpTextbox1' => $params[
'text'],
358 'format' => $contentFormat,
359 'model' => $contentModel,
360 'wpEditToken' => $params[
'token'],
361 'wpIgnoreBlankSummary' =>
true,
362 'wpIgnoreBlankArticle' =>
true,
363 'wpIgnoreSelfRedirect' =>
true,
364 'bot' => $params[
'bot'],
369 if ( $params[
'summary'] !==
null ) {
370 $requestArray[
'wpSummary'] = $params[
'summary'];
373 if ( $params[
'sectiontitle'] !==
null ) {
374 $requestArray[
'wpSectionTitle'] = $params[
'sectiontitle'];
377 if ( $params[
'undo'] > 0 ) {
378 $requestArray[
'wpUndidRevision'] = $params[
'undo'];
380 if ( $params[
'undoafter'] > 0 ) {
381 $requestArray[
'wpUndoAfter'] = $params[
'undoafter'];
385 if ( !empty( $params[
'baserevid'] ) ) {
386 $requestArray[
'editRevId'] = $params[
'baserevid'];
391 if ( $params[
'basetimestamp'] !==
null && (
bool)$this->
getMain()->getVal(
'basetimestamp' ) ) {
392 $requestArray[
'wpEdittime'] = $params[
'basetimestamp'];
393 } elseif ( empty( $params[
'baserevid'] ) ) {
396 $requestArray[
'wpEdittime'] = $pageObj->getTimestamp();
399 if ( $params[
'starttimestamp'] !==
null ) {
400 $requestArray[
'wpStarttime'] = $params[
'starttimestamp'];
405 if ( $params[
'minor'] || ( !$params[
'notminor'] &&
406 $this->userOptionsLookup->getOption( $user,
'minordefault' ) )
408 $requestArray[
'wpMinoredit'] =
'';
411 if ( $params[
'recreate'] ) {
412 $requestArray[
'wpRecreate'] =
'';
415 if ( $params[
'section'] !==
null ) {
416 $section = $params[
'section'];
417 if ( !preg_match(
'/^((T-)?\d+|new)$/', $section ) ) {
421 if ( $section !==
'0'
425 $this->
dieWithError( [
'apierror-nosuchsection', $section ] );
427 $requestArray[
'wpSection'] = $params[
'section'];
429 $requestArray[
'wpSection'] =
'';
435 if ( $params[
'watch'] ) {
437 } elseif ( $params[
'unwatch'] ) {
442 $requestArray[
'wpWatchthis'] =
true;
445 if ( $watchlistExpiry ) {
446 $requestArray[
'wpWatchlistExpiry'] = $watchlistExpiry;
451 if ( $params[
'tags'] ) {
453 if ( $tagStatus->isOK() ) {
454 $requestArray[
'wpChangeTags'] = implode(
',', $params[
'tags'] );
462 $requestArray += $this->
getRequest()->getValues();
474 $articleContext->setWikiPage( $pageObj );
475 $articleContext->setUser( $this->
getUser() );
478 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
480 $ep =
new EditPage( $articleObject );
482 $ep->setApiEditOverride(
true );
483 $ep->setContextTitle( $titleObj );
484 $ep->importFormData( $req );
488 $editRevId = $requestArray[
'editRevId'] ??
false;
489 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
490 $baseContentModel =
null;
493 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
494 $baseContentModel = $baseContent ? $baseContent->getModel() :
null;
497 if ( $baseContentModel ===
null ) {
498 $baseContentModel = $pageObj->getContentModel();
503 $contentModelsCanDiffer = $params[
'contentmodel'] || isset( $undoContentModel );
505 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
506 $this->
dieWithError( [
'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
510 $oldRevId = $articleObject->getRevIdFetched();
518 $status = $ep->attemptSave( $result );
519 $statusValue = is_int( $status->value ) ? $status->value : 0;
523 switch ( $statusValue ) {
526 if ( $status->statusData !==
null ) {
527 $r = $status->statusData;
528 $r[
'result'] =
'Failure';
532 if ( !$status->getErrors() ) {
535 $status->fatal(
'hookaborted' );
558 $r[
'result'] =
'Success';
559 $r[
'pageid'] = (int)$titleObj->getArticleID();
560 $r[
'title'] = $titleObj->getPrefixedText();
561 $r[
'contentmodel'] = $articleObject->getPage()->getContentModel();
562 $newRevId = $articleObject->getPage()->getLatest();
563 if ( $newRevId == $oldRevId ) {
564 $r[
'nochange'] =
true;
566 $r[
'oldrevid'] = (int)$oldRevId;
567 $r[
'newrevid'] = (int)$newRevId;
569 $pageObj->getTimestamp() );
573 $r[
'watched'] =
true;
576 $this->watchedItemStore,
581 if ( $watchlistExpiry ) {
582 $r[
'watchlistexpiry'] = $watchlistExpiry;
585 $this->persistGlobalSession();
589 if ( !$status->getErrors() ) {
592 switch ( $statusValue ) {
595 $status->fatal(
'apierror-noimageredirect-anon' );
598 $status->fatal(
'apierror-noimageredirect' );
602 $status->fatal(
'apierror-contenttoobig',
603 $this->
getConfig()->
get( MainConfigNames::MaxArticleSize ) );
606 $status->fatal(
'apierror-noedit-anon' );
609 $status->fatal(
'apierror-cantchangecontentmodel' );
612 $status->fatal(
'apierror-pagedeleted' );
615 $status->fatal(
'edit-conflict' );
624 $status->fatal(
'apierror-spamdetected', $result[
'spam'] );
627 $status->fatal(
'apierror-noedit' );
630 $status->fatal(
'apierror-ratelimited' );
633 $status->fatal(
'nocreate-loggedin' );
636 $status->fatal(
'apierror-emptypage' );
639 $status->fatal(
'apierror-emptynewsection' );
642 $status->fatal(
'apierror-summaryrequired' );
645 wfWarn( __METHOD__ .
": Unknown EditPage code $statusValue with no message" );
646 $status->fatal(
'apierror-unknownerror-editpage', $statusValue );
667 ParamValidator::PARAM_TYPE =>
'string',
670 ParamValidator::PARAM_TYPE =>
'integer',
674 ParamValidator::PARAM_TYPE =>
'string',
677 ParamValidator::PARAM_TYPE =>
'text',
681 ParamValidator::PARAM_TYPE =>
'tags',
682 ParamValidator::PARAM_ISMULTI =>
true,
688 ParamValidator::PARAM_TYPE =>
'integer',
691 ParamValidator::PARAM_TYPE =>
'timestamp',
693 'starttimestamp' => [
694 ParamValidator::PARAM_TYPE =>
'timestamp',
697 'createonly' =>
false,
700 ParamValidator::PARAM_DEFAULT =>
false,
701 ParamValidator::PARAM_DEPRECATED =>
true,
704 ParamValidator::PARAM_DEFAULT =>
false,
705 ParamValidator::PARAM_DEPRECATED =>
true,
716 ParamValidator::PARAM_TYPE =>
'text',
719 ParamValidator::PARAM_TYPE =>
'text',
722 ParamValidator::PARAM_TYPE =>
'integer',
723 IntegerDef::PARAM_MIN => 0,
727 ParamValidator::PARAM_TYPE =>
'integer',
728 IntegerDef::PARAM_MIN => 0,
732 ParamValidator::PARAM_TYPE =>
'boolean',
733 ParamValidator::PARAM_DEFAULT =>
false,
736 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
739 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
754 'action=edit&title=Test&summary=test%20summary&' .
755 'text=article%20content&baserevid=1234567&token=123ABC'
756 =>
'apihelp-edit-example-edit',
757 'action=edit&title=Test&summary=NOTOC&minor=&' .
758 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
759 =>
'apihelp-edit-example-prepend',
760 'action=edit&title=Test&undo=13585&undoafter=13579&' .
761 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
762 =>
'apihelp-edit-example-undo',
767 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
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.
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,...
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgTitle
This abstract class implements many basic API functions, and is the base of all API classes.
dieWithError( $msg, $code=null, $data=null, $httpCode=0)
Abort execution with an error.
getMain()
Get the main module.
const PARAM_HELP_MSG_APPEND
((string|array|Message)[]) Specify additional i18n messages to append to the normal message for this ...
dieReadOnly()
Helper function for readonly errors.
requireAtLeastOneParameter( $params,... $required)
Die if none of a certain set of parameters is set and not false.
getResult()
Get the result object.
extractRequestParams( $options=[])
Using getAllowedParams(), this function makes an array of the values provided by the user,...
const PARAM_RANGE_ENFORCE
(boolean) Inverse of IntegerDef::PARAM_IGNORE_RANGE
checkTitleUserPermissions( $pageIdentity, $actions, array $options=[])
Helper function for permission-denied errors.
getModuleName()
Get the name of the module being executed by this instance.
getTitleOrPageId( $params, $load=false)
Get a WikiPage object from a title or pageid param, if possible.
dieStatus(StatusValue $status)
Throw an ApiUsageException based on the Status object.
useTransactionalTimeLimit()
Call wfTransactionalTimeLimit() if this request was POSTed.
dieBlocked(Block $block)
Throw an ApiUsageException, which will (if uncaught) call the main module's error handler and die wit...
dieWithException(Throwable $exception, array $options=[])
Abort execution with an error derived from a throwable.
A module that allows for editing and creating pages.
__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)
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.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
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...
const UNICODE_CHECK
Used for Unicode support checks.
Exception representing a failure to serialize or unserialize a content object.
A class containing constants representing the names of configuration variables.
Service for creating WikiPage objects.
static plaintextParam( $plaintext)
Group all the pieces relevant to the context of a request into one instance.
setRequest(WebRequest $request)
Content object implementation for representing flat text.
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.
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.
const AS_READ_ONLY_PAGE_ANON
Status: this anonymous user is not allowed to edit this page.
const AS_CONFLICT_DETECTED
Status: (non-resolvable) edit conflict.
const AS_ARTICLE_WAS_DELETED
Status: article was deleted while editing and wpRecreate == false or form was not posted.
const AS_TEXTBOX_EMPTY
Status: user tried to create a new section without content.
const AS_HOOK_ERROR_EXPECTED
Status: A hook function returned an error.
const AS_CONTENT_TOO_BIG
Status: Content too big (> $wgMaxArticleSize)
const AS_SPAM_ERROR
Status: summary contained spam according to one of the regexes in $wgSummarySpamRegex.
const AS_SUCCESS_UPDATE
Status: Article successfully updated.
const AS_IMAGE_REDIRECT_ANON
Status: anonymous user is not allowed to upload (User::isAllowed('upload') == false)
const AS_SUCCESS_NEW_ARTICLE
Status: Article successfully created.
const AS_NO_CREATE_PERMISSION
Status: user tried to create this page, but is not allowed to do that.
const AS_IMAGE_REDIRECT_LOGGED
Status: logged in user is not allowed to upload (User::isAllowed('upload') == false)
const AS_MAX_ARTICLE_SIZE_EXCEEDED
Status: article is too big (> $wgMaxArticleSize), after merging in the new section.
const AS_BLOCKED_PAGE_FOR_USER
Status: User is blocked from editing this page.
const AS_SUMMARY_NEEDED
Status: no edit summary given and the user has forceeditsummary set and the user is not editing in hi...
const AS_HOOK_ERROR
Status: Article update aborted by a hook function.
const AS_BLANK_ARTICLE
Status: user tried to create a blank page and wpIgnoreBlankArticle == false.
const AS_READ_ONLY_PAGE
Status: wiki is in readonly mode (ReadOnlyMode::isReadOnly() == true)
Service for resolving a wiki page redirect.