Merge branch 'Wikidata' into master.
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 9 Oct 2012 09:34:24 +0000 (11:34 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 9 Oct 2012 09:34:24 +0000 (11:34 +0200)
This introduces the ContentHandler facility into MediaWiki,
see docs/contenthandler.txt.

For convenient review, a squashed version is available at
https://gerrit.wikimedia.org/r/27191

The ContentHandler facility is a major building block of the Wikidata project.
It has been discussed repeatedly on wikitech-l.

Change-Id: I3804e2d5f6f59e6a39db80744bdf61bfe8c14f98

27 files changed:
1  2 
RELEASE-NOTES-1.21
docs/contenthandler.txt
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/Message.php
includes/OutputPage.php
includes/Title.php
includes/actions/RawAction.php
includes/specials/SpecialNewpages.php
languages/messages/MessagesDe.php
languages/messages/MessagesQqq.php
maintenance/language/messages.inc
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/ArticleTest.php
tests/phpunit/includes/LinksUpdateTest.php
tests/phpunit/includes/RevisionStorageTest.php
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/TimestampTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/WikiPageTest.php
tests/phpunit/includes/api/ApiWatchTest.php
tests/phpunit/includes/search/SearchEngineTest.php
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/maintenance/backupPrefetchTest.php
tests/phpunit/maintenance/backupTextPassTest.php

diff --combined RELEASE-NOTES-1.21
@@@ -1,7 -1,7 +1,7 @@@
  = MediaWiki release notes =
  
 -Security reminder: MediaWiki does not require PHP's register_globals
 -setting since version 1.2.0. If you have it on, turn it '''off''' if you can.
 +Security reminder: MediaWiki does not require PHP's register_globals. If you
 +have it on, turn it '''off''' if you can.
  
  == MediaWiki 1.21 ==
  
@@@ -12,36 -12,36 +12,39 @@@ production
  
  === Configuration changes in 1.21 ===
  * (bug 29374) $wgVectorUseSimpleSearch is now enabled by default.
 -* Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname' instead
 +* Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname'
 +  instead.
  
  === New features in 1.21 ===
 +* (bug 34876) jquery.makeCollapsible has been improved in performance.
+ * Added ContentHandler facility to allow extensions to support other content than wikitext.
+   See docs/contenthandler.txt for details.
  
  === Bug fixes in 1.21 ===
  * (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
  * (bug 40352) fixDoubleRedirects.php should support interwiki redirects.
  * (bug 9237) SpecialBrokenRedirect should not list interwiki redirects.
 -* (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from recentchanges table.
 +* (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from
 +  recentchanges table.
  * (bug 32951) Do not register internal externals with absolute protocol,
    when server has relative protocol.
  
  === API changes in 1.21 ===
- * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing
-   metadata.
+ * prop=revisions can now report the contentmodel and contentformat, see docs/contenthandler.txt
+ * action=edit and action=parse now support contentmodel and contentformat parameters to control the interpretation of
+   page content; See docs/contenthandler.txt for details.
+ * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing metadata.
  
  === Languages updated in 1.21 ===
  
  MediaWiki supports over 350 languages. Many localisations are updated
  regularly. Below only new and removed languages are listed, as well as
  changes to languages because of Bugzilla reports.
  === Other changes in 1.21 ===
  
  == Compatibility ==
  
 -MediaWiki 1.21 requires PHP 5.3.2. PHP 4 is no longer supported.
 +MediaWiki 1.21 requires PHP 5.3.2 or later.
  
  MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but
  support for them is somewhat less mature. There is experimental support for IBM
@@@ -57,7 -57,9 +60,9 @@@ The supported versions are
  == Upgrading ==
  
  1.21 has several database changes since 1.20, and will not work without schema
- updates.
+ updates. Note that due to changes to some very large tables like the revision
+ table, the schema update may take quite long (minutes on a medium sized site,
+ many hours on a large site).
  
  If upgrading from before 1.11, and you are using a wiki as a commons
  repository, make sure that it is updated as well. Otherwise, errors may arise
diff --combined docs/contenthandler.txt
index 0000000,be3f4a7..3561432
mode 000000,100644..100644
--- /dev/null
@@@ -1,0 -1,184 +1,184 @@@
 -for everything. It was introduced in MediaWiki 1.20.
+ The ContentHandler facility adds support for arbitrary content types on wiki pages, instead of relying on wikitext
 -* text/plain - for future use, e.g. with some plain-html messages.
 -* text/html - for future use, e.g. with some plain-html messages.
++for everything. It was introduced in MediaWiki 1.21.
+ Each kind of content ("content model") supported by MediaWiki is identified by unique name. The content model determines
+ how a page's content is rendered, compared, stored, edited, and so on.
+ Built-in content types are:
+ * wikitext - wikitext, as usual
+ * javascript - user provided javascript code
+ * css - user provided css code
+ * text - plain text
+ In PHP, use the corresponding CONTENT_MODEL_XXX constant.
+ A page's content model is available using the Title::getContentModel() method. A page's default model is determined by
+ ContentHandler::getDefaultModelFor($title) as follows:
+ * The global setting $wgNamespaceContentModels specifies a content model for the given namespace.
+ * The hook ContentHandlerDefaultModelFor 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.
+   This is a compatibility feature. The ContentHandlerDefaultModelFor hook should be used instead if possible.
+ * The hook TitleIsWikitextPage may be used to force a page to use the wikitext model.
+   This is a compatibility feature. The ContentHandlerDefaultModelFor hook should be used instead if possible.
+ * Otherwise, the wikitext model is used.
+ Note that is currently no mechanism to convert a page from one content model to another, and there is no guarantee that
+ revisions of a page will all have the same content model. Use Revision::getContentModel() to find it.
+ == Architecture ==
+ Two class hierarchies are used to provide the functionality associated with the different content models:
+ * Content interface (and AbstractContent base class) define functionality that acts on the concrete content of a page, and
+ * ContentHandler base class provides functionality specific to a content model, but not acting on concrete content.
+ The most important function of ContentHandler is to act as a factory for the appropriate implementation of Content. These
+ Content objects are to be used by MediaWiki everywhere, instead of passing page content around as text. All manipulation
+ and analysis of page content must be done via the appropriate methods of the Content object.
+ For each content model, a subclass of ContentHandler has to be registered with $wgContentHandlers. The ContentHandler
+ object for a given content model can be obtained using ContentHandler::getForModelID( $id ). Also Title, WikiPage and
+ Revision now have getContentHandler() methods for convenience.
+ ContentHandler objects are singletons that provide functionality specific to the content type, but not directly acting
+ on the content of some page. ContentHandler::makeEmptyContent() and ContentHandler::unserializeContent() can be used to
+ create a Content object of the appropriate type. However, it is recommended to instead use WikiPage::getContent() resp.
+ Revision::getContent() to get a page's content as a Content object. These two methods should be the ONLY way in which
+ page content is accessed.
+ Another important function of ContentHandler objects is to define custom action handlers for a content model, see
+ ContentHandler::getActionOverrides(). This is similar to what WikiPage::getActionOverrides() was already doing.
+ == Serialization ==
+ With the ContentHandler facility, page content no longer has to be text based. Objects implementing the Content interface
+ are used to represent and handle the content internally. For storage and data exchange, each content model supports
+ at least one serialization format via ContentHandler::serializeContent( $content ). The list of supported formats for
+ a given content model can be accessed using ContentHandler::getSupportedFormats().
+ Content serialization formats are identified using MIME type like strings. The following formats are built in:
+ * text/x-wiki - wikitext
+ * text/javascript - for js pages
+ * text/css - for css pages
 -* Javascript and CSS pages are no longer parsed as wikitext (though pre-safe transform is still applied). Most
++* text/plain - for future use, e.g. with plain text messages.
++* text/html - for future use, e.g. with plain html messages.
+ * application/vnd.php.serialized - for future use with the api and for extensions
+ * application/json - for future use with the api, and for use by extensions
+ * application/xml - for future use with the api, and for use by extensions
+ In PHP, use the corresponding CONTENT_FORMAT_XXX constant.
+ Note that when using the API to access page content, especially action=edit, action=parse and action=query&prop=revisions,
+ the model and format of the content should always be handled explicitly. Without that information, interpretation of
+ the provided content is not reliable. The same applies to XML dumps generated via maintenance/dumpBackup.php or
+ Special:Export.
+ Also note that the API will provide encapsulated, serialized content - so if the API was called with format=json, and
+ contentformat is also json (or rather, application/json), the page content is represented as a string containing an
+ escaped json structure. Extensions that use JSON to serialize some types of page content may provide specialized API
+ modules that allow access to that content in a more natural form.
+ == Compatibility ==
+ The ContentHandler facility is introduced in a way that should allow all existing code to keep functioning at least
+ for pages that contain wikitext or other text based content. However, a number of functions and hooks have been
+ deprecated in favor of new versions that are aware of the page's content model, and will now generate warnings when
+ used.
+ Most importantly, the following functions have been deprecated:
+ * Revisions::getText() and Revisions::getRawText() is deprecated in favor Revisions::getContent()
+ * WikiPage::getText() is deprecated in favor WikiPage::getContent()
+ Also, the old Article::getContent() (which returns text) is superceded by Article::getContentObject(). However, both
+ methods should be avoided since they do not provide clean access to the page's actual content. For instance, they may
+ return a system message for non-existing pages. Use WikiPage::getContent() instead.
+ Code that relies on a textual representation of the page content should eventually be rewritten. However,
+ ContentHandler::getContentText() provides a stop-gap that can be used to get text for a page. Its behavior is controlled
+ by $wgContentHandlerTextFallback; per default it will return the text for text based content, and null for any other
+ content.
+ For rendering page content, Content::getParserOutput() should be used instead of accessing the parser directly.
+ ContentHandler::makeParserOptions() can be used to construct appropriate options.
+ Besides some functions, some hooks have also been replaced by new versions (see hooks.txt for details).
+ These hooks will now trigger a warning when used:
+ * ArticleAfterFetchContent was replaced by ArticleAfterFetchContentObject
+ * ArticleInsertComplete was replaced by ArticleContentInsertComplete
+ * ArticleSave was replaced by ArticleContentSave
+ * ArticleSaveComplete was replaced by ArticleContentSaveComplete
+ * ArticleViewCustom was replaced by ArticleContentViewCustom (also consider a custom implementation of the view action)
+ * EditFilterMerged was replaced by EditFilterMergedContent
+ * EditPageGetDiffText was replaced by EditPageGetDiffContent
+ * EditPageGetPreviewText was replaced by EditPageGetPreviewContent
+ * ShowRawCssJs was deprecated in favor of custom rendering implemented in the respective ContentHandler object.
+ == Database Storage ==
+ Page content is stored in the database using the same mechanism as before. Non-text content is serialized first. The
+ appropriate serialization and deserialization is handled by the Revision class.
+ Each revision's content model and serialization format is stored in the revision table (resp. in the archive table, if
+ the revision was deleted). The page's (current) content model (that is, the conent model of the latest revision) is also
+ stored in the page table.
+ Note however that the content model and format is only stored if it differs from the page's default, as determined by
+ ContentHandler::getDefaultModelFor( $title ). The default values are represented as NULL in the database, to preserve
+ space.
+ Storage of content model and format can be disabled altogether by setting $wgContentHandlerUseDB = false. In that case,
+ the page's default model (and the model's default format) will be used everywhere. Attempts to store a revision of a page
+ using a model or format different from the default will result in an error.
+ == Globals ==
+ There are some new globals that can be used to control the behavior of the ContentHandler facility:
+ * $wgContentHandlers associates content model IDs with the names of the appropriate ContentHandler subclasses.
+ * $wgNamespaceContentModels maps namespace IDs to a content model that should be the default for that namespace.
+ * $wgContentHandlerUseDB determines whether each revision's content model should be stored in the database.
+   Defaults is true.
+ * $wgContentHandlerTextFallback determines how the compatibility method ContentHandler::getContentText() will behave for
+   non-text content:
+     'ignore'     causes null to be returned for non-text content (default).
+     'serialize'  causes the serialized form of any non-text content to be returned (scary).
+     'fail'       causes an exception to be thrown for non-text content (strict).
+ == Caveats ==
+ There are some changes in behavior that might be surprising to users:
 -ContentHandler. If for example a File page used a content model with a custom move action, this would be overridden by
 -WikiFilePage's move handler.
++* Javascript and CSS pages are no longer parsed as wikitext (though pre-save transform is still applied). Most
+ importantly, this means that links, including categorization links, contained in the code will not work.
+ * With $wgContentHandlerUseDB = false, pages can not be moved in a way that would change the
+ default model. E.g. [[MediaWiki:foo.js]] can not be moved to [[MediaWiki:foo bar]], but can still be moved to
+ [[User:John/foo.js]]. Also, in this mode, changing the default content model for a page (e.g. by changing
+ $wgNamespaceContentModels) may cause it to become inaccessible.
+ * action=edit will fail for pages with non-text content, unless the respective ContentHandler implementation has
+ provided a specialized handler for the edit action. This is true for the API as well.
+ * action=raw will fail for all non-text content. This seems better than serving content in other formats to an
+ unsuspecting recipient. This will also cause client-side diffs to fail.
+ * File pages provide their own action overrides that do not combine gracefully with any custom handlers defined by a
++ContentHandler. If for example a File page used a content model with a custom revert action, this would be overridden by
++WikiFilePage's handler for the revert action.
@@@ -738,6 -738,18 +738,18 @@@ $wgMediaHandlers = array
        'image/x-djvu'   => 'DjVuHandler', // compat
  );
  
+ /**
+  * Plugins for page content model handling.
+  * Each entry in the array maps a model id to a class name.
+  *
+  * @since 1.21
+  */
+ $wgContentHandlers = array(
+       CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
+       CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
+       CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
+ );
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -1377,14 -1389,10 +1389,14 @@@ $wgAllDBsAreLocalhost = false
   * $wgSharedTables may be customized with a list of tables to share in the shared
   * datbase. However it is advised to limit what tables you do share as many of
   * MediaWiki's tables may have side effects if you try to share them.
 - * EXPERIMENTAL
   *
   * $wgSharedPrefix is the table prefix for the shared database. It defaults to
   * $wgDBprefix.
 + *
 + * @deprecated In new code, use the $wiki parameter to wfGetLB() to access 
 + *   remote databases. Using wfGetLB() allows the shared database to reside on 
 + *   separate servers to the wiki's own database, with suitable configuration 
 + *   of $wgLBFactoryConf.
   */
  $wgSharedDB = null;
  
@@@ -1762,7 -1770,7 +1774,7 @@@ $wgDBAhandler = 'db3'
  /**
   * Deprecated alias for $wgSessionsInObjectCache.
   *
 - * @deprecated Use $wgSessionsInObjectCache
 + * @deprecated since 1.20; Use $wgSessionsInObjectCache
   */
  $wgSessionsInMemcached = false;
  
@@@ -2077,13 -2085,13 +2089,13 @@@ $wgHTCPMulticastRouting = array()
   * setting is ignored. If $wgHTCPMulticastRouting is not set and this setting
   * is, it is used to populate $wgHTCPMulticastRouting.
   *
 - * @deprecated in favor of $wgHTCPMulticastRouting
 + * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting
   */
  $wgHTCPMulticastAddress = false;
  
  /**
   * HTCP multicast port.
 - * @deprecated in favor of $wgHTCPMulticastRouting
 + * @deprecated since 1.20 in favor of $wgHTCPMulticastRouting
   * @see $wgHTCPMulticastAddress
   */
  $wgHTCPPort = 4827;
@@@ -4285,9 -4293,7 +4297,9 @@@ $wgSecretKey = false
   */
  $wgProxyList = array();
  
 -/** deprecated */
 +/**
 + * @deprecated since 1.14
 + */
  $wgProxyKey = false;
  
  /** @} */ # end of proxy scanner settings
@@@ -6229,6 -6235,41 +6241,41 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
+ /**
+  * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
+  * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
+  * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
+  *
+  * @since 1.21
+  */
+ $wgNamespaceContentModels = array();
+ /**
+  * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
+  *
+  * * 'ignore': return null
+  * * 'fail': throw an MWException
+  * * 'serialize': serialize to default format
+  *
+  * @since 1.21
+  */
+ $wgContentHandlerTextFallback = 'ignore';
+ /**
+  * Set to false to disable use of the database fields introduced by the ContentHandler facility.
+  * This way, the ContentHandler facility can be used without any additional information in the database.
+  * A page's content model is then derived solely from the page's title. This however means that changing
+  * a page's default model (e.g. using $wgNamespaceContentModels) will break the page and/or make the content
+  * inaccessible. This also means that pages can not be moved to a title that would default to a different
+  * content model.
+  *
+  * Overall, with $wgContentHandlerUseDB = false, no database updates are needed, but content handling
+  * is less robust and less flexible.
+  *
+  * @since 1.21
+  */
+ $wgContentHandlerUseDB = true;
  /**
   * Whether the user must enter their password to change their e-mail address
   *
diff --combined includes/Defines.php
@@@ -39,7 -39,7 +39,7 @@@ define( 'MW_SPECIALPAGE_VERSION', 2 )
  define( 'DBO_DEBUG', 1 );
  define( 'DBO_NOBUFFER', 2 );
  define( 'DBO_IGNORE', 4 );
 -define( 'DBO_TRX', 8 );
 +define( 'DBO_TRX', 8 ); // automatically start transaction on first query
  define( 'DBO_DEFAULT', 16 );
  define( 'DBO_PERSISTENT', 32 );
  define( 'DBO_SYSDBA', 64 ); //for oracle maintenance
@@@ -261,7 -261,7 +261,7 @@@ define( 'APCOND_BLOCKED', 8 )
  define( 'APCOND_ISBOT', 9 );
  /**@}*/
  
- /**
+ /** @{
   * Protocol constants for wfExpandUrl()
   */
  define( 'PROTO_HTTP', 'http://' );
@@@ -270,3 -270,35 +270,35 @@@ define( 'PROTO_RELATIVE', '//' )
  define( 'PROTO_CURRENT', null );
  define( 'PROTO_CANONICAL', 1 );
  define( 'PROTO_INTERNAL', 2 );
+ /**@}*/
+ /**@{
+  * Content model ids, used by Content and ContentHandler.
+  * These IDs will be exposed in the API and XML dumps.
+  *
+  * Extensions that define their own content model IDs should take
+  * care to avoid conflicts. Using the extension name as a prefix is recommended,
+  * for example 'myextension-somecontent'.
+  */
+ define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
+ define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
+ define( 'CONTENT_MODEL_CSS', 'css' );
+ define( 'CONTENT_MODEL_TEXT', 'text' );
+ /**@}*/
+ /**@{
+  * Content formats, used by Content and ContentHandler.
+  * These should be MIME types, and will be exposed in the API and XML dumps.
+  *
+  * Extensions are free to use the below formats, or define their own.
+  * It is recommended to stick with the conventions for MIME types.
+  */
+ define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext
+ define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages
+ define( 'CONTENT_FORMAT_CSS', 'text/css' );  // for css pages
+ define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages.
+ define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages.
+ define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions
+ define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions
+ define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions
+ /**@}*/
diff --combined includes/EditPage.php
@@@ -155,6 -155,11 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
+       /**
+        * Status: can't parse content
+        */
+       const AS_PARSE_ERROR                = 240;
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
+       var $contentModel = null, $contentFormat = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
-       public $mPreloadText = '';
+       public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
  
        public $suppressIntro = false;
  
+       /**
+        * Set to true to allow editing of non-text content types.
+        *
+        * @var bool
+        */
+       public $allowNonTextContent = false;
        /**
         * @param $article Article
         */
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
+               $this->contentModel = $this->mTitle->getContentModel();
+               $handler = ContentHandler::getForModelID( $this->contentModel );
+               $this->contentFormat = $handler->getDefaultFormat();
        }
  
        /**
  
        /**
         * Get the context title object.
 -       * If not set, $wgTitle will be returned. This behavior might changed in
 +       * If not set, $wgTitle will be returned. This behavior might change in
         * the future to return $this->mTitle instead.
         *
         * @return Title object
                                wfProfileOut( __METHOD__ );
                                return;
                        }
-                       if ( !$this->mTitle->getArticleID() ) {
+                       if ( !$this->mTitle->getArticleID() )
                                wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
-                       }
-                       else {
+                       else
                                wfRunHooks( 'EditFormInitialText', array( $this ) );
-                       }
                }
  
                $this->showEditForm();
                        return;
                }
  
-               $content = $this->getContent();
+               $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
-               if ( $this->firsttime && $content === '' ) {
+               if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
-                       $content = $this->textbox1;
+                       $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
+                       $text = $this->toEditText( $content );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
-               $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
+               $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                                // modified by subclasses
                                wfProfileIn( get_class( $this ) . "::importContentFormData" );
                                $textbox1 = $this->importContentFormData( $request );
-                               if ( isset( $textbox1 ) ) {
+                               if ( isset( $textbox1 ) )
                                        $this->textbox1 = $textbox1;
-                               }
                                wfProfileOut( get_class( $this ) . "::importContentFormData" );
                        }
  
                        }
                }
  
+               $this->oldid = $request->getInt( 'oldid' );
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
-               $this->oldid = $request->getInt( 'oldid' );
+               $content_handler = ContentHandler::getForTitle( $this->mTitle );
+               $this->contentModel = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
+               $this->contentFormat = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
+               #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
+               #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
-               $this->textbox1 = $this->getContent( false );
+               $content = $this->getContentObject( false ); #TODO: track content object?!
+               $this->textbox1 = $this->toEditText( $content );
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
+        * @deprecated since 1.21
         */
