Wikidata/Berlin summit 2012/Code walk through

From Meta, a Wikimedia project coordination wiki

Content.php[edit]

<?php

abstract class Content {


	/**
	 * Returns native represenation of the data. Interpretation depends on the data model used,
	 * as given by getDataModel().
	 */
	public abstract function getNativeData( );

	/**
	 * returns the content's nominal size in bogo-bytes.
	 * 
	 * //Q: useful? bogo-bytes?
	 */
	public abstract function getSize( );

	/**
	 * Returns true if this Content object represents empty content.
	 */
	public function isEmpty() {
		return $this->getSize() == 0;
	}

	/**
	 * Returns if the content is valid. This is intended for local validity checks, not considering global consistency.
	 * It needs to be valid before it can be saved.
	 */
	public function isValid() {
		return true;
	}

	////////////////////////////////////////////////////////////////////////////

	/**
	 * //Q: move to ContentHandler?
	 * //Q: getRenderOutput(), using RenderOptions, RenderOutput?
	 * //Q: what's $revId used for by the parser?
	 */
	public abstract function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true );

	/**
	 * Returns a list of DataUpdate objects for recording information about this Content in some secondary
	 * data store. If the optional second argument, $old, is given, the updates may model only the changes that
	 * need to be made to replace information about the old content with infomration about the new content.
	 * 
	 * //Q: is $old useful/sensible?
	 * //Q: sane to split this from actually parsing the content?
	 * //Q: move to ContentHandler, so it's in the same place as getDeletionUpdates() ?
	 */
	public function getSecondaryDataUpdates( Title $title, Content $old = null, $recursive = false ) {
		$po = $this->getParserOutput( $title, null, null, false );
		return $po->getSecondaryDataUpdates( $title, $recursive );
	}

	////////////////////////////////////////////////////////////////////////////

	/**
	 * @return String a string representing the content in a way useful for building a full text search index.
	 *         If no useful representation exists, this method returns an empty string.
	 */
	public abstract function getTextForSearchIndex( );

	/**
	 * @return String the wikitext to include when another page includes this  content, or false if the content is not
	 *         includable in a wikitext page.
	 *
	 * //Q: @TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
	 * //Q: @TODO: use in parser, etc!
	 */
	public abstract function getWikitextForTransclusion( );

	/**
	 * Returns a textual representation of the content suitable for use in edit summaries and log messages.
	 */
	public abstract function getTextForSummary( $maxlength = 250 );

	/**
	 * Returns true if this content is countable as a "real" wiki page, provided
	 * that it's also in a countable location (e.g. a current revision in the main namespace).
	 */
	public abstract function isCountable( $hasLinks = null ) ;

	/**
	 * Construct the redirect destination from this content and return an
	 * array of Titles, or null if this content doesn't represent a redirect.
	 * The last element in the array is the final destination after all redirects
	 * have been resolved (up to $wgMaxRedirects times).
	 */
	public function getRedirectChain() {
		return null;
	}

	public function getRedirectTarget() {
		return null;
	}

	public function getUltimateRedirectTarget() { 
		return null;
	}

	public function isRedirect() {
		return $this->getRedirectTarget() !== null;
	}

	/**
	 * Returns the section with the given id.
	 *
	 * The default implementation returns null.
	 */
	public function getSection( $sectionId ) {
		return null;
	}

	//////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Replaces a section of the content and returns a Content object with the section replaced.
	 * 
	 * //Q: what to do if sections are not supported?
	 */
	public function replaceSection( $section, Content $with, $sectionTitle = ''  ) {
		return null;
	}

	/**
	 * Returns a new WikitextContent object with the given section heading prepended, if supported.
	 * The default implementation just returns this Content object unmodified, ignoring the section header.
	 * 
	 * //Q: what to do if sections are not supported?
	 */
	public function addSectionHeader( $header ) {
		return $this;
	}

	/**
	 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
	 */
	public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
		return $this;
	}

	/**
	 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
	 */
	public function preloadTransform( Title $title, ParserOptions $popts ) {
		return $this;
	}

	/////////////////////////////////////////////////////////////////////////////////

	/**
	 * Returns true if this Content objects is conceptually equivalent to the given Content object.
	 *
	 * Will returns false if $that is null.
	 * Will return true if $that === $this.
	 * Will return false if $that->getModleName() != $this->getModel().
	 * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
	 * where the meaning of "equal" depends on the actual data model.
	 *
	 * Implementations should be careful to make equals() transitive and reflexive:
	 *
	 * * $a->equals( $b ) <=> $b->equals( $a )
	 * * $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
	 *
	 * @since WD.1
	 *
	 * @param Content $that the Content object to compare to
	 * @return bool true if this Content object is euqual to $that, false otherwise.
	 */
	public function equals( Content $that = null ) {
		if ( is_null( $that ) ){
			return false;
		}

		if ( $that === $this ) {
			return true;
		}

		if ( $that->getModel() !== $this->getModel() ) {
			return false;
		}

		return $this->getNativeData() === $that->getNativeData();
	}

	/**
	 * Return a copy of this Content object. The following must be true for the object returned
	 * if $copy = $original->copy()
	 *
	 * * get_class($original) === get_class($copy)
	 * * $original->getModel() === $copy->getModel()
	 * * $original->equals( $copy )
	 *
	 * If and only if the Content object is imutable, the copy() method can and should
	 * return $this. That is,  $copy === $original may be true, but only for imutable content
	 * objects.
	 *
	 * @since WD.1
	 *
	 * @return Content. A copy of this object
	 */
	public abstract function copy( );


	# TODO: handle ImagePage and CategoryPage
	# TODO: make sure we cover lucene search / wikisearch.
	# TODO: make sure ReplaceTemplates still works
	# FUTURE: nice&sane integration of GeSHi syntax highlighting
	#   [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting
	#   [12:00] <vvv> And default it to a DummyHighlighter

	# TODO: make sure we cover the external editor interface (does anyone actually use that?!)

	# TODO: tie into API to provide contentModel for Revisions
	# TODO: tie into API to provide serialized version and contentFormat for Revisions
	# TODO: tie into API edit interface
	# FUTURE: make EditForm plugin for EditPage
}

