Manual:Page content models

For content pages vs. non-content pages, see: Manual:Using custom namespaces#Content namespaces.

The ContentHandler introduced in MediaWiki 1.21 allows you to add new content models other than wikitext. It makes it possible for wiki pages to be composed of data other than wikitext, and represented in any way — for example: Markdown, reStructuredText, icalendar, or a custom XML format. The display and editing of these content models can be handled in custom ways (e.g. different syntax highlighting, or whole new data-entry forms).

This page steps through how to create a new content model in an extension. It assumes some familiarity with general extension development practices. For a brief summary of the requirements, see the Summary section at the bottom of this page.

A meaningless "Goat" content model will be used for the examples. You can also examine the DataPages extension, which is part of Extension:Examples.

Registration

First of all, add the content model's name and handler class to your extension.json:

"ContentHandlers": {
	"goat": "MediaWiki\\Extension\\GoatExt\\Content\\GoatContentHandler"
}
  • The left-hand value here is the name of the content type, it can be any unique string you want, and it lives alongside the five built-in content types: 'wikitext', 'JavaScript', 'CSS', 'plain text', 'JSON'. This value is exposed to users in places such as Special:ChangeContentModel and page information.
  • The right-hand value is the fully-qualified name of a class that extends MediaWiki\Content\ContentHandler.

This will require two new classes to be created, such as \MediaWiki\Extension\GoatExt\Content\GoatContent and \MediaWiki\Extension\GoatExt\Content\GoatContentHandler (make sure their namespace is registered in AutoloadNamespaces). More information about these classes is given below.

Optional content model constants

The 'goat' string above is the content model's ID (generally called $modelId in code), and is usually also defined as a constant. These constants are defined for all built-in content models, and lots of documentation refers to the "CONTENT_MODEL_XXX" constants. If you have not defined them, this can be a bit confusing. The definition should be done via the callback item in extension.json. For example:

In extension.json:

"callback": "MediaWiki\\Extension\\GoatExt\\Hooks::registrationCallback"

In src/Hooks.php:

<?php

namespace MediaWiki\Extension\GoatExt;

class Hooks {
    public static function registrationCallback() {
        // Must match the name used in the 'ContentHandlers' section of extension.json
        define( 'CONTENT_MODEL_GOAT', 'goat' );
    }
}

You don't have to do it this way, and could just use the string.

Assigning content models to pages

Pages can have their content type manually changed, but it's useful to have them default to the correct one. Two common ways of doing this are by namespace, and by file extension.

By namespace

If you want an entire wiki namespace to have a default content model, you can define it as such in extension.json:

"namespaces": [
	{
		"id": 550,
		"constant": "NS_GOAT",
		"name": "Goat",
		"subpages": false,
		"content": true,
		"defaultcontentmodel": "goat"
	},
	{
		"id": 551,
		"constant": "NS_GOAT_TALK",
		"name": "Goat_talk",
		"subpages": true,
		"content": false,
		"defaultcontentmodel": "wikitext"
	}
]

Note that published extensions should register the namespace IDs they use (550 and 551 above) on the Extension default namespaces page.

By file extension

If you want to determine the content type by the addition of a quasi-file-type suffix on the wiki page name, you can use the ContentHandlerDefaultModelFor hook. For example:

<?php

namespace MediaWiki\Extension\GoatExt;

use MediaWiki\Revision\Hook\ContentHandlerDefaultModelForHook;
use MediaWiki\Title\Title;

class Hooks implements ContentHandlerDefaultModelForHook {
	/**
	 * @param Title $title
	 * @param string &$model
	 */
	public function onContentHandlerDefaultModelFor( $title, &$model ) {
		// Any page title (in any namespace) ending in '.goat'.
		$ext = '.goat';

		if ( str_ends_with( $title->getText(), $ext ) ) {
			// This is the constant you defined earlier.
			$model = CONTENT_MODEL_GOAT;
			// If you change the content model, return false.
			return false;
		}
		// If you don't change it, return true.
		return true;
	}
}

ContentHandler

The next thing to define is the GoatContentHandler class, which is where we also specify what format this content type will be stored as (in this case, text). ContentHandlers don't know anything about any particular page content, but determine the general structure and storage of the content.

