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

@@@ -12,11 -12,11 +12,13 @@@ 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.
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.
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
Simple merge
@@@ -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',
@@@ -126,27 -129,17 +126,38 @@@ abstract class MediaWikiTestCase extend
                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();
 +                      }
                }
        }
  
                        }
                }
  
 -              // 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;
        }
@@@ -34,9 -38,20 +38,19 @@@ class RevisionStorageTest extends Media
                                                      '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 );
                }
        }
  
                $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
@@@ -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() {
@@@ -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 ),
                $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 ),
        }
  
  
 -      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 ),
                $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 ),
                $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 ),
                $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 ),
@@@ -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();
  
@@@ -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 */
  
                }
        }
  
 -      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() );
        }
  
  
  
        /**
 -       * @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 );
@@@ -609,11 -815,12 +815,12 @@@ more stuf
                }
  
                $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(),
                $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>',
                        ),
                );
        }