# FUTURE: special type for redirects?!
# FUTURE: MultipartContent < WikipageContent (Main + Links + X)
# FUTURE: LinksContent < LanguageLinksContent, CategoriesContent

////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////

/**
 * Content object implementation for representing flat text.
 *
 * TextContent instances are imutable
 *
 * @since WD.1
 */
abstract class TextContent extends Content {

	public function __construct( $text, $model_id = null ) {
		parent::__construct( $model_id );

		$this->mText = $text;
	}

	public function copy() {
		return $this; #NOTE: this is ok since TextContent are imutable.
	}

	public function getTextForSummary( $maxlength = 250 ) {
		global $wgContLang;

		$text = $this->getNativeData();

		$truncatedtext = $wgContLang->truncate(
			preg_replace( "/[\n\r]/", ' ', $text ),
			max( 0, $maxlength ) );

		return $truncatedtext;
	}

	/**
	 * returns the text's size in bytes.
	 *
	 * @return int the size
	 */
	public function getSize( ) {
		$text = $this->getNativeData( );
		return strlen( $text );
	}

	/**
	 * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
	 *
	 * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
	 *                        to avoid redundant parsing to find out.
	 *
	 * @return bool true if the content is countable
	 */
	public function isCountable( $hasLinks = null ) {
		global $wgArticleCountMethod;

		if ( $this->isRedirect( ) ) {
			return false;
		}

		if (  $wgArticleCountMethod === 'any' ) {
			return true;
		}

		return false;
	}

	/**
	 * Returns the text represented by this Content object, as a string.
	 *
	 * @return String the raw text
	 */
	public function getNativeData( ) {
		$text = $this->mText;
		return $text;
	}

	/**
	 * Returns the text represented by this Content object, as a string.
	 *
	 * @return String the raw text
	 */
	public function getTextForSearchIndex( ) {
		return $this->getNativeData();
	}

	/**
	 * Returns the text represented by this Content object, as a string.
	 *
	 * @return String the raw text
	 */
	public function getWikitextForTransclusion( ) {
		return $this->getNativeData();
	}

	/**
	 * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
	 *
	 * @return ParserOutput representing the HTML form of the text
	 */
	public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
		# generic implementation, relying on $this->getHtml()

