Merge branch 'master' of ssh://gerrit.wikimedia.org:29418/mediawiki/core into Wikidata
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 11 Jun 2012 15:45:59 +0000 (17:45 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Mon, 11 Jun 2012 15:45:59 +0000 (17:45 +0200)
98 files changed:
.gitreview
bin/svnstat [changed mode: 0755->0644]
bin/ulimit-tvf.sh [changed mode: 0755->0644]
bin/ulimit4.sh [changed mode: 0755->0644]
docs/hooks.txt
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/Message.php
includes/Namespace.php
includes/Revision.php
includes/Title.php
includes/WikiFilePage.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/cache/LinkCache.php
includes/cache/MessageCache.php
includes/diff/DairikiDiff.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/installer/Ibm_db2Updater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/SqliteUpdater.php
includes/job/DoubleRedirectJob.php
includes/job/RefreshLinksJob.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/specials/SpecialBooksources.php
includes/specials/SpecialComparePages.php
includes/specials/SpecialNewpages.php
includes/specials/SpecialUndelete.php
languages/Language.php
languages/LanguageConverter.php
languages/classes/LanguageFi.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.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/checkBadRedirects.php
maintenance/cleanupSpam.php
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/populateRevisionSha1.php
maintenance/refreshLinks.php
maintenance/storage/make-blobs [changed mode: 0755->0644]
maintenance/storage/testCompression.php
maintenance/tables.sql
tests/phpunit/MediaWikiPHPUnitCommand.php
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/RevisionStorageTest.php
tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/WikiPageTest.php
tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.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/includes/filerepo/FileBackendTest.php
tests/phpunit/install-phpunit.sh [changed mode: 0755->0644]
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/maintenance/backupPrefetchTest.php
tests/phpunit/maintenance/backupTextPassTest.php
tests/phpunit/phpunit.php [changed mode: 0755->0644]
tests/phpunit/suite.xml

index 0ec44b8..3b2aa76 100644 (file)
@@ -2,5 +2,5 @@
 host=gerrit.wikimedia.org
 port=29418
 project=mediawiki/core.git
-defaultbranch=master
+defaultbranch=Wikidata
 defaultrebase=0
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 8d4bdea..405ee3d 100644 (file)
@@ -401,9 +401,14 @@ token types.
 used to retrieve this type of tokens.
 
 'ArticleAfterFetchContent': after fetching content of an article from
+the database. DEPRECATED, use ArticleAfterFetchContentObject instead.
+$article: the article (object) being loaded from the database
+&$content: the content (string) of the article
+
+'ArticleAfterFetchContentObject': after fetching content of an article from
 the database
 $article: the article (object) being loaded from the database
-$content: the content (string) of the article
+&$content: the content of the article, as a Content object
 
 'ArticleConfirmDelete': before writing the confirmation form for article
        deletion
@@ -449,7 +454,7 @@ Wiki::articleFromTitle()
 $title: title (object) used to create the article object
 $article: article (object) that will be returned
 
-'ArticleInsertComplete': After a new article is created
+'ArticleInsertComplete': After a new article is created. DEPRECATED, use ArticleContentInsertComplete
 $article: WikiPage created
 $user: User creating the article
 $text: New content
@@ -460,6 +465,17 @@ $section: (No longer used)
 $flags: Flags passed to Article::doEdit()
 $revision: New Revision of the article
 
+'ArticleContentInsertComplete': After a new article is created
+$article: WikiPage created
+$user: User creating the article
+$content: New content as a Content object
+$summary: Edit summary/comment
+$isMinor: Whether or not the edit was marked as minor
+$isWatch: (No longer used)
+$section: (No longer used)
+$flags: Flags passed to Article::doEdit()
+$revision: New Revision of the article
+
 'ArticleMergeComplete': after merging to article using Special:Mergehistory
 $targetTitle: target title (object)
 $destTitle: destination title (object)
@@ -508,7 +524,7 @@ $user: the user who did the rollback
 $revision: the revision the page was reverted back to
 $current: the reverted revision
 
-'ArticleSave': before an article is saved
+'ArticleSave': before an article is saved. DEPRECATED, use ArticleContentSave instead
 $article: the WikiPage (object) being saved
 $user: the user (object) saving the article
 $text: the new article text
@@ -517,7 +533,16 @@ $isminor: minor flag
 $iswatch: watch flag
 $section: section #
 
-'ArticleSaveComplete': After an article has been updated
+'ArticleContentSave': before an article is saved.
+$article: the WikiPage (object) being saved
+$user: the user (object) saving the article
+$content: the new article content, as a Content object
+$summary: the article summary (comment)
+$isminor: minor flag
+$iswatch: watch flag
+$section: section #
+
+'ArticleSaveComplete': After an article has been updated. DEPRECATED, use ArticleContentSaveComplete instead.
 $article: WikiPage modified
 $user: User performing the modification
 $text: New content
@@ -530,6 +555,19 @@ $revision: New Revision of the article
 $status: Status object about to be returned by doEdit()
 $baseRevId: the rev ID (or false) this edit was based on
 
+'ArticleContentSaveComplete': After an article has been updated
+$article: WikiPage modified
+$user: User performing the modification
+$content: New content, as a Content object
+$summary: Edit summary/comment
+$isMinor: Whether or not the edit was marked as minor
+$isWatch: (No longer used)
+$section: (No longer used)
+$flags: Flags passed to Article::doEdit()
+$revision: New Revision of the article
+$status: Status object about to be returned by doEdit()
+$baseRevId: the rev ID (or false) this edit was based on
+
 'ArticleUndelete': When one or more revisions of an article are restored
 $title: Title corresponding to the article restored
 $create: Whether or not the restoration caused the page to be created
@@ -556,11 +594,19 @@ object to both indicate that the output is done and what parser output was used.
 follwed an redirect
 $article: target article (object)
 
-'ArticleViewCustom': allows to output the text of the article in a different format than wikitext
+'ArticleViewCustom': allows to output the text of the article in a different format than wikitext.
+DEPRECATED, use ArticleContentViewCustom instead.
+Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
 $text: text of the page
 $title: title of the page
 $output: reference to $wgOut
 
+'ArticleContentViewCustom': allows to output the text of the article in a different format than wikitext.
+Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
+$content: content of the page, as a Content object
+$title: title of the page
+$output: reference to $wgOut
+
 'AuthPluginAutoCreate': Called when creating a local account for an user logged
 in from an external authentication method
 $user: User object created locally
@@ -688,6 +734,16 @@ the collation given in $collationName.
 'ConfirmEmailComplete': Called after a user's email has been confirmed successfully
 $user: user (object) whose email is being confirmed
 
+'ContentHandlerDefaultModelFor': Called when the default content model is determiend
+for a given title. May be used to assign a different model for that title.
+$title: the Title in question
+&$model: the model name. Use with CONTENT_MODEL_XXX constants.
+
+'ContentHandlerForModelID': Called when a ContentHandler is requested for a given
+cointent model name, but no entry for that model exists in $wgContentHandlers.
+$modeName: the requested content model name
+&$handler: set this to a ContentHandler object, if desired.
+
 'ContribsPager::getQueryInfo': Before the contributions query is about to run
 &$pager: Pager object for contributions
 &$queryInfo: The query for the contribs Pager
@@ -753,12 +809,19 @@ $section: Section being edited
 &$error: Error message to return
 $summary: Edit summary for page
 
-'EditFilterMerged': Post-section-merge edit filter
+'EditFilterMerged': Post-section-merge edit filter.
+DEPRECATED, use EditFilterMergedContent instead.
 $editor: EditPage instance (object)
 $text: content of the edit box
 &$error: error message to return
 $summary: Edit summary for page
 
+'EditFilterMergedContent': Post-section-merge edit filter
+$editor: EditPage instance (object)
+$content: content of the edit box, as a Content object
+&$error: error message to return
+$summary: Edit summary for page
+
 'EditFormPreloadText': Allows population of the edit form when creating
 new pages
 &$text: Text to preload with
@@ -821,14 +884,28 @@ $title: title of page being edited
 &$msg: localization message name, overridable. Default is either 'copyrightwarning' or 'copyrightwarning2'
 
 'EditPageGetDiffText': Allow modifying the wikitext that will be used in
-"Show changes"
+"Show changes". DEPRECATED. Use EditPageGetDiffContent instead.
+Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
 $editPage: EditPage object
 &$newtext: wikitext that will be used as "your version"
 
-'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed
+'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
+"Show changes".
+Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
+$editPage: EditPage object
+&$newtext: wikitext that will be used as "your version"
+
+'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed.
+DEPRECATED. Use EditPageGetPreviewContent instead.
+Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
 $editPage: EditPage object
 &$toparse: wikitext that will be parsed
 
+'EditPageGetPreviewContent': Allow modifying the wikitext that will be previewed.
+Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
+$editPage: EditPage object
+&$content: Content object to be previewed (may be replaced by hook function)
+
 'EditPageNoSuchSection': When a section edit request is given for an non-existent section
 &$editpage: The current EditPage object
 &$res: the HTML of the error text
@@ -1652,7 +1729,8 @@ $query : Original query.
 'ShowMissingArticle': Called when generating the output for a non-existent page
 $article: The article object corresponding to the page
 
-'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views
+'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views.
+DEPRECATED, use the ContentHandler facility to handle CSS and JavaScript!
 $text: Text being shown
 $title: Title of the custom script/stylesheet page
 $output: Current OutputPage object
@@ -2263,6 +2341,13 @@ One, and only one hook should set this, and return false.
 &$opts: Options to use for the query
 &$join: Join conditions
 
+'WikiPageDeletionUpdates': manipulate the list of DataUpdates to be applied when
+       a page is deleted. Called in WikiPage::getDeletionUpdates().
+       Note that updates specific to a content model should be provided by the
+       respective ContentHandler's getDeletionUpdates() method.
+$page: the WikiPage
+&$updates: the array of DataUpdate objects. Hook function may want to add to it.
+
 'wfShellWikiCmd': Called when generating a shell-escaped command line
        string to run a MediaWiki cli script.
 &$script: MediaWiki cli script path
index fdf0820..b252920 100644 (file)
@@ -57,10 +57,17 @@ class Article extends Page {
        public $mParserOptions;
 
        /**
-        * Content of the revision we are working on
+        * Text of the revision we are working on
         * @var string $mContent
         */
-       var $mContent;                    // !<
+       var $mContent;                    // !< #BC cruft
+
+       /**
+        * Content of the revision we are working on
+        * @var Content
+        * @since 1.WD
+        */
+       var $mContentObject;              // !<
 
        /**
         * Is the content ($mContent) already loaded?
@@ -231,9 +238,35 @@ class Article extends Page {
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
+        * @deprecated in 1.WD; use getContentObject() instead
+        *
         * @return string Return the text of this revision
         */
        public function getContent() {
+               wfDeprecated( __METHOD__, '1.WD' );
+               $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 Return the content of this revision
+        *
+        * @since 1.WD
+        *
+        * @todo: FIXME: this should really be protected, all callers should be changed to use WikiPage::getContent() instead.
+        */
+       public function getContentObject() {
+               global $wgUser;
                wfProfileIn( __METHOD__ );
 
                if ( $this->mPage->getID() === 0 ) {
@@ -244,17 +277,19 @@ class Article extends Page {
                                if ( $text === false ) {
                                        $text = '';
                                }
+
+                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
-                               $text = wfMsgExt( $this->getContext()->getUser()->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;
                }
        }
 
@@ -335,16 +370,54 @@ class Article extends Page {
         * Get text of an article from database
         * Does *NOT* follow redirects.
         *
+        * @protected
+        * @note this is really internal functionality that should really NOT be used by other functions. For accessing
+        *       article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
+        *       uses this method to retrieve page text from the database, so the function has to remain public for now.
+        *
         * @return mixed string containing article contents, or false if null
+        * @deprecated in 1.WD, use WikiPage::getContent() instead
         */
-       function fetchContent() {
-               if ( $this->mContentLoaded ) {
+       function fetchContent() { #BC cruft!
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
 
                wfProfileIn( __METHOD__ );
 
+               $content = $this->fetchContentObject();
+
+               $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
+               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft! #XXX: can we deprecate that hook?
+
+               wfProfileOut( __METHOD__ );
+
+               return $this->mContent;
+       }
+
+
+       /**
+        * Get text content object
+        * Does *NOT* follow redirects.
+        * TODO: when is this null?
+        *
+        * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
+        *
+        * @return Content|null
+        *
+        * @since 1.WD
+        */
+       protected function fetchContentObject() {
+               if ( $this->mContentLoaded ) {
+                       return $this->mContentObject;
+               }
+
+               wfProfileIn( __METHOD__ );
+
                $this->mContentLoaded = true;
+               $this->mContent = null;
 
                $oldid = $this->getOldID();
 
@@ -352,7 +425,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()
@@ -372,6 +445,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__ );
@@ -381,14 +455,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 ) );
 
                wfProfileOut( __METHOD__ );
 
-               return $this->mContent;
+               return $this->mContentObject;
        }
 
        /**
@@ -421,7 +495,7 @@ class Article extends Page {
         * @return Revision|null
         */
        public function getRevisionFetched() {
-               $this->fetchContent();
+               $this->fetchContentObject();
 
                return $this->mRevision;
        }
@@ -581,7 +655,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 ) {
@@ -605,18 +679,21 @@ class Article extends Page {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
-                                       } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
+                                       } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
+                                               # Allow extensions do their own custom view for certain pages
+                                               $outputDone = true;
+                                       } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $outputPage ) ) ) { #FIXME: fetchContent() is deprecated!
                                                # 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)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
-                                                       $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
+                                                       $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
@@ -626,8 +703,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();
@@ -720,7 +798,9 @@ class Article extends Page {
                $unhide = $request->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 );
@@ -738,22 +818,21 @@ class Article extends Page {
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
-       protected function showCssOrJsPage() {
-               $dir = $this->getContext()->getLanguage()->getDir();
-               $lang = $this->getContext()->getLanguage()->getCode();
+       protected function showCssOrJsPage( $showCacheHint = true ) {
+               global $wgOut;
 
-               $outputPage = $this->getContext()->getOutput();
-               $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
-                       'clearyourcache' );
+               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' );
+               }
 
                // Give hooks a chance to customise the output
-               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
-                       // Wrap the whole lot in a <pre> and don't parse
-                       $m = array();
-                       preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
-                       $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                       $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
-                       $outputPage->addHTML( "\n</pre>\n" );
+               if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
+                       $po = $this->mContentObject->getParserOutput( $this->getTitle() );
+                       $wgOut->addHTML( $po->getText() );
                }
        }
 
