59 private $contentHandlerFactory;
62 private $revisionLookup;
65 private $watchedItemStore;
68 private $wikiPageFactory;
71 private $userOptionsLookup;
74 private $redirectLookup;
79 private function persistGlobalSession() {
105 parent::__construct( $mainModule, $moduleName );
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();
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();
132 $titleObj = $pageObj->getTitle();
136 if ( $params[
'redirect'] ) {
137 if ( $params[
'prependtext'] ===
null
138 && $params[
'appendtext'] ===
null
139 && $params[
'section'] !==
'new'
143 if ( $titleObj->isRedirect() ) {
144 $oldTarget = $titleObj;
145 $redirTarget = $this->redirectLookup->getRedirectTarget( $oldTarget );
146 $redirTarget = Title::castFromLinkTarget( $redirTarget );
149 'from' => $titleObj->getPrefixedText(),
150 'to' => $redirTarget->getPrefixedText()
154 if ( $redirTarget->isExternal() || !$redirTarget->canExist() ) {
155 $redirValues[
'to'] = $redirTarget->getFullText();
158 'apierror-edit-invalidredirect',
162 'edit-invalidredirect',
163 [
'redirects' => $redirValues ]
168 $apiResult->addValue(
null,
'redirects', $redirValues );
171 $pageObj = $this->wikiPageFactory->newFromTitle( $redirTarget );
172 $titleObj = $pageObj->getTitle();
178 if ( $params[
'contentmodel'] ) {
179 $contentHandler = $this->contentHandlerFactory->getContentHandler( $params[
'contentmodel'] );
181 $contentHandler = $pageObj->getContentHandler();
183 $contentModel = $contentHandler->getModelID();
185 $name = $titleObj->getPrefixedDBkey();
187 if ( $params[
'undo'] > 0 ) {
189 } elseif ( $contentHandler->supportsDirectApiEditing() ===
false ) {
190 $this->
dieWithError( [
'apierror-no-direct-editing', $contentModel, $name ] );
193 $contentFormat = $params[
'contentformat'] ?: $contentHandler->getDefaultFormat();
195 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
196 $this->
dieWithError( [
'apierror-badformat', $contentFormat, $contentModel, $name ] );
199 if ( $params[
'createonly'] && $titleObj->exists() ) {
202 if ( $params[
'nocreate'] && !$titleObj->exists() ) {
210 [
'autoblock' =>
true ]
213 $toMD5 = $params[
'text'];
214 if ( $params[
'appendtext'] !==
null || $params[
'prependtext'] !==
null ) {
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 ) {
230 'wrap' =>
ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
234 # Otherwise, make a new empty content.
235 $content = $contentHandler->makeEmptyContent();
242 $this->
dieWithError( [
'apierror-appendnotsupported', $contentModel ] );
245 if ( $params[
'section'] !==
null ) {
246 if ( !$contentHandler->supportsSections() ) {
247 $this->
dieWithError( [
'apierror-sectionsnotsupported', $contentModel ] );
250 if ( $params[
'section'] ==
'new' ) {
255 $section = $params[
'section'];
267 $text =
$content->serialize( $contentFormat );
270 $params[
'text'] = $params[
'prependtext'] . $text . $params[
'appendtext'];
271 $toMD5 = $params[
'prependtext'] . $params[
'appendtext'];
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'] ] );
280 if ( $params[
'undoafter'] > 0 ) {
281 $undoafterRev = $this->revisionLookup->getRevisionById( $params[
'undoafter'] );
284 $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
286 if ( $undoafterRev ===
null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
287 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'undoafter'] ] );
290 if ( $undoRev->getPageId() != $pageObj->getId() ) {
291 $this->
dieWithError( [
'apierror-revwrongpage', $undoRev->getId(),
292 $titleObj->getPrefixedText() ] );
294 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
295 $this->
dieWithError( [
'apierror-revwrongpage', $undoafterRev->getId(),
296 $titleObj->getPrefixedText() ] );
299 $newContent = $contentHandler->getUndoContent(
301 $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
303 $undoRev->getContent( SlotRecord::MAIN ),
305 $undoafterRev->getContent( SlotRecord::MAIN ),
306 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
309 if ( !$newContent ) {
312 if ( !$params[
'contentmodel'] && !$params[
'contentformat'] ) {
318 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
319 $undoafterRevMainSlot = $undoafterRev->getSlot(
323 $contentFormat = $undoafterRevMainSlot->getFormat();
324 if ( !$contentFormat ) {
327 $contentFormat = $this->contentHandlerFactory
328 ->getContentHandler( $undoafterRevMainSlot->getModel() )
329 ->getDefaultFormat();
333 $contentModel = $newContent->getModel();
334 $undoContentModel =
true;
336 $params[
'text'] = $newContent->serialize( $contentFormat );
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();
352 if ( $params[
'md5'] !==
null && md5( $toMD5 ) !== $params[
'md5'] ) {
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,
372 if ( $params[
'summary'] !==
null ) {
373 $requestArray[
'wpSummary'] = $params[
'summary'];
376 if ( $params[
'sectiontitle'] !==
null ) {
377 $requestArray[
'wpSectionTitle'] = $params[
'sectiontitle'];
380 if ( $params[
'undo'] > 0 ) {
381 $requestArray[
'wpUndidRevision'] = $params[
'undo'];
383 if ( $params[
'undoafter'] > 0 ) {
384 $requestArray[
'wpUndoAfter'] = $params[
'undoafter'];
388 if ( !empty( $params[
'baserevid'] ) ) {
389 $requestArray[
'editRevId'] = $params[
'baserevid'];
394 if ( $params[
'basetimestamp'] !==
null && (
bool)$this->
getMain()->getVal(
'basetimestamp' ) ) {
395 $requestArray[
'wpEdittime'] = $params[
'basetimestamp'];
396 } elseif ( empty( $params[
'baserevid'] ) ) {
399 $requestArray[
'wpEdittime'] = $pageObj->getTimestamp();
402 if ( $params[
'starttimestamp'] !==
null ) {
403 $requestArray[
'wpStarttime'] = $params[
'starttimestamp'];
408 if ( $params[
'minor'] || ( !$params[
'notminor'] &&
409 $this->userOptionsLookup->getOption( $user,
'minordefault' ) )
411 $requestArray[
'wpMinoredit'] =
'';
414 if ( $params[
'recreate'] ) {
415 $requestArray[
'wpRecreate'] =
'';
418 if ( $params[
'section'] !==
null ) {
419 $section = $params[
'section'];
420 if ( !preg_match(
'/^((T-)?\d+|new)$/', $section ) ) {
424 if ( $section !==
'0'
428 $this->
dieWithError( [
'apierror-nosuchsection', $section ] );
430 $requestArray[
'wpSection'] = $params[
'section'];
432 $requestArray[
'wpSection'] =
'';
438 if ( $params[
'watch'] ) {
440 } elseif ( $params[
'unwatch'] ) {
445 $requestArray[
'wpWatchthis'] =
true;
448 if ( $watchlistExpiry ) {
449 $requestArray[
'wpWatchlistExpiry'] = $watchlistExpiry;
454 if ( $params[
'tags'] ) {
456 if ( $tagStatus->isOK() ) {
457 $requestArray[
'wpChangeTags'] = implode(
',', $params[
'tags'] );
465 $requestArray += $this->
getRequest()->getValues();
477 $articleContext->setWikiPage( $pageObj );
478 $articleContext->setUser( $this->
getUser() );
483 $ep =
new EditPage( $articleObject );
485 $ep->setApiEditOverride(
true );
486 $ep->setContextTitle( $titleObj );
487 $ep->importFormData( $req );
488 $ep->maybeActivateTempUserCreate(
true );
492 $editRevId = $requestArray[
'editRevId'] ??
false;
493 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
494 $baseContentModel =
null;
497 $baseContent = $baseRev->getContent( SlotRecord::MAIN );
498 $baseContentModel = $baseContent ? $baseContent->getModel() :
null;
501 $baseContentModel ??= $pageObj->getContentModel();
505 $contentModelsCanDiffer = $params[
'contentmodel'] || isset( $undoContentModel );
507 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
508 $this->
dieWithError( [
'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
512 $oldRevId = $articleObject->getRevIdFetched();
520 $status = $ep->attemptSave( $result );
521 $statusValue = is_int( $status->value ) ? $status->value : 0;
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';
534 if ( !$status->getErrors() ) {
537 $status->fatal(
'hookaborted' );
546 case EditPage::AS_BLOCKED_PAGE_FOR_USER:
551 case EditPage::AS_READ_ONLY_PAGE:
555 case EditPage::AS_SUCCESS_NEW_ARTICLE:
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;
568 $r[
'oldrevid'] = (int)$oldRevId;
569 $r[
'newrevid'] = (int)$newRevId;
571 $pageObj->getTimestamp() );
575 $r[
'watched'] =
true;
578 $this->watchedItemStore,
583 if ( $watchlistExpiry ) {
584 $r[
'watchlistexpiry'] = $watchlistExpiry;
587 $this->persistGlobalSession();
591 if ( !$status->getErrors() ) {
594 switch ( $statusValue ) {
596 case EditPage::AS_IMAGE_REDIRECT_ANON:
597 $status->fatal(
'apierror-noimageredirect-anon' );
599 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
600 $status->fatal(
'apierror-noimageredirect' );
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 ) );
607 case EditPage::AS_READ_ONLY_PAGE_ANON:
608 $status->fatal(
'apierror-noedit-anon' );
610 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
611 $status->fatal(
'apierror-cantchangecontentmodel' );
613 case EditPage::AS_ARTICLE_WAS_DELETED:
614 $status->fatal(
'apierror-pagedeleted' );
616 case EditPage::AS_CONFLICT_DETECTED:
617 $status->fatal(
'edit-conflict' );
624 case EditPage::AS_SPAM_ERROR:
626 $status->fatal(
'apierror-spamdetected', $result[
'spam'] );
628 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
629 $status->fatal(
'apierror-noedit' );
631 case EditPage::AS_RATE_LIMITED:
632 $status->fatal(
'apierror-ratelimited' );
634 case EditPage::AS_NO_CREATE_PERMISSION:
635 $status->fatal(
'nocreate-loggedin' );
637 case EditPage::AS_BLANK_ARTICLE:
638 $status->fatal(
'apierror-emptypage' );
640 case EditPage::AS_TEXTBOX_EMPTY:
641 $status->fatal(
'apierror-emptynewsection' );
643 case EditPage::AS_SUMMARY_NEEDED:
644 $status->fatal(
'apierror-summaryrequired' );
647 wfWarn( __METHOD__ .
": Unknown EditPage code $statusValue with no message" );
648 $status->fatal(
'apierror-unknownerror-editpage', $statusValue );
669 ParamValidator::PARAM_TYPE =>
'string',
672 ParamValidator::PARAM_TYPE =>
'integer',
676 ParamValidator::PARAM_TYPE =>
'string',
679 ParamValidator::PARAM_TYPE =>
'text',
683 ParamValidator::PARAM_TYPE =>
'tags',
684 ParamValidator::PARAM_ISMULTI =>
true,
690 ParamValidator::PARAM_TYPE =>
'integer',
693 ParamValidator::PARAM_TYPE =>
'timestamp',
695 'starttimestamp' => [
696 ParamValidator::PARAM_TYPE =>
'timestamp',
699 'createonly' =>
false,
702 ParamValidator::PARAM_DEFAULT =>
false,
703 ParamValidator::PARAM_DEPRECATED =>
true,
706 ParamValidator::PARAM_DEFAULT =>
false,
707 ParamValidator::PARAM_DEPRECATED =>
true,
718 ParamValidator::PARAM_TYPE =>
'text',
721 ParamValidator::PARAM_TYPE =>
'text',
724 ParamValidator::PARAM_TYPE =>
'integer',
725 IntegerDef::PARAM_MIN => 0,
729 ParamValidator::PARAM_TYPE =>
'integer',
730 IntegerDef::PARAM_MIN => 0,
734 ParamValidator::PARAM_TYPE =>
'boolean',
735 ParamValidator::PARAM_DEFAULT =>
false,
738 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
741 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
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',
769 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.
static create( $msg, $code=null, array $data=null)
Create an IApiMessage for the message.
static setIndexedTagName(array &$arr, $tag)
Set the tag name for numeric-keyed values in XML format.
static newFromWikiPage(WikiPage $page, IContextSource $context)
Create an Article object of the appropriate class for the given page.
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.
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...
Service for resolving a wiki page redirect.