3 namespace MediaWiki\Content
;
6 use LogFormatterFactory
;
8 use MediaWiki\Context\DerivativeContext
;
9 use MediaWiki\Context\IContextSource
;
10 use MediaWiki\Context\RequestContext
;
11 use MediaWiki\HookContainer\HookContainer
;
12 use MediaWiki\HookContainer\HookRunner
;
13 use MediaWiki\Message\Message
;
14 use MediaWiki\Page\PageIdentity
;
15 use MediaWiki\Page\WikiPageFactory
;
16 use MediaWiki\Permissions\Authority
;
17 use MediaWiki\Permissions\PermissionStatus
;
18 use MediaWiki\Revision\RevisionLookup
;
19 use MediaWiki\Revision\SlotRecord
;
20 use MediaWiki\Status\Status
;
21 use MediaWiki\User\UserFactory
;
26 * Backend logic for changing the content model of a page.
28 * Note that you can create a new page directly with a desired content
29 * model and format, e.g. via EditPage or externally from ApiEditPage.
34 class ContentModelChange
{
35 /** @var IContentHandlerFactory */
36 private $contentHandlerFactory;
37 /** @var HookRunner */
39 /** @var RevisionLookup */
41 /** @var UserFactory */
43 private LogFormatterFactory
$logFormatterFactory;
44 /** @var Authority making the change */
48 /** @var PageIdentity */
49 private $pageIdentity;
52 /** @var string[] tags to add */
56 /** @var int|false latest revision id, or false if creating */
58 /** @var string 'new' or 'change' */
60 /** @var string 'apierror-' or empty string, for status messages */
64 * @internal Create via the ContentModelChangeFactory service.
66 * @param IContentHandlerFactory $contentHandlerFactory
67 * @param HookContainer $hookContainer
68 * @param RevisionLookup $revLookup
69 * @param UserFactory $userFactory
70 * @param WikiPageFactory $wikiPageFactory
71 * @param LogFormatterFactory $logFormatterFactory
72 * @param Authority $performer
73 * @param PageIdentity $page
74 * @param string $newModel
76 public function __construct(
77 IContentHandlerFactory
$contentHandlerFactory,
78 HookContainer
$hookContainer,
79 RevisionLookup
$revLookup,
80 UserFactory
$userFactory,
81 WikiPageFactory
$wikiPageFactory,
82 LogFormatterFactory
$logFormatterFactory,
87 $this->contentHandlerFactory
= $contentHandlerFactory;
88 $this->hookRunner
= new HookRunner( $hookContainer );
89 $this->revLookup
= $revLookup;
90 $this->userFactory
= $userFactory;
91 $this->logFormatterFactory
= $logFormatterFactory;
93 $this->performer
= $performer;
94 $this->page
= $wikiPageFactory->newFromTitle( $page );
95 $this->pageIdentity
= $page;
96 $this->newModel
= $newModel;
98 // SpecialChangeContentModel doesn't support tags
99 // api can specify tags via ::setTags, which also checks if user can add
100 // the tags specified
103 // Requires createNewContent to be called first
104 $this->logAction
= '';
106 // Defaults to nothing, for special page
107 $this->msgPrefix
= '';
111 * @param string $msgPrefix
113 public function setMessagePrefix( $msgPrefix ) {
114 $this->msgPrefix
= $msgPrefix;
118 * @param callable $authorizer ( string $action, PageIdentity $target, PermissionStatus $status )
120 * @return PermissionStatus
122 private function authorizeInternal( callable
$authorizer ): PermissionStatus
{
123 $current = $this->page
->getTitle();
124 $titleWithNewContentModel = clone $current;
125 $titleWithNewContentModel->setContentModel( $this->newModel
);
127 $status = PermissionStatus
::newEmpty();
128 $authorizer( 'editcontentmodel', $this->pageIdentity
, $status );
129 $authorizer( 'edit', $this->pageIdentity
, $status );
130 $authorizer( 'editcontentmodel', $titleWithNewContentModel, $status );
131 $authorizer( 'edit', $titleWithNewContentModel, $status );
137 * Check whether $performer can execute the content model change.
139 * @note this method does not guarantee full permissions check, so it should
140 * only be used to to decide whether to show a content model change form.
141 * To authorize the content model change action use {@link self::authorizeChange} instead.
143 * @return PermissionStatus
145 public function probablyCanChange(): PermissionStatus
{
146 return $this->authorizeInternal(
147 function ( string $action, PageIdentity
$target, PermissionStatus
$status ) {
148 return $this->performer
->probablyCan( $action, $target, $status );
154 * Authorize the content model change by $performer.
156 * @note this method should be used right before the actual content model change is performed.
157 * To check whether a current performer has the potential to change the content model of the page,
158 * use {@link self::probablyCanChange} instead.
160 * @return PermissionStatus
162 public function authorizeChange(): PermissionStatus
{
163 return $this->authorizeInternal(
164 function ( string $action, PageIdentity
$target, PermissionStatus
$status ) {
165 return $this->performer
->authorizeWrite( $action, $target, $status );
171 * Check user can edit and editcontentmodel before and after
173 * @deprecated since 1.36. Use ::probablyCanChange or ::authorizeChange instead.
174 * @return array Errors in legacy error array format
176 public function checkPermissions() {
177 wfDeprecated( __METHOD__
, '1.36' );
178 $status = $this->authorizeInternal(
179 function ( string $action, PageIdentity
$target, PermissionStatus
$status ) {
180 return $this->performer
->definitelyCan( $action, $target, $status );
182 return $status->toLegacyErrorArray();
186 * Specify the tags the user wants to add, and check permissions
188 * @param string[] $tags
192 public function setTags( $tags ) {
193 $tagStatus = ChangeTags
::canAddTagsAccompanyingChange( $tags, $this->performer
);
194 if ( $tagStatus->isOK() ) {
197 return Status
::newGood();
206 private function createNewContent() {
207 $contentHandlerFactory = $this->contentHandlerFactory
;
209 $title = $this->page
->getTitle();
210 $latestRevRecord = $this->revLookup
->getRevisionByTitle( $this->pageIdentity
);
212 if ( $latestRevRecord ) {
213 $latestContent = $latestRevRecord->getContent( SlotRecord
::MAIN
);
214 $latestHandler = $latestContent->getContentHandler();
215 $latestModel = $latestContent->getModel();
216 if ( !$latestHandler->supportsDirectEditing() ) {
217 // Only reachable via api
218 return Status
::newFatal(
219 'apierror-changecontentmodel-nodirectediting',
220 ContentHandler
::getLocalizedName( $latestModel )
224 $newModel = $this->newModel
;
225 if ( $newModel === $latestModel ) {
226 // Only reachable via api
227 return Status
::newFatal( 'apierror-nochanges' );
229 $newHandler = $contentHandlerFactory->getContentHandler( $newModel );
230 if ( !$newHandler->canBeUsedOn( $title ) ) {
231 // Only reachable via api
232 return Status
::newFatal(
233 'apierror-changecontentmodel-cannotbeused',
234 ContentHandler
::getLocalizedName( $newModel ),
235 Message
::plaintextParam( $title->getPrefixedText() )
240 $newContent = $newHandler->unserializeContent(
241 $latestContent->serialize()
243 } catch ( MWException
$e ) {
244 // Messages: changecontentmodel-cannot-convert,
245 // apierror-changecontentmodel-cannot-convert
246 return Status
::newFatal(
247 $this->msgPrefix
. 'changecontentmodel-cannot-convert',
248 Message
::plaintextParam( $title->getPrefixedText() ),
249 ContentHandler
::getLocalizedName( $newModel )
252 $this->latestRevId
= $latestRevRecord->getId();
253 $this->logAction
= 'change';
255 // Page doesn't exist, create an empty content object
256 $newContent = $contentHandlerFactory
257 ->getContentHandler( $this->newModel
)
258 ->makeEmptyContent();
259 $this->latestRevId
= false;
260 $this->logAction
= 'new';
262 $this->newContent
= $newContent;
264 return Status
::newGood();
268 * Handle change and logging after validation
270 * Can still be intercepted by hooks
272 * @param IContextSource $context
273 * @param string $comment
274 * @param bool $bot Mark as a bot edit if the user can
278 public function doContentModelChange(
279 IContextSource
$context,
283 $status = $this->createNewContent();
284 if ( !$status->isGood() ) {
289 $title = $page->getTitle();
290 $user = $this->userFactory
->newFromAuthority( $this->performer
);
293 $log = new ManualLogEntry( 'contentmodel', $this->logAction
);
294 $log->setPerformer( $this->performer
->getUser() );
295 $log->setTarget( $title );
296 $log->setComment( $comment );
297 $log->setParameters( [
298 '4::oldmodel' => $title->getContentModel(),
299 '5::newmodel' => $this->newModel
301 $log->addTags( $this->tags
);
303 $formatter = $this->logFormatterFactory
->newFromEntry( $log );
304 $formatter->setContext( RequestContext
::newExtraneousContext( $title ) );
305 $reason = $formatter->getPlainActionText();
307 if ( $comment !== '' ) {
308 $reason .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $comment;
312 $derivativeContext = new DerivativeContext( $context );
313 $derivativeContext->setTitle( $title );
314 $derivativeContext->setWikiPage( $page );
315 $status = new Status();
317 $newContent = $this->newContent
;
319 if ( !$this->hookRunner
->onEditFilterMergedContent( $derivativeContext, $newContent,
320 $status, $reason, $user, false )
322 if ( $status->isGood() ) {
323 // TODO: extensions should really specify an error message
324 $status->fatal( 'hookaborted' );
329 if ( !$status->isOK() ) {
330 if ( !$status->getMessages() ) {
331 $status->fatal( 'hookaborted' );
338 $flags = $this->latestRevId ? EDIT_UPDATE
: EDIT_NEW
;
339 $flags |
= EDIT_INTERNAL
;
340 if ( $bot && $this->performer
->isAllowed( 'bot' ) ) {
341 $flags |
= EDIT_FORCE_BOT
;
344 $status = $page->doUserEditContent(
353 if ( !$status->isOK() ) {
357 $logid = $log->insert();
358 $log->publish( $logid );
364 return Status
::newGood( $values );
369 /** @deprecated class alias since 1.43 */
370 class_alias( ContentModelChange
::class, 'ContentModelChange' );