@@ -1383,7 +1462,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
@@ -1890,7 +1975,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 ) ****** //
@@ -1928,6 +2015,7 @@ class Article extends Page {
         * @param $newtext
         * @param $flags
         * @return string
+        * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
index 78ed450..ab48881 100644 (file)
@@ -280,6 +280,19 @@ $wgAutoloadLocalClasses = array(
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
 
+    # content handler
+    'Content' => 'includes/Content.php',
+    'AbstractContent' => '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',
@@ -322,6 +335,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',
@@ -998,6 +1012,14 @@ $wgAutoloadLocalClasses = array(
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
 
+       # tests/phpunit
+       'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
+       'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
+       '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..5965753
--- /dev/null
@@ -0,0 +1,1142 @@
+<?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.
+ *
+ * @since 1.WD
+ */
+interface Content {
+
+       /**
+        * @since WD.1
+        *
+        * @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.
+        *
+        * @todo: test that this actually works
+        * @todo: make sure this also works with LuceneSearch / WikiSearch
+        */
+       public function getTextForSearchIndex( );
+
+       /**
+        * @since WD.1
+        *
+        * @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: allow transclusion into other content models than Wikitext!
+        * @TODO: used in WikiPage and MessageCache to get message text. Not so nice. What should we use instead?!
+        */
+       public function getWikitextForTransclusion( );
+
+       /**
+        * Returns a textual representation of the content suitable for use in edit summaries and log messages.
+        *
+        * @since WD.1
+        *
+        * @param int $maxlength maximum length of the summary text
+        * @return String the summary text
+        */
+       public function getTextForSummary( $maxlength = 250 );
+
+       /**
+        * Returns native representation of the data. Interpretation depends on the data model used,
+        * as given by getDataModel().
+        *
+        * @since WD.1
+        *
+        * @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 function getNativeData( );
+
+       /**
+        * returns the content's nominal size in bogo-bytes.
+        *
+        * @return int
+        */
+       public function getSize( );
+
+       /**
+        * Returns the id of the content model used by this content objects.
+        * Corresponds to the CONTENT_MODEL_XXX constants.
+        *
+        * @since WD.1
+        *
+        * @return int the model id
+        */
+       public function getModel();
+
+       /**
+        * Convenience method that returns the ContentHandler singleton for handling the content
+        * model this Content object uses.
+        *
+        * Shorthand for ContentHandler::getForContent( $this )
+        *
+        * @since WD.1
+        *
+        * @return ContentHandler
+        */
+       public function getContentHandler();
+
+       /**
+        * Convenience method that returns the default serialization format for the content model
+        * model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getDefaultFormat()
+        *
+        * @since WD.1
+        *
+        * @return ContentHandler
+        */
+       public function getDefaultFormat();
+
+       /**
+        * Convenience method that returns the list of serialization formats supported
+        * for the content model model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getSupportedFormats()
+        *
+        * @since WD.1
+        *
+        * @return array of supported serialization formats
+        */
+       public function getSupportedFormats();
+
+       /**
+        * Returns true if $format is a supported serialization format for this Content object,
+        * false if it isn't.
+        *
+        * Note that this should always return true if $format is null, because null stands for the
+        * default serialization.
+        *
+        * Shorthand for $this->getContentHandler()->isSupportedFormat( $format )
+        *
+        * @since WD.1
+        *
+        * @param String $format the format to check
+        * @return bool whether the format is supported
+        */
+       public function isSupportedFormat( $format );
+
+       /**
+        * Convenience method for serializing this Content object.
+        *
+        * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
+        *
+        * @since WD.1
+        *
+        * @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 );
+
+       /**
+        * Returns true if this Content object represents empty content.
+        *
+        * @since WD.1
+        *
+        * @return bool whether this Content object is empty
+        */
+       public function isEmpty();
+
+       /**
+        * Returns whether the content is valid. This is intended for local validity checks, not considering global consistency.
+        * Content needs to be valid before it can be saved.
+        *
+        * This default implementation always returns true.
+        *
+        * @since WD.1
+        *
+        * @return boolean
+        */
+       public function isValid();
+
+       /**
+        * Returns true if this Content objects is conceptually equivalent to the given Content object.
+        * Contract:
+        *
+        * * Will return false if $that is null.
+        * * Will return true if $that === $this.
+        * * Will return false if $that->getModelName() != $this->getModel().
+        * * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
+        *   where the meaning of "equal" depends on the actual data model.
+        *
+        * Implementations should be careful to make equals() transitive and reflexive:
+        *
+        * * $a->equals( $b ) <=> $b->equals( $a )
+        * * $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
+        *
+        * @since WD.1
+        *
+        * @param Content $that the Content object to compare to
+        * @return bool true if this Content object is equal to $that, false otherwise.
+        */
+       public function equals( Content $that = null );
+
+       /**
+        * Return a copy of this Content object. The following must be true for the object returned
+        * if $copy = $original->copy()
+        *
+        * * get_class($original) === get_class($copy)
+        * * $original->getModel() === $copy->getModel()
+        * * $original->equals( $copy )
+        *
+        * If and only if the Content object is immutable, the copy() method can and should
+        * return $this. That is,  $copy === $original may be true, but only for immutable content
+        * objects.
+        *
+        * @since WD.1
+        *
+        * @return Content. A copy of this object
+        */
+       public 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).
+        *
+        * @since WD.1
+        *
+        * @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 ) ;
+
+       /**
+        * Convenience method, shorthand for
+        * $this->getContentHandler()->getParserOutput( $this, $title, $revId, $options, $generateHtml )
+        *
+        * @note: subclasses should NOT override this to provide custom rendering.
+        *        Override ContentHandler::getParserOutput() instead!
+        *
+        * @param Title $title
+        * @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.
+        *
+        * @since WD.1
+        *
+        * @return ParserOutput
+        */
+       public function getParserOutput( Title $title, $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).
+        *
+        * @since WD.1
+        *
+        * @return Array of Titles, with the destination last
+        */
+       public function getRedirectChain();
+
+       /**
+        * Construct the redirect destination from this content and return a Title,
+        * 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.
+        *
+        * @since WD.1
+        *
+        * @return Title: The corresponding Title
+        */
+       public function getRedirectTarget();
+
+       /**
+        * 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.
+        *
+        * @since WD.1
+        *
+        * @return Title
+        */
+       public function getUltimateRedirectTarget();
+
+       /**
+        * Returns whether this Content represents a redirect.
+        * Shorthand for getRedirectTarget() !== null.
+        *
+        * @since WD.1
+        *
+        * @return bool
+        */
+       public function isRedirect();
+
+       /**
+        * Returns the section with the given id.
+        *
+        * @since WD.1
+        *
+        * @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 (included) 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 );
+
+       /**
+        * Replaces a section of the content and returns a Content object with the section replaced.
+        *
+        * @since WD.1
+        *
+        * @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 = ''  );
+
+       /**
+        * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+        *
+        * @since WD.1
+        *
+        * @param Title $title
+        * @param User $user
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts );
+
+       /**
+        * 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.
+        *
+        * @since WD.1
+        *
+        * @param $header String
+        * @return Content
+        */
+       public function addSectionHeader( $header );
+
+       /**
+        * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+        *
+        * @since WD.1
+        *
+        * @param Title $title
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts );
+
+       # 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
+
+// @TODO: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API
+}
+
+
+/**
+ * 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.
+ *
+ * @since 1.WD
+ */
+abstract class AbstractContent implements Content {
+
+       /**
+        * Name of the content model this Content object represents.
+        * Use with CONTENT_MODEL_XXX constants
+        *
+        * @var String $model_id
+        */
+       protected $model_id;
+
+       /**
+        * @param int $model_id
+        */
+       public function __construct( $model_id = null ) {
+               $this->model_id = $model_id;
+       }
+
+       /**
+        * Returns the id of the content model used by this content objects.
+        * Corresponds to the CONTENT_MODEL_XXX constants.
+        *
+        * @since WD.1
+        *
+        * @return int the model id
+        */
+       public function getModel() {
+               return $this->model_id;
+       }
+
+       /**
+        * Throws an MWException if $model_id is not the id of the content model
+        * supported by this Content object.
+        *
+        * @param int $model_id the model to check
+        *
+        * @throws MWException
+        */
+       protected function checkModelID( $model_id ) {
+               if ( $model_id !== $this->model_id ) {
+                       $model_name = ContentHandler::getContentModelName( $model_id );
+                       $own_model_name = ContentHandler::getContentModelName( $this->model_id );
+
+                       throw new MWException( "Bad content model: expected {$this->model_id} ($own_model_name) but got found $model_id ($model_name)." );
+               }
+       }
+
+       /**
+        * Convenience method that returns the ContentHandler singleton for handling the content
+        * model this Content object uses.
+        *
+        * Shorthand for ContentHandler::getForContent( $this )
+        *
+        * @since WD.1
+        *
+        * @return ContentHandler
+        */
+       public function getContentHandler() {
+               return ContentHandler::getForContent( $this );
+       }
+
+       /**
+        * Convenience method that returns the default serialization format for the content model
+        * model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getDefaultFormat()
+        *
+        * @since WD.1
+        *
+        * @return ContentHandler
+        */
+       public function getDefaultFormat() {
+               return $this->getContentHandler()->getDefaultFormat();
+       }
+
+       /**
+        * Convenience method that returns the list of serialization formats supported
+        * for the content model model this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getSupportedFormats()
+        *
+        * @since WD.1
+        *
+        * @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 )
+        *
+        * @since WD.1
+        *
+        * @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->getModel() );
+               }
+       }
+
+       /**
+        * Convenience method for serializing this Content object.
+        *
+        * Shorthand for $this->getContentHandler()->serializeContent( $this, $format )
+        *
+        * @since WD.1
+        *
+        * @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.
+        *
+        * @since WD.1
+        *
+        * @return bool whether this Content object is empty
+        */
+       public function isEmpty() {
+               return $this->getSize() == 0;
+       }
+
+       /**
+        * Returns if the content is valid. This is intended for local validity checks, not considering global consistency.
+        * It needs to be valid before it can be saved.
+        *
+        * This default implementation always returns true.
+        *
+        * @since WD.1
+        *
+        * @return boolean
+        */
+       public function isValid() {
+               return true;
+       }
+
+       /**
+        * 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->getModelName() != $this->getModel().
+        * Will return false if $that->getNativeData() is not equal to $this->getNativeData(),
+        * where the meaning of "equal" depends on the actual data model.
+        *
+        * Implementations should be careful to make equals() transitive and reflexive:
+        *
+        * * $a->equals( $b ) <=> $b->equals( $a )
+        * * $a->equals( $b ) &&  $b->equals( $c ) ==> $a->equals( $c )
+        *
+        * @since WD.1
+        *
+        * @param Content $that the Content object to compare to
+        * @return bool true if this Content object is euqual to $that, false otherwise.
+        */
+       public function equals( Content $that = null ) {
+               if ( is_null( $that ) ){
+                       return false;
+               }
+
+               if ( $that === $this ) {
+                       return true;
+               }
+
+               if ( $that->getModel() !== $this->getModel() ) {
+                       return false;
+               }
+
+               return $this->getNativeData() === $that->getNativeData();
+       }
+
+       /**
+        * Convenience method, shorthand for
+        * $this->getContentHandler()->getParserOutput( $this, $title, $revId, $options, $generateHtml )
+        *
+        * @note: subclasses should NOT override this to provide custom rendering.
+        *        Override ContentHandler::getParserOutput() instead!
+        *
+        * @param Title $title
+        * @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.
+        *
+        * @since WD.1
+        *
+        * @return ParserOutput
+        */
+       public function getParserOutput( Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               return $this->getContentHandler()->getParserOutput( $this, $title, $revId, $options, $generateHtml );
+       }
+
+       /**
+        * 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).
+        *
+        * There is usually no need to override the default behaviour, subclasses that
+        * want to implement redirects should override getRedirectTarget().
+        *
+        * @since WD.1
+        *
+        * @return Array of Titles, with the destination last
+        * @note: migrated here from Title::newFromRedirectArray
+        */
+       public function getRedirectChain() {
+               global $wgMaxRedirects;
+               $title = $this->getRedirectTarget();
+               if ( is_null( $title ) ) {
+                       return null;
+               }
+               // recursive check to follow double redirects
+               $recurse = $wgMaxRedirects;
+               $titles = array( $title );
+               while ( --$recurse > 0 ) {
+                       if ( $title->isRedirect() ) {
+                               $page = WikiPage::factory( $title );
+                               $newtitle = $page->getRedirectTarget();
+                       } else {
+                               break;
+                       }
+                       // Redirects to some special pages are not permitted
+                       if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
+                               // the new title passes the checks, so make that our current title so that further recursion can be checked
+                               $title = $newtitle;
+                               $titles[] = $newtitle;
+                       } else {
+                               break;
+                       }
+               }
+               return $titles;
+       }
+
+       /**
+        * Construct the redirect destination from this content and return a Title,
+        * or null if this content doesn't represent a redirect.
+        *
+        * This shall only return the immediate redirect target, useful for
+        * the redirect table and other checks that don't need full recursion.
+        *
+        * This implementation always returns null, subclasses should implement it
+        * according to their data model.
+        *
+        * @since WD.1
+        *
+        * @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.
+        *
+        * There is usually no need to override the default behaviour, subclasses that
+        * want to implement redirects should override getRedirectTarget().
+        *
+        * @since WD.1
+        *
+        * @return Title
+        * @note: migrated here from Title::newFromRedirectRecurse
+        */
+       public function getUltimateRedirectTarget() {
+               $titles = $this->getRedirectChain();
+               return $titles ? array_pop( $titles ) : null;
+       }
+
+       /**
+        * @since WD.1
+        *
+        * @return bool
+        */
+       public function isRedirect() {
+               return $this->getRedirectTarget() !== null;
+       }
+
+       /**
+        * Returns the section with the given id.
+        *
+        * The default implementation returns null.
+        *
+        * @since WD.1
+        *
+        * @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 (included) 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.
+        *
+        * @since WD.1
+        *
+        * @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).
+        *
+        * @since WD.1
+        *
+        * @param Title $title
+        * @param User $user
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               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.
+        *
+        * @since WD.1
+        *
+        * @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).
+        *
+        * @since WD.1
+        *
+        * @param Title $title
+        * @param null|ParserOptions $popts
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts ) {
+               return $this;
+       }
+}
+
+/**
+ * Content object implementation for representing flat text.
+ *
+ * TextContent instances are immutable
+ *
+ * @since WD.1
+ */
+abstract class TextContent extends AbstractContent {
+
+       public function __construct( $text, $model_id = null ) {
+               parent::__construct( $model_id );
+
+               $this->mText = $text;
+       }
+
+       public function copy() {
+               return $this; #NOTE: this is ok since TextContent are immutable.
+       }
+
+       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();
+       }
+
+       /**
+        * Diff this content object with another content object..
+        *
+        * @since WD.diff
+        *
+        * @param Content $that the other content object to compare this content object to
+        * @param Language $lang the language object to use for text segmentation. If not given, $wgContentLang is used.
+        *
+        * @return DiffResult a diff representing the changes that would have to be made to this content object
+        *         to make it equal to $that.
+        */
+       public function diff( Content $that, Language $lang = null ) {
+               global $wgContLang;
+
+               $this->checkModelID( $that->getModel() );
+
+               #@todo: could implement this in DifferenceEngine and just delegate here?
+
+               if ( !$lang ) $lang = $wgContLang;
+
+               $otext = $this->getNativeData();
+               $ntext = $this->getNativeData();
+
+               # Note: Use native PHP diff, external engines don't give us abstract output
+               $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
+               $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
+
+               $diff = new Diff( $ota, $nta );
+               return $diff;
+       }
+
+
+}
+
+/**
+ * @since WD.1
+ */
+class WikitextContent extends TextContent {
+
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_WIKITEXT);
+       }
+
+       /**
+        * Returns the section with the given id.
+        *
+        * @param String $section
+        *
+        * @internal 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'
+        *
+        * @throws MWException
+        * @return Content Complete article content, or null if error
+        */
+       public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+               wfProfileIn( __METHOD__ );
+
+               $myModelId = $this->getModel();
+               $sectionModelId = $with->getModel();
+
+               if ( $sectionModelId != $myModelId  ) {
+                       $myModelName = ContentHandler::getContentModelName( $myModelId );
+                       $sectionModelName = ContentHandler::getContentModelName( $sectionModelId );
+
+                       throw new MWException( "Incompatible content model for section: document uses $myModelId ($myModelName), "
+                                                               . "section uses $sectionModelId ($sectionModelName)." );
+               }
+
+               $oldtext = $this->getNativeData();
+               $text = $with->getNativeData();
+
+               if ( $section === '' ) {
+                       return $with; #XXX: copy first?
+               } if ( $section == 'new' ) {
+                       # Inserting a new section
+                       $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
+                       if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
+                               $text = strlen( trim( $oldtext ) ) > 0
+                                       ? "{$oldtext}\n\n{$subject}{$text}"
+                                       : "{$subject}{$text}";
+                       }
+               } else {
+                       # Replacing an existing section; roll out the big guns
+                       global $wgParser;
+
+                       $text = $wgParser->replaceSection( $oldtext, $section, $text );
+               }
+
+               $newContent = new WikitextContent( $text );
+
+               wfProfileOut( __METHOD__ );
+               return $newContent;
+       }
+
+       /**
+        * Returns a new WikitextContent object with the given section heading prepended.
+        *
+        * @param $header String
+        * @return Content
+        */
+       public function addSectionHeader( $header ) {
+               $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $header ) . "\n\n" . $this->getNativeData();
+
+               return new WikitextContent( $text );
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param User $user
+        * @param ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) { #FIXME: also needed for JS/CSS!
+               global $wgParser, $wgConteLang;
+
+               $text = $this->getNativeData();
+               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+               return new WikitextContent( $pst );
+       }
+
+       /**
+        * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+        *
+        * @param Title $title
+        * @param ParserOptions $popts
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts ) {
+               global $wgParser, $wgConteLang;
+
+               $text = $this->getNativeData();
+               $plt = $wgParser->getPreloadText( $text, $title, $popts );
+
+               return new WikitextContent( $plt );
+       }
+
+       /**
+        * Implement redirect extraction for wikitext.
+        *
+        * @return null|Title
+        *
+        * @note: migrated here from Title::newFromRedirectInternal()
+        *
+        * @see Content::getRedirectTarget
+        * @see AbstractContent::getRedirectTarget
+        */
+       public function getRedirectTarget() {
+               global $wgMaxRedirects;
+               if ( $wgMaxRedirects < 1 ) {
+                       //redirects are disabled, so quit early
+                       return null;
+               }
+               $redir = MagicWord::get( 'redirect' );
+               $text = trim( $this->getNativeData() );
+               if ( $redir->matchStartAndRemove( $text ) ) {
+                       // Extract the first link and see if it's usable
+                       // Ensure that it really does come directly after #REDIRECT
+                       // Some older redirects included a colon, so don't freak about that!
+                       $m = array();
+                       if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
+                               // Strip preceding colon used to "escape" categories, etc.
+                               // and URL-decode links
+                               if ( strpos( $m[1], '%' ) !== false ) {
+                                       // Match behavior of inline link parsing here;
+                                       $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
+                               }
+                               $title = Title::newFromText( $m[1] );
+                               // If the title is a redirect to bad special pages or is invalid, return null
+                               if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
+                                       return null;
+                               }
+                               return $title;
+                       }
+               }
+               return null;
+       }
+
+       /**
+        * Returns true if this content is not a redirect, and this content's text is countable according to
+        * the criteria defined 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 null|\Title $title
+        *
+        * @internal param \IContextSource $context context for parsing if necessary
+        *
+        * @return bool true if the content is countable
+        */
+       public function isCountable( $hasLinks = null, Title $title = null ) {
+               global $wgArticleCountMethod;
+
+               if ( $this->isRedirect( ) ) {
+                       return false;
+               }
+
+               $text = $this->getNativeData();
+
+               switch ( $wgArticleCountMethod ) {
+                       case 'any':
+                               return true;
+                       case 'comma':
+                               return strpos( $text,  ',' ) !== false;
+                       case 'link':
+                               if ( $hasLinks === null ) { # not known, find out
+                                       if ( !$title ) {
+                                               $context = RequestContext::getMain();
+                                               $title = $context->getTitle();
+                                       }
+
+                                       $po = $this->getParserOutput( $title, null, null, false );
+                                       $links = $po->getLinks();
+                                       $hasLinks = !empty( $links );
+                               }
+
+                               return $hasLinks;
+               }
+
+               return false;
+       }
+
+       public function getTextForSummary( $maxlength = 250 ) {
+               $truncatedtext = parent::getTextForSummary( $maxlength );
+
+               #clean up unfinished links
+               #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
+               $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
+
+               return $truncatedtext;
+       }
+
+}
+
+/**
+ * @since WD.1
+ */
+class MessageContent extends TextContent {
+       public function __construct( $msg_key, $params = null, $options = null ) {
+               parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
+
+               $this->mMessageKey = $msg_key;
+
+               $this->mParameters = $params;
+
+               if ( is_null( $options ) ) {
+                       $options = array();
+               }
+               elseif ( is_string( $options ) ) {
+                       $options = array( $options );
+               }
+
+               $this->mOptions = $options;
+       }
+
+       /**
+        * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
+        * @return String the message text, parsed
+        */
+       public 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".
+        *
+        * @return String the message text, unparsed.
+        */
+       public function getNativeData( ) {
+               $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
+
+               return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
+       }
+
+}
+
+/**
+ * @since WD.1
+ */
+class JavaScriptContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
+       }
+
+}
+
+/**
+ * @since WD.1
+ */
+class CssContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct($text, CONTENT_MODEL_CSS);
+       }
+}
diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php
new file mode 100644 (file)
index 0000000..ce7d083
--- /dev/null
@@ -0,0 +1,1092 @@
+<?php
+
+/**
+ * Exception representing a failure to serialize or unserialize a content object.
+ */
+class MWContentSerializationException extends MWException {
+
+}
+
+/**
+ * A content handler knows how do deal with a specific type of content on a wiki page.
+ * Content is stored in the database in a serialized form (using a serialization format aka mime type)
+ * and is be unserialized into it's native PHP representation (the content model), which is wrapped in
+ * an instance of the appropriate subclass of Content.
+ *
+ * ContentHandler instances are stateless singletons that serve, among other things, as a factory for
+ * Content objects. Generally, there is one subclass of ContentHandler and one subclass of Content
+ * for every type of content model.
+ *
+ * Some content types have a flat model, that is, their native representation is the
+ * same as their serialized form. Examples would be JavaScript and CSS code. As of now,
+ * this also applies to wikitext (mediawiki's default content type), but wikitext
+ * content may be represented by a DOM or AST structure in the future.
+ *
+ * @since 1.WD
+ */
+abstract class ContentHandler {
+
+       /**
+        * Convenience function for getting flat text from a Content object. This should only
+        * be used in the context of backwards compatibility with code that is not yet able
+        * to handle Content objects!
+        *
+        * 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 behavior of this method depends on the global $wgContentHandlerTextFallback:
+        * * If $wgContentHandlerTextFallback is 'fail' and $content is not a TextContent object, an MWException is thrown.
+        * * If $wgContentHandlerTextFallback is 'serialize' and $content is not a TextContent object, $content->serialize()
+        * is called to get a string form of the content.
+        * * If $wgContentHandlerTextFallback is 'ignore' and $content is not a TextContent object, this method returns null.
+        * * otherwise, the behaviour is undefined.
+        *
+        * @since WD.1
+        *
+        * @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->getModel() );
+               }
+
+               if ( $wgContentHandlerTextFallback == 'serialize' ) {
+                       return $content->serialize();
+               }
+
+               return null;
+       }
+
+       /**
+        * Convenience function for creating a Content object from a given textual representation.
+        *
+        * $text will be deserialized into a Content object of the model specified by $modelId (or,
+        * if that is not given, $title->getContentModel()) using the given format.
+        *
+        * @since WD.1
+        *
+        * @static
+        *
+        * @param string      $text    the textual representation, will be unserialized to create the Content object
+        * @param null|Title  $title   the title of the page this text belongs to. Required if $modelId is not provided.
+        * @param null|String $modelId the model to deserialize to. If not provided, $title->getContentModel() is used.
+        * @param null|String $format  the format to use for deserialization. If not given, the model's default format is used.
+        *
+        * @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 = null, $modelId = null, $format = null ) {
+
+               if ( is_null( $modelId ) ) {
+                       if ( is_null( $title ) ) {
+                               throw new MWException( "Must provide a Title object or a content model ID." );
+                       }
+
+                       $modelId = $title->getContentModel();
+               }
+
+               $handler = ContentHandler::getForModelID( $modelId );
+               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::getContentModel().
+        *
+        * Which model is to be used per default for the page is determined based on several factors:
+        * * The global setting $wgNamespaceContentModels specifies a content model per namespace.
+        * * The hook DefaultModelFor may be used to override the page's default model.
+        * * Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript model if they end in .js or .css, respectively.
+        * * Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+        * * The hook TitleIsCssOrJsPage may be used to force a page to use the CSS or JavaScript model if they end in .js or .css, respectively.
+        * * The hook TitleIsWikitextPage may be used to force a page to use the wikitext model.
+        *
+        * If none of the above applies, the wikitext model is used.
+        *
+        * Note: this is used by, and may thus not use, Title::getContentModel()
+        *
+        * @since WD.1
+        *
+        * @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->getContentModel() directly or indirectly,
+               //       because it is used to initialized the mContentModel member.
+
+               $ns = $title->getNamespace();
+
+               $ext = false;
+               $m = null;
+               $model = null;
+
+               if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+                       $model = $wgNamespaceContentModels[ $ns ];
+               }
+
+               // hook can determin default model
+               if ( !wfRunHooks( 'ContentHandlerDefaultModelFor', array( $title, &$model ) ) ) {
+                       if ( !is_null( $model ) ) {
+                               return $model;
+                       }
+               }
+
+               // Could this page contain custom CSS or JavaScript, based on the title?
+               $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
+               if ( $isCssOrJsPage ) {
+                       $ext = $m[1];
+               }
+
+               // hook can force js/css
+               wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
+
+               // Is this a .css subpage of a user page?
+               $isJsCssSubpage = NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
+               if ( $isJsCssSubpage ) {
+                       $ext = $m[1];
+               }
+
+               // is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+               $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
+               $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
+
+               // hook can override $isWikitext
+               wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
+
+               if ( !$isWikitext ) {
+                       switch ( $ext ) {
+                               case 'js':
+                                       return CONTENT_MODEL_JAVASCRIPT;
+                               case 'css':
+                                       return CONTENT_MODEL_CSS;
+                               default:
+                                       return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
+                       }
+               }
+
+               // we established that is must be wikitext
+
+               return CONTENT_MODEL_WIKITEXT;
+       }
+
+       /**
+        * returns the appropriate ContentHandler singleton for the given title
+        *
+        * @since WD.1
+        *
+        * @static
+        * @param Title $title
+        * @return ContentHandler
+        */
+       public static function getForTitle( Title $title ) {
+               $modelId = $title->getContentModel();
+               return ContentHandler::getForModelID( $modelId );
+       }
+
+       /**
+        * returns the appropriate ContentHandler singleton for the given Content object
+        *
+        * @since WD.1
+        *
+        * @static
+        * @param Content $content
+        * @return ContentHandler
+        */
+       public static function getForContent( Content $content ) {
+               $modelId = $content->getModel();
+               return ContentHandler::getForModelID( $modelId );
+       }
+
+       /**
+        * returns the ContentHandler singleton for the given model id. Use the CONTENT_MODEL_XXX constants to
+        * identify the desired content model.
+        *
+        * ContentHandler singletons are take from the global $wgContentHandlers array. Keys in that array are
+        * model names, the values are either ContentHandler singleton objects, or strings specifying the appropriate
+        * 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 $modelId, the ContentHandler may be provided by the
+        * a ContentHandlerForModelID hook. if no ContentHandler can be determined, an MWException is raised.
+        *
+        * @since WD.1
+        *
+        * @static
+        * @param $modelId int the id of the content model for which to get a handler. Use CONTENT_MODEL_XXX constants.
+        * @return ContentHandler the ContentHandler singleton for handling the model given by $modelId
+        * @throws MWException if no handler is known for $modelId.
+        */
+       public static function getForModelID( $modelId ) {
+               global $wgContentHandlers;
+
+               if ( empty( $wgContentHandlers[$modelId] ) ) {
+                       $handler = null;
+
+                       wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
+
+                       if ( $handler ) { // NOTE: may be a string or an object, either is fine!
+                               $wgContentHandlers[$modelId] = $handler;
+                       } else {
+                               throw new MWException( "No handler for model #$modelId registered in \$wgContentHandlers" );
+                       }
+               }
+
+               if ( is_string( $wgContentHandlers[$modelId] ) ) {
+                       $class = $wgContentHandlers[$modelId];
+                       $wgContentHandlers[$modelId] = new $class( $modelId );
+               }
+
+               return $wgContentHandlers[$modelId];
+       }
+
+       /**
+        * Returns the appropriate mime type for a given content format,
+        * or null if no mime type is known for this format.
+        *
+        * Mime types can be registered in the global array $wgContentFormatMimeTypes.
+        *
+        * @static
+        * @param int $id the content format id, as given by a CONTENT_FORMAT_XXX constant
+        *        or returned by Revision::getContentFormat().
+        *
+        * @return String|null the content format's mime type.
+        */
+       public static function getContentFormatMimeType( $id ) {
+               global $wgContentFormatMimeTypes;
+
+               if ( !isset( $wgContentFormatMimeTypes[ $id ] ) ) {
+                       return null;
+               }
+
+               return $wgContentFormatMimeTypes[ $id ];
+       }
+
+       /**
+        * Returns the content format if for a given mime type,
+        * or null if no format id if known for this mime type.
+        *
+        * Mime types can be registered in the global array $wgContentFormatMimeTypes.
+        *
+        * @static
+        * @param String $mime the mime type
+        *
+        * @return int|null the format id, as defined by a CONTENT_FORMAT_XXX constant
+        */
+       public static function getContentFormatID( $mime ) {
+               global $wgContentFormatMimeTypes;
+
+               static $format_ids = null;
+
+               if ( $format_ids === null ) {
+                       $format_ids = array_flip( $wgContentFormatMimeTypes );
+               }
+
+               if ( !isset( $format_ids[ $mime ] ) ) {
+                       return null;
+               }
+
+               return $format_ids[ $mime ];
+       }
+
+       /**
+        * Returns the localized name for a given content model,
+        * or null of no mime type is known.
+        *
+        * Model names are localized using system messages. Message keys
+        * have the form content-model-$id.
+        *
+        * @static
+        * @param int $id the content model id, as given by a CONTENT_MODEL_XXX constant
+        *        or returned by Revision::getContentModel().
+        *
+        * @return String|null the content format's mime type.
+        */
+       public static function getContentModelName( $id ) {
+               $key = "content-model-$id";
+
+               if ( wfEmptyMsg( $key ) ) return null;
+               else return wfMsg( $key );
+       }
+
+       // ----------------------------------------------------------------------------------------------------------
+
+       protected $mModelID;
+       protected $mSupportedFormats;
+
+       /**
+        * Constructor, initializing the ContentHandler instance with it's model id and a list of supported formats.
+        * Values for the parameters are typically provided as literals by subclasses' constructors.
+        *
+        * @param int $modelId (use CONTENT_MODEL_XXX constants).
+        * @param array $formats list for supported serialization formats (typically as MIME types)
+        */
+       public function __construct( $modelId, $formats ) {
+               $this->mModelID = $modelId;
+               $this->mSupportedFormats = $formats;
+       }
+
+
+       /**
+        * Serializes Content object of the type supported by this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @abstract
+        * @param Content $content the Content object to serialize
+        * @param null $format the desired serialization format
+        * @return String serialized form of the content
+        */
+       public abstract function serializeContent( Content $content, $format = null );
+
+       /**
+        * Unserializes a Content object of the type supported by this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @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.
+        *
+        * @since WD.1
+        *
+        * @return Content
+        */
+       public abstract function makeEmptyContent();
+
+       /**
+        * Returns the model id that identifies the content model this ContentHandler can handle.
+        * Use with the CONTENT_MODEL_XXX constants.
+        *
+        * @since WD.1
+        *
+        * @return int the model id
+        */
+       public function getModelID() {
+               return $this->mModelID;
+       }
+
+       /**
+        * Throws an MWException if $model_id is not the id of the content model
+        * supported by this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @param int $model_id the model to check
+        *
+        * @throws MWException
+        */
+       protected function checkModelID( $model_id ) {
+               if ( $model_id !== $this->mModelID ) {
+                       $model_name = ContentHandler::getContentModelName( $model_id );
+                       $own_model_name = ContentHandler::getContentModelName( $this->mModelID );
+
+                       throw new MWException( "Bad content model: expected {$this->mModelID} ($own_model_name) but got found $model_id ($model_name)." );
+               }
+       }
+
+       /**
+        * Returns a list of serialization formats supported by the serializeContent() and unserializeContent() methods of
+        * this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @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.
+        *
+        * @since WD.1
+        *
+        * @return String the name of the default serialization format as a MIME type
+        */
+       public function getDefaultFormat() {
+               return $this->mSupportedFormats[0];
+       }
+
+       /**
+        * Returns true if $format is a serialization format supported by this ContentHandler,
+        * and false otherwise.
+        *
+        * Note that if $format is null, this method always returns true, because null
+        * means "use the default format".
+        *
+        * @since WD.1
+        *
+        * @param String $format the serialization format to check
+        * @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
+        *
+        * @throws MWException
+        */
+       protected function checkFormat( $format ) {
+               if ( !$this->isSupportedFormat( $format ) ) {
+                       throw new MWException( "Format $format is not supported for content model " . $this->getModelID() );
+               }
+       }
+
+       /**
+        * Returns if the content is consistent with the database, that is if saving it to the database would not violate any
+        * global constraints.
+        *
+        * Content needs to be valid using this method before it can be saved.
+        *
+        * This default implementation always returns true.
+        *
+        * @since WD.1
+        *
+        * @param \Content $content
+        *
+        * @return boolean
+        */
+       public function isConsistentWithDatabase( Content $content ) {
+               return true;
+       }
+
+       /**
+        * Returns overrides for action handlers.
+        * Classes listed here will be used instead of the default one when
+        * (and only when) $wgActions[$action] === true. This allows subclasses
+        * to override the default action handlers.
+        *
+        * @since WD.1
+        *
+        * @return Array
+        */
+       public function getActionOverrides() {
+               return array();
+       }
+
+       /**
+        * Factory creating an appropriate DifferenceEngine for this content model.
+        *
+        * @since WD.1
+        *
+        * @param            $context      IContextSource context to use, anything else will be ignored
+        * @param            $old          Integer old ID we want to show and diff with.
+        * @param int|String $new          String either 'prev' or 'next'.
+        * @param            $rcid         Integer ??? FIXME (default 0)
+        * @param            $refreshCache boolean If set, refreshes the diff cache
+        * @param            $unhide       boolean If set, allow viewing deleted revs
+        *
+        * @return DifferenceEngine
+        */
+       public function createDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, #FIMXE: use everywhere!
+                                                                                $refreshCache = false, $unhide = false ) {
+
+               $this->checkModelID( $context->getTitle()->getContentModel() );
+
+               $diffEngineClass = $this->getDiffEngineClass();
+
+               return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
+       }
+
+       /**
+        * Returns the name of the diff engine to use.
+        *
+        * @since WD.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.
+        *
+        * @since WD.1
+        *
+        * @param Content|String $oldContent  String
+        * @param Content|String $myContent   String
+        * @param Content|String $yourContent String
+        *
+        * @return Content|Bool
+        */
+       public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+               return false;
+       }
+
+       /**
+        * Return an applicable auto-summary if one exists for the given edit.
+        *
+        * @since WD.1
+        *
+        * @param $oldContent Content|null: the previous text of the page.
+        * @param $newContent Content|null: The submitted text of the page.
+        * @param $flags Int bit mask: a bit mask of flags submitted for the edit.
+        *
+        * @return string An appropriate auto-summary, or an empty string.
+        */
+       public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
+               global $wgContLang;
+
+               // Decide what kind of auto-summary is needed.
+
+               // Redirect auto-summaries
+
+               /**
+                * @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 auto-summaries
+               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 auto-summaries
+               if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+                       return wfMsgForContent( 'autosumm-blank' );
+               } elseif ( !empty( $oldContent ) && $oldContent->getSize() > 10 * $newContent->getSize() && $newContent->getSize() < 500 ) {
+                       // 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 auto-summary for our case, so our
+               // auto-summary is empty.
+
+               return '';
+       }
+
+       /**
+        * Auto-generates a deletion reason
+        *
+        * @since WD.1
+        *
+        * @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;
+
+               $this->checkModelID( $content->getModel() );
+
+               // If the page is blank, use the text from the previous revision,
+               // which can only be blank if there's a move/import/protect dummy revision involved
+               if ( $content->getSize() == 0 ) {
+                       $prev = $rev->getPrevious();
+
+                       if ( $prev )    {
+                               $content = $prev->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;
+       }
+
+       /**
+        * Parse the Content object and generate a ParserObject from the result. $result->getText() can
+        * be used to obtain the generated HTML. If no HTML is needed,  $generateHtml can be set to false;
+        * in that case, $result->getText() may return null.
+        *
+        * @param Content $content the content to render
+        * @param Title $title the page title to use as a context for rendering
+        * @param null|int $revId the revision being rendered (optional)
+        * @param null|ParserOptions $options any parser options
+        * @param Boolean $generateHtml whether to generate Html (default: true). If false,
+        *        the result of calling getText() on the ParserOutput object returned by
+        *        this method is undefined.
+        *
+        * @since WD.1
+        *
+        * @return ParserOutput
+        */
+       public abstract function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true );
+       #TODO: make RenderOutput and RenderOptions base classes
+
+       /**
+        * Returns a list of DataUpdate objects for recording information about this Content in some secondary
+        * data store. If the optional second argument, $old, is given, the updates may model only the changes that
+        * need to be made to replace information about the old content with information about the new content.
+        *
+        * This default implementation calls $this->getParserOutput( $content, $title, null, null, false ), and then
+        * calls getSecondaryDataUpdates( $title, $recursive ) on the resulting ParserOutput object.
+        *
+        * Subclasses may implement this to determine the necessary updates more efficiently, or make use of information
+        * about the old content.
+        *
+        * @param Content $content the content for determining the necessary updates
+        * @param Title $title the context for determining the necessary updates
+        * @param Content|null $old an optional Content object representing the previous content, i.e. the content being
+        *                     replaced by this Content object.
+        * @param boolean $recursive whether to include recursive updates (default: false).
+        * @param ParserOutput|null $parserOutput optional ParserOutput object. Provide if you have one handy, to avoid re-parsing
+        *        of the content.
+        *
+        * @return Array. A list of DataUpdate objects for putting information about this content object somewhere.
+        *
+        * @since WD.1
+        */
+       public function getSecondaryDataUpdates( Content $content, Title $title, Content $old = null,
+                                                                                       $recursive = true, ParserOutput $parserOutput = null ) {
+
+               if ( !$parserOutput ) {
+                       $parserOutput = $this->getParserOutput( $content, $title, null, null, false );
+               }
+
+               return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
+       }
+
+
+       /**
+        * Get the Content object that needs to be saved in order to undo all revisions
+        * between $undo and $undoafter. Revisions must belong to the same page,
+        * must exist and must not be deleted
+        *
+        * @since WD.1
+        *
+        * @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();
+
+               $this->checkModelID( $cur_content->getModel() );
+               $this->checkModelID( $undo_content->getModel() );
+               $this->checkModelID( $undoafter_content->getModel() );
+
+               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().
+        *
+        * @since WD.1
+        *
+        * @return bool
+        */
+       public function isParserCacheSupported() {
+               return true;
+       }
+
+       /**
+        * Returns a lost of updates to perform when the given content is deleted.
+        * The necessary updates may be taken from the Content object, or depend on the current state of the database.
+        *
+        * @since WD.1
+        *
+        * @param \Content           $content the Content object for deletion
+        * @param \Title             $title   the title of the deleted page
+        * @param null|\ParserOutput $parserOutput optional parser output object for efficient access to meta-information
+        *                           about the content object. Provide if you have one handy.
+        *
+        * @return array a list of DataUpdate instances that will clean up the database ofter deletion.
+        */
+       public function getDeletionUpdates( Content $content, Title $title, ParserOutput $parserOutput = null ) {
+               return array(
+                       new LinksDeletionUpdate( $title ),
+               );
+       }
+
+       /**
+        * Returns true iff this content model supports sections.
+        *
+        * This default implementation returns false,
+        *
+        * @return boolean whether sections are supported.
+        */
+       public function supportsSections() {
+               return false;
+       }
+}
+
+/**
+ * @since WD.1
+ */
+abstract class TextContentHandler extends ContentHandler {
+
+       public function __construct( $modelId, $formats ) {
+               parent::__construct( $modelId, $formats );
+       }
+
+       /**
+        * Returns the content's text as-is.
+        *
+        * @param Content $content
+        * @param String|null $format
+        * @return mixed
+        */
+       public function serializeContent( Content $content, $format = null ) {
+               $this->checkFormat( $format );
+               return $content->getNativeData();
+       }
+
+       /**
+        * attempts to merge differences between three versions.
+        * Returns a new Content object for a clean merge and false for failure or a conflict.
+        *
+        * All three Content objects passed as parameters must have the same content model.
+        *
+        * This text-based implementation uses wfMerge().
+        *
+        * @param \Content|String $oldContent  String
+        * @param \Content|String $myContent   String
+        * @param \Content|String $yourContent String
+        *
+        * @return Content|Bool
+        */
+       public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+               $this->checkModelID( $oldContent->getModel() );
+               $this->checkModelID( $myContent->getModel() );
+               $this->checkModelID( $yourContent->getModel() );
+
+               $format = $this->getDefaultFormat();
+
+               $old = $this->serializeContent( $oldContent, $format );
+               $mine = $this->serializeContent( $myContent, $format );
+               $yours = $this->serializeContent( $yourContent, $format );
+
+               $ok = wfMerge( $old, $mine, $yours, $result );
+
+               if ( !$ok ) {
+                       return false;
+               }
+
+               if ( !$result ) {
+                       return $this->makeEmptyContent();
+               }
+
+               $mergedContent = $this->unserializeContent( $result, $format );
+               return $mergedContent;
+       }
+
+       /**
+        * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
+        *
+        * @param Content $content the content to render
+        * @param Title              $title context title for parsing
+        * @param int|null           $revId revision id (the parser wants that for some reason)
+        * @param ParserOptions|null $options parser options
+        * @param bool               $generateHtml whether or not to generate HTML
+        *
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               $this->checkModelID( $content->getModel() );
+
+               # generic implementation, relying on $this->getHtml()
+
+               if ( $generateHtml ) $html = $this->getHtml( $content );
+               else $html = '';
+
+               $po = new ParserOutput( $html );
+               return $po;
+       }
+
+       /**
+        * Generates an HTML version of the content, for display.
+        * Used by getParserOutput() to construct a ParserOutput object.
+        *
+        * This default implementation just calls getHighlightHtml(). Content models that
+        * have another mapping to HTML (as is the case for markup languages like wikitext)
+        * should override this method to generate the appropriate html.
+        *
+        * @param Content $content the content to render
+        *
+        * @return String an HTML representation of the content
+        */
+       protected function getHtml( Content $content ) {
+               $this->checkModelID( $content->getModel() );
+
+               return $this->getHighlightHtml( $content );
+       }
+
+       /**
+        * Generates a syntax-highlighted version the content, as HTML.
+        * Used by the default implementation if getHtml().
+        *
+        * @param Content $content the content to render
+        *
+        * @return String an HTML representation of the content's markup
+        */
+       protected function getHighlightHtml( Content $content ) {
+               $this->checkModelID( $content->getModel() );
+
+               #TODO: make Highlighter interface, use here highlighter, if available
+               return htmlspecialchars( $content->getNativeData() );
+       }
+
+
+}
+
+/**
+ * @since WD.1
+ */
+class WikitextContentHandler extends TextContentHandler {
+
+       public function __construct( $modelId = CONTENT_MODEL_WIKITEXT ) {
+               parent::__construct( $modelId, array( CONTENT_FORMAT_WIKITEXT ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new WikitextContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new WikitextContent( '' );
+       }
+
+       /**
+        * Returns a ParserOutput object resulting from parsing the content's text using $wgParser.
+        *
+        * @since    WD.1
+        *
+        * @param Content $content the content to render
+        * @param \Title             $title
+        * @param null               $revId
+        * @param null|ParserOptions $options
+        * @param bool               $generateHtml
+        *
+        * @internal param \IContextSource|null $context
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( Content $content, Title $title, $revId = null, ParserOptions $options = null, $generateHtml = true ) {
+               global $wgParser;
+
+               $this->checkModelID( $content->getModel() );
+
+               if ( !$options ) {
+                       $options = new ParserOptions();
+               }
+
+               $po = $wgParser->parse( $content->getNativeData(), $title, $options, true, true, $revId );
+               return $po;
+       }
+
+       protected function getHtml( Content $content ) {
+               throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
+       }
+
+       /**
+        * Returns true because wikitext supports sections.
+        *
+        * @return boolean whether sections are supported.
+        */
+       public function supportsSections() {
+               return true;
+       }
+}
+
+#XXX: make ScriptContentHandler base class, do highlighting stuff there?
+
+/**
+ * @since WD.1
+ */
+class JavaScriptContentHandler extends TextContentHandler {
+
+       public function __construct( $modelId = CONTENT_MODEL_JAVASCRIPT ) {
+               parent::__construct( $modelId, array( CONTENT_FORMAT_JAVASCRIPT ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new JavaScriptContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new JavaScriptContent( '' );
+       }
+
+       protected function getHtml( Content $content ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+               $html .= $this->getHighlightHtml( $content );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+}
+
+/**
+ * @since WD.1
+ */
+class CssContentHandler extends TextContentHandler {
+
+       public function __construct( $modelId = CONTENT_MODEL_CSS ) {
+               parent::__construct( $modelId, array( CONTENT_FORMAT_CSS ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new CssContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new CssContent( '' );
+       }
+
+
+       protected function getHtml( Content $content ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+               $html .= $this->getHighlightHtml( $content );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+}
index 508add4..35a7329 100644 (file)
@@ -660,6 +660,40 @@ $wgMediaHandlers = array(
        'image/x-djvu' => 'DjVuHandler', // compat
 );
 
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model id to a class name
+ */
+$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>
+);
+
+/**
+ * Mime types for content formats.
+ * Each entry in the array maps a content format to a mime type.
+ *
+ * Extensions that define their own content formats can register
+ * the appropriate mime types in this array.
+ *
+ * Such extensions shall use content format IDs
+ * larger than 100 and register the ids they use at
+ * <http://mediawiki.org/ContentHandler/registry>
+ * to avoid conflicts with other extensions.
+ */
+$wgContentFormatMimeTypes = array(
+       CONTENT_FORMAT_WIKITEXT => 'text/x-wiki',
+       CONTENT_FORMAT_JAVASCRIPT => 'text/javascript',
+       CONTENT_FORMAT_CSS => 'text/css',
+       CONTENT_FORMAT_TEXT => 'text/plain',
+       CONTENT_FORMAT_HTML => 'text/html',
+       CONTENT_FORMAT_XML => 'application/xml',
+       CONTENT_FORMAT_JSON => 'application/json',
+       CONTENT_FORMAT_SERIALIZED => 'application/vnd.php.serialized',
+);
+
 /**
  * Resizing can be done using PHP's internal image libraries or using
  * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -5860,6 +5894,31 @@ $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';
+
+/**
+ * Compatibility switch for running ContentHandler code withoput a schema update.
+ * Set to false to disable use of the database fields introduced by the ContentHandler facility.
+ *
+ * @deprecated this is only here to allow code deployment without a database schema update on large sites.
+ *             get rid of it in the next version.
+ */
+$wgContentHandlerUseDB = true;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index d0f0c26..fdbda95 100644 (file)
@@ -259,7 +259,7 @@ define( 'APCOND_BLOCKED', 8 );
 define( 'APCOND_ISBOT', 9 );
 /**@}*/
 
-/**
+/** @{
  * Protocol constants for wfExpandUrl()
  */
 define( 'PROTO_HTTP', 'http://' );
@@ -268,3 +268,40 @@ define( 'PROTO_RELATIVE', '//' );
 define( 'PROTO_CURRENT', null );
 define( 'PROTO_CANONICAL', 1 );
 define( 'PROTO_INTERNAL', 2 );
+/**@}*/
+
+/**@{
+ * Content model ids, used by Content and ContentHandler
+ *
+ * Extensions that define their own content models shall use IDs
+ * larger than 100 and register the ids they use at
+ * <http://mediawiki.org/ContentHandler/registry>
+ * to avoid conflicts with other extensions.
+ */
+define( 'CONTENT_MODEL_WIKITEXT', 1 );
+define( 'CONTENT_MODEL_JAVASCRIPT', 2 );
+define( 'CONTENT_MODEL_CSS', 3 );
+define( 'CONTENT_MODEL_TEXT', 4 );
+/**@}*/
+
+/**@{
+ * Content format ids, used by Content and ContentHandler.
+ * Use ContentHander::getFormatMimeType() to get the associated mime type.
+ * Register mime types in $wgContentFormatMimeTypes.
+ *
+ * Extensions that define their own content formats shall use IDs
+ * larger than 100 and register the ids they use at
+ * <http://mediawiki.org/ContentHandler/registry>
+ * to avoid conflicts with other extensions.
+ */
+define( 'CONTENT_FORMAT_WIKITEXT', 1 ); // wikitext
+define( 'CONTENT_FORMAT_JAVASCRIPT', 2 ); // for js pages
+define( 'CONTENT_FORMAT_CSS', 3 );  // for css pages
+define( 'CONTENT_FORMAT_TEXT', 4 ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_HTML', 5 ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_SERIALIZED', 11 ); // for future use with the api, and for use by extensions
+define( 'CONTENT_FORMAT_JSON', 12 ); // for future use with the api, and for use by extensions
+define( 'CONTENT_FORMAT_XML', 13 ); // for future use with the api, and for use by extensions
+/**@}*/
+
+
index 9e337fd..cc09d7f 100644 (file)
@@ -155,6 +155,11 @@ class EditPage {
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
 
+       /**
+        * Status: can't parse content
+        */
+       const AS_PARSE_ERROR                = 240;
+
        /**
         * HTML id and name for the beginning of the edit form.
         */
@@ -214,6 +219,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
@@ -225,7 +231,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;
@@ -239,6 +245,11 @@ class EditPage {
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
+
+               $this->content_model = $this->mTitle->getContentModel();
+
+               $handler = ContentHandler::getForModelID( $this->content_model );
+               $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
 
        /**
@@ -450,10 +461,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 );
@@ -467,13 +478,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() ) ) );
@@ -679,7 +691,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     = '';
@@ -711,10 +723,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->getModelID() ); #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',
@@ -747,7 +766,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' ) ) {
@@ -776,33 +798,54 @@ class EditPage {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
+        * @deprecated since 1.WD
         */
-       function getContent( $def_text = '' ) {
-               global $wgOut, $wgRequest, $wgParser;
+       function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               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' );
@@ -818,15 +861,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 {
@@ -858,14 +902,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;
        }
 
        /**
@@ -884,39 +928,69 @@ class EditPage {
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
-                       return $this->getCurrentText();
+                       return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
-                       return '';
+                       if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
+                       $handler = ContentHandler::getForModelID( $this->content_model );
+
+                       return $handler->makeEmptyContent();
                }
-               return $this->mArticle->getContent();
+               $content = $revision->getContent();
+               return $content;
        }
 
        /**
-        * Get the actual text of the page. This is basically similar to
-        * WikiPage::getRawText() except that when the page doesn't exist an empty
-        * string is returned instead of false.
+        * Get the current content of the page. This is basically similar to
+        * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
+        * content object is returned instead of null.
         *
-        * @since 1.19
+        * @since 1.WD
         * @return string
         */
-       private function getCurrentText() {
-               $text = $this->mArticle->getRawText();
-               if ( $text === false ) {
-                       return '';
+       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()->getContentModel();
+                       $handler = ContentHandler::getForModelID( $this->content_model );
+
+                       return $handler->makeEmptyContent();
                } else {
-                       return $text;
+                       #FIXME: nasty side-effect!
+                       $this->content_model = $rev->getContentModel();
+                       $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.WD
         */
        public function setPreloadedText( $text ) {
-               $this->mPreloadText = $text;
+               wfDeprecated( __METHOD__, "1.WD" );
+
+               $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
+        *
+        * @since 1.WD
+        */
+       public function setPreloadedContent( Content $content ) {
+               $this->mPreloadedContent = $content;
        }
 
        /**
@@ -924,23 +998,47 @@ class EditPage {
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
+        *
         * @return String
+        *
+        * @deprecated since 1.WD, use getPreloadedContent() instead
         */
-       protected function getPreloadedText( $preload ) {
-               global $wgUser, $wgParser;
+       protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
+               wfDeprecated( __METHOD__, "1.WD" );
+
+               $content = $this->getPreloadedContent( $preload );
+               $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
 
-               if ( !empty( $this->mPreloadText ) ) {
-                       return $this->mPreloadText;
+               return $text;
+       }
+
+       /**
+        * Get the contents to be preloaded into the box, either set by
+        * an earlier setPreloadText() or by loading the given page.
+        *
+        * @param $preload String: representing the title to preload from.
+        *
+        * @return Content
+        *
+        * @since 1.WD
+        */
+       protected function getPreloadedContent( $preload ) { #@todo: use this!
+               global $wgUser;
+
+               if ( !empty( $this->mPreloadContent ) ) {
+                       return $this->mPreloadContent;
                }
 
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+
                if ( $preload === '' ) {
-                       return '';
+                       return $handler->makeEmptyContent();
                }
 
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                       return '';
+                       return $handler->makeEmptyContent();
                }
 
                $page = WikiPage::factory( $title );
@@ -948,13 +1046,15 @@ class EditPage {
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                               return '';
+                               return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
 
                $parserOptions = ParserOptions::newFromUser( $wgUser );
-               return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
+               $content = $page->getContent( Revision::RAW );
+
+               return $content->preloadTransform( $title, $parserOptions );
        }
 
        /**
@@ -1002,6 +1102,11 @@ class EditPage {
                        case self::AS_HOOK_ERROR:
                                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'] : '';
@@ -1097,7 +1202,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 );
@@ -1207,262 +1312,281 @@ class EditPage {
                $this->mArticle->loadPageData( 'forupdate' );
                $new = !$this->mArticle->exists();
 
-               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;
-                       }
+               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;
-                       }
+                               # 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;
-                       }
+                               // 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;
+                               $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 
-                                       // Jump to the new section
-                                       $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
+                               $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 );
 
-                                       // Create a link to the new section from the edit summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+                                               // 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;
+                               $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 
-               } else {
+                       } else { # not $new
 
-                       # Article exists. Check for edit conflict.
-                       $timestamp = $this->mArticle->getTimestamp();
-                       wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
+                               # Article exists. Check for edit conflict.
 
-                       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->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;
-                                               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" );
+                               // 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 {
-                                       $this->section = '';
-                                       $this->textbox1 = $text;
-                                       wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
+                                       $sectionTitle = $this->summary;
                                }
-                       }
 
-                       if ( $this->isConflict ) {
-                               $status->setResult( false, self::AS_CONFLICT_DETECTED );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+                               $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+                               $content = null;
 
-                       // 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;
-                       }
+                               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 );
+                               }
 
-                       # 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;
+                               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" );
+                                       }
                                }
-                       }
 
-                       # 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;
+                               if ( $this->isConflict ) {
+                                       $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        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' );
+                               // 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 ) ) ) {
+                                       # 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;
                                }
-                               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 );
+
+                               $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;
                                        }
-                               } 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] );
+
+                               # 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;
+                                       }
                                }
-                       }
-                       $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 = '';
+                               # 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' );
 
-                       $status->value = self::AS_SUCCESS_UPDATE;
-               }
+                               // 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 = '';
 
-               // 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 );
-                       wfProfileOut( __METHOD__ );
-                       return $status;
-               }
+                               $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 );
+                       $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 );
+                               $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
 
-               if ( $doEditStatus->isOK() ) {
-                       $result['redirect'] = Title::newFromRedirect( $text ) !== null;
-                       $this->commitWatch();
-                       wfProfileOut( __METHOD__ );
-                       return $status;
-               } else {
-                       // Failure from doEdit()
-                       // Show the edit conflict page for certain recognized errors from doEdit(),
-                       // but don't show it for errors from extension hooks
-                       $errors = $doEditStatus->getErrorsArray();
-                       if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
-                               'edit-already-exists' ) ) )
-                       {
-                               $this->isConflict = true;
-                               // Destroys data doEdit() put in $status->value but who cares
-                               $doEditStatus->value = self::AS_END;
+                       if ( $doEditStatus->isOK() ) {
+                                       $result['redirect'] = $content->isRedirect();
+                               $this->commitWatch();
+                               wfProfileOut( __METHOD__ );
+                               return $status;
+                       } else {
+                               // Failure from doEdit()
+                               // Show the edit conflict page for certain recognized errors from doEdit(),
+                               // but don't show it for errors from extension hooks
+                               $errors = $doEditStatus->getErrorsArray();
+                               if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
+                                       'edit-already-exists' ) ) )
+                               {
+                                       $this->isConflict = true;
+                                       // Destroys data doEdit() put in $status->value but who cares
+                                       $doEditStatus->value = self::AS_END;
+                               }
+                               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 $doEditStatus;
+                       return $status;
                }
        }
 
@@ -1519,8 +1643,33 @@ class EditPage {
         * @parma $editText string
         *
         * @return bool
+        * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
-       function mergeChangesInto( &$editText ) {
+       function mergeChangesInto( &$editText ){
+               wfDebug( __METHOD__, "1.WD" );
+
+               $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.WD
+        */
+       private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
 
                $db = wfGetDB( DB_MASTER );
@@ -1531,7 +1680,7 @@ class EditPage {
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $baseText = $baseRevision->getText();
+               $baseContent = $baseRevision->getContent();
 
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
@@ -1539,11 +1688,14 @@ class EditPage {
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $currentText = $currentRevision->getText();
+               $currentContent = $currentRevision->getContent();
+
+               $handler = ContentHandler::getForModelID( $baseContent->getModel() );
 
-               $result = '';
-               if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
-                       $editText = $result;
+               $result = $handler->merge3( $baseContent, $editContent, $currentContent );
+
+               if ( $result ) {
+                       $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
@@ -1790,6 +1942,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' ) ) );
@@ -1850,6 +2003,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 ) );
@@ -1867,7 +2023,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 {
@@ -2275,10 +2433,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.
@@ -2362,24 +2520,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();
                }
-               $newtext = $this->mArticle->replaceSection(
-                       $this->section, $this->textbox1, $this->summary, $this->edittime );
 
+               $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->getModel() ); #XXX: handle parse errors ?
+               }
+
+               wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
+
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-               $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
+               $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
 
-               if ( $oldtext !== false  || $newtext != '' ) {
+               if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = 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 {
@@ -2469,8 +2646,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::getForModelID( $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" );
@@ -2590,84 +2771,101 @@ 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' ) . ']]';
-               }
+               $note = '';
 
-               $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;
-                       }
+               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 = '';
-                       }
+                               $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;
+                               }
 
-                       $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
-               } else {
-                       $toparse = $this->textbox1;
+                               if ( $content->getModel() == CONTENT_MODEL_CSS ) {
+                                       $format = 'css';
+                               } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
+                                       $format = 'js';
+                               } else {
+                                       $format = false;
+                               }
 
-                       # 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;
+                               # 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' );
                        }
 
-                       wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
-
-                       $parserOptions->enableLimitReport();
-
-                       $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
-                       $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
+                       $rt = $content->getRedirectChain();
 
-                       $rt = Title::newFromRedirectArray( $this->textbox1 );
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
-                               $previewHTML = $parserOutput->getText();
-                       }
 
-                       $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputNoText( $parserOutput );
+                               # If we're adding a comment, we need to show the
+                               # summary as the headline
+                               if ( $this->section == "new" && $this->summary != "" ) {
+                                       $content = $content->addSectionHeader( $this->summary );
+                               }
+
+                               $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 ) );
 
-                       if ( count( $parserOutput->getWarnings() ) ) {
-                               $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               $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()->getTitle(), null, $parserOptions );
+
+                               $previewHTML = $parserOutput->getText();
+                               $this->mParserOutput = $parserOutput;
+                               $wgOut->addParserOutputNoText( $parserOutput );
+
+                               if ( count( $parserOutput->getWarnings() ) ) {
+                                       $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               }
                        }
+               } catch (MWContentSerializationException $ex) {
+                       $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                       $previewHTML = '';
                }
 
                if ( $this->isConflict ) {
index 6c47e34..4e02ffa 100644 (file)
@@ -644,12 +644,6 @@ class XmlDumpWriter {
                        $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
 
-               if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
-                       $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
-               } else {
-                       $out .= "      <sha1/>\n";
-               }
-
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
@@ -666,6 +660,36 @@ class XmlDumpWriter {
                                "" ) . "\n";
                }
 
+               if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
+                       $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
+               } else {
+                       $out .= "      <sha1/>\n";
+               }
+
+               if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
+                       $content_model = intval( $row->rev_content_model );
+               } else {
+                       // probably using $wgContentHandlerUseDB = false;
+                       // @todo: test!
+                       $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+                       $content_model = ContentHandler::getDefaultModelFor( $title );
+               }
+
+               $name = ContentHandler::getContentModelName( $content_model );
+               $out .= "      " . Xml::element('model', array( 'name' => $name ), strval( $content_model ) ) . "\n";
+
+               if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
+                       $content_format = intval( $row->rev_content_format );
+               } else {
+                       // probably using $wgContentHandlerUseDB = false;
+                       // @todo: test!
+                       $content_handler = ContentHandler::getForModelID( $content_model );
+                       $content_format = $content_handler->getDefaultFormat();
+               }
+
+               $mime = ContentHandler::getContentFormatMimeType( $content_format );
+               $out .= "      " . Xml::element('format', array( 'mime' => $mime ), strval( $content_format ) ) . "\n";
+
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
 
                $out .= "    </revision>\n";
