Merge "Skin: Remove deprecated skin methods"
[mediawiki.git] / includes / api / ApiEditPage.php
blob73c70bde309dae3550b8982103b90b2130991146
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 /**
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(
101 ApiMain $mainModule,
102 $moduleName,
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
135 * @return User
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() )
144 return $user;
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 );
172 $redirValues = [
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();
180 $this->dieWithError(
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'] );
204 } else {
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(
232 $titleObj,
233 'edit',
234 [ 'autoblock' => true, 'user' => $this->getUserForPermissions() ]
237 $toMD5 = $params['text'];
238 if ( $params['appendtext'] !== null || $params['prependtext'] !== null ) {
239 $content = $pageObj->getContent();
241 if ( !$content ) {
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 ) {
247 $text = '';
250 try {
251 $content = ContentHandler::makeContent( $text, $titleObj );
252 } catch ( MWContentSerializationException $ex ) {
253 $this->dieWithException( $ex, [
254 'wrap' => ApiMessage::create( 'apierror-contentserializationexception', 'parseerror' )
255 ] );
257 } else {
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.
276 $content = null;
277 } else {
278 // Process the content for section edits
279 $section = $params['section'];
280 $content = $content->getSection( $section );
282 if ( !$content ) {
283 $this->dieWithError( [ 'apierror-nosuchsection', wfEscapeWikiText( $section ) ] );
288 if ( !$content ) {
289 $text = '';
290 } else {
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'] );
306 } else {
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
341 // parameter.
342 if ( !$newContent->isSupportedFormat( $contentFormat ) ) {
343 $undoafterRevMainSlot = $undoafterRev->getSlot(
344 SlotRecord::MAIN,
345 RevisionRecord::RAW
347 $contentFormat = $undoafterRevMainSlot->getFormat();
348 if ( !$contentFormat ) {
349 // fall back to default content format for the model
350 // of $undoafterRev
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
382 $requestArray = [
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'];
428 } else {
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'
449 && $section != 'new'
450 && ( !$content || !$content->getSection( $section ) )
452 $this->dieWithError( [ 'apierror-nosuchsection', $section ] );
454 $requestArray['wpSection'] = $params['section'];
455 } else {
456 $requestArray['wpSection'] = '';
459 $watch = $this->getWatchlistValue( $params['watchlist'], $titleObj, $user );
461 // Deprecated parameters
462 if ( $params['watch'] ) {
463 $watch = true;
464 } elseif ( $params['unwatch'] ) {
465 $watch = false;
468 if ( $watch ) {
469 $requestArray['wpWatchthis'] = true;
470 $watchlistExpiry = $this->getExpiryFromParams( $params );
472 if ( $watchlistExpiry ) {
473 $requestArray['wpWatchlistExpiry'] = $watchlistExpiry;
477 // Apply change tags
478 if ( $params['tags'] ) {
479 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $params['tags'], $this->getAuthority() );
480 if ( $tagStatus->isOK() ) {
481 $requestArray['wpChangeTags'] = implode( ',', $params['tags'] );
482 } else {
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;
524 if ( $baseRev ) {
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();
541 $result = null;
543 // Fake $wgRequest for some hooks inside EditPage
544 // @todo FIXME: This interface SUCKS
545 // phpcs:disable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
546 $oldRequest = $wgRequest;
547 $wgRequest = $req;
549 $status = $ep->attemptSave( $result );
550 $statusValue = is_int( $status->value ) ? $status->value : 0;
551 $wgRequest = $oldRequest;
552 // phpcs:enable MediaWiki.Usage.ExtendClassUsage.FunctionVarUsage
554 $r = [];
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 );
562 return;
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:
586 $r['new'] = true;
587 // fall-through
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;
597 } else {
598 $r['oldrevid'] = (int)$oldRevId;
599 $r['newrevid'] = (int)$newRevId;
600 $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
601 $pageObj->getTimestamp() );
604 if ( $watch ) {
605 $r['watched'] = true;
607 $watchlistExpiry = $this->getWatchlistExpiry(
608 $this->watchedItemStore,
609 $titleObj,
610 $user
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(
633 $params,
634 $result['savedTempUser'] ?? $user
636 if ( $redirectUrl ) {
637 $r['tempusercreatedredirect'] = $redirectUrl;
641 break;
643 default:
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 ) {
648 // Currently needed
649 case EditPage::AS_IMAGE_REDIRECT_ANON:
650 $status->fatal( 'apierror-noimageredirect-anon' );
651 break;
652 case EditPage::AS_IMAGE_REDIRECT_LOGGED:
653 $status->fatal( 'apierror-noimageredirect' );
654 break;
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 ) );
659 break;
660 case EditPage::AS_READ_ONLY_PAGE_ANON:
661 $status->fatal( 'apierror-noedit-anon' );
662 break;
663 case EditPage::AS_NO_CHANGE_CONTENT_MODEL:
664 $status->fatal( 'apierror-cantchangecontentmodel' );
665 break;
666 case EditPage::AS_ARTICLE_WAS_DELETED:
667 $status->fatal( 'apierror-pagedeleted' );
668 break;
669 case EditPage::AS_CONFLICT_DETECTED:
670 $status->fatal( 'edit-conflict' );
671 break;
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'] );
680 break;
681 case EditPage::AS_READ_ONLY_PAGE_LOGGED:
682 $status->fatal( 'apierror-noedit' );
683 break;
684 case EditPage::AS_RATE_LIMITED:
685 $status->fatal( 'apierror-ratelimited' );
686 break;
687 case EditPage::AS_NO_CREATE_PERMISSION:
688 $status->fatal( 'nocreate-loggedin' );
689 break;
690 case EditPage::AS_BLANK_ARTICLE:
691 $status->fatal( 'apierror-emptypage' );
692 break;
693 case EditPage::AS_TEXTBOX_EMPTY:
694 $status->fatal( 'apierror-emptynewsection' );
695 break;
696 case EditPage::AS_SUMMARY_NEEDED:
697 $status->fatal( 'apierror-summaryrequired' );
698 break;
699 default:
700 wfWarn( __METHOD__ . ": Unknown EditPage code $statusValue with no message" );
701 $status->fatal( 'apierror-unknownerror-editpage', $statusValue );
702 break;
703 // @codeCoverageIgnoreEnd
706 $this->dieStatus( $status );
708 $apiResult->addValue( null, $this->getModuleName(), $r );
711 public function mustBePosted() {
712 return true;
715 public function isWriteMode() {
716 return true;
719 public function getAllowedParams() {
720 $params = [
721 'title' => [
722 ParamValidator::PARAM_TYPE => 'string',
724 'pageid' => [
725 ParamValidator::PARAM_TYPE => 'integer',
727 'section' => null,
728 'sectiontitle' => [
729 ParamValidator::PARAM_TYPE => 'string',
731 'text' => [
732 ParamValidator::PARAM_TYPE => 'text',
734 'summary' => null,
735 'tags' => [
736 ParamValidator::PARAM_TYPE => 'tags',
737 ParamValidator::PARAM_ISMULTI => true,
739 'minor' => false,
740 'notminor' => false,
741 'bot' => false,
742 'baserevid' => [
743 ParamValidator::PARAM_TYPE => 'integer',
745 'basetimestamp' => [
746 ParamValidator::PARAM_TYPE => 'timestamp',
748 'starttimestamp' => [
749 ParamValidator::PARAM_TYPE => 'timestamp',
751 'recreate' => false,
752 'createonly' => false,
753 'nocreate' => false,
754 'watch' => [
755 ParamValidator::PARAM_DEFAULT => false,
756 ParamValidator::PARAM_DEPRECATED => true,
758 'unwatch' => [
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();
768 $params += [
769 'md5' => null,
770 'prependtext' => [
771 ParamValidator::PARAM_TYPE => 'text',
773 'appendtext' => [
774 ParamValidator::PARAM_TYPE => 'text',
776 'undo' => [
777 ParamValidator::PARAM_TYPE => 'integer',
778 IntegerDef::PARAM_MIN => 0,
779 ApiBase::PARAM_RANGE_ENFORCE => true,
781 'undoafter' => [
782 ParamValidator::PARAM_TYPE => 'integer',
783 IntegerDef::PARAM_MIN => 0,
784 ApiBase::PARAM_RANGE_ENFORCE => true,
786 'redirect' => [
787 ParamValidator::PARAM_TYPE => 'boolean',
788 ParamValidator::PARAM_DEFAULT => false,
790 'contentformat' => [
791 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(),
793 'contentmodel' => [
794 ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(),
796 'token' => [
797 // Standard definition automatically inserted
798 ApiBase::PARAM_HELP_MSG_APPEND => [ 'apihelp-edit-param-token' ],
802 $params += $this->getCreateTempUserParams();
804 return $params;
807 public function needsToken() {
808 return 'csrf';
811 protected function getExamplesMessages() {
812 return [
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' );