-       function getContent( $def_text = '' ) {
-               global $wgOut, $wgRequest, $wgParser;
+       function getContent( $def_text = false ) {
+               wfDeprecated( __METHOD__, '1.21' );
+               if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+                       $def_content = $this->toEditContent( $def_text );
+               } else {
+                       $def_content = false;
+               }
+               $content = $this->getContentObject( $def_content );
+               // Note: EditPage should only be used with text based content anyway.
+               return $this->toEditText( $content );
+       }
+       /**
+        * @param Content|false $def_content The default value to return
+        *
+        * @return mixed Content on success, $def_content for invalid sections
+        *
+        * @since 1.21
+        */
+       protected function getContentObject( $def_content = null ) {
+               global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
-               $text = false;
+               $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
-                               $text = $this->mTitle->getDefaultMessageText();
+                               $msg = $this->mTitle->getDefaultMessageText();
+                               $content = $this->toEditContent( $msg );
                        }
-                       if ( $text === false ) {
+                       if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-                               $text = $this->getPreloadedText( $preload );
+                               $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
-                               $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
+                               $orig = $this->getOriginalContent();
+                               $content = $orig ? $orig->getSection( $this->section ) : null;
+                               if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
-                                       # Otherwise, $text will be left as-is.
+                                       # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
-                                               $text = $this->mArticle->getUndoText( $undorev, $oldrev );
-                                               if ( $text === false ) {
+                                               $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
+                                               if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
  
-                               if ( $text === false ) {
-                                       $text = $this->getOriginalContent();
+                               if ( $content === false ) {
+                                       $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
-               return $text;
+               return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
-                       return $this->getCurrentText();
+                       return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
-                       return '';
+                       if ( !$this->contentModel ) $this->contentModel = $this->getTitle()->getContentModel();
+                       $handler = ContentHandler::getForModelID( $this->contentModel );
+                       return $handler->makeEmptyContent();
                }
-               return $this->mArticle->getContent();
+               $content = $revision->getContent();
+               return $content;
        }
  
        /**
-        * Get the actual text of the page. This is basically similar to
-        * WikiPage::getRawText() except that when the page doesn't exist an empty
-        * string is returned instead of false.
+        * Get the current content of the page. This is basically similar to
+        * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
+        * content object is returned instead of null.
         *
-        * @since 1.19
-        * @return string
+        * @since 1.21
+        * @return Content
         */
-       private function getCurrentText() {
-               $text = $this->mArticle->getRawText();
-               if ( $text === false ) {
-                       return '';
+       protected function getCurrentContent() {
+               $rev = $this->mArticle->getRevision();
+               $content = $rev ? $rev->getContent( Revision::RAW ) : null;
+               if ( $content  === false || $content === null ) {
+                       if ( !$this->contentModel ) $this->contentModel = $this->getTitle()->getContentModel();
+                       $handler = ContentHandler::getForModelID( $this->contentModel );
+                       return $handler->makeEmptyContent();
                } else {
-                       return $text;
+                       # nasty side-effect, but needed for consistency
+                       $this->contentModel = $rev->getContentModel();
+                       $this->contentFormat = $rev->getContentFormat();
+                       return $content;
                }
        }
  
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
+        * @deprecated since 1.21
         */
        public function setPreloadedText( $text ) {
-               $this->mPreloadText = $text;
+               wfDeprecated( __METHOD__, "1.21" );
+               $content = $this->toEditContent( $text );
+               $this->setPreloadedContent( $content );
+       }
+       /**
+        * Use this method before edit() to preload some content into the edit box
+        *
+        * @param $content Content
+        *
+        * @since 1.21
+        */
+       public function setPreloadedContent( Content $content ) {
+               $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
+        *
         * @return String
+        *
+        * @deprecated since 1.21, use getPreloadedContent() instead
         */
        protected function getPreloadedText( $preload ) {
-               global $wgUser, $wgParser;
+               wfDeprecated( __METHOD__, "1.21" );
+               $content = $this->getPreloadedContent( $preload );
+               $text = $this->toEditText( $content );
+               return $text;
+       }
+       /**
+        * Get the contents to be preloaded into the box, either set by
+        * an earlier setPreloadText() or by loading the given page.
+        *
+        * @param $preload String: representing the title to preload from.
+        *
+        * @return Content
+        *
+        * @since 1.21
+        */
+       protected function getPreloadedContent( $preload ) {
+               global $wgUser;
  
-               if ( !empty( $this->mPreloadText ) ) {
-                       return $this->mPreloadText;
+               if ( !empty( $this->mPreloadContent ) ) {
+                       return $this->mPreloadContent;
                }
  
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
                if ( $preload === '' ) {
-                       return '';
+                       return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
-               if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
-                       return '';
+               if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+                       return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                if ( $page->isRedirect() ) {
                        $title = $page->getRedirectTarget();
                        # Same as before
-                       if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
-                               return '';
+                       if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+                               return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
-               return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
+               $content = $page->getContent( Revision::RAW );
+               return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                        case self::AS_HOOK_ERROR:
                                return false;
  
+                       case self::AS_PARSE_ERROR:
+                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
+                               return true;
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
                        return $status;
                }
  
+               try {
+                       # Construct Content object
+                       $textbox_content = $this->toEditContent( $this->textbox1 );
+               } catch (MWContentSerializationException $ex) {
+                       $status->fatal( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+                       $status->value = self::AS_PARSE_ERROR;
+                       wfProfileOut( __METHOD__ );
+                       return $status;
+               }
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
-                       Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+                       $textbox_content->isRedirect() &&
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
  
                if ( $new ) {
                        // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
+                       if ( !$this->mTitle->userCan( 'create' ) ) {
                                $status->fatal( 'nocreatetext' );
                                $status->value = self::AS_NO_CREATE_PERMISSION;
                                wfDebug( __METHOD__ . ": no create permission\n" );
                                return $status;
                        }
  
-                       $text = $this->textbox1;
+                       $content = $textbox_content;
                        $result['sectionanchor'] = '';
                        if ( $this->section == 'new' ) {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
-                                       $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->sectiontitle )
-                                               ->inContentLanguage()->text() . "\n\n" . $text;
+                                       $content = $content->addSectionHeader( $this->sectiontitle );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
                                        // passed.
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
+                                               $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
+                                                       ->inContentLanguage()->text() ;
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
-                                       $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->summary )
-                                               ->inContentLanguage()->text() . "\n\n" . $text;
+                                       $content = $content->addSectionHeader( $this->summary );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
  
                                        // Create a link to the new section from the edit summary.
                                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
+                                       $this->summary = wfMessage( 'newsectionsummary', $cleanSummary )
+                                               ->inContentLanguage()->text();
                                }
                        }
  
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
-               } else {
+               } else { # not $new
  
                        # Article exists. Check for edit conflict.
+                       $this->mArticle->clear(); # Force reload of dates, etc.
                        $timestamp = $this->mArticle->getTimestamp();
                        wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
  
                        if ( $timestamp != $this->edittime ) {
                                                $this->isConflict = false;
                                                wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
-                               } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER,  $this->mTitle->getArticleID(), $wgUser->getId(), $this->edittime ) ) {
+                               } elseif ( $this->section == '' && Revision::userWasLastToEdit( DB_MASTER,  $this->mTitle->getArticleID(),
+                                                       $wgUser->getId(), $this->edittime ) ) {
                                        # Suppress edit conflict with self, except for section edits where merging is required.
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                        $this->isConflict = false;
                                $sectionTitle = $this->summary;
                        }
  
+                       $content = null;
                        if ( $this->isConflict ) {
-                               wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
-                               $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
+                               wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
+                                               . " (article time '{$timestamp}')\n" );
+                               $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
                        } else {
-                               wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
-                               $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
+                               wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
+                               $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
                        }
-                       if ( is_null( $text ) ) {
+                       if ( is_null( $content ) ) {
                                wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
                                $this->isConflict = true;
-                               $text = $this->textbox1; // do not try to merge here!
+                               $content = $textbox_content; // do not try to merge here!
                        } elseif ( $this->isConflict ) {
                                # Attempt merge
-                               if ( $this->mergeChangesInto( $text ) ) {
+                               if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
                                        // Successful merge! Maybe we should tell the user the good news?
                                        $this->isConflict = false;
+                                       $content = $textbox_content;
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
                                } else {
                                        $this->section = '';
-                                       $this->textbox1 = $text;
+                                       #$this->textbox1 = $text; #redundant, nothing to do here?
                                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
                                }
                        }
                        }
  
                        // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
+                       $hook_args = array( $this, $content, &$this->hookError, $this->summary );
+                       if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args )
+                               || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) {
                                # Error messages etc. could be handled within the hook...
                                $status->fatal( 'hookaborted' );
                                $status->value = self::AS_HOOK_ERROR;
  
                        # Handle the user preference to force summaries here, but not for null edits
                        if ( $this->section != 'new' && !$this->allowBlankSummary
-                               && $this->getOriginalContent() != $text
-                               && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
+                               && !$content->equals( $this->getOriginalContent() )
+                               && !$content->isRedirect() ) # check if it's not a redirect
                        {
                                if ( md5( $this->summary ) == $this->autoSumm ) {
                                        $this->missingSummary = true;
                                        // passed.
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary' )
-                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
+                                               $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
+                                                       ->inContentLanguage()->text();
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
                                        # This is a new section, so create a link to the new section
                                        # in the revision summary.
                                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary' )
-                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
+                                       $this->summary = wfMessage( 'newsectionsummary', $cleanSummary )
+                                               ->inContentLanguage()->text();
                                }
                        } elseif ( $this->section != '' ) {
                                # Try to get a section anchor from the section source, redirect to edited section if header found
                        // merged the section into full text. Clear the section field
                        // so that later submission of conflict forms won't try to
                        // replace that into a duplicated mess.
-                       $this->textbox1 = $text;
+                       $this->textbox1 = $this->toEditText( $content );
                        $this->section = '';
  
                        $status->value = self::AS_SUCCESS_UPDATE;
                }
  
                // Check for length errors again now that the section is merged in
-               $this->kblength = (int)( strlen( $text ) / 1024 );
+                       $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
                if ( $this->kblength > $wgMaxArticleSize ) {
                        $this->tooBig = true;
                        $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
                        ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 );
  
-               $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
+                       $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
+                                                                                                                       false, null, $this->contentFormat );
  
                if ( $doEditStatus->isOK() ) {
-                       $result['redirect'] = Title::newFromRedirect( $text ) !== null;
-                       $this->updateWatchlist();
+                               $result['redirect'] = $content->isRedirect();
+                       $this->commitWatch();
                        wfProfileOut( __METHOD__ );
                        return $status;
                } else {
        }
  
        /**
-        * Register the change of watch status
+        * Commit the change of watch status
         */
-       protected function updateWatchlist() {
+       protected function commitWatch() {
                global $wgUser;
                if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) {
-                       $fname = __METHOD__;
-                       $title = $this->mTitle;
-                       $watch = $this->watchthis;
-                       // Do this in its own transaction to reduce contention...
                        $dbw = wfGetDB( DB_MASTER );
-                       $dbw->onTransactionIdle( function() use ( $dbw, $title, $watch, $wgUser, $fname ) {
-                               $dbw->begin( $fname );
-                               if ( $watch ) {
-                                       WatchAction::doWatch( $title, $wgUser );
-                               } else {
-                                       WatchAction::doUnwatch( $title, $wgUser );
-                               }
-                               $dbw->commit( $fname );
-                       } );
+                       $dbw->begin( __METHOD__ );
+                       if ( $this->watchthis ) {
+                               WatchAction::doWatch( $this->mTitle, $wgUser );
+                       } else {
+                               WatchAction::doUnwatch( $this->mTitle, $wgUser );
+                       }
+                       $dbw->commit( __METHOD__ );
                }
        }
  
         * @param $editText string
         *
         * @return bool
+        * @deprecated since 1.21, use mergeChangesIntoContent() instead
+        */
+       function mergeChangesInto( &$editText ){
+               wfDebug( __METHOD__, "1.21" );
+               $editContent = $this->toEditContent( $editText );
+               $ok = $this->mergeChangesIntoContent( $editContent );
+               if ( $ok ) {
+                       $editText = $this->toEditText( $editContent );
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+       /**
+        * @private
+        * @todo document
+        *
+        * @parma $editText string
+        *
+        * @return bool
+        * @since since 1.WD
         */
-       function mergeChangesInto( &$editText ) {
+       private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $baseText = $baseRevision->getText();
+               $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $currentText = $currentRevision->getText();
+               $currentContent = $currentRevision->getContent();
+               $handler = ContentHandler::getForModelID( $baseContent->getModel() );
+               $result = $handler->merge3( $baseContent, $editContent, $currentContent );
  
-               $result = '';
-               if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
-                       $editText = $result;
+               if ( $result ) {
+                       $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                $wgOut->addModules( 'mediawiki.action.edit' );
  
                if ( $wgUser->getOption( 'uselivepreview', false ) ) {
-                       $wgOut->addModules( 'mediawiki.action.edit.preview' );
+                       $wgOut->addModules( 'mediawiki.legacy.preview' );
                }
                // Bug #19334: textarea jumps when editing articles in IE8
                $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
                }
        }
  
+       /**
+        * Gets an editable textual representation of the given Content object.
+        * The textual representation can be turned by into a Content object by the
+        * toEditContent() method.
+        *
+        * If the given Content object is not of a type that can be edited using the text base EditPage,
+        * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+        * content.
+        *
+        * @param Content $content
+        * @return String the editable text form of the content.
+        *
+        * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true.
+        */
+       protected function toEditText( Content $content ) {
+               if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+                       throw new MWException( "This content model can not be edited as text: "
+                                                               . ContentHandler::getLocalizedName( $content->getModel() ) );
+               }
+               return $content->serialize( $this->contentFormat );
+       }
+       /**
+        * Turns the given text into a Content object by unserializing it.
+        *
+        * If the resulting Content object is not of a type that can be edited using the text base EditPage,
+        * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+        * content.
+        *
+        * @param String $text Text to unserialize
+        * @return Content the content object created from $text
+        *
+        * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent
+        *          and $this->allowNonTextContent is not true.
+        */
+       protected function toEditContent( $text ) {
+               $content = ContentHandler::makeContent( $text, $this->getTitle(),
+                       $this->contentModel, $this->contentFormat );
+               if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+                       throw new MWException( "This content model can not be edited as text: "
+                               . ContentHandler::getLocalizedName( $content->getModel() ) );
+               }
+               return $content;
+       }
        /**
         * Send the edit form and related headers to $wgOut
         * @param $formCallback Callback that takes an OutputPage parameter; will be called
                        }
                }
  
+               //@todo: add EditForm plugin interface and use it here!
+               //       search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
+               $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+               $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
-                       $this->textbox1 = $this->getCurrentText();
+                       $content = $this->getCurrentContent();
+                       $this->textbox1 = $this->toEditText( $content );
  
                        $this->showTextbox1();
                } else {
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
  
                if ( $this->isConflict ) {
-                       $this->showConflict();
+                       try {
+                               $this->showConflict();
+                       } catch ( MWContentSerializationException $ex ) {
+                               // this can't really happen, but be nice if it does.
+                               $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
+                       }
                }
  
                $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
  
                        if ( $this->section != '' && $this->section != 'new' ) {
                                if ( !$this->summary && !$this->preview && !$this->diff ) {
-                                       $sectionTitle = self::extractSectionTitle( $this->textbox1 );
+                                       $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
                                        if ( $sectionTitle !== false ) {
                                                $this->summary = "/* $sectionTitle */ ";
                                        }
                                if ( $revision ) {
                                        // Let sysop know that this will make private content public if saved
  
-                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
+                                       if ( !$revision->userCan( Revision::DELETED_TEXT ) ) {
                                                $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
                                        } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
                                                $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
                                        $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) );
                                }
                                if ( $this->formtype !== 'preview' ) {
-                                       if ( $this->isCssSubpage ) {
+                                       if ( $this->isCssSubpage )
                                                $wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) );