		if ( $generateHtml ) $html = $this->getHtml( $options );
		else $html = '';

		$po = new ParserOutput( $html );

		return $po;
	}

	protected abstract function getHtml( );

	/**
	 * Diff this content object with another content object..
	 *
	 * @since WD.diff
	 *
	 * @param Content $that the other content object to compare this content object to
	 * @param Language $lang the language object to use for text segmentation. If not given, $wgContentLang is used.
	 *
	 * @return DiffResult a diff representing the changes that would have to be made to this content object
	 *         to make it equal to $that.
	 */
	public function diff( Content $that, Language $lang = null ) {
		global $wgContLang;

		$this->checkModelID( $that->getModel() );

		#@todo: could implement this in DifferenceEngine and just delegate here?

		if ( !$lang ) $lang = $wgContLang;

		$otext = $this->getNativeData();
		$ntext = $this->getNativeData();

		# Note: Use native PHP diff, external engines don't give us abstract output
		$ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
		$nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );

		$diffs = new Diff( $ota, $nta );
		return $diff;
	}


}

///////////////////////////////////////////////////////////////////////////////////////

/**
 * @since WD.1
 */
class WikitextContent extends TextContent {

	public function __construct( $text ) {
		parent::__construct($text, CONTENT_MODEL_WIKITEXT);
	}

	protected function getHtml( ) {
		throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
	}

	/**
	 * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
	 *
	 * @since WikiData1
	 *
	 * @param IContextSource|null $context
	 * @param null $revId
	 * @param null|ParserOptions $options
	 * @param bool $generateHtml
	 *
	 * @return ParserOutput representing the HTML form of the text
	 */
	public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
		global $wgParser;

		if ( !$options ) {
			$options = new ParserOptions();
		}

		$po = $wgParser->parse( $this->mText, $title, $options, true, true, $revId );

		return $po;
	}

	/**
	 * Returns the section with the given id.
	 *
	 * @param String $sectionId the section's id
	 * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
	 */
	public function getSection( $section ) {
		global $wgParser;

		$text = $this->getNativeData();
		$sect = $wgParser->getSection( $text, $section, false );

		return  new WikitextContent( $sect );
	}

	/**
	 * Replaces a section in the wikitext
	 *
	 * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
	 * @param $with Content: new content of the section
	 * @param $sectionTitle String: new section's subject, only if $section is 'new'
	 * @return Content Complete article content, or null if error
	 */
	public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
		wfProfileIn( __METHOD__ );

		$myModelId = $this->getModel();
		$sectionModelId = $with->getModel();

		if ( $sectionModelId != $myModelId  ) {
			$myModelName = ContentHandler::getContentModelName( $myModelId );
			$sectionModelName = ContentHandler::getContentModelName( $sectionModelId );

			throw new MWException( "Incompatible content model for section: document uses $myModelId ($myModelName), "
								. "section uses $sectionModelId ($sectionModelName)." );
		}

		$oldtext = $this->getNativeData();
		$text = $with->getNativeData();

		if ( $section === '' ) {
			return $with; #XXX: copy first?
		} if ( $section == 'new' ) {
			# Inserting a new section
			$subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
			if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
				$text = strlen( trim( $oldtext ) ) > 0
					? "{$oldtext}\n\n{$subject}{$text}"
					: "{$subject}{$text}";
			}
		} else {
			# Replacing an existing section; roll out the big guns
			global $wgParser;

			$text = $wgParser->replaceSection( $oldtext, $section, $text );
		}

		$newContent = new WikitextContent( $text );

		wfProfileOut( __METHOD__ );
		return $newContent;
	}

	/**
	 * Returns a new WikitextContent object with the given section heading prepended.
	 *
	 * @param $header String
	 * @return Content
	 */
	public function addSectionHeader( $header ) {
		$text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" . $this->getNativeData();

		return new WikitextContent( $text );
	}

	/**
	 * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
	 *
	 * @param Title $title
	 * @param User $user
	 * @param ParserOptions $popts
	 * @return Content
	 */
	public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
		global $wgParser, $wgConteLang;

		$text = $this->getNativeData();
		$pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );

		return new WikitextContent( $pst );
	}

	/**
	 * Returns a Content object with preload transformations applied (or this object if no transformations apply).
	 *
	 * @param Title $title
	 * @param ParserOptions $popts
	 * @return Content
	 */
	public function preloadTransform( Title $title, ParserOptions $popts ) {
		global $wgParser, $wgConteLang;

		$text = $this->getNativeData();
		$plt = $wgParser->getPreloadText( $text, $title, $popts );

		return new WikitextContent( $plt );
	}

	public function getRedirectChain() {
		$text = $this->getNativeData();
		return Title::newFromRedirectArray( $text );
	}

	public function getRedirectTarget() {
		$text = $this->getNativeData();
		return Title::newFromRedirect( $text );
	}

	public function getUltimateRedirectTarget() {
		$text = $this->getNativeData();
		return Title::newFromRedirectRecurse( $text );
	}

	/**
	 * Returns true if this content is not a redirect, and this content's text is countable according to
	 * the criteria defiend by $wgArticleCountMethod.
	 *
	 * @param Bool $hasLinks if it is known whether this content contains links, provide this information here,
	 *                        to avoid redundant parsing to find out.
	 * @param IContextSource $context context for parsing if necessary
	 *
	 * @return bool true if the content is countable
	 */
	public function isCountable( $hasLinks = null, Title $title = null ) {
		global $wgArticleCountMethod, $wgRequest;

		if ( $this->isRedirect( ) ) {
			return false;
		}

		$text = $this->getNativeData();

		switch ( $wgArticleCountMethod ) {
			case 'any':
				return true;
			case 'comma':
				return strpos( $text,  ',' ) !== false;
			case 'link':
				if ( $hasLinks === null ) { # not known, find out
					if ( !$title ) {
						$context = RequestContext::getMain();
						$title = $context->getTitle();
					}

					$po = $this->getParserOutput( $title, null, null, false );
					$links = $po->getLinks();
					$hasLinks = !empty( $links );
				}

				return $hasLinks;
		}
	}

	public function getTextForSummary( $maxlength = 250 ) {
		$truncatedtext = parent::getTextForSummary( $maxlength );

		#clean up unfinished links
		#XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
		$truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );

		return $truncatedtext;
	}

}

