contentHandlerFactory = $contentHandlerFactory; $this->hookRunner = new HookRunner( $hookContainer ); $this->revLookup = $revLookup; $this->userFactory = $userFactory; $this->logFormatterFactory = $logFormatterFactory; $this->performer = $performer; $this->page = $wikiPageFactory->newFromTitle( $page ); $this->pageIdentity = $page; $this->newModel = $newModel; // SpecialChangeContentModel doesn't support tags // api can specify tags via ::setTags, which also checks if user can add // the tags specified $this->tags = []; // Requires createNewContent to be called first $this->logAction = ''; // Defaults to nothing, for special page $this->msgPrefix = ''; } /** * @param string $msgPrefix */ public function setMessagePrefix( $msgPrefix ) { $this->msgPrefix = $msgPrefix; } /** * @param callable $authorizer ( string $action, PageIdentity $target, PermissionStatus $status ) * * @return PermissionStatus */ private function authorizeInternal( callable $authorizer ): PermissionStatus { $current = $this->page->getTitle(); $titleWithNewContentModel = clone $current; $titleWithNewContentModel->setContentModel( $this->newModel ); $status = PermissionStatus::newEmpty(); $authorizer( 'editcontentmodel', $this->pageIdentity, $status ); $authorizer( 'edit', $this->pageIdentity, $status ); $authorizer( 'editcontentmodel', $titleWithNewContentModel, $status ); $authorizer( 'edit', $titleWithNewContentModel, $status ); return $status; } /** * Check whether $performer can execute the content model change. * * @note this method does not guarantee full permissions check, so it should * only be used to to decide whether to show a content model change form. * To authorize the content model change action use {@link self::authorizeChange} instead. * * @return PermissionStatus */ public function probablyCanChange(): PermissionStatus { return $this->authorizeInternal( function ( string $action, PageIdentity $target, PermissionStatus $status ) { return $this->performer->probablyCan( $action, $target, $status ); } ); } /** * Authorize the content model change by $performer. * * @note this method should be used right before the actual content model change is performed. * To check whether a current performer has the potential to change the content model of the page, * use {@link self::probablyCanChange} instead. * * @return PermissionStatus */ public function authorizeChange(): PermissionStatus { return $this->authorizeInternal( function ( string $action, PageIdentity $target, PermissionStatus $status ) { return $this->performer->authorizeWrite( $action, $target, $status ); } ); } /** * Check user can edit and editcontentmodel before and after * * @deprecated since 1.36. Use ::probablyCanChange or ::authorizeChange instead. * @return array Errors in legacy error array format */ public function checkPermissions() { wfDeprecated( __METHOD__, '1.36' ); $status = $this->authorizeInternal( function ( string $action, PageIdentity $target, PermissionStatus $status ) { return $this->performer->definitelyCan( $action, $target, $status ); } ); return $status->toLegacyErrorArray(); } /** * Specify the tags the user wants to add, and check permissions * * @param string[] $tags * * @return Status */ public function setTags( $tags ) { $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->performer ); if ( $tagStatus->isOK() ) { $this->tags = $tags; return Status::newGood(); } else { return $tagStatus; } } /** * @return Status */ private function createNewContent() { $contentHandlerFactory = $this->contentHandlerFactory; $title = $this->page->getTitle(); $latestRevRecord = $this->revLookup->getRevisionByTitle( $this->pageIdentity ); if ( $latestRevRecord ) { $latestContent = $latestRevRecord->getContent( SlotRecord::MAIN ); $latestHandler = $latestContent->getContentHandler(); $latestModel = $latestContent->getModel(); if ( !$latestHandler->supportsDirectEditing() ) { // Only reachable via api return Status::newFatal( 'apierror-changecontentmodel-nodirectediting', ContentHandler::getLocalizedName( $latestModel ) ); } $newModel = $this->newModel; if ( $newModel === $latestModel ) { // Only reachable via api return Status::newFatal( 'apierror-nochanges' ); } $newHandler = $contentHandlerFactory->getContentHandler( $newModel ); if ( !$newHandler->canBeUsedOn( $title ) ) { // Only reachable via api return Status::newFatal( 'apierror-changecontentmodel-cannotbeused', ContentHandler::getLocalizedName( $newModel ), Message::plaintextParam( $title->getPrefixedText() ) ); } try { $newContent = $newHandler->unserializeContent( $latestContent->serialize() ); } catch ( MWException $e ) { // Messages: changecontentmodel-cannot-convert, // apierror-changecontentmodel-cannot-convert return Status::newFatal( $this->msgPrefix . 'changecontentmodel-cannot-convert', Message::plaintextParam( $title->getPrefixedText() ), ContentHandler::getLocalizedName( $newModel ) ); } $this->latestRevId = $latestRevRecord->getId(); $this->logAction = 'change'; } else { // Page doesn't exist, create an empty content object $newContent = $contentHandlerFactory ->getContentHandler( $this->newModel ) ->makeEmptyContent(); $this->latestRevId = false; $this->logAction = 'new'; } $this->newContent = $newContent; return Status::newGood(); } /** * Handle change and logging after validation * * Can still be intercepted by hooks * * @param IContextSource $context * @param string $comment * @param bool $bot Mark as a bot edit if the user can * * @return Status */ public function doContentModelChange( IContextSource $context, string $comment, $bot ) { $status = $this->createNewContent(); if ( !$status->isGood() ) { return $status; } $page = $this->page; $title = $page->getTitle(); $user = $this->userFactory->newFromAuthority( $this->performer ); // Create log entry $log = new ManualLogEntry( 'contentmodel', $this->logAction ); $log->setPerformer( $this->performer->getUser() ); $log->setTarget( $title ); $log->setComment( $comment ); $log->setParameters( [ '4::oldmodel' => $title->getContentModel(), '5::newmodel' => $this->newModel ] ); $log->addTags( $this->tags ); $formatter = $this->logFormatterFactory->newFromEntry( $log ); $formatter->setContext( RequestContext::newExtraneousContext( $title ) ); $reason = $formatter->getPlainActionText(); if ( $comment !== '' ) { $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment; } // Run edit filters $derivativeContext = new DerivativeContext( $context ); $derivativeContext->setTitle( $title ); $derivativeContext->setWikiPage( $page ); $status = new Status(); $newContent = $this->newContent; if ( !$this->hookRunner->onEditFilterMergedContent( $derivativeContext, $newContent, $status, $reason, $user, false ) ) { if ( $status->isGood() ) { // TODO: extensions should really specify an error message $status->fatal( 'hookaborted' ); } return $status; } if ( !$status->isOK() ) { if ( !$status->getMessages() ) { $status->fatal( 'hookaborted' ); } return $status; } // Make the edit $flags = $this->latestRevId ? EDIT_UPDATE : EDIT_NEW; $flags |= EDIT_INTERNAL; if ( $bot && $this->performer->isAllowed( 'bot' ) ) { $flags |= EDIT_FORCE_BOT; } $status = $page->doUserEditContent( $newContent, $this->performer, $reason, $flags, $this->latestRevId, $this->tags ); if ( !$status->isOK() ) { return $status; } $logid = $log->insert(); $log->publish( $logid ); $values = [ 'logid' => $logid ]; return Status::newGood( $values ); } } /** @deprecated class alias since 1.43 */ class_alias( ContentModelChange::class, 'ContentModelChange' );