-                                       }
-                                       if ( $this->isJsSubpage ) {
+                                       if ( $this->isJsSubpage )
                                                $wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) );
-                                       }
                                }
                        }
                }
         * @return String
         */
        protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
-               if ( !$summary || ( !$this->preview && !$this->diff ) ) {
+               if ( !$summary || ( !$this->preview && !$this->diff ) )
                        return "";
-               }
  
                global $wgParser;
  
-               if ( $isSubjectPreview ) {
+               if ( $isSubjectPreview )
                        $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) )
                                ->inContentLanguage()->text();
-               }
  
                $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
  
  
  HTML
                );
-               if ( !$this->checkUnicodeCompliantBrowser() ) {
+               if ( !$this->checkUnicodeCompliantBrowser() )
                        $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
-               }
        }
  
        protected function showFormAfterText() {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
-       protected function showTextbox( $content, $name, $customAttribs = array() ) {
+       protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
-               $wikitext = $this->safeUnicodeOutput( $content );
+               $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
        protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
                global $wgOut;
                $classes = array();
-               if ( $isOnTop ) {
+               if ( $isOnTop )
                        $classes[] = 'ontop';
-               }
  
                $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
  
-               if ( $this->formtype != 'preview' ) {
+               if ( $this->formtype != 'preview' )
                        $attribs['style'] = 'display: none;';
-               }
  
                $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
  
                $wgOut->addHTML( '</div>' );
  
                if ( $this->formtype == 'diff' ) {
-                       $this->showDiff();
+                       try {
+                               $this->showDiff();
+                       } catch ( MWContentSerializationException $ex ) {
+                               $msg = wfMessage( 'content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
+                       }
                }
        }
  
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
+                               $oldContent = $this->toEditContent( $oldtext );
+                       } else {
+                               $oldContent = null;
                        }
                } else {
-                       $oldtext = $this->mArticle->getRawText();
+                       $oldContent = $this->getOriginalContent();
                }
-               $newtext = $this->mArticle->replaceSection(
-                       $this->section, $this->textbox1, $this->summary, $this->edittime );
  
-               wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+               $textboxContent = $this->toEditContent( $this->textbox1 );
+               $newContent = $this->mArticle->replaceSectionContent(
+                                                       $this->section, $textboxContent,
+                                                       $this->summary, $this->edittime );
+               ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
+               wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
  
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-               $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
+               $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
-               if ( $oldtext !== false  || $newtext != '' ) {
+               if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMessage( $oldtitlemsg )->parse();
                        $newtitle = wfMessage( 'yourtext' )->parse();
  
-                       $de = new DifferenceEngine( $this->mArticle->getContext() );
-                       $de->setText( $oldtext, $newtext );
+                       $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $oldContent, $newContent );
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
-                       $de = new DifferenceEngine( $this->mArticle->getContext() );
-                       $de->setText( $this->textbox2, $this->textbox1 );
+                       $content1 = $this->toEditContent( $this->textbox1 );
+                       $content2 = $this->toEditContent( $this->textbox2 );
+                       $handler = ContentHandler::getForModelID( $this->contentModel );
+                       $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $content2, $content1 );
                        $de->showDiff(
                                wfMessage( 'yourtext' )->parse(),
                                wfMessage( 'storedversion' )->text()
                );
                // Quick paranoid permission checks...
                if ( is_object( $data ) ) {
-                       if ( $data->log_deleted & LogPage::DELETED_USER ) {
+                       if ( $data->log_deleted & LogPage::DELETED_USER )
                                $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
-                       }
-                       if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
+                       if ( $data->log_deleted & LogPage::DELETED_COMMENT )
                                $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
-                       }
                }
                return $data;
        }
                        return $parsedNote;
                }
  
-               if ( $this->mTriedSave && !$this->mTokenOk ) {
-                       if ( $this->mTokenOkExceptSuffix ) {
-                               $note = wfMessage( 'token_suffix_mismatch' )->plain();
-                       } else {
-                               $note = wfMessage( 'session_fail_preview' )->plain();
-                       }
-               } elseif ( $this->incompleteForm ) {
-                       $note = wfMessage( 'edit_form_incomplete' )->plain();
-               } else {
-                       $note = wfMessage( 'previewnote' )->plain() .
-                               ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
-               }
+               $note = '';
  
-               $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+               try {
+                       $content = $this->toEditContent( $this->textbox1 );
  
-               $parserOptions->setEditSection( false );
-               $parserOptions->setIsPreview( true );
-               $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
+                       if ( $this->mTriedSave && !$this->mTokenOk ) {
+                               if ( $this->mTokenOkExceptSuffix ) {
+                                       $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
  
-               # don't parse non-wikitext pages, show message about preview
-               if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
-                       if ( $this->mTitle->isCssJsSubpage() ) {
-                               $level = 'user';
-                       } elseif ( $this->mTitle->isCssOrJsPage() ) {
-                               $level = 'site';
-                       } else {
-                               $level = false;
-                       }
-                       # Used messages to make sure grep find them:
-                       # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
-                       $class = 'mw-code';
-                       if ( $level ) {
-                               if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
-                                       $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
-                                       $class .= " mw-css";
-                               } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
-                                       $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
-                                       $class .= " mw-js";
                                } else {
-                                       throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
+                                       $note = wfMessage( 'session_fail_preview' )->plain() ;
                                }
-                               $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
-                               $previewHTML = $parserOutput->getText();
+                       } elseif ( $this->incompleteForm ) {
+                               $note = wfMessage( 'edit_form_incomplete' )->plain() ;
                        } else {
-                               $previewHTML = '';
+                               $note = wfMessage( 'previewnote' )->plain() .
+                                       ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
                        }
  
-                       $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
-               } else {
-                       $toparse = $this->textbox1;
+                       $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+                       $parserOptions->setEditSection( false );
+                       $parserOptions->setIsPreview( true );
+                       $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
  
-                       # If we're adding a comment, we need to show the
-                       # summary as the headline
-                       if ( $this->section == "new" && $this->summary != "" ) {
-                               $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
-                       }
+                       # don't parse non-wikitext pages, show message about preview
+                       if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
+                               if( $this->mTitle->isCssJsSubpage() ) {
+                                       $level = 'user';
+                               } elseif( $this->mTitle->isCssOrJsPage() ) {
+                                       $level = 'site';
+                               } else {
+                                       $level = false;
+                               }
  
-                       wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
+                               if ( $content->getModel() == CONTENT_MODEL_CSS ) {
+                                       $format = 'css';
+                               } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
+                                       $format = 'js';
+                               } else {
+                                       $format = false;
+                               }
  
-                       $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
-                       $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
+                               # Used messages to make sure grep find them:
+                               # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+                               if( $level && $format ) {
+                                       $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text()  . "</div>";
+                               } else {
+                                       $note = wfMessage( 'previewnote' )->text() ;
+                               }
+                       } else {
+                               $note = wfMessage( 'previewnote' )->text() ;
+                       }
  
-                       $rt = Title::newFromRedirectArray( $this->textbox1 );
+                       $rt = $content->getRedirectChain();
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
-                               $previewHTML = $parserOutput->getText();
-                       }
  
-                       $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputNoText( $parserOutput );
+                               # If we're adding a comment, we need to show the
+                               # summary as the headline
+                               if ( $this->section === "new" && $this->summary !== "" ) {
+                                       $content = $content->addSectionHeader( $this->summary );
+                               }
+                               $hook_args = array( $this, &$content );
+                               ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
+                               wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
+                               $parserOptions->enableLimitReport();
  
-                       if ( count( $parserOutput->getWarnings() ) ) {
-                               $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
+                               # But it's now deprecated, so never mind
+                               $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+                               $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
+                               $previewHTML = $parserOutput->getText();
+                               $this->mParserOutput = $parserOutput;
+                               $wgOut->addParserOutputNoText( $parserOutput );
+                               if ( count( $parserOutput->getWarnings() ) ) {
+                                       $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               }
                        }
+               } catch ( MWContentSerializationException $ex ) {
+                       $m = wfMessage('content-failed-to-parse', $this->contentModel, $this->contentFormat, $ex->getMessage() );
+                       $note .= "\n\n" . $m->parse();
+                       $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
diff --combined includes/Message.php
@@@ -202,6 -202,11 +202,11 @@@ class Message 
         */
        protected $title = null;
  
+       /**
+        * Content object representing the message
+        */
+       protected $content = null;
        /**
         * @var string
         */
                return $this;
        }
  
+       /**
+        * Returns the message as a Content object.
+        * @return Content
+        */
+       public function content() {
+               if ( !$this->content ) {
+                       $this->content = new MessageContent( $this->key );
+               }
+               return $this->content;
+       }
        /**
         * Returns the message parsed from wikitext to HTML.
         * @since 1.17
                        return '&lt;' . $key . '&gt;';
                }
  
 +              # Replace $* with a list of parameters for &uselang=qqx.
 +              if ( strpos( $string, '$*' ) !== false ) {
 +                      $paramlist = '';
 +                      if ( $this->parameters !== array() ) {
 +                              $paramlist = ': $' . implode( ', $', range( 1, count( $this->parameters ) ) );
 +                      }
 +                      $string = str_replace( '$*', $paramlist, $string );
 +              }
 +
                # Replace parameters before text parsing
                $string = $this->replaceParameters( $string, 'before' );
  
diff --combined includes/OutputPage.php
@@@ -1993,7 -1993,9 +1993,9 @@@ class OutputPage extends ContextSource 
                wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
  
                $this->sendCacheControl();
                ob_end_flush();
                wfProfileOut( __METHOD__ );
        }
  
         * @param $action String: action that was denied or null if unknown
         */
        public function showPermissionsErrorPage( $errors, $action = null ) {
 -              global $wgGroupPermissions;
 -
                // For some action (read, edit, create and upload), display a "login to do this action"
                // error if all of the following conditions are met:
                // 1. the user is not logged in
                if ( in_array( $action, array( 'read', 'edit', 'createpage', 'createtalk', 'upload' ) )
                        && $this->getUser()->isAnon() && count( $errors ) == 1 && isset( $errors[0][0] )
                        && ( $errors[0][0] == 'badaccess-groups' || $errors[0][0] == 'badaccess-group0' )
 -                      && ( ( isset( $wgGroupPermissions['user'][$action] ) && $wgGroupPermissions['user'][$action] )
 -                      || ( isset( $wgGroupPermissions['autoconfirmed'][$action] ) && $wgGroupPermissions['autoconfirmed'][$action] ) )
 +                      && ( User::groupHasPermission( 'user', $action )
 +                      || User::groupHasPermission( 'autoconfirmed', $action ) )
                ) {
                        $displayReturnto = null;
  
diff --combined includes/Title.php
@@@ -65,6 -65,7 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
+       var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
                }
        }
  