/**
 * @since WD.1
 */
class MessageContent extends TextContent {
	public function __construct( $msg_key, $params = null, $options = null ) {
		parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.

		$this->mMessageKey = $msg_key;

		$this->mParameters = $params;

		if ( is_null( $options ) ) {
			$options = array();
		}
		elseif ( is_string( $options ) ) {
			$options = array( $options );
		}

		$this->mOptions = $options;

		$this->mHtmlOptions = null;
	}

	/**
	 * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
	 */
	protected function getHtml(  ) {
		$opt = array_merge( $this->mOptions, array('parse') );

		return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
	}


	/**
	 * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
	 */
	public function getNativeData( ) {
		$opt = array_diff( $this->mOptions, array('parse', 'parseinline') );

		return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
	}

}

//////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////

/**
 * @since WD.1
 */
class JavaScriptContent extends TextContent {
	public function __construct( $text ) {
		parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
	}

	protected function getHtml( ) {
		$html = "";
		$html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
		$html .= htmlspecialchars( $this->getNativeData() );
		$html .= "\n</pre>\n";

		return $html;
	}

}

/**
 * @since WD.1
 */
class CssContent extends TextContent {
	public function __construct( $text ) {
		parent::__construct($text, CONTENT_MODEL_CSS);
	}

	protected function getHtml( ) {
		$html = "";
		$html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
		$html .= htmlspecialchars( $this->getNativeData() );
		$html .= "\n</pre>\n";

		return $html;
	}
}


ContentHandler.php[edit]

<?php


class MWContentSerializationException extends MWException {

}


abstract class ContentHandler {

	/**
	 * Serializes Content object of the type supported by this ContentHandler.
	 */
	public abstract function serializeContent( Content $content, $format = null );

	/**
	 * Unserializes a Content object of the type supported by this ContentHandler.
	 */
	public abstract function unserializeContent( $blob, $format = null );

	/**
	 * Creates an empty Content object of the type supported by this ContentHandler.
	 */
	public abstract function makeEmptyContent();

