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