config = $config; $this->contentHandlerFactory = $contentHandlerFactory; $this->titleParser = $titleParser; $this->titleFormatter = $titleFormatter; $this->revisionLookup = $revisionLookup; } public function needsWriteAccess() { return true; } /** * Returns the requested title. * * @return string */ abstract protected function getTitleParameter(); /** * @inheritDoc */ public function validate( Validator $restValidator ) { parent::validate( $restValidator ); $this->validateToken( true ); } /** * @inheritDoc */ protected function mapActionModuleResult( array $data ) { if ( isset( $data['error'] ) ) { throw new LocalizedHttpException( new MessageValue( 'apierror-' . $data['error'] ), 400 ); } if ( !isset( $data['edit'] ) || !$data['edit']['result'] ) { throw new RuntimeException( 'Bad result structure received from ApiEditPage' ); } if ( $data['edit']['result'] !== 'Success' ) { // Probably an edit conflict // TODO: which code for null edits? throw new LocalizedHttpException( new MessageValue( "rest-edit-conflict", [ $data['edit']['result'] ] ), 409 ); } $title = $this->titleParser->parseTitle( $data['edit']['title'] ); // This seems wasteful. This is the downside of delegating to the action API module: // if we need additional data in the response, we have to load it. $revision = $this->revisionLookup->getRevisionById( (int)$data['edit']['newrevid'] ); $content = $revision->getContent( SlotRecord::MAIN ); return [ 'id' => $data['edit']['pageid'], 'title' => $this->titleFormatter->getPrefixedText( $title ), 'key' => $this->titleFormatter->getPrefixedDBkey( $title ), 'latest' => [ 'id' => $data['edit']['newrevid'], 'timestamp' => $data['edit']['newtimestamp'], ], 'license' => [ 'url' => $this->config->get( MainConfigNames::RightsUrl ), 'title' => $this->config->get( MainConfigNames::RightsText ) ], 'content_model' => $data['edit']['contentmodel'], 'source' => $content->serialize(), ]; } /** * @inheritDoc */ protected function throwHttpExceptionForActionModuleError( IApiMessage $msg, $statusCode = 400 ) { $code = $msg->getApiCode(); if ( $code === 'protectedpage' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 403 ); } if ( $code === 'badtoken' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 403 ); } if ( $code === 'missingtitle' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 404 ); } if ( $code === 'articleexists' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 409 ); } if ( $code === 'editconflict' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 409 ); } if ( $code === 'ratelimited' ) { throw new LocalizedHttpException( MessageValue::newFromSpecifier( $msg ), 429 ); } // Fall through to generic handling of the error (status 400). parent::throwHttpExceptionForActionModuleError( $msg, $statusCode ); } protected function mapActionModuleResponse( WebResponse $actionModuleResponse, array $actionModuleResult, Response $response ) { parent::mapActionModuleResponse( $actionModuleResponse, $actionModuleResult, $response ); if ( $actionModuleResult['edit']['new'] ?? false ) { $response->setStatus( 201 ); } } protected function generateResponseSpec( string $method ): array { $spec = parent::generateResponseSpec( $method ); $spec['201'][parent::OPENAPI_DESCRIPTION_KEY] = 'OK'; $spec['201']['content']['application/json']['schema'] = $spec['200']['content']['application/json']['schema']; $spec['403'] = [ '$ref' => '#/components/responses/GenericErrorResponse' ]; $spec['409'] = [ '$ref' => '#/components/responses/GenericErrorResponse' ]; return $spec; } }