Localisation updates from https://translatewiki.net.
[mediawiki.git] / includes / content / ContentModelChange.php
blob0aad37b6aa4d17837f5a6b460003162611d5d0a4
1 <?php
3 namespace MediaWiki\Content;
5 use ChangeTags;
6 use LogFormatterFactory;
7 use ManualLogEntry;
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;
22 use MWException;
23 use WikiPage;
25 /**
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.
31 * @since 1.35
32 * @author DannyS712
34 class ContentModelChange {
35 /** @var IContentHandlerFactory */
36 private $contentHandlerFactory;
37 /** @var HookRunner */
38 private $hookRunner;
39 /** @var RevisionLookup */
40 private $revLookup;
41 /** @var UserFactory */
42 private $userFactory;
43 private LogFormatterFactory $logFormatterFactory;
44 /** @var Authority making the change */
45 private $performer;
46 /** @var WikiPage */
47 private $page;
48 /** @var PageIdentity */
49 private $pageIdentity;
50 /** @var string */
51 private $newModel;
52 /** @var string[] tags to add */
53 private $tags;
54 /** @var Content */
55 private $newContent;
56 /** @var int|false latest revision id, or false if creating */
57 private $latestRevId;
58 /** @var string 'new' or 'change' */
59 private $logAction;
60 /** @var string 'apierror-' or empty string, for status messages */
61 private $msgPrefix;
63 /**
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,
83 Authority $performer,
84 PageIdentity $page,
85 string $newModel
86 ) {
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
101 $this->tags = [];
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 );
133 return $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 );
181 } );
182 return $status->toLegacyErrorArray();
186 * Specify the tags the user wants to add, and check permissions
188 * @param string[] $tags
190 * @return Status
192 public function setTags( $tags ) {
193 $tagStatus = ChangeTags::canAddTagsAccompanyingChange( $tags, $this->performer );
194 if ( $tagStatus->isOK() ) {
195 $this->tags = $tags;
197 return Status::newGood();
198 } else {
199 return $tagStatus;
204 * @return Status
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() )
239 try {
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';
254 } else {
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
276 * @return Status
278 public function doContentModelChange(
279 IContextSource $context,
280 string $comment,
281 $bot
283 $status = $this->createNewContent();
284 if ( !$status->isGood() ) {
285 return $status;
288 $page = $this->page;
289 $title = $page->getTitle();
290 $user = $this->userFactory->newFromAuthority( $this->performer );
292 // Create log entry
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
300 ] );
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;
311 // Run edit filters
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' );
327 return $status;
329 if ( !$status->isOK() ) {
330 if ( !$status->getMessages() ) {
331 $status->fatal( 'hookaborted' );
334 return $status;
337 // Make the edit
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(
345 $newContent,
346 $this->performer,
347 $reason,
348 $flags,
349 $this->latestRevId,
350 $this->tags
353 if ( !$status->isOK() ) {
354 return $status;
357 $logid = $log->insert();
358 $log->publish( $logid );
360 $values = [
361 'logid' => $logid
364 return Status::newGood( $values );
369 /** @deprecated class alias since 1.43 */
370 class_alias( ContentModelChange::class, 'ContentModelChange' );