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