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();
87 public function __construct(
90 ?IContentHandlerFactory
$contentHandlerFactory = null,
91 ?RevisionLookup
$revisionLookup = null,
92 ?WatchedItemStoreInterface
$watchedItemStore = null,
93 ?WikiPageFactory
$wikiPageFactory = null,
94 ?WatchlistManager
$watchlistManager = null,
95 ?UserOptionsLookup
$userOptionsLookup = null,
96 ?RedirectLookup
$redirectLookup = null,
97 ?TempUserCreator
$tempUserCreator = null,
98 ?UserFactory
$userFactory = null
100 parent
::__construct( $mainModule, $moduleName );
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();
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 $this->tempUserCreator
= $tempUserCreator ??
$services->getTempUserCreator();
117 $this->userFactory
= $userFactory ??
$services->getUserFactory();
121 * @see EditPage::getUserForPermissions
124 private function getUserForPermissions() {
125 $user = $this->getUser();
126 if ( $this->tempUserCreator
->shouldAutoCreate( $user, 'edit' ) ) {
127 return $this->userFactory
->newUnsavedTempUser(
128 $this->tempUserCreator
->getStashedName( $this->getRequest()->getSession() )
134 public function execute() {
135 $this->useTransactionalTimeLimit();
137 $user = $this->getUser();
138 $params = $this->extractRequestParams();
140 $this->requireAtLeastOneParameter( $params, 'text', 'appendtext', 'prependtext', 'undo' );
142 $pageObj = $this->getTitleOrPageId( $params );
143 $titleObj = $pageObj->getTitle();
144 $this->getErrorFormatter()->setContextTitle( $titleObj );
145 $apiResult = $this->getResult();
147 if ( $params['redirect'] ) {
148 if ( $params['prependtext'] === null
149 && $params['appendtext'] === null
150 && $params['section'] !== 'new'
152 $this->dieWithError( 'apierror-redirect-appendonly' );
154 if ( $titleObj->isRedirect() ) {
155 $oldTarget = $titleObj;
156 $redirTarget = $this->redirectLookup
->getRedirectTarget( $oldTarget );
157 $redirTarget = Title
::castFromLinkTarget( $redirTarget );
160 'from' => $titleObj->getPrefixedText(),
161 'to' => $redirTarget->getPrefixedText()
164 // T239428: Check whether the new title is valid
165 if ( $redirTarget->isExternal() ||
!$redirTarget->canExist() ) {
166 $redirValues['to'] = $redirTarget->getFullText();
169 'apierror-edit-invalidredirect',
170 Message
::plaintextParam( $oldTarget->getPrefixedText() ),
171 Message
::plaintextParam( $redirTarget->getFullText() ),
173 'edit-invalidredirect',
174 [ 'redirects' => $redirValues ]
178 ApiResult
::setIndexedTagName( $redirValues, 'r' );
179 $apiResult->addValue( null, 'redirects', $redirValues );
181 // Since the page changed, update $pageObj and $titleObj
182 $pageObj = $this->wikiPageFactory
->newFromTitle( $redirTarget );
183 $titleObj = $pageObj->getTitle();
185 $this->getErrorFormatter()->setContextTitle( $redirTarget );
189 if ( $params['contentmodel'] ) {
190 $contentHandler = $this->contentHandlerFactory
->getContentHandler( $params['contentmodel'] );
192 $contentHandler = $pageObj->getContentHandler();
194 $contentModel = $contentHandler->getModelID();
196 $name = $titleObj->getPrefixedDBkey();
198 if ( $params['undo'] > 0 ) {
199 // allow undo via api
200 } elseif ( $contentHandler->supportsDirectApiEditing() === false ) {
201 $this->dieWithError( [ 'apierror-no-direct-editing', $contentModel, $name ] );
204 $contentFormat = $params['contentformat'] ?
: $contentHandler->getDefaultFormat();
206 if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
207 $this->dieWithError( [ 'apierror-badformat', $contentFormat, $contentModel, $name ] );
210 if ( $params['createonly'] && $titleObj->exists() ) {
211 $this->dieWithError( 'apierror-articleexists' );
213 if ( $params['nocreate'] && !$titleObj->exists() ) {
214 $this->dieWithError( 'apierror-missingtitle' );
217 // Now let's check whether we're even allowed to do this
218 $this->checkTitleUserPermissions(
221 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
224 $toMD5 = $params['text'];
225 if ( $params['appendtext'] !== null ||
$params['prependtext'] !== null ) {
226 $content = $pageObj->getContent();
229 if ( $titleObj->getNamespace() === NS_MEDIAWIKI
) {
230 # If this is a MediaWiki:x message, then load the messages
231 # and return the message value for x.
232 $text = $titleObj->getDefaultMessageText();
233 if ( $text === false ) {
238 $content = ContentHandler
::makeContent( $text, $titleObj );
239 } catch ( MWContentSerializationException
$ex ) {
240 $this->dieWithException( $ex, [
241 'wrap' => ApiMessage
::create( 'apierror-contentserializationexception', 'parseerror' )
245 # Otherwise, make a new empty content.
246 $content = $contentHandler->makeEmptyContent();
250 // @todo Add support for appending/prepending to the Content interface
252 if ( !( $content instanceof TextContent
) ) {
253 $this->dieWithError( [ 'apierror-appendnotsupported', $contentModel ] );
256 if ( $params['section'] !== null ) {
257 if ( !$contentHandler->supportsSections() ) {
258 $this->dieWithError( [ 'apierror-sectionsnotsupported', $contentModel ] );
261 if ( $params['section'] == 'new' ) {
262 // DWIM if they're trying to prepend/append to a new section.
265 // Process the content for section edits
266 $section = $params['section'];
267 $content = $content->getSection( $section );
270 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
278 $text = $content->serialize( $contentFormat );
281 $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
282 $toMD5 = $params['prependtext'] . $params['appendtext'];
285 if ( $params['undo'] > 0 ) {
286 $undoRev = $this->revisionLookup
->getRevisionById( $params['undo'] );
287 if ( $undoRev === null ||
$undoRev->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
288 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undo'] ] );
291 if ( $params['undoafter'] > 0 ) {
292 $undoafterRev = $this->revisionLookup
->getRevisionById( $params['undoafter'] );
294 // undoafter=0 or null
295 $undoafterRev = $this->revisionLookup
->getPreviousRevision( $undoRev );
297 if ( $undoafterRev === null ||
$undoafterRev->isDeleted( RevisionRecord
::DELETED_TEXT
) ) {
298 $this->dieWithError( [ 'apierror-nosuchrevid', $params['undoafter'] ] );
301 if ( $undoRev->getPageId() != $pageObj->getId() ) {
302 $this->dieWithError( [ 'apierror-revwrongpage', $undoRev->getId(),
303 $titleObj->getPrefixedText() ] );
305 if ( $undoafterRev->getPageId() != $pageObj->getId() ) {
306 $this->dieWithError( [ 'apierror-revwrongpage', $undoafterRev->getId(),
307 $titleObj->getPrefixedText() ] );
310 $newContent = $contentHandler->getUndoContent(
311 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
312 $pageObj->getRevisionRecord()->getContent( SlotRecord
::MAIN
),
313 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
314 $undoRev->getContent( SlotRecord
::MAIN
),
315 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Content is for public use here
316 $undoafterRev->getContent( SlotRecord
::MAIN
),
317 $pageObj->getRevisionRecord()->getId() === $undoRev->getId()
320 if ( !$newContent ) {
321 $this->dieWithError( 'undo-failure', 'undofailure' );
323 if ( !$params['contentmodel'] && !$params['contentformat'] ) {
324 // If we are reverting content model, the new content model
325 // might not support the current serialization format, in
326 // which case go back to the old serialization format,
327 // but only if the user hasn't specified a format/model
329 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
330 $undoafterRevMainSlot = $undoafterRev->getSlot(
334 $contentFormat = $undoafterRevMainSlot->getFormat();
335 if ( !$contentFormat ) {
336 // fall back to default content format for the model
338 $contentFormat = $this->contentHandlerFactory
339 ->getContentHandler( $undoafterRevMainSlot->getModel() )
340 ->getDefaultFormat();
343 // Override content model with model of undid revision.
344 $contentModel = $newContent->getModel();
345 $undoContentModel = true;
347 $params['text'] = $newContent->serialize( $contentFormat );
348 // If no summary was given and we only undid one rev,
349 // use an autosummary
351 if ( $params['summary'] === null ) {
352 $nextRev = $this->revisionLookup
->getNextRevision( $undoafterRev );
353 if ( $nextRev && $nextRev->getId() == $params['undo'] ) {
354 $undoRevUser = $undoRev->getUser();
355 $params['summary'] = $this->msg( 'undo-summary' )
356 ->params( $params['undo'], $undoRevUser ?
$undoRevUser->getName() : '' )
357 ->inContentLanguage()->text();
362 // See if the MD5 hash checks out
363 if ( $params['md5'] !== null && md5( $toMD5 ) !== $params['md5'] ) {
364 $this->dieWithError( 'apierror-badmd5' );
367 // EditPage wants to parse its stuff from a WebRequest
368 // That interface kind of sucks, but it's workable
370 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
371 'wpTextbox1' => $params['text'],
372 'format' => $contentFormat,
373 'model' => $contentModel,
374 'wpEditToken' => $params['token'],
375 'wpIgnoreBlankSummary' => true,
376 'wpIgnoreBlankArticle' => true,
377 'wpIgnoreSelfRedirect' => true,
378 'wpIgnoreBrokenRedirects' => true,
379 'bot' => $params['bot'],
380 'wpUnicodeCheck' => EditPage
::UNICODE_CHECK
,
383 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset False positive
384 if ( $params['summary'] !== null ) {
385 $requestArray['wpSummary'] = $params['summary'];
388 if ( $params['sectiontitle'] !== null ) {
389 $requestArray['wpSectionTitle'] = $params['sectiontitle'];
392 if ( $params['undo'] > 0 ) {
393 $requestArray['wpUndidRevision'] = $params['undo'];
395 if ( $params['undoafter'] > 0 ) {
396 $requestArray['wpUndoAfter'] = $params['undoafter'];
399 // Skip for baserevid == null or '' or '0' or 0
400 if ( !empty( $params['baserevid'] ) ) {
401 $requestArray['editRevId'] = $params['baserevid'];
404 // Watch out for basetimestamp == '' or '0'
405 // It gets treated as NOW, almost certainly causing an edit conflict
406 if ( $params['basetimestamp'] !== null && (bool)$this->getMain()->getVal( 'basetimestamp' ) ) {
407 $requestArray['wpEdittime'] = $params['basetimestamp'];
408 } elseif ( empty( $params['baserevid'] ) ) {
409 // Only set if baserevid is not set. Otherwise, conflicts would be ignored,
410 // due to the way userWasLastToEdit() works.
411 $requestArray['wpEdittime'] = $pageObj->getTimestamp();
414 if ( $params['starttimestamp'] !== null ) {
415 $requestArray['wpStarttime'] = $params['starttimestamp'];
417 $requestArray['wpStarttime'] = wfTimestampNow(); // Fake wpStartime
420 if ( $params['minor'] ||
( !$params['notminor'] &&
421 $this->userOptionsLookup
->getOption( $user, 'minordefault' ) )
423 $requestArray['wpMinoredit'] = '';
426 if ( $params['recreate'] ) {
427 $requestArray['wpRecreate'] = '';
430 if ( $params['section'] !== null ) {
431 $section = $params['section'];
432 if ( !preg_match( '/^((T-)?\d+|new)$/', $section ) ) {
433 $this->dieWithError( 'apierror-invalidsection' );
435 $content = $pageObj->getContent();
436 if ( $section !== '0'
438 && ( !$content ||
!$content->getSection( $section ) )
440 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
442 $requestArray['wpSection'] = $params['section'];
444 $requestArray['wpSection'] = '';
447 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
449 // Deprecated parameters
450 if ( $params['watch'] ) {
452 } elseif ( $params['unwatch'] ) {
457 $requestArray['wpWatchthis'] = true;
458 $watchlistExpiry = $this->getExpiryFromParams( $params );
460 if ( $watchlistExpiry ) {
461 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
466 if ( $params['tags'] ) {
467 $tagStatus = ChangeTags
::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
468 if ( $tagStatus->isOK() ) {
469 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
471 $this->dieStatus( $tagStatus );
475 // Pass through anything else we might have been given, to support extensions
476 // This is kind of a hack but it's the best we can do to make extensions work
477 $requestArray +
= $this->getRequest()->getValues();
479 // phpcs:ignore MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage,MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle
480 global $wgTitle, $wgRequest;
482 $req = new DerivativeRequest( $this->getRequest(), $requestArray, true );
484 // Some functions depend on $wgTitle == $ep->mTitle
485 // TODO: Make them not or check if they still do
486 $wgTitle = $titleObj;
488 $articleContext = new RequestContext
;
489 $articleContext->setRequest( $req );
490 $articleContext->setWikiPage( $pageObj );
491 $articleContext->setUser( $this->getUser() );
493 /** @var Article $articleObject */
494 $articleObject = Article
::newFromWikiPage( $pageObj, $articleContext );
496 $ep = new EditPage( $articleObject );
498 $ep->setApiEditOverride( true );
499 $ep->setContextTitle( $titleObj );
500 $ep->importFormData( $req );
501 $tempUserCreateStatus = $ep->maybeActivateTempUserCreate( true );
502 if ( !$tempUserCreateStatus->isOK() ) {
503 $this->dieWithError( 'apierror-tempuseracquirefailed', 'tempuseracquirefailed' );
506 // T255700: Ensure content models of the base content
507 // and fetched revision remain the same before attempting to save.
508 $editRevId = $requestArray['editRevId'] ??
false;
509 $baseRev = $this->revisionLookup
->getRevisionByTitle( $titleObj, $editRevId );
510 $baseContentModel = null;
513 $baseContent = $baseRev->getContent( SlotRecord
::MAIN
);
514 $baseContentModel = $baseContent ?
$baseContent->getModel() : null;
517 $baseContentModel ??
= $pageObj->getContentModel();
519 // However, allow the content models to possibly differ if we are intentionally
520 // changing them or we are doing an undo edit that is reverting content model change.
521 $contentModelsCanDiffer = $params['contentmodel'] ||
isset( $undoContentModel );
523 if ( !$contentModelsCanDiffer && $contentModel !== $baseContentModel ) {
524 $this->dieWithError( [ 'apierror-contentmodel-mismatch', $contentModel, $baseContentModel ] );
527 // Do the actual save
528 $oldRevId = $articleObject->getRevIdFetched();
531 // Fake $wgRequest for some hooks inside EditPage
532 // @todo FIXME: This interface SUCKS
533 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
534 $oldRequest = $wgRequest;
537 $status = $ep->attemptSave( $result );
538 $statusValue = is_int( $status->value
) ?
$status->value
: 0;
539 $wgRequest = $oldRequest;
540 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
543 switch ( $statusValue ) {
544 case EditPage
::AS_HOOK_ERROR
:
545 case EditPage
::AS_HOOK_ERROR_EXPECTED
:
546 if ( $status->statusData
!== null ) {
547 $r = $status->statusData
;
548 $r['result'] = 'Failure';
549 $apiResult->addValue( null, $this->getModuleName(), $r );
552 if ( !$status->getMessages() ) {
553 // This appears to be unreachable right now, because all
554 // code paths will set an error. Could change, though.
555 $status->fatal( 'hookaborted' ); // @codeCoverageIgnore
557 $this->dieStatus( $status );
559 // These two cases will normally have been caught earlier, and will
560 // only occur if something blocks the user between the earlier
561 // check and the check in EditPage (presumably a hook). It's not
562 // obvious that this is even possible.
563 // @codeCoverageIgnoreStart
564 case EditPage
::AS_BLOCKED_PAGE_FOR_USER
:
565 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable Block is checked and not null
566 $this->dieBlocked( $user->getBlock() );
567 // dieBlocked prevents continuation
569 case EditPage
::AS_READ_ONLY_PAGE
:
570 $this->dieReadOnly();
571 // @codeCoverageIgnoreEnd
573 case EditPage
::AS_SUCCESS_NEW_ARTICLE
:
577 case EditPage
::AS_SUCCESS_UPDATE
:
578 $r['result'] = 'Success';
579 $r['pageid'] = (int)$titleObj->getArticleID();
580 $r['title'] = $titleObj->getPrefixedText();
581 $r['contentmodel'] = $articleObject->getPage()->getContentModel();
582 $newRevId = $articleObject->getPage()->getLatest();
583 if ( $newRevId == $oldRevId ) {
584 $r['nochange'] = true;
586 $r['oldrevid'] = (int)$oldRevId;
587 $r['newrevid'] = (int)$newRevId;
588 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601
,
589 $pageObj->getTimestamp() );
593 $r['watched'] = true;
595 $watchlistExpiry = $this->getWatchlistExpiry(
596 $this->watchedItemStore
,
601 if ( $watchlistExpiry ) {
602 $r['watchlistexpiry'] = $watchlistExpiry;
605 $this->persistGlobalSession();
607 // If the temporary account was created in this request,
608 // or if the temporary account has zero edits (implying
609 // that the account was created during a failed edit
610 // attempt in a previous request), perform the top-level
611 // redirect to ensure the account is attached.
612 // Note that the temp user could already have performed
613 // the top-level redirect if this a first edit on
614 // a wiki that is not the user's home wiki.
615 $shouldRedirectForTempUser = isset( $result['savedTempUser'] ) ||
616 ( $user->isTemp() && ( $user->getEditCount() === 0 ) );
617 if ( $shouldRedirectForTempUser ) {
618 $r['tempusercreated'] = true;
619 $params['returnto'] ??
= $titleObj->getPrefixedDBkey();
620 $redirectUrl = $this->getTempUserRedirectUrl(
622 $result['savedTempUser'] ??
$user
624 if ( $redirectUrl ) {
625 $r['tempusercreatedredirect'] = $redirectUrl;
632 if ( !$status->getMessages() ) {
633 // EditPage sometimes only sets the status code without setting
634 // any actual error messages. Supply defaults for those cases.
635 switch ( $statusValue ) {
637 case EditPage
::AS_IMAGE_REDIRECT_ANON
:
638 $status->fatal( 'apierror-noimageredirect-anon' );
640 case EditPage
::AS_IMAGE_REDIRECT_LOGGED
:
641 $status->fatal( 'apierror-noimageredirect' );
643 case EditPage
::AS_CONTENT_TOO_BIG
:
644 case EditPage
::AS_MAX_ARTICLE_SIZE_EXCEEDED
:
645 $status->fatal( 'apierror-contenttoobig',
646 $this->getConfig()->get( MainConfigNames
::MaxArticleSize
) );
648 case EditPage
::AS_READ_ONLY_PAGE_ANON
:
649 $status->fatal( 'apierror-noedit-anon' );
651 case EditPage
::AS_NO_CHANGE_CONTENT_MODEL
:
652 $status->fatal( 'apierror-cantchangecontentmodel' );
654 case EditPage
::AS_ARTICLE_WAS_DELETED
:
655 $status->fatal( 'apierror-pagedeleted' );
657 case EditPage
::AS_CONFLICT_DETECTED
:
658 $status->fatal( 'edit-conflict' );
661 // Currently shouldn't be needed, but here in case
662 // hooks use them without setting appropriate
663 // errors on the status.
664 // @codeCoverageIgnoreStart
665 case EditPage
::AS_SPAM_ERROR
:
666 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset
667 $status->fatal( 'apierror-spamdetected', $result['spam'] );
669 case EditPage
::AS_READ_ONLY_PAGE_LOGGED
:
670 $status->fatal( 'apierror-noedit' );
672 case EditPage
::AS_RATE_LIMITED
:
673 $status->fatal( 'apierror-ratelimited' );
675 case EditPage
::AS_NO_CREATE_PERMISSION
:
676 $status->fatal( 'nocreate-loggedin' );
678 case EditPage
::AS_BLANK_ARTICLE
:
679 $status->fatal( 'apierror-emptypage' );
681 case EditPage
::AS_TEXTBOX_EMPTY
:
682 $status->fatal( 'apierror-emptynewsection' );
684 case EditPage
::AS_SUMMARY_NEEDED
:
685 $status->fatal( 'apierror-summaryrequired' );
688 wfWarn( __METHOD__
. ": Unknown EditPage code $statusValue with no message" );
689 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
691 // @codeCoverageIgnoreEnd
694 $this->dieStatus( $status );
696 $apiResult->addValue( null, $this->getModuleName(), $r );
699 public function mustBePosted() {
703 public function isWriteMode() {
707 public function getAllowedParams() {
710 ParamValidator
::PARAM_TYPE
=> 'string',
713 ParamValidator
::PARAM_TYPE
=> 'integer',
717 ParamValidator
::PARAM_TYPE
=> 'string',
720 ParamValidator
::PARAM_TYPE
=> 'text',
724 ParamValidator
::PARAM_TYPE
=> 'tags',
725 ParamValidator
::PARAM_ISMULTI
=> true,
731 ParamValidator
::PARAM_TYPE
=> 'integer',
734 ParamValidator
::PARAM_TYPE
=> 'timestamp',
736 'starttimestamp' => [
737 ParamValidator
::PARAM_TYPE
=> 'timestamp',
740 'createonly' => false,
743 ParamValidator
::PARAM_DEFAULT
=> false,
744 ParamValidator
::PARAM_DEPRECATED
=> true,
747 ParamValidator
::PARAM_DEFAULT
=> false,
748 ParamValidator
::PARAM_DEPRECATED
=> true,
752 // Params appear in the docs in the order they are defined,
753 // which is why this is here and not at the bottom.
754 $params +
= $this->getWatchlistParams();
759 ParamValidator
::PARAM_TYPE
=> 'text',
762 ParamValidator
::PARAM_TYPE
=> 'text',
765 ParamValidator
::PARAM_TYPE
=> 'integer',
766 IntegerDef
::PARAM_MIN
=> 0,
767 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
770 ParamValidator
::PARAM_TYPE
=> 'integer',
771 IntegerDef
::PARAM_MIN
=> 0,
772 ApiBase
::PARAM_RANGE_ENFORCE
=> true,
775 ParamValidator
::PARAM_TYPE
=> 'boolean',
776 ParamValidator
::PARAM_DEFAULT
=> false,
779 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getAllContentFormats(),
782 ParamValidator
::PARAM_TYPE
=> $this->contentHandlerFactory
->getContentModels(),
785 // Standard definition automatically inserted
786 ApiBase
::PARAM_HELP_MSG_APPEND
=> [ 'apihelp-edit-param-token' ],
790 $params +
= $this->getCreateTempUserParams();
795 public function needsToken() {
799 protected function getExamplesMessages() {
801 'action=edit&title=Test&summary=test%20summary&' .
802 'text=article%20content&baserevid=1234567&token=123ABC'
803 => 'apihelp-edit-example-edit',
804 'action=edit&title=Test&summary=NOTOC&minor=&' .
805 'prependtext=__NOTOC__%0A&basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
806 => 'apihelp-edit-example-prepend',
807 'action=edit&title=Test&undo=13585&undoafter=13579&' .
808 'basetimestamp=2007-08-24T12:34:54Z&token=123ABC'
809 => 'apihelp-edit-example-undo',
813 public function getHelpUrls() {
814 return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Edit';
818 /** @deprecated class alias since 1.43 */
819 class_alias( ApiEditPage
::class, 'ApiEditPage' );