+       /**
+        * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
+        * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
+        *
+        * @return array
+        */
+       protected static function getSelectFields() {
+               global $wgContentHandlerUseDB;
+               $fields = array(
+                       'page_namespace', 'page_title', 'page_id',
+                       'page_len', 'page_is_redirect', 'page_latest',
+               );
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+               return $fields;
+       }
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
+                       if ( isset( $row->page_content_model ) )
+                               $this->mContentModel = strval( $row->page_content_model );
+                       else
+                               $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+                       $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
+               $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
+        * @deprecated since 1.21, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
-               return self::newFromRedirectInternal( $text );
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Title
+        * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
-               $titles = self::newFromRedirectArray( $text );
-               return $titles ? array_pop( $titles ) : null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getUltimateRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
+        * @deprecated since 1.21, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
-               global $wgMaxRedirects;
-               $title = self::newFromRedirectInternal( $text );
-               if ( is_null( $title ) ) {
-                       return null;
-               }
-               // recursive check to follow double redirects
-               $recurse = $wgMaxRedirects;
-               $titles = array( $title );
-               while ( --$recurse > 0 ) {
-                       if ( $title->isRedirect() ) {
-                               $page = WikiPage::factory( $title );
-                               $newtitle = $page->getRedirectTarget();
-                       } else {
-                               break;
-                       }
-                       // Redirects to some special pages are not permitted
-                       if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
-                               // the new title passes the checks, so make that our current title so that further recursion can be checked
-                               $title = $newtitle;
-                               $titles[] = $newtitle;
-                       } else {
-                               break;
-                       }
-               }
-               return $titles;
-       }
-       /**
-        * Really extract the redirect destination
-        * Do not call this function directly, use one of the newFromRedirect* functions above
-        *
-        * @param $text String Text with possible redirect
-        * @return Title
-        */
-       protected static function newFromRedirectInternal( $text ) {
-               global $wgMaxRedirects;
-               if ( $wgMaxRedirects < 1 ) {
-                       //redirects are disabled, so quit early
-                       return null;
-               }
-               $redir = MagicWord::get( 'redirect' );
-               $text = trim( $text );
-               if ( $redir->matchStartAndRemove( $text ) ) {
-                       // Extract the first link and see if it's usable
-                       // Ensure that it really does come directly after #REDIRECT
-                       // Some older redirects included a colon, so don't freak about that!
-                       $m = array();
-                       if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
-                               // Strip preceding colon used to "escape" categories, etc.
-                               // and URL-decode links
-                               if ( strpos( $m[1], '%' ) !== false ) {
-                                       // Match behavior of inline link parsing here;
-                                       $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
-                               }
-                               $title = Title::newFromText( $m[1] );
-                               // If the title is a redirect to bad special pages or is invalid, return null
-                               if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
-                                       return null;
-                               }
-                               return $title;
-                       }
-               }
-               return null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectChain();
        }
  
        /**
                return $this->mNamespace;
        }
  
+       /**
+        * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+        *
+        * @return String: Content model id
+        */
+       public function getContentModel() {
+               if ( !$this->mContentModel ) {
+                       $linkCache = LinkCache::singleton();
+                       $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+               }
+               if ( !$this->mContentModel ) {
+                       $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+               }
+               if( !$this->mContentModel ) {
+                       throw new MWException( "failed to determin content model!" );
+               }
+               return $this->mContentModel;
+       }
+       /**
+        * Convenience method for checking a title's content model name
+        *
+        * @param String $id The content model ID (use the CONTENT_MODEL_XXX constants).
+        * @return Boolean true if $this->getContentModel() == $id
+        */
+       public function hasContentModel( $id ) {
+               return $this->getContentModel() == $id;
+       }
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function isConversionTable() {
+               //@todo: ConversionTable should become a separate content model.
                return $this->getNamespace() == NS_MEDIAWIKI &&
                        strpos( $this->getText(), 'Conversiontable/' ) === 0;
        }
         * @return Bool
         */
        public function isWikitextPage() {
-               $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
-               wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
-               return $retval;
+               return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
-        * Could this page contain custom CSS or JavaScript, based
-        * on the title?
+        * Could this page contain custom CSS or JavaScript for the global UI.
+        * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
+        * or CONTENT_MODEL_JAVASCRIPT.
+        *
+        * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
+        *
+        * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
-               $retval = $this->mNamespace == NS_MEDIAWIKI
-                       && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
-               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
-               return $retval;
+               $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
+                       && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+                               || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+               #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
+               #      hook funktions can force this method to return true even outside the mediawiki namespace.
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
+               return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
-               return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                               && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+                                       || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                       && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+               return ( NS_USER == $this->mNamespace && $this->isSubpage()
+                       && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
  
                        if ( !$user->isAllowed( 'move' ) ) {
                                // User can't move anything
 -                              global $wgGroupPermissions;
 -                              $userCanMove = false;
 -                              if ( isset( $wgGroupPermissions['user']['move'] ) ) {
 -                                      $userCanMove = $wgGroupPermissions['user']['move'];
 -                              }
 -                              $autoconfirmedCanMove = false;
 -                              if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) {
 -                                      $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move'];
 -                              }
 +                              $userCanMove = User::groupHasPermission( 'user', 'move' );
 +                              $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
                                if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
                                        // custom message if logged-in users without any special rights can move
                                        $errors[] = array( 'movenologintext' );
         * @return Array list of errors
         */
        private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
 -              global $wgWhitelistRead, $wgGroupPermissions, $wgRevokePermissions;
 +              global $wgWhitelistRead, $wgRevokePermissions;
                static $useShortcut = null;
  
                # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below
                if ( is_null( $useShortcut ) ) {
                        $useShortcut = true;
 -                      if ( empty( $wgGroupPermissions['*']['read'] ) ) {
 +                      if ( !User::groupHasPermission( '*', 'read' ) ) {
                                # Not a public wiki, so no shortcut
                                $useShortcut = false;
                        } elseif ( !empty( $wgRevokePermissions ) ) {
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
                $linkCache = LinkCache::singleton();
-               $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+               $this->mRedirect = (bool)$cached;
  
                return $this->mRedirect;
        }
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+               $this->mLength = intval( $cached );
  
                return $this->mLength;
        }
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+               $this->mLatestID = intval( $cached );
  
                return $this->mLatestID;
        }
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
+               $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
  
  
                $res = $db->select(
                        array( 'page', $table ),
-                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               global $wgContentHandlerUseDB;
                $id = $this->getArticleID();
  
                # If the page doesn't exist; there can't be any link from this page
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
  
+               $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
                $res = $db->select(
                        array( $table, 'page' ),
-                       array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
         * @return Mixed True on success, getUserPermissionsErrors()-like array on failure
         */
        public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
-               global $wgUser;
+               global $wgUser, $wgContentHandlerUseDB;
  
                $errors = array();
                if ( !$nt ) {
                        $errors[] = array( 'badarticleerror' );
                }
  
+               // Content model checks
+               if ( !$wgContentHandlerUseDB &&
+                               $this->getContentModel() !== $nt->getContentModel() ) {
+                       // can't move a page if that would change the page's content model
+                       $errors[] = array( 'bad-target-model',
+                                                       ContentHandler::getLocalizedName( $this->getContentModel() ),
+                                                       ContentHandler::getLocalizedName( $nt->getContentModel() ) );
+               }
                // Image-specific checks
                if ( $this->getNamespace() == NS_FILE ) {
                        $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
                        $logType = 'move';
                }
  
-               $redirectSuppressed = !$createRedirect;
+               if ( $createRedirect ) {
+                       $contentHandler = ContentHandler::getForTitle( $this );
+                       $redirectContent = $contentHandler->makeRedirectContent( $nt );
+                       // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
+               } else {
+                       $redirectContent = null;
+               }
  
                $logEntry = new ManualLogEntry( 'move', $logType );
                $logEntry->setPerformer( $wgUser );
                $logEntry->setComment( $reason );
                $logEntry->setParameters( array(
                        '4::target' => $nt->getPrefixedText(),
-                       '5::noredir' => $redirectSuppressed ? '1': '0',
+                       '5::noredir' => $redirectContent ? '0': '1',
                ) );
  
                $formatter = LogFormatter::newFromEntry( $logEntry );
                }
  
                # Recreate the redirect, this time in the other direction.
-               if ( $redirectSuppressed ) {
+               if ( !$redirectContent ) {
                        WikiPage::onArticleDelete( $this );
                } else {
-                       $mwRedir = MagicWord::get( 'redirect' );
-                       $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
                        $redirectArticle = WikiPage::factory( $this );
                        $newid = $redirectArticle->insertOn( $dbw );
                        if ( $newid ) { // sanity
                                $redirectRevision = new Revision( array(
                                        'page'    => $newid,
                                        'comment' => $comment,
-                                       'text'    => $redirectText ) );
+                                       'content'    => $redirectContent ) );
                                $redirectRevision->insertOn( $dbw );
                                $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
  
         * @return Bool
         */
        public function isSingleRevRedirect() {
+               global $wgContentHandlerUseDB;
                $dbw = wfGetDB( DB_MASTER );
                # Is it a redirect?
+               $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
                $row = $dbw->selectRow( 'page',
-                       array( 'page_is_redirect', 'page_latest', 'page_id' ),
+                       $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
+               $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
                if( !is_object( $rev ) ){
                        return false;
                }
-               $text = $rev->getText();
+               $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
-               $m = array();
-               if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
-                       $redirTitle = Title::newFromText( $m[1] );
-                       if ( !is_object( $redirTitle ) ||
-                               ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
-                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+               $redirTitle = $content->getRedirectTarget();
+               if ( $redirTitle ) {
+                       if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
+                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
+                       } else {
+                               return true;
                        }
                } else {
-                       # Fail safe
-                       wfDebug( __METHOD__ . ": failsafe\n" );
+                       # Fail safe (not a redirect after all. strange.)
+                       wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
+                                               " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
-               return true;
        }
  
        /**
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
-               } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
-                       // css/js should always be LTR and is, in fact, English
-                       return wfGetLangObj( 'en' );
-               } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
-                       // Parse mediawiki messages with correct target language
-                       list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
-                       return wfGetLangObj( $lang );
                }
-               global $wgContLang;
-               // If nothing special, it should be in the wiki content language
-               $pageLang = $wgContLang;
+               //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request.
+               //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
+               $contentHandler = ContentHandler::getForTitle( $this );
+               $pageLang = $contentHandler->getPageLanguage( $this );
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
         * @return Language
         */
        public function getPageViewLanguage() {
-               $pageLang = $this->getPageLanguage();
-               // If this is nothing special (so the content is converted when viewed)
-               if ( !$this->isSpecialPage()
-                       && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
-                       && $this->getNamespace() !== NS_MEDIAWIKI
-               ) {
+               global $wgLang;
+               if ( $this->isSpecialPage() ) {
                        // 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 );
+                       $variant = $wgLang->getPreferredVariant();
+                       if ( $wgLang->getCode() !== $variant ) {
+                               return Language::factory( $variant );
                        }
+                       return $wgLang;
                }
+               //NOTE: can't be cached persistently, depends on user settings
+               //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
+               $contentHandler = ContentHandler::getForTitle( $this );
+               $pageLang = $contentHandler->getPageViewLanguage( $this );
                return $pageLang;
        }
  }
@@@ -46,7 -46,7 +46,7 @@@ class RawAction extends FormlessAction 
        }
  
        function onView() {
 -              global $wgGroupPermissions, $wgSquidMaxage, $wgForcedRawSMaxage, $wgJsMimeType;
 +              global $wgSquidMaxage, $wgForcedRawSMaxage, $wgJsMimeType;
  
                $this->getOutput()->disable();
                $request = $this->getRequest();
@@@ -91,7 -91,7 +91,7 @@@
                $response->header( 'Content-type: ' . $contentType . '; charset=UTF-8' );
                # Output may contain user-specific data;
                # vary generated content for open sessions on private wikis
 -              $privateCache = !$wgGroupPermissions['*']['read'] && ( $smaxage == 0 || session_id() != '' );
 +              $privateCache = !User::groupHasPermission( '*', 'read' ) && ( $smaxage == 0 || session_id() != '' );
                # allow the client to cache this for 24 hours
                $mode = $privateCache ? 'private' : 'public';
                $response->header( 'Cache-Control: ' . $mode . ', s-maxage=' . $smaxage . ', max-age=' . $maxage );
                                $request->response()->header( "Last-modified: $lastmod" );
  
                                // Public-only due to cache headers
-                               $text = $rev->getText();
+                               $content = $rev->getContent();
+                               if ( !$content instanceof TextContent ) {
+                                       wfHttpError( 406, "Not Acceptable", "The requested page uses the content model `"
+                                                                                                               . $content->getModel() . "` which is not supported via this interface." );
+                                       die();
+                               }
                                $section = $request->getIntOrNull( 'section' );
                                if ( $section !== null ) {
-                                       $text = $wgParser->getSection( $text, $section );
+                                       $content = $content->getSection( $section );
                                }
+                               $text = $content->getNativeData();
                        }
                }
  
@@@ -164,6 -164,8 +164,6 @@@ class SpecialNewpages extends Includabl
        }
  
        protected function filterLinks() {
 -              global $wgGroupPermissions;
 -
                // show/hide links
                $showhide = array( $this->msg( 'show' )->escaped(), $this->msg( 'hide' )->escaped() );
  
                }
  
                // Disable some if needed
 -              # @todo FIXME: Throws E_NOTICEs if not set; and doesn't obey hooks etc.
 -              if ( $wgGroupPermissions['*']['createpage'] !== true ) {
 +              if ( !User::groupHasPermission( '*', 'createpage' ) ) {
                        unset( $filters['hideliu'] );
                }
                if ( !$this->getUser()->useNPPatrol() ) {
        protected function feedItemDesc( $row ) {
                $revision = Revision::newFromId( $row->rev_id );
                if( $revision ) {
+                       //XXX: include content model/type in feed item?
                        return '<p>' . htmlspecialchars( $revision->getUserText() ) .
                                $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
                                htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
                                "</p>\n<hr />\n<div>" .
-                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+                               nl2br( htmlspecialchars( $revision->getContent()->serialize() ) ) . "</div>";
                }
                return '';
        }
@@@ -485,7 -489,7 +486,7 @@@ class NewPagesPager extends ReverseChro
        }
  
        function getQueryInfo() {
 -              global $wgEnableNewpagesUserFilter, $wgGroupPermissions;
 +              global $wgEnableNewpagesUserFilter;
                $conds = array();
                $conds['rc_new'] = 1;
  
                        $conds['rc_user_text'] = $user->getText();
                        $rcIndexes = 'rc_user_text';
                # If anons cannot make new pages, don't "exclude logged in users"!
 -              } elseif( $wgGroupPermissions['*']['createpage'] && $this->opts->getValue( 'hideliu' ) ) {
 +              } elseif( User::groupHasPermission( '*', 'createpage' ) && $this->opts->getValue( 'hideliu' ) ) {
                        $conds['rc_user'] = 0;
                }
                # If this user cannot see patrolled edits or they are off, don't do dumb queries!
@@@ -650,8 -650,8 +650,8 @@@ $1'
  'mainpage' => 'Hauptseite',
  'mainpage-description' => 'Hauptseite',
  'policy-url' => 'Project:Richtlinien',
 -'portal' => 'Gemeinschafts-Portal',
 -'portal-url' => 'Project:Gemeinschafts-Portal',
 +'portal' => 'Gemeinschaftsportal',
 +'portal-url' => 'Project:Gemeinschaftsportal',
  'privacy' => 'Datenschutz',
  'privacypage' => 'Project:Datenschutz',
  
@@@ -1400,7 -1400,7 +1400,7 @@@ Einzelheiten sind im [{{fullurl:{{#Spec
  'searchhelp-url' => 'Help:Hilfe',
  'searchmenu-prefix' => '[[Special:PrefixIndex/$1|Zeige alle Seiten, die mit dem Suchbegriff anfangen]]',
  'searchprofile-articles' => 'Inhaltsseiten',
 -'searchprofile-project' => 'Hilfe und Projektseiten',
 +'searchprofile-project' => 'Hilfe- und Projektseiten',
  'searchprofile-images' => 'Multimedia',
  'searchprofile-everything' => 'Alles',
  'searchprofile-advanced' => 'Erweitert',
@@@ -2602,7 -2602,8 +2602,8 @@@ Der aktuelle Text der gelöschten Seit
  'undeletedrevisions' => '{{PLURAL:$1|1 Version wurde|$1 Versionen wurden}} wiederhergestellt',
  'undeletedrevisions-files' => '{{PLURAL:$1|1 Version|$1 Versionen}} und {{PLURAL:$2|1 Datei|$2 Dateien}} wurden wiederhergestellt',
  'undeletedfiles' => '{{PLURAL:$1|1 Datei wurde|$1 Dateien wurden}} wiederhergestellt',
- 'cannotundelete' => 'Wiederherstellung fehlgeschlagen; jemand anderes hat die Seite bereits wiederhergestellt.',
+ 'cannotundelete' => 'Wiederherstellung fehlgeschlagen:
+ $1',
  'undeletedpage' => "'''„$1“''' wurde wiederhergestellt.
  
  Im [[Special:Log/delete|Lösch-Logbuch]] findest du eine Übersicht der gelöschten und wiederhergestellten Seiten.",
@@@ -3041,7 -3042,7 +3042,7 @@@ Diese auf dem lokalen Rechner speicher
  'tooltip-pt-anontalk' => 'Diskussion über Änderungen von dieser IP-Adresse',
  'tooltip-pt-preferences' => 'Eigene Einstellungen',
  'tooltip-pt-watchlist' => 'Liste der beobachteten Seiten',
 -'tooltip-pt-mycontris' => 'Liste deiner Beiträge',
 +'tooltip-pt-mycontris' => 'Liste eigener Beiträge',
  'tooltip-pt-login' => 'Sich anzumelden wird zwar gerne gesehen, ist aber keine Pflicht.',
  'tooltip-pt-anonlogin' => 'Sich anzumelden wird zwar gerne gesehen, ist aber keine Pflicht.',
  'tooltip-pt-logout' => 'Abmelden',
  'tooltip-n-portal' => 'Über das Projekt, was du tun kannst, wo was zu finden ist',
  'tooltip-n-currentevents' => 'Hintergrundinformationen zu aktuellen Ereignissen',
  'tooltip-n-recentchanges' => 'Liste der letzten Änderungen in {{SITENAME}}',
 -'tooltip-n-randompage' => 'Zufällige Seite',
 +'tooltip-n-randompage' => 'Zufällige Seite aufrufen',
  'tooltip-n-help' => 'Hilfeseite anzeigen',
  'tooltip-t-whatlinkshere' => 'Liste aller Seiten, die hierher verlinken',
  'tooltip-t-recentchangeslinked' => 'Letzte Änderungen an Seiten, die von hier verlinkt sind',
@@@ -1060,6 -1060,11 +1060,11 @@@ Please report at [[Support]] if you ar
  'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
  'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
  'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
+ 'content-failed-to-parse' => "Error message indicating that the page\'s content can not be saved because it is syntactically invalid. This may occurr for content types using serialization or a strict markup syntax.",
+ 'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for content types with internal consistency constraints.',
+ 'content-not-allowed-here'         => 'Error message indicating that the desired content model is not supported in given localtion.
+ * $1 is the human readable name of the content model
+ * $1 is the title of the page in question.',
  
  # Parser/template warnings
  'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@@ -1861,7 -1866,7 +1866,7 @@@ This action allows editing of all of th
  'recentchanges-legend' => 'Legend of the fieldset of [[Special:RecentChanges]]',
  'recentchanges-summary' => 'Summary of [[Special:RecentChanges]].',
  'recentchanges-label-newpage' => 'Tooltip for {{msg-mw|newpageletter}}',
 -'recentchanges-label-minor' => 'Tooltip for {{msg-mw|newpageletter}}',
 +'recentchanges-label-minor' => 'Tooltip for {{msg-mw|minoreditletter}}',
  'recentchanges-label-bot' => 'Tooltip for {{msg-mw|boteditletter}}',
  'recentchanges-label-unpatrolled' => 'Tooltip for {{msg-mw|unpatrolledletter}}',
  'rcnote' => 'Used on [[Special:RecentChanges]].
@@@ -2889,6 -2894,8 +2894,8 @@@ Options for the duration of the page pr
  {{Identical|Reset}}',
  'undeleteinvert' => '{{Identical|Invert selection}}',
  'undeletecomment' => '{{Identical|Reason}}',
+ 'cannotundelete' => 'Message shown when undeletion failed for some reason.
+ * <code>$1</code> is the combined wikitext of messages for all errors that caused the failure.',
  'undelete-search-title' => 'Page title when showing the search form in Special:Undelete',
  'undelete-search-submit' => '{{Identical|Search}}',
  'undelete-error' => 'Page title when a page could not be undeleted',
@@@ -3193,6 -3200,10 +3200,10 @@@ Parameters
  'immobile-target-namespace-iw' => "This message appears when attempting to move a page, if a person has typed an interwiki link as a namespace prefix in the input box labelled 'To new title'.  The special page 'Movepage' cannot be used to move a page to another wiki.
  
  'Destination' can be used instead of 'target' in this message.",
+ 'bad-target-model'             => "This message is shown when attempting to move a page, but the move would change the page's content model.
+ This may be the case when \$wgContentHandlerUseDB is set to false, because then a page's content model is derived from the page's title.
+ * $1: The localized name of the original page's content model.
+ * $2: The localized name of the content model used by the destination title.",
  'fix-double-redirects' => 'This is a checkbox in [[Special:MovePage]] which allows to move all redirects from the old title to the new title.',
  'protectedpagemovewarning' => 'Related message: [[MediaWiki:protectedpagewarning/{{#titleparts:{{PAGENAME}}|1|2}}]]
  {{Related|Semiprotectedpagewarning}}',
@@@ -4902,4 -4913,10 +4913,10 @@@ $4 is the gender of the target user.'
  'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
  
+ # Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
+ 'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
+ 'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
+ 'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
+ 'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
  );
@@@ -689,6 -689,15 +689,15 @@@ $wgMessageStructure = array
                'addsection-preload',
                'addsection-editintro',
                'defaultmessagetext',
 -      'contentmodel' => array(
+               'content-failed-to-parse',
+               'invalid-content-data',
+               'content-not-allowed-here',
+       ),
++      'contentmodels' => array(
+               'content-model-wikitext',
+               'content-model-text',
+               'content-model-javascript',
+               'content-model-css',
        ),
        'parserwarnings' => array(
                'expensive-parserfunction-warning',
@@@ -3829,6 -3838,6 +3838,7 @@@ XHTML id names."
        'toolbar'             => 'Edit page toolbar',
        'edit'                => 'Edit pages',
        'parserwarnings'      => 'Parser/template warnings',
++      'contentmodels'       => 'Content models',
        'undo'                => '"Undo" feature',
        'cantcreateaccount'   => 'Account creation failure',
        'history'             => 'History pages',
@@@ -19,6 -19,16 +19,6 @@@ abstract class MediaWikiTestCase extend
        protected $reuseDB = false;
        protected $tablesUsed = array(); // tables with data
  
 -      protected $restoreGlobals = array( // global variables to restore for each test
 -              'wgLang',
 -              'wgContLang',
 -              'wgLanguageCode',
 -              'wgUser',
 -              'wgTitle',
 -      );
 -
 -      private $savedGlobals = array();
 -
        private static $dbSetup = false;
  
        /**
         */
        private $tmpfiles = array();
  
 +      /**
 +       * Holds original values of MediaWiki configuration settings
 +       * to be restored in tearDown().
 +       * See also setMwGlobal().
 +       * @var array
 +       */
 +      private $mwGlobals = array();
  
        /**
         * Table name prefixes. Oracle likes it shorter.
                return $fname;
        }
  
 -      protected function setup() {
 -              parent::setup();
 -
 -              foreach ( $this->restoreGlobals as $var ) {
 -                      $v = $GLOBALS[ $var ];
 +      /**
 +       * setUp and tearDown should (where significant)
 +       * happen in reverse order.
 +       */
 +      protected function setUp() {
 +              parent::setUp();
 +
++              /*
++              //@todo: global variables to restore for *every* test
++              array(
++                      'wgLang',
++                      'wgContLang',
++                      'wgLanguageCode',
++                      'wgUser',
++                      'wgTitle',
++              );
++              */
 -                      if ( is_object( $v ) ) {
 -                              $v = clone $v;
 +              // Cleaning up temporary files
 +              foreach ( $this->tmpfiles as $fname ) {
 +                      if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
 +                              unlink( $fname );
 +                      } elseif ( is_dir( $fname ) ) {
 +                              wfRecursiveRemoveDir( $fname );
                        }
 +              }
  
 -                      $this->savedGlobals[$var] = $v;
 +              // Clean up open transactions
 +              if ( $this->needsDB() && $this->db ) {
 +                      while( $this->db->trxLevel() > 0 ) {
 +                              $this->db->rollback();
 +                      }
                }
        }
  
 -      protected function teardown() {
 +      protected function tearDown() {
                // Cleaning up temporary files
                foreach ( $this->tmpfiles as $fname ) {
                        if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
                        }
                }
  
 -              // clean up open transactions
 -              if( $this->needsDB() && $this->db ) {
 +              // Clean up open transactions
 +              if ( $this->needsDB() && $this->db ) {
                        while( $this->db->trxLevel() > 0 ) {
                                $this->db->rollback();
                        }
                }
  
 -              // restore saved globals
 -              foreach ( $this->savedGlobals as $k => $v ) {
 -                      $GLOBALS[ $k ] = $v;
 +              // Restore mw globals
 +              foreach ( $this->mwGlobals as $key => $value ) {
 +                      $GLOBALS[$key] = $value;
 +              }
 +              $this->mwGlobals = array();
 +
 +              parent::tearDown();
 +      }
 +
 +      /**
 +       * Individual test functions may override globals (either directly or through this
 +       * setMwGlobals() function), however one must call this method at least once for
 +       * each key within the setUp().
 +       * That way the key is added to the array of globals that will be reset afterwards
 +       * in the tearDown(). And, equally important, that way all other tests are executed
 +       * with the same settings (instead of using the unreliable local settings for most
 +       * tests and fix it only for some tests).
 +       *
 +       * @example
 +       * <code>
 +       *     protected function setUp() {
 +       *         $this->setMwGlobals( 'wgRestrictStuff', true );
 +       *     }
 +       *
 +       *     function testFoo() {}
 +       *
 +       *     function testBar() {}
 +       *         $this->assertTrue( self::getX()->doStuff() );
 +       *
 +       *         $this->setMwGlobals( 'wgRestrictStuff', false );
 +       *         $this->assertTrue( self::getX()->doStuff() );
 +       *     }
 +       *
 +       *     function testQuux() {}
 +       * </code>
 +       *
 +       * @param array|string $pairs Key to the global variable, or an array
 +       *  of key/value pairs.
 +       * @param mixed $value Value to set the global to (ignored
 +       *  if an array is given as first argument).
 +       */
 +      protected function setMwGlobals( $pairs, $value = null ) {
 +              if ( !is_array( $pairs ) ) {
 +                      $key = $pairs;
 +                      $this->mwGlobals[$key] = $GLOBALS[$key];
 +                      $GLOBALS[$key] = $value;
 +              } else {
 +                      foreach ( $pairs as $key => $value ) {
 +                              $this->mwGlobals[$key] = $GLOBALS[$key];
 +                              $GLOBALS[$key] = $value;
 +                      }
 +              }
 +      }
 +
++      /**
++       * Merges the given values into a MW global array variable.
++       * Useful for setting some entries in a configuration array, instead of
++       * setting the entire array.
++       *
++       * @param String $name The name of the global, as in wgFooBar
++       * @param Array $values The array containing the entries to set in that global
++       *
++       * @throws MWException if the designated global is not an array.
++       */
++      protected function mergeMwGlobalArrayValue( $name, $values ) {
++              if ( !isset( $GLOBALS[$name] ) ) {
++                      $merged = $values;
++              } else {
++                      if ( !is_array( $GLOBALS[$name] ) ) {
++                              throw new MWException( "MW global $name is not an array." );
++                      }
++
++                      //NOTE: do not use array_merge, it screws up for numeric keys.
++                      $merged = $GLOBALS[$name];
++                      foreach ( $values as $k => $v ) {
++                              $merged[$k] = $v;
++                      }
+               }
 -              parent::teardown();
++              $this->setMwGlobals( $name, $merged );
+       }
        function dbPrefix() {
                return $this->db->getType() == 'oracle' ? self::ORA_DB_PREFIX : self::DB_PREFIX;
        }
                //Make 1 page with 1 revision
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                if ( !$page->getId() == 0 ) {
-                       $page->doEdit( 'UTContent',
-                                                       'UTPageSummary',
-                                                       EDIT_NEW,
-                                                       false,
-                                                       User::newFromName( 'UTSysop' ) );
+                       $page->doEditContent(
+                               new WikitextContent( 'UTContent' ),
+                               'UTPageSummary',
+                               EDIT_NEW,
+                               false,
+                               User::newFromName( 'UTSysop' ) );
                }
        }
  
@@@ -2,25 -2,19 +2,25 @@@
  
  class ArticleTest extends MediaWikiTestCase {
  
 -      private $title; // holds a Title object
 -      private $article; // holds an article
 +      /**
 +       * @var Title
 +       */
 +      private $title;
 +      /**
 +       * @var Article
 +       */
 +      private $article;
  
        /** creates a title object and its article object */
 -      function setUp() {
 -              $this->title   = Title::makeTitle( NS_MAIN, 'SomePage' );
 +      protected function setUp() {
 +              $this->title = Title::makeTitle( NS_MAIN, 'SomePage' );
                $this->article = new Article( $this->title );
  
        }
  
        /** cleanup title object and its article object */
 -      function tearDown() {
 -              $this->title   = null;
 +      protected function tearDown() {
 +              $this->title = null;
                $this->article = null;
  
        }
@@@ -60,6 -54,9 +60,9 @@@
         * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class)
         */
        function testStaticFunctions() {
+               $this->hideDeprecated( 'Article::getAutosummary' );
+               $this->hideDeprecated( 'WikiPage::getAutosummary' );
                $this->assertEquals( WikiPage::selectFields(), Article::selectFields(),
                        "Article static functions" );
                $this->assertEquals( true, is_callable( "Article::onArticleCreate" ),
@@@ -25,7 -25,7 +25,7 @@@ class LinksUpdateTest extends MediaWiki
                );
        }
  
 -      function setUp() {
 +      protected function setUp() {
                $dbw = wfGetDB( DB_MASTER );
                $dbw->replace(
                        'interwiki',
        protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
                $update = new LinksUpdate( $title, $parserOutput );
  
+               //NOTE: make sure LinksUpdate does not generate warnings when called inside a transaction.
+               $update->beginTransaction();
                $update->doUpdate();
+               $update->commitTransaction();
  
                $this->assertSelect( $table, $fields, $condition, $expectedRows );
        }
@@@ -3,6 -3,7 +3,7 @@@
  /**
   * Test class for Revision storage.
   *
+  * @group ContentHandler
   * @group Database
   * ^--- important, causes temporary tables to be used instead of the real database
   *
@@@ -11,6 -12,9 +12,9 @@@
   */
  class RevisionStorageTest extends MediaWikiTestCase {
  
+       /**
+        * @var WikiPage $the_page
+        */
        var $the_page;
  
        function  __construct( $name = null, array $data = array(), $dataName = '' ) {
                                                      'iwlinks' ) );
        }
  
-       protected function setUp() {
+       public function setUp() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+               $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
+               $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
 -
                if ( !$this->the_page ) {
-                       $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" );
+                       $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page", CONTENT_MODEL_WIKITEXT );
                }
        }
  
+       public function tearDown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+               unset( $wgExtraNamespaces[ 12312 ] );
+               unset( $wgExtraNamespaces[ 12313 ] );
+               unset( $wgNamespaceContentModels[ 12312 ] );
+               unset( $wgContentHandlers[ 'DUMMY' ] );
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
        protected function makeRevision( $props = null ) {
                if ( $props === null ) $props = array();
  
@@@ -63,7 -91,8 +90,8 @@@
                        $page->doDeleteArticle( "done" );
                }
  
-               $page->doEdit( $text, "testing", EDIT_NEW );
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
                $this->assertEquals( $orig->getPage(), $rev->getPage() );
                $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
                $this->assertEquals( $orig->getUser(), $rev->getUser() );
+               $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
+               $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
                $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
        }
  
         */
        public function testNewFromArchiveRow()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum' );
+               $page = $this->createPage( 'RevisionStorageTest_testNewFromArchiveRow', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT );
                $orig = $page->getRevision();
                $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
  
         */
        public function testFetchRevision()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' );
+               $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one', CONTENT_MODEL_WIKITEXT );
                $id1 = $page->getRevision()->getId();
  
-               $page->doEdit( 'two', 'second rev' );
+               $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
                $id2 = $page->getRevision()->getId();
  
                $res = Revision::fetchRevision( $page->getTitle() );
         */
        public function testSelectFields()
        {
+               global $wgContentHandlerUseDB;
                $fields = Revision::selectFields();
  
                $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields');
                $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
                $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
                $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
+               if ( $wgContentHandlerUseDB ) {
+                       $this->assertTrue( in_array( 'rev_content_model', $fields ),
+                                                               'missing rev_content_model in list of fields');
+                       $this->assertTrue( in_array( 'rev_content_format', $fields ),
+                                                               'missing rev_content_format in list of fields');
+               } else {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
        }
  
        /**
         */
        public function testGetText()
        {
+               $this->hideDeprecated( 'Revision::getText' );
                $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
                $rev = Revision::newFromId( $orig->getId() );
  
                $this->assertEquals( 'hello hello.', $rev->getText() );
        }
  
+       /**
+        * @covers Revision::getContent
+        */
+       public function testGetContent()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+               $rev = Revision::newFromId( $orig->getId() );
+               $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
+       }
        /**
         * @covers Revision::revText
         */
         */
        public function testGetRawText()
        {
+               $this->hideDeprecated( 'Revision::getRawText' );
                $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) );
                $rev = Revision::newFromId( $orig->getId() );
  
                $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
        }
+       /**
+        * @covers Revision::getContentModel
+        */
+       public function testGetContentModel()
+       {
+               global $wgContentHandlerUseDB;
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+                                                                                       'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+       /**
+        * @covers Revision::getContentFormat
+        */
+       public function testGetContentFormat()
+       {
+               global $wgContentHandlerUseDB;
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+                                                                                       'content_model' => CONTENT_MODEL_JAVASCRIPT,
+                                                                                       'content_format' => CONTENT_FORMAT_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+               $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
+       }
        /**
         * @covers Revision::isCurrent
         */
        public function testIsCurrent()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum' );
+               $page = $this->createPage( 'RevisionStorageTest_testIsCurrent', 'Lorem Ipsum', CONTENT_MODEL_WIKITEXT );
                $rev1 = $page->getRevision();
  
                # @todo: find out if this should be true
                $rev1x = Revision::newFromId( $rev1->getId() );
                $this->assertTrue( $rev1x->isCurrent() );
  
-               $page->doEdit( 'Bla bla', 'second rev' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ), 'second rev' );
                $rev2 = $page->getRevision();
  
                # @todo: find out if this should be true
         */
        public function testGetPrevious()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious' );
+               $page = $this->createPage( 'RevisionStorageTest_testGetPrevious', 'Lorem Ipsum testGetPrevious', CONTENT_MODEL_WIKITEXT );
                $rev1 = $page->getRevision();
  
                $this->assertNull( $rev1->getPrevious() );
  
-               $page->doEdit( 'Bla bla', 'second rev testGetPrevious' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+                                                               'second rev testGetPrevious' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev2->getPrevious() );
         */
        public function testGetNext()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext' );
+               $page = $this->createPage( 'RevisionStorageTest_testGetNext', 'Lorem Ipsum testGetNext', CONTENT_MODEL_WIKITEXT );
                $rev1 = $page->getRevision();
  
                $this->assertNull( $rev1->getNext() );
  
-               $page->doEdit( 'Bla bla', 'second rev testGetNext' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+                                                               'second rev testGetNext' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev1->getNext() );
         */
        public function testNewNullRevision()
        {
-               $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text' );
+               $page = $this->createPage( 'RevisionStorageTest_testNewNullRevision', 'some testing text', CONTENT_MODEL_WIKITEXT );
                $orig = $page->getRevision();
  
                $dbw = wfGetDB( DB_MASTER );
                $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
  
-               $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' );
-               $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' );
-               $this->assertEquals( 'some testing text', $rev->getText() );
+               $this->assertNotEquals( $orig->getId(), $rev->getId(),
+                                                               'new null revision shold have a different id from the original revision' );
+               $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
+                                                               'new null revision shold have the same text id as the original revision' );
+               $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
        }
  
 -      public function dataUserWasLastToEdit() {
 +      public static function provideUserWasLastToEdit() {
                return array(
                        array( #0
                                3, true, # actually the last edit
        }
  
        /**
 -       * @dataProvider dataUserWasLastToEdit
 +       * @dataProvider provideUserWasLastToEdit
         */
        public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
                $userA = \User::newFromName( "RevisionStorageTest_userA" );
                # zero
                $revisions[0] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(), // we need the title to determine the page's default content model
                        'timestamp' => '20120101000000',
                        'user' => $userA->getId(),
                        'text' => 'zero',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
                        'summary' => 'edit zero'
                ) );
                $revisions[0]->insertOn( $dbw );
                # one
                $revisions[1] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table)
                        'timestamp' => '20120101000100',
                        'user' => $userA->getId(),
                        'text' => 'one',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
                        'summary' => 'edit one'
                ) );
                $revisions[1]->insertOn( $dbw );
                # two
                $revisions[2] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000200',
                        'user' => $userB->getId(),
                        'text' => 'two',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
                        'summary' => 'edit two'
                ) );
                $revisions[2]->insertOn( $dbw );
                # three
                $revisions[3] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000300',
                        'user' => $userA->getId(),
                        'text' => 'three',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
                        'summary' => 'edit three'
                ) );
                $revisions[3]->insertOn( $dbw );
                # four
                $revisions[4] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000200',
                        'user' => $userA->getId(),
                        'text' => 'zero',
+                       'content_model' => CONTENT_MODEL_WIKITEXT,
                        'summary' => 'edit four'
                ) );
                $revisions[4]->insertOn( $dbw );
@@@ -1,14 -1,54 +1,56 @@@
  <?php
  
+ /**
+  * @group ContentHandler
+  */
  class RevisionTest extends MediaWikiTestCase {
 -      var $saveGlobals = array();
 -
 -      function setUp() {
 +      protected function setUp() {
+               global $wgContLang;
 -              $wgContLang = Language::factory( 'en' );
 -              $globalSet = array(
 +              parent::setUp();
 +
 +              $this->setMwGlobals( array(
 +                      'wgContLang' => Language::factory( 'en' ),
                        'wgLegacyEncoding' => false,
                        'wgCompressRevisions' => false,
 -                      'wgContentHandlerTextFallback' => $GLOBALS['wgContentHandlerTextFallback'],
 -                      'wgExtraNamespaces' => $GLOBALS['wgExtraNamespaces'],
 -                      'wgNamespaceContentModels' => $GLOBALS['wgNamespaceContentModels'],
 -                      'wgContentHandlers' => $GLOBALS['wgContentHandlers'],
 -              );
 -              foreach ( $globalSet as $var => $data ) {
 -                      $this->saveGlobals[$var] = $GLOBALS[$var];
 -                      $GLOBALS[$var] = $data;
 -              }
++                      'wgContentHandlerTextFallback' => 'ignore',
 +              ) );
 -              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
 -              $wgExtraNamespaces[ 12312 ] = 'Dummy';
 -              $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
++              $this->mergeMwGlobalArrayValue(
++                      'wgExtraNamespaces',
++                      array(
++                              12312 => 'Dummy',
++                              12313 => 'Dummy_talk',
++                      )
++              );
 -              $wgNamespaceContentModels[ 12312 ] = "testing";
 -              $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting';
 -              $wgContentHandlers[ "RevisionTestModifyableContent" ] = 'RevisionTestModifyableContentHandler';
++              $this->mergeMwGlobalArrayValue(
++                      'wgNamespaceContentModels',
++                      array(
++                              12312 => 'testing',
++                      )
++              );
 -
 -              global $wgContentHandlerTextFallback;
 -              $wgContentHandlerTextFallback = 'ignore';
++              $this->mergeMwGlobalArrayValue(
++                      'wgContentHandlers',
++                      array(
++                              'testing' => 'DummyContentHandlerForTesting',
++                              'RevisionTestModifyableContent' => 'RevisionTestModifyableContentHandler',
++                      )
++              );
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
 -              foreach ( $this->saveGlobals as $var => $data ) {
 -                      $GLOBALS[$var] = $data;
 -              }
 -
+       }
+       function tearDown() {
+               global $wgContLang;
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
++
++              parent::tearDown();
        }
  
        function testGetRevisionText() {
        }
  
        function testCompressRevisionTextUtf8Gzip() {
 -              $GLOBALS['wgCompressRevisions'] = true;
 +              global $wgCompressRevisions;
 +
 +              $wgCompressRevisions = true;
 +
                $row = new stdClass;
                $row->old_text = "Wiki est l'\xc3\xa9cole superieur !";
                $row->old_flags = Revision::compressRevisionText( $row->old_text );
                $this->assertEquals( "Wiki est l'\xc3\xa9cole superieur !",
                        Revision::getRevisionText( $row ), "getRevisionText" );
        }
+       # =================================================================================================================
+       /**
+        * @param string $text
+        * @param string $title
+        * @param string $model
+        * @return Revision
+        */
+       function newTestRevision( $text, $title = "Test", $model = CONTENT_MODEL_WIKITEXT, $format = null ) {
+               if ( is_string( $title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+               $content = ContentHandler::makeContent( $text, $title, $model, $format );
+               $rev = new Revision(
+                       array(
+                               'id'         => 42,
+                               'page'       => 23,
+                               'title'      => $title,
+                               'content'    => $content,
+                               'length'     => $content->getSize(),
+                               'comment'    => "testing",
+                               'minor_edit' => false,
+                               'content_format' => $format,
+                       )
+               );
+               return $rev;
+       }
+       function dataGetContentModel() {
+               //NOTE: we expect the help namespace to always contain wikitext
+               return array(
+                       array( 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ),
+                       array( 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, "testing" ),
+               );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetContentModel
+        */
+       function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $this->assertEquals( $expectedModel, $rev->getContentModel() );
+       }
+       function dataGetContentFormat() {
+               //NOTE: we expect the help namespace to always contain wikitext
+               return array(
+                       array( 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ),
+                       array( 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ),
+                       array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, "testing" ),
+               );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetContentFormat
+        */
+       function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+       }
+       function dataGetContentHandler() {
+               //NOTE: we expect the help namespace to always contain wikitext
+               return array(
+                       array( 'hello world', 'Help:Hello', null, null, 'WikitextContentHandler' ),
+                       array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ),
+               );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetContentHandler
+        */
+       function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
+       }
+       function dataGetContent() {
+               //NOTE: we expect the help namespace to always contain wikitext
+               return array(
+                       array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, serialize('hello world') ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize('hello world') ),
+               );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetContent
+        */
+       function testGetContent( $text, $title, $model, $format, $audience, $expectedSerialization ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $content = $rev->getContent( $audience );
+               $this->assertEquals( $expectedSerialization, is_null( $content ) ? null : $content->serialize( $format ) );
+       }
+       function dataGetText() {
+               //NOTE: we expect the help namespace to always contain wikitext
+               return array(
+                       array( 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ),
+               );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetText
+        */
+       function testGetText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $this->hideDeprecated( 'Revision::getText' );
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $this->assertEquals( $expectedText, $rev->getText( $audience ) );
+       }
+       /**
+        * @group Database
+        * @dataProvider dataGetText
+        */
+       function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $this->hideDeprecated( 'Revision::getRawText' );
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $this->assertEquals( $expectedText, $rev->getRawText( $audience ) );
+       }
+       public function dataGetSize( ) {
+               return array(
+                       array( "hello world.", CONTENT_MODEL_WIKITEXT, 12 ),
+                       array( serialize( "hello world." ), "testing", 12 ),
+               );
+       }
+       /**
+        * @covers Revision::getSize
+        * @group Database
+        * @dataProvider dataGetSize
+        */
+       public function testGetSize( $text, $model, $expected_size )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+               $this->assertEquals( $expected_size, $rev->getSize() );
+       }
+       public function dataGetSha1( ) {
+               return array(
+                       array( "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ),
+                       array( serialize( "hello world." ), "testing", Revision::base36Sha1( serialize( "hello world." ) ) ),
+               );
+       }
+       /**
+        * @covers Revision::getSha1
+        * @group Database
+        * @dataProvider dataGetSha1
+        */
+       public function testGetSha1( $text, $model, $expected_hash )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+               $this->assertEquals( $expected_hash, $rev->getSha1() );
+       }
+       public function testConstructWithText() {
+               $this->hideDeprecated( "Revision::getText" );
+               $rev = new Revision( array(
+                                         'text' => 'hello world.',
+                                         'content_model' => CONTENT_MODEL_JAVASCRIPT
+                                    ));
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+       public function testConstructWithContent() {
+               $this->hideDeprecated( "Revision::getText" );
+               $title = Title::newFromText( 'RevisionTest_testConstructWithContent' );
+               $rev = new Revision( array(
+                                         'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ),
+                                    ));
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+       /**
+        * Tests whether $rev->getContent() returns a clone when needed.
+        *
+        * @group Database
+        */
+       function testGetContentClone( ) {
+               $content = new RevisionTestModifyableContent( "foo" );
+               $rev = new Revision(
+                       array(
+                               'id'         => 42,
+                               'page'       => 23,
+                               'title'      => Title::newFromText( "testGetContentClone_dummy" ),
+                               'content'    => $content,
+                               'length'     => $content->getSize(),
+                               'comment'    => "testing",
+                               'minor_edit' => false,
+                       )
+               );
+               $content = $rev->getContent( Revision::RAW );
+               $content->setText( "bar" );
+               $content2 = $rev->getContent( Revision::RAW );
+               $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone
+               $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text
+               $content2->setText( "bla bla" );
+               $this->assertEquals( "bar", $content->getText() ); // clones should be independent
+       }
+       /**
+        * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+        *
+        * @group Database
+        */
+       function testGetContentUncloned() {
+               $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+               $content = $rev->getContent( Revision::RAW );
+               $content2 = $rev->getContent( Revision::RAW );
+               // for immutable content like wikitext, this should be the same object
+               $this->assertSame( $content, $content2 );
+       }
+ }
+ class RevisionTestModifyableContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct( $text, "RevisionTestModifyableContent" );
+       }
+       public function copy( ) {
+               return new RevisionTestModifyableContent( $this->mText );
+       }
+       public function getText() {
+               return $this->mText;
+       }
+       public function setText( $text ) {
+               $this->mText = $text;
+       }
  }
  