	/**
	 * Returns a list of serialization formats supported by the serializeContent() and unserializeContent() methods of
	 * this ContentHandler.
	 */
	public function getSupportedFormats() {
		return $this->mSupportedFormats;
	}

	/**
	 * The format used for serialization/deserialization per default by this ContentHandler.
	 *
	 * This default implementation will return the first element of the array of formats
	 * that was passed to the constructor.
	 *
	 * @since WD.1
	 *
	 * @return String the name of the default serialiozation format as a MIME type
	 */
	public function getDefaultFormat() {
		return $this->mSupportedFormats[0];
	}

	///////////////////////////////////////////////////////////////////////////////

	/**
	 * Returns if the content is consistent with the database, that is if saving it to the database would not violate any
	 * global constraints.
	 */
	public function isConsistentWithDatabase( Content $content ) {
		//Q: when, where and how often should this be called? 
		//Q: need $dbr as a param?
		return true;
	}

	/**
	 * Returns overrides for action handlers.
	 */
	public function getActionOverrides() {
		//Q: how to mix with WikiPage::getActionOverrides()
		//Q: how to get the appropriate page display (View action) injected into ImagePage and CategoryPage?
		return array();
	}

	/**
	 * Factory
	 */
	public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, 
										 $refreshCache = false, $unhide = false ) {

		//Q: is there a better/more abstract way? Should we try to get a common, abstract representation for diffs?
		//return new DifferenceEngine( $context, $old, $new, $rcid, $refreshCache, $unhide );
	}

	/**
	 * attempts to merge differences between three versions.
	 * Returns a new Content object for a clean merge and false for failure or a conflict.
	 */
	public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
		return false; //implementation for text is below
	}

	/**
	 * Return an applicable autosummary if one exists for the given edit.
	 */
	public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
		//...
	}

	/**
	 * Auto-generates a deletion reason
	 */
	public function getAutoDeleteReason( Title $title, &$hasHistory ) {
		//...
	}

	/**
	 * Get the Content object that needs to be saved in order to undo all revisions
	 * between $undo and $undoafter. 
	 */
	public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
		//...
		
		$undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );

		return $undone_content;
	}

	/**
	 * Returns true for content models that support caching using the ParserCache mechanism.
	 * See WikiPage::isParserCacheUser().
	 */
	public function isParserCacheSupported() {
		//Q: can this ever be false?
		return true; 
	}
	
	/**
	 * Get updates to be performed on deletion
	 */
	public function getDeletionUpdates( WikiPage $page ) {
		return array(
			new LinksDeletionUpdate( $page ),
		);
	}
	
	///////////////////////////////////////////////////////////////////////////////////////////////////////////////////

	/**
	 * Conveniance function for getting flat text from a Content object. This should only
	 * be used in the context of backwards compatibility with code that is not yet able
	 * to handle Content objects!
	 */
	public static function getContentText( Content $content = null ) {
		global $wgContentHandlerTextFallback;
		
		//Q: sensible B/C?

		if ( is_null( $content ) ) {
			return '';
		}

		if ( $content instanceof TextContent ) {
			return $content->getNativeData();
		}

		if ( $wgContentHandlerTextFallback == 'fail' ) {
			throw new MWException( "Attempt to get text from Content with model " . $content->getModel() );
		}

		if ( $wgContentHandlerTextFallback == 'serialize' ) {
			return $content->serialize();
		}

		return null;
	}

	/**
	 * Returns the name of the default content model to be used for the page with the given title.
	 */
	public static function getDefaultModelFor( Title $title ) {
		global $wgNamespaceContentModels;

		// NOTE: this method must not rely on $title->getContentModel() directly or indirectly,
		//       because it is used to initialized the mContentModel memebr.

		$ns = $title->getNamespace();

		$ext = false;
		$m = null;
		$model = null;

		if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
			$model = $wgNamespaceContentModels[ $ns ];
		}

		// hook can determin default model
		if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
			if ( !is_null( $model ) ) {
				return $model;
			}
		}

		// Could this page contain custom CSS or JavaScript, based on the title?
		$isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
		if ( $isCssOrJsPage ) {
			$ext = $m[1];
		}

		// hook can force js/css
		wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );

		// Is this a .css subpage of a user page?
		$isJsCssSubpage = NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
		if ( $isJsCssSubpage ) {
			$ext = $m[1];
		}

		// is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
		$isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
		$isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;

		// hook can override $isWikitext
		wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );

		if ( !$isWikitext ) {
			switch ( $ext ) {
				case 'js':
					return CONTENT_MODEL_JAVASCRIPT;
				case 'css':
					return CONTENT_MODEL_CSS;
				default:
					return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
			}
		}

		// we established that is must be wikitext

		return CONTENT_MODEL_WIKITEXT;
	}

	/**
	 * Returns the appropriate mime type for a given content format,
	 * or null if no mime type is known for this format.
	 */
	public static function getContentFormatMimeType( $id ) {
		global $wgContentFormatMimeTypes;

		if ( !isset( $wgContentFormatMimeTypes[ $id ] ) ) {
			return null;
		}

		return $wgContentFormatMimeTypes[ $id ];
	}

	/**
	 * Returns the localized name for a given content model,
	 * or null of no mime type is known.
	 */
	public static function getContentModelName( $id ) {
		$key = "content-model-$id";

		if ( wfEmptyMsg( $key ) ) return null;
		else return wfMsg( $key );
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////

/**
 * @since WD.1
 */
abstract class TextContentHandler extends ContentHandler {

	public function __construct( $modelId, $formats ) {
		parent::__construct( $modelId, $formats );
	}

	/**
	 * Returns the content's text as-is.
	 *
	 * @param Content $content
	 * @param String|null $format
	 * @return mixed
	 */
	public function serializeContent( Content $content, $format = null ) {
		$this->checkFormat( $format );
		return $content->getNativeData();
	}

	/**
	 * attempts to merge differences between three versions.
	 * Returns a new Content object for a clean merge and false for failure or a conflict.
	 *
	 * All three Content objects passed as parameters must have the same content model.
	 *
	 * This text-based implementation uses wfMerge().
	 *
	 * @param $oldContent String
	 * @param $myContent String
	 * @param $yourContent String
	 * @return Content|Bool
	 */
	public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
		$this->checkModelID( $oldContent->getModel() );
		$this->checkModelID( $myContent->getModel() );
		$this->checkModelID( $yourContent->getModel() );

		$format = $this->getDefaultFormat();

		$old = $this->serializeContent( $oldContent, $format );
		$mine = $this->serializeContent( $myContent, $format );
		$yours = $this->serializeContent( $yourContent, $format );

		$ok = wfMerge( $old, $mine, $yours, $result );

		if ( !$ok ) {
			return false;
		}

		if ( !$result ) {
			return $this->makeEmptyContent();
		}

		$mergedContent = $this->unserializeContent( $result, $format );
		return $mergedContent;
	}


}