<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\Content;
use MediaWiki\Content\ContentHandler;

class GoatContentHandler extends ContentHandler {

	public function __construct( $modelId = 'goat' ) {
		parent::__construct( $modelId, [ CONTENT_FORMAT_TEXT ] );
	}

	public function serializeContent( Content $content, $format = null ) {
	}

	public function unserializeContent( $blob, $format = null ) {
	}

	public function makeEmptyContent() {
		return new GoatContent();
	}

	public function supportsDirectEditing() {
		return true;
	}
}

Content

The GoatContent class is the representation of the content's data, and does not know anything about pages, revisions, or how it is stored in the database. Beside the required seven inherited methods, you can add other public methods are domain-specific; in this case we want to be able to retrieve the goat's name.

Notably, AbstractContent::getNativeData() has been deprecated since 1.33. If you're extending MediaWiki\Content\TextContent instead, then you could use the getText() method. If you're extending MediaWiki\Content\JsonContent, the parent class provides the getData() method.

<?php

namespace MediaWiki\Extension\GoatExt;

use MediaWiki\Content\AbstractContent;

class GoatContent extends AbstractContent {

	public function __construct( $modelId = 'goat' ) {
		parent::__construct( $modelId );
	}

	public function getTextForSearchIndex() {
	}

	public function getWikitextForTransclusion() {
	}

	public function getTextForSummary( $maxLength = 250 ) {
	}

	public function getSize() {
	}

	public function copy() {
	}

	public function isCountable( $hasLinks = null ) {
	}

	public function getName() {
		return 'Garry';
	}
}

Edit form

Now we've got the skeleton set up, we'll want to try editing a Goat. To do this, we create GoatContentHandler::getActionOverrides() and specify what actions we want to map to what classes. To start with, we'll just deal with 'edit' (which corresponds to ?action=edit in the URL).

	public function getActionOverrides() {
		return [
			'edit' => GoatEditAction::class,
		];
	}

And we'll create our new GoatEditAction class, basically the same as the core EditAction but using our own GoatEditPage:

<?php

namespace MediaWiki\Extension\GoatExt\Action;

use MediaWiki\Actions\EditAction;
use MediaWiki\Extension\GoatExt\EditPage;

class GoatEditAction extends EditAction {

	public function show() {
		$this->useTransactionalTimeLimit();
		$editPage = new GoatEditPage( $this->getArticle() );
		$editPage->setContextTitle( $this->getTitle() );
		$editPage->edit();
	}

}

Our new GoatEditPage class is where the action happens (excuse the pun):

<?php

namespace MediaWiki\Extension\GoatExt;

use MediaWiki\EditPage\EditPage;
use OOUI\FieldLayout;
use OOUI\TextInputWidget;

class GoatEditPage extends EditPage {

	protected function showContentForm() {
		$out = $this->context->getOutput();

		// Get the data.
		$name = $this->getCurrentContent()->getGoatName();

		// Create the form.
		$nameField = new FieldLayout(
			new TextInputWidget( [ 'name' => 'goat_name', 'value' => $name ] ),
			[ 'label' => 'Name', 'align' => 'left' ]
		);
		$out->addHTML( $nameField );
	}

}

You should now be able to edit a page and see your form. But when you put data into it, and hit 'preview', you'll see that things are not yet working fully and that you get no output, nor is your submitted text shown again in the form.

So we must override the 'submit' action as well, with a new GoatSubmitAction class and the addition of 'submit' => GoatSubmitAction::class, to our GoatContentHandler::getActionOverrides() method. Our GoatSubmitAction class should be the same as that of core, but inheriting from our GoatEditAction.

Display

A content model is responsible for producing any required output for display. This usually involves working with its data and producing HTML in some way, to add to the parser output.

<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\ContentHandler;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Parser\ParserOutput;

class GoatContentHandler extends ContentHandler {

	protected function fillParserOutput(
		Content $content,
		ContentParseParams $cpoParams,
		ParserOutput &$output
	) {
		// define $html somewhere
		// e.g. $output->setText( $html );
	}

}

Display a description/documentation

Sometimes you may want to display some information or some documentation for a wiki page that has a custom content model such as JSON. There are no system messages to display text above such pages (except for MediaWiki:Clearyourcache displayed above only JavaScript and CSS pages). You may want to see phab:T355159 for further details.

