merged from master
authordaniel <daniel.kinzler@wikimedia.de>
Wed, 25 Apr 2012 17:49:09 +0000 (19:49 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Wed, 25 Apr 2012 17:49:09 +0000 (19:49 +0200)
71 files changed:
.gitreview
bin/svnstat [changed mode: 0755->0644]
bin/ulimit-tvf.sh [changed mode: 0755->0644]
bin/ulimit4.sh [changed mode: 0755->0644]
includes/Article.php
includes/AutoLoader.php
includes/Content.php [new file with mode: 0644]
includes/ContentHandler.php [new file with mode: 0644]
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/Export.php
includes/FeedUtils.php
includes/ImagePage.php
includes/Import.php
includes/LinksUpdate.php
includes/Revision.php
includes/SecondaryDBDataUpdate.php [new file with mode: 0644]
includes/SecondaryDataUpdate.php [new file with mode: 0644]
includes/Title.php
includes/WikiPage.php
includes/actions/EditAction.php
includes/actions/RawAction.php
includes/actions/RollbackAction.php
includes/api/ApiComparePages.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiFormatNone.php [new file with mode: 0644]
includes/api/ApiMain.php
includes/api/ApiParse.php
includes/api/ApiPurge.php
includes/api/ApiQueryRevisions.php
includes/diff/DairikiDiff.php
includes/diff/DifferenceEngine.php
includes/installer/Ibm_db2Updater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/SqliteUpdater.php
includes/job/RefreshLinksJob.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/specials/SpecialComparePages.php
includes/specials/SpecialUndelete.php
languages/messages/MessagesEn.php
maintenance/archives/patch-archive-ar_content_format.sql [new file with mode: 0644]
maintenance/archives/patch-archive-ar_content_model.sql [new file with mode: 0644]
maintenance/archives/patch-page-page_content_model.sql [new file with mode: 0644]
maintenance/archives/patch-revision-rev_content_format.sql [new file with mode: 0644]
maintenance/archives/patch-revision-rev_content_model.sql [new file with mode: 0644]
maintenance/cssjanus/cssjanus.py [changed mode: 0755->0644]
maintenance/cssjanus/csslex.py [changed mode: 0755->0644]
maintenance/dev/install.sh [changed mode: 0755->0644]
maintenance/dev/installmw.sh [changed mode: 0755->0644]
maintenance/dev/installphp.sh [changed mode: 0755->0644]
maintenance/dev/start.sh [changed mode: 0755->0644]
maintenance/hiphop/make [changed mode: 0755->0644]
maintenance/hiphop/run-server [changed mode: 0755->0644]
maintenance/populateRevisionLength.php
maintenance/refreshLinks.php
maintenance/storage/make-blobs [changed mode: 0755->0644]
maintenance/tables.sql
tests/phpunit/includes/ContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/CssContentTest.php [new file with mode: 0644]
tests/phpunit/includes/JavascriptContentTest.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/WikiPageTest.php [new file with mode: 0644]
tests/phpunit/includes/WikitextContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/WikitextContentTest.php [new file with mode: 0644]
tests/phpunit/install-phpunit.sh [changed mode: 0755->0644]
tests/phpunit/phpunit.php [changed mode: 0755->0644]

index 0ec44b8..46f6714 100644 (file)
@@ -3,4 +3,4 @@ host=gerrit.wikimedia.org
 port=29418
 project=mediawiki/core.git
 defaultbranch=master
-defaultrebase=0
+defaultrebase=0
\ No newline at end of file
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 393f770..a80f908 100644 (file)
@@ -37,7 +37,13 @@ class Article extends Page {
         */
        public $mParserOptions;
 
-       var $mContent;                    // !<
+       var $mContent;                    // !< #BC cruft
+
+       /**
+        * @var Content
+        */
+       var $mContentObject;
+
        var $mContentLoaded = false;      // !<
        var $mOldId;                      // !<
 
@@ -112,13 +118,14 @@ class Article extends Page {
                if ( !$page ) {
                        switch( $title->getNamespace() ) {
                                case NS_FILE:
-                                       $page = new ImagePage( $title );
+                                       $page = new ImagePage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                case NS_CATEGORY:
-                                       $page = new CategoryPage( $title );
+                                       $page = new CategoryPage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                default:
-                                       $page = new Article( $title );
+                                       $handler = ContentHandler::getForTitle( $title );
+                                       $page = $handler->createArticle( $title );
                        }
                }
                $page->setContext( $context );
@@ -188,9 +195,30 @@ class Article extends Page {
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
-        * @return string Return the text of this revision
+        * @deprecated in 1.20; use getContentObject() instead
+        *
+        * @return string The text of this revision
         */
        public function getContent() {
+               wfDeprecated( __METHOD__, '1.20' );
+               $content = $this->getContentObject();
+               return ContentHandler::getContentText( $content );
+       }
+
+       /**
+        * Returns a Content object representing the pages effective display content,
+     * not necessarily the revision's content!
+     *
+     * Note that getContent/loadContent do not follow redirects anymore.
+        * If you need to fetch redirectable content easily, try
+        * the shortcut in WikiPage::getRedirectTarget()
+        *
+        * This function has side effects! Do not use this function if you
+        * only want the real revision text if any.
+        *
+        * @return Content
+        */
+   protected function getContentObject() {
                global $wgUser;
 
                wfProfileIn( __METHOD__ );
@@ -203,17 +231,19 @@ class Article extends Page {
                                if ( $text === false ) {
                                        $text = '';
                                }
+
+                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
-                               $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
+                               $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
 
-                       return $text;
+                       return $content;
                } else {
-                       $this->fetchContent();
+                       $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
 
-                       return $this->mContent;
+                       return $this->mContentObject;
                }
        }
 
@@ -296,15 +326,44 @@ class Article extends Page {
         * Does *NOT* follow redirects.
         *
         * @return mixed string containing article contents, or false if null
+        * @deprecated in 1.20, use getContentObject() instead
         */
-       function fetchContent() {
-               if ( $this->mContentLoaded ) {
+       protected function fetchContent() { #BC cruft!
+               wfDeprecated( __METHOD__, '1.20' );
+
+               if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
 
                wfProfileIn( __METHOD__ );
 
+               $content = $this->fetchContentObject();
+
+               $this->mContent = ContentHandler::getContentText( $content ); #FIXME: get rid of mContent everywhere!
+               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft!
+
+               wfProfileOut( __METHOD__ );
+
+               return $this->mContent;
+       }
+
+
+       /**
+        * Get text content object
+        * Does *NOT* follow redirects.
+        * TODO: when is this null?
+        *
+        * @return Content|null
+        */
+       protected function fetchContentObject() {
+               if ( $this->mContentLoaded ) {
+                       return $this->mContentObject;
+               }
+
+               wfProfileIn( __METHOD__ );
+
                $this->mContentLoaded = true;
+               $this->mContent = null;
 
                $oldid = $this->getOldID();
 
@@ -312,7 +371,7 @@ class Article extends Page {
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
-               $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
+               $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; // @todo: this isn't page content but a UI message. horrible.
 
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
@@ -332,6 +391,7 @@ class Article extends Page {
                        }
 
                        $this->mRevision = $this->mPage->getRevision();
+
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
@@ -341,14 +401,14 @@ class Article extends Page {
 
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
-               $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
+               $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
 
-               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+               wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); #FIXME: register new hook
 
                wfProfileOut( __METHOD__ );
 
-               return $this->mContent;
+               return $this->mContentObject;
        }
 
        /**
@@ -381,7 +441,7 @@ class Article extends Page {
         * @return Revision|null
         */
        public function getRevisionFetched() {
-               $this->fetchContent();
+               $this->fetchContentObject();
 
                return $this->mRevision;
        }
@@ -540,7 +600,7 @@ class Article extends Page {
                                        break;
                                case 3:
                                        # This will set $this->mRevision if needed
-                                       $this->fetchContent();
+                                       $this->fetchContentObject();
 
                                        # Are we looking at an old revision
                                        if ( $oldid && $this->mRevision ) {
@@ -564,18 +624,21 @@ class Article extends Page {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
-                                       } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
+                                       } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) { #FIXME: document new hook!
+                                               # Allow extensions do their own custom view for certain pages
+                                               $outputDone = true;
+                                       } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated! #FIXME: deprecate hook!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
-                                               $text = $this->getContent();
-                                               $rt = Title::newFromRedirectArray( $text );
+                                               $content = $this->getContentObject();
+                                               $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $wgOut->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
-                                                       $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
+                                                       $this->mParserOutput = $content->getParserOutput( $this->getContext(), $oldid, $parserOptions, false );
                                                        $wgOut->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
@@ -585,8 +648,9 @@ class Article extends Page {
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
 
+                                       // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this?
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
-                                               $this->getRevIdFetched(), $useParserCache, $this->getContent() );
+                                               $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() );
 
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
@@ -680,7 +744,9 @@ class Article extends Page {
                $unhide = $wgRequest->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
 
-               $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+               $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+               $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
@@ -698,23 +764,21 @@ class Article extends Page {
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
-       protected function showCssOrJsPage() {
+       protected function showCssOrJsPage( $showCacheHint = true ) {
                global $wgOut;
 
-               $dir = $this->getContext()->getLanguage()->getDir();
-               $lang = $this->getContext()->getLanguage()->getCode();
+               if ( $showCacheHint ) {
+                       $dir = $this->getContext()->getLanguage()->getDir();
+                       $lang = $this->getContext()->getLanguage()->getCode();
 
-               $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
-                       'clearyourcache' );
+                       $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
+                               'clearyourcache' );
+               }
 
                // Give hooks a chance to customise the output
-               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
-                       // Wrap the whole lot in a <pre> and don't parse
-                       $m = array();
-                       preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
-                       $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                       $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
-                       $wgOut->addHTML( "\n</pre>\n" );
+               if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated #FIXME: hook is deprecated
+                       $po = $this->mContentObject->getParserOutput( $this->getContext() );
+                       $wgOut->addHTML( $po->getText() );
                }
        }
 
@@ -1349,7 +1413,13 @@ class Article extends Page {
                // Generate deletion reason
                $hasHistory = false;
                if ( !$reason ) {
-                       $reason = $this->generateReason( $hasHistory );
+                       try {
+                               $reason = $this->generateReason( $hasHistory );
+                       } catch (MWException $e) {
+                               # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
+                               wfDebug("Error while building auto delete summary: $e");
+                               $reason = '';
+                       }
                }
 
                // If the page has a history, insert a warning
@@ -1866,7 +1936,9 @@ class Article extends Page {
         * @return mixed
         */
        public function generateReason( &$hasHistory ) {
-               return $this->mPage->getAutoDeleteReason( $hasHistory );
+               $title = $this->mPage->getTitle();
+               $handler = ContentHandler::getForTitle( $title );
+               return $handler->getAutoDeleteReason( $title, $hasHistory );
        }
 
        // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
@@ -1904,6 +1976,7 @@ class Article extends Page {
         * @param $newtext
         * @param $flags
         * @return string
+        * @deprecated since 1.20, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
index 60cbbaa..77f69e3 100644 (file)
@@ -195,6 +195,8 @@ $wgAutoloadLocalClasses = array(
        'RevisionList' => 'includes/RevisionList.php',
        'RSSFeed' => 'includes/Feed.php',
        'Sanitizer' => 'includes/Sanitizer.php',
+    'SecondaryDataUpdate' => 'includes/SecondaryDataUpdate.php',
+    'SecondaryDBDataUpdate' => 'includes/SecondaryDBDataUpdate.php',
        'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
        'SiteConfiguration' => 'includes/SiteConfiguration.php',
        'SiteStats' => 'includes/SiteStats.php',
@@ -259,6 +261,18 @@ $wgAutoloadLocalClasses = array(
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
 
+    # content handler
+    'Content' => 'includes/Content.php',
+    'ContentHandler' => 'includes/ContentHandler.php',
+    'CssContent' => 'includes/Content.php',
+    'CssContentHandler' => 'includes/ContentHandler.php',
+    'JavaScriptContent' => 'includes/Content.php',
+    'JavaScriptContentHandler' => 'includes/ContentHandler.php',
+    'MessageContent' => 'includes/Content.php',
+    'TextContent' => 'includes/Content.php',
+    'WikitextContent' => 'includes/Content.php',
+    'WikitextContentHandler' => 'includes/ContentHandler.php',
+
        # includes/actions
        'CachedAction' => 'includes/actions/CachedAction.php',
        'CreditsAction' => 'includes/actions/CreditsAction.php',
@@ -301,6 +315,7 @@ $wgAutoloadLocalClasses = array(
        'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
        'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
        'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
+       'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
        'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
        'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
@@ -962,6 +977,12 @@ $wgAutoloadLocalClasses = array(
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
 
+       # tests/phpunit
+       'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
+       'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
+       'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
+       'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
+
        # tests/parser
        'ParserTest' => 'tests/parser/parserTest.inc',
        'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php',
diff --git a/includes/Content.php b/includes/Content.php
new file mode 100644 (file)
index 0000000..6f731cb
--- /dev/null
@@ -0,0 +1,766 @@
+<?php
+
+/**
+ * A content object represents page content, e.g. the text to show on a page.
+ * Content objects have no knowledge about how they relate to Wiki pages.
+ *
+ */
+abstract class Content {
+
+       /**
+        * Name of the content model this COntent object represents.
+        * Use with CONTENT_MODEL_XXX constants
+        *
+        * @var String $model_name
+        */
+       protected $model_name;
+
+       /**
+        * @return String a string representing the content in a way useful for building a full text search index.
+        *         If no useful representation exists, this method returns an empty string.
+        */
+       public abstract function getTextForSearchIndex( );
+
+       /**
+        * @return String the wikitext to include when another page includes this  content, or false if the content is not
+        *         includable in a wikitext page.
+        *
+        * @TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
+        * @TODO: use in parser, etc!
+        */
+       public abstract function getWikitextForTransclusion( );
+
+       /**
+        * Returns a textual representation of the content suitable for use in edit summaries and log messages.
+        *
+        * @param int $maxlength maximum length of the summary text
+        * @return String the summary text
+        */
+       public abstract function getTextForSummary( $maxlength = 250 );
+
+       /**
+        * Returns native represenation of the data. Interpretation depends on the data model used,
+        * as given by getDataModel().
+        *
+        * @return mixed the native representation of the content. Could be a string, a nested array
+        *         structure, an object, a binary blob... anything, really.
+        *
+        * @NOTE: review all calls carefully, caller must be aware of content model!
+        */
+       public abstract function getNativeData( );
+
+       /**
+        * returns the content's nominal size in bogo-bytes.
+        *
+        * @return int
+        */
+       public abstract function getSize( );
+
+       /**
+        * @param $model_name
+        */
+       public function __construct( $model_name = null ) {
+               $this->model_name = $model_name;
+       }
+
+       /**
+        * Returns the name of the content model used by this content objects.
+        * Corresponds to the CONTENT_MODEL_XXX constants.
+        *
+        * @return String the model name
+        */
+       public function getModelName() {
+               return $this->model_name;
+       }
+
+       /**
+        * Throws an MWException if $model_name is not the name of the content model
+        * supported by this Content object.
+        *
+        * @param String $model_name the model to check
+        */
+       protected function checkModelName( $model_name ) {
+               if ( $model_name !== $this->model_name ) {
+                       throw new MWException( "Bad content model: expected " . $this->model_name . " but got found " . $model_name );
+               }
+       }
+
+       /**
+        * Conveniance method that returns the ContentHandler singleton for handling the content
+        * model this Content object uses.
+        *
+        * Shorthand for ContentHandler::getForContent( $this )
+        *
+        * @return ContentHandler
+        */
+       public function getContentHandler() {
+               return ContentHandler::getForContent( $this );
+       }
+
+       /**
+        * Conveniance method that returns the default serialization format for the content model
+        * model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getDefaultFormat()
+        *
+        * @return ContentHandler
+        */
+       public function getDefaultFormat() {
+               return $this->getContentHandler()->getDefaultFormat();
+       }
+
+       /**
+        * Conveniance method that returns the list of serialization formats supported
+        * for the content model model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getSupportedFormats()
+        *
+        * @return array of supported serialization formats
+        */
+       public function getSupportedFormats() {
+               return $this->getContentHandler()->getSupportedFormats();
+       }
+
+       /**
+        * Returns true if $format is a supported serialization format for this Content object,
+        * false if it isn't.
+        *
+        * Note that this will always return true if $format is null, because null stands for the
+        * default serialization.
+        *
+        * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
+        *
+        * @param String $format the format to check
+        * @return bool whether the format is supported
+        */
+       public function isSupportedFormat( $format ) {
+               if ( !$format ) {
+                       return true; // this means "use the default"
+               }
+
+               return $this->getContentHandler()->isSupportedFormat( $format );
+       }
+
+       /**
+        * Throws an MWException if $this->isSupportedFormat( $format ) doesn't return true.
+        *
+        * @param $format
+        * @throws MWException
+        */
+       protected function checkFormat( $format ) {
+               if ( !$this->isSupportedFormat( $format ) ) {
+                       throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
+               }
+       }
+
+       /**
+        * Conveniance method for serializing this Content object.
+        *
+        * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
+        *
+        * @param null|String $format the desired serialization format (or null for the default format).
+        * @return String serialized form of this Content object
+        */
+       public function serialize( $format = null ) {
+               return $this->getContentHandler()->serializeContent( $this, $format );
+       }
+
+       /**
+        * Returns true if this Content object represents empty content.
+        *
+        * @return bool whether this Content object is empty
+        */
+       public function isEmpty() {
+               return $this->getSize() == 0;
+       }
+
+       /**
+        * Returns true if this Content objects is conceptually equivalent to the given Content object.
+        *
+        * Will returns false if $that is null.
+        * Will return true if $that === $this.
+        * Will return false if $that->getModleName() != $this->getModelName().
+        * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
+        * where the meaning of "equal" depends on the actual data model.
+        *
+        * Implementations should be careful to make equals() transitive and reflexive:
+        *
+        * * $a->equals( $b ) <=> $b->equals( $b )
+        * * $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
+        *
+        * @param Content $that the Content object to compare to
+        * @return bool true if this Content object is euqual to $that, false otherwise.
+        */
+       public function equals( Content $that = null ) {
+               if ( is_null( $that ) ){
+                       return false;
+               }
+
+               if ( $that === $this ) {
+                       return true;
+               }
+
+               if ( $that->getModelName() !== $this->getModelName() ) {
+                       return false;
+               }
+
+               return $this->getNativeData() === $that->getNativeData();
+       }
+
+       /**
+        * Return a copy of this Content object. The following must be true for the object returned
+        * if $copy = $original->copy()
+        *
+        * * get_class($original) === get_class($copy)
+        * * $original->getModelName() === $copy->getModelName()
+        * * $original->equals( $copy )
+        *
+        * If and only if the Content object is imutable, the copy() method can and should
+        * return $this. That is,  $copy === $original may be true, but only for imutable content
+        * objects.
+        *
+        * @return Content. A copy of this object
+        */
+       public abstract function copy( );
+
+       /**
+        * Returns true if this content is countable as a "real" wiki page, provided
+        * that it's also in a countable location (e.g. a current revision in the main namespace).
+        *
+        * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
+        *                        to avoid redundant parsing to find out.
+        * @return boolean
+        */
+       public abstract function isCountable( $hasLinks = null ) ;
+
+       /**
+        * @param IContextSource $context
+        * @param null $revId
+        * @param null|ParserOptions $options
+        * @param Boolean $generateHtml whether to generate Html (default: true). If false,
+        *        the result of calling getText() on the ParserOutput object returned by
+        *        this method is undefined.
+        *
+        * @return ParserOutput
+        */
+       public abstract function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = NULL, $generateHtml = true );
+
+       /**
+        * Construct the redirect destination from this content and return an
+        * array of Titles, or null if this content doesn't represent a redirect.
+        * The last element in the array is the final destination after all redirects
+        * have been resolved (up to $wgMaxRedirects times).
+        *
+        * @return Array of Titles, with the destination last
+        */
+       public function getRedirectChain() {
+               return null;
+       }
+
+       /**
+        * Construct the redirect destination from this content and return an
+        * array of Titles, or null if this content doesn't represent a redirect.
+        * This will only return the immediate redirect target, useful for
+        * the redirect table and other checks that don't need full recursion.
+        *
+        * @return Title: The corresponding Title
+        */
+       public function getRedirectTarget() {
+               return null;
+       }
+
+       /**
+        * Construct the redirect destination from this content and return the
+        * Title, or null if this content doesn't represent a redirect.
+        * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
+        * in order to provide (hopefully) the Title of the final destination instead of another redirect.
+        *
+        * @return Title
+        */
+       public function getUltimateRedirectTarget() {
+               return null;
+       }
+
+       public function isRedirect() {
+               return $this->getRedirectTarget() != null;
+       }
+
+       /**
+        * Returns the section with the given id.
+        *
+        * The default implementation returns null.
+        *
+        * @param String $sectionId the section's id, given as a numeric string. The id "0" retrieves the section before
+        *          the first heading, "1" the text between the first heading (inluded) and the second heading (excluded), etc.
+        * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
+        */
+       public function getSection( $sectionId ) {
+               return null;
+       }
+
+       /**
+        * Replaces a section of the content and returns a Content object with the section replaced.
+        *
+        * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
+        * @param $with Content: new content of the section
+        * @param $sectionTitle String: new section's subject, only if $section is 'new'
+        * @return string Complete article text, or null if error
+        */
+       public function replaceSection( $section, Content $with, $sectionTitle = ''  ) {
+               return null;
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param User $user
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
+               return $this;
+       }
+
+       /**
+        * Returns a new WikitextContent object with the given section heading prepended, if supported.
+        * The default implementation just returns this Content object unmodified, ignoring the section header.
+        *
+        * @param $header String
+        * @return Content
+        */
+       public function addSectionHeader( $header ) {
+               return $this;
+       }
+
+       /**
+        * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts = null ) {
+               return $this;
+       }
+
+       # TODO: handle ImagePage and CategoryPage
+       # TODO: make sure we cover lucene search / wikisearch.
+       # TODO: make sure ReplaceTemplates still works
+       # FUTURE: nice&sane integration of GeSHi syntax highlighting
+       #   [11:59] <vvv> Hooks are ugly; make CodeHighlighter interface and a config to set the class which handles syntax highlighting
+       #   [12:00] <vvv> And default it to a DummyHighlighter
+
+       # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
+
+       # TODO: tie into API to provide contentModel for Revisions
+       # TODO: tie into API to provide serialized version and contentFormat for Revisions
+       # TODO: tie into API edit interface
+       # FUTURE: make EditForm plugin for EditPage
+}
+       # FUTURE: special type for redirects?!
+       # FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
+       # FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
+
+/**
+ * Content object implementation for representing flat text.
+ *
+ * TextContent instances are imutable
+ */
+abstract class TextContent extends Content {
+
+       public function __construct( $text, $model_name = null ) {
+               parent::__construct( $model_name );
+
+               $this->mText = $text;
+       }
+
+       public function copy() {
+               return $this; #NOTE: this is ok since TextContent are imutable.
+       }
+
+       public function getTextForSummary( $maxlength = 250 ) {
+               global $wgContLang;
+
+               $text = $this->getNativeData();
+
+               $truncatedtext = $wgContLang->truncate(
+                       preg_replace( "/[\n\r]/", ' ', $text ),
+                       max( 0, $maxlength ) );
+
+               return $truncatedtext;
+       }
+
+       /**
+        * returns the text's size in bytes.
+        *
+        * @return int the size
+        */
+       public function getSize( ) {
+               $text = $this->getNativeData( );
+               return strlen( $text );
+       }
+
+       /**
+        * Returns true if this content is not a redirect, and $wgArticleCountMethod is "any".
+        *
+        * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
+        *                        to avoid redundant parsing to find out.
+        *
+        * @return bool true if the content is countable
+        */
+       public function isCountable( $hasLinks = null ) {
+               global $wgArticleCountMethod;
+
+               if ( $this->isRedirect( ) ) {
+                       return false;
+               }
+
+               if (  $wgArticleCountMethod === 'any' ) {
+                       return true;
+               }
+
+               return false;
+       }
+
+       /**
+        * Returns the text represented by this Content object, as a string.
+        *
+        * @return String the raw text
+        */
+       public function getNativeData( ) {
+               $text = $this->mText;
+               return $text;
+       }
+
+       /**
+        * Returns the text represented by this Content object, as a string.
+        *
+        * @return String the raw text
+        */
+       public function getTextForSearchIndex( ) {
+               return $this->getNativeData();
+       }
+
+       /**
+        * Returns the text represented by this Content object, as a string.
+        *
+        * @return String the raw text
+        */
+       public function getWikitextForTransclusion( ) {
+               return $this->getNativeData();
+       }
+
+       /**
+        * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
+        *
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               # generic implementation, relying on $this->getHtml()
+
+               if ( $generateHtml ) $html = $this->getHtml( $options );
+               else $html = '';
+
+               $po = new ParserOutput( $html );
+
+               return $po;
+       }
+
+       protected abstract function getHtml( );
+
+}
+
+class WikitextContent extends TextContent {
+
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_WIKITEXT);
+
+               $this->mDefaultParserOptions = null; #TODO: use per-class static member?!
+       }
+
+       protected function getHtml( ) {
+               throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
+       }
+
+       public function getDefaultParserOptions() {
+               global $wgUser, $wgContLang;
+
+               if ( !$this->mDefaultParserOptions ) { #TODO: use per-class static member?!
+                       $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+               }
+
+               return $this->mDefaultParserOptions;
+       }
+
+       /**
+        * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
+        *
+        * @since WikiData1
+        *
+        * @param IContextSource|null $context
+        * @param null $revId
+        * @param null|ParserOptions $options
+        * @param bool $generateHtml
+        *
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               global $wgParser;
+
+               if ( !$options ) {
+                       $options = $this->getDefaultParserOptions();
+               }
+
+               $po = $wgParser->parse( $this->mText, $context->getTitle(), $options, true, true, $revId );
+
+               return $po;
+       }
+
+       /**
+        * Returns the section with the given id.
+        *
+        * @param String $sectionId the section's id
+        * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
+        */
+       public function getSection( $section ) {
+               global $wgParser;
+
+               $text = $this->getNativeData();
+               $sect = $wgParser->getSection( $text, $section, false );
+
+               return  new WikitextContent( $sect );
+       }
+
+       /**
+        * Replaces a section in the wikitext
+        *
+        * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
+        * @param $with Content: new content of the section
+        * @param $sectionTitle String: new section's subject, only if $section is 'new'
+        * @return Content Complete article content, or null if error
+        */
+       public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+               wfProfileIn( __METHOD__ );
+
+               $myModelName = $this->getModelName();
+               $sectionModelName = $with->getModelName();
+
+               if ( $sectionModelName != $myModelName  ) {
+                       throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
+               }
+
+               $oldtext = $this->getNativeData();
+               $text = $with->getNativeData();
+
+               if ( $section === '' ) {
+                       return $with; #XXX: copy first?
+               } if ( $section == 'new' ) {
+                       # Inserting a new section
+                       $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
+                       if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
+                               $text = strlen( trim( $oldtext ) ) > 0
+                                       ? "{$oldtext}\n\n{$subject}{$text}"
+                                       : "{$subject}{$text}";
+                       }
+               } else {
+                       # Replacing an existing section; roll out the big guns
+                       global $wgParser;
+
+                       $text = $wgParser->replaceSection( $oldtext, $section, $text );
+               }
+
+               $newContent = new WikitextContent( $text );
+
+               wfProfileOut( __METHOD__ );
+               return $newContent;
+       }
+
+       /**
+        * Returns a new WikitextContent object with the given section heading prepended.
+        *
+        * @param $header String
+        * @return Content
+        */
+       public function addSectionHeader( $header ) {
+               $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" . $this->getNativeData();
+
+               return new WikitextContent( $text );
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param User $user
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
+               global $wgParser;
+
+               if ( $popts == null ) $popts = $this->getDefaultParserOptions();
+
+               $text = $this->getNativeData();
+               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+               return new WikitextContent( $pst );
+       }
+
+       /**
+        * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts = null ) {
+               global $wgParser;
+
+               if ( $popts == null ) $popts = $this->getDefaultParserOptions();
+
+               $text = $this->getNativeData();
+               $plt = $wgParser->getPreloadText( $text, $title, $popts );
+
+               return new WikitextContent( $plt );
+       }
+
+       public function getRedirectChain() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectArray( $text );
+       }
+
+       public function getRedirectTarget() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirect( $text );
+       }
+
+       public function getUltimateRedirectTarget() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectRecurse( $text );
+       }
+
+       /**
+        * Returns true if this content is not a redirect, and this content's text is countable according to
+        * the criteria defiend by $wgArticleCountMethod.
+        *
+        * @param Bool $hasLinks if it is known whether this content contains links, provide this information here,
+        *                        to avoid redundant parsing to find out.
+        * @param IContextSource $context context for parsing if necessary
+        *
+        * @return bool true if the content is countable
+        */
+       public function isCountable( $hasLinks = null, IContextSource $context = null ) {
+               global $wgArticleCountMethod, $wgRequest;
+
+               if ( $this->isRedirect( ) ) {
+                       return false;
+               }
+
+               $text = $this->getNativeData();
+
+               switch ( $wgArticleCountMethod ) {
+                       case 'any':
+                               return true;
+                       case 'comma':
+                               return strpos( $text,  ',' ) !== false;
+                       case 'link':
+                               if ( $hasLinks === null ) { # not known, find out
+                                       if ( !$context ) { # make dummy context
+                                               //XXX: caller of this method often knows the title, but not a context...
+                                               $context = new RequestContext( $wgRequest );
+                                       }
+
+                                       $po = $this->getParserOutput( $context, null, null, false );
+                                       $links = $po->getLinks();
+                                       $hasLinks = !empty( $links );
+                               }
+
+                               return $hasLinks;
+               }
+       }
+
+       public function getTextForSummary( $maxlength = 250 ) {
+               $truncatedtext = parent::getTextForSummary( $maxlength );
+
+               #clean up unfinished links
+               #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
+               $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
+
+               return $truncatedtext;
+       }
+
+}
+
+class MessageContent extends TextContent {
+       public function __construct( $msg_key, $params = null, $options = null ) {
+               parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
+
+               $this->mMessageKey = $msg_key;
+
+               $this->mParameters = $params;
+
+               if ( is_null( $options ) ) {
+                       $options = array();
+               }
+               elseif ( is_string( $options ) ) {
+                       $options = array( $options );
+               }
+
+               $this->mOptions = $options;
+
+               $this->mHtmlOptions = null;
+       }
+
+       /**
+        * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
+        */
+       protected function getHtml(  ) {
+               $opt = array_merge( $this->mOptions, array('parse') );
+
+               return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
+       }
+
+
+       /**
+        * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
+        */
+       public function getNativeData( ) {
+               $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
+
+               return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
+       }
+
+}
+
+
+class JavaScriptContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
+       }
+
+       protected function getHtml( ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+               $html .= htmlspecialchars( $this->getNativeData() );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+
+}
+
+class CssContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_CSS);
+       }
+
+       protected function getHtml( ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+               $html .= htmlspecialchars( $this->getNativeData() );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+}
diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php
new file mode 100644 (file)
index 0000000..d434a21
--- /dev/null
@@ -0,0 +1,765 @@
+<?php
+
+class MWContentSerializationException extends MWException {
+
+}
+
+
+/**
+ * A content handler knows how do deal with a specific type of content on a wiki page.
+ * Content is stored in the database in a serialized form (using a serialization format aka mime type)
+ * and is be unserialized into it's native PHP represenation (the content model), which is wrappe in
+ * an instance of the appropriate subclass of Content.
+ *
+ * ContentHandler instances are stateless singletons that serve, among other things, as a factory for
+ * Content objects. Generally, there is one subclass of ContentHandler and one subclass of Content
+ * for every type of content model.
+ *
+ * Some content types have a flat model, that is, their native represenation is the
+ * same as their serialized form. Examples would be JavaScript and CSS code. As of now,
+ * this also applies to wikitext (mediawiki's default content type), but wikitext
+ * content may be represented by a DOM or AST structure in the future.
+ */
+abstract class ContentHandler {
+
+       /**
+        * Conveniance function for getting flat text from a Content object. This should only
+        * be used in the context of backwards compatibility with code that is not yet able
+        * to handle Content objects!
+        *
+        * If $content is null, this method returns the empty string.
+        *
+        * If $content is an instance of TextContent, this method returns the flat text as returned by $content->getNativeData().
+        *
+        * If $content is not a TextContent object, the bahaviour of this method depends on the global $wgContentHandlerTextFallback:
+        * * If $wgContentHandlerTextFallback is 'fail' and $content is not a TextContent object, an MWException is thrown.
+        * * If $wgContentHandlerTextFallback is 'serialize' and $content is not a TextContent object, $content->serialize()
+        * is called to get a string form of the content.
+        * * If $wgContentHandlerTextFallback is 'ignore' and $content is not a TextContent object, this method returns null.
+        * * otherwise, the behaviour is undefined.
+        *
+        * @static
+        * @param Content|null $content
+        * @return null|string the textual form of $content, if available
+        * @throws MWException if $content is not an instance of TextContent and $wgContentHandlerTextFallback was set to 'fail'.
+        */
+       public static function getContentText( Content $content = null ) {
+               global $wgContentHandlerTextFallback;
+
+               if ( is_null( $content ) ) {
+                       return '';
+               }
+
+               if ( $content instanceof TextContent ) {
+                       return $content->getNativeData();
+               }
+
+               if ( $wgContentHandlerTextFallback == 'fail' ) {
+                       throw new MWException( "Attempt to get text from Content with model " . $content->getModelName() );
+               }
+
+               if ( $wgContentHandlerTextFallback == 'serialize' ) {
+                       return $content->serialize();
+               }
+
+               return null;
+       }
+
+       /**
+        * Conveniance function for creating a Content object from a given textual representation.
+        *
+        * $text will be deserialized into a Content object of the model specified by $modelName (or,
+        * if that is not given, $title->getContentModelName()) using the given format.
+        *
+        * @static
+        * @param string $text the textual represenation, will be unserialized to create the Content object
+        * @param Title $title the title of the page this text belongs to, required as a context for deserialization
+        * @param null|String $modelName the model to deserialize to. If not provided, $title->getContentModelName() is used.
+        * @param null|String $format the format to use for deserialization. If not given, the model's default format is used.
+        *
+        * @return Content a Content object representing $text
+        * @throw MWException if $model or $format is not supported or if $text can not be unserialized using $format.
+        */
+       public static function makeContent( $text, Title $title, $modelName = null, $format = null ) {
+
+               if ( is_null( $modelName ) ) {
+                       $modelName = $title->getContentModelName();
+               }
+
+               $handler = ContentHandler::getForModelName( $modelName );
+               return $handler->unserializeContent( $text, $format );
+       }
+
+       /**
+        * Returns the name of the default content model to be used for the page with the given title.
+        *
+        * Note: There should rarely be need to call this method directly.
+        * To determine the actual content model for a given page, use Title::getContentModelName().
+        *
+        * Which model is to be used per default for the page is determined based on several factors:
+        * * The global setting $wgNamespaceContentModels specifies a content model per namespace.
+        * * The hook DefaultModelFor may be used to override the page's default model.
+        * * Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript model if they end in .js or .css, respectively.
+        * * Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+        * * The hook TitleIsCssOrJsPage may be used to force a page to use the CSS or JavaScript model if they end in .js or .css, respectively.
+        * * The hook TitleIsWikitextPage may be used to force a page to use the wikitext model.
+        *
+        * If none of the above applies, the wikitext model is used.
+        *
+        * Note: this is used by, and may thus not use, Title::getContentModelName()
+        *
+        * @static
+        * @param Title $title
+        * @return null|string default model name for the page given by $title
+        */
+       public static function getDefaultModelFor( Title $title ) {
+               global $wgNamespaceContentModels;
+
+               // NOTE: this method must not rely on $title->getContentModelName() directly or indirectly,
+               //       because it is used to initialized the mContentModelName memebr.
+
+               $ns = $title->getNamespace();
+
+               $ext = false;
+               $m = null;
+               $model = null;
+
+               if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+                       $model = $wgNamespaceContentModels[ $ns ];
+               }
+
+               // hook can determin default model
+               if ( !wfRunHooks( 'DefaultModelFor', array( $title, &$model ) ) ) { #FIXME: document new hook!
+                       if ( !is_null( $model ) ) {
+                               return $model;
+                       }
+               }
+
+               // Could this page contain custom CSS or JavaScript, based on the title?
+               $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
+               if ( $isCssOrJsPage ) {
+                       $ext = $m[1];
+               }
+
+               // hook can force js/css
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
+
+               // Is this a .css subpage of a user page?
+               $isJsCssSubpage = NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
+               if ( $isJsCssSubpage ) {
+                       $ext = $m[1];
+               }
+
+               // is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+               $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
+               $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
+
+               // hook can override $isWikitext
+               wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
+
+               if ( !$isWikitext ) {
+                       switch ( $ext ) {
+                               case 'js':
+                                       return CONTENT_MODEL_JAVASCRIPT;
+                               case 'css':
+                                       return CONTENT_MODEL_CSS;
+                               default:
+                                       return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
+                       }
+               }
+
+               // we established that is must be wikitext
+
+               return CONTENT_MODEL_WIKITEXT;
+       }
+
+       /**
+        * returns the appropriate ContentHandler singleton for the given title
+        *
+        * @static
+        * @param Title $title
+        * @return ContentHandler
+        */
+       public static function getForTitle( Title $title ) {
+               $modelName = $title->getContentModelName();
+               return ContentHandler::getForModelName( $modelName );
+       }
+
+       /**
+        * returns the appropriate ContentHandler singleton for the given Content object
+        *
+        * @static
+        * @param Content $content
+        * @return ContentHandler
+        */
+       public static function getForContent( Content $content ) {
+               $modelName = $content->getModelName();
+               return ContentHandler::getForModelName( $modelName );
+       }
+
+       /**
+        * returns the ContentHandler singleton for the given model name. Use the CONTENT_MODEL_XXX constants to
+        * identify the desired content model.
+        *
+        * ContentHandler singletons are take from the global $wgContentHandlers array. Keys in that array are
+        * model names, the values are either ContentHandler singleton objects, or strings specifying the appropriate
+        * subclass of ContentHandler.
+        *
+        * If a class name in encountered when looking up the singleton for a given model name, the class is
+        * instantiated and the class name is replaced by te resulting singleton in $wgContentHandlers.
+        *
+        * If no ContentHandler is defined for the desired $modelName, the ContentHandler may be provided by the
+        * a ContentHandlerForModelName hook. if no Contenthandler can be determined, an MWException is raised.
+        *
+        * @static
+        * @param $modelName String the name of the content model for which to get a handler. Use CONTENT_MODEL_XXX constants.
+        * @return ContentHandler the ContentHandler singleton for handling the model given by $modelName
+        * @throws MWException if no handler is known for $modelName.
+        */
+       public static function getForModelName( $modelName ) {
+               global $wgContentHandlers;
+
+               if ( empty( $wgContentHandlers[$modelName] ) ) {
+                       $handler = null;
+
+                       // FIXME: document new hook
+                       wfRunHooks( 'ContentHandlerForModelName', array( $modelName, &$handler ) );
+
+                       if ( $handler ) { // NOTE: may be a string or an object, either is fine!
+                               $wgContentHandlers[$modelName] = $handler;
+                       } else {
+                               throw new MWException( "No handler for model $modelName registered in \$wgContentHandlers" );
+                       }
+               }
+
+               if ( is_string( $wgContentHandlers[$modelName] ) ) {
+                       $class = $wgContentHandlers[$modelName];
+                       $wgContentHandlers[$modelName] = new $class( $modelName );
+               }
+
+               return $wgContentHandlers[$modelName];
+       }
+
+       // ----------------------------------------------------------------------------------------------------------
+
+       /**
+        * Constructor, initializing the ContentHandler instance with it's model name and a list of supported formats.
+        * Values for the parameters are typically provided as literals by subclasses' constructors.
+        *
+        * @param String $modelName (use CONTENT_MODEL_XXX constants).
+        * @param array $formats list for supported serialization formats (typically as MIME types)
+        */
+       public function __construct( $modelName, $formats ) {
+               $this->mModelName = $modelName;
+               $this->mSupportedFormats = $formats;
+       }
+
+
+       /**
+        * Serializes Content object of the type supported by this ContentHandler.
+        *
+        * @abstract
+        * @param Content $content the Content object to serialize
+        * @param null $format the desired serialization format
+        * @return String serialized form of the content
+        */
+       public abstract function serializeContent( Content $content, $format = null );
+
+       /**
+        * Unserializes a Content object of the type supported by this ContentHandler.
+        *
+        * @abstract
+        * @param $blob String serialized form of the content
+        * @param null $format the format used for serialization
+        * @return Content the Content object created by deserializing $blob
+        */
+       public abstract function unserializeContent( $blob, $format = null );
+
+       /**
+        * Creates an empty Content object of the type supported by this ContentHandler.
+        *
+        * @return Content
+        */
+       public abstract function makeEmptyContent();
+
+       /**
+        * Returns the model name that identifies the content model this ContentHandler can handle.
+        * Use with the CONTENT_MODEL_XXX constants.
+        *
+        * @return String the model name
+        */
+       public function getModelName() {
+               return $this->mModelName;
+       }
+
+       /**
+        * Throws an MWException if $modelName is not the content model handeled by this ContentHandler.
+        *
+        * @param String $modelName the model name to check
+        */
+       protected function checkModelName( $modelName ) {
+               if ( $modelName !== $this->mModelName ) {
+                       throw new MWException( "Bad content model: expected " . $this->mModelName . " but got found " . $modelName );
+               }
+       }
+
+       /**
+        * Returns a list of serialization formats supported by the serializeContent() and unserializeContent() methods of
+        * this ContentHandler.
+        *
+        * @return array of serialization formats as MIME type like strings
+        */
+       public function getSupportedFormats() {
+               return $this->mSupportedFormats;
+       }
+
+       /**
+        * The format used for serialization/deserialization per default by this ContentHandler.
+        *
+        * This default implementation will return the first element of the array of formats
+        * that was passed to the constructor.
+        *
+        * @return String the name of the default serialiozation format as a MIME type
+        */
+       public function getDefaultFormat() {
+               return $this->mSupportedFormats[0];
+       }
+
+       /**
+        * Returns true if $format is a serialization format supported by this ContentHandler,
+        * and false otherwise.
+        *
+        * Note that if $format is null, this method always returns true, because null
+        * means "use the default format".
+        *
+        * @param String $format the serialization format to check
+        * @return bool
+        */
+       public function isSupportedFormat( $format ) {
+
+               if ( !$format ) {
+                       return true; // this means "use the default"
+               }
+
+               return in_array( $format, $this->mSupportedFormats );
+       }
+
+       /**
+        * Throws an MWException if isSupportedFormat( $format ) is not true. Convenient
+        * for checking whether a format provided as a parameter is actually supported.
+        *
+        * @param String $format the serialization format to check
+        */
+       protected function checkFormat( $format ) {
+               if ( !$this->isSupportedFormat( $format ) ) {
+                       throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
+               }
+       }
+
+       /**
+        * Returns overrides for action handlers.
+        * Classes listed here will be used instead of the default one when
+        * (and only when) $wgActions[$action] === true. This allows subclasses
+        * to override the default action handlers.
+        *
+        * @return Array
+        */
+       public function getActionOverrides() {
+               return array();
+       }
+
+       /**
+        * Return an Article object suitable for viewing the given object
+        *
+        * NOTE: does *not* do special handling for Image and Category pages!
+        *       Use Article::newFromTitle() for that!
+        *
+        * @param Title $title
+        * @return Article
+        * @todo Article is being refactored into an action class, keep track of that
+        * @todo Article really defines the view of the content... rename this method to createViewPage ?
+        */
+       public function createArticle( Title $title ) {
+               $this->checkModelName( $title->getContentModelName() );
+
+               $article = new Article($title);
+               return $article;
+       }
+
+       /**
+        * Return an EditPage object suitable for editing the given object
+        *
+        * @param Article $article
+        * @return EditPage
+        */
+       public function createEditPage( Article $article ) {
+               $this->checkModelName( $article->getContentModelName() );
+
+               $editPage = new EditPage( $article );
+               return $editPage;
+       }
+
+       /**
+        * Return an ExternalEdit object suitable for editing the given object
+        *
+        * @param IContextSource $context
+        * @return ExternalEdit
+        * @todo does anyone or anythign actually use the external edit facility? Can we just deprecate and ignore it?
+        */
+       public function createExternalEdit( IContextSource $context ) {
+               $this->checkModelName( $context->getTitle()->getContentModelName() );
+
+               $externalEdit = new ExternalEdit( $context );
+               return $externalEdit;
+       }
+
+       /**
+        * Factory
+        * @param $context IContextSource context to use, anything else will be ignored
+        * @param $old Integer old ID we want to show and diff with.
+        * @param $new String either 'prev' or 'next'.
+        * @param $rcid Integer ??? FIXME (default 0)
+        * @param $refreshCache boolean If set, refreshes the diff cache
+        * @param $unhide boolean If set, allow viewing deleted revs
+        *
+        * @return DifferenceEngine
+        */
+       public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, #FIMXE: use everywhere!
+                                                                                $refreshCache = false, $unhide = false ) {
+
+               $this->checkModelName( $context->getTitle()->getContentModelName() );
+
+               $diffEngineClass = $this->getDiffEngineClass();
+
+               return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
+       }
+
+       /**
+        * Returns the name of the diff engine to use.
+        *
+        * @since 0.1
+        *
+        * @return string
+        */
+       protected function getDiffEngineClass() {
+               return 'DifferenceEngine';
+       }
+
+       /**
+        * attempts to merge differences between three versions.
+        * Returns a new Content object for a clean merge and false for failure or a conflict.
+        *
+        * This default implementation always returns false.
+        *
+        * @param $oldContent String
+        * @param $myContent String
+        * @param $yourContent String
+        * @return Content|Bool
+        */
+       public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+               return false;
+       }
+
+       /**
+        * Return an applicable autosummary if one exists for the given edit.
+        *
+        * @param $oldContent Content|null: the previous text of the page.
+        * @param $newContent Content|null: The submitted text of the page.
+        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
+        *
+        * @return string An appropriate autosummary, or an empty string.
+        */
+       public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
+               global $wgContLang;
+
+               // Decide what kind of autosummary is needed.
+
+               // Redirect autosummaries
+
+               /**
+                * @var $ot Title
+                * @var $rt Title
+                */
+
+               $ot = !is_null( $oldContent ) ? $oldContent->getRedirectTarget() : null;
+               $rt = !is_null( $newContent ) ? $newContent->getRedirectTarget() : null;
+
+               if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
+
+                       $truncatedtext = $newContent->getTextForSummary(
+                               250
+                                       - strlen( wfMsgForContent( 'autoredircomment' ) )
+                                       - strlen( $rt->getFullText() ) );
+
+                       return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
+               }
+
+               // New page autosummaries
+               if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
+                       // If they're making a new article, give its text, truncated, in the summary.
+
+                       $truncatedtext = $newContent->getTextForSummary(
+                               200 - strlen( wfMsgForContent( 'autosumm-new' ) ) );
+
+                       return wfMsgForContent( 'autosumm-new', $truncatedtext );
+               }
+
+               // Blanking autosummaries
+               if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+                       return wfMsgForContent( 'autosumm-blank' );
+               } elseif ( !empty( $oldContent ) && $oldContent->getSize() > 10 * $newContent->getSize() && $newContent->getSize() < 500 ) {
+                       // Removing more than 90% of the article
+
+                       $truncatedtext = $newContent->getTextForSummary(
+                               200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) );
+
+                       return wfMsgForContent( 'autosumm-replace', $truncatedtext );
+               }
+
+               // If we reach this point, there's no applicable autosummary for our case, so our
+               // autosummary is empty.
+
+               return '';
+       }
+
+       /**
+        * Auto-generates a deletion reason
+        *
+        * @param $title Title: the page's title
+        * @param &$hasHistory Boolean: whether the page has a history
+        * @return mixed String containing deletion reason or empty string, or boolean false
+        *    if no revision occurred
+        *
+        * @XXX &$hasHistory is extremely ugly, it's here because WikiPage::getAutoDeleteReason() and Article::getReason() have it / want it.
+        */
+       public function getAutoDeleteReason( Title $title, &$hasHistory ) {
+               $dbw = wfGetDB( DB_MASTER );
+
+               // Get the last revision
+               $rev = Revision::newFromTitle( $title );
+
+               if ( is_null( $rev ) ) {
+                       return false;
+               }
+
+               // Get the article's contents
+               $content = $rev->getContent();
+               $blank = false;
+
+               // If the page is blank, use the text from the previous revision,
+               // which can only be blank if there's a move/import/protect dummy revision involved
+               if ( $content->getSize() == 0 ) {
+                       $prev = $rev->getPrevious();
+
+                       if ( $prev )    {
+                               $content = $rev->getContent();
+                               $blank = true;
+                       }
+               }
+
+               // Find out if there was only one contributor
+               // Only scan the last 20 revisions
+               $res = $dbw->select( 'revision', 'rev_user_text',
+                       array( 'rev_page' => $title->getArticleID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
+                       __METHOD__,
+                       array( 'LIMIT' => 20 )
+               );
+
+               if ( $res === false ) {
+                       // This page has no revisions, which is very weird
+                       return false;
+               }
+
+               $hasHistory = ( $res->numRows() > 1 );
+               $row = $dbw->fetchObject( $res );
+
+               if ( $row ) { // $row is false if the only contributor is hidden
+                       $onlyAuthor = $row->rev_user_text;
+                       // Try to find a second contributor
+                       foreach ( $res as $row ) {
+                               if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
+                                       $onlyAuthor = false;
+                                       break;
+                               }
+                       }
+               } else {
+                       $onlyAuthor = false;
+               }
+
+               // Generate the summary with a '$1' placeholder
+               if ( $blank ) {
+                       // The current revision is blank and the one before is also
+                       // blank. It's just not our lucky day
+                       $reason = wfMsgForContent( 'exbeforeblank', '$1' );
+               } else {
+                       if ( $onlyAuthor ) {
+                               $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
+                       } else {
+                               $reason = wfMsgForContent( 'excontent', '$1' );
+                       }
+               }
+
+               if ( $reason == '-' ) {
+                       // Allow these UI messages to be blanked out cleanly
+                       return '';
+               }
+
+               // Max content length = max comment length - length of the comment (excl. $1)
+               $text = $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) );
+
+               // Now replace the '$1' placeholder
+               $reason = str_replace( '$1', $text, $reason );
+
+               return $reason;
+       }
+
+       #@TODO: getSecondaryUpdatesForDeletion( Content ) returns an array of SecondaryDataUpdate objects
+       #... or do that in the Content class?
+
+       /**
+        * Get the Content object that needs to be saved in order to undo all revisions
+        * between $undo and $undoafter. Revisions must belong to the same page,
+        * must exist and must not be deleted
+        * @param $current Revision the current text
+        * @param $undo Revision the revision to undo
+        * @param $undoafter Revision Must be an earlier revision than $undo
+        * @return mixed string on success, false on failure
+        */
+       public function getUndoContent( Revision $current, Revision $undo, Revision $undoafter ) {
+               $cur_content = $current->getContent();
+
+               if ( empty( $cur_content ) ) {
+                       return false; // no page
+               }
+
+               $undo_content = $undo->getContent();
+               $undoafter_content = $undoafter->getContent();
+
+               if ( $cur_content->equals( $undo_content ) ) {
+                       // No use doing a merge if it's just a straight revert.
+                       return $undoafter_content;
+               }
+
+               $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
+
+               return $undone_content;
+       }
+
+       /**
+        * Returns true for content models that support caching using the ParserCache mechanism.
+        * See WikiPage::isParserCacheUser().
+        *
+        * @return bool
+        */
+       public function isParserCacheSupported() {
+               return true;
+       }
+}
+
+
+abstract class TextContentHandler extends ContentHandler {
+
+       public function __construct( $modelName, $formats ) {
+               parent::__construct( $modelName, $formats );
+       }
+
+       public function serializeContent( Content $content, $format = null ) {
+               $this->checkFormat( $format );
+               return $content->getNativeData();
+       }
+
+       /**
+        * attempts to merge differences between three versions.
+        * Returns a new Content object for a clean merge and false for failure or a conflict.
+        *
+        * All three Content objects passed as parameters must have the same content model.
+        *
+        * This text-based implementation uses wfMerge().
+        *
+        * @param $oldContent String
+        * @param $myContent String
+        * @param $yourContent String
+        * @return Content|Bool
+        */
+       public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+               $this->checkModelName( $oldContent->getModelName() );
+               $this->checkModelName( $myContent->getModelName() );
+               $this->checkModelName( $yourContent->getModelName() );
+
+               $format = $this->getDefaultFormat();
+
+               $old = $this->serializeContent( $oldContent, $format );
+               $mine = $this->serializeContent( $myContent, $format );
+               $yours = $this->serializeContent( $yourContent, $format );
+
+               $ok = wfMerge( $old, $mine, $yours, $result );
+
+               if ( !$ok ) {
+                       return false;
+               }
+
+               if ( !$result ) {
+                       return $this->makeEmptyContent();
+               }
+
+               $mergedContent = $this->unserializeContent( $result, $format );
+               return $mergedContent;
+       }
+
+
+}
+class WikitextContentHandler extends TextContentHandler {
+
+       public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+               parent::__construct( $modelName, array( 'application/x-wiki' ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new WikitextContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new WikitextContent( '' );
+       }
+
+
+}
+
+#XXX: make ScriptContentHandler base class with plugin interface for syntax highlighting?
+
+class JavaScriptContentHandler extends TextContentHandler {
+
+       public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+               parent::__construct( $modelName, array( 'text/javascript' ) ); #XXX: or use $wgJsMimeType? this is for internal storage, not HTTP...
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new JavaScriptContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new JavaScriptContent( '' );
+       }
+}
+
+class CssContentHandler extends TextContentHandler {
+
+       public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+               parent::__construct( $modelName, array( 'text/css' ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new CssContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new CssContent( '' );
+       }
+
+}
index 7c4db0d..ab811f4 100644 (file)
@@ -640,6 +640,17 @@ $wgMediaHandlers = array(
        'image/x-djvu' => 'DjVuHandler', // compat
 );
 
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model name type to a class name
+ */
+$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
+    CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
+);
+
 /**
  * Resizing can be done using PHP's internal image libraries or using
  * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -5806,6 +5817,22 @@ $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).
+ */
+$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
+ * * 'serializeContent': serializeContent to default format
+ */
+$wgContentHandlerTextFallback = 'ignore';
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index e40c9b2..610d0a5 100644 (file)
@@ -253,3 +253,12 @@ define( 'PROTO_RELATIVE', '//' );
 define( 'PROTO_CURRENT', null );
 define( 'PROTO_CANONICAL', 1 );
 define( 'PROTO_INTERNAL', 2 );
