3 * Copyright © 2007 Iker Labarga "<Firstname><Lastname>@gmail.com"
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
23 namespace MediaWiki\Api
;
27 use MediaWiki\Content\ContentHandler
;
28 use MediaWiki\Content\IContentHandlerFactory
;
29 use MediaWiki\Content\TextContent
;
30 use MediaWiki\Context\RequestContext
;
31 use MediaWiki\EditPage\EditPage
;
32 use MediaWiki\MainConfigNames
;
33 use MediaWiki\MediaWikiServices
;
34 use MediaWiki\Message\Message
;
35 use MediaWiki\Page\RedirectLookup
;
36 use MediaWiki\Page\WikiPageFactory
;
37 use MediaWiki\Request\DerivativeRequest
;
38 use MediaWiki\Revision\RevisionLookup
;
39 use MediaWiki\Revision\RevisionRecord
;
40 use MediaWiki\Revision\SlotRecord
;
41 use MediaWiki\Title\Title
;
42 use MediaWiki\User\Options\UserOptionsLookup
;
43 use MediaWiki\User\TempUser\TempUserCreator
;
44 use MediaWiki\User\User
;
45 use MediaWiki\User\UserFactory
;
46 use MediaWiki\Watchlist\WatchedItemStoreInterface
;
47 use MediaWiki\Watchlist\WatchlistManager
;
48 use MWContentSerializationException
;
49 use Wikimedia\ParamValidator\ParamValidator
;
50 use Wikimedia\ParamValidator\TypeDef\IntegerDef
;
53 * A module that allows for editing and creating pages.
55 * Currently, this wraps around the EditPage class in an ugly way,
56 * EditPage.php should be rewritten to provide a cleaner interface,
57 * see T20654 if you're inspired to fix this.
59 * WARNING: This class is //not// stable to extend. However, it is
60 * currently extended by the ApiThreadAction class in the LiquidThreads
61 * extension, which is deployed on WMF servers. Changes that would
62 * break LiquidThreads will likely be reverted. See T264200 for context
63 * and T264213 for removing LiquidThreads' unsupported extending of this
68 class ApiEditPage
extends ApiBase
{
69 use ApiCreateTempUserTrait
;
70 use ApiWatchlistTrait
;
72 private IContentHandlerFactory
$contentHandlerFactory;
73 private RevisionLookup
$revisionLookup;
74 private WatchedItemStoreInterface
$watchedItemStore;
75 private WikiPageFactory
$wikiPageFactory;
76 private RedirectLookup
$redirectLookup;
77 private TempUserCreator
$tempUserCreator;
78 private UserFactory
$userFactory;
81 * Sends a cookie so anons get talk message notifications, mirroring SubmitAction (T295910)
83 private function persistGlobalSession() {
84 \MediaWiki\Session\SessionManager
::getGlobalSession()->persist();
88 * @param ApiMain $mainModule
89 * @param string $moduleName
90 * @param IContentHandlerFactory|null $contentHandlerFactory
91 * @param RevisionLookup|null $revisionLookup
92 * @param WatchedItemStoreInterface|null $watchedItemStore
93 * @param WikiPageFactory|null $wikiPageFactory
94 * @param WatchlistManager|null $watchlistManager
95 * @param UserOptionsLookup|null $userOptionsLookup
96 * @param RedirectLookup|null $redirectLookup
97 * @param TempUserCreator|null $tempUserCreator
98 * @param UserFactory|null $userFactory
100 public function __construct(
103 ?IContentHandlerFactory
$contentHandlerFactory = null,
104 ?RevisionLookup
$revisionLookup = null,
105 ?WatchedItemStoreInterface
$watchedItemStore = null,
106 ?WikiPageFactory
$wikiPageFactory = null,
107 ?WatchlistManager
$watchlistManager = null,
108 ?UserOptionsLookup
$userOptionsLookup = null,
109 ?RedirectLookup
$redirectLookup = null,
110 ?TempUserCreator
$tempUserCreator = null,
111 ?UserFactory
$userFactory = null
113 parent
::__construct( $mainModule, $moduleName );
115 // This class is extended and therefor fallback to global state - T264213
116 $services = MediaWikiServices
::getInstance();
117 $this->contentHandlerFactory
= $contentHandlerFactory ??
$services->getContentHandlerFactory();
118 $this->revisionLookup
= $revisionLookup ??
$services->getRevisionLookup();
119 $this->watchedItemStore
= $watchedItemStore ??
$services->getWatchedItemStore();
120 $this->wikiPageFactory
= $wikiPageFactory ??
$services->getWikiPageFactory();
122 // Variables needed in ApiWatchlistTrait trait
123 $this->watchlistExpiryEnabled
= $this->getConfig()->get( MainConfigNames
::WatchlistExpiry
);
124 $this->watchlistMaxDuration
=
125 $this->getConfig()->get( MainConfigNames
::WatchlistExpiryMaxDuration
);
126 $this->watchlistManager
= $watchlistManager ??
$services->getWatchlistManager();
127 $this->userOptionsLookup
= $userOptionsLookup ??
$services->getUserOptionsLookup();
128 $this->redirectLookup
= $redirectLookup ??
$services->getRedirectLookup();
129 $this->tempUserCreator
= $tempUserCreator ??
$services->getTempUserCreator();
130 $this->userFactory
= $userFactory ??
$services->getUserFactory();
134 * @see EditPage::getUserForPermissions
137 private function getUserForPermissions() {
138 $user = $this->getUser();
139 if ( $this->tempUserCreator
->shouldAutoCreate( $user, 'edit' ) ) {
140 return $this->userFactory
->newUnsavedTempUser(
141 $this->tempUserCreator
->getStashedName( $this->getRequest()->getSession() )
147 public function execute() {
148 $this->useTransactionalTimeLimit();
150 $user = $this->getUser();
151 $params = $this->extractRequestParams();
153 $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
155 $pageObj = $this->getTitleOrPageId( $params );
156 $titleObj = $pageObj->getTitle();
157 $this->getErrorFormatter()->setContextTitle( $titleObj );
158 $apiResult = $this->getResult();
160 if ( $params['redirect'] ) {
161 if ( $params['prependtext'] === null
162 && $params['appendtext'] === null
163 && $params['section'] !== 'new'
165 $this->dieWithError( 'apierror-redirect-appendonly' );
167 if ( $titleObj->isRedirect() ) {
168 $oldTarget = $titleObj;
169 $redirTarget = $this->redirectLookup
->getRedirectTarget( $oldTarget );
170 $redirTarget = Title
::castFromLinkTarget( $redirTarget );
173 'from' => $titleObj->getPrefixedText(),
174 'to' => $redirTarget->getPrefixedText()
177 // T239428: Check whether the new title is valid
178 if ( $redirTarget->isExternal() ||
!$redirTarget->canExist() ) {
179 $redirValues['to'] = $redirTarget->getFullText();
182 'apierror-edit-invalidredirect',
183 Message
::plaintextParam( $oldTarget->getPrefixedText() ),
184 Message
::plaintextParam( $redirTarget->getFullText() ),
186 'edit-invalidredirect',
187 [ 'redirects' => $redirValues ]
191 ApiResult
::setIndexedTagName( $redirValues, 'r' );
192 $apiResult->addValue( null, 'redirects', $redirValues );
194 // Since the page changed, update $pageObj and $titleObj
195 $pageObj = $this->wikiPageFactory
->newFromTitle( $redirTarget );
196 $titleObj = $pageObj->getTitle();
198 $this->getErrorFormatter()->setContextTitle( $redirTarget );
202 if ( $params['contentmodel'] ) {
203 $contentHandler = $this->contentHandlerFactory
->getContentHandler( $params['contentmodel'] );
205 $contentHandler = $pageObj->getContentHandler();
207 $contentModel = $contentHandler->getModelID();
209 $name = $titleObj->getPrefixedDBkey();
211 if ( $params['undo'] > 0 ) {
212 // allow undo via api
213 } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
214 $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
217 $contentFormat = $params['contentformat'] ?
: $contentHandler->getDefaultFormat();
219 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
220 $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
223 if ( $params['createonly'] && $titleObj->exists() ) {
224 $this->dieWithError( 'apierror-articleexists' );
226 if ( $params['nocreate'] && !$titleObj->exists() ) {
227 $this->dieWithError( 'apierror-missingtitle' );
230 // Now let's check whether we're even allowed to do this
231 $this->checkTitleUserPermissions(
234 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
237 $toMD5 = $params['text'];
238 if ( $params['appendtext'] !== null ||
$params['prependtext'] !== null ) {
239 $content = $pageObj->getContent();
242 if ( $titleObj->getNamespace() === NS_MEDIAWIKI
) {
243 # If this is a MediaWiki:x message, then load the messages
244 # and return the message value for x.
245 $text = $titleObj->getDefaultMessageText();
246 if ( $text === false ) {
251 $content = ContentHandler
::makeContent( $text, $titleObj );
252 } catch ( MWContentSerializationException
$ex ) {
253 $this->dieWithException( $ex, [
254 'wrap' => ApiMessage
::create( 'apierror-contentserializationexception', 'parseerror' )
258 # Otherwise, make a new empty content.
259 $content = $contentHandler->makeEmptyContent();
263 // @todo Add support for appending/prepending to the Content interface
265 if ( !( $content instanceof TextContent
) ) {
266 $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
269 if ( $params['section'] !== null ) {
270 if ( !$contentHandler->supportsSections() ) {
271 $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
274 if ( $params['section'] == 'new' ) {
275 // DWIM if they're trying to prepend/append to a new section.
278 // Process the content for section edits
279 $section = $params['section'];
280 $content = $content->getSection( $section );
283 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
291 $text = $content->serialize( $contentFormat );
294 $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
295 $toMD5 = $params['prependtext'] . $params['appendtext'];
298 if ( $params['undo'] > 0 ) {
299 $undoRev = $this->revisionLookup
->getRevisionById( $params['undo'] );
300 if ( $undoRev === null ||
$undoRev->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
301 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
304 if ( $params['undoafter'] > 0 ) {
305 $undoafterRev = $this->revisionLookup
->getRevisionById( $params['undoafter'] );
307 // undoafter=0 or null
308 $undoafterRev = $this->revisionLookup
->getPreviousRevision( $undoRev );
310 if ( $undoafterRev === null ||
$undoafterRev->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
311 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
314 if ( $undoRev->getPageId() != $pageObj->getId() ) {
315 $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
316 $titleObj->getPrefixedText() ] );
318 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
319 $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
320 $titleObj->getPrefixedText() ] );
323 $newContent = $contentHandler->getUndoContent(
324 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
325 $pageObj->getRevisionRecord()->getContent( SlotRecord
::MAIN
),
326 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
327 $undoRev->getContent( SlotRecord
::MAIN
),
328 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
329 $undoafterRev->getContent( SlotRecord
::MAIN
),
330 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
333 if ( !$newContent ) {
334 $this->dieWithError( 'undo-failure', 'undofailure' );
336 if ( !$params['contentmodel'] && !$params['contentformat'] ) {
337 // If we are reverting content model, the new content model
338 // might not support the current serialization format, in
339 // which case go back to the old serialization format,
340 // but only if the user hasn't specified a format/model
342 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
343 $undoafterRevMainSlot = $undoafterRev->getSlot(
347 $contentFormat = $undoafterRevMainSlot->getFormat();
348 if ( !$contentFormat ) {
349 // fall back to default content format for the model
351 $contentFormat = $this->contentHandlerFactory
352 ->getContentHandler( $undoafterRevMainSlot->getModel() )
353 ->getDefaultFormat();
356 // Override content model with model of undid revision.
357 $contentModel = $newContent->getModel();
358 $undoContentModel = true;
360 $params['text'] = $newContent->serialize( $contentFormat );
361 // If no summary was given and we only undid one rev,
362 // use an autosummary
364 if ( $params['summary'] === null ) {
365 $nextRev = $this->revisionLookup
->getNextRevision( $undoafterRev );
366 if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
367 $undoRevUser = $undoRev->getUser();
368 $params['summary'] = $this->msg( 'undo-summary' )
369 ->params( $params['undo'], $undoRevUser ?
$undoRevUser->getName() : '' )
370 ->inContentLanguage()->text();
375 // See if the MD5 hash checks out
376 if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
377 $this->dieWithError( 'apierror-badmd5' );
380 // EditPage wants to parse its stuff from a WebRequest
381 // That interface kind of sucks, but it's workable
383 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
384 'wpTextbox1' => $params['text'],
385 'format' => $contentFormat,
386 'model' => $contentModel,
387 'wpEditToken' => $params['token'],
388 'wpIgnoreBlankSummary' => true,
389 'wpIgnoreBlankArticle' => true,
390 'wpIgnoreSelfRedirect' => true,
391 'bot' => $params['bot'],
392 'wpUnicodeCheck' => EditPage
::UNICODE_CHECK
,
395 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
396 if ( $params['summary'] !== null ) {
397 $requestArray['wpSummary'] = $params['summary'];
400 if ( $params['sectiontitle'] !== null ) {
401 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
404 if ( $params['undo'] > 0 ) {
405 $requestArray['wpUndidRevision'] = $params['undo'];
407 if ( $params['undoafter'] > 0 ) {
408 $requestArray['wpUndoAfter'] = $params['undoafter'];
411 // Skip for baserevid == null or '' or '0' or 0
412 if ( !empty( $params['baserevid'] ) ) {
413 $requestArray['editRevId'] = $params['baserevid'];
416 // Watch out for basetimestamp == '' or '0'
417 // It gets treated as NOW, almost certainly causing an edit conflict
418 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
419 $requestArray['wpEdittime'] = $params['basetimestamp'];
420 } elseif ( empty( $params['baserevid'] ) ) {
421 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
422 // due to the way userWasLastToEdit() works.
423 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
426 if ( $params['starttimestamp'] !== null ) {
427 $requestArray['wpStarttime'] = $params['starttimestamp'];
429 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
432 if ( $params['minor'] ||
( !$params['notminor'] &&
433 $this->userOptionsLookup
->getOption( $user, 'minordefault' ) )
435 $requestArray['wpMinoredit'] = '';
438 if ( $params['recreate'] ) {
439 $requestArray['wpRecreate'] = '';
442 if ( $params['section'] !== null ) {
443 $section = $params['section'];
444 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
445 $this->dieWithError( 'apierror-invalidsection' );
447 $content = $pageObj->getContent();
448 if ( $section !== '0'
450 && ( !$content ||
!$content->getSection( $section ) )
452 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
454 $requestArray['wpSection'] = $params['section'];
456 $requestArray['wpSection'] = '';
459 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
461 // Deprecated parameters
462 if ( $params['watch'] ) {
464 } elseif ( $params['unwatch'] ) {
469 $requestArray['wpWatchthis'] = true;
470 $watchlistExpiry = $this->getExpiryFromParams( $params );
472 if ( $watchlistExpiry ) {
473 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
478 if ( $params['tags'] ) {
479 $tagStatus = ChangeTags
::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
480 if ( $tagStatus->isOK() ) {
481 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
483 $this->dieStatus( $tagStatus );
487 // Pass through anything else we might have been given, to support extensions
488 // This is kind of a hack but it's the best we can do to make extensions work
489 $requestArray +
= $this->getRequest()->getValues();
491 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
492 global $wgTitle, $wgRequest;
494 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
496 // Some functions depend on $wgTitle == $ep->mTitle
497 // TODO: Make them not or check if they still do
498 $wgTitle = $titleObj;
500 $articleContext = new RequestContext
;
501 $articleContext->setRequest( $req );
502 $articleContext->setWikiPage( $pageObj );
503 $articleContext->setUser( $this->getUser() );
505 /** @var Article $articleObject */
506 $articleObject = Article
::newFromWikiPage( $pageObj, $articleContext );
508 $ep = new EditPage( $articleObject );
510 $ep->setApiEditOverride( true );
511 $ep->setContextTitle( $titleObj );
512 $ep->importFormData( $req );
513 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
514 if ( !$tempUserCreateStatus->isOK() ) {
515 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
518 // T255700: Ensure content models of the base content
519 // and fetched revision remain the same before attempting to save.
520 $editRevId = $requestArray['editRevId'] ??
false;
521 $baseRev = $this->revisionLookup
->getRevisionByTitle( $titleObj, $editRevId );
522 $baseContentModel = null;
525 $baseContent = $baseRev->getContent( SlotRecord
::MAIN
);
526 $baseContentModel = $baseContent ?
$baseContent->getModel() : null;
529 $baseContentModel ??
= $pageObj->getContentModel();
531 // However, allow the content models to possibly differ if we are intentionally
532 // changing them or we are doing an undo edit that is reverting content model change.
533 $contentModelsCanDiffer = $params['contentmodel'] ||
isset( $undoContentModel );
535 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
536 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
539 // Do the actual save
540 $oldRevId = $articleObject->getRevIdFetched();
543 // Fake $wgRequest for some hooks inside EditPage
544 // @todo FIXME: This interface SUCKS
545 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
546 $oldRequest = $wgRequest;
549 $status = $ep->attemptSave( $result );
550 $statusValue = is_int( $status->value
) ?
$status->value
: 0;
551 $wgRequest = $oldRequest;
552 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
555 switch ( $statusValue ) {
556 case EditPage
::AS_HOOK_ERROR
:
557 case EditPage
::AS_HOOK_ERROR_EXPECTED
:
558 if ( $status->statusData
!== null ) {
559 $r = $status->statusData
;
560 $r['result'] = 'Failure';
561 $apiResult->addValue( null, $this->getModuleName(), $r );
564 if ( !$status->getMessages() ) {
565 // This appears to be unreachable right now, because all
566 // code paths will set an error. Could change, though.
567 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
569 $this->dieStatus( $status );
571 // These two cases will normally have been caught earlier, and will
572 // only occur if something blocks the user between the earlier
573 // check and the check in EditPage (presumably a hook). It's not
574 // obvious that this is even possible.
575 // @codeCoverageIgnoreStart
576 case EditPage
::AS_BLOCKED_PAGE_FOR_USER
:
577 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
578 $this->dieBlocked( $user->getBlock() );
579 // dieBlocked prevents continuation
581 case EditPage
::AS_READ_ONLY_PAGE
:
582 $this->dieReadOnly();
583 // @codeCoverageIgnoreEnd
585 case EditPage
::AS_SUCCESS_NEW_ARTICLE
:
589 case EditPage
::AS_SUCCESS_UPDATE
:
590 $r['result'] = 'Success';
591 $r['pageid'] = (int)$titleObj->getArticleID();
592 $r['title'] = $titleObj->getPrefixedText();
593 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
594 $newRevId = $articleObject->getPage()->getLatest();
595 if ( $newRevId == $oldRevId ) {
596 $r['nochange'] = true;
598 $r['oldrevid'] = (int)$oldRevId;
599 $r['newrevid'] = (int)$newRevId;
600 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601
,
601 $pageObj->getTimestamp() );
605 $r['watched'] = true;
607 $watchlistExpiry = $this->getWatchlistExpiry(
608 $this->watchedItemStore
,
613 if ( $watchlistExpiry ) {
614 $r['watchlistexpiry'] = $watchlistExpiry;
617 $this->persistGlobalSession();
619 // If the temporary account was created in this request,
620 // or if the temporary account has zero edits (implying
621 // that the account was created during a failed edit
622 // attempt in a previous request), perform the top-level
623 // redirect to ensure the account is attached.
624 // Note that the temp user could already have performed
625 // the top-level redirect if this a first edit on
626 // a wiki that is not the user's home wiki.
627 $shouldRedirectForTempUser = isset( $result['savedTempUser'] ) ||
628 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
629 if ( $shouldRedirectForTempUser ) {
630 $r['tempusercreated'] = true;
631 $params['returnto'] ??
= $titleObj->getPrefixedDBkey();
632 $redirectUrl = $this->getTempUserRedirectUrl(
634 $result['savedTempUser'] ??
$user
636 if ( $redirectUrl ) {
637 $r['tempusercreatedredirect'] = $redirectUrl;
644 if ( !$status->getMessages() ) {
645 // EditPage sometimes only sets the status code without setting
646 // any actual error messages. Supply defaults for those cases.
647 switch ( $statusValue ) {
649 case EditPage
::AS_IMAGE_REDIRECT_ANON
:
650 $status->fatal( 'apierror-noimageredirect-anon' );
652 case EditPage
::AS_IMAGE_REDIRECT_LOGGED
:
653 $status->fatal( 'apierror-noimageredirect' );
655 case EditPage
::AS_CONTENT_TOO_BIG
:
656 case EditPage
::AS_MAX_ARTICLE_SIZE_EXCEEDED
:
657 $status->fatal( 'apierror-contenttoobig',
658 $this->getConfig()->get( MainConfigNames
::MaxArticleSize
) );
660 case EditPage
::AS_READ_ONLY_PAGE_ANON
:
661 $status->fatal( 'apierror-noedit-anon' );
663 case EditPage
::AS_NO_CHANGE_CONTENT_MODEL
:
664 $status->fatal( 'apierror-cantchangecontentmodel' );
666 case EditPage
::AS_ARTICLE_WAS_DELETED
:
667 $status->fatal( 'apierror-pagedeleted' );
669 case EditPage
::AS_CONFLICT_DETECTED
:
670 $status->fatal( 'edit-conflict' );
673 // Currently shouldn't be needed, but here in case
674 // hooks use them without setting appropriate
675 // errors on the status.
676 // @codeCoverageIgnoreStart
677 case EditPage
::AS_SPAM_ERROR
:
678 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
679 $status->fatal( 'apierror-spamdetected', $result['spam'] );
681 case EditPage
::AS_READ_ONLY_PAGE_LOGGED
:
682 $status->fatal( 'apierror-noedit' );
684 case EditPage
::AS_RATE_LIMITED
:
685 $status->fatal( 'apierror-ratelimited' );
687 case EditPage
::AS_NO_CREATE_PERMISSION
:
688 $status->fatal( 'nocreate-loggedin' );
690 case EditPage
::AS_BLANK_ARTICLE
:
691 $status->fatal( 'apierror-emptypage' );
693 case EditPage
::AS_TEXTBOX_EMPTY
:
694 $status->fatal( 'apierror-emptynewsection' );
696 case EditPage
::AS_SUMMARY_NEEDED
:
697 $status->fatal( 'apierror-summaryrequired' );
700 wfWarn( __METHOD__
. ": Unknown EditPage code $statusValue with no message" );
701 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
703 // @codeCoverageIgnoreEnd
706 $this->dieStatus( $status );
708 $apiResult->addValue( null, $this->getModuleName(), $r );
711 public function mustBePosted() {
715 public function isWriteMode() {
719 public function getAllowedParams() {
722 ParamValidator
::PARAM_TYPE
=> 'string',
725 ParamValidator
::PARAM_TYPE
=> 'integer',
729 ParamValidator
::PARAM_TYPE
=> 'string',
732 ParamValidator
::PARAM_TYPE
=> 'text',
736 ParamValidator
::PARAM_TYPE
=> 'tags',
737 ParamValidator
::PARAM_ISMULTI
=> true,
743 ParamValidator
::PARAM_TYPE
=> 'integer',
746 ParamValidator
::PARAM_TYPE
=> 'timestamp',
748 'starttimestamp' => [
749 ParamValidator
::PARAM_TYPE
=> 'timestamp',
752 'createonly' => false,
755 ParamValidator
::PARAM_DEFAULT
=> false,
756 ParamValidator
::PARAM_DEPRECATED
=> true,
759 ParamValidator
::PARAM_DEFAULT
=> false,
760 ParamValidator
::PARAM_DEPRECATED
=> true,
764 // Params appear in the docs in the order they are defined,
765 // which is why this is here and not at the bottom.
766 $params +
= $this->getWatchlistParams();
771 ParamValidator
::PARAM_TYPE
=> 'text',
774 ParamValidator
::PARAM_TYPE
=> 'text',
777 ParamValidator
::PARAM_TYPE
=> 'integer',
778 IntegerDef
::PARAM_MIN
=> 0,
779 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
782 ParamValidator
::PARAM_TYPE
=> 'integer',
783 IntegerDef
::PARAM_MIN
=> 0,
784 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
787 ParamValidator
::PARAM_TYPE
=> 'boolean',
788 ParamValidator
::PARAM_DEFAULT
=> false,
791 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getAllContentFormats(),
794 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getContentModels(),
797 // Standard definition automatically inserted
798 ApiBase
::PARAM_HELP_MSG_APPEND
=> [ 'apihelp-edit-param-token' ],
802 $params +
= $this->getCreateTempUserParams();
807 public function needsToken() {
811 protected function getExamplesMessages() {
813 'action=edit&title=Test&summary=test%20summary&' .
814 'text=article%20content&baserevid=1234567&token=123ABC'
815 => 'apihelp-edit-example-edit',
816 'action=edit&title=Test&summary=NOTOC&minor=&' .
817 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
818 => 'apihelp-edit-example-prepend',
819 'action=edit&title=Test&undo=13585&undoafter=13579&' .
820 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
821 => 'apihelp-edit-example-undo',
825 public function getHelpUrls() {
826 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
830 /** @deprecated class alias since 1.43 */
831 class_alias( ApiEditPage
::class, 'ApiEditPage' );