Comparing revisions

Extending DifferenceEngine (deprecated in 1.32)

The GoatDifferenceEngine class is the representation of the difference between goat contents. We override the default generateContentDiffBody method to generate a diff.

<?php

namespace MediaWiki\Extension\GoatExt\Diff;

use DifferenceEngine;
use MediaWiki\Content\Content;

class GoatDifferenceEngine extends DifferenceEngine {

	public function generateContentDiffBody( Content $oldContent, Content $newContent ) {
	}
}

In order to tell MediaWiki to use our GoatDifferenceEngine, we overwrite the getDiffEngineClass in our GoatContentHandler.

<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\ContentHandler;
use MediaWiki\Extension\GoatExt\Diff\GoatDifferenceEngine;

class GoatContentHandler extends ContentHandler {

	public function getDiffEngineClass() {
		return GoatDifferenceEngine::class;
	}
}

Extending SlotDiffRenderer

Using the newer method, we can extend SlotDiffRenderer instead. This follows the above pretty similarly:

<?php

namespace MediaWiki\Extension\GoatExt\Diff;

use MediaWiki\Content\Content;
use SlotDiffRenderer;

class GoatSlotDiffRenderer extends SlotDiffRenderer {

	public function getDiff( ?Content $oldContent = null, ?Content $newContent = null ) {
	}
}

Then, we tell our custom GoatContentHandler to use our custom GoatSlotDiffRenderer:

<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\ContentHandler;
use MediaWiki\Context\IContextSource;
use MediaWiki\Extension\GoatExt\Diff\GoatSlotDiffRenderer;

class GoatContentHandler extends ContentHandler {

	protected function getSlotDiffRendererWithOptions( IContextSource $context, $options = [] ) {
		return new GoatSlotDiffRenderer();
	}
}

Summary

To implement a new content model with a custom editing form, create the following:

<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\AbstractContent;

class GoatContent extends AbstractContent  {
	public function __construct( $modelId = 'goat' ) {
		parent::__construct( $modelId );
	}
	public function getTextForSearchIndex() {}
	public function getWikitextForTransclusion() {}
	public function getTextForSummary( $maxLength = 250 ) {}
	public function getSize() {}
	public function copy() {}
	public function isCountable( $hasLinks = null ) {}
}
<?php

namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\Content;
use MediaWiki\Content\ContentHandler;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Parser\ParserOutput;

class GoatContentHandler extends ContentHandler {	
	public function __construct( $modelId = CONTENT_MODEL_GOAT, $formats = ['text/x-goat'] ) {
		parent::__construct($modelId, $formats);
	}
	protected function getContentClass() {}
	public function supportsDirectEditing() {}
	public function serializeContent( Content $content, $format = null ) {}
	public function unserializeContent( $blob, $format = null ) {}
	public function makeEmptyContent() {}
	public function getActionOverrides() {}
	protected function fillParserOutput(
		Content $content,
		ContentParseParams $cpoParams,
		ParserOutput &$output
	) {}

}

Checklist

Action Example
Define a content model id (al. content type) and content model constant goat
CONTENT_MODEL_GOAT
Choose your preferred mechanism of applying a content model:
(a) By namespace. Define namespace constants, ids and names
given any of them is not already taken
NS_GOAT (id: 550, name 'Goat')
NS_GOAT_TALK (id: 551, name: 'Goat talk')
(b) By extension in pagename .goat
(c) Other through 'Page information' interface (&action=info)
extension.json
ContentHandlers - register a ContentHandler subclass for the new content model
"ContentHandlers": {
	"goat": "MediaWiki\\Extension\\GoatExt\\Content\\GoatContentHandler"
}
callback - name your class method for callbacks, conventionally in your Hooks file
"callback": "MediaWiki\Extension\\GoatExt\\Hooks::registrationCallback"
Hooks, HookHandlers - provide a hook method for ContentHandlerDefaultModelFor
"Hooks": {
   "ContentHandlerDefaultModelFor": "main"
},
"HookHandlers": {
   "main": {
      "class": "MediaWiki\\Extension\\GoatExt\\Hooks"
   }
}
namespaces - define namespaces with the appropriate value for defaultcontentmodel.

