Introducing ContentHandler::canBeUsedOn()
[lhc/web/wiklou.git] / includes / ContentHandler.php
index ce7d083..584a6ad 100644 (file)
@@ -8,46 +8,55 @@ class MWContentSerializationException extends MWException {
 }
 
 /**
- * A content handler knows how do deal with a specific type of content on a wiki page.
- * Content is stored in the database in a serialized form (using a serialization format aka mime type)
- * and is be unserialized into it's native PHP representation (the content model), which is wrapped in
- * an instance of the appropriate subclass of Content.
+ * A content handler knows how do deal with a specific type of content on a wiki
+ * page. Content is stored in the database in a serialized form (using a
+ * serialization format a.k.a. MIME type) and is unserialized into its native
+ * PHP representation (the content model), which is wrapped in an instance of
+ * the appropriate subclass of Content.
  *
- * ContentHandler instances are stateless singletons that serve, among other things, as a factory for
- * Content objects. Generally, there is one subclass of ContentHandler and one subclass of Content
- * for every type of content model.
+ * ContentHandler instances are stateless singletons that serve, among other
+ * things, as a factory for Content objects. Generally, there is one subclass
+ * of ContentHandler and one subclass of Content for every type of content model.
  *
- * Some content types have a flat model, that is, their native representation is the
- * same as their serialized form. Examples would be JavaScript and CSS code. As of now,
- * this also applies to wikitext (mediawiki's default content type), but wikitext
- * content may be represented by a DOM or AST structure in the future.
+ * Some content types have a flat model, that is, their native representation
+ * is the same as their serialized form. Examples would be JavaScript and CSS
+ * code. As of now, this also applies to wikitext (MediaWiki's default content
+ * type), but wikitext content may be represented by a DOM or AST structure in
+ * the future.
  *
  * @since 1.WD
  */
 abstract class ContentHandler {
 
        /**
-        * Convenience 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!
+        * Convenience 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!
         *
         * If $content is null, this method returns the empty string.
         *
-        * If $content is an instance of TextContent, this method returns the flat text as returned by $content->getNativeData().
+        * If $content is an instance of TextContent, this method returns the flat
+        * text as returned by $content->getNativeData().
         *
-        * If $content is not a TextContent object, the behavior of this method depends on the global $wgContentHandlerTextFallback:
-        * * If $wgContentHandlerTextFallback is 'fail' and $content is not a TextContent object, an MWException is thrown.
-        * * If $wgContentHandlerTextFallback is 'serialize' and $content is not a TextContent object, $content->serialize()
-        * is called to get a string form of the content.
-        * * If $wgContentHandlerTextFallback is 'ignore' and $content is not a TextContent object, this method returns null.
-        * * otherwise, the behaviour is undefined.
+        * If $content is not a TextContent object, the behavior of this method
+        * depends on the global $wgContentHandlerTextFallback:
+        * - If $wgContentHandlerTextFallback is 'fail' and $content is not a
+        *   TextContent object, an MWException is thrown.
+        * - If $wgContentHandlerTextFallback is 'serialize' and $content is not a
+        *   TextContent object, $content->serialize() is called to get a string
+        *   form of the content.
+        * - If $wgContentHandlerTextFallback is 'ignore' and $content is not a
+        *   TextContent object, this method returns null.
+        * - otherwise, the behaviour is undefined.
         *
         * @since WD.1
+        * @deprecated since WD.1. Always try to use the content object.
         *
         * @static
-        * @param Content|null $content
+        * @param $content Content|null
         * @return null|string the textual form of $content, if available
-        * @throws MWException if $content is not an instance of TextContent and $wgContentHandlerTextFallback was set to 'fail'.
+        * @throws MWException if $content is not an instance of TextContent and
+        *   $wgContentHandlerTextFallback was set to 'fail'.
         */
        public static function getContentText( Content $content = null ) {
                global $wgContentHandlerTextFallback;
@@ -61,7 +70,10 @@ abstract class ContentHandler {
                }
 
                if ( $wgContentHandlerTextFallback == 'fail' ) {
-                       throw new MWException( "Attempt to get text from Content with model " . $content->getModel() );
+                       throw new MWException(
+                               "Attempt to get text from Content with model " .
+                               $content->getModel()
+                       );
                }
 
                if ( $wgContentHandlerTextFallback == 'serialize' ) {
@@ -72,26 +84,34 @@ abstract class ContentHandler {
        }
 
        /**
-        * Convenience function for creating a Content object from a given textual representation.
+        * Convenience function for creating a Content object from a given textual
+        * representation.
         *
-        * $text will be deserialized into a Content object of the model specified by $modelId (or,
-        * if that is not given, $title->getContentModel()) using the given format.
+        * $text will be deserialized into a Content object of the model specified
+        * by $modelId (or, if that is not given, $title->getContentModel()) using
+        * the given format.
         *
         * @since WD.1
         *
         * @static
         *
-        * @param string      $text    the textual representation, will be unserialized to create the Content object
-        * @param null|Title  $title   the title of the page this text belongs to. Required if $modelId is not provided.
-        * @param null|String $modelId the model to deserialize to. If not provided, $title->getContentModel() is used.
-        * @param null|String $format  the format to use for deserialization. If not given, the model's default format is used.
+        * @param $text string the textual representation, will be
+        *    unserialized to create the Content object
+        * @param $title null|Title the title of the page this text belongs to.
+        *    Required if $modelId is not provided.
+        * @param $modelId null|string the model to deserialize to. If not provided,
+        *    $title->getContentModel() is used.
+        * @param $format null|string the format to use for deserialization. If not
+        *    given, the model's default format is used.
         *
         * @return Content a Content object representing $text
         *
-        * @throw MWException if $model or $format is not supported or if $text can not be unserialized using $format.
+        * @throw MWException if $model or $format is not supported or if $text can
+        *    not be unserialized using $format.
         */
-       public static function makeContent( $text, Title $title = null, $modelId = null, $format = null ) {
-
+       public static function makeContent( $text, Title $title = null,
+               $modelId = null, $format = null )
+       {
                if ( is_null( $modelId ) ) {
                        if ( is_null( $title ) ) {
                                throw new MWException( "Must provide a Title object or a content model ID." );
@@ -105,18 +125,26 @@ abstract class ContentHandler {
        }
 
        /**
-        * Returns the name of the default content model to be used for the page with the given title.
+        * Returns the name of the default content model to be used for the page
+        * with the given title.
         *
         * Note: There should rarely be need to call this method directly.
-        * To determine the actual content model for a given page, use Title::getContentModel().
-        *
-        * Which model is to be used per default for the page is determined based on several factors:
-        * * The global setting $wgNamespaceContentModels specifies a content model per namespace.
-        * * The hook DefaultModelFor may be used to override the page's default model.
-        * * Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript model if they end in .js or .css, respectively.
-        * * Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
-        * * The hook TitleIsCssOrJsPage may be used to force a page to use the CSS or JavaScript model if they end in .js or .css, respectively.
-        * * The hook TitleIsWikitextPage may be used to force a page to use the wikitext model.
+        * To determine the actual content model for a given page, use
+        * Title::getContentModel().
+        *
+        * Which model is to be used by default for the page is determined based
+        * on several factors:
+        * - The global setting $wgNamespaceContentModels specifies a content model
+        *   per namespace.
+        * - The hook DefaultModelFor may be used to override the page's default
+        *   model.
+        * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript
+        *   model if they end in .js or .css, respectively.
+        * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+        * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS
+        *   or JavaScript model if they end in .js or .css, respectively.
+        * - The hook TitleIsWikitextPage may be used to force a page to use the
+        *   wikitext model.
         *
         * If none of the above applies, the wikitext model is used.
         *
@@ -125,14 +153,14 @@ abstract class ContentHandler {
         * @since WD.1
         *
         * @static
-        * @param Title $title
+        * @param $title Title
         * @return null|string default model name for the page given by $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 member.
+               //       because it is used to initialize the mContentModel member.
 
                $ns = $title->getNamespace();
 
@@ -144,7 +172,7 @@ abstract class ContentHandler {
                        $model = $wgNamespaceContentModels[ $ns ];
                }
 
-               // hook can determin default model
+               // Hook can determine default model
                if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
                        if ( !is_null( $model ) ) {
                                return $model;
@@ -157,20 +185,22 @@ abstract class ContentHandler {
                        $ext = $m[1];
                }
 
-               // hook can force js/css
+               // 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 );
+               $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?
+               // 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
+               // Hook can override $isWikitext
                wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
 
                if ( !$isWikitext ) {
@@ -184,18 +214,18 @@ abstract class ContentHandler {
                        }
                }
 
-               // we established that is must be wikitext
+               // We established that it must be wikitext
 
                return CONTENT_MODEL_WIKITEXT;
        }
 
        /**
-        * returns the appropriate ContentHandler singleton for the given title
+        * Returns the appropriate ContentHandler singleton for the given title.
         *
         * @since WD.1
         *
         * @static
-        * @param Title $title
+        * @param $title Title
         * @return ContentHandler
         */
        public static function getForTitle( Title $title ) {
@@ -204,12 +234,13 @@ abstract class ContentHandler {
        }
 
        /**
-        * returns the appropriate ContentHandler singleton for the given Content object
+        * Returns the appropriate ContentHandler singleton for the given Content
+        * object.
         *
         * @since WD.1
         *
         * @static
-        * @param Content $content
+        * @param $content Content
         * @return ContentHandler
         */
        public static function getForContent( Content $content ) {
@@ -218,145 +249,140 @@ abstract class ContentHandler {
        }
 
        /**
-        * returns the ContentHandler singleton for the given model id. Use the CONTENT_MODEL_XXX constants to
-        * identify the desired content model.
+        * @var Array A Cache of ContentHandler instances by model id
+        */
+       static $handlers;
+
+       /**
+        * Returns the ContentHandler singleton for the given model ID. Use the
+        * CONTENT_MODEL_XXX constants to identify the desired content model.
         *
-        * ContentHandler singletons are take from the global $wgContentHandlers array. Keys in that array are
-        * model names, the values are either ContentHandler singleton objects, or strings specifying the appropriate
+        * ContentHandler singletons are taken from the global $wgContentHandlers
+        * array. Keys in that array are model names, the values are either
+        * ContentHandler singleton objects, or strings specifying the appropriate
         * subclass of ContentHandler.
         *
-        * If a class name in encountered when looking up the singleton for a given model name, the class is
-        * instantiated and the class name is replaced by te resulting singleton in $wgContentHandlers.
+        * If a class name is encountered when looking up the singleton for a given
+        * model name, the class is instantiated and the class name is replaced by
+        * the resulting singleton in $wgContentHandlers.
         *
-        * If no ContentHandler is defined for the desired $modelId, the ContentHandler may be provided by the
-        * a ContentHandlerForModelID hook. if no ContentHandler can be determined, an MWException is raised.
+        * If no ContentHandler is defined for the desired $modelId, the
+        * ContentHandler may be provided by the ContentHandlerForModelID hook.
+        * If no ContentHandler can be determined, an MWException is raised.
         *
         * @since WD.1
         *
         * @static
-        * @param $modelId int the id of the content model for which to get a handler. Use CONTENT_MODEL_XXX constants.
-        * @return ContentHandler the ContentHandler singleton for handling the model given by $modelId
+        * @param $modelId String The ID of the content model for which to get a
+        *    handler. Use CONTENT_MODEL_XXX constants.
+        * @return ContentHandler The ContentHandler singleton for handling the
+        *    model given by $modelId
         * @throws MWException if no handler is known for $modelId.
         */
        public static function getForModelID( $modelId ) {
                global $wgContentHandlers;
 
+               if ( isset( ContentHandler::$handlers[$modelId] ) ) {
+                       return ContentHandler::$handlers[$modelId];
+               }
+
                if ( empty( $wgContentHandlers[$modelId] ) ) {
                        $handler = null;
 
                        wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
 
-                       if ( $handler ) { // NOTE: may be a string or an object, either is fine!
-                               $wgContentHandlers[$modelId] = $handler;
-                       } else {
+                       if ( $handler === null ) {
                                throw new MWException( "No handler for model #$modelId registered in \$wgContentHandlers" );
                        }
-               }
 
-               if ( is_string( $wgContentHandlers[$modelId] ) ) {
+                       if ( !( $handler instanceof ContentHandler ) ) {
+                               throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
+                       }
+               } else {
                        $class = $wgContentHandlers[$modelId];
-                       $wgContentHandlers[$modelId] = new $class( $modelId );
+                       $handler = new $class( $modelId );
+
+                       if ( !( $handler instanceof ContentHandler ) ) {
+                               throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
+                       }
                }
 
-               return $wgContentHandlers[$modelId];
+               ContentHandler::$handlers[$modelId] = $handler;
+               return ContentHandler::$handlers[$modelId];
        }
 
        /**
-        * Returns the appropriate mime type for a given content format,
-        * or null if no mime type is known for this format.
+        * Returns the localized name for a given content model.
         *
-        * Mime types can be registered in the global array $wgContentFormatMimeTypes.
+        * Model names are localized using system messages. Message keys
+        * have the form content-model-$name, where $name is getContentModelName( $id ).
         *
         * @static
-        * @param int $id the content format id, as given by a CONTENT_FORMAT_XXX constant
-        *        or returned by Revision::getContentFormat().
+        * @param $name String The content model ID, as given by a CONTENT_MODEL_XXX
+        *    constant or returned by Revision::getContentModel().
         *
-        * @return String|null the content format's mime type.
+        * @return string The content format's localized name.
+        * @throws MWException if the model id isn't known.
         */
-       public static function getContentFormatMimeType( $id ) {
-               global $wgContentFormatMimeTypes;
-
-               if ( !isset( $wgContentFormatMimeTypes[ $id ] ) ) {
-                       return null;
-               }
+       public static function getLocalizedName( $name ) {
+               $key = "content-model-$name";
 
-               return $wgContentFormatMimeTypes[ $id ];
+               if ( wfEmptyMsg( $key ) ) return $name;
+               else return wfMsg( $key );
        }
 
-       /**
-        * Returns the content format if for a given mime type,
-        * or null if no format id if known for this mime type.
-        *
-        * Mime types can be registered in the global array $wgContentFormatMimeTypes.
-        *
-        * @static
-        * @param String $mime the mime type
-        *
-        * @return int|null the format id, as defined by a CONTENT_FORMAT_XXX constant
-        */
-       public static function getContentFormatID( $mime ) {
-               global $wgContentFormatMimeTypes;
-
-               static $format_ids = null;
+       public static function getContentModels() {
+               global $wgContentHandlers;
 
-               if ( $format_ids === null ) {
-                       $format_ids = array_flip( $wgContentFormatMimeTypes );
-               }
+               return array_keys( $wgContentHandlers );
+       }
 
-               if ( !isset( $format_ids[ $mime ] ) ) {
-                       return null;
-               }
+       public static function getAllContentFormats() {
+               global $wgContentHandlers;
 
-               return $format_ids[ $mime ];
-       }
+               $formats = array();
 
-       /**
-        * Returns the localized name for a given content model,
-        * or null of no mime type is known.
-        *
-        * Model names are localized using system messages. Message keys
-        * have the form content-model-$id.
-        *
-        * @static
-        * @param int $id the content model id, as given by a CONTENT_MODEL_XXX constant
-        *        or returned by Revision::getContentModel().
-        *
-        * @return String|null the content format's mime type.
-        */
-       public static function getContentModelName( $id ) {
-               $key = "content-model-$id";
+               foreach ( $wgContentHandlers as $model => $class ) {
+                       $handler = ContentHandler::getForModelID( $model );
+                       $formats = array_merge( $formats, $handler->getSupportedFormats() );
+               }
 
-               if ( wfEmptyMsg( $key ) ) return null;
-               else return wfMsg( $key );
+               $formats = array_unique( $formats );
+               return $formats;
        }
 
-       // ----------------------------------------------------------------------------------------------------------
+       // ------------------------------------------------------------------------
 
        protected $mModelID;
        protected $mSupportedFormats;
 
        /**
-        * Constructor, initializing the ContentHandler instance with it's model id and a list of supported formats.
-        * Values for the parameters are typically provided as literals by subclasses' constructors.
+        * Constructor, initializing the ContentHandler instance with its model ID
+        * and a list of supported formats. Values for the parameters are typically
+        * provided as literals by subclass's constructors.
         *
-        * @param int $modelId (use CONTENT_MODEL_XXX constants).
-        * @param array $formats list for supported serialization formats (typically as MIME types)
+        * @param $modelId String (use CONTENT_MODEL_XXX constants).
+        * @param $formats array List for supported serialization formats
+        *    (typically as MIME types)
         */
        public function __construct( $modelId, $formats ) {
                $this->mModelID = $modelId;
                $this->mSupportedFormats = $formats;
-       }
 
+               $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
+               $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
+               $this->mModelName = strtolower( $this->mModelName );
+       }
 
        /**
-        * Serializes Content object of the type supported by this ContentHandler.
+        * Serializes Content object of the type supported by this ContentHandler.
         *
         * @since WD.1
         *
         * @abstract
-        * @param Content $content the Content object to serialize
-        * @param null $format the desired serialization format
-        * @return String serialized form of the content
+        * @param $content Content The Content object to serialize
+        * @param $format null|String The desired serialization format
+        * @return string Serialized form of the content
         */
        public abstract function serializeContent( Content $content, $format = null );
 
@@ -366,14 +392,15 @@ abstract class ContentHandler {
         * @since WD.1
         *
         * @abstract
-        * @param $blob String serialized form of the content
-        * @param null $format the format used for serialization
+        * @param $blob string serialized form of the content
+        * @param $format null|String the format used for serialization
         * @return Content the Content object created by deserializing $blob
         */
        public abstract function unserializeContent( $blob, $format = null );
 
        /**
-        * Creates an empty Content object of the type supported by this ContentHandler.
+        * Creates an empty Content object of the type supported by this
+        * ContentHandler.
         *
         * @since WD.1
         *
@@ -382,39 +409,39 @@ abstract class ContentHandler {
        public abstract function makeEmptyContent();
 
        /**
-        * Returns the model id that identifies the content model this ContentHandler can handle.
-        * Use with the CONTENT_MODEL_XXX constants.
+        * Returns the model id that identifies the content model this
+        * ContentHandler can handle. Use with the CONTENT_MODEL_XXX constants.
         *
         * @since WD.1
         *
-        * @return int the model id
+        * @return String The model ID
         */
        public function getModelID() {
                return $this->mModelID;
        }
 
        /**
-        * Throws an MWException if $model_id is not the id of the content model
+        * Throws an MWException if $model_id is not the ID of the content model
         * supported by this ContentHandler.
         *
         * @since WD.1
         *
-        * @param int $model_id the model to check
+        * @param String $model_id The model to check
         *
         * @throws MWException
         */
        protected function checkModelID( $model_id ) {
                if ( $model_id !== $this->mModelID ) {
-                       $model_name = ContentHandler::getContentModelName( $model_id );
-                       $own_model_name = ContentHandler::getContentModelName( $this->mModelID );
-
-                       throw new MWException( "Bad content model: expected {$this->mModelID} ($own_model_name) but got found $model_id ($model_name)." );
+                       throw new MWException( "Bad content model: " .
+                               "expected {$this->mModelID} " .
+                               "but got $model_id." );
                }
        }
 
        /**
-        * Returns a list of serialization formats supported by the serializeContent() and unserializeContent() methods of
-        * this ContentHandler.
+        * Returns a list of serialization formats supported by the
+        * serializeContent() and unserializeContent() methods of this
+        * ContentHandler.
         *
         * @since WD.1
         *
@@ -425,29 +452,30 @@ abstract class ContentHandler {
        }
 
        /**
-        * The format used for serialization/deserialization per default by this ContentHandler.
+        * The format used for serialization/deserialization by default by this
+        * ContentHandler.
         *
-        * This default implementation will return the first element of the array of formats
-        * that was passed to the constructor.
+        * 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 serialization format as a MIME type
+        * @return string the name of the default serialization format as a MIME type
         */
        public function getDefaultFormat() {
                return $this->mSupportedFormats[0];
        }
 
        /**
-        * Returns true if $format is a serialization format supported by this ContentHandler,
-        * and false otherwise.
+        * Returns true if $format is a serialization format supported by this
+        * ContentHandler, and false otherwise.
         *
-        * Note that if $format is null, this method always returns true, because null
-        * means "use the default format".
+        * Note that if $format is null, this method always returns true, because
+        * null means "use the default format".
         *
         * @since WD.1
         *
-        * @param String $format the serialization format to check
+        * @param $format string the serialization format to check
         * @return bool
         */
        public function isSupportedFormat( $format ) {
@@ -460,37 +488,23 @@ abstract class ContentHandler {
        }
 
        /**
-        * Throws an MWException if isSupportedFormat( $format ) is not true. Convenient
-        * for checking whether a format provided as a parameter is actually supported.
+        * Throws an MWException if isSupportedFormat( $format ) is not true.
+        * Convenient for checking whether a format provided as a parameter is
+        * actually supported.
         *
-        * @param String $format the serialization format to check
+        * @param $format string the serialization format to check
         *
         * @throws MWException
         */
        protected function checkFormat( $format ) {
                if ( !$this->isSupportedFormat( $format ) ) {
-                       throw new MWException( "Format $format is not supported for content model " . $this->getModelID() );
+                       throw new MWException(
+                               "Format $format is not supported for content model "
+                               . $this->getModelID()
+                       );
                }
        }
 
-       /**
-        * Returns if the content is consistent with the database, that is if saving it to the database would not violate any
-        * global constraints.
-        *
-        * Content needs to be valid using this method before it can be saved.
-        *
-        * This default implementation always returns true.
-        *
-        * @since WD.1
-        *
-        * @param \Content $content
-        *
-        * @return boolean
-        */
-       public function isConsistentWithDatabase( Content $content ) {
-               return true;
-       }
-
        /**
         * Returns overrides for action handlers.
         * Classes listed here will be used instead of the default one when
@@ -506,22 +520,25 @@ abstract class ContentHandler {
        }
 
        /**
-        * Factory creating an appropriate DifferenceEngine for this content model.
+        * Factory for creating an appropriate DifferenceEngine for this content model.
         *
         * @since WD.1
         *
-        * @param            $context      IContextSource context to use, anything else will be ignored
-        * @param            $old          Integer old ID we want to show and diff with.
-        * @param int|String $new          String either 'prev' or 'next'.
-        * @param            $rcid         Integer ??? FIXME (default 0)
-        * @param            $refreshCache boolean If set, refreshes the diff cache
-        * @param            $unhide       boolean If set, allow viewing deleted revs
+        * @param $context IContextSource context to use, anything else will be
+        *    ignored
+        * @param $old Integer Old ID we want to show and diff with.
+        * @param $new int|string String either 'prev' or 'next'.
+        * @param $rcid Integer ??? FIXME (default 0)
+        * @param $refreshCache boolean If set, refreshes the diff cache
+        * @param $unhide boolean If set, allow viewing deleted revs
         *
         * @return DifferenceEngine
         */
-       public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, #FIMXE: use everywhere!
-                                                                                $refreshCache = false, $unhide = false ) {
-
+       public function createDifferenceEngine( IContextSource $context,
+               $old = 0, $new = 0,
+               $rcid = 0, # FIXME: use everywhere!
+               $refreshCache = false, $unhide = false
+       ) {
                $this->checkModelID( $context->getTitle()->getContentModel() );
 
                $diffEngineClass = $this->getDiffEngineClass();
@@ -529,6 +546,89 @@ abstract class ContentHandler {
                return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
        }
 
+       /**
+        * Get the language in which the content of the given page is written.
+        *
+        * This default implementation just returns $wgContLang (except for pages in the MediaWiki namespace)
+        *
+        * Note that a page's language must be permanent and cacheable, that is, it must not depend
+        * on user preferences, request parameters or session state. The only exception is pages in the
+        * MediaWiki namespace.
+        *
+        * Also note that the page language may or may not depend on the actual content of the page,
+        * that is, this method may load the content in order to determine the language.
+        *
+        * @since 1.WD
+        *
+        * @param Title        $title the page to determine the language for.
+        * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+        *
+        * @return Language the page's language code
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               global $wgContLang;
+
+               if ( $title->getNamespace() == NS_MEDIAWIKI ) {
+                       // Parse mediawiki messages with correct target language
+                       list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
+                       return wfGetLangObj( $lang );
+               }
+
+               return $wgContLang;
+       }
+
+       /**
+        * Get the language in which the content of this page is written when
+        * viewed by user. Defaults to $this->getPageLanguage(), but if the user
+        * specified a preferred variant, the variant will be used.
+        *
+        * This default implementation just returns $this->getPageLanguage( $title, $content ) unless
+        * the user specified a preferred variant.
+        *
+        * Note that the pages view language is not cacheable, since it depends on user settings.
+        *
+        * Also note that the page language may or may not depend on the actual content of the page,
+        * that is, this method may load the content in order to determine the language.
+        *
+        * @since 1.WD
+        *
+        * @param Title        $title the page to determine the language for.
+        * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+        *
+        * @return Language the page's language code for viewing
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               $pageLang = $this->getPageLanguage( $title, $content );
+
+               if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
+                       // If the user chooses a variant, the content is actually
+                       // in a language whose code is the variant code.
+                       $variant = $pageLang->getPreferredVariant();
+                       if ( $pageLang->getCode() !== $variant ) {
+                               $pageLang = Language::factory( $variant );
+                       }
+               }
+
+               return $pageLang;
+       }
+
+       /**
+        * Determines whether the content type handled by this ContentHandler
+        * can be used on the given page.
+        *
+        * This default implementation always returns true.
+        * Subclasses may override this to restrict the use of this content model to specific locations,
+        * typically based on the namespace or some other aspect of the title, such as a special suffix
+        * (e.g. ".svg" for SVG content).
+        *
+        * @param Title $title the page's title.
+        *
+        * @return bool true if content of this kind can be used on the given page, false otherwise.
+        */
+       public function canBeUsedOn( Title $title ) {
+               return true;
+       }
+
        /**
         * Returns the name of the diff engine to use.
         *
@@ -541,16 +641,17 @@ abstract class ContentHandler {
        }
 
        /**
-        * attempts to merge differences between three versions.
-        * Returns a new Content object for a clean merge and false for failure or a conflict.
+        * Attempts to merge differences between three versions.
+        * Returns a new Content object for a clean merge and false for failure or
+        * a conflict.
         *
         * This default implementation always returns false.
         *
         * @since WD.1
         *
-        * @param Content|String $oldContent  String
-        * @param Content|String $myContent   String
-        * @param Content|String $yourContent String
+        * @param $oldContent Content|string  String
+        * @param $myContent Content|string   String
+        * @param $yourContent Content|string String
         *
         * @return Content|Bool
         */
@@ -565,7 +666,7 @@ abstract class ContentHandler {
         *
         * @param $oldContent Content|null: the previous text of the page.
         * @param $newContent Content|null: The submitted text of the page.
-        * @param $flags Int bit mask: a bit mask of flags submitted for the edit.
+        * @param $flags int Bit mask: a bit mask of flags submitted for the edit.
         *
         * @return string An appropriate auto-summary, or an empty string.
         */
@@ -584,41 +685,51 @@ abstract class ContentHandler {
                $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
                $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
 
-               if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
-
-                       $truncatedtext = $newContent->getTextForSummary(
-                               250
-                                       - strlen( wfMsgForContent( 'autoredircomment' ) )
-                                       - strlen( $rt->getFullText() ) );
-
-                       return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
+               if ( is_object( $rt ) ) {
+                       if ( !is_object( $ot )
+                               || !$rt->equals( $ot )
+                               || $ot->getFragment() != $rt->getFragment() )
+                       {
+                               $truncatedtext = $newContent->getTextForSummary(
+                                       250
+                                               - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
+                                               - strlen( $rt->getFullText() ) );
+
+                               return wfMessage( 'autoredircomment', $rt->getFullText() )
+                                               ->rawParams( $truncatedtext )->inContentLanguage()->text();
+                       }
                }
 
                // New page auto-summaries
                if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
-                       // If they're making a new article, give its text, truncated, in the summary.
+                       // If they're making a new article, give its text, truncated, in
+                       // the summary.
 
                        $truncatedtext = $newContent->getTextForSummary(
-                               200 - strlen( wfMsgForContent( 'autosumm-new' ) ) );
+                               200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
 
-                       return wfMsgForContent( 'autosumm-new', $truncatedtext );
+                       return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+                                       ->inContentLanguage()->text();
                }
 
                // Blanking auto-summaries
                if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
-                       return wfMsgForContent( 'autosumm-blank' );
-               } elseif ( !empty( $oldContent ) && $oldContent->getSize() > 10 * $newContent->getSize() && $newContent->getSize() < 500 ) {
+                       return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
+               } elseif ( !empty( $oldContent )
+                       && $oldContent->getSize() > 10 * $newContent->getSize()
+                       && $newContent->getSize() < 500 )
+               {
                        // Removing more than 90% of the article
 
                        $truncatedtext = $newContent->getTextForSummary(
-                               200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) );
+                               200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
 
-                       return wfMsgForContent( 'autosumm-replace', $truncatedtext );
+                       return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+                                       ->inContentLanguage()->text();
                }
 
-               // If we reach this point, there's no applicable auto-summary for our case, so our
-               // auto-summary is empty.
-
+               // If we reach this point, there's no applicable auto-summary for our
+               // case, so our auto-summary is empty.
                return '';
        }
 
@@ -629,10 +740,12 @@ abstract class ContentHandler {
         *
         * @param $title Title: the page's title
         * @param &$hasHistory Boolean: whether the page has a history
-        * @return mixed String containing deletion reason or empty string, or boolean false
-        *    if no revision occurred
+        * @return mixed String containing deletion reason or empty string, or
+        *    boolean false if no revision occurred
         *
-        * @XXX &$hasHistory is extremely ugly, it's here because WikiPage::getAutoDeleteReason() and Article::getReason() have it / want it.
+        * @XXX &$hasHistory is extremely ugly, it's here because
+        * WikiPage::getAutoDeleteReason() and Article::getReason()
+        * have it / want it.
         */
        public function getAutoDeleteReason( Title $title, &$hasHistory ) {
                $dbw = wfGetDB( DB_MASTER );
@@ -651,7 +764,8 @@ abstract class ContentHandler {
                $this->checkModelID( $content->getModel() );
 
                // If the page is blank, use the text from the previous revision,
-               // which can only be blank if there's a move/import/protect dummy revision involved
+               // which can only be blank if there's a move/import/protect dummy
+               // revision involved
                if ( $content->getSize() == 0 ) {
                        $prev = $rev->getPrevious();
 
@@ -664,7 +778,10 @@ abstract class ContentHandler {
                // Find out if there was only one contributor
                // Only scan the last 20 revisions
                $res = $dbw->select( 'revision', 'rev_user_text',
-                       array( 'rev_page' => $title->getArticleID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
+                       array(
+                               'rev_page' => $title->getArticleID(),
+                               $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0'
+                       ),
                        __METHOD__,
                        array( 'LIMIT' => 20 )
                );
@@ -694,12 +811,16 @@ abstract class ContentHandler {
                if ( $blank ) {
                        // The current revision is blank and the one before is also
                        // blank. It's just not our lucky day
-                       $reason = wfMsgForContent( 'exbeforeblank', '$1' );
+                       $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
                } else {
                        if ( $onlyAuthor ) {
-                               $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
+                               $reason = wfMessage(
+                                       'excontentauthor',
+                                       '$1',
+                                       $onlyAuthor
+                               )->inContentLanguage()->text();
                        } else {
-                               $reason = wfMsgForContent( 'excontent', '$1' );
+                               $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
                        }
                }
 
@@ -717,72 +838,18 @@ abstract class ContentHandler {
                return $reason;
        }
 
-       /**
-        * Parse the Content object and generate a ParserObject from the result. $result->getText() can
-        * be used to obtain the generated HTML. If no HTML is needed,  $generateHtml can be set to false;
-        * in that case, $result->getText() may return null.
-        *
-        * @param Content $content the content to render
-        * @param Title $title the page title to use as a context for rendering
-        * @param null|int $revId the revision being rendered (optional)
-        * @param null|ParserOptions $options any parser options
-        * @param Boolean $generateHtml whether to generate Html (default: true). If false,
-        *        the result of calling getText() on the ParserOutput object returned by
-        *        this method is undefined.
-        *
-        * @since WD.1
-        *
-        * @return ParserOutput
-        */
-       public abstract function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true );
-       #TODO: make RenderOutput and RenderOptions base classes
-
-       /**
-        * 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 information about the new content.
-        *
-        * This default implementation calls $this->getParserOutput( $content, $title, null, null, false ), and then
-        * calls getSecondaryDataUpdates( $title, $recursive ) on the resulting ParserOutput object.
-        *
-        * Subclasses may implement this to determine the necessary updates more efficiently, or make use of information
-        * about the old content.
-        *
-        * @param Content $content the content for determining the necessary updates
-        * @param Title $title the context for determining the necessary updates
-        * @param Content|null $old an optional Content object representing the previous content, i.e. the content being
-        *                     replaced by this Content object.
-        * @param boolean $recursive whether to include recursive updates (default: false).
-        * @param ParserOutput|null $parserOutput optional ParserOutput object. Provide if you have one handy, to avoid re-parsing
-        *        of the content.
-        *
-        * @return Array. A list of DataUpdate objects for putting information about this content object somewhere.
-        *
-        * @since WD.1
-        */
-       public function getSecondaryDataUpdates( Content $content, Title $title, Content $old = null,
-                                                                                       $recursive = true, ParserOutput $parserOutput = null ) {
-
-               if ( !$parserOutput ) {
-                       $parserOutput = $this->getParserOutput( $content, $title, null, null, false );
-               }
-
-               return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
-       }
-
-
        /**
         * Get the Content object that needs to be saved in order to undo all revisions
         * between $undo and $undoafter. Revisions must belong to the same page,
-        * must exist and must not be deleted
+        * must exist and must not be deleted.
         *
         * @since WD.1
         *
-        * @param $current Revision the current text
-        * @param $undo Revision the revision to undo
+        * @param $current Revision The current text
+        * @param $undo Revision The revision to undo
         * @param $undoafter Revision Must be an earlier revision than $undo
         *
-        * @return mixed string on success, false on failure
+        * @return mixed String on success, false on failure
         */
        public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
                $cur_content = $current->getContent();
@@ -809,8 +876,8 @@ abstract class ContentHandler {
        }
 
        /**
-        * Returns true for content models that support caching using the ParserCache mechanism.
-        * See WikiPage::isParserCacheUser().
+        * Returns true for content models that support caching using the
+        * ParserCache mechanism. See WikiPage::isParserCacheUser().
         *
         * @since WD.1
         *
@@ -821,33 +888,74 @@ abstract class ContentHandler {
        }
 
        /**
-        * Returns a lost of updates to perform when the given content is deleted.
-        * The necessary updates may be taken from the Content object, or depend on the current state of the database.
+        * Returns true if this content model supports sections.
         *
-        * @since WD.1
-        *
-        * @param \Content           $content the Content object for deletion
-        * @param \Title             $title   the title of the deleted page
-        * @param null|\ParserOutput $parserOutput optional parser output object for efficient access to meta-information
-        *                           about the content object. Provide if you have one handy.
+        * This default implementation returns false.
         *
-        * @return array a list of DataUpdate instances that will clean up the database ofter deletion.
+        * @return boolean whether sections are supported.
         */
-       public function getDeletionUpdates( Content $content, Title $title, ParserOutput $parserOutput = null ) {
-               return array(
-                       new LinksDeletionUpdate( $title ),
-               );
+       public function supportsSections() {
+               return false;
        }
 
        /**
-        * Returns true iff this content model supports sections.
-        *
-        * This default implementation returns false,
-        *
-        * @return boolean whether sections are supported.
+        * Call a legacy hook that uses text instead of Content objects.
+        * Will log a warning when a matching hook function is registered.
+        * If the textual representation of the content is changed by the
+        * hook function, a new Content object is constructed from the new
+        * text.
+        *
+        * @param $event String: event name
+        * @param $args Array: parameters passed to hook functions
+        * @param $warn bool: whether to log a warning (default: true). Should generally be true,
+        *                    may be set to false for testing.
+        *
+        * @return Boolean True if no handler aborted the hook
         */
-       public function supportsSections() {
-               return false;
+       public static function runLegacyHooks( $event, $args = array(), $warn = true ) {
+               if ( !Hooks::isRegistered( $event ) ) {
+                       return true; // nothing to do here
+               }
+
+               if ( $warn ) {
+                       wfWarn( "Using obsolete hook $event" );
+               }
+
+               // convert Content objects to text
+               $contentObjects = array();
+               $contentTexts = array();
+
+               foreach ( $args as $k => $v ) {
+                       if ( $v instanceof Content ) {
+                               /* @var Content $v */
+
+                               $contentObjects[$k] = $v;
+
+                               $v = $v->serialize();
+                               $contentTexts[ $k ] = $v;
+                               $args[ $k ] = $v;
+                       }
+               }
+
+               // call the hook functions
+               $ok = wfRunHooks( $event, $args );
+
+               // see if the hook changed the text
+               foreach ( $contentTexts as $k => $orig ) {
+                       /* @var Content $content */
+
+                       $modified = $args[ $k ];
+                       $content = $contentObjects[$k];
+
+                       if ( $modified !== $orig ) {
+                               // text was changed, create updated Content object
+                               $content = $content->getContentHandler()->unserializeContent( $modified );
+                       }
+
+                       $args[ $k ] = $content;
+               }
+
+               return $ok;
        }
 }
 
@@ -863,8 +971,8 @@ abstract class TextContentHandler extends ContentHandler {
        /**
         * Returns the content's text as-is.
         *
-        * @param Content $content
-        * @param String|null $format
+        * @param $content Content
+        * @param $format string|null
         * @return mixed
         */
        public function serializeContent( Content $content, $format = null ) {
@@ -873,16 +981,17 @@ abstract class TextContentHandler extends ContentHandler {
        }
 
        /**
-        * attempts to merge differences between three versions.
-        * Returns a new Content object for a clean merge and false for failure or a conflict.
+        * 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.
+        * All three Content objects passed as parameters must have the same
+        * content model.
         *
         * This text-based implementation uses wfMerge().
         *
-        * @param \Content|String $oldContent  String
-        * @param \Content|String $myContent   String
-        * @param \Content|String $yourContent String
+        * @param $oldContent \Content|string  String
+        * @param $myContent \Content|string   String
+        * @param $yourContent \Content|string String
         *
         * @return Content|Bool
         */
@@ -911,63 +1020,6 @@ abstract class TextContentHandler extends ContentHandler {
                return $mergedContent;
        }
 
-       /**
-        * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
-        *
-        * @param Content $content the content to render
-        * @param Title              $title context title for parsing
-        * @param int|null           $revId revision id (the parser wants that for some reason)
-        * @param ParserOptions|null $options parser options
-        * @param bool               $generateHtml whether or not to generate HTML
-        *
-        * @return ParserOutput representing the HTML form of the text
-        */
-       public function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
-               $this->checkModelID( $content->getModel() );
-
-               # generic implementation, relying on $this->getHtml()
-
-               if ( $generateHtml ) $html = $this->getHtml( $content );
-               else $html = '';
-
-               $po = new ParserOutput( $html );
-               return $po;
-       }
-
-       /**
-        * Generates an HTML version of the content, for display.
-        * Used by getParserOutput() to construct a ParserOutput object.
-        *
-        * This default implementation just calls getHighlightHtml(). Content models that
-        * have another mapping to HTML (as is the case for markup languages like wikitext)
-        * should override this method to generate the appropriate html.
-        *
-        * @param Content $content the content to render
-        *
-        * @return String an HTML representation of the content
-        */
-       protected function getHtml( Content $content ) {
-               $this->checkModelID( $content->getModel() );
-
-               return $this->getHighlightHtml( $content );
-       }
-
-       /**
-        * Generates a syntax-highlighted version the content, as HTML.
-        * Used by the default implementation if getHtml().
-        *
-        * @param Content $content the content to render
-        *
-        * @return String an HTML representation of the content's markup
-        */
-       protected function getHighlightHtml( Content $content ) {
-               $this->checkModelID( $content->getModel() );
-
-               #TODO: make Highlighter interface, use here highlighter, if available
-               return htmlspecialchars( $content->getNativeData() );
-       }
-
-
 }
 
 /**
@@ -989,37 +1041,6 @@ class WikitextContentHandler extends TextContentHandler {
                return new WikitextContent( '' );
        }
 
-       /**
-        * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
-        *
-        * @since    WD.1
-        *
-        * @param Content $content the content to render
-        * @param \Title             $title
-        * @param null               $revId
-        * @param null|ParserOptions $options
-        * @param bool               $generateHtml
-        *
-        * @internal param \IContextSource|null $context
-        * @return ParserOutput representing the HTML form of the text
-        */
-       public function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
-               global $wgParser;
-
-               $this->checkModelID( $content->getModel() );
-
-               if ( !$options ) {
-                       $options = new ParserOptions();
-               }
-
-               $po = $wgParser->parse( $content->getNativeData(), $title, $options, true, true, $revId );
-               return $po;
-       }
-
-       protected function getHtml( Content $content ) {
-               throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
-       }
-
        /**
         * Returns true because wikitext supports sections.
         *
@@ -1030,7 +1051,7 @@ class WikitextContentHandler extends TextContentHandler {
        }
 }
 
-#XXX: make ScriptContentHandler base class, do highlighting stuff there?
+# XXX: make ScriptContentHandler base class, do highlighting stuff there?
 
 /**
  * @since WD.1
@@ -1051,13 +1072,26 @@ class JavaScriptContentHandler extends TextContentHandler {
                return new JavaScriptContent( '' );
        }
 
-       protected function getHtml( Content $content ) {
-               $html = "";
-               $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
-               $html .= $this->getHighlightHtml( $content );
-               $html .= "\n</pre>\n";
+       /**
+        * Returns the english language, because JS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageLanguage()
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
 
-               return $html;
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageViewLanguage()
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
        }
 }
 
@@ -1080,13 +1114,25 @@ class CssContentHandler extends TextContentHandler {
                return new CssContent( '' );
        }
 
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageLanguage()
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
 
-       protected function getHtml( Content $content ) {
-               $html = "";
-               $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
-               $html .= $this->getHighlightHtml( $content );
-               $html .= "\n</pre>\n";
-
-               return $html;
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageViewLanguage()
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
        }
 }