86 parent::__construct( $mainModule, $moduleName );
89 $services = MediaWikiServices::getInstance();
91 $this->revisionLookup =
$revisionLookup ?? $services->getRevisionLookup();
93 $this->wikiPageFactory =
$wikiPageFactory ?? $services->getWikiPageFactory();
96 $this->watchlistExpiryEnabled = $this->
getConfig()->get(
'WatchlistExpiry' );
97 $this->watchlistMaxDuration = $this->
getConfig()->get(
'WatchlistExpiryMaxDuration' );
111 $titleObj = $pageObj->getTitle();
115 if ( $params[
'redirect'] ) {
116 if ( $params[
'prependtext'] ===
null
117 && $params[
'appendtext'] ===
null
118 && $params[
'section'] !==
'new'
122 if ( $titleObj->isRedirect() ) {
123 $oldTitle = $titleObj;
125 $titles = $this->revisionLookup
126 ->getRevisionByTitle( $oldTitle, 0, IDBAccessObject::READ_LATEST )
127 ->getContent( SlotRecord::MAIN, RevisionRecord::FOR_THIS_USER, $user )
128 ->getRedirectChain();
130 '@phan-var Title[] $titles';
135 foreach ( $titles as $id => $newTitle ) {
136 $titles[$id - 1] = $titles[$id - 1] ?? $oldTitle;
139 'from' => $titles[$id - 1]->getPrefixedText(),
140 'to' => $newTitle->getPrefixedText()
143 $titleObj = $newTitle;
146 if ( $titleObj->isExternal() || !$titleObj->canExist() ) {
147 $redirValues[count( $redirValues ) - 1][
'to'] = $titleObj->getFullText();
150 'apierror-edit-invalidredirect',
154 'edit-invalidredirect',
155 [
'redirects' => $redirValues ]
160 ApiResult::setIndexedTagName( $redirValues,
'r' );
161 $apiResult->addValue(
null,
'redirects', $redirValues );
164 $pageObj = $this->wikiPageFactory->newFromTitle( $titleObj );
169 if ( $params[
'contentmodel'] ) {
170 $contentHandler = $this->contentHandlerFactory->getContentHandler( $params[
'contentmodel'] );
172 $contentHandler = $pageObj->getContentHandler();
174 $contentModel = $contentHandler->getModelID();
176 $name = $titleObj->getPrefixedDBkey();
178 if ( $params[
'undo'] > 0 ) {
180 } elseif ( $contentHandler->supportsDirectApiEditing() ===
false ) {
181 $this->
dieWithError( [
'apierror-no-direct-editing', $contentModel, $name ] );
184 $contentFormat = $params[
'contentformat'] ?: $contentHandler->getDefaultFormat();
186 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
187 $this->
dieWithError( [
'apierror-badformat', $contentFormat, $contentModel, $name ] );
190 if ( $params[
'createonly'] && $titleObj->exists() ) {
193 if ( $params[
'nocreate'] && !$titleObj->exists() ) {
201 [
'autoblock' =>
true ]
204 $toMD5 = $params[
'text'];
205 if ( $params[
'appendtext'] !==
null || $params[
'prependtext'] !==
null ) {
210 # If this is a MediaWiki:x message, then load the messages
211 # and return the message value for x.
212 $text = $titleObj->getDefaultMessageText();
213 if ( $text ===
false ) {
218 $content = ContentHandler::makeContent( $text, $titleObj );
221 'wrap' => ApiMessage::create(
'apierror-contentserializationexception',
'parseerror' )
225 # Otherwise, make a new empty content.
226 $content = $contentHandler->makeEmptyContent();
233 $this->
dieWithError( [
'apierror-appendnotsupported', $contentModel ] );
236 if ( $params[
'section'] !==
null ) {
237 if ( !$contentHandler->supportsSections() ) {
238 $this->
dieWithError( [
'apierror-sectionsnotsupported', $contentModel ] );
241 if ( $params[
'section'] ==
'new' ) {
246 $section = $params[
'section'];
258 $text =
$content->serialize( $contentFormat );
261 $params[
'text'] = $params[
'prependtext'] . $text . $params[
'appendtext'];
262 $toMD5 = $params[
'prependtext'] . $params[
'appendtext'];
265 if ( $params[
'undo'] > 0 ) {
266 $undoRev = $this->revisionLookup->getRevisionById( $params[
'undo'] );
267 if ( $undoRev ===
null || $undoRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
268 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'undo'] ] );
271 if ( $params[
'undoafter'] > 0 ) {
272 $undoafterRev = $this->revisionLookup->getRevisionById( $params[
'undoafter'] );
274 if ( $params[
'undoafter'] == 0 ) {
275 $undoafterRev = $this->revisionLookup->getPreviousRevision( $undoRev );
277 if ( $undoafterRev ===
null || $undoafterRev->isDeleted( RevisionRecord::DELETED_TEXT ) ) {
278 $this->
dieWithError( [
'apierror-nosuchrevid', $params[
'undoafter'] ] );
281 if ( $undoRev->getPageId() != $pageObj->getId() ) {
282 $this->
dieWithError( [
'apierror-revwrongpage', $undoRev->getId(),
283 $titleObj->getPrefixedText() ] );
285 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
286 $this->
dieWithError( [
'apierror-revwrongpage', $undoafterRev->getId(),
287 $titleObj->getPrefixedText() ] );
290 $newContent = $contentHandler->getUndoContent(
291 $pageObj->getRevisionRecord()->getContent( SlotRecord::MAIN ),
292 $undoRev->getContent( SlotRecord::MAIN ),
293 $undoafterRev->getContent( SlotRecord::MAIN ),
294 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
297 if ( !$newContent ) {
300 if ( !$params[
'contentmodel'] && !$params[
'contentformat'] ) {
306 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
307 $undoafterRevMainSlot = $undoafterRev->getSlot(
311 $contentFormat = $undoafterRevMainSlot->getFormat();
312 if ( !$contentFormat ) {
315 $contentFormat = $this->contentHandlerFactory
316 ->getContentHandler( $undoafterRevMainSlot->getModel() )
317 ->getDefaultFormat();
321 $contentModel = $newContent->getModel();
322 $undoContentModel =
true;
324 $params[
'text'] = $newContent->serialize( $contentFormat );
328 if ( $params[
'summary'] ===
null ) {
329 $nextRev = $this->revisionLookup->getNextRevision( $undoafterRev );
330 if ( $nextRev && $nextRev->getId() == $params[
'undo'] ) {
331 $undoRevUser = $undoRev->getUser();
332 $params[
'summary'] =
wfMessage(
'undo-summary' )
333 ->params( $params[
'undo'], $undoRevUser ? $undoRevUser->getName() :
'' )
334 ->inContentLanguage()->text();
340 if ( $params[
'md5'] !==
null && md5( $toMD5 ) !== $params[
'md5'] ) {
347 'wpTextbox1' => $params[
'text'],
348 'format' => $contentFormat,
349 'model' => $contentModel,
350 'wpEditToken' => $params[
'token'],
351 'wpIgnoreBlankSummary' =>
true,
352 'wpIgnoreBlankArticle' =>
true,
353 'wpIgnoreSelfRedirect' =>
true,
354 'bot' => $params[
'bot'],
358 if ( $params[
'summary'] !==
null ) {
359 $requestArray[
'wpSummary'] = $params[
'summary'];
362 if ( $params[
'sectiontitle'] !==
null ) {
363 $requestArray[
'wpSectionTitle'] = $params[
'sectiontitle'];
366 if ( $params[
'undo'] > 0 ) {
367 $requestArray[
'wpUndidRevision'] = $params[
'undo'];
369 if ( $params[
'undoafter'] > 0 ) {
370 $requestArray[
'wpUndoAfter'] = $params[
'undoafter'];
374 if ( !empty( $params[
'baserevid'] ) ) {
375 $requestArray[
'editRevId'] = $params[
'baserevid'];
380 if ( $params[
'basetimestamp'] !==
null && (
bool)$this->
getMain()->getVal(
'basetimestamp' ) ) {
381 $requestArray[
'wpEdittime'] = $params[
'basetimestamp'];
382 } elseif ( empty( $params[
'baserevid'] ) ) {
385 $requestArray[
'wpEdittime'] = $pageObj->getTimestamp();
388 if ( $params[
'starttimestamp'] !==
null ) {
389 $requestArray[
'wpStarttime'] = $params[
'starttimestamp'];
394 if ( $params[
'minor'] || ( !$params[
'notminor'] &&
395 $this->userOptionsLookup->getOption( $user,
'minordefault' ) )
397 $requestArray[
'wpMinoredit'] =
'';
400 if ( $params[
'recreate'] ) {
401 $requestArray[
'wpRecreate'] =
'';
404 if ( $params[
'section'] !==
null ) {
405 $section = $params[
'section'];
406 if ( !preg_match(
'/^((T-)?\d+|new)$/', $section ) ) {
410 if ( $section !==
'0'
414 $this->
dieWithError( [
'apierror-nosuchsection', $section ] );
416 $requestArray[
'wpSection'] = $params[
'section'];
418 $requestArray[
'wpSection'] =
'';
424 if ( $params[
'watch'] ) {
426 } elseif ( $params[
'unwatch'] ) {
431 $requestArray[
'wpWatchthis'] =
true;
434 if ( $watchlistExpiry ) {
435 $requestArray[
'wpWatchlistExpiry'] = $watchlistExpiry;
440 if ( $params[
'tags'] ) {
442 if ( $tagStatus->isOK() ) {
443 $requestArray[
'wpChangeTags'] = implode(
',', $params[
'tags'] );
451 $requestArray += $this->
getRequest()->getValues();
463 $articleContext->setWikiPage( $pageObj );
464 $articleContext->setUser( $this->
getUser() );
467 $articleObject = Article::newFromWikiPage( $pageObj, $articleContext );
469 $ep =
new EditPage( $articleObject );
471 $ep->setApiEditOverride(
true );
472 $ep->setContextTitle( $titleObj );
473 $ep->importFormData( $req );
477 $editRevId = $requestArray[
'editRevId'] ??
false;
478 $baseRev = $this->revisionLookup->getRevisionByTitle( $titleObj, $editRevId );
479 $baseContentModel = $baseRev
480 ? $baseRev->getContent( SlotRecord::MAIN )->getModel()
481 : $pageObj->getContentModel();
485 $contentModelsCanDiffer = $params[
'contentmodel'] || isset( $undoContentModel );
487 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
488 $this->
dieWithError( [
'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
492 $oldRevId = $articleObject->getRevIdFetched();
500 $status = $ep->attemptSave( $result );
501 $statusValue = is_int( $status->value ) ? $status->value : 0;
505 switch ( $statusValue ) {
508 if ( isset( $status->apiHookResult ) ) {
509 $r = $status->apiHookResult;
510 $r[
'result'] =
'Failure';
514 if ( !$status->getErrors() ) {
517 $status->fatal(
'hookaborted' );
539 $r[
'result'] =
'Success';
540 $r[
'pageid'] = (int)$titleObj->getArticleID();
541 $r[
'title'] = $titleObj->getPrefixedText();
542 $r[
'contentmodel'] = $articleObject->getPage()->getContentModel();
543 $newRevId = $articleObject->getPage()->getLatest();
544 if ( $newRevId == $oldRevId ) {
545 $r[
'nochange'] =
true;
547 $r[
'oldrevid'] = (int)$oldRevId;
548 $r[
'newrevid'] = (int)$newRevId;
550 $pageObj->getTimestamp() );
554 $r[
'watched'] =
true;
557 $this->watchedItemStore,
562 if ( $watchlistExpiry ) {
563 $r[
'watchlistexpiry'] = $watchlistExpiry;
569 if ( !$status->getErrors() ) {
572 switch ( $statusValue ) {
575 $status->fatal(
'apierror-noimageredirect-anon' );
578 $status->fatal(
'apierror-noimageredirect' );
582 $status->fatal(
'apierror-contenttoobig', $this->
getConfig()->
get(
'MaxArticleSize' ) );
585 $status->fatal(
'apierror-noedit-anon' );
588 $status->fatal(
'apierror-cantchangecontentmodel' );
591 $status->fatal(
'apierror-pagedeleted' );
594 $status->fatal(
'edit-conflict' );
602 $status->fatal(
'apierror-spamdetected', $result[
'spam'] );
605 $status->fatal(
'apierror-noedit' );
608 $status->fatal(
'apierror-ratelimited' );
611 $status->fatal(
'nocreate-loggedin' );
614 $status->fatal(
'apierror-emptypage' );
617 $status->fatal(
'apierror-emptynewsection' );
620 $status->fatal(
'apierror-summaryrequired' );
623 wfWarn( __METHOD__ .
": Unknown EditPage code $statusValue with no message" );
624 $status->fatal(
'apierror-unknownerror-editpage', $statusValue );
671 'starttimestamp' => [
675 'createonly' =>
false,
732 'action=edit&title=Test&summary=test%20summary&' .
733 'text=article%20content&baserevid=1234567&token=123ABC'
734 =>
'apihelp-edit-example-edit',
735 'action=edit&title=Test&summary=NOTOC&minor=&' .
736 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
737 =>
'apihelp-edit-example-prepend',
738 'action=edit&title=Test&undo=13585&undoafter=13579&' .
739 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
740 =>
'apihelp-edit-example-undo',
745 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.
WatchlistManager $watchlistManager
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,...
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.
WatchedItemStoreInterface $watchedItemStore
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.
__construct(ApiMain $mainModule, $moduleName, IContentHandlerFactory $contentHandlerFactory=null, RevisionLookup $revisionLookup=null, WatchedItemStoreInterface $watchedItemStore=null, WikiPageFactory $wikiPageFactory=null, WatchlistManager $watchlistManager=null, UserOptionsLookup $userOptionsLookup=null)
mustBePosted()
Indicates whether this module must be called with a POST request.
UserOptionsLookup $userOptionsLookup
getAllowedParams()
Returns an array of allowed parameters (parameter name) => (default value) or (parameter name) => (ar...
RevisionLookup $revisionLookup
getExamplesMessages()
Returns usage examples for this module.
WikiPageFactory $wikiPageFactory
IContentHandlerFactory $contentHandlerFactory
getHelpUrls()
Return links to more detailed help pages about the module.
This is the main API class, used for both external and internal processing.
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.
static plaintextParam( $plaintext)
Group all the pieces relevant to the context of a request into one instance @newable.
setRequest(WebRequest $request)
Content object implementation for representing flat text.
Represents a title within MediaWiki.
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 (wfReadOnly() == true)