contentHandlerFactory = $contentHandlerFactory; $this->pageEditStash = $pageEditStash; $this->revisionLookup = $revisionLookup; $this->stats = $statsFactory; $this->wikiPageFactory = $wikiPageFactory; $this->tempUserCreator = $tempUserCreator; $this->userFactory = $userFactory; } public function execute() { $user = $this->getUser(); $params = $this->extractRequestParams(); if ( $user->isBot() ) { $this->dieWithError( 'apierror-botsnotsupported' ); } $page = $this->getTitleOrPageId( $params ); $title = $page->getTitle(); $this->getErrorFormatter()->setContextTitle( $title ); if ( !$this->contentHandlerFactory ->getContentHandler( $params['contentmodel'] ) ->isSupportedFormat( $params['contentformat'] ) ) { $this->dieWithError( [ 'apierror-badformat-generic', $params['contentformat'], $params['contentmodel'] ], 'badmodelformat' ); } $this->requireOnlyOneParameter( $params, 'stashedtexthash', 'text' ); if ( $params['stashedtexthash'] !== null ) { // Load from cache since the client indicates the text is the same as last stash $textHash = $params['stashedtexthash']; if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) { $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' ); } $text = $this->pageEditStash->fetchInputText( $textHash ); if ( !is_string( $text ) ) { $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' ); } } else { // 'text' was passed. Trim and fix newlines so the key SHA1's // match (see WebRequest::getText()) $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) ); $textHash = sha1( $text ); } $textContent = $this->contentHandlerFactory ->getContentHandler( $params['contentmodel'] ) ->unserializeContent( $text, $params['contentformat'] ); $page = $this->wikiPageFactory->newFromTitle( $title ); if ( $page->exists() ) { // Page exists: get the merged content with the proposed change $baseRev = $this->revisionLookup->getRevisionByPageId( $page->getId(), $params['baserevid'] ); if ( !$baseRev ) { $this->dieWithError( [ 'apierror-nosuchrevid', $params['baserevid'] ] ); } $currentRev = $page->getRevisionRecord(); if ( !$currentRev ) { $this->dieWithError( [ 'apierror-missingrev-pageid', $page->getId() ], 'missingrev' ); } // Merge in the new version of the section to get the proposed version $editContent = $page->replaceSectionAtRev( $params['section'], $textContent, $params['sectiontitle'], $baseRev->getId() ); if ( !$editContent ) { $this->dieWithError( 'apierror-sectionreplacefailed', 'replacefailed' ); } if ( $currentRev->getId() == $baseRev->getId() ) { // Base revision was still the latest; nothing to merge $content = $editContent; } else { // Merge the edit into the current version $baseContent = $baseRev->getContent( SlotRecord::MAIN ); $currentContent = $currentRev->getContent( SlotRecord::MAIN ); if ( !$baseContent || !$currentContent ) { $this->dieWithError( [ 'apierror-missingcontent-pageid', $page->getId() ], 'missingrev' ); } $baseModel = $baseContent->getModel(); $currentModel = $currentContent->getModel(); // T255700: Put this in try-block because if the models of these three Contents // happen to not be identical, the ContentHandler may throw exception here. try { $content = $this->contentHandlerFactory ->getContentHandler( $baseModel ) ->merge3( $baseContent, $editContent, $currentContent ); } catch ( Exception $e ) { $this->dieWithException( $e, [ 'wrap' => ApiMessage::create( [ 'apierror-contentmodel-mismatch', $currentModel, $baseModel ] ) ] ); } } } else { // New pages: use the user-provided content model $content = $textContent; } if ( !$content ) { // merge3() failed $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => 'editconflict' ] ); return; } if ( !$user->authorizeWrite( 'stashedit', $title ) ) { $status = 'ratelimited'; } else { $user = $this->getUserForPreview(); $updater = $page->newPageUpdater( $user ); $status = $this->pageEditStash->parseAndCache( $updater, $content, $user, $params['summary'] ); $this->pageEditStash->stashInputText( $text, $textHash ); } $this->stats->getCounter( 'editstash_cache_stores_total' ) ->setLabel( 'status', $status ) ->copyToStatsdAt( "editstash.cache_stores.$status" ) ->increment(); $ret = [ 'status' => $status ]; // If we were rate-limited, we still return the pre-existing valid hash if one was passed if ( $status !== 'ratelimited' || $params['stashedtexthash'] !== null ) { $ret['texthash'] = $textHash; } $this->getResult()->addValue( null, $this->getModuleName(), $ret ); } private function getUserForPreview(): UserIdentity { $user = $this->getUser(); if ( $this->tempUserCreator->shouldAutoCreate( $user, 'edit' ) ) { return $this->userFactory->newUnsavedTempUser( $this->tempUserCreator->getStashedName( $this->getRequest()->getSession() ) ); } return $user; } public function getAllowedParams() { return [ 'title' => [ ParamValidator::PARAM_TYPE => 'string', ParamValidator::PARAM_REQUIRED => true ], 'section' => [ ParamValidator::PARAM_TYPE => 'string', ], 'sectiontitle' => [ ParamValidator::PARAM_TYPE => 'string' ], 'text' => [ ParamValidator::PARAM_TYPE => 'text', ParamValidator::PARAM_DEFAULT => null ], 'stashedtexthash' => [ ParamValidator::PARAM_TYPE => 'string', ParamValidator::PARAM_DEFAULT => null ], 'summary' => [ ParamValidator::PARAM_TYPE => 'string', ParamValidator::PARAM_DEFAULT => '' ], 'contentmodel' => [ ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getContentModels(), ParamValidator::PARAM_REQUIRED => true ], 'contentformat' => [ ParamValidator::PARAM_TYPE => $this->contentHandlerFactory->getAllContentFormats(), ParamValidator::PARAM_REQUIRED => true ], 'baserevid' => [ ParamValidator::PARAM_TYPE => 'integer', ParamValidator::PARAM_REQUIRED => true ] ]; } public function needsToken() { return 'csrf'; } public function mustBePosted() { return true; } public function isWriteMode() { return true; } public function isInternal() { return true; } public function getHelpUrls() { return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Stashedit'; } } /** @deprecated class alias since 1.43 */ class_alias( ApiStashEdit::class, 'ApiStashEdit' );