/**
 * @since WD.1
 */
class WikitextContentHandler extends TextContentHandler {

	public function __construct( $modelId = CONTENT_MODEL_WIKITEXT ) {
		parent::__construct( $modelId, array( CONTENT_FORMAT_WIKITEXT ) );
	}

	public function unserializeContent( $text, $format = null ) {
		$this->checkFormat( $format );

		return new WikitextContent( $text );
	}

	public function makeEmptyContent() {
		return new WikitextContent( '' );
	}


}

#XXX: make ScriptContentHandler base class with plugin interface for syntax highlighting?

/**
 * @since WD.1
 */
class JavaScriptContentHandler extends TextContentHandler {

	public function __construct( $modelId = CONTENT_MODEL_JAVASCRIPT ) {
		parent::__construct( $modelId, array( CONTENT_FORMAT_JAVASCRIPT ) );
	}

	public function unserializeContent( $text, $format = null ) {
		$this->checkFormat( $format );

		return new JavaScriptContent( $text );
	}

	public function makeEmptyContent() {
		return new JavaScriptContent( '' );
	}
}

/**
 * @since WD.1
 */
class CssContentHandler extends TextContentHandler {

	public function __construct( $modelId = CONTENT_MODEL_CSS ) {
		parent::__construct( $modelId, array( CONTENT_FORMAT_CSS ) );
	}

	public function unserializeContent( $text, $format = null ) {
		$this->checkFormat( $format );

		return new CssContent( $text );
	}

	public function makeEmptyContent() {
		return new CssContent( '' );
	}

}