SpecialChangeContentModel.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. class SpecialChangeContentModel extends FormSpecialPage {
  4. public function __construct() {
  5. parent::__construct( 'ChangeContentModel', 'editcontentmodel' );
  6. }
  7. public function doesWrites() {
  8. return true;
  9. }
  10. /**
  11. * @var Title|null
  12. */
  13. private $title;
  14. /**
  15. * @var Revision|bool|null
  16. *
  17. * A Revision object, false if no revision exists, null if not loaded yet
  18. */
  19. private $oldRevision;
  20. protected function setParameter( $par ) {
  21. $par = $this->getRequest()->getVal( 'pagetitle', $par );
  22. $title = Title::newFromText( $par );
  23. if ( $title ) {
  24. $this->title = $title;
  25. $this->par = $title->getPrefixedText();
  26. } else {
  27. $this->par = '';
  28. }
  29. }
  30. protected function postText() {
  31. $text = '';
  32. if ( $this->title ) {
  33. $contentModelLogPage = new LogPage( 'contentmodel' );
  34. $text = Xml::element( 'h2', null, $contentModelLogPage->getName()->text() );
  35. $out = '';
  36. LogEventsList::showLogExtract( $out, 'contentmodel', $this->title );
  37. $text .= $out;
  38. }
  39. return $text;
  40. }
  41. protected function getDisplayFormat() {
  42. return 'ooui';
  43. }
  44. protected function alterForm( HTMLForm $form ) {
  45. if ( !$this->title ) {
  46. $form->setMethod( 'GET' );
  47. }
  48. $this->addHelpLink( 'Help:ChangeContentModel' );
  49. // T120576
  50. $form->setSubmitTextMsg( 'changecontentmodel-submit' );
  51. }
  52. public function validateTitle( $title ) {
  53. if ( !$title ) {
  54. // No form input yet
  55. return true;
  56. }
  57. // Already validated by HTMLForm, but if not, throw
  58. // and exception instead of a fatal
  59. $titleObj = Title::newFromTextThrow( $title );
  60. $this->oldRevision = Revision::newFromTitle( $titleObj ) ?: false;
  61. if ( $this->oldRevision ) {
  62. $oldContent = $this->oldRevision->getContent();
  63. if ( !$oldContent->getContentHandler()->supportsDirectEditing() ) {
  64. return $this->msg( 'changecontentmodel-nodirectediting' )
  65. ->params( ContentHandler::getLocalizedName( $oldContent->getModel() ) )
  66. ->escaped();
  67. }
  68. }
  69. return true;
  70. }
  71. protected function getFormFields() {
  72. $fields = [
  73. 'pagetitle' => [
  74. 'type' => 'title',
  75. 'creatable' => true,
  76. 'name' => 'pagetitle',
  77. 'default' => $this->par,
  78. 'label-message' => 'changecontentmodel-title-label',
  79. 'validation-callback' => [ $this, 'validateTitle' ],
  80. ],
  81. ];
  82. if ( $this->title ) {
  83. $options = $this->getOptionsForTitle( $this->title );
  84. if ( empty( $options ) ) {
  85. throw new ErrorPageError(
  86. 'changecontentmodel-emptymodels-title',
  87. 'changecontentmodel-emptymodels-text',
  88. [ $this->title->getPrefixedText() ]
  89. );
  90. }
  91. $fields['pagetitle']['readonly'] = true;
  92. $fields += [
  93. 'currentmodel' => [
  94. 'type' => 'text',
  95. 'name' => 'currentcontentmodel',
  96. 'default' => $this->title->getContentModel(),
  97. 'label-message' => 'changecontentmodel-current-label',
  98. 'readonly' => true
  99. ],
  100. 'model' => [
  101. 'type' => 'select',
  102. 'name' => 'model',
  103. 'options' => $options,
  104. 'label-message' => 'changecontentmodel-model-label'
  105. ],
  106. 'reason' => [
  107. 'type' => 'text',
  108. 'name' => 'reason',
  109. 'validation-callback' => function ( $reason ) {
  110. $match = EditPage::matchSummarySpamRegex( $reason );
  111. if ( $match ) {
  112. return $this->msg( 'spamprotectionmatch', $match )->parse();
  113. }
  114. return true;
  115. },
  116. 'label-message' => 'changecontentmodel-reason-label',
  117. ],
  118. ];
  119. }
  120. return $fields;
  121. }
  122. private function getOptionsForTitle( Title $title = null ) {
  123. $models = ContentHandler::getContentModels();
  124. $options = [];
  125. foreach ( $models as $model ) {
  126. $handler = ContentHandler::getForModelID( $model );
  127. if ( !$handler->supportsDirectEditing() ) {
  128. continue;
  129. }
  130. if ( $title ) {
  131. if ( $title->getContentModel() === $model ) {
  132. continue;
  133. }
  134. if ( !$handler->canBeUsedOn( $title ) ) {
  135. continue;
  136. }
  137. }
  138. $options[ContentHandler::getLocalizedName( $model )] = $model;
  139. }
  140. return $options;
  141. }
  142. public function onSubmit( array $data ) {
  143. if ( $data['pagetitle'] === '' ) {
  144. // Initial form view of special page, pass
  145. return false;
  146. }
  147. // At this point, it has to be a POST request. This is enforced by HTMLForm,
  148. // but lets be safe verify that.
  149. if ( !$this->getRequest()->wasPosted() ) {
  150. throw new RuntimeException( "Form submission was not POSTed" );
  151. }
  152. $this->title = Title::newFromText( $data['pagetitle'] );
  153. $titleWithNewContentModel = clone $this->title;
  154. $titleWithNewContentModel->setContentModel( $data['model'] );
  155. $user = $this->getUser();
  156. // Check permissions and make sure the user has permission to:
  157. $errors = wfMergeErrorArrays(
  158. // edit the contentmodel of the page
  159. $this->title->getUserPermissionsErrors( 'editcontentmodel', $user ),
  160. // edit the page under the old content model
  161. $this->title->getUserPermissionsErrors( 'edit', $user ),
  162. // edit the contentmodel under the new content model
  163. $titleWithNewContentModel->getUserPermissionsErrors( 'editcontentmodel', $user ),
  164. // edit the page under the new content model
  165. $titleWithNewContentModel->getUserPermissionsErrors( 'edit', $user )
  166. );
  167. if ( $errors ) {
  168. $out = $this->getOutput();
  169. $wikitext = $out->formatPermissionsErrorMessage( $errors );
  170. // Hack to get our wikitext parsed
  171. return Status::newFatal( new RawMessage( '$1', [ $wikitext ] ) );
  172. }
  173. $page = WikiPage::factory( $this->title );
  174. if ( $this->oldRevision === null ) {
  175. $this->oldRevision = $page->getRevision() ?: false;
  176. }
  177. $oldModel = $this->title->getContentModel();
  178. if ( $this->oldRevision ) {
  179. $oldContent = $this->oldRevision->getContent();
  180. try {
  181. $newContent = ContentHandler::makeContent(
  182. $oldContent->serialize(), $this->title, $data['model']
  183. );
  184. } catch ( MWException $e ) {
  185. return Status::newFatal(
  186. $this->msg( 'changecontentmodel-cannot-convert' )
  187. ->params(
  188. $this->title->getPrefixedText(),
  189. ContentHandler::getLocalizedName( $data['model'] )
  190. )
  191. );
  192. }
  193. } else {
  194. // Page doesn't exist, create an empty content object
  195. $newContent = ContentHandler::getForModelID( $data['model'] )->makeEmptyContent();
  196. }
  197. // All other checks have passed, let's check rate limits
  198. if ( $user->pingLimiter( 'editcontentmodel' ) ) {
  199. throw new ThrottledError();
  200. }
  201. $flags = $this->oldRevision ? EDIT_UPDATE : EDIT_NEW;
  202. $flags |= EDIT_INTERNAL;
  203. if ( MediaWikiServices::getInstance()
  204. ->getPermissionManager()
  205. ->userHasRight( $user, 'bot' )
  206. ) {
  207. $flags |= EDIT_FORCE_BOT;
  208. }
  209. $log = new ManualLogEntry( 'contentmodel', $this->oldRevision ? 'change' : 'new' );
  210. $log->setPerformer( $user );
  211. $log->setTarget( $this->title );
  212. $log->setComment( $data['reason'] );
  213. $log->setParameters( [
  214. '4::oldmodel' => $oldModel,
  215. '5::newmodel' => $data['model']
  216. ] );
  217. $formatter = LogFormatter::newFromEntry( $log );
  218. $formatter->setContext( RequestContext::newExtraneousContext( $this->title ) );
  219. $reason = $formatter->getPlainActionText();
  220. if ( $data['reason'] !== '' ) {
  221. $reason .= $this->msg( 'colon-separator' )->inContentLanguage()->text() . $data['reason'];
  222. }
  223. // Run edit filters
  224. $derivativeContext = new DerivativeContext( $this->getContext() );
  225. $derivativeContext->setTitle( $this->title );
  226. $derivativeContext->setWikiPage( $page );
  227. $status = new Status();
  228. if ( !Hooks::run( 'EditFilterMergedContent',
  229. [ $derivativeContext, $newContent, $status, $reason,
  230. $user, false ] )
  231. ) {
  232. if ( $status->isGood() ) {
  233. // TODO: extensions should really specify an error message
  234. $status->fatal( 'hookaborted' );
  235. }
  236. return $status;
  237. }
  238. $status = $page->doEditContent(
  239. $newContent,
  240. $reason,
  241. $flags,
  242. $this->oldRevision ? $this->oldRevision->getId() : false,
  243. $user
  244. );
  245. if ( !$status->isOK() ) {
  246. return $status;
  247. }
  248. $logid = $log->insert();
  249. $log->publish( $logid );
  250. return $status;
  251. }
  252. public function onSuccess() {
  253. $out = $this->getOutput();
  254. $out->setPageTitle( $this->msg( 'changecontentmodel-success-title' ) );
  255. $out->addWikiMsg( 'changecontentmodel-success-text', $this->title );
  256. }
  257. /**
  258. * Return an array of subpages beginning with $search that this special page will accept.
  259. *
  260. * @param string $search Prefix to search for
  261. * @param int $limit Maximum number of results to return (usually 10)
  262. * @param int $offset Number of results to skip (usually 0)
  263. * @return string[] Matching subpages
  264. */
  265. public function prefixSearchSubpages( $search, $limit, $offset ) {
  266. return $this->prefixSearchString( $search, $limit, $offset );
  267. }
  268. protected function getGroupName() {
  269. return 'pagetools';
  270. }
  271. }