+ class RevisionTestModifyableContentHandler extends TextContentHandler {
+       public function __construct( ) {
+               parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) );
+       }
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+               return new RevisionTestModifyableContent( $text );
+       }
  
+       public function makeEmptyContent() {
+               return new RevisionTestModifyableContent( '' );
+       }
+ }
@@@ -4,16 -4,6 +4,16 @@@
   * Tests timestamp parsing and output.
   */
  class TimestampTest extends MediaWikiTestCase {
 +
 +      protected function setUp() {
 +              parent::setUp();
 +
 +              $this->setMwGlobals( array(
 +                      'wgLanguageCode' => 'en',
 +                      'wgContLang' => Language::factory( 'en' ),
 +                      'wgLang' => Language::factory( 'en' ),
 +              ) );
 +      }
        /**
         * Test parsing of valid timestamps and outputing to MW format.
         * @dataProvider provideValidTimestamps
         */
        function testHumanOutput() {
                $timestamp = new MWTimestamp( time() - 3600 );
-               $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->toString() );
+               $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->inLanguage( 'en' )->text() );
        }
  
        /**
         * Returns a list of valid timestamps in the format:
         * array( type, timestamp_of_type, timestamp_in_MW )
         */
 -      function provideValidTimestamps() {
 +      public static function provideValidTimestamps() {
                return array(
                        // Various formats
                        array( TS_UNIX, '1343761268', '20120731190108' ),
@@@ -1,8 -1,39 +1,44 @@@
  <?php
  
+ /**
+  * @group ContentHandler
+  *
+  * @note: We don't make assumptions about the main namespace.
+  *        But we do expect the Help namespace to contain Wikitext.
+  *
+  */
  class TitleMethodsTest extends MediaWikiTestCase {
  
 -              $wgExtraNamespaces[ 12302 ] = 'TEST-JS';
 -              $wgExtraNamespaces[ 12303 ] = 'TEST-JS_TALK';
+       public function setup() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
 -              $wgNamespaceContentModels[ 12302 ] = CONTENT_MODEL_JAVASCRIPT;
++              $this->mergeMwGlobalArrayValue(
++                      'wgExtraNamespaces',
++                      array(
++                              12302 => 'TEST-JS',
++                              12303 => 'TEST-JS_TALK',
++                      )
++              );
 -              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
 -
 -              unset( $wgExtraNamespaces[ 12302 ] );
 -              unset( $wgExtraNamespaces[ 12303 ] );
 -
 -              unset( $wgNamespaceContentModels[ 12302 ] );
++              $this->mergeMwGlobalArrayValue(
++                      'wgNamespaceContentModels',
++                      array(
++                              12302 => CONTENT_MODEL_JAVASCRIPT,
++                      )
++              );
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+       public function teardown() {
 -      public function dataEquals() {
++              global $wgContLang;
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
 +      public static function provideEquals() {
                return array(
                        array( 'Main Page', 'Main Page', true ),
                        array( 'Main Page', 'Not The Main Page', false ),
@@@ -15,7 -46,7 +51,7 @@@
        }
  
        /**
 -       * @dataProvider dataEquals
 +       * @dataProvider provideEquals
         */
        public function testEquals( $titleA, $titleB, $expectedBool ) {
                $titleA = Title::newFromText( $titleA );
@@@ -25,7 -56,7 +61,7 @@@
                $this->assertEquals( $expectedBool, $titleB->equals( $titleA ) );
        }
  
 -      public function dataInNamespace() {
 +      public static function provideInNamespace() {
                return array(
                        array( 'Main Page', NS_MAIN, true ),
                        array( 'Main Page', NS_TALK, false ),
@@@ -39,7 -70,7 +75,7 @@@
        }
  
        /**
 -       * @dataProvider dataInNamespace
 +       * @dataProvider provideInNamespace
         */
        public function testInNamespace( $title, $ns, $expectedBool ) {
                $title = Title::newFromText( $title );
@@@ -54,7 -85,7 +90,7 @@@
                $this->assertFalse( $mainpage->inNamespaces( array( NS_PROJECT, NS_TEMPLATE ) ) );
        }
  
 -      public function dataHasSubjectNamespace() {
 +      public static function provideHasSubjectNamespace() {
                return array(
                        array( 'Main Page', NS_MAIN, true ),
                        array( 'Main Page', NS_TALK, true ),
        }
  
        /**
 -       * @dataProvider dataHasSubjectNamespace
 +       * @dataProvider provideHasSubjectNamespace
         */
        public function testHasSubjectNamespace( $title, $ns, $expectedBool ) {
                $title = Title::newFromText( $title );
                $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
        }
  
 -      public function dataIsCssOrJsPage() {
+       public function dataGetContentModel() {
+               return array(
+                       array( 'Help:Foo', CONTENT_MODEL_WIKITEXT ),
+                       array( 'Help:Foo.js', CONTENT_MODEL_WIKITEXT ),
+                       array( 'Help:Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
+                       array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
+                       array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
+                       array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+                       array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
+                       array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
+                       array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
+                       array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
+                       array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+                       array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
+                       array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ),
+                       array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
+                       array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+                       array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+                       array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ),
+                       array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
+                       array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
+                       array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ),
+               );
+       }
+       /**
+        * @dataProvider dataGetContentModel
+        */
+       public function testGetContentModel( $title, $expectedModelId ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expectedModelId, $title->getContentModel() );
+       }
+       /**
+        * @dataProvider dataGetContentModel
+        */
+       public function testHasContentModel( $title, $expectedModelId ) {
+               $title = Title::newFromText( $title );
+               $this->assertTrue( $title->hasContentModel( $expectedModelId ) );
+       }
 +      public static function provideIsCssOrJsPage() {
                return array(
-                       array( 'Foo', false ),
-                       array( 'Foo.js', false ),
-                       array( 'Foo/bar.js', false ),
+                       array( 'Help:Foo', false ),
+                       array( 'Help:Foo.js', false ),
+                       array( 'Help:Foo/bar.js', false ),
                        array( 'User:Foo', false ),
                        array( 'User:Foo.js', false ),
                        array( 'User:Foo/bar.js', false ),
                        array( 'MediaWiki:Foo.JS', false ),
                        array( 'MediaWiki:Foo.CSS', false ),
                        array( 'MediaWiki:Foo.css.xxx', false ),
+                       array( 'TEST-JS:Foo', false ),
+                       array( 'TEST-JS:Foo.js', false ),
                );
        }
  
        /**
 -       * @dataProvider dataIsCssOrJsPage
 +       * @dataProvider provideIsCssOrJsPage
         */
        public function testIsCssOrJsPage( $title, $expectedBool ) {
                $title = Title::newFromText( $title );
        }
  
  
 -      public function dataIsCssJsSubpage() {
 +      public static function provideIsCssJsSubpage() {
                return array(
-                       array( 'Foo', false ),
-                       array( 'Foo.js', false ),
-                       array( 'Foo/bar.js', false ),
+                       array( 'Help:Foo', false ),
+                       array( 'Help:Foo.js', false ),
+                       array( 'Help:Foo/bar.js', false ),
                        array( 'User:Foo', false ),
                        array( 'User:Foo.js', false ),
                        array( 'User:Foo/bar.js', true ),
                        array( 'MediaWiki:Foo.js', false ),
                        array( 'User:Foo/bar.JS', false ),
                        array( 'User:Foo/bar.CSS', false ),
+                       array( 'TEST-JS:Foo', false ),
+                       array( 'TEST-JS:Foo.js', false ),
                );
        }
  
        /**
 -       * @dataProvider dataIsCssJsSubpage
 +       * @dataProvider provideIsCssJsSubpage
         */
        public function testIsCssJsSubpage( $title, $expectedBool ) {
                $title = Title::newFromText( $title );
                $this->assertEquals( $expectedBool, $title->isCssJsSubpage() );
        }
  
 -      public function dataIsCssSubpage() {
 +      public static function provideIsCssSubpage() {
                return array(
-                       array( 'Foo', false ),
-                       array( 'Foo.css', false ),
+                       array( 'Help:Foo', false ),
+                       array( 'Help:Foo.css', false ),
                        array( 'User:Foo', false ),
                        array( 'User:Foo.js', false ),
                        array( 'User:Foo.css', false ),
        }
  
        /**
 -       * @dataProvider dataIsCssSubpage
 +       * @dataProvider provideIsCssSubpage
         */
        public function testIsCssSubpage( $title, $expectedBool ) {
                $title = Title::newFromText( $title );
                $this->assertEquals( $expectedBool, $title->isCssSubpage() );
        }
  
 -      public function dataIsJsSubpage() {
 +      public static function provideIsJsSubpage() {
                return array(
-                       array( 'Foo', false ),
-                       array( 'Foo.css', false ),
+                       array( 'Help:Foo', false ),
+                       array( 'Help:Foo.css', false ),
                        array( 'User:Foo', false ),
                        array( 'User:Foo.js', false ),
                        array( 'User:Foo.css', false ),
        }
  
        /**
 -       * @dataProvider dataIsJsSubpage
 +       * @dataProvider provideIsJsSubpage
         */
        public function testIsJsSubpage( $title, $expectedBool ) {
                $title = Title::newFromText( $title );
                $this->assertEquals( $expectedBool, $title->isJsSubpage() );
        }
  
 -      public function dataIsWikitextPage() {
 +      public static function provideIsWikitextPage() {
                return array(
-                       array( 'Foo', true ),
-                       array( 'Foo.js', true ),
-                       array( 'Foo/bar.js', true ),
+                       array( 'Help:Foo', true ),
+                       array( 'Help:Foo.js', true ),
+                       array( 'Help:Foo/bar.js', true ),
                        array( 'User:Foo', true ),
                        array( 'User:Foo.js', true ),
                        array( 'User:Foo/bar.js', false ),
                        array( 'MediaWiki:Foo/bar.css', false ),
                        array( 'User:Foo/bar.JS', true ),
                        array( 'User:Foo/bar.CSS', true ),
+                       array( 'TEST-JS:Foo', false ),
+                       array( 'TEST-JS:Foo.js', false ),
+                       array( 'TEST-JS_TALK:Foo.js', true ),
                );
        }
  
        /**
 -       * @dataProvider dataIsWikitextPage
 +       * @dataProvider provideIsWikitextPage
         */
        public function testIsWikitextPage( $title, $expectedBool ) {
                $title = Title::newFromText( $title );
@@@ -1,20 -1,12 +1,25 @@@
  <?php
  
+ /**
+  *
+  * @group Database
+  *        ^--- needed for language cache stuff
+  */
  class TitleTest extends MediaWikiTestCase {
  
 +      protected function setUp() {
 +              parent::setUp();
 +
 +              $this->setMwGlobals( array(
 +                      'wgLanguageCode' => 'en',
 +                      'wgContLang' => Language::factory( 'en' ),
 +                      // User language
 +                      'wgLang' => Language::factory( 'en' ),
 +                      'wgAllowUserJs' => false,
 +                      'wgDefaultLanguageVariant' => false,
 +              ) );
 +      }
 +
        function testLegalChars() {
                $titlechars = Title::legalChars();
  
@@@ -29,7 -21,7 +34,7 @@@
        }
  
        /**
 -       * @dataProvider dataBug31100
 +       * @dataProvider provideBug31100
         */
        function testBug31100FixSpecialName( $text, $expectedParam ) {
                $title = Title::newFromText( $text );
@@@ -43,7 -35,7 +48,7 @@@
                $this->assertEquals( $expectedParam, $par, "Bug 31100 regression check: Title->fixSpecialName() should preserve parameter" );
        }
  
 -      function dataBug31100() {
 +      public static function provideBug31100() {
                return array(
                        array( 'Special:Version', null ),
                        array( 'Special:Version/', '' ),
@@@ -58,7 -50,7 +63,7 @@@
         * @param string $source
         * @param string $target
         * @param array|string|true $expected Required error
 -       * @dataProvider dataTestIsValidMoveOperation
 +       * @dataProvider provideTestIsValidMoveOperation
         */
        function testIsValidMoveOperation( $source, $target, $expected ) {
                $title = Title::newFromText( $source );
@@@ -82,7 -74,7 +87,7 @@@
                return $result;
        }
        
 -      function dataTestIsValidMoveOperation() {
 +      public static function provideTestIsValidMoveOperation() {
                return array( 
                        array( 'Test', 'Test', 'selfmove' ),
                        array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
        /**
         * @dataProvider provideCasesForGetpageviewlanguage
         */
 -      function testGetpageviewlanguage( $expected, $titleText, $contLang, $lang, $variant, $msg='' ) {
 -              // Save globals
 -              global $wgContLang, $wgLang, $wgAllowUserJs, $wgLanguageCode, $wgDefaultLanguageVariant;
 -              $save['wgContLang']               = $wgContLang;
 -              $save['wgLang']                   = $wgLang;
 -              $save['wgAllowUserJs']            = $wgAllowUserJs;
 -              $save['wgLanguageCode']           = $wgLanguageCode;
 -              $save['wgDefaultLanguageVariant'] = $wgDefaultLanguageVariant;
 -
 -              // Setup test environnement:
 -              $wgContLang = Language::factory( $contLang );
 -              $wgLang     = Language::factory( $lang );
 -              # To test out .js titles:
 -              $wgAllowUserJs = true;
 +      function testGetpageviewlanguage( $expected, $titleText, $contLang, $lang, $variant, $msg = '' ) {
 +              global $wgLanguageCode, $wgContLang, $wgLang, $wgDefaultLanguageVariant, $wgAllowUserJs;
 +
 +              // Setup environnement for this test
                $wgLanguageCode = $contLang;
 +              $wgContLang = Language::factory( $contLang );
 +              $wgLang = Language::factory( $lang );
                $wgDefaultLanguageVariant = $variant;
 +              $wgAllowUserJs = true;
  
                $title = Title::newFromText( $titleText );
                $this->assertInstanceOf( 'Title', $title,
                        $title->getPageViewLanguage()->getCode(),
                        $msg
                );
 -
 -              // Restore globals
 -              $wgContLang               = $save['wgContLang'];
 -              $wgLang                   = $save['wgLang'];
 -              $wgAllowUserJs            = $save['wgAllowUserJs'];
 -              $wgLanguageCode           = $save['wgLanguageCode'];
 -              $wgDefaultLanguageVariant = $save['wgDefaultLanguageVariant'];
        }
  
        function provideCasesForGetpageviewlanguage() {
                );
        }
  
 -      function provideRootTitleCases() {
 +      public static function provideRootTitleCases() {
                return array(
                        # Title, expected base, optional message
                        array('User:John_Doe/subOne/subTwo', 'John Doe' ),
@@@ -1,5 -1,6 +1,6 @@@
  <?php
  /**
+ * @group ContentHandler
  * @group Database
  * ^--- important, causes temporary tables to be used instead of the real database
  **/
@@@ -11,30 -12,33 +12,33 @@@ class WikiPageTest extends MediaWikiLan
        function  __construct( $name = null, array $data = array(), $dataName = '' ) {
                parent::__construct( $name, $data, $dataName );
  
-               $this->tablesUsed = array_merge ( $this->tablesUsed,
-                                                 array( 'page',
-                                                      'revision',
-                                                      'text',
+               $this->tablesUsed = array_merge (
+                       $this->tablesUsed,
+                       array( 'page',
+                                       'revision',
+                                       'text',
  
-                                                      'recentchanges',
-                                                      'logging',
+                                       'recentchanges',
+                                       'logging',
  
-                                                      'page_props',
-                                                      'pagelinks',
-                                                      'categorylinks',
-                                                      'langlinks',
-                                                      'externallinks',
-                                                      'imagelinks',
-                                                      'templatelinks',
-                                                      'iwlinks' ) );
+                                       'page_props',
+                                       'pagelinks',
+                                       'categorylinks',
+                                       'langlinks',
+                                       'externallinks',
+                                       'imagelinks',
+                                       'templatelinks',
+                                       'iwlinks' ) );
        }
  
 -      public function setUp() {
 +      protected function setUp() {
                parent::setUp();
                $this->pages_to_delete = array();
+               LinkCache::singleton()->clear(); # avoid cached redirect status, etc
        }
  
 -      public function tearDown() {
 +      protected function tearDown() {
                foreach ( $this->pages_to_delete as $p ) {
                        /* @var $p WikiPage */
  
                parent::tearDown();
        }
  
-       protected function newPage( $title ) {
-               if ( is_string( $title ) ) $title = Title::newFromText( $title );
+       /**
+        * @param Title $title
+        * @param String $model
+        * @return WikiPage
+        */
+       protected function newPage( $title, $model = null ) {
+               if ( is_string( $title ) ) {
+                       $title = Title::newFromText( $title );
+               }
  
                $p = new WikiPage( $title );
  
                return $p;
        }
  
+       /**
+        * @param String|Title|WikiPage $page
+        * @param String $text
+        * @param int $model
+        *
+        * @return WikiPage
+        */
        protected function createPage( $page, $text, $model = null ) {
-               if ( is_string( $page ) ) $page = Title::newFromText( $page );
-               if ( $page instanceof Title ) $page = $this->newPage( $page );
+               if ( is_string( $page ) ) {
+                       $page = Title::newFromText( $page );
+               }
+               if ( $page instanceof Title ) {
+                       $page = $this->newPage( $page, $model );
+               }
  
-               $page->doEdit( $text, "testing", EDIT_NEW );
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
  
+       public function testDoEditContent() {
+               $title = Title::newFromText( "WikiPageTest_testDoEditContent" );
+               $page = $this->newPage( $title );
+               $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+                                               . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
+                                               $title, CONTENT_MODEL_WIKITEXT );
+               $page->doEditContent( $content, "[[testing]] 1" );
+               $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+               $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+               $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+               $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+               $id = $page->getId();
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+               $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+               # ------------------------
+               $page = new WikiPage( $title );
+               $retrieved = $page->getContent();
+               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+               # ------------------------
+               $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+                                                                                               . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
+                                                                                               $title, CONTENT_MODEL_WIKITEXT );
+               $page->doEditContent( $content, "testing 2" );
+               # ------------------------
+               $page = new WikiPage( $title );
+               $retrieved = $page->getContent();
+               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+               $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+       }
        public function testDoEdit() {
-               $title = Title::newFromText( "WikiPageTest_testDoEdit" );
+               $this->hideDeprecated( "WikiPage::doEdit" );
+               $this->hideDeprecated( "WikiPage::getText" );
+               $this->hideDeprecated( "Revision::getText" );
+               //NOTE: assume help namespace will default to wikitext
+               $title = Title::newFromText( "Help:WikiPageTest_testDoEdit" );
  
                $page = $this->newPage( $title );
  
                $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
-                      . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
+                               . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
  
-               $page->doEdit( $text, "testing 1" );
+               $page->doEdit( $text, "[[testing]] 1" );
  
+               $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+               $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
                $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
                $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
  
                $id = $page->getId();
  
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+               $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
                # ------------------------
                $page = new WikiPage( $title );
  
  
                # ------------------------
                $text = "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
-                      . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.";
+                               . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.";
  
                $page->doEdit( $text, "testing 2" );
  
        public function testDoQuickEdit() {
                global $wgUser;
  
-               $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" );
+               $this->hideDeprecated( "WikiPage::doQuickEdit" );
+               //NOTE: assume help namespace will default to wikitext
+               $page = $this->createPage( "Help:WikiPageTest_testDoQuickEdit", "original text" );
  
                $text = "quick text";
                $page->doQuickEdit( $text, $wgUser, "testing q" );
                $this->assertEquals( $text, $page->getText() );
        }
  
+       public function testDoQuickEditContent() {
+               global $wgUser;
+               $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text", CONTENT_MODEL_WIKITEXT );
+               $content = ContentHandler::makeContent( "quick text", $page->getTitle(), CONTENT_MODEL_WIKITEXT );
+               $page->doQuickEditContent( $content, $wgUser, "testing q" );
+               # ---------------------
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertTrue( $content->equals( $page->getContent() ) );
+       }
        public function testDoDeleteArticle() {
-               $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
+               $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT );
                $id = $page->getId();
  
                $page->doDeleteArticle( "testing deletion" );
  
+               $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" );
+               $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
                $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
+               $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
                $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
  
                $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
        }
  
        public function testDoDeleteUpdates() {
-               $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
+               $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo", CONTENT_MODEL_WIKITEXT );
                $id = $page->getId();
  
                $page->doDeleteUpdates( $id );
                $this->assertNull( $rev );
  
                # -----------------
-               $this->createPage( $page, "some text" );
+               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
  
                $rev = $page->getRevision();
  
                $this->assertEquals( $page->getLatest(), $rev->getId() );
-               $this->assertEquals( "some text", $rev->getText() );
+               $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
+       }
+       public function testGetContent() {
+               $page = $this->newPage( "WikiPageTest_testGetContent" );
+               $content = $page->getContent();
+               $this->assertNull( $content );
+               # -----------------
+               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
+               $content = $page->getContent();
+               $this->assertEquals( "some text", $content->getNativeData() );
        }
  
        public function testGetText() {
+               $this->hideDeprecated( "WikiPage::getText" );
                $page = $this->newPage( "WikiPageTest_testGetText" );
  
                $text = $page->getText();
                $this->assertFalse( $text );
  
                # -----------------
-               $this->createPage( $page, "some text" );
+               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
  
                $text = $page->getText();
                $this->assertEquals( "some text", $text );
        }
  
        public function testGetRawText() {
+               $this->hideDeprecated( "WikiPage::getRawText" );
                $page = $this->newPage( "WikiPageTest_testGetRawText" );
  
                $text = $page->getRawText();
                $this->assertFalse( $text );
  
                # -----------------
-               $this->createPage( $page, "some text" );
+               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
  
                $text = $page->getRawText();
                $this->assertEquals( "some text", $text );
        }
  
-       
+       public function testGetContentModel() {
+               global $wgContentHandlerUseDB;
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+               $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
+       }
+       public function testGetContentHandler() {
+               global $wgContentHandlerUseDB;
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+               $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
+       }
        public function testExists() {
                $page = $this->newPage( "WikiPageTest_testExists" );
                $this->assertFalse( $page->exists() );
  
                # -----------------
-               $this->createPage( $page, "some text" );
+               $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
                $this->assertTrue( $page->exists() );
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertFalse( $page->exists() );
        }
  
 -      public function dataHasViewableContent() {
 +      public static function provideHasViewableContent() {
                return array(
                        array( 'WikiPageTest_testHasViewableContent', false, true ),
                        array( 'Special:WikiPageTest_testHasViewableContent', false ),
        }
  
        /**
 -       * @dataProvider dataHasViewableContent
 +       * @dataProvider provideHasViewableContent
         */
        public function testHasViewableContent( $title, $viewable, $create = false ) {
                $page = $this->newPage( $title );
                $this->assertEquals( $viewable, $page->hasViewableContent() );
  
                if ( $create ) {
-                       $this->createPage( $page, "some text" );
+                       $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT );
                        $this->assertTrue( $page->hasViewableContent() );
  
                        $page = new WikiPage( $page->getTitle() );
                }
        }
  
 -      public function dataGetRedirectTarget() {
 +      public static function provideGetRedirectTarget() {
                return array(
-                       array( 'WikiPageTest_testGetRedirectTarget_1', "hello world", null ),
-                       array( 'WikiPageTest_testGetRedirectTarget_2', "#REDIRECT [[hello world]]", "Hello world" ),
+                       array( 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT, "hello world", null ),
+                       array( 'WikiPageTest_testGetRedirectTarget_2', CONTENT_MODEL_WIKITEXT, "#REDIRECT [[hello world]]", "Hello world" ),
                );
        }
  
        /**
 -       * @dataProvider dataGetRedirectTarget
 +       * @dataProvider provideGetRedirectTarget
         */
-       public function testGetRedirectTarget( $title, $text, $target ) {
-               $page = $this->createPage( $title, $text );
+       public function testGetRedirectTarget( $title, $model, $text, $target ) {
+               $page = $this->createPage( $title, $text, $model );
+               # sanity check, because this test seems to fail for no reason for some people.
+               $c = $page->getContent();
+               $this->assertEquals( 'WikitextContent', get_class( $c ) );
  
                # now, test the actual redirect
                $t = $page->getRedirectTarget();
        }
  
        /**
 -       * @dataProvider dataGetRedirectTarget
 +       * @dataProvider provideGetRedirectTarget
         */
-       public function testIsRedirect( $title, $text, $target ) {
-               $page = $this->createPage( $title, $text );
+       public function testIsRedirect( $title, $model, $text, $target ) {
+               $page = $this->createPage( $title, $text, $model );
                $this->assertEquals( !is_null( $target ), $page->isRedirect() );
        }
  
 -      public function dataIsCountable() {
 +      public static function provideIsCountable() {
                return array(
  
                        // any
                        array( 'WikiPageTest_testIsCountable',
-                              '',
-                              'any',
-                              true
+                                       CONTENT_MODEL_WIKITEXT,
+                                       '',
+                                       'any',
+                                       true
                        ),
                        array( 'WikiPageTest_testIsCountable',
-                              'Foo',
-                              'any',
-                              true
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo',
+                                       'any',
+                                       true
                        ),
  
                        // comma
                        array( 'WikiPageTest_testIsCountable',
-                              'Foo',
-                              'comma',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo',
+                                       'comma',
+                                       false
                        ),
                        array( 'WikiPageTest_testIsCountable',
-                              'Foo, bar',
-                              'comma',
-                              true
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo, bar',
+                                       'comma',
+                                       true
                        ),
  
                        // link
                        array( 'WikiPageTest_testIsCountable',
-                              'Foo',
-                              'link',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo',
+                                       'link',
+                                       false
                        ),
                        array( 'WikiPageTest_testIsCountable',
-                              'Foo [[bar]]',
-                              'link',
-                              true
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo [[bar]]',
+                                       'link',
+                                       true
                        ),
  
                        // redirects
                        array( 'WikiPageTest_testIsCountable',
-                              '#REDIRECT [[bar]]',
-                              'any',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       '#REDIRECT [[bar]]',
+                                       'any',
+                                       false
                        ),
                        array( 'WikiPageTest_testIsCountable',
-                              '#REDIRECT [[bar]]',
-                              'comma',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       '#REDIRECT [[bar]]',
+                                       'comma',
+                                       false
                        ),
                        array( 'WikiPageTest_testIsCountable',
-                              '#REDIRECT [[bar]]',
-                              'link',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       '#REDIRECT [[bar]]',
+                                       'link',
+                                       false
                        ),
  
                        // not a content namespace
                        array( 'Talk:WikiPageTest_testIsCountable',
-                              'Foo',
-                              'any',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo',
+                                       'any',
+                                       false
                        ),
                        array( 'Talk:WikiPageTest_testIsCountable',
-                              'Foo, bar',
-                              'comma',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo, bar',
+                                       'comma',
+                                       false
                        ),
                        array( 'Talk:WikiPageTest_testIsCountable',
-                              'Foo [[bar]]',
-                              'link',
-                              false
+                                       CONTENT_MODEL_WIKITEXT,
+                                       'Foo [[bar]]',
+                                       'link',
+                                       false
                        ),
  
                        // not a content namespace, different model
                        array( 'MediaWiki:WikiPageTest_testIsCountable.js',
-                              'Foo',
-                              'any',
-                              false
+                                       null,
+                                       'Foo',
+                                       'any',
+                                       false
                        ),
                        array( 'MediaWiki:WikiPageTest_testIsCountable.js',
-                              'Foo, bar',
-                              'comma',
-                              false
+                                       null,
+                                       'Foo, bar',
+                                       'comma',
+                                       false
                        ),
                        array( 'MediaWiki:WikiPageTest_testIsCountable.js',
-                              'Foo [[bar]]',
-                              'link',
-                              false
+                                       null,
+                                       'Foo [[bar]]',
+                                       'link',
+                                       false
                        ),
                );
        }
  
  
        /**
 -       * @dataProvider dataIsCountable
 +       * @dataProvider provideIsCountable
         */
-       public function testIsCountable( $title, $text, $mode, $expected ) {
+       public function testIsCountable( $title, $model, $text, $mode, $expected ) {
                global $wgArticleCountMethod;
  
-               $old = $wgArticleCountMethod;
+               $oldArticleCountMethod = $wgArticleCountMethod;
                $wgArticleCountMethod = $mode;
  
-               $page = $this->createPage( $title, $text );
-               $editInfo = $page->prepareTextForEdit( $page->getText() );
+               $page = $this->createPage( $title, $text, $model );
+               $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                                       array( 'pl_from' => $page->getId() ), __METHOD__ );
+               $editInfo = $page->prepareContentForEdit( $page->getContent() );
  
                $v = $page->isCountable();
                $w = $page->isCountable( $editInfo );
-               $wgArticleCountMethod = $old;
+               $wgArticleCountMethod = $oldArticleCountMethod;
  
                $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true )
-                                                   . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+                                                                                       . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
  
                $this->assertEquals( $expected, $w, "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
-                                                   . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+                                                                                       . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
        }
  
 -      public function dataGetParserOutput() {
 +      public static function provideGetParserOutput() {
                return array(
-                       array("hello ''world''\n", "<p>hello <i>world</i></p>"),
+                       array( CONTENT_MODEL_WIKITEXT, "hello ''world''\n", "<p>hello <i>world</i></p>"),
                        // @todo: more...?
                );
        }
  
        /**
 -       * @dataProvider dataGetParserOutput
 +       * @dataProvider provideGetParserOutput
         */
-       public function testGetParserOutput( $text, $expectedHtml ) {
-               $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text );
+       public function testGetParserOutput( $model, $text, $expectedHtml ) {
+               $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text, $model );
  
                $opt = new ParserOptions();
                $po = $page->getParserOutput( $opt );
@@@ -427,57 -605,79 +605,79 @@@ more stuf
  ";
  
  
-       public static function provideReplaceSection() {
+       public function dataReplaceSection() {
+               //NOTE: assume the Help namespace to contain wikitext
                return array(
-                       array( 'WikiPageTest_testReplaceSection',
-                              WikiPageTest::$sections,
-                              "0",
-                              "No more",
-                              null,
-                              trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) )
-                       ),
-                       array( 'WikiPageTest_testReplaceSection',
-                              WikiPageTest::$sections,
-                              "",
-                              "No more",
-                              null,
-                              "No more"
-                       ),
-                       array( 'WikiPageTest_testReplaceSection',
-                              WikiPageTest::$sections,
-                              "2",
-                              "== TEST ==\nmore fun",
-                              null,
-                              trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikiPageTest::$sections ) )
-                       ),
-                       array( 'WikiPageTest_testReplaceSection',
-                              WikiPageTest::$sections,
-                              "8",
-                              "No more",
-                              null,
-                              trim( WikiPageTest::$sections )
-                       ),
-                       array( 'WikiPageTest_testReplaceSection',
-                              WikiPageTest::$sections,
-                              "new",
-                              "No more",
-                              "New",
-                              trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more"
+                       array( 'Help:WikiPageTest_testReplaceSection',
+                                       CONTENT_MODEL_WIKITEXT,
+                                       WikiPageTest::$sections,
+                                       "0",
+                                       "No more",
+                                       null,
+                                       trim( preg_replace( '/^Intro/sm', 'No more', WikiPageTest::$sections ) )
+                       ),
+                       array( 'Help:WikiPageTest_testReplaceSection',
+                                       CONTENT_MODEL_WIKITEXT,
+                                       WikiPageTest::$sections,
+                                       "",
+                                       "No more",
+                                       null,
+                                       "No more"
+                       ),
+                       array( 'Help:WikiPageTest_testReplaceSection',
+                                       CONTENT_MODEL_WIKITEXT,
+                                       WikiPageTest::$sections,
+                                       "2",
+                                       "== TEST ==\nmore fun",
+                                       null,
+                                       trim( preg_replace( '/^== test ==.*== foo ==/sm',
+                                                                               "== TEST ==\nmore fun\n\n== foo ==",
+                                                                               WikiPageTest::$sections ) )
+                       ),
+                       array( 'Help:WikiPageTest_testReplaceSection',
+                                       CONTENT_MODEL_WIKITEXT,
+                                       WikiPageTest::$sections,
+                                       "8",
+                                       "No more",
+                                       null,
+                                       trim( WikiPageTest::$sections )
+                       ),
+                       array( 'Help:WikiPageTest_testReplaceSection',
+                                       CONTENT_MODEL_WIKITEXT,
+                                       WikiPageTest::$sections,
+                                       "new",
+                                       "No more",
+                                       "New",
+                                       trim( WikiPageTest::$sections ) . "\n\n== New ==\n\nNo more"
                        ),
                );
        }
  
        /**
-        * @dataProvider provideReplaceSection
+        * @dataProvider dataReplaceSection
         */
-       public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) {
-               $page = $this->createPage( $title, $text );
+       public function testReplaceSection( $title, $model, $text, $section, $with, $sectionTitle, $expected ) {
+               $this->hideDeprecated( "WikiPage::replaceSection" );
+               $page = $this->createPage( $title, $text, $model );
                $text = $page->replaceSection( $section, $with, $sectionTitle );
                $text = trim( $text );
  
                $this->assertEquals( $expected, $text );
        }
  
+       /**
+        * @dataProvider dataReplaceSection
+        */
+       public function testReplaceSectionContent( $title, $model, $text, $section, $with, $sectionTitle, $expected ) {
+               $page = $this->createPage( $title, $text, $model );
+               $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
+               $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
+               $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+       }
        /* @todo FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
-               $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+                                                               "section one", EDIT_NEW, false, $admin );
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section two", 0, false, $user1 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+                                                               "adding section two", 0, false, $user1 );
  
                $user2 = new User();
                $user2->setName( "127.0.2.13" );
                $text .= "\n\nthree";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section three", 0, false, $user2 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
+                                                               "adding section three", 0, false, $user2 );
  
                # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
                # or not committed under some circumstances. so, make sure the last revision has the right user name.
                }
  
                $page = new WikiPage( $page->getTitle() );
-               $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
-               $this->assertEquals( "one\n\ntwo", $page->getText() );
+               $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
+                                                               "rollback did not revert to the correct revision" );
+               $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
  
        /**
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
-               $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+                                                               "section one", EDIT_NEW, false, $admin );
                $rev1 = $page->getRevision();
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section two", 0, false, $user1 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
+                                                               "adding section two", 0, false, $user1 );
  
                # now, try the rollback
                $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
                }
  
                $page = new WikiPage( $page->getTitle() );
-               $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
-               $this->assertEquals( "one", $page->getText() );
+               $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
+                                                       "rollback did not revert to the correct revision" );
+               $this->assertEquals( "one", $page->getContent()->getNativeData() );
        }
  
 -      public function dataGetAutosummary( ) {
 +      public static function provideGetAutosummary( ) {
                return array(
                        array(
                                'Hello there, world!',
        }
  
        /**
 -       * @dataProvider dataGetAutoSummary
 +       * @dataProvider provideGetAutoSummary
         */
        public function testGetAutosummary( $old, $new, $flags, $expected ) {
+               $this->hideDeprecated( "WikiPage::getAutosummary" );
                $page = $this->newPage( "WikiPageTest_testGetAutosummary" );
  
                $summary = $page->getAutosummary( $old, $new, $flags );
  
-               $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" );
+               $this->assertTrue( (bool)preg_match( $expected, $summary ),
+                                                       "Autosummary didn't match expected pattern $expected: $summary" );
        }
  
 -      public function dataGetAutoDeleteReason( ) {
 +      public static function provideGetAutoDeleteReason( ) {
                return array(
                        array(
                                array(),
                        array(
                                array(
                                        array( "first edit: "
-                                            . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
-                                            . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. "
-                                            . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea "
-                                            . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ),
+                                                . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
+                                                . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. "
+                                                . "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea "
+                                                . "takimata sanctus est Lorem ipsum dolor sit amet.'", null ),
                                ),
                                '/first edit:.*\.\.\."/',
                                false
        }
  
        /**
 -       * @dataProvider dataGetAutoDeleteReason
 +       * @dataProvider provideGetAutoDeleteReason
         */
        public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
                global $wgUser;
  
-               $page = $this->newPage( "WikiPageTest_testGetAutoDeleteReason" );
+               //NOTE: assume Help namespace to contain wikitext
+               $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
  
                $c = 1;
  
                        if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
                        else $user = $wgUser;
  
-                       $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
+                       $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
+                       $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
  
                        $c += 1;
                }
                $reason = $page->getAutoDeleteReason( $hasHistory );
  
                if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) $this->assertEquals( $expectedResult, $reason );
-               else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), "Autosummary didn't match expected pattern $expectedResult: $reason" );
+               else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
+                                                               "Autosummary didn't match expected pattern $expectedResult: $reason" );
  
-               $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
+               $this->assertEquals( $expectedHistory, $hasHistory,
+                                                       "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
  
                $page->doDeleteArticle( "done" );
        }
  
 -      public function dataPreSaveTransform() {
 +      public static function providePreSaveTransform() {
                return array(
                        array( 'hello this is ~~~',
-                              "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+                                       "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
                        ),
                        array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
-                              'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                                       'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
                        ),
                );
        }
  
        /**
 -       * @dataProvider dataPreSaveTransform
 +       * @dataProvider providePreSaveTransform
         */
        public function testPreSaveTransform( $text, $expected ) {
                $this->hideDeprecated( 'WikiPage::preSaveTransform' );
                $user = new User();
                $user->setName("127.0.0.1");
  
-               $page = $this->newPage( "WikiPageTest_testPreloadTransform" );
+               //NOTE: assume Help namespace to contain wikitext
+               $page = $this->newPage( "Help:WikiPageTest_testPreloadTransform" );
                $text = $page->preSaveTransform( $text, $user );
  
                $this->assertEquals( $expected, $text );
@@@ -7,7 -7,7 +7,7 @@@
   */
  class ApiWatchTest extends ApiTestCase {
  
 -      function setUp() {
 +      protected function setUp() {
                parent::setUp();
                $this->doLogin();
        }
@@@ -29,7 -29,7 +29,7 @@@
  
                $data = $this->doApiRequest( array(
                        'action' => 'edit',
-                       'title' => 'UTPage',
+                       'title' => 'Help:UTPage', // Help namespace is hopefully wikitext
                        'text' => 'new text',
                        'token' => $pageinfo['edittoken'],
                        'watchlist' => 'watch' ) );
@@@ -81,7 -81,7 +81,7 @@@
                $data = $this->doApiRequest( array(
                        'action' => 'protect',
                        'token' => $pageinfo['protecttoken'],
-                       'title' => 'UTPage',
+                       'title' => 'Help:UTPage',
                        'protections' => 'edit=sysop',
                        'watchlist' => 'unwatch' ) );
  
  
                $pageinfo = $this->getTokens();
  
-               if ( !Title::newFromText( 'UTPage' )->exists() ) {
-                       $this->markTestSkipped( "The article [[UTPage]] does not exist" ); //TODO: just create it?
+               if ( !Title::newFromText( 'Help:UTPage' )->exists() ) {
+                       $this->markTestSkipped( "The article [[Help:UTPage]] does not exist" ); //TODO: just create it?
                }
  
                $data = $this->doApiRequest( array(
                        'action' => 'query',
                        'prop' => 'revisions',
-                       'titles' => 'UTPage',
+                       'titles' => 'Help:UTPage',
                        'rvtoken' => 'rollback' ) );
  
                $this->assertArrayHasKey( 'query', $data[0] );
                $key = array_pop( $keys );
  
                if ( isset( $data[0]['query']['pages'][$key]['missing'] ) ) {
-                       $this->markTestSkipped( "Target page (UTPage) doesn't exist" );
+                       $this->markTestSkipped( "Target page (Help:UTPage) doesn't exist" );
                }
  
                $this->assertArrayHasKey( 'pageid', $data[0]['query']['pages'][$key] );
                try {
                        $data = $this->doApiRequest( array(
                                'action' => 'rollback',
-                               'title' => 'UTPage',
+                               'title' => 'Help:UTPage',
                                'user' => $revinfo['user'],
                                'token' => $pageinfo['rollbacktoken'],
                                'watchlist' => 'watch' ) );
                        $this->assertArrayHasKey( 'title', $data[0]['rollback'] );
                } catch( UsageException $ue ) {
                        if( $ue->getCodeString() == 'onlyauthor' ) {
-                               $this->markTestIncomplete( "Only one author to 'UTPage', cannot test rollback" );
+                               $this->markTestIncomplete( "Only one author to 'Help:UTPage', cannot test rollback" );
                        } else {
                                $this->fail( "Received error '" . $ue->getCodeString() . "'" );
                        }
                $data = $this->doApiRequest( array(
                        'action' => 'delete',
                        'token' => $pageinfo['deletetoken'],
-                       'title' => 'UTPage' ) );
+                       'title' => 'Help:UTPage' ) );
                $this->assertArrayHasKey( 'delete', $data[0] );
                $this->assertArrayHasKey( 'title', $data[0]['delete'] );
  
@@@ -7,11 -7,15 +7,11 @@@
  class SearchEngineTest extends MediaWikiTestCase {
        protected $search, $pageList;
  
 -      function tearDown() {
 -              unset( $this->search );
 -      }
 -
        /**
         * Checks for database type & version.
         * Will skip current test if DB does not support search.
         */
 -      function setUp() {
 +      protected function setUp() {
                parent::setUp();
                // Search tests require MySQL or SQLite with FTS
                # Get database type and version
                $this->search = new $searchType( $this->db );
        }
  
 +      protected function tearDown() {
 +              unset( $this->search );
 +      }
 +
        function pageExists( $title ) {
                return false;
        }
@@@ -93,7 -93,7 +93,7 @@@
                LinkCache::singleton()->clear();
  
                $page = WikiPage::factory( $title );
-               $page->doEdit( $text, $comment, 0, false, $user );
+               $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
  
                $this->pageList[] = array( $title, $page->getId() );
  
@@@ -35,7 -35,7 +35,7 @@@ abstract class DumpTestCase extends Med
         * @throws MWExcepion
         */
        protected function addRevision( Page $page, $text, $summary ) {
-               $status = $page->doEdit( $text, $summary );
+               $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary );
                if ( $status->isGood() ) {
                        $value = $status->getValue();
                        $revision = $value['revision'];
@@@ -72,7 -72,7 +72,7 @@@
         *
         * Clears $wgUser, and reports errors from addDBData to PHPUnit
         */
 -      public function setUp() {
 +      protected function setUp() {
                global $wgUser;
  
                parent::setUp();
         * @param $text_sha1 string: the base36 SHA-1 of the revision's text
         * @param $text string|false: (optional) The revision's string, or false to check for a
         *            revision stub
+        * @param $model String: the expected content model id (default: CONTENT_MODEL_WIKITEXT)
+        * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT)
         * @param $parentid int|false: (optional) id of the parent revision
         */
-       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) {
+       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false,
+                                               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) {
  
                $this->assertNodeStart( "revision" );
                $this->skipWhitespace();
                $this->skipWhitespace();
  
                $this->assertTextNode( "comment", $summary );
+               $this->skipWhitespace();
+               if ( $this->xml->name == "text" ) {
+                       // note: <text> tag may occur here or at the very end.
+                       $text_found = true;
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               } else {
+                       $text_found = false;
+               }
  
                $this->assertTextNode( "sha1", $text_sha1 );
  
+               $this->assertTextNode( "model", $model );
+               $this->skipWhitespace();
+               $this->assertTextNode( "format", $format );
+               $this->skipWhitespace();
+               if ( !$text_found ) {
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               }
+               $this->assertNodeEnd( "revision" );
+               $this->skipWhitespace();
+       }
+       protected function assertText( $id, $text_id, $text_bytes, $text ) {
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
                        $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
                        $this->assertNodeEnd( "text" );
                        $this->skipWhitespace();
                }
-               $this->assertNodeEnd( "revision" );
-               $this->skipWhitespace();
        }
  }
@@@ -156,7 -156,7 +156,7 @@@ class BaseDumpTest extends MediaWikiTes
    <siteinfo>
      <sitename>wikisvn</sitename>
      <base>http://localhost/wiki-svn/index.php/Main_Page</base>
 -    <generator>MediaWiki 1.20alpha</generator>
 +    <generator>MediaWiki 1.21alpha</generator>
      <case>first-letter</case>
      <namespaces>
        <namespace key="-2" case="first-letter">Media</namespace>
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
        <text xml:space="preserve">BackupDumperTestP1Text1</text>
+       <model name="wikitext">1</model>
+       <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
        <text xml:space="preserve">BackupDumperTestP2Text1</text>
+       <model name="wikitext">1</model>
+       <format mime="text/x-wiki">1</format>
      </revision>
      <revision>
        <id>5</id>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
        <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
+       <model name="wikitext">1</model>
+       <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+       <model name="wikitext">1</model>
+       <format mime="text/x-wiki">1</format>
        <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
      </revision>
    </page>
@@@ -74,7 -74,7 +74,7 @@@ class TextPassDumperTest extends DumpTe
  
        }
  
 -      public function setUp() {
 +      protected function setUp() {
                parent::setUp();
  
                // Since we will restrict dumping by page ranges (to allow
                $this->assertEmpty( $files, "Remaining unchecked files" );
  
                // ... and have dealt with more than one checkpoint file
-               $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" );
+               $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" );
  
                $this->expectETAOutput();
        }
    <siteinfo>
      <sitename>wikisvn</sitename>
      <base>http://localhost/wiki-svn/index.php/Main_Page</base>
 -    <generator>MediaWiki 1.20alpha</generator>
 +    <generator>MediaWiki 1.21alpha</generator>
      <case>first-letter</case>
      <namespaces>
        <namespace key="-2" case="first-letter">Media</namespace>
        </contributor>
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId1_1 . '" bytes="23" />
      </revision>
    </page>
        </contributor>
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId2_1 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary2</comment>
        <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId2_2 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary3</comment>
        <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId2_3 . '" bytes="23" />
      </revision>
      <revision>
        </contributor>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId2_4 . '" bytes="44" />
      </revision>
    </page>
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+       <model>wikitext</model>
+       <format>text/x-wiki</format>
        <text id="' . $this->textId4_1 . '" bytes="35" />
      </revision>
    </page>