Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 124 |
|
0.00% |
0 / 13 |
CRAP | |
0.00% |
0 / 1 |
SpecialChangeContentModel | |
0.00% |
0 / 123 |
|
0.00% |
0 / 13 |
930 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
doesWrites | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setParameter | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
postHtml | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
getDisplayFormat | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
alterForm | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
validateTitle | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
getFormFields | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
42 | |||
getOptionsForTitle | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
30 | |||
onSubmit | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
onSuccess | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
prefixSearchSubpages | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Specials; |
4 | |
5 | use ErrorPageError; |
6 | use LogEventsList; |
7 | use LogPage; |
8 | use MediaWiki\Collation\CollationFactory; |
9 | use MediaWiki\CommentStore\CommentStore; |
10 | use MediaWiki\Content\ContentHandler; |
11 | use MediaWiki\Content\IContentHandlerFactory; |
12 | use MediaWiki\EditPage\SpamChecker; |
13 | use MediaWiki\HTMLForm\HTMLForm; |
14 | use MediaWiki\Language\RawMessage; |
15 | use MediaWiki\Page\ContentModelChangeFactory; |
16 | use MediaWiki\Page\WikiPageFactory; |
17 | use MediaWiki\Revision\RevisionLookup; |
18 | use MediaWiki\Revision\RevisionRecord; |
19 | use MediaWiki\Revision\SlotRecord; |
20 | use MediaWiki\SpecialPage\FormSpecialPage; |
21 | use MediaWiki\Status\Status; |
22 | use MediaWiki\Title\Title; |
23 | use MediaWiki\Xml\Xml; |
24 | use SearchEngineFactory; |
25 | |
26 | /** |
27 | * @ingroup SpecialPage |
28 | */ |
29 | class SpecialChangeContentModel extends FormSpecialPage { |
30 | |
31 | private IContentHandlerFactory $contentHandlerFactory; |
32 | private ContentModelChangeFactory $contentModelChangeFactory; |
33 | private SpamChecker $spamChecker; |
34 | private RevisionLookup $revisionLookup; |
35 | private WikiPageFactory $wikiPageFactory; |
36 | private SearchEngineFactory $searchEngineFactory; |
37 | private CollationFactory $collationFactory; |
38 | |
39 | /** |
40 | * @param IContentHandlerFactory $contentHandlerFactory |
41 | * @param ContentModelChangeFactory $contentModelChangeFactory |
42 | * @param SpamChecker $spamChecker |
43 | * @param RevisionLookup $revisionLookup |
44 | * @param WikiPageFactory $wikiPageFactory |
45 | * @param SearchEngineFactory $searchEngineFactory |
46 | * @param CollationFactory $collationFactory |
47 | */ |
48 | public function __construct( |
49 | IContentHandlerFactory $contentHandlerFactory, |
50 | ContentModelChangeFactory $contentModelChangeFactory, |
51 | SpamChecker $spamChecker, |
52 | RevisionLookup $revisionLookup, |
53 | WikiPageFactory $wikiPageFactory, |
54 | SearchEngineFactory $searchEngineFactory, |
55 | CollationFactory $collationFactory |
56 | ) { |
57 | parent::__construct( 'ChangeContentModel', 'editcontentmodel' ); |
58 | |
59 | $this->contentHandlerFactory = $contentHandlerFactory; |
60 | $this->contentModelChangeFactory = $contentModelChangeFactory; |
61 | $this->spamChecker = $spamChecker; |
62 | $this->revisionLookup = $revisionLookup; |
63 | $this->wikiPageFactory = $wikiPageFactory; |
64 | $this->searchEngineFactory = $searchEngineFactory; |
65 | $this->collationFactory = $collationFactory; |
66 | } |
67 | |
68 | public function doesWrites() { |
69 | return true; |
70 | } |
71 | |
72 | /** |
73 | * @var Title|null |
74 | */ |
75 | private $title; |
76 | |
77 | /** |
78 | * @var RevisionRecord|bool|null |
79 | * |
80 | * A RevisionRecord object, false if no revision exists, null if not loaded yet |
81 | */ |
82 | private $oldRevision; |
83 | |
84 | protected function setParameter( $par ) { |
85 | $par = $this->getRequest()->getVal( 'pagetitle', $par ); |
86 | $title = Title::newFromText( $par ); |
87 | if ( $title ) { |
88 | $this->title = $title; |
89 | $this->par = $title->getPrefixedText(); |
90 | } else { |
91 | $this->par = ''; |
92 | } |
93 | } |
94 | |
95 | protected function postHtml() { |
96 | $text = ''; |
97 | if ( $this->title ) { |
98 | $contentModelLogPage = new LogPage( 'contentmodel' ); |
99 | $text = Xml::element( 'h2', null, $contentModelLogPage->getName()->text() ); |
100 | $out = ''; |
101 | LogEventsList::showLogExtract( $out, 'contentmodel', $this->title ); |
102 | $text .= $out; |
103 | } |
104 | return $text; |
105 | } |
106 | |
107 | protected function getDisplayFormat() { |
108 | return 'ooui'; |
109 | } |
110 | |
111 | protected function alterForm( HTMLForm $form ) { |
112 | $this->addHelpLink( 'Help:ChangeContentModel' ); |
113 | |
114 | if ( $this->title ) { |
115 | $form->setFormIdentifier( 'modelform' ); |
116 | } else { |
117 | $form->setFormIdentifier( 'titleform' ); |
118 | } |
119 | |
120 | // T120576 |
121 | $form->setSubmitTextMsg( 'changecontentmodel-submit' ); |
122 | |
123 | if ( $this->title ) { |
124 | $this->getOutput()->addBacklinkSubtitle( $this->title ); |
125 | } |
126 | } |
127 | |
128 | public function validateTitle( $title ) { |
129 | // Already validated by HTMLForm, but if not, throw |
130 | // an exception instead of a fatal |
131 | $titleObj = Title::newFromTextThrow( $title ); |
132 | |
133 | $this->oldRevision = $this->revisionLookup->getRevisionByTitle( $titleObj ) ?: false; |
134 | |
135 | if ( $this->oldRevision ) { |
136 | $oldContent = $this->oldRevision->getContent( SlotRecord::MAIN ); |
137 | if ( !$oldContent->getContentHandler()->supportsDirectEditing() ) { |
138 | return $this->msg( 'changecontentmodel-nodirectediting' ) |
139 | ->params( ContentHandler::getLocalizedName( $oldContent->getModel() ) ) |
140 | ->escaped(); |
141 | } |
142 | } |
143 | |
144 | return true; |
145 | } |
146 | |
147 | protected function getFormFields() { |
148 | $fields = [ |
149 | 'pagetitle' => [ |
150 | 'type' => 'title', |
151 | 'creatable' => true, |
152 | 'name' => 'pagetitle', |
153 | 'default' => $this->par, |
154 | 'label-message' => 'changecontentmodel-title-label', |
155 | 'validation-callback' => [ $this, 'validateTitle' ], |
156 | ], |
157 | ]; |
158 | if ( $this->title ) { |
159 | $options = $this->getOptionsForTitle( $this->title ); |
160 | if ( !$options ) { |
161 | throw new ErrorPageError( |
162 | 'changecontentmodel-emptymodels-title', |
163 | 'changecontentmodel-emptymodels-text', |
164 | [ $this->title->getPrefixedText() ] |
165 | ); |
166 | } |
167 | $fields['pagetitle']['readonly'] = true; |
168 | $fields += [ |
169 | 'model' => [ |
170 | 'type' => 'select', |
171 | 'name' => 'model', |
172 | 'default' => $this->title->getContentModel(), |
173 | 'options' => $options, |
174 | 'label-message' => 'changecontentmodel-model-label' |
175 | ], |
176 | 'reason' => [ |
177 | 'type' => 'text', |
178 | 'maxlength' => CommentStore::COMMENT_CHARACTER_LIMIT, |
179 | 'name' => 'reason', |
180 | 'validation-callback' => function ( $reason ) { |
181 | if ( $reason === null || $reason === '' ) { |
182 | // Null on form display, or no reason given |
183 | return true; |
184 | } |
185 | |
186 | $match = $this->spamChecker->checkSummary( $reason ); |
187 | |
188 | if ( $match ) { |
189 | return $this->msg( 'spamprotectionmatch', $match )->parse(); |
190 | } |
191 | |
192 | return true; |
193 | }, |
194 | 'label-message' => 'changecontentmodel-reason-label', |
195 | ], |
196 | ]; |
197 | } |
198 | |
199 | return $fields; |
200 | } |
201 | |
202 | /** |
203 | * @return array $options An array of data for an OOUI drop-down list. The array keys |
204 | * correspond to the human readable text in the drop-down list. The array values |
205 | * correspond to the <option value="">. |
206 | */ |
207 | private function getOptionsForTitle( ?Title $title = null ) { |
208 | $models = $this->contentHandlerFactory->getContentModels(); |
209 | $options = []; |
210 | foreach ( $models as $model ) { |
211 | $handler = $this->contentHandlerFactory->getContentHandler( $model ); |
212 | if ( !$handler->supportsDirectEditing() ) { |
213 | continue; |
214 | } |
215 | if ( $title ) { |
216 | if ( !$handler->canBeUsedOn( $title ) ) { |
217 | continue; |
218 | } |
219 | } |
220 | $options[ContentHandler::getLocalizedName( $model )] = $model; |
221 | } |
222 | |
223 | // Put the options in the drop-down list in alphabetical order. |
224 | // Sort by array key, case insensitive. |
225 | $collation = $this->collationFactory->getCategoryCollation(); |
226 | uksort( $options, static function ( $a, $b ) use ( $collation ) { |
227 | $a = $collation->getSortKey( $a ); |
228 | $b = $collation->getSortKey( $b ); |
229 | return strcmp( $a, $b ); |
230 | } ); |
231 | |
232 | return $options; |
233 | } |
234 | |
235 | public function onSubmit( array $data ) { |
236 | $this->title = Title::newFromText( $data['pagetitle'] ); |
237 | $page = $this->wikiPageFactory->newFromTitle( $this->title ); |
238 | |
239 | $changer = $this->contentModelChangeFactory->newContentModelChange( |
240 | $this->getContext()->getAuthority(), |
241 | $page, |
242 | $data['model'] |
243 | ); |
244 | |
245 | $permissionStatus = $changer->authorizeChange(); |
246 | if ( !$permissionStatus->isGood() ) { |
247 | $out = $this->getOutput(); |
248 | $wikitext = $out->formatPermissionStatus( $permissionStatus ); |
249 | // Hack to get our wikitext parsed |
250 | return Status::newFatal( new RawMessage( '$1', [ $wikitext ] ) ); |
251 | } |
252 | |
253 | $status = $changer->doContentModelChange( |
254 | $this->getContext(), |
255 | $data['reason'], |
256 | true |
257 | ); |
258 | |
259 | return $status; |
260 | } |
261 | |
262 | public function onSuccess() { |
263 | $out = $this->getOutput(); |
264 | $out->setPageTitleMsg( $this->msg( 'changecontentmodel-success-title' ) ); |
265 | $out->addWikiMsg( 'changecontentmodel-success-text', $this->title ); |
266 | } |
267 | |
268 | /** |
269 | * Return an array of subpages beginning with $search that this special page will accept. |
270 | * |
271 | * @param string $search Prefix to search for |
272 | * @param int $limit Maximum number of results to return (usually 10) |
273 | * @param int $offset Number of results to skip (usually 0) |
274 | * @return string[] Matching subpages |
275 | */ |
276 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
277 | return $this->prefixSearchString( $search, $limit, $offset, $this->searchEngineFactory ); |
278 | } |
279 | |
280 | protected function getGroupName() { |
281 | return 'pagetools'; |
282 | } |
283 | } |
284 | |
285 | /** @deprecated class alias since 1.41 */ |
286 | class_alias( SpecialChangeContentModel::class, 'SpecialChangeContentModel' ); |