1<?php
2
3use MediaWiki\Content\IContentHandlerFactory;
4use MediaWiki\MediaWikiServices;
5use MediaWiki\Revision\RevisionRecord;
6use MediaWiki\Revision\SlotRecord;
7
8class SpecialChangeContentModel extends FormSpecialPage {
9
10	/** @var IContentHandlerFactory */
11	private $contentHandlerFactory;
12
13	/**
14	 * @param IContentHandlerFactory|null $contentHandlerFactory
15	 * @internal use @see SpecialPageFactory::getPage
16	 */
17	public function __construct( ?IContentHandlerFactory $contentHandlerFactory = null ) {
18		parent::__construct( 'ChangeContentModel', 'editcontentmodel' );
19
20		if ( !$contentHandlerFactory ) {
21			wfDeprecated( __METHOD__ . ' without $contentHandlerFactory parameter', '1.35' );
22			$contentHandlerFactory = MediaWikiServices::getInstance()->getContentHandlerFactory();
23		}
24		$this->contentHandlerFactory = $contentHandlerFactory;
25	}
26
27	public function doesWrites() {
28		return true;
29	}
30
31	/**
32	 * @var Title|null
33	 */
34	private $title;
35
36	/**
37	 * @var RevisionRecord|bool|null
38	 *
39	 * A RevisionRecord object, false if no revision exists, null if not loaded yet
40	 */
41	private $oldRevision;
42
43	protected function setParameter( $par ) {
44		$par = $this->getRequest()->getVal( 'pagetitle', $par );
45		$title = Title::newFromText( $par );
46		if ( $title ) {
47			$this->title = $title;
48			$this->par = $title->getPrefixedText();
49		} else {
50			$this->par = '';
51		}
52	}
53
54	protected function postText() {
55		$text = '';
56		if ( $this->title ) {
57			$contentModelLogPage = new LogPage( 'contentmodel' );
58			$text = Xml::element( 'h2', null, $contentModelLogPage->getName()->text() );
59			$out = '';
60			LogEventsList::showLogExtract( $out, 'contentmodel', $this->title );
61			$text .= $out;
62		}
63		return $text;
64	}
65
66	protected function getDisplayFormat() {
67		return 'ooui';
68	}
69
70	protected function alterForm( HTMLForm $form ) {
71		$this->addHelpLink( 'Help:ChangeContentModel' );
72
73		if ( $this->title ) {
74			$form->setFormIdentifier( 'modelform' );
75		} else {
76			$form->setFormIdentifier( 'titleform' );
77		}
78
79		// T120576
80		$form->setSubmitTextMsg( 'changecontentmodel-submit' );
81
82		if ( $this->title ) {
83			$this->getOutput()->addBacklinkSubtitle( $this->title );
84		}
85	}
86
87	public function validateTitle( $title ) {
88		// Already validated by HTMLForm, but if not, throw
89		// an exception instead of a fatal
90		$titleObj = Title::newFromTextThrow( $title );
91
92		$this->oldRevision = MediaWikiServices::getInstance()
93			->getRevisionLookup()
94			->getRevisionByTitle( $titleObj ) ?: false;
95
96		if ( $this->oldRevision ) {
97			$oldContent = $this->oldRevision->getContent( SlotRecord::MAIN );
98			if ( !$oldContent->getContentHandler()->supportsDirectEditing() ) {
99				return $this->msg( 'changecontentmodel-nodirectediting' )
100					->params( ContentHandler::getLocalizedName( $oldContent->getModel() ) )
101					->escaped();
102			}
103		}
104
105		return true;
106	}
107
108	protected function getFormFields() {
109		$fields = [
110			'pagetitle' => [
111				'type' => 'title',
112				'creatable' => true,
113				'name' => 'pagetitle',
114				'default' => $this->par,
115				'label-message' => 'changecontentmodel-title-label',
116				'validation-callback' => [ $this, 'validateTitle' ],
117			],
118		];
119		if ( $this->title ) {
120			$spamChecker = MediaWikiServices::getInstance()->getSpamChecker();
121
122			$options = $this->getOptionsForTitle( $this->title );
123			if ( empty( $options ) ) {
124				throw new ErrorPageError(
125					'changecontentmodel-emptymodels-title',
126					'changecontentmodel-emptymodels-text',
127					[ $this->title->getPrefixedText() ]
128				);
129			}
130			$fields['pagetitle']['readonly'] = true;
131			$fields += [
132				'currentmodel' => [
133					'type' => 'text',
134					'name' => 'currentcontentmodel',
135					'default' => $this->title->getContentModel(),
136					'label-message' => 'changecontentmodel-current-label',
137					'readonly' => true
138				],
139				'model' => [
140					'type' => 'select',
141					'name' => 'model',
142					'options' => $options,
143					'label-message' => 'changecontentmodel-model-label'
144				],
145				'reason' => [
146					'type' => 'text',
147					'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT,
148					'name' => 'reason',
149					'validation-callback' => function ( $reason ) use ( $spamChecker ) {
150						if ( $reason === null || $reason === '' ) {
151							// Null on form display, or no reason given
152							return true;
153						}
154
155						$match = $spamChecker->checkSummary( $reason );
156
157						if ( $match ) {
158							return $this->msg( 'spamprotectionmatch', $match )->parse();
159						}
160
161						return true;
162					},
163					'label-message' => 'changecontentmodel-reason-label',
164				],
165			];
166		}
167
168		return $fields;
169	}
170
171	private function getOptionsForTitle( Title $title = null ) {
172		$models = $this->contentHandlerFactory->getContentModels();
173		$options = [];
174		foreach ( $models as $model ) {
175			$handler = $this->contentHandlerFactory->getContentHandler( $model );
176			if ( !$handler->supportsDirectEditing() ) {
177				continue;
178			}
179			if ( $title ) {
180				if ( $title->getContentModel() === $model ) {
181					continue;
182				}
183				if ( !$handler->canBeUsedOn( $title ) ) {
184					continue;
185				}
186			}
187			$options[ContentHandler::getLocalizedName( $model )] = $model;
188		}
189
190		return $options;
191	}
192
193	public function onSubmit( array $data ) {
194		$user = $this->getUser();
195		$this->title = Title::newFromText( $data['pagetitle'] );
196		$page = WikiPage::factory( $this->title );
197
198		$changer = MediaWikiServices::getInstance()
199			->getContentModelChangeFactory()
200			->newContentModelChange(
201				$user,
202				$page,
203				$data['model']
204			);
205
206		$errors = $changer->checkPermissions();
207		if ( $errors ) {
208			$out = $this->getOutput();
209			$wikitext = $out->formatPermissionsErrorMessage( $errors );
210			// Hack to get our wikitext parsed
211			return Status::newFatal( new RawMessage( '$1', [ $wikitext ] ) );
212		}
213
214		// Can also throw a ThrottledError, don't catch it
215		$status = $changer->doContentModelChange(
216			$this->getContext(),
217			$data['reason'],
218			true
219		);
220
221		return $status;
222	}
223
224	public function onSuccess() {
225		$out = $this->getOutput();
226		$out->setPageTitle( $this->msg( 'changecontentmodel-success-title' ) );
227		$out->addWikiMsg( 'changecontentmodel-success-text', $this->title );
228	}
229
230	/**
231	 * Return an array of subpages beginning with $search that this special page will accept.
232	 *
233	 * @param string $search Prefix to search for
234	 * @param int $limit Maximum number of results to return (usually 10)
235	 * @param int $offset Number of results to skip (usually 0)
236	 * @return string[] Matching subpages
237	 */
238	public function prefixSearchSubpages( $search, $limit, $offset ) {
239		return $this->prefixSearchString( $search, $limit, $offset );
240	}
241
242	protected function getGroupName() {
243		return 'pagetools';
244	}
245}
246