index 1c5e777..a094255 100644 (file)
@@ -138,7 +138,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 b1a5057..23bbc01 100644 (file)
@@ -157,8 +157,10 @@ class ImagePage extends Article {
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
-                       parent::view();
-                       $out->addHTML( Xml::closeElement( 'div' ) );
+
+            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
+
+                       $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
                        $out->setArticleFlag( true );
@@ -268,20 +270,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 $wgImageLimits, $wgEnableUploads, $wgSend404Code;
index 9ebc34c..5b182ad 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 int
+        */
+       function getModel() {
+               if ( is_null( $this->model ) ) {
+                       $this->model = $this->getTitle()->getContentModel();
+               }
+
+               return $this->model;
+       }
+
+       /**
+        * @return int
+        */
+       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 0712ac8..3e8e362 100644 (file)
@@ -39,8 +39,6 @@ class LinksUpdate extends SqlDataUpdate {
                $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
 
        /**
@@ -71,6 +69,7 @@ class LinksUpdate extends SqlDataUpdate {
                }
 
                $this->mParserOutput = $parserOutput;
+
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
@@ -817,7 +816,7 @@ class LinksUpdate extends SqlDataUpdate {
  **/
 class LinksDeletionUpdate extends SqlDataUpdate {
 
-       protected $mPage;     //!< WikiPage the wikipage that was deleted
+       protected $mTitle;     //!< Title the title of page that was deleted
 
        /**
         * Constructor
@@ -826,18 +825,22 @@ class LinksDeletionUpdate extends SqlDataUpdate {
         * @param $parserOutput ParserOutput: output from a full parse of this page
         * @param $recursive Boolean: queue jobs for recursive updates?
         */
-       function __construct( WikiPage $page ) {
+       function __construct( Title $title ) {
                parent::__construct( );
 
-               $this->mPage = $page;
+               $this->mTitle = $title;
+
+               if ( !$title->getArticleID() ) {
+                       throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" );
+               }
        }
 
        /**
         * Do some database updates after deletion
         */
        public function doUpdate() {
-               $title = $this->mPage->getTitle();
-               $id = $this->mPage->getId();
+               $title = $this->mTitle;
+               $id = $title->getArticleID();
 
                # Delete restrictions for it
                $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
@@ -850,7 +853,7 @@ class LinksDeletionUpdate extends SqlDataUpdate {
                        $cats [] = $row->cl_to;
                }
 
-               $this->mPage->updateCategoryCounts( array(), $cats );
+               $this->updateCategoryCounts( array(), $cats );
 
                # If using cascading deletes, we can skip some explicit deletes
                if ( !$this->mDb->cascadingDeletes() ) {
@@ -881,4 +884,16 @@ class LinksDeletionUpdate extends SqlDataUpdate {
                                __METHOD__ );
                }
        }
+
+       /**
+        * Update all the appropriate counts in the category table.
+        * @param $added array associative array of category name => sort key
+        * @param $deleted array associative array of category name => sort key
+        */
+       function updateCategoryCounts( $added, $deleted ) {
+               $a = WikiPage::factory( $this->mTitle );
+               $a->updateCategoryCounts(
+                       array_keys( $added ), array_keys( $deleted )
+               );
+       }
 }
index 3a87a00..b908981 100644 (file)
@@ -202,6 +202,11 @@ class Message {
         */
        protected $title = null;
 
+       /**
+        * Content object representing the message
+        */
+       protected $content = null;
+
        /**
         * @var string
         */
@@ -395,7 +400,15 @@ class Message {
                return $this;
        }
 
-       /**
+       public function content() {
+               if ( !$this->content ) {
+                       $this->content = new MessageContent( $this->key );
+               }
+
+               return $this->content;
+       }
+
+               /**
         * Returns the message parsed from wikitext to HTML.
         * @return String: HTML
         */
index c87a12b..9662fc8 100644 (file)
@@ -209,12 +209,14 @@ class MWNamespace {
         * Returns array of all defined namespaces with their canonical
         * (English) names.
         *
+        * @param bool $rebuild rebuild namespace list (default = false). Used for testing.
+        *
         * @return array
         * @since 1.17
         */
-       public static function getCanonicalNamespaces() {
+       public static function getCanonicalNamespaces( $rebuild = false ) {
                static $namespaces = null;
-               if ( $namespaces === null ) {
+               if ( $namespaces === null || $rebuild ) {
                        global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
                        $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
                        if ( is_array( $wgExtraNamespaces ) ) {
index 6b8aabc..6861605 100644 (file)
@@ -40,6 +40,10 @@ class Revision {
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
+       protected $mContentModel;
+       protected $mContentFormat;
+       protected $mContent;
+       protected $mContentHandler;
 
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
@@ -133,6 +137,8 @@ class Revision {
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
+               global $wgContentHandlerUseDB;
+
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
@@ -145,7 +151,15 @@ 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 ( !$wgContentHandlerUseDB ) {
+                       unset( $attribs['content_model'] );
+                       unset( $attribs['content_format'] );
+               }
+
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
@@ -344,7 +358,9 @@ class Revision {
         * @return array
         */
        public static function selectFields() {
-               return array(
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
@@ -356,8 +372,15 @@ class Revision {
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
-                       'rev_sha1'
+                       'rev_sha1',
                );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'rev_content_format';
+                       $fields[] = 'rev_content_model';
+               }
+
+               return $fields;
        }
 
        /**
@@ -438,6 +461,18 @@ class Revision {
                                $this->mTitle = null;
                        }
 
+                       if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
+                               $this->mContentModel = null; # determine on demand if needed
+                       } else {
+                               $this->mContentModel = intval( $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 = intval( $row->rev_content_format );
+                       }
+
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
@@ -459,6 +494,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'] ) ) { //@todo: when is that set? test with external store setup! check out insertOn() [dk]
+                                       throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" );
+                               }
+
+                               $row['content_model'] = $row['content']->getModel();
+                               # 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;
@@ -471,21 +519,48 @@ class Revision {
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
 
+                       $this->mContentModel = isset( $row['content_model']  )  ? intval( $row['content_model'] )  : null;
+                       $this->mContentFormat    = isset( $row['content_format']  ) ? intval( $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 mContentModel
+                       if ( !empty( $row['content'] ) ) {
+                               $handler = $this->getContentHandler();
+                               $this->mContent = $row['content'];
+
+                               $this->mContentModel = $this->mContent->getModel();
+                               $this->mContentHandler = null;
+
+                               $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
+                       } elseif ( !is_null( $this->mText ) ) {
+                               $handler = $this->getContentHandler();
+                               $this->mContent = $handler->unserializeContent( $this->mText );
+                       }
+
                        $this->mTitle     = null; # Load on demand if needed
-                       $this->mCurrent   = false;
+                       $this->mCurrent   = false; # XXX: really? we are about to create a revision. it will usually then be the current one.
+
                        # If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
-                               $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
+                               if ( !is_null( $this->mContent ) ) {
+                                       $this->mSize = $this->mContent->getSize();
+                               } else {
+                                       #NOTE: this should never happen if we have either text or content object!
+                                       $this->mSize = null;
+                               }
                        }
+
                        # Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
+
+                       $this->getContentModel(); # force lazy init
+                       $this->getContentFormat();    # force lazy init
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
@@ -558,7 +633,7 @@ class Revision {
                if( isset( $this->mTitle ) ) {
                        return $this->mTitle;
                }
-               if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
+               if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
                        $dbr = wfGetDB( DB_SLAVE );
                        $row = $dbr->selectRow(
                                array( 'page', 'revision' ),
@@ -570,6 +645,8 @@ class Revision {
                                $this->mTitle = Title::newFromRow( $row );
                        }
                }
+
+               //@todo: as a last resort, perhaps load from page table, if $this->mPage is given?!
                return $this->mTitle;
        }
 
@@ -753,14 +830,38 @@ class Revision {
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
+        * @deprecated in 1.WD, use getContent() instead
+        * @todo: replace usage in core
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               $content = $this->getContent( $audience, $user );
+               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
+        *
+        * @since 1.WD
+        */
+       public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
-                       return '';
+                       return null;
                } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
-                       return '';
+                       return null;
                } else {
-                       return $this->getRawText();
+                       return $this->getContentInternal();
                }
        }
 
@@ -779,15 +880,107 @@ class Revision {
         * Fetch revision text without regard for view restrictions
         *
         * @return String
+        *
+        * @deprecated since 1.WD. Instead, use Revision::getContent( Revision::RAW ) or Revision::getSerializedData() as appropriate.
         */
        public function getRawText() {
-               if( is_null( $this->mText ) ) {
-                       // Revision text is immutable. Load on demand:
-                       $this->mText = $this->loadText();
-               }
+               wfDeprecated( __METHOD__, "1.WD" );
+
+               return $this->getText( self::RAW );
+       }
+
+       /**
+        * Fetch original serialized data without regard for view restrictions
+        *
+        * @return String
+        *
+        * @since 1.WD
+        */
+       public function getSerializedData() {
                return $this->mText;
        }
 
+       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;
+       }
+
+       /**
+        * Returns the content model for this revision.
+        *
+        * If no content model was stored in the database, $this->getTitle()->getContentModel() is
+        * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
+        * is used as a last resort.
+        *
+        * @return int the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
+        **/
+       public function getContentModel() {
+               if ( !$this->mContentModel ) {
+                       $title = $this->getTitle();
+                       $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
+
+                       assert( !empty( $this->mContentModel ) );
+               }
+
+               return $this->mContentModel;
+       }
+
+       /**
+        * Returns the content format for this revision.
+        *
+        * If no content format was stored in the database, the default format for this
+        * revision's content model is returned.
+        *
+        * @return int the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
+        **/
+       public function getContentFormat() {
+               if ( !$this->mContentFormat ) {
+                       $handler = $this->getContentHandler();
+                       $this->mContentFormat = $handler->getDefaultFormat();
+
+                       assert( !empty( $this->mContentFormat ) );
+               }
+
+               return $this->mContentFormat;
+       }
+
+       /**
+        * Returns the content handler appropriate for this revision's content model.
+        *
+        * @return ContentHandler
+        */
+       public function getContentHandler() {
+               if ( !$this->mContentHandler ) {
+                       $model = $this->getContentModel();
+                       $this->mContentHandler = ContentHandler::getForModelID( $model );
+
+                       $format = $this->getContentFormat();
+
+                       if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
+                               $formatName = ContentHandler::getContentFormatMimeType( $format );
+                               $modelName = ContentHandler::getContentModelName( $model );
+
+                               throw new MWException( "Oops, the content format #$format ($formatName) is not supported for this content model, #$model ($modelName)" );
+                       }
+               }
+
+               return $this->mContentHandler;
+       }
+
        /**
         * @return String
         */
@@ -970,7 +1163,7 @@ class Revision {
         * @return Integer
         */
        public function insertOn( $dbw ) {
-               global $wgDefaultExternalStore;
+               global $wgDefaultExternalStore, $wgContentHandlerUseDB;
 
                wfProfileIn( __METHOD__ );
 
@@ -1009,27 +1202,35 @@ 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,
                );
 
+               if ( $wgContentHandlerUseDB ) {
+                       $row[ 'rev_content_model' ] = $this->getContentModel();
+                       $row[ 'rev_content_format' ] = $this->getContentFormat();
+               }
+
+               $this->checkContentModel();
+
+               $dbw->insert( 'revision', $row, __METHOD__ );
+
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
 
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
@@ -1038,6 +1239,57 @@ class Revision {
                return $this->mId;
        }
 
+       protected function checkContentModel() {
+               global $wgContentHandlerUseDB;
+
+               $title = $this->getTitle(); //note: returns null for revisions that have not yet been inserted.
+
+               $model = $this->getContentModel();
+               $format = $this->getContentFormat();
+               $handler = $this->getContentHandler();
+
+               if ( !$handler->isSupportedFormat( $format ) ) {
+                       $t = $title->getPrefixedDBkey();
+                       $modelName = ContentHandler::getContentModelName( $model );
+                       $formatName = ContentHandler::getContentFormatMimeType( $format );
+
+                       throw new MWException( "Can't use format #$format ($formatName) with content model #$model ($modelName) on $t" );
+               }
+
+               if ( !$wgContentHandlerUseDB && $title ) {
+                       // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $defaultHandler = ContentHandler::getForModelID( $defaultModel );
+                       $defaultFormat = $defaultHandler->getDefaultFormat();
+
+                       if ( $this->getContentModel() != $defaultModel ) {
+                               $defaultModelName = ContentHandler::getContentModelName( $defaultModel );
+                               $modelName = ContentHandler::getContentModelName( $model );
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: model is #$model ($modelName), default for $t is #$defaultModel ($defaultModelName)" );
+                       }
+
+                       if ( $this->getContentFormat() != $defaultFormat ) {
+                               $defaultFormatName = ContentHandler::getContentFormatMimeType( $defaultFormat );
+                               $formatName = ContentHandler::getContentFormatMimeType( $format );
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: format is #$format ($formatName), default for $t is #$defaultFormat ($defaultFormatName)" );
+                       }
+               }
+
+               $content = $this->getContent( Revision::RAW );
+
+               if ( !$content->isValid() ) {
+                       $t = $title->getPrefixedDBkey();
+                       $modelName = ContentHandler::getContentModelName( $model );
+
+                       throw new MWException( "Content of $t is not valid! Content model is #$model ($modelName)" );
+               }
+       }
+
        /**
         * Get the base 36 SHA-1 value for a string of text
         * @param $text String
@@ -1122,12 +1374,21 @@ class Revision {
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
+               global $wgContentHandlerUseDB;
+
                wfProfileIn( __METHOD__ );
 
+               $fields = array( 'page_latest', 'page_namespace', 'page_title',
+                                               'rev_text_id', 'rev_len', 'rev_sha1' );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'rev_content_model';
+                       $fields[] = 'rev_content_format';
+               }
+
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
-                       array( 'page_latest', 'page_namespace', 'page_title',
-                               'rev_text_id', 'rev_len', 'rev_sha1' ),
+                       $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
@@ -1135,7 +1396,7 @@ class Revision {
                        __METHOD__ );
 
                if( $current ) {
-                       $revision = new Revision( array(
+                       $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
@@ -1143,7 +1404,14 @@ class Revision {
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
                                'sha1'       => $current->rev_sha1
-                               ) );
+                       );
+
+                       if ( $wgContentHandlerUseDB ) {
+                               $row[ 'content_model' ] = $current->rev_content_model;
+                               $row[ 'content_format' ] = $current->rev_content_format;
+                       }
+
+                       $revision = new Revision( $row );
                        $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
index 481f480..b0849db 100644 (file)
@@ -65,6 +65,7 @@ class Title {
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
+       var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
@@ -200,6 +201,27 @@ class Title {
                }
        }
 
+       /**
+        * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
+        * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
+        *
+        * @return array
+        */
+       protected static function getSelectFields() {
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
+                       'page_namespace', 'page_title', 'page_id',
+                       'page_len', 'page_is_redirect', 'page_latest',
+               );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+
+               return $fields;
+       }
+
        /**
         * Create a new Title from an article ID
         *
@@ -211,10 +233,7 @@ class Title {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
@@ -240,10 +259,7 @@ class Title {
 
                $res = $dbr->select(
                        'page',
-                       array(
-                               'page_namespace', 'page_title', 'page_id',
-                               'page_len', 'page_is_redirect', 'page_latest',
-                       ),
+                       self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
@@ -283,11 +299,16 @@ class Title {
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
+                       if ( isset( $row->page_content_model ) )
+                               $this->mContentModel = intval( $row->page_content_model );
+                       else
+                               $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+                       $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
 
@@ -313,6 +334,7 @@ class Title {
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
+               $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
 
@@ -363,9 +385,11 @@ class Title {
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
+        * @deprecated since 1.WD, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
-               return self::newFromRedirectInternal( $text );
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectTarget();
        }
 
        /**
@@ -376,10 +400,11 @@ class Title {
         *
         * @param $text String Text with possible redirect
         * @return Title
+        * @deprecated since 1.WD, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
-               $titles = self::newFromRedirectArray( $text );
-               return $titles ? array_pop( $titles ) : null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getUltimateRedirectTarget();
        }
 
        /**
@@ -390,71 +415,11 @@ class Title {
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
+        * @deprecated since 1.WD, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
-               global $wgMaxRedirects;
-               $title = self::newFromRedirectInternal( $text );
-               if ( is_null( $title ) ) {
-                       return null;
-               }
-               // recursive check to follow double redirects
-               $recurse = $wgMaxRedirects;
-               $titles = array( $title );
-               while ( --$recurse > 0 ) {
-                       if ( $title->isRedirect() ) {
-                               $page = WikiPage::factory( $title );
-                               $newtitle = $page->getRedirectTarget();
-                       } else {
-                               break;
-                       }
-                       // Redirects to some special pages are not permitted
-                       if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
-                               // the new title passes the checks, so make that our current title so that further recursion can be checked
-                               $title = $newtitle;
-                               $titles[] = $newtitle;
-                       } else {
-                               break;
-                       }
-               }
-               return $titles;
-       }
-
-       /**
-        * Really extract the redirect destination
-        * Do not call this function directly, use one of the newFromRedirect* functions above
-        *
-        * @param $text String Text with possible redirect
-        * @return Title
-        */
-       protected static function newFromRedirectInternal( $text ) {
-               global $wgMaxRedirects;
-               if ( $wgMaxRedirects < 1 ) {
-                       //redirects are disabled, so quit early
-                       return null;
-               }
-               $redir = MagicWord::get( 'redirect' );
-               $text = trim( $text );
-               if ( $redir->matchStartAndRemove( $text ) ) {
-                       // Extract the first link and see if it's usable
-                       // Ensure that it really does come directly after #REDIRECT
-                       // Some older redirects included a colon, so don't freak about that!
-                       $m = array();
-                       if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
-                               // Strip preceding colon used to "escape" categories, etc.
-                               // and URL-decode links
-                               if ( strpos( $m[1], '%' ) !== false ) {
-                                       // Match behavior of inline link parsing here;
-                                       $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
-                               }
-                               $title = Title::newFromText( $m[1] );
-                               // If the title is a redirect to bad special pages or is invalid, return null
-                               if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
-                                       return null;
-                               }
-                               return $title;
-                       }
-               }
-               return null;
+               $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+               return $content->getRedirectChain();
        }
 
        /**
@@ -702,6 +667,38 @@ class Title {
                return $this->mNamespace;
        }
 
+       /**
+        * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+        *
+        * @return Integer: Content model id
+        */
+       public function getContentModel() {
+               if ( !$this->mContentModel ) {
+                       $linkCache = LinkCache::singleton();
+                       $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+               }
+
+               if ( !$this->mContentModel ) {
+                       $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+               }
+
+               if( !$this->mContentModel ) {
+                       throw new MWException( "failed to determin content model!" );
+               }
+
+               return $this->mContentModel;
+       }
+
+       /**
+        * Convenience method for checking a title's content model name
+        *
+        * @param int $id
+        * @return Boolean true if $this->getContentModel() == $id
+        */
+       public function hasContentModel( $id ) {
+               return $this->getContentModel() == $id;
+       }
+
        /**
         * Get the namespace text
         *
@@ -945,22 +942,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;
        }
 
        /**
@@ -968,7 +974,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 ) ) );
        }
 
        /**
@@ -991,7 +999,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 ) );
        }
 
        /**
@@ -1000,7 +1009,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 ) );
        }
 
        /**
@@ -2824,8 +2834,16 @@ class Title {
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
+
                $linkCache = LinkCache::singleton();
-               $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mRedirect = (bool)$cached;
 
                return $this->mRedirect;
        }
@@ -2846,7 +2864,14 @@ class Title {
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mLength = intval( $cached );
 
                return $this->mLength;
        }
@@ -2866,7 +2891,14 @@ class Title {
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
-               $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
+               $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
+               if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+                       # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+                       #      as a stop gap, perhaps log this, but don't throw an exception?
+                       throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+               }
+
+               $this->mLatestID = intval( $cached );
 
                return $this->mLatestID;
        }
@@ -2895,6 +2927,7 @@ class Title {
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
+               $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
 
@@ -3133,7 +3166,7 @@ class Title {
 
                $res = $db->select(
                        array( 'page', $table ),
-                       array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
@@ -3183,6 +3216,8 @@ class Title {
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+               global $wgContentHandlerUseDB;
+
                $id = $this->getArticleID();
 
                # If the page doesn't exist; there can't be any link from this page
@@ -3199,9 +3234,12 @@ class Title {
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
 
+               $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
                $res = $db->select(
                        array( $table, 'page' ),
-                       array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+                       $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
@@ -3763,10 +3801,16 @@ class Title {
         * @return Bool
         */
        public function isSingleRevRedirect() {
+               global $wgContentHandlerUseDB;
+
                $dbw = wfGetDB( DB_MASTER );
+
                # Is it a redirect?
+               $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
+               if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
                $row = $dbw->selectRow( 'page',
-                       array( 'page_is_redirect', 'page_latest', 'page_id' ),
+                       $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
@@ -3775,6 +3819,7 @@ class Title {
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
+               $this->mContentModel = $row && isset( $row->page_content_model ) ? intval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
@@ -3819,24 +3864,24 @@ class Title {
                if( !is_object( $rev ) ){
                        return false;
                }
-               $text = $rev->getText();
+               $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
-               $m = array();
-               if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
-                       $redirTitle = Title::newFromText( $m[1] );
-                       if ( !is_object( $redirTitle ) ||
-                               ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
-                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+               $redirTitle = $content->getRedirectTarget();
+
+               if ( $redirTitle ) {
+                       if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
+                               $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
+                       } else {
+                               return true;
                        }
                } else {
-                       # Fail safe
-                       wfDebug( __METHOD__ . ": failsafe\n" );
+                       # Fail safe (not a redirect after all. strange.)
+                       wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
-               return true;
        }
 
        /**
index 9fb1522..0114cce 100644 (file)
@@ -41,7 +41,9 @@ class WikiFilePage extends WikiPage {
        }
 
        public function getActionOverrides() {
-               return array( 'revert' => 'RevertFileAction' );
+               $overrides = parent::getActionOverrides();
+               $overrides[ 'revert' ] = 'RevertFileAction';
+               return $overrides;
        }
 
        /**
@@ -103,13 +105,12 @@ class WikiFilePage extends WikiPage {
        }
 
        /**
-        * @param bool $text
         * @return bool
         */
-       public function isRedirect( $text = false ) {
+       public function isRedirect( ) {
                $this->loadFile();
                if ( $this->mFile->isLocal() ) {
-                       return parent::isRedirect( $text );
+                       return parent::isRedirect();
                }
 
                return (bool)$this->mFile->getRedirected();
index b5f4c1d..5a8f5e6 100644 (file)
@@ -230,7 +230,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::getForModelID( $this->getContentModel() );
+        *
+        * @return ContentHandler
+        *
+        * @since 1.WD
+        */
+       public function getContentHandler() {
+               return ContentHandler::getForModelID( $this->getContentModel() );
        }
 
        /**
@@ -265,7 +279,9 @@ class WikiPage extends Page {
         * @return array
         */
        public static function selectFields() {
-               return array(
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
                        'page_id',
                        'page_namespace',
                        'page_title',
@@ -278,6 +294,12 @@ class WikiPage extends Page {
                        'page_latest',
                        'page_len',
                );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+
+               return $fields;
        }
 
        /**
@@ -444,21 +466,41 @@ class WikiPage extends Page {
        }
 
        /**
-        * Tests if the article text represents a redirect
+        * Tests if the article content represents a redirect
         *
-        * @param $text mixed string containing article contents, or boolean
         * @return bool
         */
-       public function isRedirect( $text = false ) {
-               if ( $text === false ) {
-                       if ( !$this->mDataLoaded ) {
-                               $this->loadPageData();
-                       }
+       public function isRedirect( ) {
+               $content = $this->getContent();
+               if ( !$content ) return false;
 
-                       return (bool)$this->mIsRedirect;
-               } else {
-                       return Title::newFromRedirect( $text ) !== null;
+               return $content->isRedirect();
+       }
+
+       /**
+        * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
+        *
+        * 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
+        *
+        * @since 1.WD
+        */
+       public function getContentModel() {
+               if ( $this->exists() ) {
+                       # look at the revision's actual content model
+                       $rev = $this->getRevision();
+
+                       if ( $rev !== null ) {
+                               return $rev->getContentModel();
+                       } else {
+                               wfWarn( "Page exists but has no revision!" );
+                       }
                }
+
+               # use the default model for this page
+               return $this->mTitle->getContentModel();
        }
 
        /**
@@ -573,6 +615,25 @@ 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
+        *
+        * @since 1.WD
+        */
+       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...
         *
@@ -580,9 +641,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.WD, 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.WD' );
+
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
                        return $this->mLastRevision->getText( $audience );
@@ -594,13 +658,12 @@ 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.WD, getContent() should be used instead.
         */
        public function getRawText() {
-               $this->loadLastEdit();
-               if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getRawText();
-               }
-               return false;
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               return $this->getText( Revision::RAW );
        }
 
        /**
@@ -611,7 +674,7 @@ class WikiPage extends Page {
                if ( !$this->mTimestamp ) {
                        $this->loadLastEdit();
                }
-               
+
                return wfTimestamp( TS_MW, $this->mTimestamp );
        }
 
@@ -742,32 +805,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':
+               $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.
-                               return (bool)count( $editInfo->output->getLinks() );
+                               $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
-                               return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                               $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
                                        array( 'pl_from' => $this->getId() ), __METHOD__ );
                        }
                }
+
+               return $content->isCountable( $hasLinks );
        }
 
        /**
@@ -813,7 +878,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;
                }
@@ -1009,7 +1075,7 @@ class WikiPage extends Page {
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
-                       && $this->mTitle->isWikitextPage();
+                       && $this->getContentHandler()->isParserCacheSupported();
        }
 
        /**
@@ -1020,6 +1086,7 @@ 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)
+        *
         * @return ParserOutput or false if the revision was not found
         */
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
@@ -1097,8 +1164,16 @@ class WikiPage extends Page {
                }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
+                       //@todo: move this logic to MessageCache
+
                        if ( $this->mTitle->exists() ) {
-                               $text = $this->getRawText();
+                               // NOTE: use transclusion text for messages.
+                               //       This is consistent with  MessageCache::getMsgFromNamespace()
+
+                               $content = $this->getContent();
+                               $text = $content === null ? null : $content->getWikitextForTransclusion();
+
+                               if ( $text === null ) $text = false;
                        } else {
                                $text = false;
                        }
@@ -1163,11 +1238,13 @@ class WikiPage extends Page {
         * @private
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
+               global $wgContentHandlerUseDB;
+
                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() );
 
@@ -1177,14 +1254,20 @@ class WikiPage extends Page {
                }
 
                $now = wfTimestampNow();
+               $row = array( /* SET */
+                       'page_latest'      => $revision->getId(),
+                       'page_touched'     => $dbw->timestamp( $now ),
+                       'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
+                       'page_is_redirect' => $rt !== null ? 1 : 0,
+                       'page_len'         => $len,
+               );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $row[ 'page_content_model' ] = $revision->getContentModel();
+               }
+
                $dbw->update( 'page',
-                       array( /* SET */
-                               'page_latest'      => $revision->getId(),
-                               'page_touched'     => $dbw->timestamp( $now ),
-                               'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
-                               'page_is_redirect' => $rt !== null ? 1 : 0,
-                               'page_len'         => $len,
-                       ),
+                       $row,
                        $conditions,
                        __METHOD__ );
 
@@ -1196,7 +1279,7 @@ class WikiPage extends Page {
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        # Update the LinkCache.
-                       LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
+                       LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest, $revision->getContentModel() );
                }
 
                wfProfileOut( __METHOD__ );
@@ -1286,27 +1369,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.WD: 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();
+               wfDeprecated( __METHOD__, '1.WD' );
 
-               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;
        }
 
        /**
@@ -1314,18 +1399,62 @@ 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 String new complete article text, or null if error
+        *
+        * @deprecated since 1.WD, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               if ( !$this->supportsSections() ) {
+                       return null;
+               }
+
+               $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); # could even make section title, but that's not required.
+
+               $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
+
+               return ContentHandler::getContentText( $newContent );
+       }
+
+       /**
+        * Returns true iff this page's content model supports sections.
+        *
+        * @return boolean whether sections are supported.
+        *
+        * @todo: the skin should check this and not offer section functionality if sections are not supported.
+        * @todo: the EditPage should check this and not offer section functionality if sections are not supported.
+        */
+       public function supportsSections() {
+               return $this->getContentHandler()->supportsSections();
+       }
+
+       /**
+        * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
+        * @param $content Content: new content 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 Content new complete article content, or null if error
+        *
+        * @since 1.WD
+        */
+       public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
 
+               if ( !$this->supportsSections() ) {
+                       #XXX: log this?
+                       return null;
+               }
+
                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' ) {
-                               $oldtext = $this->getRawText();
-                               if ( $oldtext === false ) {
+                               $oldContent = $this->getContent();
+                               if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
@@ -1341,27 +1470,14 @@ class WikiPage extends Page {
                                        return null;
                                }
 
-                               $oldtext = $rev->getText();
+                               $oldContent = $rev->getContent();
                        }
 
-                       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 = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
 
                wfProfileOut( __METHOD__ );
-               return $text;
+               return $newContent;
        }
 
        /**
@@ -1426,8 +1542,66 @@ 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
+        *
+        * @deprecated since 1.WD: use doEditContent() instead.
         */
-       public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
+       public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               $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
+        *
+        * @since 1.WD
+        */
+       public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
+                                                                  User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
 
                # Low-level sanity check
@@ -1447,10 +1621,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,
+                       $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+
+               if ( $hook_ok && Hooks::isRegistered( '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,
+                               $flags & EDIT_MINOR, null, null, &$flags, &$status ) ); #TODO: survey extensions using this hook
+
+                       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' );
@@ -1464,20 +1653,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();
@@ -1505,16 +1699,23 @@ 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->getModel(),
+                               'content_format' => $serialisation_format,
+                       ) ); #XXX: pass content object?!
 
-                       $changed = ( strcmp( $text, $oldtext ) != 0 );
+                       $changed = !$content->equals( $old_content );
 
                        if ( $changed ) {
+                               if ( !$content->isValid() ) {
+                                       throw new MWException( "New content failed validity check!" );
+                               }
+
                                $dbw->begin( __METHOD__ );
                                $revisionId = $revision->insertOn( $dbw );
 
@@ -1577,8 +1778,14 @@ class WikiPage extends Page {
                        }
 
                        # Update links tables, site stats, etc.
-                       $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
-                               'oldcountable' => $oldcountable ) );
+                       $this->doEditUpdates(
+                               $revision,
+                               $user,
+                               array(
+                                       'changed' => $changed,
+                                       'oldcountable' => $oldcountable
+                               )
+                       );
 
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
@@ -1610,10 +1817,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->getModel(),
+                               'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
 
@@ -1631,7 +1841,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 ) {
@@ -1644,7 +1854,10 @@ 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,
+                               $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+
+                       wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
 
@@ -1656,7 +1869,10 @@ 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,
+                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+
+               wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 
                # Promote user to any groups they meet the criteria for
@@ -1686,15 +1902,39 @@ class WikiPage extends Page {
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
-        * @return bool|object
+        *
+        * @deprecated in 1.WD: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
+               wfDeprecated( __METHOD__, '1.WD' );
+               $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
+        *
+        * @since 1.WD
+        */
+       public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                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;
@@ -1705,11 +1945,21 @@ 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 ); #XXX: do we need this??
+               $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();
+
+               $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
+
+               $edit->newContent = $content;
+               $edit->oldContent = $this->getContent( Revision::RAW );
+
+               #NOTE: B/C for hooks! don't use these fields!
+               $edit->newText = ContentHandler::getContentText( $edit->newContent );
+               $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
 
                $this->mPreparedEdit = $edit;
 
@@ -1722,7 +1972,6 @@ class WikiPage extends Page {
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
-        * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
@@ -1739,13 +1988,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;
@@ -1758,7 +2007,8 @@ class WikiPage extends Page {
                }
 
                # Update the links tables and other secondary data
-               $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
+               $contentHandler = $revision->getContentHandler();
+               $updates = $contentHandler->getSecondaryDataUpdates( $content, $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
 
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
@@ -1803,7 +2053,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() ) ); #TODO: let the search engine decide what to do with the content object
 
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
@@ -1829,7 +2079,10 @@ class WikiPage extends Page {
                }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
-                       MessageCache::singleton()->replace( $shortTitle, $text );
+                       $msgtext = $content->getWikitextForTransclusion(); #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'] ) {
@@ -1850,17 +2103,40 @@ class WikiPage extends Page {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
+        *
+        * @deprecated since 1.WD, use doEditContent() instead.
         */
        public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+               wfDeprecated( __METHOD__, "1.WD" );
+
+               $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,
-               ) );
+               ) ); #XXX: set the content object?
                $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
 
@@ -2143,7 +2419,7 @@ class WikiPage extends Page {
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
-               global $wgUser;
+               global $wgUser, $wgContentHandlerUseDB;
 
                wfDebug( __METHOD__ . "\n" );
 
@@ -2176,6 +2452,9 @@ class WikiPage extends Page {
                        $bitfield = 'rev_deleted';
                }
 
+               // we need to remember the old content so we can use it to generate all deletion updates.
+               $content = $this->getContent( Revision::RAW );
+
                $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
                // For now, shunt the revision data into the archive table.
@@ -2188,25 +2467,34 @@ class WikiPage extends Page {
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
+
+               $row = array(
+                       'ar_namespace'  => 'page_namespace',
+                       'ar_title'      => 'page_title',
+                       'ar_comment'    => 'rev_comment',
+                       'ar_user'       => 'rev_user',
+                       'ar_user_text'  => 'rev_user_text',
+                       'ar_timestamp'  => 'rev_timestamp',
+                       'ar_minor_edit' => 'rev_minor_edit',
+                       'ar_rev_id'     => 'rev_id',
+                       'ar_parent_id'  => 'rev_parent_id',
+                       'ar_text_id'    => 'rev_text_id',
+                       'ar_text'       => '\'\'', // Be explicit to appease
+                       'ar_flags'      => '\'\'', // MySQL's "strict mode"...
+                       'ar_len'        => 'rev_len',
+                       'ar_page_id'    => 'page_id',
+                       'ar_deleted'    => $bitfield,
+                       'ar_sha1'       => 'rev_sha1',
+               );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $row[ 'ar_content_model' ] = 'rev_content_model';
+                       $row[ 'ar_content_format' ] = 'rev_content_format';
+               }
+
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
+                       $row,
                        array(
-                               'ar_namespace'  => 'page_namespace',
-                               'ar_title'      => 'page_title',
-                               'ar_comment'    => 'rev_comment',
-                               'ar_user'       => 'rev_user',
-                               'ar_user_text'  => 'rev_user_text',
-                               'ar_timestamp'  => 'rev_timestamp',
-                               'ar_minor_edit' => 'rev_minor_edit',
-                               'ar_rev_id'     => 'rev_id',
-                               'ar_parent_id'  => 'rev_parent_id',
-                               'ar_text_id'    => 'rev_text_id',
-                               'ar_text'       => '\'\'', // Be explicit to appease
-                               'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                               'ar_len'        => 'rev_len',
-                               'ar_page_id'    => 'page_id',
-                               'ar_deleted'    => $bitfield,
-                               'ar_sha1'       => 'rev_sha1'
-                       ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
@@ -2221,7 +2509,7 @@ class WikiPage extends Page {
                        return WikiPage::DELETE_NO_REVISIONS;
                }
 
-               $this->doDeleteUpdates( $id );
+               $this->doDeleteUpdates( $id, $content );
 
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
@@ -2245,13 +2533,15 @@ class WikiPage extends Page {
         * Do some database updates after deletion
         *
         * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
+        * @param $content Content: optional page content to be used when determining the required updates.
+        *        This may be needed because $this->getContent() may already return null when the page proper was deleted.
         */
-       public function doDeleteUpdates( $id ) {
+       public function doDeleteUpdates( $id, Content $content = null ) {
                # update site status
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
 
                # remove secondary indexes, etc
-               $updates = $this->getDeletionUpdates( );
+               $updates = $this->getDeletionUpdates( $content );
                DataUpdate::runUpdates( $updates );
 
                # Clear caches
@@ -2264,23 +2554,13 @@ class WikiPage extends Page {
                $this->mTitle->resetArticleID( 0 );
        }
 
-       public function getDeletionUpdates() {
-               $updates = array(
-                       new LinksDeletionUpdate( $this ),
-               );
-
-               //@todo: make a hook to add update objects
-               //NOTE: deletion updates will be determined by the ContentHandler in the future
-               return $updates;
-       }
-
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
         * roll back to, e.g. user is the sole contributor. This function
         * performs permissions checks on $user, then calls commitRollback()
         * to do the dirty work
-        * 
+        *
         * @todo: seperate the business/permission stuff out from backend code
         *
         * @param $fromP String: Name of the user whose edits to rollback.
@@ -2439,7 +2719,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 {
@@ -2585,57 +2865,22 @@ 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.WD, 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' ) ) ) );
+               wfDeprecated( __METHOD__, '1.WD' );
 
-                       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 );
-               }
+               $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+               $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
+               $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
 
-               # 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 );
        }
 
        /**
@@ -2644,93 +2889,15 @@ 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.WD, use ContentHandler::getAutoDeleteReason() instead
         */
        public function getAutoDeleteReason( &$hasHistory ) {
-               global $wgContLang;
-
-               // Get the last revision
-               $rev = $this->getRevision();
-
-               if ( is_null( $rev ) ) {
-                       return false;
-               }
-
-               // Get the article's contents
-               $contents = $rev->getText();
-               $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 ( $contents == '' ) {
-                       $prev = $rev->getPrevious();
-
-                       if ( $prev )    {
-                               $contents = $prev->getText();
-                               $blank = true;
-                       }
-               }
-
-               $dbw = wfGetDB( DB_MASTER );
+               #NOTE: stub for backwards-compatibility.
 
-               // Find out if there was only one contributor
-               // Only scan the last 20 revisions
-               $res = $dbw->select( 'revision', 'rev_user_text',
-                       array( 'rev_page' => $this->getID(), $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 '';
-               }
-
-               // Replace newlines with spaces to prevent uglyness
-               $contents = preg_replace( "/[\n\r]/", ' ', $contents );
-               // Calculate the maximum amount of chars to get
-               // Max content length = max comment length - length of the comment (excl. $1)
-               $maxLength = 255 - ( strlen( $reason ) - 2 );
-               $contents = $wgContLang->truncate( $contents, $maxLength );
-               // Remove possible unfinished links
-               $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
-               // Now replace the '$1' placeholder
-               $reason = str_replace( '$1', $contents, $reason );
+               wfDeprecated( __METHOD__, '1.WD' );
 
-               return $reason;
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+               return $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
 
        /**
@@ -2971,6 +3138,27 @@ class WikiPage extends Page {
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
+
+       /**
+        * Returns a list of updates to be performed when this page is deleted. The updates should remove any infomration
+        * about this page from secondary data stores such as links tables.
+        *
+        * @param Content|null $content optional Content object for determining the necessary updates
+        * @return Array an array of DataUpdates objects
+        */
+       public function getDeletionUpdates( Content $content = null ) {
+               if ( !$content ) {
+                       // load content object, which may be used to determine the necessary updates
+                       // XXX: the content may not be needed to determine the updates, then this would be overhead.
+                       $content = $this->getContent( Revision::RAW );
+               }
+
+               $updates = $this->getContentHandler()->getDeletionUpdates( $content, $this->mTitle );
+
+               wfRunHooks( 'WikiPageDeletionUpdates', array( $this, &$updates ) );
+               return $updates;
+       }
+
 }
 
 class PoolWorkArticleView extends PoolCounterWork {
@@ -2996,9 +3184,9 @@ class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
 
        /**
-        * @var string|null
+        * @var Content|null
         */
-       private $text;
+       private $content = null;
 
        /**
         * @var ParserOutput|bool
@@ -3022,14 +3210,20 @@ 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
         */
-       function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
+       function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
+               if ( is_string($content) ) { #BC: old style call
+                       $modelId = $page->getRevision()->getContentModel();
+                       $format = $page->getRevision()->getContentFormat();
+                       $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
+               }
+
                $this->page = $page;
                $this->revid = $revid;
                $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 );
        }
@@ -3065,25 +3259,26 @@ 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 );
+               $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
 
                # Timing hack
@@ -3152,3 +3347,4 @@ class PoolWorkArticleView extends PoolCounterWork {
                return false;
        }
 }
+
index 08a33f4..1c97b26 100644 (file)
@@ -71,4 +71,4 @@ class SubmitAction extends EditAction {
                parent::show();
        }
 
-}
+}
\ No newline at end of file
index 174ca3f..beabb3b 100644 (file)
@@ -148,11 +148,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->getModel() . "` which is not supported via this interface." );
+                                       die();
+                               }
+
                                $section = $request->getIntOrNull( 'section' );
                                if ( $section !== null ) {
-                                       $text = $wgParser->getSection( $text, $section );
+                                       $content = $content->getSection( $section );
                                }
+
+                               $text = $content->getNativeData();
                        }
                }
 
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..bd740e7 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::getForModelID( $rev1->getContentModel() );
+        $de = $contentHandler->createDifferenceEngine( $this->getContext(),
                        $rev1,
                        $rev2,
                        null, // rcid
index cefdaac..121cb48 100644 (file)
@@ -111,7 +111,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 0b7ac41..a9727e2 100644 (file)
@@ -60,7 +60,7 @@ class ApiEditPage extends ApiBase {
                        if ( $titleObj->isRedirect() ) {
                                $oldTitle = $titleObj;
 
-                               $titles = Title::newFromRedirectArray( Revision::newFromTitle( $oldTitle )->getText( Revision::FOR_THIS_USER ) );
+                               $titles = Revision::newFromTitle( $oldTitle )->getContent( Revision::FOR_THIS_USER )->getRedirectChain();
                                // array_shift( $titles );
 
                                $redirValues = array();
@@ -109,21 +109,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'];
                }
 
@@ -240,7 +242,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 7414a97..fbf2f3e 100644 (file)
@@ -104,6 +104,7 @@ class ApiMain extends ApiBase {
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
+               'none' => 'ApiFormatNone',
        );
 
        /**
index dbcd43c..72808e0 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();
@@ -322,9 +322,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 );
@@ -336,13 +336,14 @@ class ApiParse extends ApiBase {
                                $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
                        }
                        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 8e9c198..66aa3b0 100644 (file)
@@ -91,10 +91,10 @@ class ApiPurge extends ApiBase {
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
                                        $popts->setTidy( true );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
-                                               true, true, $page->getLatest() );
+                                               true, true, $page->getLatest() ); #FIXME: content!
 
                                        # Update the links tables
-                                       $updates = $p_result->getSecondaryDataUpdates( $title );
+                                       $updates = $p_result->getSecondaryDataUpdates( $title ); #FIXME: content handler
                                        DataUpdate::runUpdates( $updates );
 
                                        $r['linkupdate'] = '';
index 44cb46e..4ac7a44 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;
@@ -513,11 +513,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 f759c02..a48d007 100644 (file)
@@ -74,7 +74,7 @@ class LinkCache {
         * Get a field of a title object from cache.
         * If this link is not good, it will return NULL.
         * @param $title Title
-        * @param $field String: ('length','redirect','revision')
+        * @param $field String: ('length','redirect','revision','model')
         * @return mixed
         */
        public function getGoodLinkFieldObj( $title, $field ) {
@@ -102,14 +102,16 @@ class LinkCache {
         * @param $len Integer: text's length
         * @param $redir Integer: whether the page is a redirect
         * @param $revision Integer: latest revision's ID
+        * @param $model Integer: latest revision's content model ID
         */
-       public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false ) {
+       public function addGoodLinkObj( $id, $title, $len = -1, $redir = null, $revision = false, $model = false ) {
                $dbkey = $title->getPrefixedDbKey();
                $this->mGoodLinks[$dbkey] = intval( $id );
                $this->mGoodLinkFields[$dbkey] = array(
                        'length' => intval( $len ),
                        'redirect' => intval( $redir ),
-                       'revision' => intval( $revision ) );
+                       'revision' => intval( $revision ),
+                       'model' => intval( $model ) );
        }
 
        /**
@@ -117,7 +119,7 @@ class LinkCache {
         * @since 1.19
         * @param $title Title
         * @param $row object which has the fields page_id, page_is_redirect,
-        *  page_latest
+        *  page_latest and page_content_model
         */
        public function addGoodLinkObjFromRow( $title, $row ) {
                $dbkey = $title->getPrefixedDbKey();
@@ -126,6 +128,7 @@ class LinkCache {
                        'length' => intval( $row->page_len ),
                        'redirect' => intval( $row->page_is_redirect ),
                        'revision' => intval( $row->page_latest ),
+                       'model' => !empty( $row->page_content_model ) ? intval( $row->page_content_model ) : null,
                );
        }
 
@@ -178,7 +181,8 @@ class LinkCache {
         * @return Integer
         */
        public function addLinkObj( $nt ) {
-               global $wgAntiLockFlags;
+               global $wgAntiLockFlags, $wgContentHandlerUseDB;
+
                wfProfileIn( __METHOD__ );
 
                $key = $nt->getPrefixedDBkey();
@@ -210,8 +214,10 @@ class LinkCache {
                        $options = array();
                }
 
-               $s = $db->selectRow( 'page',
-                       array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+               $f = array( 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+               if ( $wgContentHandlerUseDB ) $f[] = 'page_content_model';
+
+               $s = $db->selectRow( 'page', $f,
                        array( 'page_namespace' => $nt->getNamespace(), 'page_title' => $nt->getDBkey() ),
                        __METHOD__, $options );
                # Set fields...
index 24f32d6..70b53f9 100644 (file)
@@ -768,16 +768,27 @@ class MessageCache {
                # Try loading it from the database
                $revision = Revision::newFromTitle( Title::makeTitle( NS_MEDIAWIKI, $title ) );
                if ( $revision ) {
-                       $message = $revision->getText();
-                       if ($message === false) {
+                       $content = $revision->getContent();
+                       if ( !$content ) {
                                // A possibly temporary loading failure.
                                wfDebugLog( 'MessageCache', __METHOD__ . ": failed to load message page text for {$title} ($code)" );
+                               $message = null; // no negative caching
                        } else {
-                               $this->mCache[$code][$title] = ' ' . $message;
-                               $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
+                               $message = $content->getWikitextForTransclusion(); #XXX: is this the reight way to turn a Content object into a mesage?
+
+                               if ( $message === false || $message === null ) {
+                                       wfDebugLog( 'MessageCache', __METHOD__ . ": message content doesn't provide wikitext (content model: #" . $content->getContentHandler() . ")" );
+                                       $message = false; // negative caching
+                               } else {
+                                       $this->mCache[$code][$title] = ' ' . $message;
+                                       $this->mMemc->set( $titleKey, ' ' . $message, $this->mExpiry );
+                               }
                        }
                } else {
-                       $message = false;
+                       $message = false; // negative caching
+               }
+
+               if ( $message === false ) { // negative caching
                        $this->mCache[$code][$title] = '!NONEXISTENT';
                        $this->mMemc->set( $titleKey, '!NONEXISTENT', $this->mExpiry );
                }
index 72eb5d3..6c706ba 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;
        }
 
        /**
@@ -726,9 +751,6 @@ class Diff {
        /**
         * Get the closing set of lines.
         *
-        * This reconstructs the $to_lines parameter passed to the
-        * constructor.
-        *
         * @return array The sequence of strings.
         */
        function closing() {
index 5772958..a0a3fa7 100644 (file)
@@ -38,7 +38,7 @@ class DifferenceEngine extends ContextSource {
         * @private
         */
        var $mOldid, $mNewid;
-       var $mOldtext, $mNewtext;
+       var $mOldContent, $mNewContent;
        protected $mDiffLang;
 
        /**
@@ -500,20 +500,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 ) ) {
@@ -644,7 +645,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 ) ) ) {
@@ -681,14 +684,50 @@ 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
+     *
+     * @since 1.WD
+     */
+    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.WD, use generateContentDiffBody() instead!
+     */
+    function generateDiffBody( $otext, $ntext ) {
+        wfDeprecated( __METHOD__, "1.WD" );
+
+        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__ );
@@ -942,13 +981,28 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Use specified text instead of loading from the database
+     * @deprecated since 1.WD
         */
-       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.WD" );
+
+        $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.WD
+     */
+    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
@@ -1073,14 +1127,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;
                        }
                }
@@ -1101,7 +1155,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 2f55ec1..22131c6 100644 (file)
@@ -1458,9 +1458,9 @@ class LocalFile extends File {
                global $wgParser;
                $revision = Revision::newFromTitle( $this->title );
                if ( !$revision ) return false;
-               $text = $revision->getText();
-               if ( !$text ) return false;
-               $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
+               $content = $revision->getContent();
+               if ( !$content ) return false;
+               $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
                return $pout->getText();
        }
 
index f812ac2..6b7e88d 100644 (file)
@@ -87,6 +87,13 @@ class Ibm_db2Updater extends DatabaseUpdater {
 
                        // 1.20
                        array( 'addTable', 'config',                            'patch-config.sql' ),
+
+                       // 1.WD
+                       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' ),
                );
        }
 }
index e453b01..e22f1d1 100644 (file)
@@ -206,6 +206,7 @@ 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' ),
+
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
 
                        // 1.20
@@ -213,6 +214,13 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
                        array( 'addField', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id.sql' ),
                        array( 'addIndex', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id-index.sql' ),
+
+                       // 1.WD
+                       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' ),
                );
        }
 
index aa3c334..dc02afb 100644 (file)
@@ -70,6 +70,13 @@ class OracleUpdater extends DatabaseUpdater {
                        //1.20
                        array( 'addTable', 'config', 'patch-config.sql' ),
 
+                       //1.WD
+                       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' ),
+
                        // KEEP THIS AT THE BOTTOM!!
                        array( 'doRebuildDuplicateFunction' ),
 
index 8146274..f936fb6 100644 (file)
@@ -85,6 +85,7 @@ 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' ),
+
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
 
                        // 1.20
@@ -92,6 +93,13 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
                        array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
                        array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
+
+                       // 1.WD
+                       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' ),
                );
        }
 
index 4e02258..8be6db2 100644 (file)
@@ -94,16 +94,17 @@ class DoubleRedirectJob extends Job {
                        wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
                        return true;
                }
-               $text = $targetRev->getText();
-               $currentDest = Title::newFromRedirect( $text );
+               $content = $targetRev->getContent();
+               $currentDest = $content->getRedirectTarget();
                if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
                        wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" );
                        return true;
                }
 
                # Check for a suppression tag (used e.g. in periodically archived discussions)
+               $text = ContentHandler::getContentText( $content );
                $mw = MagicWord::get( 'staticredirect' );
-               if ( $mw->match( $text ) ) {
+               if ( $mw->match( $text ) ) { #FIXME: add support for this to ContentHandler/Content
                        wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
                        return true;
                }
@@ -129,7 +130,7 @@ class DoubleRedirectJob extends Job {
                # so the regex has to be fairly general
                $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
                        '[[' . $newTitle->getFullText() . ']]',
-                       $text, 1 );
+                       $text, 1 ); #FIXME: need a way to do this via ContentHandler!
 
                if ( $newText === $text ) {
                        $this->setLastError( 'Text unchanged???' );
index 7ccf00d..05adec0 100644 (file)
@@ -58,11 +58,12 @@ class RefreshLinksJob extends Job {
 
                wfProfileIn( __METHOD__.'-parse' );
                $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
-               $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
+               $content = $revision->getContent();
+               $parserOutput = $content->getParserOutput( $this->title, $revision->getId(), $options, false );
                wfProfileOut( __METHOD__.'-parse' );
                wfProfileIn( __METHOD__.'-update' );
 
-               $updates = $parserOutput->getSecondaryDataUpdates( $this->title, false );
+               $updates = $content->getContentHandler()->getSecondaryDataUpdates( $content, $this->title, null, false, $parserOutput  );
                DataUpdate::runUpdates( $updates );
 
                wfProfileOut( __METHOD__.'-update' );
@@ -132,11 +133,13 @@ class RefreshLinksJob2 extends Job {
                                return false;
                        }
                        wfProfileIn( __METHOD__.'-parse' );
-                       $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
+                       $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
+                       $content = $revision->getContent();
+                       $parserOutput = $content->getParserOutput( $title, $revision->getId(), $options, false );
                        wfProfileOut( __METHOD__.'-parse' );
                        wfProfileIn( __METHOD__.'-update' );
 
-                       $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
+                       $updates = $content->getContentHandler()->getSecondaryDataUpdates( $content, $title, null, false, $parserOutput  );
                        DataUpdate::runUpdates( $updates );
 
                        wfProfileOut( __METHOD__.'-update' );
index f881455..e7553a2 100644 (file)
@@ -3553,7 +3553,13 @@ class Parser {
                        }
 
                        if ( $rev ) {
-                               $text = $rev->getText();
+                               $content = $rev->getContent();
+                               $text = $content->getWikitextForTransclusion();
+
+                               if ( $text === false || $text === null ) {
+                                       $text = false;
+                                       break;
+                               }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
@@ -3562,15 +3568,16 @@ class Parser {
                                        break;
                                }
                                $text = $message->plain();
+                               $content = ContentHandler::makeContent( $text, $title ); #TODO: use Message::content() instead, once that exists
                        } else {
                                break;
                        }
-                       if ( $text === false ) {
+                       if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
-                       $title = Title::newFromRedirect( $text );
+                       $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
index d929f1a..0687049 100644 (file)
@@ -157,7 +157,7 @@ class ParserOutput extends CacheTime {
                $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 $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
+               private $mSecondaryDataUpdates = array(); # List of instances of DataUpdate, used to cause some information extracted from the page in a custom place.
 
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
@@ -480,6 +480,9 @@ class ParserOutput extends CacheTime {
         * extracted from the page's content, including a LinksUpdate object for all links stored in
         * this ParserOutput object.
         *
+        * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content
+        *        handler may provide additional update objects.
+        *
         * @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?
         *
index f35e774..446559a 100644 (file)
@@ -82,7 +82,16 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
                if ( !$revision ) {
                        return null;
                }
-               return $revision->getRawText();
+
+               $content = $revision->getContent( Revision::RAW );
+               $model = $content->getModel();
+
+               if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
+                       wfDebug( __METHOD__ . "bad content model #$model for JS/CSS page!\n" );
+                       return null;
+               }
+
+               return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
        }
 
        /* Methods */
index 678c530..ed51f75 100644 (file)
@@ -801,11 +801,12 @@ class SearchResult {
         */
        protected function initText() {
                if ( !isset( $this->mText ) ) {
-                       if ( $this->mRevision != null )
-                               $this->mText = $this->mRevision->getText();
-                       else // TODO: can we fetch raw wikitext for commons images?
+                       if ( $this->mRevision != null ) {
+                               $content = $this->mRevision->getContent();
+                               $this->mText = $content->getTextForSearchIndex(); //XXX: maybe we don't even need the text, but the content object?
+                       } else { // TODO: can we fetch raw wikitext for commons images?
                                $this->mText = '';
-
+                       }
                }
        }
 
@@ -817,7 +818,7 @@ class SearchResult {
                global $wgUser, $wgAdvancedSearchHighlighting;
                $this->initText();
                list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
-               $h = new SearchHighlighter();
+               $h = new SearchHighlighter(); // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
                if ( $wgAdvancedSearchHighlighting )
                        return $h->highlightText( $this->mText, $terms, $contextlines, $contextchars );
                else
index bc07d58..6182f7d 100644 (file)
@@ -144,7 +144,7 @@ class SpecialBookSources extends SpecialPage {
                $title = Title::makeTitleSafe( NS_PROJECT, $page ); # Show list in content language
                if( is_object( $title ) && $title->exists() ) {
                        $rev = Revision::newFromTitle( $title );
-                       $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
+                       $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) ); #FIXME: need a way to do this via ContentHandler (or enforce flat text-based content)
                        return true;
                }
 
index 9e3c52b..ecc0b83 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::getForModelID( $rev1->getContentModel() );
+                       $de = $contentHandler->createDifferenceEngine( $form->getContext(),
                                $rev1,
                                $rev2,
                                null, // rcid
index 4c32c30..b279d8a 100644 (file)
@@ -463,7 +463,7 @@ class SpecialNewpages extends IncludableSpecialPage {
                                $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
                                htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
                                "</p>\n<hr />\n<div>" .
-                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+                               nl2br( htmlspecialchars( $revision->getContent()->serialize() ) ) . "</div>"; //TODO: include content model/type in feed item?
                }
                return '';
        }
index 79c051a..542ecff 100644 (file)
@@ -112,12 +112,22 @@ class PageArchive {
         * @return ResultWrapper
         */
        function listRevisions() {
+               global $wgContentHandlerNoDB;
+
                $dbr = wfGetDB( DB_SLAVE );
+
+               $fields = array(
+                       'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
+                       'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+               );
+
+               if ( !$wgContentHandlerNoDB ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
                $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'
-                       ),
+                       $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        'PageArchive::listRevisions',
@@ -174,22 +184,32 @@ class PageArchive {
         * @return Revision
         */
        function getRevision( $timestamp ) {
+               global $wgContentHandlerNoDB;
+
                $dbr = wfGetDB( DB_SLAVE );
+
+               $fields = array(
+                       'ar_rev_id',
+                       'ar_text',
+                       'ar_comment',
+                       'ar_user',
+                       'ar_user_text',
+                       'ar_timestamp',
+                       'ar_minor_edit',
+                       'ar_flags',
+                       'ar_text_id',
+                       'ar_deleted',
+                       'ar_len',
+                       'ar_sha1',
+               );
+
+               if ( !$wgContentHandlerNoDB ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
                $row = $dbr->selectRow( 'archive',
-                       array(
-                               'ar_rev_id',
-                               'ar_text',
-                               'ar_comment',
-                               'ar_user',
-                               'ar_user_text',
-                               'ar_timestamp',
-                               'ar_minor_edit',
-                               'ar_flags',
-                               'ar_text_id',
-                               'ar_deleted',
-                               'ar_len',
-                               'ar_sha1',
-                       ),
+                       $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
@@ -392,6 +412,8 @@ class PageArchive {
         * @return Mixed: number of revisions restored or false on failure
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
+               global $wgContentHandlerNoDB;
+
                if ( wfReadOnly() ) {
                        return false;
                }
@@ -445,24 +467,31 @@ class PageArchive {
                        $oldones = "ar_timestamp IN ( {$oldts} )";
                }
 
+               $fields = array(
+                       'ar_rev_id',
+                       'ar_text',
+                       'ar_comment',
+                       'ar_user',
+                       'ar_user_text',
+                       'ar_timestamp',
+                       'ar_minor_edit',
+                       'ar_flags',
+                       'ar_text_id',
+                       'ar_deleted',
+                       'ar_page_id',
+                       'ar_len',
+                       'ar_sha1');
+
+               if ( !$wgContentHandlerNoDB ) {
+                       $fields[] = 'ar_content_format';
+                       $fields[] = 'ar_content_model';
+               }
+
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
-                       /* fields */ array(
-                               'ar_rev_id',
-                               'ar_text',
-                               'ar_comment',
-                               'ar_user',
-                               'ar_user_text',
-                               'ar_timestamp',
-                               'ar_minor_edit',
-                               'ar_flags',
-                               'ar_text_id',
-                               'ar_deleted',
-                               'ar_page_id',
-                               'ar_len',
-                               'ar_sha1' ),
+                       $fields,
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@ -843,6 +872,7 @@ class SpecialUndelete extends SpecialPage {
 
                if( $this->mPreview ) {
                        // Hide [edit]s
+                       //FIXME: ContentHandler will have to provide some specialized magic to do this
                        $popts = $out->parserOptions();
                        $popts->setEditSection( false );
                        $out->parserOptions( $popts );
@@ -854,7 +884,7 @@ class SpecialUndelete extends SpecialPage {
                                        'readonly' => 'readonly',
                                        'cols' => intval( $user->getOption( 'cols' ) ),
                                        'rows' => intval( $user->getOption( 'rows' ) ) ),
-                               $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
+                               $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) . //FIXME: ContentHandler will have to provide some specialized magic to do this
                        Xml::openElement( 'div' ) .
                        Xml::openElement( 'form', array(
                                'method' => 'post',
@@ -892,7 +922,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 +940,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 5bb7762..0dca39d 100644 (file)
@@ -414,6 +414,16 @@ class Language {
         */
        public function setNamespaces( array $namespaces ) {
                $this->namespaceNames = $namespaces;
+               $this->mNamespaceIds = null;
+       }
+
+       /**
+        * Resets all of the namespace caches. Mainly used for testing
+        */
+       public function resetNamespaces( ) {
+               $this->namespaceNames = null;
+               $this->mNamespaceIds = null;
+               $this->namespaceAliases = null;
        }
 
        /**
index 18d1dbc..e902c64 100644 (file)
@@ -907,7 +907,11 @@ class LanguageConverter {
                        if ( $title && $title->exists() ) {
                                $revision = Revision::newFromTitle( $title );
                                if ( $revision ) {
-                                       $txt = $revision->getRawText();
+                                       if ( $revision->getContentModel() == CONTENT_MODEL_WIKITEXT ) {
+                                               $txt = $revision->getContent( Revision::RAW )->getNativeData();
+                                       }
+
+                                       //@todo: in the future, use a specialized content model, perhaps based on json!
                                }
                        }
                }
index 1865cc5..6a2820d 100644 (file)
@@ -102,7 +102,7 @@ class LanguageFi extends Language {
                        'monday' => 'maanantai',
                        'tuesday' => 'tiistai',
                        'wednesday' => 'keskiviikko',
-                       'thursay' => 'torstai',
+                       'thursday' => 'torstai',
                        'friday' => 'perjantai',
                        'saturday' => 'lauantai',
                        'sunday' => 'sunnuntai',
index 1fdb65a..e2dc0f1 100644 (file)
@@ -889,6 +889,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.',
@@ -4874,4 +4875,10 @@ Otherwise, you can use the easy form below. Your comment will be added to the pa
 'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
 'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
 
+# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
+'content-model-1' => 'wikitext',
+'content-model-2' => 'JavaScript',
+'content-model-3' => 'CSS',
+'content-model-4' => 'plain text',
+
 );
index 115d579..c3f89e4 100644 (file)
@@ -4738,4 +4738,10 @@ $4 is the gender of the target user.',
 'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
 'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
 
+# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
+'content-model-1' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
+'content-model-2' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
+'content-model-3' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
+'content-model-4' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
+
 );
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..c62ddfb
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_format int unsigned 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..8c18bba
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_model int unsigned 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..89df112
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/page
+  ADD page_content_model int unsigned 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..eed0306
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_format int unsigned 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..1834b75
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_model int unsigned DEFAULT NULL;
index bac2ff6..c2e7871 100644 (file)
@@ -46,7 +46,7 @@ class CheckBadRedirects extends Maintenance {
                        $title = Title::makeTitle( $row->page_namespace, $row->page_title );
                        $rev = Revision::newFromId( $row->page_latest );
                        if ( $rev ) {
-                               $target = Title::newFromRedirect( $rev->getText() );
+                               $target = $rev->getContent()->getRedirectTarget();
                                if ( !$target ) {
                                        $this->output( $title->getPrefixedText() . "\n" );
                                }
index 8a760cd..cdbba55 100644 (file)
@@ -96,6 +96,7 @@ class CleanupSpam extends Maintenance {
                $rev = Revision::newFromTitle( $title );
                $currentRevId = $rev->getId();
 
+               //FIXME: LinkFilter needs to handle Content objects! Or rather, ContentHandler needs to provide the appropriate LinkFilter.
                while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) || LinkFilter::matchEntry( $rev->getText() , $domain ) ) ) {
                        $rev = $rev->getPrevious();
                }
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..56e0ca1 100644 (file)
@@ -67,16 +67,16 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance {
                        # Go through and update rev_len from these rows.
                        foreach ( $res as $row ) {
                                $rev = new Revision( $row );
-                               $text = $rev->getRawText();
-                               if ( !is_string( $text ) ) {
+                               $content = $rev->getContent();
+                               if ( !$content ) {
                                        # This should not happen, but sometimes does (bug 20757)
-                                       $this->output( "Text of revision {$row->rev_id} unavailable!\n" );
+                                       $this->output( "Content of revision {$row->rev_id} unavailable!\n" );
                                        $missing++;
                                }
                                else {
                                        # Update the row...
                                        $db->update( 'revision',
-                                                        array( 'rev_len' => strlen( $text ) ),
+                                                        array( 'rev_len' => $content->getSize() ),
                                                         array( 'rev_id' => $row->rev_id ),
                                                         __METHOD__ );
                                        $count++;
index 1d8e4c8..af9006b 100644 (file)
@@ -136,14 +136,14 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance {
                        $rev = ( $table === 'archive' )
                                ? Revision::newFromArchiveRow( $row )
                                : new Revision( $row );
-                       $text = $rev->getRawText();
+                       $text = $rev->getSerializedData();
                } catch ( MWException $e ) {
-                       $this->output( "Text of revision with {$idCol}={$row->$idCol} unavailable!\n" );
+                       $this->output( "Data of revision with {$idCol}={$row->$idCol} unavailable!\n" );
                        return false; // bug 22624?
                }
                if ( !is_string( $text ) ) {
                        # This should not happen, but sometimes does (bug 20757)
-                       $this->output( "Text of revision with {$idCol}={$row->$idCol} unavailable!\n" );
+                       $this->output( "Data of revision with {$idCol}={$row->$idCol} unavailable!\n" );
                        return false;
                } else {
                        $db->update( $table,
@@ -167,10 +167,10 @@ class PopulateRevisionSha1 extends LoggedUpdateMaintenance {
                        $this->output( "Text of revision with timestamp {$row->ar_timestamp} unavailable!\n" );
                        return false; // bug 22624?
                }
-               $text = $rev->getRawText();
+               $text = $rev->getSerializedData();
                if ( !is_string( $text ) ) {
                        # This should not happen, but sometimes does (bug 20757)
-                       $this->output( "Text of revision with timestamp {$row->ar_timestamp} unavailable!\n" );
+                       $this->output( "Data of revision with timestamp {$row->ar_timestamp} unavailable!\n" );
                        return false;
                } else {
                        # Archive table as no PK, but (NS,title,time) should be near unique.
index b5aa85f..21182d7 100644 (file)
@@ -210,18 +210,17 @@ class RefreshLinks extends Maintenance {
                        return;
                }
 
-               $text = $page->getRawText();
-               if ( $text === false ) {
+               $content = $page->getContent( REVISION::RAW );
+               if ( null === false ) {
                        return;
                }
 
                $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
 
-               $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
-               $parserOutput = $wgParser->parse( $text, $page->getTitle(), $options, true, true, $page->getLatest() );
-               $update = new LinksUpdate( $page->getTitle(), $parserOutput, false );
-               $update->doUpdate();
+               $contentHandler = $content->getContentHandler();
+               $updates = $contentHandler->getSecondaryDataUpdates( $content, $page->getTitle() );
+               DataUpdate::runUpdates( $updates );
 
                $dbw->commit( __METHOD__ );
        }
old mode 100755 (executable)
new mode 100644 (file)
index 9ae2633..a2124c3 100644 (file)
@@ -65,7 +65,7 @@ $uncompressedSize = 0;
 $t = -microtime( true );
 foreach ( $res as $row ) {
        $revision = new Revision( $row );
-       $text = $revision->getText();
+       $text = $revision->getSerializedData();
        $uncompressedSize += strlen( $text );
        $hashes[$row->rev_id] = md5( $text );
        $keys[$row->rev_id] = $blob->addItem( $text );
index 0a5b2fb..0aee741 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, see CONTENT_MODEL_XXX constants
+  page_content_model  int unsigned  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, see CONTENT_MODEL_XXX constants
+  rev_content_model  int unsigned  default NULL,
+
+  -- content format, see CONTENT_FORMAT_XXX constants
+  rev_content_format int unsigned  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, see CONTENT_MODEL_XXX constants
+  ar_content_model  int unsigned default NULL,
+
+  -- content format, see CONTENT_FORMAT_XXX constants
+  ar_content_format int unsigned default NULL
+
 ) /*$wgDBTableOptions*/;
 
 CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
index ea385ad..18475ca 100644 (file)
@@ -53,6 +53,19 @@ class MediaWikiPHPUnitCommand extends PHPUnit_TextUI_Command {
                }
        }
 
+       protected function handleCustomTestSuite() {
+               if ( empty( $this->arguments['printer'] ) ) {
+                       $this->arguments['printer'] = new PHPUnit_TextUI_ResultPrinter(
+                               null,
+                               isset($this->arguments['verbose']) ? $this->arguments['verbose'] : false,
+                               isset($this->arguments['colors']) ? $this->arguments['colors'] : true,
+                               isset($this->arguments['debug']) ? $this->arguments['debug'] : false
+                       );
+               }
+
+               parent::handleCustomTestSuite();
+       }
+
        public function showHelp() {
                parent::showHelp();
 
@@ -74,3 +87,30 @@ EOT;
        }
 
 }
+
+class MediaWikiPHPUnitResultPrinter extends PHPUnit_TextUI_ResultPrinter {
+       /**
+        * Overrides original method to ignore incomplete tests except in verbose mode.
+        *
+        * @param  PHPUnit_Framework_TestResult  $result
+        */
+       protected function printIncompletes(PHPUnit_Framework_TestResult $result)
+       {
+               if ( $this->verbose ) {
+                       parent::printIncompletes( $result );
+               }
+       }
+
+       /**
+        * Overrides original method to ignore skipped tests except in verbose mode.
+        *
+        * @param  PHPUnit_Framework_TestResult  $result
+        */
+       protected function printSkipped(PHPUnit_Framework_TestResult $result)
+       {
+               if ( $this->verbose ) {
+                       parent::printSkipped( $result );
+               }
+       }
+
+}
diff --git a/tests/phpunit/includes/ContentHandlerTest.php b/tests/phpunit/includes/ContentHandlerTest.php
new file mode 100644 (file)
index 0000000..6454032
--- /dev/null
@@ -0,0 +1,506 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class ContentHandlerTest extends MediaWikiTestCase {
+
+       public function setUp() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = 999999;
+               $wgContentHandlers[ 999999 ] = 'DummyContentHandlerForTesting';
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
+       public function tearDown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               unset( $wgExtraNamespaces[ 12312 ] );
+               unset( $wgExtraNamespaces[ 12313 ] );
+
+               unset( $wgNamespaceContentModels[ 12312 ] );
+               unset( $wgContentHandlers[ 999999 ] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
+       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, $expectedModelId ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expectedModelId, ContentHandler::getDefaultModelFor( $title ) );
+       }
+       /**
+        * @dataProvider dataGetDefaultModelFor
+        */
+       public function testGetForTitle( $title, $expectedContentModel ) {
+               $title = Title::newFromText( $title );
+               $handler = ContentHandler::getForTitle( $title );
+               $this->assertEquals( $expectedContentModel, $handler->getModelID() );
+       }
+
+       public function dataGetContentFormatMimeType( ) {
+               return array(
+                       array( 0, null ),
+                       array( null, null ),
+                       array( 99887766, null ),
+
+                       array( CONTENT_FORMAT_WIKITEXT, 'text/x-wiki' ),
+                       array( CONTENT_FORMAT_JAVASCRIPT, 'text/javascript' ),
+                       array( CONTENT_FORMAT_CSS, 'text/css' ),
+                       array( CONTENT_FORMAT_JSON, 'application/json' ),
+                       array( CONTENT_FORMAT_XML, 'application/xml' ),
+                       array( CONTENT_FORMAT_SERIALIZED, 'application/vnd.php.serialized' ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentFormatMimeType
+        */
+       public function testGetContentFormatMimeType( $id, $expectedMime ) {
+               $mime = ContentHandler::getContentFormatMimeType( $id );
+
+               $this->assertEquals( $expectedMime, $mime );
+       }
+
+       public function dataGetContentFormatID( ) {
+               return array(
+                       array( '', null ),
+                       array( 'foo', null ),
+                       array( null, null ),
+
+                       array( 'text/x-wiki', CONTENT_FORMAT_WIKITEXT ),
+                       array( 'text/javascript', CONTENT_FORMAT_JAVASCRIPT ),
+                       array( 'text/css', CONTENT_FORMAT_CSS ),
+                       array( 'application/json', CONTENT_FORMAT_JSON ),
+                       array( 'application/xml', CONTENT_FORMAT_XML ),
+                       array( 'application/vnd.php.serialized', CONTENT_FORMAT_SERIALIZED ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentFormatID
+        */
+       public function testGetContentFormatID( $mime, $expectedId ) {
+               $id = ContentHandler::getContentFormatID( $mime );
+
+               $this->assertEquals( $expectedId, $id );
+       }
+
+       public function dataGetContentModelName() {
+               return array(
+                       array( 0, null ),
+                       array( null, null ),
+                       array( 99887766, null ),
+
+                       array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ), //XXX: depends on content language
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentModelName
+        */
+       public function testGetContentModelName( $id, $expected ) {
+               $name = ContentHandler::getContentModelName( $id );
+
+               if ( $expected === null ) {
+                       $this->assertNull( $name, "content model name for #$id was expected to be null" );
+               } else {
+                       $this->assertNotNull( $name, "no name found for content model #$id" );
+                       $this->assertTrue( preg_match( $expected, $name ) > 0 , "content model name for #$id did not match pattern $expected" );
+               }
+       }
+
+       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, $modelId = 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, 999999, 'hallo', false ),
+
+                       array( 'hallo', 'Test', null, CONTENT_FORMAT_WIKITEXT, CONTENT_MODEL_WIKITEXT, 'hallo', false ),
+                       array( 'hallo', 'MediaWiki:Test.js', null, CONTENT_FORMAT_JAVASCRIPT, CONTENT_MODEL_JAVASCRIPT, 'hallo', false ),
+                       array( serialize('hallo'), 'Dummy:Test', null, 999999, 999999, '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, 999999, null, null, true ),
+                       array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, 999999, null, null, true ),
+                       array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, 999999, null, null, true ),
+               );
+       }
+
+       /**
+        * @dataProvider dataMakeContent
+        */
+       public function testMakeContent( $data, $title, $modelId, $format, $expectedModelId, $expectedNativeData, $shouldFail ) {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers;
+
+               $title = Title::newFromText( $title );
+
+               try {
+                       $content = ContentHandler::makeContent( $data, $title, $modelId, $format );
+
+                       if ( $shouldFail ) $this->fail( "ContentHandler::makeContent should have failed!" );
+
+                       $this->assertEquals( $expectedModelId, $content->getModel(), 'bad model id' );
+                       $this->assertEquals( $expectedNativeData, $content->getNativeData(), 'bads native data' );
+               } catch ( MWException $ex ) {
+                       if ( !$shouldFail ) $this->fail( "ContentHandler::makeContent failed unexpectedly: " . $ex->getMessage() );
+                       else $this->assertTrue( true ); // dummy, so we don't get the "test did not perform any assertions" message.
+               }
+
+       }
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("ContentHandlerTest_testGetParserOutput", "hello ''world''\n", "<p>hello <i>world</i>\n</p>"),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetParserOutput
+        */
+       public function testGetParserOutput( $title, $text, $expectedHtml ) {
+               $title = Title::newFromText( $title );
+               $handler = ContentHandler::getForModelID( $title->getContentModel() );
+               $content = ContentHandler::makeContent( $text, $title );
+
+               $po = $handler->getParserOutput( $content, $title );
+
+               $this->assertEquals( $expectedHtml, $po->getText() );
+               // @todo: assert more properties
+       }
+
+       public function dataGetSecondaryDataUpdates() {
+               return array(
+                       array("ContentHandlerTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
+                               array( 'LinksUpdate' => array(  'mRecursive' => true,
+                                                               'mLinks' => array() ) )
+                       ),
+                       array("ContentHandlerTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n",
+                               array( 'LinksUpdate' => array(  'mRecursive' => true,
+                                                               'mLinks' => array( array( 'World_test_21344' => 0 ) ) ) )
+                       ),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetSecondaryDataUpdates
+        */
+       public function testGetSecondaryDataUpdates( $title, $text, $expectedStuff ) {
+               $title = Title::newFromText( $title );
+               $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates!
+
+               $handler = ContentHandler::getForModelID( $title->getContentModel() );
+               $content = ContentHandler::makeContent( $text, $title );
+
+               $updates = $handler->getSecondaryDataUpdates( $content, $title );
+
+               // make updates accessible by class name
+               foreach ( $updates as $update ) {
+                       $class = get_class( $update );
+                       $updates[ $class ] = $update;
+               }
+
+               foreach ( $expectedStuff as $class => $fieldValues ) {
+                       $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+                       $update = $updates[ $class ];
+
+                       foreach ( $fieldValues as $field => $value ) {
+                               $v = $update->$field; #if the field doesn't exist, just crash and burn
+                               $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+                       }
+               }
+       }
+
+       public function dataGetDeletionUpdates() {
+               return array(
+                       array("ContentHandlerTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
+                               array( 'LinksDeletionUpdate' => array( ) )
+                       ),
+                       array("ContentHandlerTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n",
+                               array( 'LinksDeletionUpdate' => array( ) )
+                       ),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetDeletionUpdates
+        */
+       public function testDeletionUpdates( $title, $text, $expectedStuff ) {
+               $title = Title::newFromText( $title );
+               $title->resetArticleID( 2342 ); //dummy id. fine as long as we don't try to execute the updates!
+
+               $handler = ContentHandler::getForModelID( $title->getContentModel() );
+               $content = ContentHandler::makeContent( $text, $title );
+
+               $updates = $handler->getDeletionUpdates( $content, $title );
+
+               // make updates accessible by class name
+               foreach ( $updates as $update ) {
+                       $class = get_class( $update );
+                       $updates[ $class ] = $update;
+               }
+
+               foreach ( $expectedStuff as $class => $fieldValues ) {
+                       $this->assertArrayHasKey( $class, $updates, "missing an update of type $class" );
+
+                       $update = $updates[ $class ];
+
+                       foreach ( $fieldValues as $field => $value ) {
+                               $v = $update->$field; #if the field doesn't exist, just crash and burn
+                               $this->assertEquals( $value, $v, "unexpected value for field $field in instance of $class" );
+                       }
+               }
+       }
+}
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+       public function __construct( $dataModel ) {
+               parent::__construct( $dataModel, array( 999999 ) );
+       }
+
+       /**
+        * 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( '' );
+       }
+
+       /**
+        * @param Content $content
+        * @param Title $title
+        * @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( Content $content, Title $title, $revId = null, ParserOptions $options = NULL, $generateHtml = true )
+       {
+               return new ParserOutput( $content->getNativeData() );
+       }
+}
+
+class DummyContentForTesting extends AbstractContent {
+
+       public function __construct( $data ) {
+               parent::__construct( 999999 );
+
+               $this->data = $data;
+       }
+
+       public function serialize( $format = null ) {
+               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 strlen( $this->data );
+       }
+
+       /**
+        * Return a copy of this Content object. The following must be true for the object returned
+        * if $copy = $original->copy()
+        *
+        * * get_class($original) === get_class($copy)
+        * * $original->getModel() === $copy->getModel()
+        * * $original->equals( $copy )
+        *
+        * If and only if the Content object is imutable, the copy() method can and should
+        * return $this. That is,  $copy === $original may be true, but only for imutable content
+        * objects.
+        *
+        * @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;
+       }
+}
+
diff --git a/tests/phpunit/includes/CssContentTest.php b/tests/phpunit/includes/CssContentTest.php
new file mode 100644 (file)
index 0000000..66b6e7f
--- /dev/null
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+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 testGetModel() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_CSS, $content->getModel() );
+       }
+
+       public function testGetContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_CSS, $content->getContentHandler()->getModelID() );
+       }
+
+       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..7a12ada
--- /dev/null
@@ -0,0 +1,236 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+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 testGetModel() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getModel() );
+       }
+
+       public function testGetContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $content->getContentHandler()->getModelID() );
+       }
+
+       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 20199b2..8b77746 100644 (file)
@@ -3,6 +3,7 @@
 /**
  * Test class for Revision storage.
  *
+ * @group ContentHandler
  * @group Database
  * ^--- important, causes temporary tables to be used instead of the real database
  */
@@ -32,11 +33,35 @@ class RevisionStorageTest extends MediaWikiTestCase {
        }
 
        public function setUp() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
+               $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
                if ( !$this->the_page ) {
                        $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" );
                }
        }
 
+       public function tearDown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               unset( $wgExtraNamespaces[ 12312 ] );
+               unset( $wgExtraNamespaces[ 12313 ] );
+
+               unset( $wgNamespaceContentModels[ 12312 ] );
+               unset( $wgContentHandlers[ 'DUMMY' ] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
        protected function makeRevision( $props = null ) {
                if ( $props === null ) $props = array();
 
@@ -60,7 +85,8 @@ class RevisionStorageTest extends MediaWikiTestCase {
                        $page->doDeleteArticle( "done" );
                }
 
-               $page->doEdit( $text, "testing", EDIT_NEW );
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing", EDIT_NEW );
 
                return $page;
        }
@@ -72,6 +98,8 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $this->assertEquals( $orig->getPage(), $rev->getPage() );
                $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
                $this->assertEquals( $orig->getUser(), $rev->getUser() );
+               $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
+               $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
                $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
        }
 
@@ -182,6 +210,9 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
                $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
                $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
+
+               $this->assertTrue( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields');
+               $this->assertTrue( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields');
        }
 
        /**
@@ -208,6 +239,17 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $this->assertEquals( 'hello hello.', $rev->getText() );
        }
 
+       /**
+        * @covers Revision::getContent
+        */
+       public function testGetContent()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
+       }
+
        /**
         * @covers Revision::revText
         */
@@ -229,6 +271,29 @@ class RevisionStorageTest extends MediaWikiTestCase {
 
                $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
        }
+
+       /**
+        * @covers Revision::getContentModel
+        */
+       public function testGetContentModel()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       /**
+        * @covers Revision::getContentFormat
+        */
+       public function testGetContentFormat()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT, 'content_format' => CONTENT_FORMAT_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
+       }
+
        /**
         * @covers Revision::isCurrent
         */
@@ -243,7 +308,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $rev1x = Revision::newFromId( $rev1->getId() );
                $this->assertTrue( $rev1x->isCurrent() );
 
-               $page->doEdit( 'Bla bla', 'second rev' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev' );
                $rev2 = $page->getRevision();
 
                # @todo: find out if this should be true
@@ -266,7 +331,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
 
                $this->assertNull( $rev1->getPrevious() );
 
-               $page->doEdit( 'Bla bla', 'second rev testGetPrevious' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev testGetPrevious' );
                $rev2 = $page->getRevision();
 
                $this->assertNotNull( $rev2->getPrevious() );
@@ -283,7 +348,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
 
                $this->assertNull( $rev1->getNext() );
 
-               $page->doEdit( 'Bla bla', 'second rev testGetNext' );
+               $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev testGetNext' );
                $rev2 = $page->getRevision();
 
                $this->assertNotNull( $rev1->getNext() );
@@ -303,6 +368,6 @@ class RevisionStorageTest extends MediaWikiTestCase {
 
                $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' );
                $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' );
-               $this->assertEquals( 'some testing text', $rev->getText() );
+               $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
        }
 }
diff --git a/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php
new file mode 100644 (file)
index 0000000..3dfaa8d
--- /dev/null
@@ -0,0 +1,86 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class RevisionTest_ContentHandlerUseDB extends RevisionStorageTest {
+       var $saveContentHandlerNoDB = null;
+
+       function setUp() {
+               global $wgContentHandlerUseDB;
+
+               $this->saveContentHandlerNoDB = $wgContentHandlerUseDB;
+
+               $wgContentHandlerUseDB = false;
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               $page_table = $dbw->tableName( 'page' );
+               $revision_table = $dbw->tableName( 'revision' );
+               $archive_table = $dbw->tableName( 'archive' );
+
+               if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+                       $dbw->query( "alter table $page_table drop column page_content_model" );
+                       $dbw->query( "alter table $revision_table drop column rev_content_model" );
+                       $dbw->query( "alter table $revision_table drop column rev_content_format" );
+                       $dbw->query( "alter table $archive_table drop column ar_content_model" );
+                       $dbw->query( "alter table $archive_table drop column ar_content_format" );
+               }
+
+               parent::setUp();
+       }
+
+       function tearDown() {
+               global $wgContentHandlerUseDB;
+
+               parent::tearDown();
+
+               $wgContentHandlerUseDB = $this->saveContentHandlerNoDB;
+       }
+
+       /**
+        * @covers Revision::selectFields
+        */
+       public function testSelectFields()
+       {
+               $fields = Revision::selectFields();
+
+               $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields');
+               $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
+               $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
+               $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
+
+               $this->assertFalse( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields');
+               $this->assertFalse( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields');
+       }
+
+       /**
+        * @covers Revision::getContentModel
+        */
+       public function testGetContentModel()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               //NOTE: database fields for the content_model are disabled, so the model name is not retained.
+               //      We expect to get the default here instead of what was suppleid when creating the revision.
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $rev->getContentModel() );
+       }
+
+
+       /**
+        * @covers Revision::getContentFormat
+        */
+       public function testGetContentFormat()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT, 'content_format' => 'text/javascript' ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( CONTENT_FORMAT_WIKITEXT, $rev->getContentFormat() );
+       }
+
+}
+
+
index d7654db..ed7d919 100644 (file)
@@ -1,25 +1,53 @@
 <?php
 
+/**
+ * @group ContentHandler
+ */
 class RevisionTest extends MediaWikiTestCase {
        var $saveGlobals = array();
 
        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, $wgContLang;
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = 999999;
+               $wgContentHandlers[ 999999 ] = 'DummyContentHandlerForTesting';
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
+               global $wgContentHandlerTextFallback;
+               $wgContentHandlerTextFallback = 'ignore';
        }
 
        function tearDown() {
+               global $wgContLang;
+
                foreach ( $this->saveGlobals as $var => $data ) {
                        $GLOBALS[$var] = $data;
                }
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
        }
 
        function testGetRevisionText() {
@@ -120,6 +148,196 @@ 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, 999999 ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetContentModel
+        */
+       function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedModel, $rev->getContentModel() );
+       }
+
+       function dataGetContentFormat() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, CONTENT_FORMAT_WIKITEXT ),
+                       array( 'hello world', 'Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ),
+                       array( 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, 999999 ),
+               );
+       }
+
+       /**
+        * @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', 999999, 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', 999999, 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 ) );
+       }
+
+
+       public function dataGetSize( ) {
+               return array(
+                       array( "hello world.", null, 12 ),
+                       array( serialize( "hello world." ), 999999, 12 ),
+               );
+       }
+
+       /**
+        * @covers Revision::getSize
+        * @dataProvider dataGetSize
+        */
+       public function testGetSize( $text, $model, $expected_size )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+               $this->assertEquals( $expected_size, $rev->getSize() );
+       }
+
+       public function dataGetSha1( ) {
+               return array(
+                       array( "hello world.", null, Revision::base36Sha1( "hello world." ) ),
+                       array( serialize( "hello world." ), 999999, Revision::base36Sha1( serialize( "hello world." ) ) ),
+               );
+       }
+
+       /**
+        * @covers Revision::getSha1
+        * @dataProvider dataGetSha1
+        */
+       public function testGetSha1( $text, $model, $expected_hash )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+               $this->assertEquals( $expected_hash, $rev->getSha1() );
+       }
+
+       public function testConstructWithText() {
+               $rev = new Revision( array(
+                                         'text' => 'hello world.',
+                                         'content_model' => CONTENT_MODEL_JAVASCRIPT
+                                    ));
+
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       public function testConstructWithContent() {
+               $title = Title::newFromText( 'RevisionTest_testConstructWithContent' );
+
+               $rev = new Revision( array(
+                                         'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ),
+                                    ));
+
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
 }
 
 
index aed658b..de8f18f 100644 (file)
@@ -1,7 +1,34 @@
 <?php
 
+/**
+ * @group ContentHandler
+ */
 class TitleMethodsTest extends MediaWikiTestCase {
 
+       public function setup() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
+
+               $wgExtraNamespaces[ 12302 ] = 'TEST-JS';
+               $wgExtraNamespaces[ 12303 ] = 'TEST-JS_TALK';
+
+               $wgNamespaceContentModels[ 12302 ] = CONTENT_MODEL_JAVASCRIPT;
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
+       public function teardown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
+
+               unset( $wgExtraNamespaces[ 12302 ] );
+               unset( $wgExtraNamespaces[ 12303 ] );
+
+               unset( $wgNamespaceContentModels[ 12302 ] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+       }
+
        public function dataEquals() {
                return array(
                        array( 'Main Page', 'Main Page', true ),
@@ -75,6 +102,47 @@ class TitleMethodsTest extends MediaWikiTestCase {
                $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
        }
 
+       public function dataGetContentModel() {
+               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 dataGetContentModel
+        */
+       public function testGetContentModel( $title, $expectedModelId ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expectedModelId, $title->getContentModel() );
+       }
+
+       /**
+        * @dataProvider dataGetContentModel
+        */
+       public function testHasContentModel( $title, $expectedModelId ) {
+               $title = Title::newFromText( $title );
+               $this->assertTrue( $title->hasContentModel( $expectedModelId ) );
+       }
+
        public function dataIsCssOrJsPage() {
                return array(
                        array( 'Foo', false ),
@@ -92,6 +160,8 @@ class TitleMethodsTest extends MediaWikiTestCase {
                        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 ),
                );
        }
 
@@ -119,6 +189,8 @@ class TitleMethodsTest extends MediaWikiTestCase {
                        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 ),
                );
        }
 
@@ -187,6 +259,9 @@ class TitleMethodsTest extends MediaWikiTestCase {
                        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 ),
                );
        }
 
index c8606cc..e2d3605 100644 (file)
@@ -1,5 +1,6 @@
 <?php
 /**
+* @group ContentHandler
 * @group Database
 * ^--- important, causes temporary tables to be used instead of the real database
 **/
@@ -28,10 +29,12 @@ class WikiPageTest extends MediaWikiLangTestCase {
                                                       'templatelinks',
                                                       'iwlinks' ) );
        }
-
+       
        public function setUp() {
                parent::setUp();
                $this->pages_to_delete = array();
+
+               LinkCache::singleton()->clear(); # avoid cached redirect status, etc
        }
 
        public function tearDown() {
@@ -49,6 +52,10 @@ class WikiPageTest extends MediaWikiLangTestCase {
                parent::tearDown();
        }
 
+       /**
+        * @param Title $title
+        * @return WikiPage
+        */
        protected function newPage( $title ) {
                if ( is_string( $title ) ) $title = Title::newFromText( $title );
 
@@ -59,15 +66,84 @@ class WikiPageTest extends MediaWikiLangTestCase {
                return $p;
        }
 
+
+       /**
+        * @param String|Title|WikiPage $page
+        * @param String $text
+        * @param int $model
+        *
+        * @return WikiPage
+        */
        protected function createPage( $page, $text, $model = null ) {
                if ( is_string( $page ) ) $page = Title::newFromText( $page );
-               if ( $page instanceof Title ) $page = $this->newPage( $page );
 
-               $page->doEdit( $text, "testing", EDIT_NEW );
+               if ( $page instanceof Title ) {
+                       $title = $page;
+                       $page = $this->newPage( $page );
+               } else {
+                       $title = null;
+               }
+
+               $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
+               $page->doEditContent( $content, "testing", EDIT_NEW );
 
                return $page;
        }
 
+       public function testDoEditContent() {
+               $title = Title::newFromText( "WikiPageTest_testDoEditContent" );
+
+               $page = $this->newPage( $title );
+
+               $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
+                                                       . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
+                                                       $title );
+
+               $page->doEditContent( $content, "[[testing]] 1" );
+
+               $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+               $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
+               $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
+               $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
+
+               $id = $page->getId();
+
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+
+               $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               $retrieved = $page->getContent();
+               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+               # ------------------------
+               $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
+                                                       . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
+                                                       $title );
+
+               $page->doEditContent( $content, "testing 2" );
+
+               # ------------------------
+               $page = new WikiPage( $title );
+
+               $retrieved = $page->getContent();
+               $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
+
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+
+               $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
+       }
+       
        public function testDoEdit() {
                $title = Title::newFromText( "WikiPageTest_testDoEdit" );
 
@@ -76,13 +152,23 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $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" );
+               $page->doEdit( $text, "[[testing]] 1" );
 
+               $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
+               $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
                $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
                $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
 
                $id = $page->getId();
 
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+
+               $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
+
                # ------------------------
                $page = new WikiPage( $title );
 
@@ -123,13 +209,29 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $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]] foo" );
                $id = $page->getId();
 
                $page->doDeleteArticle( "testing deletion" );
 
+               $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" );
+               $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
                $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
+               $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
                $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
 
                $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
@@ -171,7 +273,20 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $rev = $page->getRevision();
 
                $this->assertEquals( $page->getLatest(), $rev->getId() );
-               $this->assertEquals( "some text", $rev->getText() );
+               $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
+       }
+
+       public function testGetContent() {
+               $page = $this->newPage( "WikiPageTest_testGetContent" );
+
+               $content = $page->getContent();
+               $this->assertNull( $content );
+
+               # -----------------
+               $this->createPage( $page, "some text" );
+
+               $content = $page->getContent();
+               $this->assertEquals( "some text", $content->getNativeData() );
        }
 
        public function testGetText() {
@@ -200,7 +315,20 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $this->assertEquals( "some text", $text );
        }
 
-       
+       public function testGetContentModel() {
+               $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
+       }
+
+       public function testGetContentHandler() {
+               $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() );
@@ -259,6 +387,10 @@ class WikiPageTest extends MediaWikiLangTestCase {
        public function testGetRedirectTarget( $title, $text, $target ) {
                $page = $this->createPage( $title, $text );
 
+               # sanity check, because this test seems to fail for no reason for some people.
+               $c = $page->getContent();
+               $this->assertEquals( 'WikitextContent', get_class( $c ) );
+               
                # now, test the actual redirect
                $t = $page->getRedirectTarget();
                $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
@@ -371,15 +503,19 @@ class WikiPageTest extends MediaWikiLangTestCase {
        public function testIsCountable( $title, $text, $mode, $expected ) {
                global $wgArticleCountMethod;
 
-               $old = $wgArticleCountMethod;
+               $oldArticleCountMethod = $wgArticleCountMethod;
                $wgArticleCountMethod = $mode;
 
                $page = $this->createPage( $title, $text );
-               $editInfo = $page->prepareTextForEdit( $page->getText() );
+               $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                                       array( 'pl_from' => $page->getId() ), __METHOD__ );
+
+               $editInfo = $page->prepareContentForEdit( $page->getContent() );
 
                $v = $page->isCountable();
                $w = $page->isCountable( $editInfo );
-               $wgArticleCountMethod = $old;
+
+               $wgArticleCountMethod = $oldArticleCountMethod;
 
                $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true )
                                                    . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
@@ -478,6 +614,18 @@ more stuff
                $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->getContentModel() );
+               $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
+
+               $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
+       }
+       
        /* @FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
@@ -538,19 +686,19 @@ more stuff
 
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
-               $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
 
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section two", 0, false, $user1 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
 
                $user2 = new User();
                $user2->setName( "127.0.2.13" );
                $text .= "\n\nthree";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section three", 0, false, $user2 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three", 0, false, $user2 );
 
                # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
                # or not committed under some circumstances. so, make sure the last revision has the right user name.
@@ -578,7 +726,7 @@ more stuff
 
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
-               $this->assertEquals( "one\n\ntwo", $page->getText() );
+               $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
 
        /**
@@ -590,14 +738,14 @@ more stuff
 
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
-               $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
                $rev1 = $page->getRevision();
 
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
-               $page->doEdit( $text, "adding section two", 0, false, $user1 );
+               $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
 
                # now, try the rollback
                $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
@@ -610,7 +758,7 @@ more stuff
 
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
-               $this->assertEquals( "one", $page->getText() );
+               $this->assertEquals( "one", $page->getContent()->getNativeData() );
        }
 
        public function dataGetAutosummary( ) {
@@ -739,7 +887,9 @@ more stuff
                        if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
                        else $user = $wgUser;
 
-                       $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
+                       $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
+
+                       $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
 
                        $c += 1;
                }
diff --git a/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php b/tests/phpunit/includes/WikiPageTest_ContentHandlerUseDB.php
new file mode 100644 (file)
index 0000000..1af6806
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @group ContentHandler
+ * @group Database
+ * ^--- important, causes temporary tables to be used instead of the real database
+ */
+class WikiPageTest_ContentHandlerUseDB extends WikiPageTest {
+       var $saveContentHandlerNoDB = null;
+
+       function setUp() {
+               global $wgContentHandlerUseDB;
+
+               parent::setUp();
+
+               $this->saveContentHandlerNoDB = $wgContentHandlerUseDB;
+
+               $wgContentHandlerUseDB = false;
+
+               $dbw = wfGetDB( DB_MASTER );
+
+               $page_table = $dbw->tableName( 'page' );
+               $revision_table = $dbw->tableName( 'revision' );
+               $archive_table = $dbw->tableName( 'archive' );
+
+               if ( $dbw->fieldExists( $page_table, 'page_content_model' ) ) {
+                       $dbw->query( "alter table $page_table drop column page_content_model" );
+                       $dbw->query( "alter table $revision_table drop column rev_content_model" );
+                       $dbw->query( "alter table $revision_table drop column rev_content_format" );
+                       $dbw->query( "alter table $archive_table drop column ar_content_model" );
+                       $dbw->query( "alter table $archive_table drop column ar_content_format" );
+               }
+       }
+
+       function tearDown() {
+               global $wgContentHandlerUseDB;
+
+               $wgContentHandlerUseDB = $this->saveContentHandlerNoDB;
+
+               parent::tearDown();
+       }
+
+       public function testGetContentModel() {
+               $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               $page = new WikiPage( $page->getTitle() );
+
+               // NOTE: since the content model is not recorded in the database,
+               //       we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $page->getContentModel() );
+       }
+
+       public function testGetContentHandler() {
+               $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               // NOTE: since the content model is not recorded in the database,
+               //       we expect to get the default, namely CONTENT_MODEL_WIKITEXT
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( 'WikitextContentHandler', get_class( $page->getContentHandler() ) );
+       }
+
+}
+
+
diff --git a/tests/phpunit/includes/WikitextContentHandlerTest.php b/tests/phpunit/includes/WikitextContentHandlerTest.php
new file mode 100644 (file)
index 0000000..a7615cf
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+class WikitextContentHandlerTest extends MediaWikiTestCase {
+
+       /**
+        * @var ContentHandler
+        */
+       var $handler;
+
+       public function setup() {
+               $this->handler = ContentHandler::getForModelID( 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, CONTENT_FORMAT_WIKITEXT ) );
+
+               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', CONTENT_FORMAT_WIKITEXT );
+               $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( CONTENT_FORMAT_WIKITEXT, true ),
+                       array( 99887766, 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 ) {
+               global $wgDiff3;
+
+               if ( !$wgDiff3 ) {
+                       $this->markTestSkipped( "Can't test merge3(), since \$wgDiff3 is not configured" );
+               }
+
+               if ( !file_exists( $wgDiff3 ) ) {
+                       #XXX: this sucks, since it uses arcane internal knowledge about TextContentHandler::merge3 and wfMerge.
+                       $this->markTestSkipped( "Can't test merge3(), since \$wgDiff3 is misconfigured: can't find $wgDiff3" );
+               }
+
+               // test merge
+               $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..5feec83
--- /dev/null
@@ -0,0 +1,421 @@
+<?php
+
+/**
+ * @group ContentHandler
+ */
+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->getTitle() );
+
+               $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 ) {
+               global $wgUser, $wgContLang;
+               $options = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+
+               $content = $this->newContent( $text );
+               $content = $content->preSaveTransform( $this->context->getTitle(), $this->context->getUser(), $options );
+
+               $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 ) {
+               global $wgUser, $wgContLang;
+               $options = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+
+               $content = $this->newContent( $text );
+               $content = $content->preloadTransform( $this->context->getTitle(), $options );
+
+               $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->getTitle() );
+               $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 testGetModel() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getModel() );
+       }
+
+       public function testGetContentHandler() {
+               $content = $this->newContent( "hello world." );
+
+               $this->assertEquals( CONTENT_MODEL_WIKITEXT, $content->getContentHandler()->getModelID() );
+       }
+
+       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 ) );
+       }
+
+}
index 710ad83..88e39df 100644 (file)
@@ -1,6 +1,9 @@
 <?php
 
 /**
+ * @group medium
+ * ^---- causes phpunit to use a higher timeout threshold
+ * 
  * @group FileRepo
  * @group FileBackend
  */
old mode 100755 (executable)
new mode 100644 (file)
index 976fd6b..89ae688 100644 (file)
@@ -295,8 +295,11 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
         * @param $text_sha1 string: the base36 SHA-1 of the revision's text
         * @param $text string|false: (optional) The revision's string, or false to check for a
         *            revision stub
+        * @param $model int: the expected content model id (default: CONTENT_MODEL_WIKITEXT)
+        * @param $format int: the expected format model id (default: CONTENT_FORMAT_WIKITEXT)
         */
-       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false ) {
+       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false,
+                                                                               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) {
 
                $this->assertNodeStart( "revision" );
                $this->skipWhitespace();
@@ -311,9 +314,33 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
                $this->skipWhitespace();
 
                $this->assertTextNode( "comment", $summary );
+               $this->skipWhitespace();
+
+               if ( $this->xml->name == "text" ) {
+                       // note: <text> tag may occur here or at the very end.
+                       $text_found = true;
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               } else {
+                       $text_found = false;
+               }
 
                $this->assertTextNode( "sha1", $text_sha1 );
 
+               $this->assertTextNode( "model", $model );
+               $this->skipWhitespace();
+
+               $this->assertTextNode( "format", $format );
+               $this->skipWhitespace();
+
+               if ( !$text_found ) {
+                       $this->assertText( $id, $text_id, $text_bytes, $text );
+               }
+
+               $this->assertNodeEnd( "revision" );
+               $this->skipWhitespace();
+       }
+
+       protected function assertText( $id, $text_id, $text_bytes, $text ) {
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
                        $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
@@ -340,9 +367,5 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
                        $this->assertNodeEnd( "text" );
                        $this->skipWhitespace();
                }
-
-               $this->assertNodeEnd( "revision" );
-               $this->skipWhitespace();
        }
-
-}
+}
\ No newline at end of file
index a64e6d0..87d7156 100644 (file)
@@ -197,6 +197,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP1Summary1</comment>
       <text xml:space="preserve">BackupDumperTestP1Text1</text>
       <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
   </page>
 ';
@@ -214,6 +216,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP2Summary1</comment>
       <text xml:space="preserve">BackupDumperTestP2Text1</text>
       <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
     <revision>
       <id>5</id>
@@ -224,6 +228,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP2Summary4 extra</comment>
       <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
       <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
   </page>
 ';
@@ -241,6 +247,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>Talk BackupDumperTestP1 Summary1</comment>
       <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
       <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
   </page>
 ';
index 90fd902..fd43ae9 100644 (file)
@@ -478,6 +478,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP1Summary1</comment>
       <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId1_1 . '" bytes="23" />
     </revision>
   </page>
@@ -494,6 +496,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary1</comment>
       <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId2_1 . '" bytes="23" />
     </revision>
     <revision>
@@ -504,6 +508,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary2</comment>
       <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId2_2 . '" bytes="23" />
     </revision>
     <revision>
@@ -514,6 +520,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary3</comment>
       <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId2_3 . '" bytes="23" />
     </revision>
     <revision>
@@ -524,6 +532,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary4 extra</comment>
       <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId2_4 . '" bytes="44" />
     </revision>
   </page>
@@ -542,6 +552,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>Talk BackupDumperTestP1 Summary1</comment>
       <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text id="' . $this->textId4_1 . '" bytes="35" />
     </revision>
   </page>
old mode 100755 (executable)
new mode 100644 (file)
index f286fa1..ddce5c5 100644 (file)
@@ -11,7 +11,7 @@
                 timeoutForSmallTests="2"
                 timeoutForMediumTests="10"
                 timeoutForLargeTests="60"
-         strict="true"
+         strict="false"
                 verbose="true">
        <testsuites>
                <testsuite name="includes">