+
+/**
+ * Content model names, used by Content and ContentHandler
+ */
+define('CONTENT_MODEL_WIKITEXT', 'wikitext');
+define('CONTENT_MODEL_JAVASCRIPT', 'javascript');
+define('CONTENT_MODEL_CSS', 'css');
+define('CONTENT_MODEL_TEXT', 'text');
+
index 92bca9e..e8a91e3 100644 (file)
@@ -144,6 +144,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.
         */
@@ -203,6 +208,7 @@ class EditPage {
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
+       var $content_model = null, $content_format = null;
 
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
@@ -214,7 +220,7 @@ class EditPage {
        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;
@@ -228,6 +234,11 @@ class EditPage {
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
+
+               $this->content_model = $this->mTitle->getContentModelName();
+
+               $handler = ContentHandler::getForModelName( $this->content_model );
+               $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
 
        /**
@@ -439,10 +450,10 @@ class EditPage {
                        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 );
@@ -456,13 +467,14 @@ class EditPage {
                # 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 = $content->serialize( $this->content_format );
                        $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() ) ) );
@@ -668,7 +680,7 @@ class EditPage {
                } else {
                        # Not a posted form? Start with nothing.
                        wfDebug( __METHOD__ . ": Not a posted form.\n" );
-                       $this->textbox1     = '';
+                       $this->textbox1     = ''; #FIXME: track content object
                        $this->summary      = '';
                        $this->sectiontitle = '';
                        $this->edittime     = '';
@@ -700,10 +712,17 @@ class EditPage {
                        }
                }
 
+               $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->content_model = $request->getText( 'model', $content_handler->getModelName() ); #may be overridden by revision
+               $this->content_format = $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',
@@ -736,7 +755,10 @@ class EditPage {
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
-               $this->textbox1 = $this->getContent( false );
+
+               $content = $this->getContentObject( false ); #TODO: track content object?!
+               $this->textbox1 = $content->serialize( $this->content_format );
+
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -765,33 +787,52 @@ class EditPage {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
+        * @deprecated since 1.20
         */
-       function getContent( $def_text = '' ) {
-               global $wgOut, $wgRequest, $wgParser;
+       function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
+               if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+                       $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
+               } else {
+                       $def_content = false;
+               }
+
+               $content = $this->getContentObject( $def_content );
+
+               return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
+       }
+
+       private function getContentObject( $def_content = null ) { #FIXME: use this!
+               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 = ContentHandler::makeContent( $msg, $this->mTitle );
                        }
-                       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' );
@@ -807,15 +848,16 @@ class EditPage {
 
                                        # 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 {
@@ -847,14 +889,14 @@ class EditPage {
                                                wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
                                }
 
-                               if ( $text === false ) {
-                                       $text = $this->getOriginalContent();
+                               if ( $content === false ) {
+                                       $content = $this->getOriginalContent();
                                }
                        }
                }
 
                wfProfileOut( __METHOD__ );
-               return $text;
+               return $content;
        }
 
        /**
@@ -873,78 +915,120 @@ class EditPage {
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
-                       return $this->getCurrentText();
+                       return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
-                       return '';
-               }
-               return $this->mArticle->getContent();
-       }
-
-       /**
-        * 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.
-        *
-        * @since 1.19
-        * @return string
-        */
-       private function getCurrentText() {
-               $text = $this->mArticle->getRawText();
-               if ( $text === false ) {
-                       return '';
-               } else {
-                       return $text;
-               }
-       }
-
-       /**
-        * Use this method before edit() to preload some text into the edit box
-        *
-        * @param $text string
-        */
-       public function setPreloadedText( $text ) {
-               $this->mPreloadText = $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 String
-        */
-       protected function getPreloadedText( $preload ) {
-               global $wgUser, $wgParser;
-
-               if ( !empty( $this->mPreloadText ) ) {
-                       return $this->mPreloadText;
-               }
-
-               if ( $preload === '' ) {
-                       return '';
-               }
-
-               $title = Title::newFromText( $preload );
-               # Check for existence to avoid getting MediaWiki:Noarticletext
-               if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                       return '';
-               }
-
-               $page = WikiPage::factory( $title );
-               if ( $page->isRedirect() ) {
-                       $title = $page->getRedirectTarget();
-                       # Same as before
-                       if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                               return '';
-                       }
-                       $page = WikiPage::factory( $title );
-               }
-
-               $parserOptions = ParserOptions::newFromUser( $wgUser );
-               return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
-       }
+                       if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModelName();
+                       $handler = ContentHandler::getForModelName( $this->content_model );
+
+                       return $handler->makeEmptyContent();
+               }
+        $content = $revision->getContent();
+               return $content;
+       }
+
+    /**
+     * 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.20
+     * @return string
+     */
+    private function getCurrentContent() {
+        $rev = $this->mArticle->getRevision();
+        $content = $rev ? $rev->getContent( Revision::RAW ) : null;
+
+        if ( $content  === false || $content === null ) {
+            if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModelName();
+            $handler = ContentHandler::getForModelName( $this->content_model );
+
+            return $handler->makeEmptyContent();
+        } else {
+            #FIXME: nasty side-effect!
+            $this->content_model = $rev->getContentModelName();
+            $this->content_format = $rev->getContentFormat();
+
+            return $content;
+        }
+    }
+
+
+    /**
+     * Use this method before edit() to preload some text into the edit box
+     *
+     * @param $text string
+     * @deprecated since 1.20
+     */
+    public function setPreloadedText( $text ) { #FIXME: deprecated, use setPreloadedContent()
+        wfDeprecated( __METHOD__, "1.20" );
+
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+        $this->setPreloadedContent( $content );
+    }
+
+    /**
+     * Use this method before edit() to preload some content into the edit box
+     *
+     * @param $content Content
+     */
+    public function setPreloadedContent( Content $content ) { #FIXME: use this!
+        $this->mPreloadedContent = $content;
+    }
+
+    /**
+     * 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 String
+     * @deprecated since 1.20
+     */
+    protected function getPreloadedText( $preload ) { #FIXME: B/C only, replace usage!
+        wfDeprecated( __METHOD__, "1.20" );
+
+        $content = $this->getPreloadedContent( $preload );
+        $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
+
+        return $text;
+    }
+
+    protected function getPreloadedContent( $preload ) { #FIXME: use this!
+        global $wgUser;
+
+        if ( !empty( $this->mPreloadContent ) ) {
+            return $this->mPreloadContent;
+        }
+
+        $handler = ContentHandler::getForTitle( $this->getTitle() );
+
+        if ( $preload === '' ) {
+            return $handler->makeEmptyContent();
+        }
+
+        $title = Title::newFromText( $preload );
+        # Check for existence to avoid getting MediaWiki:Noarticletext
+        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' ) ) {
+                return $handler->makeEmptyContent();
+            }
+            $page = WikiPage::factory( $title );
+        }
+
+        $parserOptions = ParserOptions::newFromUser( $wgUser );
+        $content = $page->getContent( Revision::RAW );
+
+        return $content->preloadTransform( $title, $parserOptions );
+    }
 
        /**
         * Make sure the form isn't faking a user's credentials.
@@ -993,6 +1077,11 @@ class EditPage {
                        case self::AS_FILTERING:
                                return false;
 
+                       case self::AS_PARSE_ERROR:
+                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
+                               #FIXME: cause editform to be shown again, not just an error!
+                               return false;
+
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
@@ -1081,7 +1170,7 @@ class EditPage {
 
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
-                       Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+                       Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
@@ -1197,257 +1286,273 @@ class EditPage {
                $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
                $new = ( $aid == 0 );
 
-               if ( $new ) {
-                       // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create' ) ) {
-                               $status->fatal( 'nocreatetext' );
-                               $status->value = self::AS_NO_CREATE_PERMISSION;
-                               wfDebug( __METHOD__ . ": no create permission\n" );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
-
-                       # Don't save a new article if it's blank.
-                       if ( $this->textbox1 == '' ) {
-                               $status->setResult( false, self::AS_BLANK_ARTICLE );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
-
-                       // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
-                               # Error messages etc. could be handled within the hook...
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       } elseif ( $this->hookError != '' ) {
-                               # ...or the hook could be expecting us to produce an error
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR_EXPECTED;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
-
-                       $text = $this->textbox1;
-                       $result['sectionanchor'] = '';
-                       if ( $this->section == 'new' ) {
-                               if ( $this->sectiontitle !== '' ) {
-                                       // Insert the section title above the content.
-                                       $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
-
-                                       // Jump to the new section
-                                       $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
-                                       }
-                               } elseif ( $this->summary !== '' ) {
-                                       // Insert the section title above the content.
-                                       $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
-
-                                       // 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 = wfMsgForContent( 'newsectionsummary', $cleanSummary );
-                               }
-                       }
-
-                       $status->value = self::AS_SUCCESS_NEW_ARTICLE;
-
-               } else {
-
-                       # 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 = true;
-                               if ( $this->section == 'new' ) {
-                                       if ( $this->mArticle->getUserText() == $wgUser->getName() &&
-                                               $this->mArticle->getComment() == $this->summary ) {
-                                               // Probably a duplicate submission of a new comment.
-                                               // This can happen when squid resends a request after
-                                               // a timeout but the first one actually went through.
-                                               wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
-                                       } else {
-                                               // New comment; suppress conflict.
-                                               $this->isConflict = false;
-                                               wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
-                                       }
-                               } elseif ( $this->section == '' && $this->userWasLastToEdit( $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;
-                               }
-                       }
-
-                       // If sectiontitle is set, use it, otherwise use the summary as the section title (for
-                       // backwards compatibility with old forms/bots).
-                       if ( $this->sectiontitle !== '' ) {
-                               $sectionTitle = $this->sectiontitle;
-                       } else {
-                               $sectionTitle = $this->summary;
-                       }
-
-                       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 );
-                       } else {
-                               wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
-                               $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
-                       }
-                       if ( is_null( $text ) ) {
-                               wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
-                               $this->isConflict = true;
-                               $text = $this->textbox1; // do not try to merge here!
-                       } elseif ( $this->isConflict ) {
-                               # Attempt merge
-                               if ( $this->mergeChangesInto( $text ) ) {
-                                       // Successful merge! Maybe we should tell the user the good news?
-                                       $this->isConflict = false;
-                                       wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
-                               } else {
-                                       $this->section = '';
-                                       $this->textbox1 = $text;
-                                       wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
-                               }
-                       }
-
-                       if ( $this->isConflict ) {
-                               $status->setResult( false, self::AS_CONFLICT_DETECTED );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
-
-                       // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
-                               # Error messages etc. could be handled within the hook...
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       } elseif ( $this->hookError != '' ) {
-                               # ...or the hook could be expecting us to produce an error
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR_EXPECTED;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
-
-                       # 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
-                       {
-                               if ( md5( $this->summary ) == $this->autoSumm ) {
-                                       $this->missingSummary = true;
-                                       $status->fatal( 'missingsummary' );
-                                       $status->value = self::AS_SUMMARY_NEEDED;
-                                       wfProfileOut( __METHOD__ );
-                                       return $status;
-                               }
-                       }
-
-                       # And a similar thing for new sections
-                       if ( $this->section == 'new' && !$this->allowBlankSummary ) {
-                               if ( trim( $this->summary ) == '' ) {
-                                       $this->missingSummary = true;
-                                       $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
-                                       $status->value = self::AS_SUMMARY_NEEDED;
-                                       wfProfileOut( __METHOD__ );
-                                       return $status;
-                               }
-                       }
-
-                       # All's well
-                       wfProfileIn( __METHOD__ . '-sectionanchor' );
-                       $sectionanchor = '';
-                       if ( $this->section == 'new' ) {
-                               if ( $this->textbox1 == '' ) {
-                                       $this->missingComment = true;
-                                       $status->fatal( 'missingcommenttext' );
-                                       $status->value = self::AS_TEXTBOX_EMPTY;
-                                       wfProfileOut( __METHOD__ . '-sectionanchor' );
-                                       wfProfileOut( __METHOD__ );
-                                       return $status;
-                               }
-                               if ( $this->sectiontitle !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
-                                       }
-                               } 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 = wfMsgForContent( 'newsectionsummary', $cleanSummary );
-                               }
-                       } elseif ( $this->section != '' ) {
-                               # Try to get a section anchor from the section source, redirect to edited section if header found
-                               # XXX: might be better to integrate this into Article::replaceSection
-                               # for duplicate heading checking and maybe parsing
-                               $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
-                               # we can't deal with anchors, includes, html etc in the header for now,
-                               # headline would need to be parsed to improve this
-                               if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
-                               }
-                       }
-                       $result['sectionanchor'] = $sectionanchor;
-                       wfProfileOut( __METHOD__ . '-sectionanchor' );
-
-                       // Save errors may fall down to the edit form, but we've now
-                       // 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->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 );
-               if ( $this->kblength > $wgMaxArticleSize ) {
-                       $this->tooBig = true;
-                       $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
+               try {
+            if ( $new ) {
+                // Late check for create permission, just in case *PARANOIA*
+                if ( !$this->mTitle->userCan( 'create' ) ) {
+                    $status->fatal( 'nocreatetext' );
+                    $status->value = self::AS_NO_CREATE_PERMISSION;
+                    wfDebug( __METHOD__ . ": no create permission\n" );
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                }
+
+                # Don't save a new article if it's blank.
+                if ( $this->textbox1 == '' ) {
+                    $status->setResult( false, self::AS_BLANK_ARTICLE );
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                }
+
+                // Run post-section-merge edit filter
+                if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
+                    # Error messages etc. could be handled within the hook...
+                    $status->fatal( 'hookaborted' );
+                    $status->value = self::AS_HOOK_ERROR;
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                } elseif ( $this->hookError != '' ) {
+                    # ...or the hook could be expecting us to produce an error
+                    $status->fatal( 'hookaborted' );
+                    $status->value = self::AS_HOOK_ERROR_EXPECTED;
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                }
+
+                $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+
+                $result['sectionanchor'] = '';
+                if ( $this->section == 'new' ) {
+                    if ( $this->sectiontitle !== '' ) {
+                        // Insert the section title above the content.
+                        $content = $content->addSectionHeader( $this->sectiontitle );
+
+                        // Jump to the new section
+                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+
+                        // If no edit summary was specified, create one automatically from the section
+                        // title and have it link to the new section. Otherwise, respect the summary as
+                        // passed.
+                        if ( $this->summary === '' ) {
+                            $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                            $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+                        }
+                    } elseif ( $this->summary !== '' ) {
+                        // Insert the section title above the content.
+                        $content = $content->addSectionHeader( $this->sectiontitle );
+
+                        // 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 = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+                    }
+                }
+
+                $status->value = self::AS_SUCCESS_NEW_ARTICLE;
+
+            } 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 = true;
+                    if ( $this->section == 'new' ) {
+                        if ( $this->mArticle->getUserText() == $wgUser->getName() &&
+                            $this->mArticle->getComment() == $this->summary ) {
+                            // Probably a duplicate submission of a new comment.
+                            // This can happen when squid resends a request after
+                            // a timeout but the first one actually went through.
+                            wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
+                        } else {
+                            // New comment; suppress conflict.
+                            $this->isConflict = false;
+                            wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
+                        }
+                    } elseif ( $this->section == '' && $this->userWasLastToEdit( $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;
+                    }
+                }
+
+                // If sectiontitle is set, use it, otherwise use the summary as the section title (for
+                // backwards compatibility with old forms/bots).
+                if ( $this->sectiontitle !== '' ) {
+                    $sectionTitle = $this->sectiontitle;
+                } else {
+                    $sectionTitle = $this->summary;
+                }
+
+                $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+                $content = null;
+
+                if ( $this->isConflict ) {
+                    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" );
+                    $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
+                }
+
+                if ( is_null( $content ) ) {
+                    wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
+                    $this->isConflict = true;
+                    $content = $textbox_content; // do not try to merge here!
+                } elseif ( $this->isConflict ) {
+                    # Attempt merge
+                    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; #redundant, nothing to do here?
+                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
+                    }
+                }
+
+                if ( $this->isConflict ) {
+                    $status->setResult( false, self::AS_CONFLICT_DETECTED );
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                }
+
+                // Run post-section-merge edit filter
+                if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
+                        || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) { #FIXME: document new hook
+                    # Error messages etc. could be handled within the hook...
+                    $status->fatal( 'hookaborted' );
+                    $status->value = self::AS_HOOK_ERROR;
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                } elseif ( $this->hookError != '' ) {
+                    # ...or the hook could be expecting us to produce an error
+                    $status->fatal( 'hookaborted' );
+                    $status->value = self::AS_HOOK_ERROR_EXPECTED;
+                    wfProfileOut( __METHOD__ );
+                    return $status;
+                }
+
+                $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+
+                # Handle the user preference to force summaries here, but not for null edits
+                if ( $this->section != 'new' && !$this->allowBlankSummary
+                    && !$content->equals( $this->getOriginalContent() )
+                    && !$content->isRedirect() ) # check if it's not a redirect
+                {
+                    if ( md5( $this->summary ) == $this->autoSumm ) {
+                        $this->missingSummary = true;
+                        $status->fatal( 'missingsummary' );
+                        $status->value = self::AS_SUMMARY_NEEDED;
+                        wfProfileOut( __METHOD__ );
+                        return $status;
+                    }
+                }
+
+                # And a similar thing for new sections
+                if ( $this->section == 'new' && !$this->allowBlankSummary ) {
+                    if ( trim( $this->summary ) == '' ) {
+                        $this->missingSummary = true;
+                        $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
+                        $status->value = self::AS_SUMMARY_NEEDED;
+                        wfProfileOut( __METHOD__ );
+                        return $status;
+                    }
+                }
+
+                # All's well
+                wfProfileIn( __METHOD__ . '-sectionanchor' );
+                $sectionanchor = '';
+                if ( $this->section == 'new' ) {
+                    if ( $this->textbox1 == '' ) {
+                        $this->missingComment = true;
+                        $status->fatal( 'missingcommenttext' );
+                        $status->value = self::AS_TEXTBOX_EMPTY;
+                        wfProfileOut( __METHOD__ . '-sectionanchor' );
+                        wfProfileOut( __METHOD__ );
+                        return $status;
+                    }
+                    if ( $this->sectiontitle !== '' ) {
+                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+                        // If no edit summary was specified, create one automatically from the section
+                        // title and have it link to the new section. Otherwise, respect the summary as
+                        // passed.
+                        if ( $this->summary === '' ) {
+                            $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                            $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+                        }
+                    } 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 = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+                    }
+                } elseif ( $this->section != '' ) {
+                    # Try to get a section anchor from the section source, redirect to edited section if header found
+                    # XXX: might be better to integrate this into Article::replaceSection
+                    # for duplicate heading checking and maybe parsing
+                    $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
+                    # we can't deal with anchors, includes, html etc in the header for now,
+                    # headline would need to be parsed to improve this
+                    if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
+                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
+                    }
+                }
+                $result['sectionanchor'] = $sectionanchor;
+                wfProfileOut( __METHOD__ . '-sectionanchor' );
+
+                // Save errors may fall down to the edit form, but we've now
+                // 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 = $content->serialize( $this->content_format );
+                $this->section = '';
+
+                $status->value = self::AS_SUCCESS_UPDATE;
+            }
+
+            // Check for length errors again now that the section is merged in
+                $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
+            if ( $this->kblength > $wgMaxArticleSize ) {
+                $this->tooBig = true;
+                $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
+                wfProfileOut( __METHOD__ );
+                return $status;
+            }
+
+            $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
+                ( $new ? EDIT_NEW : EDIT_UPDATE ) |
+                ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
+                ( $bot ? EDIT_FORCE_BOT : 0 );
+
+                $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
+
+            if ( $doEditStatus->isOK() ) {
+                    $result['redirect'] = $content->isRedirect();
+                $this->commitWatch();
+                wfProfileOut( __METHOD__ );
+                return $status;
+            } else {
+                $this->isConflict = true;
+                $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
+                wfProfileOut( __METHOD__ );
+                return $doEditStatus;
+            }
+               } catch (MWContentSerializationException $ex) {
+                       $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                       $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
                        return $status;
                }
-
-               $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
-                       ( $new ? EDIT_NEW : EDIT_UPDATE ) |
-                       ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
-                       ( $bot ? EDIT_FORCE_BOT : 0 );
-
-               $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
-
-               if ( $doEditStatus->isOK() ) {
-                       $result['redirect'] = Title::newFromRedirect( $text ) !== null;
-                       $this->commitWatch();
-                       wfProfileOut( __METHOD__ );
-                       return $status;
-               } else {
-                       $this->isConflict = true;
-                       $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
-                       wfProfileOut( __METHOD__ );
-                       return $doEditStatus;
-               }
        }
 
        /**
@@ -1496,45 +1601,73 @@ class EditPage {
                return true;
        }
 
-       /**
-        * @private
-        * @todo document
-        *
-        * @parma $editText string
-        *
-        * @return bool
-        */
-       function mergeChangesInto( &$editText ) {
-               wfProfileIn( __METHOD__ );
-
-               $db = wfGetDB( DB_MASTER );
-
-               // This is the revision the editor started from
-               $baseRevision = $this->getBaseRevision();
-               if ( is_null( $baseRevision ) ) {
-                       wfProfileOut( __METHOD__ );
-                       return false;
-               }
-               $baseText = $baseRevision->getText();
-
-               // The current state, we want to merge updates into it
-               $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
-               if ( is_null( $currentRevision ) ) {
-                       wfProfileOut( __METHOD__ );
-                       return false;
-               }
-               $currentText = $currentRevision->getText();
-
-               $result = '';
-               if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
-                       $editText = $result;
-                       wfProfileOut( __METHOD__ );
-                       return true;
-               } else {
-                       wfProfileOut( __METHOD__ );
-                       return false;
-               }
-       }
+    /**
+     * @private
+     * @todo document
+     *
+     * @parma $editText string
+     *
+     * @return bool
+     * @deprecated since 1.20
+     */
+    function mergeChangesInto( &$editText ){
+        wfDebug( __METHOD__, "1.20" );
+
+        $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
+
+        $ok = $this->mergeChangesIntoContent( $editContent );
+
+        if ( $ok ) {
+            $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * @private
+     * @todo document
+     *
+     * @parma $editText string
+     *
+     * @return bool
+     * @since since 1.20
+     */
+    private function mergeChangesIntoContent( &$editContent ){
+        wfProfileIn( __METHOD__ );
+
+        $db = wfGetDB( DB_MASTER );
+
+        // This is the revision the editor started from
+        $baseRevision = $this->getBaseRevision();
+        if ( is_null( $baseRevision ) ) {
+            wfProfileOut( __METHOD__ );
+            return false;
+        }
+        $baseContent = $baseRevision->getContent();
+
+        // The current state, we want to merge updates into it
+        $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
+        if ( is_null( $currentRevision ) ) {
+            wfProfileOut( __METHOD__ );
+            return false;
+        }
+        $currentContent = $currentRevision->getContent();
+
+        $handler = ContentHandler::getForModelName( $baseContent->getModelName() );
+
+        $result = $handler->merge3( $baseContent, $editContent, $currentContent );
+
+        if ( $result ) {
+            $editContent = $result;
+            wfProfileOut( __METHOD__ );
+            return true;
+        } else {
+            wfProfileOut( __METHOD__ );
+            return false;
+        }
+    }
 
        /**
         * @return Revision
@@ -1774,6 +1907,7 @@ class EditPage {
                        }
                }
 
+               #FIXME: add EditForm plugin interface and use it here! #FIXME: 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' ) ) );
@@ -1834,6 +1968,9 @@ class EditPage {
 
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
 
+               $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
+               $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
+
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
@@ -1851,7 +1988,9 @@ class EditPage {
                        // 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 = $content->serialize( $this->content_format );
 
                        $this->showTextbox1();
                } else {
@@ -2259,10 +2398,10 @@ HTML
                $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.
@@ -2346,24 +2485,43 @@ HTML
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
+                               $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
+                       } else {
+                               $oldContent = null;
                        }
                } else {
-                       $oldtext = $this->mArticle->getRawText();
+                       $oldContent = $this->getOriginalContent();
+               }
+
+        $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
+                                                                                                               $this->content_model, $this->content_format ); #XXX: handle parse errors ?
+
+        $newContent = $this->mArticle->replaceSectionContent(
+                                                    $this->section, $textboxContent,
+                                                    $this->summary, $this->edittime );
+
+        # hanlde legacy text-based hook
+        $newtext_orig = $newContent->serialize( $this->content_format );
+        $newtext = $newtext_orig; #clone
+        wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+
+               if ( $newtext != $newtext_orig ) {
+                               #if the hook changed the text, create a new Content object accordingly.
+                               $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModelName() ); #XXX: handle parse errors ?
                }
-               $newtext = $this->mArticle->replaceSection(
-                       $this->section, $this->textbox1, $this->summary, $this->edittime );
 
-               wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+               wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); #FIXME: document new hook
 
                $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 = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
 
-                       $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 {
@@ -2453,8 +2611,12 @@ HTML
                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 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
+                       $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
+
+                       $handler = ContentHandler::getForModelName( $this->content_model );
+                       $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
 
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
@@ -2574,85 +2736,102 @@ HTML
                        return $parsedNote;
                }
 
-               if ( $this->mTriedSave && !$this->mTokenOk ) {
-                       if ( $this->mTokenOkExceptSuffix ) {
-                               $note = wfMsg( 'token_suffix_mismatch' );
-                       } else {
-                               $note = wfMsg( 'session_fail_preview' );
-                       }
-               } elseif ( $this->incompleteForm ) {
-                       $note = wfMsg( 'edit_form_incomplete' );
-               } else {
-                       $note = wfMsg( 'previewnote' ) .
-                               ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
-               }
-
-               $parserOptions = ParserOptions::newFromUser( $wgUser );
-               $parserOptions->setEditSection( false );
-               $parserOptions->setTidy( true );
-               $parserOptions->setIsPreview( true );
-               $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
-
-               # 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;
-                       }
+               $note = '';
+
+        try {
+                   $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 
-                       # 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" . wfMsg( "{$level}csspreview" ) . "\n</div>";
-                                       $class .= " mw-css";
-                               } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
-                                       $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
-                                       $class .= " mw-js";
+                       if ( $this->mTriedSave && !$this->mTokenOk ) {
+                               if ( $this->mTokenOkExceptSuffix ) {
+                                       $note = wfMsg( 'token_suffix_mismatch' );
                                } else {
-                                       throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
+                                       $note = wfMsg( 'session_fail_preview' );
                                }
-                               $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
-                               $previewHTML = $parserOutput->getText();
+                       } elseif ( $this->incompleteForm ) {
+                               $note = wfMsg( 'edit_form_incomplete' );
                        } else {
-                               $previewHTML = '';
-                       }
-
-                       $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
-               } else {
-                       $toparse = $this->textbox1;
-
-                       # If we're adding a comment, we need to show the
-                       # summary as the headline
-                       if ( $this->section == "new" && $this->summary != "" ) {
-                               $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
-                       }
-
-                       wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
-
-                       $parserOptions->enableLimitReport();
-
-                       $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
-                       $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
-
-                       $rt = Title::newFromRedirectArray( $this->textbox1 );
-                       if ( $rt ) {
-                               $previewHTML = $this->mArticle->viewRedirect( $rt, false );
-                       } else {
-                               $previewHTML = $parserOutput->getText();
-                       }
-
-                       $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputNoText( $parserOutput );
-
-                       if ( count( $parserOutput->getWarnings() ) ) {
-                               $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
-                       }
-               }
+                               $note = wfMsg( 'previewnote' ) .
+                                       ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
+                       }
+
+            $parserOptions = ParserOptions::newFromUser( $wgUser );
+            $parserOptions->setEditSection( false );
+            $parserOptions->setTidy( true );
+            $parserOptions->setIsPreview( true );
+            $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
+
+                       if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
+                # don't parse non-wikitext pages, show message about preview
+                if( $this->mTitle->isCssJsSubpage() ) {
+                    $level = 'user';
+                } elseif( $this->mTitle->isCssOrJsPage() ) {
+                    $level = 'site';
+                } else {
+                    $level = false;
+                }
+
+                if ( $content->getModelName() == CONTENT_MODEL_CSS ) {
+                    $format = 'css';
+                } elseif ( $content->getModelName() == CONTENT_MODEL_JAVASCRIPT ) {
+                    $format = 'js';
+                } else {
+                    $format = false;
+                }
+
+                # Used messages to make sure grep find them:
+                # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+                if( $level && $format ) {
+                    $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
+                } else {
+                    $note = wfMsg( 'previewnote' );
+                }
+            } else {
+                $note = wfMsg( 'previewnote' );
+            }
+
+            $rt = $content->getRedirectChain();
+
+            if ( $rt ) {
+                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
+            } else {
+
+                # 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 );
+                }
+
+                $toparse_orig = $content->serialize( $this->content_format );
+                $toparse = $toparse_orig;
+                wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
+
+                if ( $toparse !== $toparse_orig ) {
+                    #hook changed the text, create new Content object
+                    $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
+                }
+
+                wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) ); # FIXME: document new hook
+
+                $parserOptions->enableLimitReport();
+
+                #XXX: 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 );
+
+                               // TODO: might be a saner way to get a meaningfull context here?
+                $parserOutput = $content->getParserOutput( $this->getArticle()->getContext(), 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) {
+            $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+            $previewHTML = '';
+        }
 
                if ( $this->isConflict ) {
                        $conflict = '<h2 id="mw-previewconflict">' . htmlspecialchars( wfMsg( 'previewconflict' ) ) . "</h2>\n";
index 73b07c3..f98da1d 100644 (file)
@@ -594,6 +594,14 @@ class XmlDumpWriter {
                        $out .= "      " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
                }
 
+               if ( $row->rev_content_model ) {
+                       $out .= "      " . Xml::element('model', null, strval( $row->rev_content_model ) ) . "\n";
+               }
+
+               if ( $row->rev_content_format ) {
+                       $out .= "      " . Xml::element('format', null, strval( $row->rev_content_format ) ) . "\n";
+               }
+
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
index d280db5..8a01ab8 100644 (file)
@@ -117,7 +117,8 @@ class FeedUtils {
                        $diffText = '';
                        // Don't bother generating the diff if we won't be able to show it
                        if ( $wgFeedDiffCutoff > 0 ) {
-                               $de = new DifferenceEngine( $title, $oldid, $newid );
+                $contentHandler = ContentHandler::getForTitle( $title );
+                $de = $contentHandler->createDifferenceEngine( $title, $oldid, $newid );
                                $diffText = $de->getDiff(
                                        wfMsg( 'previousrevision' ), // hack
                                        wfMsg( 'revisionasof',
index 7453baa..5755bdc 100644 (file)
@@ -133,7 +133,9 @@ class ImagePage extends Article {
                        $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
-                       parent::view();
+
+            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
+
                        $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
@@ -244,20 +246,20 @@ class ImagePage extends Article {
                return $r;
        }
 
-       /**
-        * Overloading Article's getContent method.
-        *
-        * Omit noarticletext if sharedupload; text will be fetched from the
-        * shared upload server if possible.
-        * @return string
-        */
-       public function getContent() {
-               $this->loadFile();
-               if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
-                       return '';
-               }
-               return parent::getContent();
-       }
+    /**
+     * Overloading Article's getContentObject method.
+     *
+     * Omit noarticletext if sharedupload; text will be fetched from the
+     * shared upload server if possible.
+     * @return string
+     */
+    public function getContentObject() {
+        $this->loadFile();
+        if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
+            return null;
+        }
+        return parent::getContentObject();
+    }
 
        protected function openShowImage() {
                global $wgOut, $wgUser, $wgImageLimits, $wgRequest,
index 9ebc34c..e8f32d7 100644 (file)
@@ -578,7 +578,7 @@ class WikiImporter {
                $this->debug( "Enter revision handler" );
                $revisionInfo = array();
 
-               $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'text' );
+               $normalFields = array( 'id', 'timestamp', 'comment', 'minor', 'model', 'format', 'text' );
 
                $skip = false;
 
@@ -623,6 +623,12 @@ class WikiImporter {
                if ( isset( $revisionInfo['text'] ) ) {
                        $revision->setText( $revisionInfo['text'] );
                }
+               if ( isset( $revisionInfo['model'] ) ) {
+                       $revision->setModel( $revisionInfo['model'] );
+               }
+               if ( isset( $revisionInfo['text'] ) ) {
+                       $revision->setFormat( $revisionInfo['format'] );
+               }
                $revision->setTitle( $pageInfo['_title'] );
 
                if ( isset( $revisionInfo['timestamp'] ) ) {
@@ -972,6 +978,8 @@ class WikiRevision {
        var $timestamp = "20010115000000";
        var $user = 0;
        var $user_text = "";
+       var $model = null;
+       var $format = null;
        var $text = "";
        var $comment = "";
        var $minor = false;
@@ -1028,6 +1036,20 @@ class WikiRevision {
                $this->user_text = $ip;
        }
 
+       /**
+        * @param $model
+        */
+       function setModel( $model ) {
+               $this->model = $model;
+       }
+
+       /**
+        * @param $format
+        */
+       function setFormat( $format ) {
+               $this->format = $format;
+       }
+
        /**
         * @param $text
         */
@@ -1156,6 +1178,28 @@ class WikiRevision {
                return $this->text;
        }
 
+       /**
+        * @return string
+        */
+       function getModel() {
+               if ( is_null( $this->model ) ) {
+                       $this->model = $this->getTitle()->getContentModelName();
+               }
+
+               return $this->model;
+       }
+
+       /**
+        * @return string
+        */
+       function getFormat() {
+               if ( is_null( $this->model ) ) {
+                       $this->format = ContentHandler::getForTitle( $this->getTitle() )->getDefaultFormat();
+               }
+
+               return $this->format;
+       }
+
        /**
         * @return string
         */
@@ -1295,6 +1339,8 @@ class WikiRevision {
                # Insert the row
                $revision = new Revision( array(
                        'page'       => $pageId,
+                       'content_model'  => $this->getModel(),
+                       'content_format' => $this->getFormat(),
                        'text'       => $this->getText(),
                        'comment'    => $this->getComment(),
                        'user'       => $userId,
index 716e7d8..1973d95 100644 (file)
  *
  * @todo document (e.g. one-sentence top-level class description).
  */
-class LinksUpdate {
+class LinksUpdate extends SecondaryDBDataUpdate {
 
        /**@{{
         * @private
         */
        var $mId,            //!< Page ID of the article linked from
-               $mTitle,         //!< Title object of the article linked from
-               $mParserOutput,  //!< Parser output
-               $mLinks,         //!< Map of title strings to IDs for the links in the document
+        $mTitle,         //!< Title object of the article linked from
+        $mParserOutput,  //!< Whether to queue jobs for recursive update
+        $mLinks,         //!< Map of title strings to IDs for the links in the document
                $mImages,        //!< DB keys of the images used, in the array key only
                $mTemplates,     //!< Map of title strings to IDs for the template references, including broken ones
                $mExternals,     //!< URLs of external links, array key only
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
-               $mDb,            //!< Database connection reference
-               $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
        /**@}}*/
 
@@ -47,23 +45,22 @@ class LinksUpdate {
         * @param $recursive Boolean: queue jobs for recursive updates?
         */
        function __construct( $title, $parserOutput, $recursive = true ) {
-               global $wgAntiLockFlags;
+        parent::__construct( );
 
-               if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
-                       $this->mOptions = array();
-               } else {
-                       $this->mOptions = array( 'FOR UPDATE' );
-               }
-               $this->mDb = wfGetDB( DB_MASTER );
+        if ( !is_object( $title ) ) {
+            throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
+                "Please see Article::editUpdates() for an invocation example.\n" );
+        }
 
-               if ( !is_object( $title ) ) {
-                       throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
+               if ( !is_object( $title ) ) {
+                       throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
                                "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
-               }
-               $this->mTitle = $title;
-               $this->mId = $title->getArticleID();
+               }
+        $this->mTitle = $title;
+        $this->mId = $title->getArticleID();
+
+        $this->mParserOutput = $parserOutput;
 
-               $this->mParserOutput = $parserOutput;
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
@@ -253,51 +250,6 @@ class LinksUpdate {
                wfProfileOut( __METHOD__ );
        }
 
-       /**
-        * Invalidate the cache of a list of pages from a single namespace
-        *
-        * @param $namespace Integer
-        * @param $dbkeys Array
-        */
-       function invalidatePages( $namespace, $dbkeys ) {
-               if ( !count( $dbkeys ) ) {
-                       return;
-               }
-
-               /**
-                * Determine which pages need to be updated
-                * This is necessary to prevent the job queue from smashing the DB with
-                * large numbers of concurrent invalidations of the same page
-                */
-               $now = $this->mDb->timestamp();
-               $ids = array();
-               $res = $this->mDb->select( 'page', array( 'page_id' ),
-                       array(
-                               'page_namespace' => $namespace,
-                               'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-               foreach ( $res as $row ) {
-                       $ids[] = $row->page_id;
-               }
-               if ( !count( $ids ) ) {
-                       return;
-               }
-
-               /**
-                * Do the update
-                * We still need the page_touched condition, in case the row has changed since
-                * the non-locking select above.
-                */
-               $this->mDb->update( 'page', array( 'page_touched' => $now ),
-                       array(
-                               'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-       }
-
        /**
         * @param $cats
         */
@@ -324,20 +276,20 @@ class LinksUpdate {
                $this->invalidatePages( NS_FILE, array_keys( $images ) );
        }
 
-       /**
-        * @param $table
-        * @param $insertions
-        * @param $fromField
-        */
-       private function dumbTableUpdate( $table, $insertions, $fromField ) {
-               $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
-               if ( count( $insertions ) ) {
-                       # The link array was constructed without FOR UPDATE, so there may
-                       # be collisions.  This may cause minor link table inconsistencies,
-                       # which is better than crippling the site with lock contention.
-                       $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
-               }
-       }
+    /**
+     * @param $table
+     * @param $insertions
+     * @param $fromField
+     */
+    private function dumbTableUpdate( $table, $insertions, $fromField ) {
+        $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
+        if ( count( $insertions ) ) {
+            # The link array was constructed without FOR UPDATE, so there may
+            # be collisions.  This may cause minor link table inconsistencies,
+            # which is better than crippling the site with lock contention.
+            $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
+        }
+    }
 
        /**
         * Update a table by doing a delete query then an insert query
@@ -803,22 +755,22 @@ class LinksUpdate {
                return $arr;
        }
 
-       /**
-        * Return the title object of the page being updated
-        * @return Title
-        */
-       public function getTitle() {
-               return $this->mTitle;
-       }
-
-       /**
-        * Returns parser output
-        * @since 1.19
-        * @return ParserOutput
-        */
-       public function getParserOutput() {
-               return $this->mParserOutput;
-       }
+    /**
+     * Return the title object of the page being updated
+     * @return Title
+     */
+    public function getTitle() {
+        return $this->mTitle;
+    }
+
+    /**
+     * Returns parser output
+     * @since 1.19
+     * @return ParserOutput
+     */
+    public function getParserOutput() {
+        return $this->mParserOutput;
+    }
 
        /**
         * Return the list of images used as generated by the parser
index 1147e6a..0aee4eb 100644 (file)
@@ -20,6 +20,10 @@ class Revision {
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
+    protected $mContentModelName;
+    protected $mContentFormat;
+    protected $mContent;
+    protected $mContentHandler;
 
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
@@ -125,6 +129,8 @@ class Revision {
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+            'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+            'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
@@ -336,7 +342,9 @@ class Revision {
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
-                       'rev_sha1'
+                       'rev_sha1',
+                       'rev_content_format',
+                       'rev_content_model'
                );
        }
 
@@ -416,6 +424,18 @@ class Revision {
                                $this->mTitle = null;
                        }
 
+            if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
+                $this->mContentModelName = null; # determine on demand if needed
+            } else {
+                $this->mContentModelName = strval( $row->rev_content_model );
+            }
+
+            if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
+                $this->mContentFormat = null; # determine on demand if needed
+            } else {
+                $this->mContentFormat = strval( $row->rev_content_format );
+            }
+
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
@@ -437,6 +457,19 @@ class Revision {
                        // Build a new revision to be saved...
                        global $wgUser; // ugh
 
+
+            # if we have a content object, use it to set the model and type
+            if ( !empty( $row['content'] ) ) {
+                if ( !empty( $row['text_id'] ) ) { #FIXME: when is that set? test with external store setup! check out insertOn()
+                    throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" );
+                }
+
+                $row['content_model'] = $row['content']->getModelName();
+                # note: mContentFormat is initializes later accordingly
+                # note: content is serialized later in this method!
+                # also set text to null?
+            }
+
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
@@ -449,25 +482,46 @@ class Revision {
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
 
+            $this->mContentModelName = isset( $row['content_model']  )  ? strval( $row['content_model'] )  : null;
+            $this->mContentFormat    = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
+
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
 
+                       # if we have a content object, override mText and mContentModelName
+                       if ( !empty( $row['content'] ) ) {
+                               $handler = $this->getContentHandler();
+                               $this->mContent = $row['content'];
+
+                               $this->mContentModelName = $this->mContent->getModelName();
+                               $this->mContentHandler = null;
+                               
+                               $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
+                       }
+
                        $this->mTitle     = null; # Load on demand if needed
                        $this->mCurrent   = false;
                        # If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
+                #XXX: my be inconsistent with the notion of "size" use for the present content model
                                $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
                        }
                        # Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
+
+            $this->getContentModelName(); # force lazy init
+            $this->getContentFormat();      # force lazy init
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                $this->mUnpatrolled = null;
+
+        // @TODO: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API
+        // @TODO: get rid of $mText
        }
 
        /**
@@ -727,17 +781,38 @@ class Revision {
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
+     * @deprectaed in 1.20, use getContent() instead
         */
-       public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
-                       return '';
-               } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
-                       return '';
-               } else {
-                       return $this->getRawText();
-               }
+       public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage! #FIXME: used a LOT!
+        wfDeprecated( __METHOD__, '1.20' );
+
+        $content = $this->getContent();
+        return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
        }
 
+    /**
+     * Fetch revision content if it's available to the specified audience.
+     * If the specified audience does not have the ability to view this
+     * revision, null will be returned.
+     *
+     * @param $audience Integer: one of:
+     *      Revision::FOR_PUBLIC       to be displayed to all users
+     *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+     *      Revision::RAW              get the text regardless of permissions
+     * @param $user User object to check for, only if FOR_THIS_USER is passed
+     *              to the $audience parameter
+     * @return Content
+     */
+    public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
+        if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+            return null;
+        } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+            return null;
+        } else {
+            return $this->getContentInternal();
+        }
+    }
+
        /**
         * Alias for getText(Revision::FOR_THIS_USER)
         *
@@ -754,14 +829,61 @@ class Revision {
         *
         * @return String
         */
-       public function getRawText() {
-               if( is_null( $this->mText ) ) {
-                       // Revision text is immutable. Load on demand:
-                       $this->mText = $this->loadText();
-               }
-               return $this->mText;
+       public function getRawText() { #FIXME: deprecated, replace usage!
+               return $this->getText( self::RAW );
        }
 
+    protected function getContentInternal() {
+        if( is_null( $this->mContent ) ) {
+            // Revision is immutable. Load on demand:
+
+            $handler = $this->getContentHandler();
+            $format = $this->getContentFormat();
+            $title = $this->getTitle();
+
+            if( is_null( $this->mText ) ) {
+                // Load text on demand:
+                $this->mText = $this->loadText();
+            }
+
+            $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
+        }
+
+        return $this->mContent;
+    }
+
+    public function getContentModelName() {
+        if ( !$this->mContentModelName ) {
+            $title = $this->getTitle();
+            $this->mContentModelName = ( $title ? $title->getContentModelName() : CONTENT_MODEL_WIKITEXT );
+        }
+
+        return $this->mContentModelName;
+    }
+
+    public function getContentFormat() {
+        if ( !$this->mContentFormat ) {
+            $handler = $this->getContentHandler();
+            $this->mContentFormat = $handler->getDefaultFormat();
+        }
+
+        return $this->mContentFormat;
+    }
+
+       /**
+        * @return ContentHandlert
+        */
+    public function getContentHandler() {
+        if ( !$this->mContentHandler ) {
+            $model = $this->getContentModelName();
+            $this->mContentHandler = ContentHandler::getForModelName( $model );
+
+            assert( $this->mContentHandler->isSupportedFormat( $this->getContentFormat() ) );
+        }
+
+        return $this->mContentHandler;
+    }
+
        /**
         * @return String
         */
@@ -983,26 +1105,29 @@ class Revision {
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
-               $dbw->insert( 'revision',
-                       array(
-                               'rev_id'         => $rev_id,
-                               'rev_page'       => $this->mPage,
-                               'rev_text_id'    => $this->mTextId,
-                               'rev_comment'    => $this->mComment,
-                               'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
-                               'rev_user'       => $this->mUser,
-                               'rev_user_text'  => $this->mUserText,
-                               'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
-                               'rev_deleted'    => $this->mDeleted,
-                               'rev_len'        => $this->mSize,
-                               'rev_parent_id'  => is_null( $this->mParentId )
-                                       ? $this->getPreviousRevisionId( $dbw )
-                                       : $this->mParentId,
-                               'rev_sha1'       => is_null( $this->mSha1 )
-                                       ? Revision::base36Sha1( $this->mText )
-                                       : $this->mSha1
-                       ), __METHOD__
-               );
+
+        $row = array(
+            'rev_id'         => $rev_id,
+            'rev_page'       => $this->mPage,
+            'rev_text_id'    => $this->mTextId,
+            'rev_comment'    => $this->mComment,
+            'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+            'rev_user'       => $this->mUser,
+            'rev_user_text'  => $this->mUserText,
+            'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
+            'rev_deleted'    => $this->mDeleted,
+            'rev_len'        => $this->mSize,
+            'rev_parent_id'  => is_null( $this->mParentId )
+                ? $this->getPreviousRevisionId( $dbw )
+                : $this->mParentId,
+            'rev_sha1'       => is_null( $this->mSha1 )
+                ? Revision::base36Sha1( $this->mText )
+                : $this->mSha1,
+            'rev_content_model'       => $this->getContentModelName(),
+            'rev_content_format'        => $this->getContentFormat(),
+        );
+
+               $dbw->insert( 'revision', $row, __METHOD__ );
 
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
 
@@ -1100,7 +1225,8 @@ class Revision {
 
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
-                       array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ),
+                       array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1',
+                    'rev_content_model', 'rev_content_format' ),
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
@@ -1115,7 +1241,9 @@ class Revision {
                                'text_id'    => $current->rev_text_id,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
-                               'sha1'       => $current->rev_sha1
+                               'sha1'       => $current->rev_sha1,
+                               'content_model'  => $current->rev_content_model,
+                               'content_format'   => $current->rev_content_format
                                ) );
                } else {
                        $revision = null;
diff --git a/includes/SecondaryDBDataUpdate.php b/includes/SecondaryDBDataUpdate.php
new file mode 100644 (file)
index 0000000..1adb9a3
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that put some secondary data extracted
+ * from article content into the database.
+ */
+abstract class SecondaryDBDataUpdate extends SecondaryDataUpdate {
+
+       /**@{{
+        * @private
+        */
+       var $mDb,            //!< Database connection reference
+               $mOptions;       //!< SELECT options to be used (array)
+       /**@}}*/
+
+       /**
+        * Constructor
+     **/
+    public function __construct( ) {
+               global $wgAntiLockFlags;
+
+        parent::__construct( );
+
+               if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
+                       $this->mOptions = array();
+               } else {
+                       $this->mOptions = array( 'FOR UPDATE' );
+               }
+               $this->mDb = wfGetDB( DB_MASTER );
+       }
+
+       /**
+        * Invalidate the cache of a list of pages from a single namespace
+        *
+        * @param $namespace Integer
+        * @param $dbkeys Array
+        */
+       public function invalidatePages( $namespace, $dbkeys ) {
+               if ( !count( $dbkeys ) ) {
+                       return;
+               }
+
+               /**
+                * Determine which pages need to be updated
+                * This is necessary to prevent the job queue from smashing the DB with
+                * large numbers of concurrent invalidations of the same page
+                */
+               $now = $this->mDb->timestamp();
+               $ids = array();
+               $res = $this->mDb->select( 'page', array( 'page_id' ),
+                       array(
+                               'page_namespace' => $namespace,
+                               'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
+                               'page_touched < ' . $this->mDb->addQuotes( $now )
+                       ), __METHOD__
+               );
+               foreach ( $res as $row ) {
+                       $ids[] = $row->page_id;
+               }
+               if ( !count( $ids ) ) {
+                       return;
+               }
+
+               /**
+                * Do the update
+                * We still need the page_touched condition, in case the row has changed since
+                * the non-locking select above.
+                */
+               $this->mDb->update( 'page', array( 'page_touched' => $now ),
+                       array(
+                               'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
+                               'page_touched < ' . $this->mDb->addQuotes( $now )
+                       ), __METHOD__
+               );
+       }
+
+}
diff --git a/includes/SecondaryDataUpdate.php b/includes/SecondaryDataUpdate.php
new file mode 100644 (file)
index 0000000..eeee42f
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that do something with some secondary
+ * data extracted from article.
+ */
+abstract class SecondaryDataUpdate {
+
+       /**
+        * Constructor
+        */
+    public function __construct( ) {
+        # noop
+       }
+
+       /**
+        * Perform update.
+        */
+       public abstract function doUpdate();
+
+    /**
+     * Conveniance method, calls doUpdate() on every element in the array.
+     *
+     * @static
+     * @param $updates array
+     */
+    public static function runUpdates( $updates ) {
+        if ( empty( $updates ) ) return; # nothing to do
+
+        foreach ( $updates as $update ) {
+            $update->doUpdate();
+        }
+    }
+
+}
index 1876d75..552b6da 100644 (file)
@@ -271,12 +271,17 @@ class Title {
                        if ( isset( $row->page_is_redirect ) )
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
-                               $this->mLatestID = (int)$row->page_latest;
+                               $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model
+            if ( isset( $row->page_content_model ) )
+                $this->mContentModelName = $row->page_content_model;
+            else
+                $this->mContentModelName = null; # initialized lazily in getContentModelName()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+            $this->mContentModelName = null; # initialized lazily in getContentModelName()
                }
        }
 
@@ -302,6 +307,7 @@ class Title {
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
+        $t->mContentModelName = null; # initialized lazily in getContentModelName()
                return $t;
        }
 
@@ -691,6 +697,29 @@ class Title {
                return $this->mNamespace;
        }
 
+    /**
+     * Get the page's content model name
+     *
+     * @return Integer: Namespace index
+     */
+    public function getContentModelName() {
+        if ( empty( $this->mContentModelName ) ) {
+            $this->mContentModelName = ContentHandler::getDefaultModelFor( $this );
+        }
+
+        return $this->mContentModelName;
+    }
+
+    /**
+     * Conveniance method for checking a title's content model name
+     *
+     * @param $name
+     * @return true if $this->getContentModelName() == $name
+     */
+    public function hasContentModel( $name ) {
+        return $this->getContentModelName() == $name;
+    }
+
        /**
         * Get the namespace text
         *
@@ -941,22 +970,31 @@ class Title {
         * @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;
        }
 
        /**
@@ -964,7 +1002,9 @@ class Title {
         * @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 ) ) );
        }
 
        /**
@@ -987,7 +1027,8 @@ class Title {
         * @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 ) );
        }
 
        /**
@@ -996,7 +1037,8 @@ class Title {
         * @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 ) );
        }
 
        /**
index 6cad466..40040e2 100644 (file)
@@ -139,9 +139,21 @@ class WikiPage extends Page {
         * @return Array
         */
        public function getActionOverrides() {
-               return array();
+        $content_handler = $this->getContentHandler();
+               return $content_handler->getActionOverrides();
        }
 
+    /**
+     * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
+     *
+     * Shorthand for ContentHandler::getForModelName( $this->getContentModelName() );
+     *
+     * @return ContentHandler
+     */
+    public function getContentHandler() {
+        return ContentHandler::getForModelName( $this->getContentModelName() );
+    }
+
        /**
         * Get the title object of the article
         * @return Title object of this page
@@ -185,6 +197,7 @@ class WikiPage extends Page {
                        'page_touched',
                        'page_latest',
                        'page_len',
+            'page_content_model',
                );
        }
 
@@ -323,17 +336,31 @@ class WikiPage extends Page {
         * @return bool
         */
        public function isRedirect( $text = false ) {
-               if ( $text === false ) {
-                       if ( !$this->mDataLoaded ) {
-                               $this->loadPageData();
-                       }
+        if ( $text === false ) $content = $this->getContent();
+        else $content = ContentHandler::makeContent( $text, $this->mTitle ); # TODO: allow model and format to be provided; or better, expect a Content object
 
-                       return (bool)$this->mIsRedirect;
-               } else {
-                       return Title::newFromRedirect( $text ) !== null;
-               }
+
+        if ( empty( $content ) ) return false;
+        else return $content->isRedirect();
        }
 
+    /**
+     * Returns the page's content model name. Will use the revisions actual content model if the page exists,
+     * and the page's default if the page doesn't exist yet.
+     *
+     * @return int
+     */
+    public function getContentModelName() {
+        if ( $this->exists() ) {
+            # look at the revision's actual content model
+            $rev = $this->getRevision();
+            return $rev->getContentModelName();
+        } else {
+            # use the default model for this page
+            return $this->mTitle->getContentModelName();
+        }
+    }
+
        /**
         * Loads page_touched and returns a value indicating if it should be used
         * @return boolean true if not a redirect
@@ -407,6 +434,23 @@ class WikiPage extends Page {
                return null;
        }
 
+    /**
+     * Get the content of the current revision. No side-effects...
+     *
+     * @param $audience Integer: one of:
+     *      Revision::FOR_PUBLIC       to be displayed to all users
+     *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+     *      Revision::RAW              get the text regardless of permissions
+     * @return Content|null The content of the current revision
+     */
+    public function getContent( $audience = Revision::FOR_PUBLIC ) {
+        $this->loadLastEdit();
+        if ( $this->mLastRevision ) {
+            return $this->mLastRevision->getContent( $audience );
+        }
+        return null;
+    }
+
        /**
         * Get the text of the current revision. No side-effects...
         *
@@ -414,9 +458,12 @@ class WikiPage extends Page {
         *      Revision::FOR_PUBLIC       to be displayed to all users
         *      Revision::FOR_THIS_USER    to be displayed to $wgUser
         *      Revision::RAW              get the text regardless of permissions
-        * @return String|bool The text of the current revision. False on failure
+        * @return String|false The text of the current revision
+     * @deprecated as of 1.20, getContent() should be used instead.
         */
-       public function getText( $audience = Revision::FOR_PUBLIC ) {
+       public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
+        wfDeprecated( __METHOD__, '1.20' );
+
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
                        return $this->mLastRevision->getText( $audience );
@@ -428,15 +475,26 @@ class WikiPage extends Page {
         * Get the text of the current revision. No side-effects...
         *
         * @return String|bool The text of the current revision. False on failure
+        * @deprecated as of 1.20, getContent() should be used instead.
         */
-       public function getRawText() {
-               $this->loadLastEdit();
-               if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getRawText();
-               }
-               return false;
+       public function getRawText() { #@todo: deprecated, replace usage!
+               wfDeprecated( __METHOD__, '1.20' );
+
+               return $this->getText( Revision::RAW );
        }
 
+    /**
+     * Get the content of the current revision. No side-effects...
+     *
+     * @return Contet|false The text of the current revision
+     */
+    protected function getNativeData() { #FIXME: examine all uses carefully! caller must be aware of content model!
+        $content = $this->getContent( Revision::RAW );
+        if ( !$content ) return null;
+
+        return $content->getNativeData();
+    }
+
        /**
         * @return string MW timestamp of last article revision
         */
@@ -558,32 +616,34 @@ class WikiPage extends Page {
                        return false;
                }
 
-               $text = $editInfo ? $editInfo->pst : false;
+        if ( $editInfo ) {
+            $content = $editInfo->pstContent;
+        } else {
+            $content = $this->getContent();
+        }
 
-               if ( $this->isRedirect( $text ) ) {
+               if ( !$content || $content->isRedirect( ) ) {
                        return false;
                }
 
-               switch ( $wgArticleCountMethod ) {
-               case 'any':
-                       return true;
-               case 'comma':
-                       if ( $text === false ) {
-                               $text = $this->getRawText();
-                       }
-                       return strpos( $text,  ',' ) !== false;
-               case 'link':
-                       if ( $editInfo ) {
-                               // ParserOutput::getLinks() is a 2D array of page links, so
-                               // to be really correct we would need to recurse in the array
-                               // but the main array should only have items in it if there are
-                               // links.
-                               return (bool)count( $editInfo->output->getLinks() );
-                       } else {
-                               return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
-                                       array( 'pl_from' => $this->getId() ), __METHOD__ );
-                       }
-               }
+        $hasLinks = null;
+
+        if ( $wgArticleCountMethod === 'link' ) {
+            # nasty special case to avoid re-parsing to detect links
+
+            if ( $editInfo ) {
+                // ParserOutput::getLinks() is a 2D array of page links, so
+                // to be really correct we would need to recurse in the array
+                // but the main array should only have items in it if there are
+                // links.
+                $hasLinks = (bool)count( $editInfo->output->getLinks() );
+            } else {
+                $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                    array( 'pl_from' => $this->getId() ), __METHOD__ );
+            }
+        }
+
+               return $content->isCountable( $hasLinks );
        }
 
        /**
@@ -629,7 +689,8 @@ class WikiPage extends Page {
         */
        public function insertRedirect() {
                // recurse through to only get the final target
-               $retval = Title::newFromRedirectRecurse( $this->getRawText() );
+        $content = $this->getContent();
+               $retval = $content ? $content->getUltimateRedirectTarget() : null;
                if ( !$retval ) {
                        return null;
                }
@@ -825,7 +886,7 @@ class WikiPage extends Page {
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
-                       && $this->mTitle->isWikitextPage();
+                       && $this->getContentHandler()->isParserCacheSupported();
        }
 
        /**
@@ -836,9 +897,11 @@ class WikiPage extends Page {
         * @param $parserOptions ParserOptions to use for the parse operation
         * @param $oldid Revision ID to get the text from, passing null or 0 will
         *               get the current revision (default value)
+        * @param $context IContextSource context for parsing
+        *
         * @return ParserOutput or false if the revision was not found
         */
-       public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
+       public function getParserOutput( ParserOptions $parserOptions, $oldid = null, IContextSource $context = null ) {
                wfProfileIn( __METHOD__ );
 
                $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
@@ -859,7 +922,7 @@ class WikiPage extends Page {
                        $oldid = $this->getLatest();
                }
 
-               $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
+               $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache, null, $context );
                $pool->execute();
 
                wfProfileOut( __METHOD__ );
@@ -914,7 +977,7 @@ class WikiPage extends Page {
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        if ( $this->mTitle->exists() ) {
-                               $text = $this->getRawText();
+                               $text = $this->getNativeData(); #FIXME: may not be a string. check Content model!
                        } else {
                                $text = false;
                        }
@@ -981,9 +1044,9 @@ class WikiPage extends Page {
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
                wfProfileIn( __METHOD__ );
 
-               $text = $revision->getText();
-               $len = strlen( $text );
-               $rt = Title::newFromRedirectRecurse( $text );
+        $content = $revision->getContent();
+               $len = $content->getSize();
+               $rt = $content->getUltimateRedirectTarget();
 
                $conditions = array( 'page_id' => $this->getId() );
 
@@ -1000,6 +1063,7 @@ class WikiPage extends Page {
                                'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
                                'page_is_redirect' => $rt !== null ? 1 : 0,
                                'page_len'         => $len,
+                               'page_content_model' => $revision->getContentModelName(),
                        ),
                        $conditions,
                        __METHOD__ );
@@ -1102,27 +1166,29 @@ class WikiPage extends Page {
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
+        * @deprecated since 1.20: use ContentHandler::getUndoContent() instead.
         */
-       public function getUndoText( Revision $undo, Revision $undoafter = null ) {
-               $cur_text = $this->getRawText();
-               if ( $cur_text === false ) {
-                       return false; // no page
-               }
-               $undo_text = $undo->getText();
-               $undoafter_text = $undoafter->getText();
+       public function getUndoText( Revision $undo, Revision $undoafter = null ) { #FIXME: replace usages.
+               wfDeprecated( __METHOD__, '1.20' );
 
-               if ( $cur_text == $undo_text ) {
-                       # No use doing a merge if it's just a straight revert.
-                       return $undoafter_text;
-               }
+               $this->loadLastEdit();
 
-               $undone_text = '';
+               if ( $this->mLastRevision ) {
+                       if ( is_null( $undoafter ) ) {
+                               $undoafter = $undo->getPrevious();
+                       }
 
-               if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
-                       return false;
+                       $handler = $this->getContentHandler();
+                       $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
+
+                       if ( !$undone ) {
+                               return false;
+                       } else {
+                               return ContentHandler::getContentText( $undone );
+                       }
                }
 
-               return $undone_text;
+               return false;
        }
 
        /**
@@ -1130,55 +1196,54 @@ class WikiPage extends Page {
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
-        * @return string Complete article text, or null if error
+        * @return Content new complete article content, or null if error
+     * @deprecated since 1.20, use replaceSectionContent() instead
         */
-       public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
-               wfProfileIn( __METHOD__ );
+       public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { #FIXME: use replaceSectionContent() instead!
+        wfDeprecated( __METHOD__, '1.20' );
 
-               if ( strval( $section ) == '' ) {
-                       // Whole-page edit; let the whole text through
-               } else {
-                       // Bug 30711: always use current version when adding a new section
-                       if ( is_null( $edittime ) || $section == 'new' ) {
-                               $oldtext = $this->getRawText();
-                               if ( $oldtext === false ) {
-                                       wfDebug( __METHOD__ . ": no page text\n" );
-                                       wfProfileOut( __METHOD__ );
-                                       return null;
-                               }
-                       } else {
-                               $dbw = wfGetDB( DB_MASTER );
-                               $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
+        $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); #XXX: could make section title, but that's not required.
 
-                               if ( !$rev ) {
-                                       wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
-                                               $this->getId() . "; section: $section; edittime: $edittime)\n" );
-                                       wfProfileOut( __METHOD__ );
-                                       return null;
-                               }
+        $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 
-                               $oldtext = $rev->getText();
-                       }
+               return ContentHandler::getContentText( $newContent ); #XXX: unclear what will happen for non-wikitext!
+       }
 
-                       if ( $section == 'new' ) {
-                               # Inserting a new section
-                               $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
-                               if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
-                                       $text = strlen( trim( $oldtext ) ) > 0
-                                               ? "{$oldtext}\n\n{$subject}{$text}"
-                                               : "{$subject}{$text}";
-                               }
-                       } else {
-                               # Replacing an existing section; roll out the big guns
-                               global $wgParser;
+    public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
+        wfProfileIn( __METHOD__ );
 
-                               $text = $wgParser->replaceSection( $oldtext, $section, $text );
-                       }
-               }
+        if ( strval( $section ) == '' ) {
+            // Whole-page edit; let the whole text through
+            $newContent = $sectionContent;
+        } else {
+            // Bug 30711: always use current version when adding a new section
+            if ( is_null( $edittime ) || $section == 'new' ) {
+                $oldContent = $this->getContent();
+                if ( ! $oldContent ) {
+                    wfDebug( __METHOD__ . ": no page text\n" );
+                    wfProfileOut( __METHOD__ );
+                    return null;
+                }
+            } else {
+                $dbw = wfGetDB( DB_MASTER );
+                $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
 
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
+                if ( !$rev ) {
+                    wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
+                        $this->getId() . "; section: $section; edittime: $edittime)\n" );
+                    wfProfileOut( __METHOD__ );
+                    return null;
+                }
+
+                $oldContent = $rev->getContent();
+            }
+
+            $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
+        }
+
+        wfProfileOut( __METHOD__ );
+        return $newContent;
+    }
 
        /**
         * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
@@ -1242,8 +1307,64 @@ class WikiPage extends Page {
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
-        */
-       public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
+     * @deprecated since 1.20: use doEditContent() instead.
+        */
+    public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #FIXME: use doEditContent() instead
+        #TODO: log use of deprecated function
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+        return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
+    }
+
+    /**
+     * Change an existing article or create a new article. Updates RC and all necessary caches,
+     * optionally via the deferred update array.
+     *
+     * @param $content Content: new content
+     * @param $summary String: edit summary
+     * @param $flags Integer bitfield:
+     *      EDIT_NEW
+     *          Article is known or assumed to be non-existent, create a new one
+     *      EDIT_UPDATE
+     *          Article is known or assumed to be pre-existing, update it
+     *      EDIT_MINOR
+     *          Mark this edit minor, if the user is allowed to do so
+     *      EDIT_SUPPRESS_RC
+     *          Do not log the change in recentchanges
+     *      EDIT_FORCE_BOT
+     *          Mark the edit a "bot" edit regardless of user rights
+     *      EDIT_DEFER_UPDATES
+     *          Defer some of the updates until the end of index.php
+     *      EDIT_AUTOSUMMARY
+     *          Fill in blank summaries with generated text where possible
+     *
+     * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
+     * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
+     * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
+     * edit-already-exists error will be returned. These two conditions are also possible with
+     * auto-detection due to MediaWiki's performance-optimised locking strategy.
+     *
+     * @param $baseRevId the revision ID this edit was based off, if any
+     * @param $user User the user doing the edit
+     * @param $serialisation_format String: format for storing the content in the database
+     *
+     * @return Status object. Possible errors:
+     *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
+     *     edit-gone-missing:       In update mode, but the article didn't exist
+     *     edit-conflict:           In update mode, the article changed unexpectedly
+     *     edit-no-change:          Warning that the text was the same as before
+     *     edit-already-exists:     In creation mode, but the article already exists
+     *
+     *  Extensions may define additional errors.
+     *
+     *  $return->value will contain an associative array with members as follows:
+     *     new:                     Boolean indicating if the function attempted to create a new article
+     *     revision:                The revision object for the inserted revision, or null
+     *
+     *  Compatibility note: this function previously returned a boolean value indicating success/failure
+     */
+       public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
+                                   User $user = null, $serialisation_format = null ) { #FIXME: use this
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
 
                # Low-level sanity check
@@ -1261,10 +1382,25 @@ class WikiPage extends Page {
 
                $flags = $this->checkFlags( $flags );
 
-               if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
-               {
-                       wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
+        # call legacy hook
+        $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary, #FIXME: document new hook!
+            $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+
+        if ( $hook_ok && !empty( $wgHooks['ArticleSave'] ) ) { # avoid serialization overhead if the hook isn't present
+            $content_text = $content->serialize();
+            $txt = $content_text; # clone
+
+            $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary, #FIXME: deprecate legacy hook!
+                $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+
+            if ( $txt !== $content_text ) {
+                # if the text changed, unserialize the new version to create an updated Content object.
+                $content = $content->getContentHandler()->unserializeContent( $txt );
+            }
+        }
+
+               if ( !$hook_ok ) {
+                       wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
 
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
@@ -1278,20 +1414,25 @@ class WikiPage extends Page {
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
 
-               $oldtext = $this->getRawText(); // current revision
-               $oldsize = strlen( $oldtext );
+               $old_content = $this->getContent( Revision::RAW ); // current revision's content
+
+               $oldsize = $old_content ? $old_content->getSize() : 0;
                $oldid = $this->getLatest();
                $oldIsRedirect = $this->isRedirect();
                $oldcountable = $this->isCountable();
 
+        $handler = $content->getContentHandler();
+
                # Provide autosummaries if one is not provided and autosummaries are enabled.
                if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
-                       $summary = self::getAutosummary( $oldtext, $text, $flags );
+            if ( !$old_content ) $old_content = null;
+                       $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
 
-               $editInfo = $this->prepareTextForEdit( $text, null, $user );
-               $text = $editInfo->pst;
-               $newsize = strlen( $text );
+               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
+               $serialized = $editInfo->pst;
+        $content = $editInfo->pstContent;
+               $newsize =  $content->getSize();
 
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
@@ -1319,14 +1460,17 @@ class WikiPage extends Page {
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
-                               'text'       => $text,
+                               'text'       => $serialized,
+                'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
-                               'timestamp'  => $now
+                               'timestamp'  => $now,
+                'content_model' => $content->getModelName(),
+                'content_format' => $serialisation_format,
                        ) );
 
-                       $changed = ( strcmp( $text, $oldtext ) != 0 );
+                       $changed = !$content->equals( $old_content );
 
                        if ( $changed ) {
                                $dbw->begin( __METHOD__ );
@@ -1424,10 +1568,13 @@ class WikiPage extends Page {
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
-                               'text'       => $text,
+                               'text'       => $serialized,
+                'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
-                               'timestamp'  => $now
+                               'timestamp'  => $now,
+                'content_model' => $content->getModelName(),
+                'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
 
@@ -1445,7 +1592,7 @@ class WikiPage extends Page {
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
-                                       '', strlen( $text ), $revisionId, $patrolled );
+                                       '', $content->getSize(), $revisionId, $patrolled );
 
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
@@ -1458,8 +1605,11 @@ class WikiPage extends Page {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
 
-                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
+                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary, #FIXME: deprecate legacy hook
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+
+            wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook
+                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
 
                # Do updates right now unless deferral was requested
@@ -1470,9 +1620,12 @@ class WikiPage extends Page {
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
 
-               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
+               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,  #FIXME: deprecate legacy hook
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 
+        wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook
+            $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
 
@@ -1500,15 +1653,35 @@ class WikiPage extends Page {
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
-        * @return bool|object
-        */
-       public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
+     * @deprecated in 1.20: use prepareContentForEdit instead.
+        */
+    public function prepareTextForEdit( $text, $revid = null, User $user = null ) {  #FIXME: use prepareContentForEdit() instead #XXX: who uses this?!
+        #TODO: log use of deprecated function
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+        return $this->prepareContentForEdit( $content, $revid , $user );
+    }
+
+    /**
+     * Prepare content which is about to be saved.
+     * Returns a stdclass with source, pst and output members
+     *
+     * @param \Content $content
+     * @param null $revid
+     * @param null|\User $user
+     * @param null $serialization_format
+     * @return bool|object
+     */
+       public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { #FIXME: use this #XXX: really public?!
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
+
                if ( $this->mPreparedEdit
-                       && $this->mPreparedEdit->newText == $text
+                       && $this->mPreparedEdit->newContent
+            && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
+            && $this->mPreparedEdit->format == $serialization_format
+            #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
@@ -1519,11 +1692,23 @@ class WikiPage extends Page {
 
                $edit = (object)array();
                $edit->revid = $revid;
-               $edit->newText = $text;
-               $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
+
+               $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
+        $edit->pst = $edit->pstContent->serialize( $serialization_format );
+        $edit->format = $serialization_format;
+
                $edit->popts = $this->makeParserOptions( 'canonical' );
-               $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
-               $edit->oldText = $this->getRawText();
+
+               // TODO: is there no better way to obtain a context here?
+               $context = RequestContext::getMain();
+               $context->setTitle( $this->mTitle );
+               $edit->output = $edit->pstContent->getParserOutput( $context, $revid, $edit->popts );
+
+        $edit->newContent = $content;
+               $edit->oldContent = $this->getContent( Revision::RAW );
+
+        $edit->newText = ContentHandler::getContentText( $edit->newContent ); #FIXME: B/C only! don't use this field!
+        $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; #FIXME: B/C only! don't use this field!
 
                $this->mPreparedEdit = $edit;
 
@@ -1553,13 +1738,13 @@ class WikiPage extends Page {
                wfProfileIn( __METHOD__ );
 
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
-               $text = $revision->getText();
+        $content = $revision->getContent();
 
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
-                       $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
+                       $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
@@ -1571,9 +1756,9 @@ class WikiPage extends Page {
                        $parserCache->save( $editInfo->output, $this, $editInfo->popts );
                }
 
-               # Update the links tables
-               $u = new LinksUpdate( $this->mTitle, $editInfo->output );
-               $u->doUpdate();
+               # Update the links tables and other secondary data
+        $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
+        SecondaryDataUpdate::runUpdates( $updates );
 
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
 
@@ -1617,7 +1802,7 @@ class WikiPage extends Page {
                }
 
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
-               DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
+        DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) );
 
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
@@ -1643,7 +1828,10 @@ class WikiPage extends Page {
                }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
-                       MessageCache::singleton()->replace( $shortTitle, $text );
+            $msgtext = ContentHandler::getContentText( $content ); #XXX: could skip pseudo-messages like js/css here, based on content model.
+            if ( $msgtext === false || $msgtext === null ) $msgtext = '';
+
+                       MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
 
                if( $options['created'] ) {
@@ -1664,14 +1852,37 @@ class WikiPage extends Page {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
-        */
-       public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+        *
+        * @deprecated since 1.20, use doEditContent() instead.
+        */
+    public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+        wfDeprecated( __METHOD__, "1.20" );
+
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+        return $this->doQuickEditContent( $content, $user, $comment , $minor );
+    }
+
+    /**
+     * Edit an article without doing all that other stuff
+     * The article must already exist; link tables etc
+     * are not updated, caches are not flushed.
+     *
+     * @param $content Content: content submitted
+     * @param $user User The relevant user
+     * @param $comment String: comment submitted
+     * @param $serialisation_format String: format for storing the content in the database
+     * @param $minor Boolean: whereas it's a minor modification
+     */
+       public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
 
+        $serialized = $content->serialize( $serialisation_format );
+
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
-                       'text'       => $text,
+                       'text'       => $serialized,
+            'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
                ) );
@@ -2013,7 +2224,9 @@ class WikiPage extends Page {
                                'ar_len'        => 'rev_len',
                                'ar_page_id'    => 'page_id',
                                'ar_deleted'    => $bitfield,
-                               'ar_sha1'       => 'rev_sha1'
+                               'ar_sha1'       => 'rev_content_model',
+                               'ar_content_format'       => 'rev_content_format',
+                               'ar_content_format'       => 'rev_sha1'
                        ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
@@ -2072,6 +2285,8 @@ class WikiPage extends Page {
 
                $this->updateCategoryCounts( array(), $cats );
 
+        #TODO: move this to an Update object!
+
                # If using cascading deletes, we can skip some explicit deletes
                if ( !$dbw->cascadingDeletes() ) {
                        $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
@@ -2276,7 +2491,7 @@ class WikiPage extends Page {
                }
 
                # Actually store the edit
-               $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
+               $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
@@ -2422,57 +2637,20 @@ class WikiPage extends Page {
 
        /**
        * Return an applicable autosummary if one exists for the given edit.
-       * @param $oldtext String: the previous text of the page.
-       * @param $newtext String: The submitted text of the page.
+       * @param $oldtext String|null: the previous text of the page.
+       * @param $newtext String|null: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
+    * @deprecated since 1.20, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
-               global $wgContLang;
-
-               # Decide what kind of autosummary is needed.
-
-               # Redirect autosummaries
-               $ot = Title::newFromRedirect( $oldtext );
-               $rt = Title::newFromRedirect( $newtext );
-
-               if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
-                       $truncatedtext = $wgContLang->truncate(
-                               str_replace( "\n", ' ', $newtext ),
-                               max( 0, 250
-                                       - strlen( wfMsgForContent( 'autoredircomment' ) )
-                                       - strlen( $rt->getFullText() )
-                               ) );
-                       return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
-               }
-
-               # New page autosummaries
-               if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
-                       # If they're making a new article, give its text, truncated, in the summary.
+               # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
 
-                       $truncatedtext = $wgContLang->truncate(
-                               str_replace( "\n", ' ', $newtext ),
-                               max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
+        $handler = ContentHandler::getForModelName( CONTENT_MODEL_WIKITEXT );
+        $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
+        $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
 
-                       return wfMsgForContent( 'autosumm-new', $truncatedtext );
-               }
-
-               # Blanking autosummaries
-               if ( $oldtext != '' && $newtext == '' ) {
-                       return wfMsgForContent( 'autosumm-blank' );
-               } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
-                       # Removing more than 90% of the article
-
-                       $truncatedtext = $wgContLang->truncate(
-                               $newtext,
-                               max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
-
-                       return wfMsgForContent( 'autosumm-replace', $truncatedtext );
-               }
-
-               # If we reach this point, there's no applicable autosummary for our case, so our
-               # autosummary is empty.
-               return '';
+        return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
 
        /**
@@ -2481,8 +2659,13 @@ class WikiPage extends Page {
         * @param &$hasHistory Boolean: whether the page has a history
         * @return mixed String containing deletion reason or empty string, or boolean false
         *    if no revision occurred
+     * @deprecated since 1.20, use ContentHandler::getAutoDeleteReason() instead
         */
        public function getAutoDeleteReason( &$hasHistory ) {
+        #NOTE: stub for backwards-compatibility.
+
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+        $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory );
                global $wgContLang;
 
                // Get the last revision
@@ -2679,6 +2862,7 @@ class WikiPage extends Page {
 
                if ( count( $templates_diff ) > 0 ) {
                        # Whee, link updates time.
+            # Note: we are only interested in links here. We don't need to get other SecondaryDataUpdate items from the parser output.
                        $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
                        $u->doUpdate();
                }
@@ -2858,14 +3042,27 @@ class PoolWorkArticleView extends PoolCounterWork {
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
-        * @param $text String: text to parse or null to load it
+        * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
+        * @param $context IContextSource context for parsing
         */
-       function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
+       function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null, IContextSource $context = null ) {
+        if ( is_string($content) ) { #BC: old style call
+            $modelName = $page->getRevision()->getContentModelName();
+            $format = $page->getRevision()->getContentFormat();
+            $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelName, $format );
+        }
+
+               if ( is_null( $context ) ) {
+                       $context = RequestContext::getMain();
+                       #XXX: clone and then set title?
+               }
+
                $this->page = $page;
                $this->revid = $revid;
+               $this->context = $context;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
-               $this->text = $text;
+               $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
@@ -2901,25 +3098,27 @@ class PoolWorkArticleView extends PoolCounterWork {
         * @return bool
         */
        function doWork() {
-               global $wgParser, $wgUseFileCache;
+               global $wgUseFileCache;
+
+               // @todo: several of the methods called on $this->page are not declared in Page, but present in WikiPage and delegated by Article.
 
                $isCurrent = $this->revid === $this->page->getLatest();
 
-               if ( $this->text !== null ) {
-                       $text = $this->text;
+               if ( $this->content !== null ) {
+                       $content = $this->content;
                } elseif ( $isCurrent ) {
-                       $text = $this->page->getRawText();
+            $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
-                       $text = $rev->getText();
+            $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
 
                $time = - microtime( true );
-               $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
-                       $this->parserOptions, true, true, $this->revid );
+               // TODO: page might not have this method? Hard to tell what page is supposed to be here...
+               $this->parserOutput = $content->getParserOutput( $this->context, $this->revid, $this->parserOptions );
                $time += microtime( true );
 
                # Timing hack
index 08a33f4..2888d24 100644 (file)
@@ -40,14 +40,16 @@ class EditAction extends FormlessAction {
                $context = $this->getContext();
 
                if ( wfRunHooks( 'CustomEditor', array( $page, $user ) ) ) {
+            $handler = ContentHandler::getForTitle( $page->getTitle() );
+
                        if ( ExternalEdit::useExternalEngine( $context, 'edit' )
                                && $this->getName() == 'edit' && !$request->getVal( 'section' )
                                && !$request->getVal( 'oldid' ) )
                        {
-                               $extedit = new ExternalEdit( $context );
+                               $extedit = $handler->createExternalEdit( $context );
                                $extedit->execute();
                        } else {
-                               $editor = new EditPage( $page );
+                               $editor = $handler->createEditPage( $page );
                                $editor->edit();
                        }
                }
index 5615ad5..f07b5b6 100644 (file)
@@ -135,11 +135,20 @@ class RawAction extends FormlessAction {
                                $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 requeste page uses the content model `"
+                                                                                                               . $content->getModelName() . "` 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();
                        }
                }
 
index 0d9a902..5c85d53 100644 (file)
@@ -109,7 +109,8 @@ class RollbackAction extends FormlessAction {
                $this->getOutput()->returnToMain( false, $this->getTitle() );
 
                if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) {
-                       $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
+            $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+            $de = $contentHandler->createDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
                        $de->showDiff( '', '' );
                }
        }
index 87f0967..cd50030 100644 (file)
@@ -35,7 +35,8 @@ class ApiComparePages extends ApiBase {
                $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
                $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
 
-               $de = new DifferenceEngine( $this->getContext(),
+        $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() );
+        $de = $contentHandler->createDifferenceEngine( $this->getContext(),
                        $rev1,
                        $rev2,
                        null, // rcid
index b8ffff9..eb83432 100644 (file)
@@ -125,7 +125,7 @@ class ApiDelete extends ApiBase {
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
-                       $reason = $page->getAutoDeleteReason( $hasHistory );
+                       $reason = $page->getAutoDeleteReason( $hasHistory ); #FIXME: use ContentHandler::getAutoDeleteReason()
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
index 796b049..133ca9c 100644 (file)
@@ -117,21 +117,23 @@ class ApiEditPage extends ApiBase {
                        // We do want getContent()'s behavior for non-existent
                        // MediaWiki: pages, though
                        if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
-                               $content = '';
+                               $content = null;
+                $text = '';
                        } else {
-                               $content = $articleObj->getContent();
+                $content = $articleObj->getContentObject();
+                $text = ContentHandler::getContentText( $content ); #FIXME: serialize?! get format from params?...
                        }
 
                        if ( !is_null( $params['section'] ) ) {
                                // Process the content for section edits
-                               global $wgParser;
                                $section = intval( $params['section'] );
-                               $content = $wgParser->getSection( $content, $section, false );
-                               if ( $content === false ) {
+                $sectionContent = $content->getSection( $section );
+                $text = ContentHandler::getContentText( $sectionContent ); #FIXME: serialize?! get format from params?...
+                               if ( $text === false || $text === null ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
-                       $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+                       $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
 
@@ -248,7 +250,9 @@ class ApiEditPage extends ApiBase {
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
 
-               $ep = new EditPage( $articleObj );
+        $handler = ContentHandler::getForTitle( $titleObj );
+               $ep = $handler->createEditPage( $articleObj );
+
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
 
diff --git a/includes/api/ApiFormatNone.php b/includes/api/ApiFormatNone.php
new file mode 100644 (file)
index 0000000..31c90e1
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ *
+ *
+ * Created on Oct 22, 2006
+ *
+ * Copyright Â© 2006 Yuri Astrakhan <Firstname><Lastname>@gmail.com
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * API Serialized PHP output formatter
+ * @ingroup API
+ */
+class ApiFormatNone extends ApiFormatBase {
+
+       public function __construct( $main, $format ) {
+               parent::__construct( $main, $format );
+       }
+
+       public function getMimeType() {
+               return 'text/plain';
+       }
+
+       public function execute() {
+       }
+
+       public function getDescription() {
+               return 'Output nothing' . parent::getDescription();
+       }
+
+       public function getVersion() {
+               return __CLASS__ . ': $Id$';
+       }
+}
index 8c035dc..a1c3179 100644 (file)
@@ -103,6 +103,7 @@ class ApiMain extends ApiBase {
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
+               'none' => 'ApiFormatNone',
        );
 
        /**
index 141f779..bb079dd 100644 (file)
@@ -189,7 +189,7 @@ class ApiParse extends ApiBase {
                                return;
                        }
                        // Not cached (save or load)
-                       $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
+                       $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts ); #FIXME: use Content object¡
                }
 
                $result_array = array();
@@ -318,9 +318,9 @@ class ApiParse extends ApiBase {
 
                $page = WikiPage::factory( $titleObj );
 
-               if ( $this->section !== false ) {
+               if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ...
                        $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
-                                       ? 'page id ' . $pageId : $titleObj->getText() );
+                                       ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section...
 
                        // Not cached (save or load)
                        return $wgParser->parse( $this->text, $titleObj, $popts );
@@ -329,13 +329,14 @@ class ApiParse extends ApiBase {
                        // getParserOutput will save to Parser cache if able
                        $pout = $page->getParserOutput( $popts );
                        if ( $getWikitext ) {
-                               $this->text = $page->getRawText();
+                $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
+                               $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
                        }
                        return $pout;
                }
        }
 
-       private function getSectionText( $text, $what ) {
+       private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection
                global $wgParser;
                // Not cached (save or load)
                $text = $wgParser->getSection( $text, $this->section, false );
index 9e9320f..0f7315f 100644 (file)
@@ -90,11 +90,11 @@ class ApiPurge extends ApiBase {
 
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
-                                               true, true, $page->getLatest() );
+                                               true, true, $page->getLatest() ); #FIXME: content!
 
                                        # Update the links tables
-                                       $u = new LinksUpdate( $title, $p_result );
-                                       $u->doUpdate();
+                    $updates = $p_result->getSecondaryDataUpdates( $title );
+                    SecondaryDataUpdate::runUpdates( $updates );
 
                                        $r['linkupdate'] = '';
 
index fa58bdf..f5cb72b 100644 (file)
@@ -114,7 +114,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                }
 
                if ( !is_null( $params['difftotext'] ) ) {
-                       $this->difftotext = $params['difftotext'];
+                       $this->difftotext = $params['difftotext']; #FIXME: handle non-text content!
                } elseif ( !is_null( $params['diffto'] ) ) {
                        if ( $params['diffto'] == 'cur' ) {
                                $params['diffto'] = 0;
@@ -503,11 +503,13 @@ class ApiQueryRevisions extends ApiQueryBase {
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
+                $handler = ContentHandler::getForTitle( $title );
+
                                if ( !is_null( $this->difftotext ) ) {
-                                       $engine = new DifferenceEngine( $context );
-                                       $engine->setText( $text, $this->difftotext );
+                                       $engine = $handler->createDifferenceEngine( $context );
+                                       $engine->setText( $text, $this->difftotext ); #FIXME: use content objects!...
                                } else {
-                                       $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
+                                       $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
index 72eb5d3..56cee2e 100644 (file)
@@ -15,7 +15,7 @@
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp {
+class _DiffOp { #FIXME: no longer private!
        var $type;
        var $orig;
        var $closing;
@@ -44,7 +44,7 @@ class _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Copy extends _DiffOp {
+class _DiffOp_Copy extends _DiffOp { #FIXME: no longer private!
        var $type = 'copy';
 
        function __construct( $orig, $closing = false ) {
@@ -68,7 +68,7 @@ class _DiffOp_Copy extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Delete extends _DiffOp {
+class _DiffOp_Delete extends _DiffOp { #FIXME: no longer private!
        var $type = 'delete';
 
        function __construct( $lines ) {
@@ -89,7 +89,7 @@ class _DiffOp_Delete extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Add extends _DiffOp {
+class _DiffOp_Add extends _DiffOp { #FIXME: no longer private!
        var $type = 'add';
 
        function __construct( $lines ) {
@@ -110,7 +110,7 @@ class _DiffOp_Add extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Change extends _DiffOp {
+class _DiffOp_Change extends _DiffOp { #FIXME: no longer private!
        var $type = 'change';
 
        function __construct( $orig, $closing ) {
@@ -150,7 +150,7 @@ class _DiffOp_Change extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffEngine {
+class _DiffEngine { #FIXME: no longer private!
 
        const MAX_XREF_LENGTH =  10000;
 
@@ -637,7 +637,7 @@ class _DiffEngine {
  * @private
  * @ingroup DifferenceEngine
  */
-class Diff {
+class Diff extends DiffResult {
        var $edits;
 
        /**
@@ -647,11 +647,36 @@ class Diff {
         * @param $from_lines array An array of strings.
         *                (Typically these are lines from a file.)
         * @param $to_lines array An array of strings.
+        * @param $eng _DiffEngine|null The diff engine to use.
         */
-       function __construct( $from_lines, $to_lines ) {
-               $eng = new _DiffEngine;
-               $this->edits = $eng->diff( $from_lines, $to_lines );
-               // $this->_check($from_lines, $to_lines);
+       function __construct( $from_lines, $to_lines, $eng = null  ) {
+               if ( !$eng ) {
+                       $eng = new _DiffEngine();
+               }
+
+               $edits = $eng->diff( $from_lines, $to_lines );
+
+               parent::__construct( $edits );
+
+               //$this->_check( $from_lines, $to_lines );
+       }
+}
+
+/**
+ * Class representing the result of 'diffin' two sequences of strings.
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class DiffResult {
+
+       /**
+        * Constructor.
+        *
+        * @param $edits array An array of Edit.
+        */
+       function __construct( $edits  ) {
+               $this->edits = $edits;
        }
 
        /**
index e8f35f0..348f2dc 100644 (file)
@@ -23,7 +23,7 @@ class DifferenceEngine extends ContextSource {
         * @private
         */
        var $mOldid, $mNewid;
-       var $mOldtext, $mNewtext;
+       var $mOldContent, $mNewContent;
        protected $mDiffLang;
 
        /**
@@ -486,20 +486,21 @@ class DifferenceEngine extends ContextSource {
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
 
-                       if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
+                       if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { #NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
-                               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
-                                       // Wrap the whole lot in a <pre> and don't parse
-                                       $m = array();
-                                       preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
-                                       $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                                       $out->addHTML( htmlspecialchars( $this->mNewtext ) );
-                                       $out->addHTML( "\n</pre>\n" );
+                               if ( !Hook::isRegistered( 'ShowRawCssJs' )
+                    || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
+                    // use the content object's own rendering
+                    $po = $this->mContentObject->getParserOutput();
+                    $out->addHTML( $po->getText() );
                                }
-                       } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
-                               // Handled by extension
+            } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+                // Handled by extension
+            } elseif( Hooks::isRegistered( 'ArticleViewCustom' )
+                    && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
+                // Handled by extension
                        } else {
                                // Normal page
                                if ( $this->getTitle()->equals( $this->mNewPage ) ) {
@@ -630,7 +631,9 @@ class DifferenceEngine extends ContextSource {
                        return false;
                }
 
-               $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+        #TODO: make sure both Content objects have the same content model. What do we do if they don't?
+
+               $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
 
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
@@ -667,14 +670,48 @@ class DifferenceEngine extends ContextSource {
                }
        }
 
+    /**
+     * Generate a diff, no caching.
+     *
+     * Subclasses may override this to provide a
+     *
+     * @param $old Content: old content
+     * @param $new Content: new content
+     */
+    function generateContentDiffBody( Content $old, Content $new ) {
+        #XXX: generate a warning if $old or $new are not instances of TextContent?
+        #XXX: fail if $old and $new don't have the same content model? or what?
+
+        $otext = $old->serialize();
+        $ntext = $new->serialize();
+
+        #XXX: text should be "already segmented". what does that mean?
+        return $this->generateTextDiffBody( $otext, $ntext );
+    }
+
+    /**
+     * Generate a diff, no caching
+     *
+     * @param $otext String: old text, must be already segmented
+     * @param $ntext String: new text, must be already segmented
+     * @deprecated since 1.20, use generateContentDiffBody() instead!
+     */
+    function generateDiffBody( $otext, $ntext ) {
+        wfDeprecated( __METHOD__, "1.20" );
+
+        return $this->generateTextDiffBody( $otext, $ntext );
+    }
+
        /**
         * Generate a diff, no caching
         *
+     * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
+     *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
         * @return bool|string
         */
-       function generateDiffBody( $otext, $ntext ) {
+       function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
 
                wfProfileIn( __METHOD__ );
@@ -928,13 +965,28 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Use specified text instead of loading from the database
+     * @deprecated since 1.20
         */
-       function setText( $oldText, $newText ) {
-               $this->mOldtext = $oldText;
-               $this->mNewtext = $newText;
-               $this->mTextLoaded = 2;
-               $this->mRevisionsLoaded = true;
-       }
+       function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()!
+        wfDeprecated( __METHOD__, "1.20" );
+
+        $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
+        $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
+
+        $this->setContent( $oldContent, $newContent );
+    }
+
+    /**
+     * Use specified text instead of loading from the database
+     * @since 1.20
+     */
+    function setContent( Content $oldContent, Content $newContent ) {
+        $this->mOldContent = $oldContent;
+        $this->mNewContent = $newContent;
+
+        $this->mTextLoaded = 2;
+        $this->mRevisionsLoaded = true;
+    }
 
        /**
         * Set the language in which the diff text is written
@@ -1059,14 +1111,14 @@ class DifferenceEngine extends ContextSource {
                        return false;
                }
                if ( $this->mOldRev ) {
-                       $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
-                       if ( $this->mOldtext === false ) {
+                       $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
+                       if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
-                       $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
-                       if ( $this->mNewtext === false ) {
+                       $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
+                       if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
@@ -1087,7 +1139,7 @@ class DifferenceEngine extends ContextSource {
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
-               $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+               $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
                return true;
        }
 }
index 02d7cb1..68573e0 100644 (file)
@@ -70,6 +70,13 @@ class Ibm_db2Updater extends DatabaseUpdater {
                        array( 'addField', 'revision',      'rev_sha1',         'patch-rev_sha1.sql' ),
                        array( 'addField', 'archive',       'ar_sha1',          'patch-ar_sha1.sql' ),
 
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        // 1.20
                        array( 'addTable', 'config',                            'patch-config.sql' ),
                );
index 5e6ae7e..8182733 100644 (file)
@@ -191,6 +191,14 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
+
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
 
                        // 1.20
index 73bbc57..a79d0b9 100644 (file)
@@ -52,6 +52,13 @@ class OracleUpdater extends DatabaseUpdater {
                        array( 'addField', 'job', 'job_timestamp', 'patch-job_timestamp_field.sql' ),
                        array( 'addIndex', 'job', 'i02', 'patch-job_timestamp_index.sql' ),
 
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        //1.20
                        array( 'addTable', 'config', 'patch-config.sql' ),
 
index a98c4db..8962043 100644 (file)
@@ -70,6 +70,14 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
+
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
 
                        // 1.20
index 1aa206f..c4efcab 100644 (file)
@@ -46,9 +46,11 @@ class RefreshLinksJob extends Job {
                $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
                wfProfileOut( __METHOD__.'-parse' );
                wfProfileIn( __METHOD__.'-update' );
-               $update = new LinksUpdate( $this->title, $parserOutput, false );
-               $update->doUpdate();
-               wfProfileOut( __METHOD__.'-update' );
+
+        $updates = $parserOutput->getSecondaryDataUpdates( $this->title, false );
+        SecondaryDataUpdate::runUpdates( $updates );
+
+        wfProfileOut( __METHOD__.'-update' );
                wfProfileOut( __METHOD__ );
                return true;
        }
@@ -118,8 +120,10 @@ class RefreshLinksJob2 extends Job {
                        $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
                        wfProfileOut( __METHOD__.'-parse' );
                        wfProfileIn( __METHOD__.'-update' );
-                       $update = new LinksUpdate( $title, $parserOutput, false );
-                       $update->doUpdate();
+
+            $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
+            SecondaryDataUpdate::runUpdates( $updates );
+
                        wfProfileOut( __METHOD__.'-update' );
                        wfWaitForSlaves();
                }
index 0d597e8..5304974 100644 (file)
@@ -30,7 +30,7 @@ class CacheTime {
         */
        function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
 
-       /**
+       /**abstract
         * Sets the number of seconds after which this object should expire.
         * This value is used with the ParserCache.
         * If called with a value greater than the value provided at any previous call,
@@ -141,8 +141,9 @@ class ParserOutput extends CacheTime {
                $mProperties = array(),       # Name/value pairs to be cached in the DB
                $mTOCHTML = '',               # HTML of the TOC
                $mTimestamp;                  # Timestamp of the revision
-       private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
-       private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
+           private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
+           private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
+        private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
 
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
@@ -449,4 +450,40 @@ class ParserOutput extends CacheTime {
         function recordOption( $option ) {
                 $this->mAccessedOptions[$option] = true;
         }
+
+    /**
+     * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to
+     * store any secondary information extracted from the page's content.
+     *
+     * @param SecondaryDataUpdate $update
+     */
+    public function addSecondaryDataUpdate( SecondaryDataUpdate $update ) {
+        $this->mSecondaryDataUpdates[] = $update;
+    }
+
+    /**
+     * Returns any SecondaryDataUpdate jobs to be executed in order to store secondary information
+     * extracted from the page's content, includingt a LinksUpdate object for all links stopred in
+     * this ParserOutput object.
+     *
+     * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
+     * @param $recursive Boolean: queue jobs for recursive updates?
+     *
+     * @return array an array of instances of SecondaryDataUpdate
+     */
+    public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) {
+        if ( empty( $title ) ) {
+            $title = Title::newFromText( $this->getTitleText() );
+        }
+
+        $linksUpdate = new LinksUpdate( $title, $this, $recursive );
+
+        if ( empty( $this->mSecondaryDataUpdates ) ) {
+            return array( $linksUpdate );
+        } else {
+            $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) );
+        }
+
+        return $updates;
+    }
 }
index 91a51f8..631ca64 100644 (file)
@@ -80,7 +80,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
                if ( !$revision ) {
                        return null;
                }
-               return $revision->getRawText();
+               return $revision->getRawText(); #FIXME: get raw data from content object after checking the type;
        }
 
        /* Methods */
index 9e3c52b..ba433c8 100644 (file)
@@ -111,7 +111,8 @@ class SpecialComparePages extends SpecialPage {
                $rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] );
 
                if( $rev1 && $rev2 ) {
-                       $de = new DifferenceEngine( $form->getContext(),
+            $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() );
+                       $de = $contentHandler->createDifferenceEngine( $form->getContext(),
                                $rev1,
                                $rev2,
                                null, // rcid
index 06b578d..c2ca7ce 100644 (file)
@@ -116,7 +116,8 @@ class PageArchive {
                $res = $dbr->select( 'archive',
                        array(
                                'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
-                               'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
+                               'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+                'ar_content_format', 'ar_content_model'
                        ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
@@ -189,6 +190,8 @@ class PageArchive {
                                'ar_deleted',
                                'ar_len',
                                'ar_sha1',
+                'ar_content_format',
+                'ar_content_model',
                        ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
@@ -462,7 +465,9 @@ class PageArchive {
                                'ar_deleted',
                                'ar_page_id',
                                'ar_len',
-                               'ar_sha1' ),
+                               'ar_sha1',
+                'ar_content_format',
+                'ar_content_model' ),
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@ -892,7 +897,8 @@ class SpecialUndelete extends SpecialPage {
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
-               $diffEngine = new DifferenceEngine( $this->getContext() );
+        $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+               $diffEngine = $contentHandler->createDifferenceEngine( $this->getContext() );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
@@ -909,8 +915,8 @@ class SpecialUndelete extends SpecialPage {
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
-                       $diffEngine->generateDiffBody(
-                               $previousRev->getText(), $currentRev->getText() ) .
+                       $diffEngine->generateContentDiffBody(
+                               $previousRev->getContent(), $currentRev->getContent() ) .
                        "</table>" .
                        "</div>\n"
                );
index 2fa9002..9a53e92 100644 (file)
@@ -887,6 +887,7 @@ $1',
 'portal-url'           => 'Project:Community portal',
 'privacy'              => 'Privacy policy',
 'privacypage'          => 'Project:Privacy policy',
+'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
 
 'badaccess'        => 'Permission error',
 'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
diff --git a/maintenance/archives/patch-archive-ar_content_format.sql b/maintenance/archives/patch-archive-ar_content_format.sql
new file mode 100644 (file)
index 0000000..81f9fca
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-archive-ar_content_model.sql b/maintenance/archives/patch-archive-ar_content_model.sql
new file mode 100644 (file)
index 0000000..1a8b630
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-page-page_content_model.sql b/maintenance/archives/patch-page-page_content_model.sql
new file mode 100644 (file)
index 0000000..30434d9
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/page
+  ADD page_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_format.sql b/maintenance/archives/patch-revision-rev_content_format.sql
new file mode 100644 (file)
index 0000000..22aeb8a
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_model.sql b/maintenance/archives/patch-revision-rev_content_model.sql
new file mode 100644 (file)
index 0000000..1ba0572
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_model varbinary(32) DEFAULT NULL;
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index 6626cbc..cec91fb 100644 (file)
@@ -67,7 +67,7 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance {
                        # Go through and update rev_len from these rows.
                        foreach ( $res as $row ) {
                                $rev = new Revision( $row );
-                               $text = $rev->getRawText();
+                               $text = $rev->getRawText(); #FIXME: go via Content object; #FIXME: get size via Content object
                                if ( !is_string( $text ) ) {
                                        # This should not happen, but sometimes does (bug 20757)
                                        $this->output( "Text of revision {$row->rev_id} unavailable!\n" );
index a7c7ec4..20a8043 100644 (file)
@@ -221,6 +221,12 @@ class RefreshLinks extends Maintenance {
 
                $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
                $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
+
+        $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
+        SecondaryDataUpdate::runUpdates( $updates );
+
+        $dbw->commit();
+        // TODO: We don't know what happens here.
                $update = new LinksUpdate( $title, $parserOutput, false );
                $update->doUpdate();
                $dbw->commit( __METHOD__ );
old mode 100755 (executable)
new mode 100644 (file)
index a848bf5..a0601f1 100644 (file)
@@ -260,7 +260,10 @@ CREATE TABLE /*_*/page (
   page_latest int unsigned NOT NULL,
 
   -- Uncompressed length in bytes of the page's current source text.
-  page_len int unsigned NOT NULL
+  page_len int unsigned NOT NULL,
+
+  -- content model
+  page_content_model  varbinary(32) default NULL
 ) /*$wgDBTableOptions*/;
 
 CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
@@ -316,7 +319,13 @@ CREATE TABLE /*_*/revision (
   rev_parent_id int unsigned default NULL,
 
   -- SHA-1 text content hash in base-36
-  rev_sha1 varbinary(32) NOT NULL default ''
+  rev_sha1 varbinary(32) NOT NULL default '',
+
+  -- content model
+  rev_content_model  varbinary(32) default NULL,
+
+  -- content format (mime type)
+  rev_content_format varbinary(64) default NULL
 
 ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
 -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit
@@ -427,7 +436,14 @@ CREATE TABLE /*_*/archive (
   ar_parent_id int unsigned default NULL,
 
   -- SHA-1 text content hash in base-36
-  ar_sha1 varbinary(32) NOT NULL default ''
+  ar_sha1 varbinary(32) NOT NULL default '',
+
+  -- content model
+  ar_content_model  varbinary(32) default NULL,
+
+  -- content format (mime type)
+  ar_content_format varbinary(64) default NULL
+
 ) /*$wgDBTableOptions*/;
 
 CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
diff --git a/tests/phpunit/includes/ContentHandlerTest.php b/tests/phpunit/includes/ContentHandlerTest.php
new file mode 100644 (file)
index 0000000..704ad71
--- /dev/null
@@ -0,0 +1,319 @@
+<?php
+
+class ContentHandlerTest extends MediaWikiTestCase {
+
+       public function dataGetDefaultModelFor() {
+               return array(
+                       array( 'Foo', CONTENT_MODEL_WIKITEXT ),
+                       array( 'Foo.js', CONTENT_MODEL_WIKITEXT ),
+                       array( '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.JS', CONTENT_MODEL_WIKITEXT ),
+                       array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
+                       array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetDefaultModelFor
+        */
+       public function testGetDefaultModelFor( $title, $expectedModelName ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expectedModelName, ContentHandler::getDefaultModelFor( $title ) );
+       }
+       /**
+        * @dataProvider dataGetDefaultModelFor
+        */
+       public function testGetForTitle( $title, $expectedContentModel ) {
+               $title = Title::newFromText( $title );
+               $handler = ContentHandler::getForTitle( $title );
+               $this->assertEquals( $expectedContentModel, $handler->getModelName() );
+       }
+
+       public function testGetContentText_Null( ) {
+               global $wgContentHandlerTextFallback;
+
+               $content = null;
+
+               $wgContentHandlerTextFallback = 'fail';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( '', $text );
+
+               $wgContentHandlerTextFallback = 'serialize';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( '', $text );
+
+               $wgContentHandlerTextFallback = 'ignore';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( '', $text );
+       }
+
+       public function testGetContentText_TextContent( ) {
+               global $wgContentHandlerTextFallback;
+
+               $content = new WikitextContent( "hello world" );
+
+               $wgContentHandlerTextFallback = 'fail';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( $content->getNativeData(), $text );
+
+               $wgContentHandlerTextFallback = 'serialize';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( $content->serialize(), $text );
+
+               $wgContentHandlerTextFallback = 'ignore';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( $content->getNativeData(), $text );
+       }
+
+       public function testGetContentText_NonTextContent( ) {
+               global $wgContentHandlerTextFallback;
+
+               $content = new DummyContentForTesting( "hello world" );
+
+               $wgContentHandlerTextFallback = 'fail';
+
+               try {
+                       $text = ContentHandler::getContentText( $content );
+
+                       $this->fail( "ContentHandler::getContentText should have thrown an exception for non-text Content object" );
+               } catch (MWException $ex) {
+                       // as expected
+               }
+
+               $wgContentHandlerTextFallback = 'serialize';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertEquals( $content->serialize(), $text );
+
+               $wgContentHandlerTextFallback = 'ignore';
+               $text = ContentHandler::getContentText( $content );
+               $this->assertNull( $text );
+       }
+
+       #public static function makeContent( $text, Title $title, $modelName = null, $format = null )
+
+       public function dataMakeContent() {
+               return array(
+                       array( 'hallo', 'Test', null, null, CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+                       array( 'hallo', 'MediaWiki:Test.js', null, null, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+                       array( serialize('hallo'), 'Dummy:Test', null, null, 'DUMMY', 'hallo', false ),
+
+                       array( 'hallo', 'Test', null, 'application/x-wiki', CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+                       array( 'hallo', 'MediaWiki:Test.js', null, 'text/javascript', CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+                       array( serialize('hallo'), 'Dummy:Test', null, 'dummy', 'DUMMY', 'hallo', false ),
+
+                       array( 'hallo', 'Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ),
+                       array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, 'hallo', false ),
+                       array( serialize('hallo'), 'Dummy:Test', CONTENT_MODEL_CSS, null, CONTENT_MODEL_CSS, serialize('hallo'), false ),
+
+                       array( 'hallo', 'Test', CONTENT_MODEL_WIKITEXT, 'dummy', null, null, true ),
+                       array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, 'dummy', null, null, true ),
+                       array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, 'dummy', null, null, true ),
+               );
+       }
+
+       /**
+        * @dataProvider dataMakeContent
+        */
+       public function testMakeContent( $data, $title, $modelName, $format, $expectedModelName, $expectedNativeData, $shouldFail ) {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers;
+
+               $title = Title::newFromText( $title );
+
+               try {
+                       $content = ContentHandler::makeContent( $data, $title, $modelName, $format );
+
+                       if ( $shouldFail ) $this->fail( "ContentHandler::makeContent should have failed!" );
+
+                       $this->assertEquals( $expectedModelName, $content->getModelName(), 'bad model name' );
+                       $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' );
+               } catch ( MWException $ex ) {
+                       if ( !$shouldFail ) $this->fail( "ContentHandler::makeContent failed unexpectedly!" );
+                       else $this->assertTrue( true ); // dummy, so we don't get the "test did not perform any assertions" message.
+               }
+
+       }
+
+
+       public function setup() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers;
+
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
+               $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
+       }
+
+       public function teardown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers;
+
+               unset( $wgExtraNamespaces[ 12312 ] );
+               unset( $wgExtraNamespaces[ 12313 ] );
+
+               unset( $wgNamespaceContentModels[ 12312 ] );
+               unset( $wgContentHandlers[ 'DUMMY' ] );
+       }
+
+}
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+       public function __construct( $dataModel ) {
+               parent::__construct( $dataModel, array('dummy') );
+       }
+
+       /**
+        * Serializes Content object of the type supported by this ContentHandler.
+        *
+        * @param Content $content the Content object to serialize
+        * @param null $format the desired serialization format
+        * @return String serialized form of the content
+        */
+       public function serializeContent( Content $content, $format = null )
+       {
+          return $content->serialize();
+       }
+
+       /**
+        * Unserializes a Content object of the type supported by this ContentHandler.
+        *
+        * @param $blob String serialized form of the content
+        * @param null $format the format used for serialization
+        * @return Content the Content object created by deserializing $blob
+        */
+       public function unserializeContent( $blob, $format = null )
+       {
+               $d = unserialize( $blob );
+               return new DummyContentForTesting( $d );
+       }
+
+       /**
+        * Creates an empty Content object of the type supported by this ContentHandler.
+        *
+        */
+       public function makeEmptyContent()
+       {
+               return new DummyContentForTesting( '' );
+       }
+}
+
+class DummyContentForTesting extends Content {
+
+       public function __construct( $data ) {
+               parent::__construct( "DUMMY" );
+
+               $this->data = $data;
+       }
+
+       public function serialize() {
+               return serialize( $this->data );
+       }
+
+       /**
+        * @return String a string representing the content in a way useful for building a full text search index.
+        *         If no useful representation exists, this method returns an empty string.
+        */
+       public function getTextForSearchIndex()
+       {
+               return '';
+       }
+
+       /**
+        * @return String the wikitext to include when another page includes this  content, or false if the content is not
+        *         includable in a wikitext page.
+        */
+       public function getWikitextForTransclusion()
+       {
+               return false;
+       }
+
+       /**
+        * Returns a textual representation of the content suitable for use in edit summaries and log messages.
+        *
+        * @param int $maxlength maximum length of the summary text
+        * @return String the summary text
+        */
+       public function getTextForSummary( $maxlength = 250 )
+       {
+               return '';
+       }
+
+       /**
+        * Returns native represenation of the data. Interpretation depends on the data model used,
+        * as given by getDataModel().
+        *
+        * @return mixed the native representation of the content. Could be a string, a nested array
+        *         structure, an object, a binary blob... anything, really.
+        */
+       public function getNativeData()
+       {
+               return $this->data;
+       }
+
+       /**
+        * returns the content's nominal size in bogo-bytes.
+        *
+        * @return int
+        */
+       public function getSize()
+       {
+               return 23;
+       }
+
+       /**
+        * Return a copy of this Content object. The following must be true for the object returned
+        * if $copy = $original->copy()
+        *
+        * * get_class($original) === get_class($copy)
+        * * $original->getModelName() === $copy->getModelName()
+        * * $original->equals( $copy )
+        *
+        * If and only if the Content object is imutable, the copy() method can and should
+        * return $this. That is,  $copy === $original may be true, but only for imutable content
+        * objects.
+        *
+        * @return Content. A copy of this object
+        */
+       public function copy()
+       {
+               return $this;
+       }
+
+       /**
+        * Returns true if this content is countable as a "real" wiki page, provided
+        * that it's also in a countable location (e.g. a current revision in the main namespace).
+        *
+        * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
+        *                        to avoid redundant parsing to find out.
+        * @return boolean
+        */
+       public function isCountable( $hasLinks = null )
+       {
+               return false;
+       }
+
+       /**
+        * @param IContextSource $context
+        * @param null $revId
+        * @param null|ParserOptions $options
+        * @param Boolean $generateHtml whether to generate Html (default: true). If false,
+        *        the result of calling getText() on the ParserOutput object returned by
+        *        this method is undefined.
+        *
+        * @return ParserOutput
+        */
+       public function getParserOutput( IContextSource $context, $revId = null, ParserOptions $options = NULL, $generateHtml = true )
+       {
+               return new ParserOutput( $this->data );
+       }
+}
+
diff --git a/tests/phpunit/includes/CssContentTest.php b/tests/phpunit/includes/CssContentTest.php
new file mode 100644 (file)
index 0000000..072a6b0
--- /dev/null
@@ -0,0 +1,41 @@
+<?php
+
+class CssContentTest extends JavascriptContentTest {
+
+       public function newContent( $text ) {
+               return new CssContent( $text );
+       }
+
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("hello <world>\n", "<pre class=\"mw-code mw-css\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>\n"),
+                       // @todo: more...?
+               );
+       }
+
+
+       # =================================================================================================================
+
+       public function getModelName() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModelName() );
+       }
+
+       public function getContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelName() );
+       }
+
+       public function dataEquals( ) {
+               return array(
+                       array( new CssContent( "hallo" ), null, false ),
+                       array( new CssContent( "hallo" ), new CssContent( "hallo" ), true ),
+                       array( new CssContent( "hallo" ), new WikitextContent( "hallo" ), false ),
+                       array( new CssContent( "hallo" ), new CssContent( "HALLO" ), false ),
+               );
+       }
+
+}
diff --git a/tests/phpunit/includes/JavascriptContentTest.php b/tests/phpunit/includes/JavascriptContentTest.php
new file mode 100644 (file)
index 0000000..41f9f4a
--- /dev/null
@@ -0,0 +1,233 @@
+<?php
+
+class JavascriptContentTest extends WikitextContentTest {
+
+       public function newContent( $text ) {
+               return new JavascriptContent( $text );
+       }
+
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("hello <world>\n", "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nhello &lt;world&gt;\n\n</pre>\n"),
+                       // @todo: more...?
+               );
+       }
+
+       public function dataGetSection() {
+               return array(
+                       array( WikitextContentTest::$sections,
+                              "0",
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "2",
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "8",
+                              null
+                       ),
+               );
+       }
+
+       public function dataReplaceSection() {
+               return array(
+                       array( WikitextContentTest::$sections,
+                              "0",
+                              "No more",
+                              null,
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "",
+                              "No more",
+                              null,
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "2",
+                              "== TEST ==\nmore fun",
+                              null,
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "8",
+                              "No more",
+                              null,
+                              null
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "new",
+                              "No more",
+                              "New",
+                              null
+                       ),
+               );
+       }
+
+       public function testAddSectionHeader( ) {
+               $content = $this->newContent( 'hello world' );
+               $c = $content->addSectionHeader( 'test' );
+
+               $this->assertTrue( $content->equals( $c ) );
+       }
+
+       public function dataPreSaveTransform() {
+               return array(
+                       array( 'hello this is ~~~',
+                              "hello this is ~~~",
+                       ),
+                       array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                              'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                       ),
+               );
+       }
+
+       public function dataPreloadTransform() {
+               return array(
+                       array( 'hello this is ~~~',
+                              "hello this is ~~~",
+                       ),
+                       array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+                              'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+                       ),
+               );
+       }
+
+       public function dataGetRedirectTarget() {
+               return array(
+                       array( '#REDIRECT [[Test]]',
+                              null,
+                       ),
+                       array( '#REDIRECT Test',
+                              null,
+                       ),
+                       array( '* #REDIRECT [[Test]]',
+                              null,
+                       ),
+               );
+       }
+
+       /**
+        * @todo: test needs database!
+        */
+       /*
+       public function getRedirectChain() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectArray( $text );
+       }
+       */
+
+       /**
+        * @todo: test needs database!
+        */
+       /*
+       public function getUltimateRedirectTarget() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectRecurse( $text );
+       }
+       */
+
+
+       public function dataIsCountable() {
+               return array(
+                       array( '',
+                              null,
+                              'any',
+                              true
+                       ),
+                       array( 'Foo',
+                              null,
+                              'any',
+                              true
+                       ),
+                       array( 'Foo',
+                              null,
+                              'comma',
+                              false
+                       ),
+                       array( 'Foo, bar',
+                              null,
+                              'comma',
+                              false
+                       ),
+                       array( 'Foo',
+                              null,
+                              'link',
+                              false
+                       ),
+                       array( 'Foo [[bar]]',
+                              null,
+                              'link',
+                              false
+                       ),
+                       array( 'Foo',
+                              true,
+                              'link',
+                              false
+                       ),
+                       array( 'Foo [[bar]]',
+                              false,
+                              'link',
+                              false
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'any',
+                              true
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'comma',
+                              false
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'link',
+                              false
+                       ),
+               );
+       }
+
+       public function dataGetTextForSummary() {
+               return array(
+                       array( "hello\nworld.",
+                              16,
+                              'hello world.',
+                       ),
+                       array( 'hello world.',
+                              8,
+                              'hello...',
+                       ),
+                       array( '[[hello world]].',
+                              8,
+                              '[[hel...',
+                       ),
+               );
+       }
+
+       # =================================================================================================================
+
+       public function getModelName() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModelName() );
+       }
+
+       public function getContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelName() );
+       }
+
+       public function dataEquals( ) {
+               return array(
+                       array( new JavascriptContent( "hallo" ), null, false ),
+                       array( new JavascriptContent( "hallo" ), new JavascriptContent( "hallo" ), true ),
+                       array( new JavascriptContent( "hallo" ), new CssContent( "hallo" ), false ),
+                       array( new JavascriptContent( "hallo" ), new JavascriptContent( "HALLO" ), false ),
+               );
+       }
+
+}
index d7654db..14e2504 100644 (file)
@@ -6,14 +6,31 @@ class RevisionTest extends MediaWikiTestCase {
        function setUp() {
                global $wgContLang;
                $wgContLang = Language::factory( 'en' );
+
                $globalSet = array(
                        '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;
                }
+
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers;
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
+               $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
+
+               global $wgContentHandlerTextFallback;
+               $wgContentHandlerTextFallback = 'ignore';
        }
 
        function tearDown() {
@@ -120,6 +137,138 @@ class RevisionTest extends MediaWikiTestCase {
                $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() {
+               return array(
+                       array( 'hello world', '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, 'DUMMY' ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentModel
+        */
+       function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedModel, $rev->getContentModelName() );
+       }
+
+       function dataGetContentFormat() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, 'application/x-wiki' ),
+                       array( 'hello world', 'Hello', CONTENT_MODEL_CSS, null, 'text/css' ),
+                       array( 'hello world', 'User:hello/there.css', null, null, 'text/css' ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, 'dummy' ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentFormat
+        */
+       function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+       }
+
+       function dataGetContentHandler() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, 'WikitextContentHandler' ),
+                       array( 'hello world', 'User:hello/there.css', null, null, 'CssContentHandler' ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, 'DummyContentHandlerForTesting' ),
+               );
+       }
+
+       /**
+        * @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() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', 'DUMMY', null, Revision::FOR_PUBLIC, serialize('hello world') ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize('hello world') ),
+               );
+       }
+
+       /**
+        * @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() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', 'DUMMY', null, Revision::FOR_PUBLIC, null ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetText
+        */
+       function testGetText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedText, $rev->getText( $audience ) );
+       }
+
+       /**
+        * @dataProvider dataGetText
+        */
+       function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedText, $rev->getRawText( $audience ) );
+       }
+
+       // @todo: set up testing environment with database to tgest loading and inserting revisions
+
 }
 
 
index badd040..8d3b76c 100644 (file)
@@ -2,6 +2,24 @@
 
 class TitleMethodsTest extends MediaWikiTestCase {
 
+    public function setup() {
+        global $wgExtraNamespaces, $wgNamespaceContentModels;
+
+        $wgExtraNamespaces[ 12302 ] = 'TEST-JS';
+        $wgExtraNamespaces[ 12303 ] = 'TEST-JS_TALK';
+
+        $wgNamespaceContentModels[ 12302 ] = CONTENT_MODEL_JAVASCRIPT;
+    }
+
+    public function teardown() {
+        global $wgExtraNamespaces, $wgNamespaceContentModels;
+
+        unset( $wgExtraNamespaces[ 12302 ] );
+        unset( $wgExtraNamespaces[ 12303 ] );
+
+        unset( $wgNamespaceContentModels[ 12302 ] );
+    }
+
        public function dataEquals() {
                return array(
                        array( 'Main Page', 'Main Page', true ),
@@ -43,7 +61,7 @@ class TitleMethodsTest extends MediaWikiTestCase {
         */
        public function testInNamespace( $title, $ns, $expectedBool ) {
                $title = Title::newFromText( $title );
-               $this->assertEquals( $title->inNamespace( $ns ), $expectedBool );
+               $this->assertEquals( $expectedBool, $title->inNamespace( $ns ) );
        }
 
        public function testInNamespaces() {
@@ -75,4 +93,176 @@ class TitleMethodsTest extends MediaWikiTestCase {
                $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
        }
 
+
+    public function dataGetContentModelName() {
+        return array(
+            array( 'Foo', CONTENT_MODEL_WIKITEXT ),
+            array( 'Foo.js', CONTENT_MODEL_WIKITEXT ),
+            array( '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 dataGetContentModelName
+     */
+    public function testGetContentModelName( $title, $expectedModelName ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedModelName, $title->getContentModelName() );
+    }
+
+    /**
+     * @dataProvider dataGetContentModelName
+     */
+    public function testHasContentModel( $title, $expectedModelName ) {
+        $title = Title::newFromText( $title );
+        $this->assertTrue( $title->hasContentModel( $expectedModelName ) );
+    }
+
+    public function dataIsCssOrJsPage() {
+        return array(
+            array( 'Foo', false ),
+            array( 'Foo.js', false ),
+            array( 'Foo/bar.js', false ),
+            array( 'User:Foo', false ),
+            array( 'User:Foo.js', false ),
+            array( 'User:Foo/bar.js', false ),
+            array( 'User:Foo/bar.css', false ),
+            array( 'User talk:Foo/bar.css', false ),
+            array( 'User:Foo/bar.js.xxx', false ),
+            array( 'User:Foo/bar.xxx', false ),
+            array( 'MediaWiki:Foo.js', true ),
+            array( 'MediaWiki:Foo.css', true ),
+            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
+     */
+    public function testIsCssOrJsPage( $title, $expectedBool ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedBool, $title->isCssOrJsPage() );
+    }
+
+
+    public function dataIsCssJsSubpage() {
+        return array(
+            array( 'Foo', false ),
+            array( 'Foo.js', false ),
+            array( 'Foo/bar.js', false ),
+            array( 'User:Foo', false ),
+            array( 'User:Foo.js', false ),
+            array( 'User:Foo/bar.js', true ),
+            array( 'User:Foo/bar.css', true ),
+            array( 'User talk:Foo/bar.css', false ),
+            array( 'User:Foo/bar.js.xxx', false ),
+            array( 'User:Foo/bar.xxx', false ),
+            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
+     */
+    public function testIsCssJsSubpage( $title, $expectedBool ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedBool, $title->isCssJsSubpage() );
+    }
+
+    public function dataIsCssSubpage() {
+        return array(
+            array( 'Foo', false ),
+            array( 'Foo.css', false ),
+            array( 'User:Foo', false ),
+            array( 'User:Foo.js', false ),
+            array( 'User:Foo.css', false ),
+            array( 'User:Foo/bar.js', false ),
+            array( 'User:Foo/bar.css', true ),
+        );
+    }
+
+    /**
+     * @dataProvider dataIsCssSubpage
+     */
+    public function testIsCssSubpage( $title, $expectedBool ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedBool, $title->isCssSubpage() );
+    }
+
+    public function dataIsJsSubpage() {
+        return array(
+            array( 'Foo', false ),
+            array( 'Foo.css', false ),
+            array( 'User:Foo', false ),
+            array( 'User:Foo.js', false ),
+            array( 'User:Foo.css', false ),
+            array( 'User:Foo/bar.js', true ),
+            array( 'User:Foo/bar.css', false ),
+        );
+    }
+
+    /**
+     * @dataProvider dataIsJsSubpage
+     */
+    public function testIsJsSubpage( $title, $expectedBool ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedBool, $title->isJsSubpage() );
+    }
+
+    public function dataIsWikitextPage() {
+        return array(
+            array( 'Foo', true ),
+            array( 'Foo.js', true ),
+            array( 'Foo/bar.js', true ),
+            array( 'User:Foo', true ),
+            array( 'User:Foo.js', true ),
+            array( 'User:Foo/bar.js', false ),
+            array( 'User:Foo/bar.css', false ),
+            array( 'User talk:Foo/bar.css', true ),
+            array( 'User:Foo/bar.js.xxx', true ),
+            array( 'User:Foo/bar.xxx', true ),
+            array( 'MediaWiki:Foo.js', false ),
+            array( 'MediaWiki:Foo.css', 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
+     */
+    public function testIsWikitextPage( $title, $expectedBool ) {
+        $title = Title::newFromText( $title );
+        $this->assertEquals( $expectedBool, $title->isWikitextPage() );
+    }
+
 }
diff --git a/tests/phpunit/includes/WikiPageTest.php b/tests/phpunit/includes/WikiPageTest.php
new file mode 100644 (file)
index 0000000..96bb043
--- /dev/null
@@ -0,0 +1,752 @@
+<?php
+/**
+* @group Database
+* ^--- important, causes temporary tables to be used instead of the real database
+**/
+
+class WikiPageTest extends MediaWikiTestCase {
+
+       var $pages_to_delete;
+
+       public function setUp() {
+               $this->pages_to_delete = array();
+       }
+
+       public function tearDown() {
+               foreach ( $this->pages_to_delete as $p ) {
+                       /* @var $p WikiPage */
+
+                       try {
+                               if ( $p->exists() ) {
+                                       $p->doDeleteArticle( "testing done." );
+                               }
+                       } catch ( MWException $ex ) {
+                               // fail silently
+                       }
+               }
+       }
+
+       protected function newPage( $title ) {
+               if ( is_string( $title ) ) $title = Title::newFromText( $title );
+
+               $p = new WikiPage( $title );
+
+               $this->pages_to_delete[] = $p;
+
+               return $p;
+       }
+
+       protected function createPage( $page, $text, $model = null ) {
+               if ( is_string( $page ) ) $page = Title::newFromText( $page );
+               if ( $page instanceof Title ) $page = $this->newPage( $page );
+
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing" );
+
+               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 );
+
+               $page->doEditContent( $content, "testing 1" );
+
+               $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" );
+
+               # ------------------------
+               $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 );
+
+               $page->doEditContent( $content, "testing 2" );
+
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               $retrieved = $page->getContent();
+               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+       }
+
+       public function testDoEdit() {
+               $title = Title::newFromText( "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.";
+
+               $page->doEdit( $text, "testing 1" );
+
+               $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" );
+
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               $retrieved = $page->getText();
+               $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+
+               # ------------------------
+               $text = "At vero eos et accusam et justo duo dolores et ea rebum. "
+                      . "Stet clita kasd gubergren, no sea takimata sanctus est.";
+
+               $page->doEdit( $text, "testing 2" );
+
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               $retrieved = $page->getText();
+               $this->assertEquals( $text, $retrieved, 'retrieved text doesn\'t equal original' );
+       }
+
+       public function testDoQuickEdit() {
+               global $wgUser;
+
+               $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" );
+
+               $text = "quick text";
+               $page->doQuickEdit( $text, $wgUser, "testing q" );
+
+               # ---------------------
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( $text, $page->getText() );
+       }
+
+       public function testDoQuickEditContent() {
+               global $wgUser;
+
+               $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text" );
+
+               $content = ContentHandler::makeContent( "quick text", $page->getTitle() );
+               $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" );
+
+               $page->doDeleteArticle( "testing deletion" );
+
+               $this->assertFalse( $page->exists() );
+
+               $this->assertNull( $page->getContent() );
+               $this->assertFalse( $page->getText() );
+
+               $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
+               $this->assertFalse( $t->exists() );
+       }
+
+       public function testGetRevision() {
+               $page = $this->newPage( "WikiPageTest_testGetRevision" );
+
+               $rev = $page->getRevision();
+               $this->assertNull( $rev );
+
+               # -----------------
+               $this->createPage( $page, "some text" );
+
+               $rev = $page->getRevision();
+
+               $this->assertEquals( $page->getLatest(), $rev->getId() );
+               $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 = $page->getContent();
+               $this->assertEquals( "some text", $content->getNativeData() );
+       }
+
+       public function testGetText() {
+               $page = $this->newPage( "WikiPageTest_testGetText" );
+
+               $text = $page->getText();
+               $this->assertFalse( $text );
+
+               # -----------------
+               $this->createPage( $page, "some text" );
+
+               $text = $page->getText();
+               $this->assertEquals( "some text", $text );
+       }
+
+       public function testGetRawText() {
+               $page = $this->newPage( "WikiPageTest_testGetRawText" );
+
+               $text = $page->getRawText();
+               $this->assertFalse( $text );
+
+               # -----------------
+               $this->createPage( $page, "some text" );
+
+               $text = $page->getRawText();
+               $this->assertEquals( "some text", $text );
+       }
+
+       public function testGetContentModelName() {
+               $page = $this->createPage( "WikiPageTest_testGetContentModelName", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModelName() );
+       }
+
+       public function testGetContentHandler() {
+               $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->assertTrue( $page->exists() );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertTrue( $page->exists() );
+
+               # -----------------
+               $page->doDeleteArticle( "done testing" );
+               $this->assertFalse( $page->exists() );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertFalse( $page->exists() );
+       }
+
+       public function dataHasViewableContent() {
+               return array(
+                       array( 'WikiPageTest_testHasViewableContent', false, true ),
+                       array( 'Special:WikiPageTest_testHasViewableContent', false ),
+                       array( 'MediaWiki:WikiPageTest_testHasViewableContent', false ),
+                       array( 'Special:Userlogin', true ),
+                       array( 'MediaWiki:help', true ),
+               );
+       }
+
+       /**
+        * @dataProvider dataHasViewableContent
+        */
+       public function testHasViewableContent( $title, $viewable, $create = false ) {
+               $page = $this->newPage( $title );
+               $this->assertEquals( $viewable, $page->hasViewableContent() );
+
+               if ( $create ) {
+                       $this->createPage( $page, "some text" );
+                       $this->assertTrue( $page->hasViewableContent() );
+
+                       $page = new WikiPage( $page->getTitle() );
+                       $this->assertTrue( $page->hasViewableContent() );
+               }
+       }
+
+       public function dataGetRedirectTarget() {
+               return array(
+                       array( 'WikiPageTest_testGetRedirectTarget_1', "hello world", null ),
+                       array( 'WikiPageTest_testGetRedirectTarget_2', "#REDIRECT [[hello world]]", "Hello world" ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetRedirectTarget
+        */
+       public function testGetRedirectTarget( $title, $text, $target ) {
+               $page = $this->createPage( $title, $text );
+
+               $t = $page->getRedirectTarget();
+               $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
+       }
+
+       /**
+        * @dataProvider dataGetRedirectTarget
+        */
+       public function testIsRedirect( $title, $text, $target ) {
+               $page = $this->createPage( $title, $text );
+               $this->assertEquals( !is_null( $target ), $page->isRedirect() );
+       }
+
+       public function dataIsCountable() {
+               return array(
+
+                       // any
+                       array( 'WikiPageTest_testIsCountable',
+                              '',
+                              'any',
+                              true
+                       ),
+                       array( 'WikiPageTest_testIsCountable',
+                              'Foo',
+                              'any',
+                              true
+                       ),
+
+                       // comma
+                       array( 'WikiPageTest_testIsCountable',
+                              'Foo',
+                              'comma',
+                              false
+                       ),
+                       array( 'WikiPageTest_testIsCountable',
+                              'Foo, bar',
+                              'comma',
+                              true
+                       ),
+
+                       // link
+                       array( 'WikiPageTest_testIsCountable',
+                              'Foo',
+                              'link',
+                              false
+                       ),
+                       array( 'WikiPageTest_testIsCountable',
+                              'Foo [[bar]]',
+                              'link',
+                              true
+                       ),
+
+                       // redirects
+                       array( 'WikiPageTest_testIsCountable',
+                              '#REDIRECT [[bar]]',
+                              'any',
+                              false
+                       ),
+                       array( 'WikiPageTest_testIsCountable',
+                              '#REDIRECT [[bar]]',
+                              'comma',
+                              false
+                       ),
+                       array( 'WikiPageTest_testIsCountable',
+                              '#REDIRECT [[bar]]',
+                              'link',
+                              false
+                       ),
+
+                       // not a content namespace
+                       array( 'Talk:WikiPageTest_testIsCountable',
+                              'Foo',
+                              'any',
+                              false
+                       ),
+                       array( 'Talk:WikiPageTest_testIsCountable',
+                              'Foo, bar',
+                              'comma',
+                              false
+                       ),
+                       array( 'Talk:WikiPageTest_testIsCountable',
+                              'Foo [[bar]]',
+                              'link',
+                              false
+                       ),
+
+                       // not a content namespace, different model
+                       array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+                              'Foo',
+                              'any',
+                              false
+                       ),
+                       array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+                              'Foo, bar',
+                              'comma',
+                              false
+                       ),
+                       array( 'MediaWiki:WikiPageTest_testIsCountable.js',
+                              'Foo [[bar]]',
+                              'link',
+                              false
+                       ),
+               );
+       }
+
+
+       /**
+        * @dataProvider dataIsCountable
+        */
+       public function testIsCountable( $title, $text, $mode, $expected ) {
+               global $wgArticleCountMethod;
+
+               $old = $wgArticleCountMethod;
+               $wgArticleCountMethod = $mode;
+
+               $page = $this->createPage( $title, $text );
+               $editInfo = $page->prepareContentForEdit( $page->getContent() );
+
+               $v = $page->isCountable();
+               $w = $page->isCountable( $editInfo );
+               $wgArticleCountMethod = $old;
+
+               $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\"" );
+
+               $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\"" );
+       }
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("hello ''world''\n", "<p>hello <i>world</i>\n</p>"),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetParserOutput
+        */
+       public function testGetParserOutput( $text, $expectedHtml ) {
+               $page = $this->createPage( 'WikiPageTest_testGetParserOutput', $text );
+
+               $opt = new ParserOptions();
+               $po = $page->getParserOutput( $opt );
+               $text = $po->getText();
+
+               $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
+
+               $this->assertEquals( $expectedHtml, $text );
+               return $po;
+       }
+
+       static $sections =
+
+               "Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+
+       public function dataReplaceSection() {
+               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"
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataReplaceSection
+        */
+       public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) {
+               $page = $this->createPage( $title, $text );
+               $text = $page->replaceSection( $section, $with, $sectionTitle );
+               $text = trim( $text );
+
+               $this->assertEquals( $expected, $text );
+       }
+
+       /**
+        * @dataProvider dataReplaceSection
+        */
+       public function testReplaceSectionContent( $title, $text, $section, $with, $sectionTitle, $expected ) {
+               $page = $this->createPage( $title, $text );
+
+               $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModelName() );
+               $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
+
+               $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+       }
+
+       /* @FIXME: fix this!
+       public function testGetUndoText() {
+               global $wgDiff3;
+
+               wfSuppressWarnings();
+               $haveDiff3 = $wgDiff3 && file_exists( $wgDiff3 );
+               wfRestoreWarnings();
+
+               if( !$haveDiff3 ) {
+                       $this->markTestSkipped( "diff3 not installed or not found" );
+                       return;
+               }
+
+               $text = "one";
+               $page = $this->createPage( "WikiPageTest_testGetUndoText", $text );
+               $rev1 = $page->getRevision();
+
+               $text .= "\n\ntwo";
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two");
+               $rev2 = $page->getRevision();
+
+               $text .= "\n\nthree";
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three");
+               $rev3 = $page->getRevision();
+
+               $text .= "\n\nfour";
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section four");
+               $rev4 = $page->getRevision();
+
+               $text .= "\n\nfive";
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section five");
+               $rev5 = $page->getRevision();
+
+               $text .= "\n\nsix";
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section six");
+               $rev6 = $page->getRevision();
+
+               $undo6 = $page->getUndoText( $rev6 );
+               if ( $undo6 === false ) $this->fail( "getUndoText failed for rev6" );
+               $this->assertEquals( "one\n\ntwo\n\nthree\n\nfour\n\nfive", $undo6 );
+
+               $undo3 = $page->getUndoText( $rev4, $rev2 );
+               if ( $undo3 === false ) $this->fail( "getUndoText failed for rev4..rev2" );
+               $this->assertEquals( "one\n\ntwo\n\nfive", $undo3 );
+
+               $undo2 = $page->getUndoText( $rev2 );
+               if ( $undo2 === false ) $this->fail( "getUndoText failed for rev2" );
+               $this->assertEquals( "one\n\nfive", $undo2 );
+       }
+       */
+
+       public function testDoRollback() {
+               global $wgUser;
+
+               $text = "one";
+               $page = $this->createPage( "WikiPageTest_testDoRollback", $text );
+
+               $user1 = new User();
+               $user1->setName( "127.0.1.11" );
+               $text .= "\n\ntwo";
+               $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->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three", 0, false, $user2 );
+
+               $wgUser->addGroup( "sysop" ); #XXX: make the test user a sysop...
+               $token = $wgUser->getEditToken( array( $page->getTitle()->getPrefixedText(), $user2->getName() ), null );
+               $errors = $page->doRollback( $user2->getName(), "testing revert", $token, false, $details, $wgUser );
+
+               if ( $errors ) {
+                       $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) );
+               }
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
+       }
+
+       public function dataGetAutosummary( ) {
+               return array(
+                       array(
+                               'Hello there, world!',
+                               '#REDIRECT [[Foo]]',
+                               0,
+                               '/^Redirected page .*Foo/'
+                       ),
+
+                       array(
+                               null,
+                               'Hello world!',
+                               EDIT_NEW,
+                               '/^Created page .*Hello/'
+                       ),
+
+                       array(
+                               'Hello there, world!',
+                               '',
+                               0,
+                               '/^Blanked/'
+                       ),
+
+                       array(
+                               '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.',
+                               'Hello world!',
+                               0,
+                               '/^Replaced .*Hello/'
+                       ),
+
+                       array(
+                               'foo',
+                               'bar',
+                               0,
+                               '/^$/'
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetAutoSummary
+        */
+       public function testGetAutosummary( $old, $new, $flags, $expected ) {
+               $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" );
+       }
+
+       public function dataGetAutoDeleteReason( ) {
+               return array(
+                       array(
+                               array(),
+                               false,
+                               false
+                       ),
+
+                       array(
+                               array(
+                                       array( "first edit", null ),
+                               ),
+                               "/first edit.*only contributor/",
+                               false
+                       ),
+
+                       array(
+                               array(
+                                       array( "first edit", null ),
+                                       array( "second edit", null ),
+                               ),
+                               "/second edit.*only contributor/",
+                               true
+                       ),
+
+                       array(
+                               array(
+                                       array( "first edit", "127.0.2.22" ),
+                                       array( "second edit", "127.0.3.33" ),
+                               ),
+                               "/second edit/",
+                               true
+                       ),
+
+                       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 ),
+                               ),
+                               '/first edit:.*\.\.\."/',
+                               false
+                       ),
+
+                       array(
+                               array(
+                                       array( "first edit", "127.0.2.22" ),
+                                       array( "", "127.0.3.33" ),
+                               ),
+                               "/before blanking.*first edit/",
+                               true
+                       ),
+
+               );
+       }
+
+       /**
+        * @dataProvider dataGetAutoDeleteReason
+        */
+       public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
+               global $wgUser;
+
+               $page = $this->newPage( "WikiPageTest_testGetAutoDeleteReason" );
+
+               $c = 1;
+
+               foreach ( $edits as $edit ) {
+                       $user = new User();
+
+                       if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
+                       else $user = $wgUser;
+
+                       $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModelName() );
+
+                       $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" );
+
+               $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
+       }
+
+       public function dataPreSaveTransform() {
+               return array(
+                       array( 'hello this is ~~~',
+                              "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+                       ),
+                       array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                              'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataPreSaveTransform
+        */
+       public function testPreSaveTransform( $text, $expected ) {
+               $page = $this->newPage( "WikiPageTest_testPreloadTransform" );
+               $text = $page->preSaveTransform( $text );
+
+               $this->assertEquals( $expected, $text );
+       }
+
+}
+
diff --git a/tests/phpunit/includes/WikitextContentHandlerTest.php b/tests/phpunit/includes/WikitextContentHandlerTest.php
new file mode 100644 (file)
index 0000000..7edb642
--- /dev/null
@@ -0,0 +1,184 @@
+<?php
+
+class WikitextContentHandlerTest extends MediaWikiTestCase {
+
+       /**
+        * @var ContentHandler
+        */
+       var $handler;
+
+       public function setup() {
+               $this->handler = ContentHandler::getForModelName( CONTENT_MODEL_WIKITEXT );
+       }
+
+       public function teardown() {
+       }
+
+       public function testSerializeContent( ) {
+               $content = new WikitextContent( 'hello world' );
+
+               $this->assertEquals( 'hello world', $this->handler->serializeContent( $content ) );
+               $this->assertEquals( 'hello world', $this->handler->serializeContent( $content, 'application/x-wiki' ) );
+
+               try {
+                       $this->handler->serializeContent( $content, 'dummy/foo' );
+                       $this->fail( "serializeContent() should have failed on unknown format" );
+               } catch ( MWException $e ) {
+                       // ok, as expected
+               }
+       }
+
+       public function testUnserializeContent( ) {
+               $content = $this->handler->unserializeContent( 'hello world' );
+               $this->assertEquals( 'hello world', $content->getNativeData() );
+
+               $content = $this->handler->unserializeContent( 'hello world', 'application/x-wiki' );
+               $this->assertEquals( 'hello world', $content->getNativeData() );
+
+               try {
+                       $this->handler->unserializeContent( 'hello world', 'dummy/foo' );
+                       $this->fail( "unserializeContent() should have failed on unknown format" );
+               } catch ( MWException $e ) {
+                       // ok, as expected
+               }
+       }
+
+       public function testMakeEmptyContent() {
+               $content = $this->handler->makeEmptyContent();
+
+               $this->assertTrue( $content->isEmpty() );
+               $this->assertEquals( '', $content->getNativeData() );
+       }
+
+       public function dataIsSupportedFormat( ) {
+               return array(
+                       array( null, true ),
+                       array( 'application/x-wiki', true ),
+                       array( 'dummy/foo', false ),
+               );
+       }
+
+       /**
+        * @dataProvider dataIsSupportedFormat
+        */
+       public function testIsSupportedFormat( $format, $supported ) {
+               $this->assertEquals( $supported, $this->handler->isSupportedFormat( $format ) );
+       }
+
+       public function dataMerge3( ) {
+               return array(
+                       array( "first paragraph
+
+                                       second paragraph\n",
+
+                                       "FIRST paragraph
+
+                                       second paragraph\n",
+
+                                       "first paragraph
+
+                                       SECOND paragraph\n",
+
+                                       "FIRST paragraph
+
+                                       SECOND paragraph\n",
+                       ),
+
+                       array( "first paragraph
+                                       second paragraph\n",
+
+                                  "Bla bla\n",
+
+                                  "Blubberdibla\n",
+
+                                  false,
+                       ),
+
+               );
+       }
+
+       /**
+        * @dataProvider dataMerge3
+        */
+       public function testMerge3( $old, $mine, $yours, $expected ) {
+               $oldContent = new WikitextContent( $old );
+               $myContent = new WikitextContent( $mine );
+               $yourContent = new WikitextContent( $yours );
+
+               $merged = $this->handler->merge3( $oldContent, $myContent, $yourContent );
+
+               $this->assertEquals( $expected, $merged ? $merged->getNativeData() : $merged );
+       }
+
+       public function dataGetAutosummary( ) {
+               return array(
+                       array(
+                               'Hello there, world!',
+                               '#REDIRECT [[Foo]]',
+                               0,
+                               '/^Redirected page .*Foo/'
+                       ),
+
+                       array(
+                               null,
+                               'Hello world!',
+                               EDIT_NEW,
+                               '/^Created page .*Hello/'
+                       ),
+
+                       array(
+                               'Hello there, world!',
+                               '',
+                               0,
+                               '/^Blanked/'
+                       ),
+
+                       array(
+                               '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.',
+                               'Hello world!',
+                               0,
+                               '/^Replaced .*Hello/'
+                       ),
+
+                       array(
+                               'foo',
+                               'bar',
+                               0,
+                               '/^$/'
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetAutoSummary
+        */
+       public function testGetAutosummary( $old, $new, $flags, $expected ) {
+               global $wgLanguageCode, $wgContLang;
+
+               $oldContent = is_null( $old ) ? null : new WikitextContent( $old );
+               $newContent = is_null( $new ) ? null : new WikitextContent( $new );
+
+               $summary = $this->handler->getAutosummary( $oldContent, $newContent, $flags );
+
+               $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" );
+       }
+
+       /**
+        * @todo Text case required database!
+        */
+       /*
+       public function testGetAutoDeleteReason( Title $title, &$hasHistory ) {
+       }
+       */
+
+       /**
+        * @todo Text case required database!
+        */
+       /*
+       public function testGetUndoContent( Revision $current, Revision $undo, Revision $undoafter = null ) {
+       }
+       */
+
+}
diff --git a/tests/phpunit/includes/WikitextContentTest.php b/tests/phpunit/includes/WikitextContentTest.php
new file mode 100644 (file)
index 0000000..96eea7a
--- /dev/null
@@ -0,0 +1,412 @@
+<?php
+
+class WikitextContentTest extends MediaWikiTestCase {
+
+       public function setup() {
+               $this->context = new RequestContext( new FauxRequest() );
+               $this->context->setTitle( Title::newFromText( "Test" ) );
+       }
+
+       public function newContent( $text ) {
+               return new WikitextContent( $text );
+       }
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("hello ''world''\n", "<p>hello <i>world</i>\n</p>"),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetParserOutput
+        */
+       public function testGetParserOutput( $text, $expectedHtml ) {
+               $content = $this->newContent( $text );
+
+               $po = $content->getParserOutput( $this->context );
+
+               $this->assertEquals( $expectedHtml, $po->getText() );
+               return $po;
+       }
+
+       static $sections =
+
+"Intro
+
+== stuff ==
+hello world
+
+== test ==
+just a test
+
+== foo ==
+more stuff
+";
+
+       public function dataGetSection() {
+               return array(
+                       array( WikitextContentTest::$sections,
+                                       "0",
+                                       "Intro"
+                       ),
+                       array( WikitextContentTest::$sections,
+                                       "2",
+"== test ==
+just a test"
+                       ),
+                       array( WikitextContentTest::$sections,
+                                       "8",
+                                       false
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetSection
+        */
+       public function testGetSection( $text, $sectionId, $expectedText ) {
+               $content = $this->newContent( $text );
+
+               $sectionContent = $content->getSection( $sectionId );
+
+               $this->assertEquals( $expectedText, is_null( $sectionContent ) ? null : $sectionContent->getNativeData() );
+       }
+
+       public function dataReplaceSection() {
+               return array(
+                       array( WikitextContentTest::$sections,
+                              "0",
+                              "No more",
+                              null,
+                              trim( preg_replace( '/^Intro/sm', 'No more', WikitextContentTest::$sections ) )
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "",
+                              "No more",
+                              null,
+                              "No more"
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "2",
+                              "== TEST ==\nmore fun",
+                              null,
+                              trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikitextContentTest::$sections ) )
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "8",
+                              "No more",
+                              null,
+                              WikitextContentTest::$sections
+                       ),
+                       array( WikitextContentTest::$sections,
+                              "new",
+                              "No more",
+                              "New",
+                              trim( WikitextContentTest::$sections ) . "\n\n\n== New ==\n\nNo more"
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataReplaceSection
+        */
+       public function testReplaceSection( $text, $section, $with, $sectionTitle, $expected ) {
+               $content = $this->newContent( $text );
+               $c = $content->replaceSection( $section, $this->newContent( $with ), $sectionTitle );
+
+               $this->assertEquals( $expected, is_null( $c ) ? null : $c->getNativeData() );
+       }
+
+       public function testAddSectionHeader( ) {
+               $content = $this->newContent( 'hello world' );
+               $content = $content->addSectionHeader( 'test' );
+
+               $this->assertEquals( "== test ==\n\nhello world", $content->getNativeData() );
+       }
+
+       public function dataPreSaveTransform() {
+               return array(
+                       array( 'hello this is ~~~',
+                              "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
+                       ),
+                       array( 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                              'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataPreSaveTransform
+        */
+       public function testPreSaveTransform( $text, $expected ) {
+               $content = $this->newContent( $text );
+               $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser() );
+
+               $this->assertEquals( $expected, $content->getNativeData() );
+       }
+
+       public function dataPreloadTransform() {
+               return array(
+                       array( 'hello this is ~~~',
+                              "hello this is ~~~",
+                       ),
+                       array( 'hello \'\'this\'\' is <noinclude>foo</noinclude><includeonly>bar</includeonly>',
+                              'hello \'\'this\'\' is bar',
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataPreloadTransform
+        */
+       public function testPreloadTransform( $text, $expected ) {
+               $content = $this->newContent( $text );
+               $content = $content->preloadTransform( $this->context->getTitle() );
+
+               $this->assertEquals( $expected, $content->getNativeData() );
+       }
+
+       public function dataGetRedirectTarget() {
+               return array(
+                       array( '#REDIRECT [[Test]]',
+                              'Test',
+                       ),
+                       array( '#REDIRECT Test',
+                              null,
+                       ),
+                       array( '* #REDIRECT [[Test]]',
+                              null,
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetRedirectTarget
+        */
+       public function testGetRedirectTarget( $text, $expected ) {
+               $content = $this->newContent( $text );
+               $t = $content->getRedirectTarget( );
+
+               if ( is_null( $expected ) ) $this->assertNull( $t, "text should not have generated a redirect target: $text" );
+               else $this->assertEquals( $expected, $t->getPrefixedText() );
+       }
+
+       /**
+        * @dataProvider dataGetRedirectTarget
+        */
+       public function isRedirect( $text, $expected ) {
+               $content = $this->newContent( $text );
+
+               $this->assertEquals( !is_null($expected), $content->isRedirect() );
+       }
+
+
+       /**
+        * @todo: test needs database!
+        */
+       /*
+       public function getRedirectChain() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectArray( $text );
+       }
+       */
+
+       /**
+        * @todo: test needs database!
+        */
+       /*
+       public function getUltimateRedirectTarget() {
+               $text = $this->getNativeData();
+               return Title::newFromRedirectRecurse( $text );
+       }
+       */
+
+
+       public function dataIsCountable() {
+               return array(
+                       array( '',
+                              null,
+                              'any',
+                              true
+                       ),
+                       array( 'Foo',
+                              null,
+                              'any',
+                              true
+                       ),
+                       array( 'Foo',
+                              null,
+                              'comma',
+                              false
+                       ),
+                       array( 'Foo, bar',
+                              null,
+                              'comma',
+                              true
+                       ),
+                       array( 'Foo',
+                              null,
+                              'link',
+                              false
+                       ),
+                       array( 'Foo [[bar]]',
+                              null,
+                              'link',
+                              true
+                       ),
+                       array( 'Foo',
+                              true,
+                              'link',
+                              true
+                       ),
+                       array( 'Foo [[bar]]',
+                              false,
+                              'link',
+                              false
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'any',
+                              false
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'comma',
+                              false
+                       ),
+                       array( '#REDIRECT [[bar]]',
+                              true,
+                              'link',
+                              false
+                       ),
+               );
+       }
+
+
+       /**
+        * @dataProvider dataIsCountable
+        */
+       public function testIsCountable( $text, $hasLinks, $mode, $expected ) {
+               global $wgArticleCountMethod;
+
+               $old = $wgArticleCountMethod;
+               $wgArticleCountMethod = $mode;
+
+               $content = $this->newContent( $text );
+
+               $v = $content->isCountable( $hasLinks, $this->context );
+               $wgArticleCountMethod = $old;
+
+               $this->assertEquals( $expected, $v, "isCountable() returned unexpected value " . var_export( $v, true )
+                                                   . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
+       }
+
+       public function dataGetTextForSummary() {
+               return array(
+                       array( "hello\nworld.",
+                              16,
+                              'hello world.',
+                       ),
+                       array( 'hello world.',
+                              8,
+                              'hello...',
+                       ),
+                       array( '[[hello world]].',
+                              8,
+                              'hel...',
+                       ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetTextForSummary
+        */
+       public function testGetTextForSummary( $text, $maxlength, $expected ) {
+               $content = $this->newContent( $text );
+
+               $this->assertEquals( $expected, $content->getTextForSummary( $maxlength ) );
+       }
+
+
+       public function testGetTextForSearchIndex( ) {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( "hello world.", $content->getTextForSearchIndex() );
+       }
+
+       public function testCopy() {
+               $content = $this->newContent( "hello world." );
+               $copy = $content->copy();
+
+               $this->assertTrue( $content->equals( $copy ), "copy must be equal to original" );
+               $this->assertEquals( "hello world.", $copy->getNativeData() );
+       }
+
+       public function testGetSize( ) {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( 12, $content->getSize() );
+       }
+
+       public function testGetNativeData( ) {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( "hello world.", $content->getNativeData() );
+       }
+
+       public function testGetWikitextForTransclusion( ) {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( "hello world.", $content->getWikitextForTransclusion() );
+       }
+
+       # =================================================================================================================
+
+       public function getModelName() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModelName() );
+       }
+
+       public function getContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelName() );
+       }
+
+       public function dataIsEmpty( ) {
+               return array(
+                       array( '', true ),
+                       array( '  ', false ),
+                       array( '0', false ),
+                       array( 'hallo welt.', false ),
+               );
+       }
+
+       /**
+        * @dataProvider dataIsEmpty
+        */
+       public function testIsEmpty( $text, $empty ) {
+               $content = $this->newContent( $text );
+
+               $this->assertEquals( $empty, $content->isEmpty() );
+       }
+
+       public function dataEquals( ) {
+               return array(
+                       array( new WikitextContent( "hallo" ), null, false ),
+                       array( new WikitextContent( "hallo" ), new WikitextContent( "hallo" ), true ),
+                       array( new WikitextContent( "hallo" ), new JavascriptContent( "hallo" ), false ),
+                       array( new WikitextContent( "hallo" ), new WikitextContent( "HALLO" ), false ),
+               );
+       }
+
+       /**
+        * @dataProvider dataEquals
+        */
+       public function testEquals( Content $a, Content $b = null, $equal = false ) {
+               $this->assertEquals( $equal, $a->equals( $b ) );
+       }
+
+}
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)