For more options, use ContentHandlerDefaultModelFor hook instead.

"namespaces": [
	{
		"id": 550,
		"constant": "NS_GOAT",
		"name": "Goat",
		"subpages": false,
		"content": false,
		"defaultcontentmodel": "goat"
	},
	{
		"id": 551,
		"constant": "NS_GOAT_TALK",
		"name": "Goat_talk",
		"subpages": true,
		"content": false,
		"defaultcontentmodel": "wikitext"
	}
]
AutoloadNamespaces - load Content and ContentHandler subclasses in subdirectory
"AutoloadNamespaces": {
   "MediaWiki\\Extension\\GoatExt\\": "src/"
}
AutoloadClasses - load Content and ContentHandler subclasses explicitly
"AutoloadClasses": {
   "MediaWiki\\Extension\\GoatExt\\Hooks": "src/Hooks.php",
   "MediaWiki\\Extension\\GoatExt\\Content\\GoatContent": "src/Content/GoatContent.php",
   "MediaWiki\\Extension\\GoatExt\\Content\\GoatContentHandler": "src/Content/GoatContentHandler.php"
}
Hooks file MediaWiki\\Extension\\GoatExt\\Hooks
Add method for the callback
public static function registrationCallback() {
   // Must match the name used in the 'ContentHandlers' section of extension.json
   define( 'CONTENT_MODEL_GOAT', 'goat' );
}
Add method for the ContentHandlerDefaultModelFor hook for wiki pages in the intended namespace
public function onContentHandlerDefaultModelFor( $title, &$model ) {
   if ( $title->inNamespace( NS_GOAT ) {
      $model = CONTENT_MODEL_GOAT;
      return false;
   }
   return true;
}
Or add method for the ContentHandlerDefaultModelFor hook for wiki pages with the intended file extension
public function onContentHandlerDefaultModelFor( $title, &$model ) {
   $ext = '.goat';
   if ( str_ends_with( $title->getText(), $ext ) ) {
      $model = CONTENT_MODEL_GOAT;
      return false;
   }
   return true;
}
Content and ContentHandler subclasses
Add a Content subclass by extending AbstractContent or one of the subclasses available (e.g. TextContent)
namespace MediaWiki\Extension\GoatExt\Content;

use MediaWiki\Content\AbstractContent;

class GoatContent extends AbstractContent {

	public function __construct( $modelId = 'goat' ) {
		parent::__construct( $modelId );
	}

	public function getTextForSearchIndex() {}
	public function getWikitextForTransclusion() {}
	public function getTextForSummary( $maxLength = 250 ) {}
	public function getSize() {}
	public function copy() {}
	public function isCountable( $hasLinks = null ) {}

   // etc. See documentation for details.
}
Add a ContentHandler subclass by extending either ContentHandler or one of its subclasses (e.g. TextContentHandler or CodeContentHandler)
<?php

namespace MediaWiki\Extension\GoatExt;

use MediaWiki\Content\Content;
use MediaWiki\Content\ContentHandler;
use MediaWiki\Content\Renderer\ContentParseParams;
use MediaWiki\Parser\ParserOutput;

class GoatContentHandler extends ContentHandler {

	public function __construct( $modelId = 'goat' ) {
		parent::__construct( $modelId, [ CONTENT_FORMAT_TEXT ] );
	}

	public function serializeContent( Content $content, $format = null ) {
	}

	public function unserializeContent( $blob, $format = null ) {
	}

	public function makeEmptyContent() {
		return new GoatContent();
	}

	public function supportsDirectEditing() {
		return true;
	}

	protected function fillParserOutput(
		Content $content,
		ContentParseParams $cpoParams,
		ParserOutput &$output
	) {
		// define $html somewhere
		// e.g. $output->setText( $html );
	}

    // etc. See documentation for details
}
Compare revisions Either override ContentHandler::getDiffEngineClass() and DifferenceEngine::generateContentDiffBody(), or override ContentHandler::getSlotDiffRendererWithOptions() and SlotDiffRenderer::getDiff
Modify associated actions such as 'edit' Override ContentHandler::getActionOverrides() and EditAction::show()

See also

Category:ContentHandler Category:Extension creation
Category:ContentHandler Category:Extension creation