Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / api / ApiEditPage.php
blob45dd432019b8e0a8a6f31d472ef3d731a1eb6c30
1 <?php
2 /**
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
20 * @file
23 namespace MediaWiki\Api;
25 use Article;
26 use ChangeTags;
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;
52 /**
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
64 * class.
66 * @ingroup API
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;
80 /**
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(
88 ApiMain $mainModule,
89 string $moduleName,
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
99 ) {
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
122 * @return User
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() )
131 return $user;
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 );
159 $redirValues = [
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();
167 $this->dieWithError(
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'] );
191 } else {
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(
219 $titleObj,
220 'edit',
221 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
224 $toMD5 = $params['text'];
225 if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
226 $content = $pageObj->getContent();
228 if ( !$content ) {
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 ) {
234 $text = '';
237 try {
238 $content = ContentHandler::makeContent( $text, $titleObj );
239 } catch ( MWContentSerializationException $ex ) {
240 $this->dieWithException( $ex, [
241 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
242 ] );
244 } else {
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.
263 $content = null;
264 } else {
265 // Process the content for section edits
266 $section = $params['section'];
267 $content = $content->getSection( $section );
269 if ( !$content ) {
270 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
275 if ( !$content ) {
276 $text = '';
277 } else {
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'] );
293 } else {
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
328 // parameter.
329 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
330 $undoafterRevMainSlot = $undoafterRev->getSlot(
331 SlotRecord::MAIN,
332 RevisionRecord::RAW
334 $contentFormat = $undoafterRevMainSlot->getFormat();
335 if ( !$contentFormat ) {
336 // fall back to default content format for the model
337 // of $undoafterRev
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
369 $requestArray = [
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'];
416 } else {
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'
437 && $section != 'new'
438 && ( !$content || !$content->getSection( $section ) )
440 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
442 $requestArray['wpSection'] = $params['section'];
443 } else {
444 $requestArray['wpSection'] = '';
447 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
449 // Deprecated parameters
450 if ( $params['watch'] ) {
451 $watch = true;
452 } elseif ( $params['unwatch'] ) {
453 $watch = false;
456 if ( $watch ) {
457 $requestArray['wpWatchthis'] = true;
458 $watchlistExpiry = $this->getExpiryFromParams( $params );
460 if ( $watchlistExpiry ) {
461 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
465 // Apply change tags
466 if ( $params['tags'] ) {
467 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
468 if ( $tagStatus->isOK() ) {
469 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
470 } else {
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;
512 if ( $baseRev ) {
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();
529 $result = null;
531 // Fake $wgRequest for some hooks inside EditPage
532 // @todo FIXME: This interface SUCKS
533 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
534 $oldRequest = $wgRequest;
535 $wgRequest = $req;
537 $status = $ep->attemptSave( $result );
538 $statusValue = is_int( $status->value ) ? $status->value : 0;
539 $wgRequest = $oldRequest;
540 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
542 $r = [];
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 );
550 return;
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:
574 $r['new'] = true;
575 // fall-through
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;
585 } else {
586 $r['oldrevid'] = (int)$oldRevId;
587 $r['newrevid'] = (int)$newRevId;
588 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
589 $pageObj->getTimestamp() );
592 if ( $watch ) {
593 $r['watched'] = true;
595 $watchlistExpiry = $this->getWatchlistExpiry(
596 $this->watchedItemStore,
597 $titleObj,
598 $user
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(
621 $params,
622 $result['savedTempUser'] ?? $user
624 if ( $redirectUrl ) {
625 $r['tempusercreatedredirect'] = $redirectUrl;
629 break;
631 default:
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 ) {
636 // Currently needed
637 case EditPage::AS_IMAGE_REDIRECT_ANON:
638 $status->fatal( 'apierror-noimageredirect-anon' );
639 break;
640 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
641 $status->fatal( 'apierror-noimageredirect' );
642 break;
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 ) );
647 break;
648 case EditPage::AS_READ_ONLY_PAGE_ANON:
649 $status->fatal( 'apierror-noedit-anon' );
650 break;
651 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
652 $status->fatal( 'apierror-cantchangecontentmodel' );
653 break;
654 case EditPage::AS_ARTICLE_WAS_DELETED:
655 $status->fatal( 'apierror-pagedeleted' );
656 break;
657 case EditPage::AS_CONFLICT_DETECTED:
658 $status->fatal( 'edit-conflict' );
659 break;
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'] );
668 break;
669 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
670 $status->fatal( 'apierror-noedit' );
671 break;
672 case EditPage::AS_RATE_LIMITED:
673 $status->fatal( 'apierror-ratelimited' );
674 break;
675 case EditPage::AS_NO_CREATE_PERMISSION:
676 $status->fatal( 'nocreate-loggedin' );
677 break;
678 case EditPage::AS_BLANK_ARTICLE:
679 $status->fatal( 'apierror-emptypage' );
680 break;
681 case EditPage::AS_TEXTBOX_EMPTY:
682 $status->fatal( 'apierror-emptynewsection' );
683 break;
684 case EditPage::AS_SUMMARY_NEEDED:
685 $status->fatal( 'apierror-summaryrequired' );
686 break;
687 default:
688 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
689 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
690 break;
691 // @codeCoverageIgnoreEnd
694 $this->dieStatus( $status );
696 $apiResult->addValue( null, $this->getModuleName(), $r );
699 public function mustBePosted() {
700 return true;
703 public function isWriteMode() {
704 return true;
707 public function getAllowedParams() {
708 $params = [
709 'title' => [
710 ParamValidator::PARAM_TYPE => 'string',
712 'pageid' => [
713 ParamValidator::PARAM_TYPE => 'integer',
715 'section' => null,
716 'sectiontitle' => [
717 ParamValidator::PARAM_TYPE => 'string',
719 'text' => [
720 ParamValidator::PARAM_TYPE => 'text',
722 'summary' => null,
723 'tags' => [
724 ParamValidator::PARAM_TYPE => 'tags',
725 ParamValidator::PARAM_ISMULTI => true,
727 'minor' => false,
728 'notminor' => false,
729 'bot' => false,
730 'baserevid' => [
731 ParamValidator::PARAM_TYPE => 'integer',
733 'basetimestamp' => [
734 ParamValidator::PARAM_TYPE => 'timestamp',
736 'starttimestamp' => [
737 ParamValidator::PARAM_TYPE => 'timestamp',
739 'recreate' => false,
740 'createonly' => false,
741 'nocreate' => false,
742 'watch' => [
743 ParamValidator::PARAM_DEFAULT => false,
744 ParamValidator::PARAM_DEPRECATED => true,
746 'unwatch' => [
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();
756 $params += [
757 'md5' => null,
758 'prependtext' => [
759 ParamValidator::PARAM_TYPE => 'text',
761 'appendtext' => [
762 ParamValidator::PARAM_TYPE => 'text',
764 'undo' => [
765 ParamValidator::PARAM_TYPE => 'integer',
766 IntegerDef::PARAM_MIN => 0,
767 ApiBase::PARAM_RANGE_ENFORCE => true,
769 'undoafter' => [
770 ParamValidator::PARAM_TYPE => 'integer',
771 IntegerDef::PARAM_MIN => 0,
772 ApiBase::PARAM_RANGE_ENFORCE => true,
774 'redirect' => [
775 ParamValidator::PARAM_TYPE => 'boolean',
776 ParamValidator::PARAM_DEFAULT => false,
778 'contentformat' => [
779 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
781 'contentmodel' => [
782 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
784 'token' => [
785 // Standard definition automatically inserted
786 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
790 $params += $this->getCreateTempUserParams();
792 return $params;
795 public function needsToken() {
796 return 'csrf';
799 protected function getExamplesMessages() {
800 return [
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' );