merged master
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 18 Sep 2012 15:43:20 +0000 (17:43 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Wed, 19 Sep 2012 10:34:10 +0000 (12:34 +0200)
Change-Id: I0ef7c7f33a5dc5855f38b20c03ddc5306f38ec66

130 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/DeprecatedGlobal.php
includes/EditPage.php
includes/Export.php
includes/FeedUtils.php
includes/GlobalFunctions.php
includes/ImagePage.php
includes/Import.php
includes/LinkFilter.php
includes/LinksUpdate.php
includes/Message.php
includes/Namespace.php
includes/OutputPage.php
includes/Revision.php
includes/SqlDataUpdate.php
includes/Title.php
includes/Uri.php [new file with mode: 0644]
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/ApiFeedContributions.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/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/installer/Ibm_db2Updater.php
includes/installer/Installer.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
includes/upload/UploadFromUrl.php
languages/Language.php
languages/LanguageConverter.php
languages/classes/LanguageFi.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesGa.php
languages/messages/MessagesQqq.php
maintenance/Maintenance.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/compareParsers.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/dumpIterator.php
maintenance/edit.php
maintenance/getText.php
maintenance/hiphop/make [changed mode: 0755->0644]
maintenance/hiphop/run-server [changed mode: 0755->0644]
maintenance/importSiteScripts.php
maintenance/importTextFile.php
maintenance/populateRevisionLength.php
maintenance/populateRevisionSha1.php
maintenance/preprocessDump.php
maintenance/refreshLinks.php
maintenance/renderDump.php
maintenance/storage/drop_content_model_info.sql [new file with mode: 0644]
maintenance/storage/make-blobs [changed mode: 0755->0644]
maintenance/storage/testCompression.php
maintenance/tables.sql
tests/parser/parserTest.inc
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/ArticleTablesTest.php
tests/phpunit/includes/ArticleTest.php
tests/phpunit/includes/ContentHandlerTest.php [new file with mode: 0644]
tests/phpunit/includes/CssContentTest.php [new file with mode: 0644]
tests/phpunit/includes/GlobalFunctions/GlobalTest.php
tests/phpunit/includes/JavascriptContentTest.php [new file with mode: 0644]
tests/phpunit/includes/LinksUpdateTest.php
tests/phpunit/includes/RevisionStorageTest.php
tests/phpunit/includes/RevisionStorageTest_ContentHandlerUseDB.php [new file with mode: 0644]
tests/phpunit/includes/RevisionTest.php
tests/phpunit/includes/TemplateCategoriesTest.php
tests/phpunit/includes/TimestampTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/UriTest.php [new file with mode: 0644]
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/api/ApiEditPageTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/includes/search/SearchEngineTest.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/maintenance/fetchTextTest.php
tests/phpunit/suite.xml
thumb.php

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 5207730..9b8d3a9 100644 (file)
@@ -283,6 +283,11 @@ $article: Article object
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
 
+'AfterFinalPageOutput': At the end of OutputPage::output() but before
+final ob_end_flush() which will send the buffered output to the client.
+This allows for last-minute modification of the output within the buffer
+by using ob_get_clean().
+
 'AfterImportPage': When a page import is completed
 $title: Title under which the revisions were imported
 $origTitle: Title provided by the XML file
@@ -429,9 +434,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
@@ -479,7 +489,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
@@ -487,7 +497,18 @@ $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()
+$flags: Flags passed to WikiPage::doEditContent()
+$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 WikiPage::doEditContent()
 $revision: New Revision of the article
 
 'ArticleMergeComplete': after merging to article using Special:Mergehistory
@@ -538,7 +559,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
@@ -547,7 +568,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
@@ -555,9 +585,22 @@ $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()
+$flags: Flags passed to WikiPage::doEditContent()
+$revision: New Revision of the article
+$status: Status object about to be returned by doEditContent()
+$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 WikiPage::doEditContent()
 $revision: New Revision of the article
-$status: Status object about to be returned by doEdit()
+$status: Status object about to be returned by doEditContent()
 $baseRevId: the rev ID (or false) this edit was based on
 
 'ArticleUndelete': When one or more revisions of an article are restored
@@ -586,11 +629,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
@@ -722,6 +773,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
@@ -795,12 +856,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
@@ -811,7 +879,7 @@ pages
 $editPage: EditPage    object
 
 'EditPage::attemptSave': called before an article is
-saved, that is before Article::doEdit() is called
+saved, that is before WikiPage::doEditContent() is called
 $editpage_Obj: the current EditPage object
 
 'EditPage::importFormData': allow extensions to read additional data
@@ -863,14 +931,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"
+
+'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
+'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
@@ -1730,7 +1812,8 @@ $title : Current Title object being displayed in search results.
 '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
@@ -2349,6 +2432,14 @@ 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 Content's getDeletionUpdates() method.
+$page: the WikiPage
+$content: the Content to generate updates for
+&$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 9ab4b6b..93e1dbf 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,18 +277,20 @@ class Article extends Page {
                                if ( $text === false ) {
                                        $text = '';
                                }
+
+                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
                                $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
-                               $text = wfMessage( $message )->text();
+                               $content = new MessageContent( $message, null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
 
-                       return $text;
+                       return $content;
                } else {
-                       $this->fetchContent();
+                       $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
 
-                       return $this->mContent;
+                       return $this->mContentObject;
                }
        }
 
@@ -336,22 +371,61 @@ 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!
+               ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+
+               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();
 
                # Pre-fill content with error message so that if something
                # fails we'll have something telling us what we intended.
-               $this->mContent = wfMessage( 'missing-revision', $oldid )->plain();
+               //XXX: this isn't page content but a UI message. horrible.
+               $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ) ;
 
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
@@ -371,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__ );
@@ -380,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;
        }
 
        /**
@@ -420,7 +495,7 @@ class Article extends Page {
         * @return Revision|null
         */
        public function getRevisionFetched() {
-               $this->fetchContent();
+               $this->fetchContentObject();
 
                return $this->mRevision;
        }
@@ -580,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 ) {
@@ -604,18 +679,28 @@ 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( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
+                                                                                                                               array( $this->fetchContentObject(), $this->getTitle(),
+                                                                                                                                               $outputPage ) ) ) {
+
                                                # 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;
                                                }
@@ -625,8 +710,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();
@@ -708,6 +794,8 @@ class Article extends Page {
        /**
         * Show a diff page according to current request variables. For use within
         * Article::view() only, other callers should use the DifferenceEngine class.
+        *
+        * @todo: make protected
         */
        public function showDiffPage() {
                $request = $this->getContext()->getRequest();
@@ -719,7 +807,17 @@ class Article extends Page {
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
 
-               $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+               $rev = $this->getRevisionFetched();
+
+               if ( !$rev ) {
+                       $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' )->text() );
+                       $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 );
+                       return;
+               }
+
+               $contentHandler = $rev->getContentHandler();
+               $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
@@ -737,22 +835,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 ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) {
+                       $po = $this->mContentObject->getParserOutput( $this->getTitle() );
+                       $wgOut->addHTML( $po->getText() );
                }
        }
 
@@ -1377,7 +1474,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
@@ -1617,6 +1720,8 @@ class Article extends Page {
         * @return ParserOutput or false if the given revsion ID is not found
         */
        public function getParserOutput( $oldid = null, User $user = null ) {
+               //XXX: bypasses mParserOptions and thus setParserOptions()
+
                if ( $user === null ) {
                        $parserOptions = $this->getParserOptions();
                } else {
@@ -1626,6 +1731,21 @@ class Article extends Page {
                return $this->mPage->getParserOutput( $parserOptions, $oldid );
        }
 
+       /**
+        * Override the ParserOptions used to render the primary article wikitext.
+        *
+        * @param ParserOptions $options
+        * @throws MWException if the parser options where already initialized.
+        */
+       public function setParserOptions( ParserOptions $options ) {
+               if ( $this->mParserOptions ) {
+                       throw new MWException( "can't change parser options after they have already been set" );
+               }
+
+               // clone, so if $options is modified later, it doesn't confuse the parser cache.
+               $this->mParserOptions = clone $options;
+       }
+
        /**
         * Get parser options suitable for rendering the primary article wikitext
         * @return ParserOptions
@@ -1891,7 +2011,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 ) ****** //
@@ -1929,6 +2051,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 706e1c8..788e3f2 100644 (file)
@@ -253,6 +253,7 @@ $wgAutoloadLocalClasses = array(
        'UnlistedSpecialPage' => 'includes/SpecialPage.php',
        'UploadSourceAdapter' => 'includes/Import.php',
        'UppercaseCollation' => 'includes/Collation.php',
+       'Uri' => 'includes/Uri.php',
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
        'UserArrayFromResult' => 'includes/UserArray.php',
@@ -288,6 +289,20 @@ $wgAutoloadLocalClasses = array(
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
        'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
 
+       # content handler
+       'Content' => 'includes/Content.php',
+       'AbstractContent' => 'includes/Content.php',
+       'ContentHandler' => 'includes/ContentHandler.php',
+       'CssContent' => 'includes/Content.php',
+       'TextContentHandler' => 'includes/ContentHandler.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',
@@ -330,6 +345,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',
@@ -1057,6 +1073,13 @@ $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/phpunit/includes
        'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php',
 
diff --git a/includes/Content.php b/includes/Content.php
new file mode 100644 (file)
index 0000000..da3b8ff
--- /dev/null
@@ -0,0 +1,1471 @@
+<?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 $maxlength int Maximum length of the summary text
+        * @return   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 object.
+        * Corresponds to the CONTENT_MODEL_XXX constants.
+        *
+        * @since WD.1
+        *
+        * @return String The model id
+        */
+       public function getModel();
+
+       /**
+        * Convenience method that returns the ContentHandler singleton for handling
+        * the content model that 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 that this Content object uses.
+        *
+        * Shorthand for $this->getContentHandler()->getDefaultFormat()
+        *
+        * @since WD.1
+        *
+        * @return String
+        */
+       public function getDefaultFormat();
+
+       /**
+        * Convenience method that returns the list of serialization formats
+        * supported for the content model that 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 $format string 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 $format null|string 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 $that Content 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 ) ;
+
+
+       /**
+        * Parse the Content object and generate a ParserOutput 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 $title Title The page title to use as a context for rendering
+        * @param $revId null|int The revision being rendered (optional)
+        * @param $options null|ParserOptions Any parser options
+        * @param $generateHtml Boolean 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 );
+       # 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 $title Title The context for determining the necessary updates
+        * @param $old Content|null An optional Content object representing the
+        *    previous content, i.e. the content being replaced by this Content
+        *    object.
+        * @param $recursive boolean Whether to include recursive updates (default:
+        *    false).
+        * @param $parserOutput ParserOutput|null 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( Title $title,
+               Content $old = null,
+               $recursive = true, ParserOutput $parserOutput = null
+       );
+
+       /**
+        * 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.
+        *
+        * There is usually no need to override the default behaviour, subclasses that
+        * want to implement redirects should override getRedirectTarget().
+        * 
+        * @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();
+
+       /**
+        * If this Content object is a redirect, this method updates the redirect target.
+        * Otherwise, it does nothing.
+        *
+        * @since WD.1
+        *
+        * @param Title $target the new redirect target
+        *
+        * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+        */
+       public function updateRedirect( Title $target );
+
+       /**
+        * Returns the section with the given ID.
+        *
+        * @since WD.1
+        *
+        * @param $sectionId string 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 $popts null|ParserOptions
+        * @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 $popts null|ParserOptions
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts );
+
+       /**
+        * Prepare Content for saving. Called before Content is saved by WikiPage::doEditContent() and in
+        * similar places.
+        *
+        * This may be used to check the content's consistency with global state. This function should
+        * NOT write any information to the database.
+        *
+        * Note that this method will usually be called inside the same transaction bracket that will be used
+        * to save the new revision.
+        *
+        * Note that this method is called before any update to the page table is performed. This means that
+        * $page may not yet know a page ID.
+        *
+        * @param WikiPage $page The page to be saved.
+        * @param int      $flags bitfield for use with EDIT_XXX constants, see WikiPage::doEditContent()
+        * @param int      $baseRevId the ID of the current revision
+        * @param User     $user
+        *
+        * @return Status A status object indicating whether the content was successfully prepared for saving.
+        *                If the returned status indicates an error, a rollback will be performed and the
+        *                transaction aborted.
+        *
+        * @see see WikiPage::doEditContent()
+        */
+       public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user );
+
+       /**
+        * Returns a list of updates to perform when this 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 $page \WikiPage the deleted page
+        * @param $parserOutput null|\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 after deletion.
+        */
+       public function getDeletionUpdates( WikiPage $page,
+               ParserOutput $parserOutput = null );
+
+       /**
+        * Returns true if this Content object matches the given magic word.
+        *
+        * @param MagicWord $word the magic word to match
+        *
+        * @return bool whether this Content object matches the given magic word.
+        */
+       public function matchMagicWord( MagicWord $word );
+
+       # TODO: ImagePage and CategoryPage interfere with per-content action handlers
+       # TODO: make sure WikiSearch extension still works
+       # TODO: make sure ReplaceTemplates extension still works
+       # TODO: 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
+}
+
+
+/**
+ * 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 String $model_id
+        */
+       public function __construct( $model_id = null ) {
+               $this->model_id = $model_id;
+       }
+
+       /**
+        * @see Content::getModel()
+        */
+       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 $model_id int the model to check
+        *
+        * @throws MWException
+        */
+       protected function checkModelID( $model_id ) {
+               if ( $model_id !== $this->model_id ) {
+                       throw new MWException( "Bad content model: " .
+                               "expected {$this->model_id}  " .
+                               "but got $model_id." );
+               }
+       }
+
+       /**
+        * @see Content::getContentHandler()
+        */
+       public function getContentHandler() {
+               return ContentHandler::getForContent( $this );
+       }
+
+       /**
+        * @see Content::getDefaultFormat()
+        */
+       public function getDefaultFormat() {
+               return $this->getContentHandler()->getDefaultFormat();
+       }
+
+       /**
+        * @see Content::getSupportedFormats()
+        */
+       public function getSupportedFormats() {
+               return $this->getContentHandler()->getSupportedFormats();
+       }
+
+       /**
+        * @see Content::isSupportedFormat()
+        */
+       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() );
+               }
+       }
+
+       /**
+        * @see Content::serialize
+        */
+       public function serialize( $format = null ) {
+               return $this->getContentHandler()->serializeContent( $this, $format );
+       }
+
+       /**
+        * @see Content::isEmpty()
+        */
+       public function isEmpty() {
+               return $this->getSize() == 0;
+       }
+
+       /**
+        * @see Content::isValid()
+        */
+       public function isValid() {
+               return true;
+       }
+
+       /**
+        * @see Content::equals()
+        */
+       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();
+       }
+
+
+       /**
+        * Returns a list of DataUpdate objects for recording information about this
+        * Content in some secondary data store.
+        *
+        * This default implementation calls
+        * $this->getParserOutput( $content, $title, null, null, false ),
+        * and then calls getSecondaryDataUpdates( $title, $recursive ) on the
+        * resulting ParserOutput object.
+        *
+        * Subclasses may override this to determine the secondary data updates more
+        * efficiently, preferrably without the need to generate a parser output object.
+        *
+        * @see Content::getSecondaryDataUpdates()
+        *
+        * @param $title Title The context for determining the necessary updates
+        * @param $old Content|null An optional Content object representing the
+        *    previous content, i.e. the content being replaced by this Content
+        *    object.
+        * @param $recursive boolean Whether to include recursive updates (default:
+        *    false).
+        * @param $parserOutput ParserOutput|null 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( Title $title,
+               Content $old = null,
+               $recursive = true, ParserOutput $parserOutput = null
+       ) {
+               if ( !$parserOutput ) {
+                       $parserOutput = $this->getParserOutput( $title, null, null, false );
+               }
+
+               return $parserOutput->getSecondaryDataUpdates( $title, $recursive );
+       }
+
+
+       /**
+        * @see Content::getRedirectChain()
+        */
+       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;
+       }
+
+       /**
+        * @see Content::getRedirectTarget()
+        */
+       public function getRedirectTarget() {
+               return null;
+       }
+
+       /**
+        * @see Content::getUltimateRedirectTarget()
+        * @note: migrated here from Title::newFromRedirectRecurse
+        */
+       public function getUltimateRedirectTarget() {
+               $titles = $this->getRedirectChain();
+               return $titles ? array_pop( $titles ) : null;
+       }
+
+       /**
+        * @see Content::isRedirect()
+        *
+        * @since WD.1
+        *
+        * @return bool
+        */
+       public function isRedirect() {
+               return $this->getRedirectTarget() !== null;
+       }
+
+       /**
+        * @see Content::updateRedirect()
+        *
+        * This default implementation always returns $this.
+        *
+        * @since WD.1
+        *
+        * @return Content $this
+        */
+       public function updateRedirect( Title $target ) {
+               return $this;
+       }
+
+       /**
+        * @see Content::getSection()
+        */
+       public function getSection( $sectionId ) {
+               return null;
+       }
+
+       /**
+        * @see Content::replaceSection()
+        */
+       public function replaceSection( $section, Content $with, $sectionTitle = ''  ) {
+               return null;
+       }
+
+       /**
+        * @see Content::preSaveTransform()
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               return $this;
+       }
+
+       /**
+        * @see Content::addSectionHeader()
+        */
+       public function addSectionHeader( $header ) {
+               return $this;
+       }
+
+       /**
+        * @see Content::preloadTransform()
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts ) {
+               return $this;
+       }
+
+       /**
+        * @see  Content::prepareSave()
+        */
+       public function prepareSave( WikiPage $page, $flags, $baseRevId, User $user ) {
+               if ( $this->isValid() ) {
+                       return Status::newGood();
+               } else {
+                       return Status::newFatal( "invalid-content-data" );
+               }
+       }
+
+       /**
+        * @see  Content::getDeletionUpdates()
+        *
+        * @since WD.1
+        *
+        * @param $page \WikiPage the deleted page
+        * @param $parserOutput null|\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 after deletion.
+        */
+       public function getDeletionUpdates( WikiPage $page,
+               ParserOutput $parserOutput = null )
+       {
+               return array(
+                       new LinksDeletionUpdate( $page ),
+               );
+       }
+
+       /**
+        * @see  Content::matchMagicWord()
+        *
+        * This default implementation always returns false. Subclasses may override this to supply matching logic.
+        *
+        * @param MagicWord $word
+        *
+        * @return bool
+        */
+       public function matchMagicWord( MagicWord $word ) {
+               return false;
+       }
+}
+
+/**
+ * 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.
+        *
+        * @param   the raw text
+        */
+       public function getNativeData( ) {
+               $text = $this->mText;
+               return $text;
+       }
+
+       /**
+        * Returns the text represented by this Content object, as a string.
+        *
+        * @param   the raw text
+        */
+       public function getTextForSearchIndex( ) {
+               return $this->getNativeData();
+       }
+
+       /**
+        * Returns the text represented by this Content object, as a string.
+        *
+        * @param   the raw text
+        */
+       public function getWikitextForTransclusion( ) {
+               return $this->getNativeData();
+       }
+
+       /**
+        * Diff this content object with another content object..
+        *
+        * @since WD.diff
+        *
+        * @param $that Content the other content object to compare this content object to
+        * @param $lang Language 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;
+       }
+
+
+       /**
+        * Returns a generic ParserOutput object, wrapping the HTML returned by
+        * getHtml().
+        *
+        * @param $title Title Context title for parsing
+        * @param $revId int|null Revision ID (for {{REVISIONID}})
+        * @param $options ParserOptions|null Parser options
+        * @param $generateHtml bool Whether or not to generate HTML
+        *
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( Title $title,
+               $revId = null,
+               ParserOptions $options = null, $generateHtml = true
+       ) {
+               # Generic implementation, relying on $this->getHtml()
+
+               if ( $generateHtml ) {
+                       $html = $this->getHtml();
+               } 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.
+        *
+        * @return string An HTML representation of the content
+        */
+       protected function getHtml() {
+               return $this->getHighlightHtml();
+       }
+
+       /**
+        * Generates a syntax-highlighted version of the content, as HTML.
+        * Used by the default implementation of getHtml().
+        *
+        * @return string an HTML representation of the content's markup
+        */
+       protected function getHighlightHtml( ) {
+               # TODO: make Highlighter interface, use highlighter here, if available
+               return htmlspecialchars( $this->getNativeData() );
+       }
+}
+
+/**
+ * @since WD.1
+ */
+class WikitextContent extends TextContent {
+
+       public function __construct( $text ) {
+               parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
+       }
+
+       /**
+        * @see Content::getSection()
+        */
+       public function getSection( $section ) {
+               global $wgParser;
+
+               $text = $this->getNativeData();
+               $sect = $wgParser->getSection( $text, $section, false );
+
+               return  new WikitextContent( $sect );
+       }
+
+       /**
+        * @see Content::replaceSection()
+        */
+       public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+               wfProfileIn( __METHOD__ );
+
+               $myModelId = $this->getModel();
+               $sectionModelId = $with->getModel();
+
+               if ( $sectionModelId != $myModelId  ) {
+                       throw new MWException( "Incompatible content model for section: " .
+                               "document uses $myModelId but " .
+                               "section uses $sectionModelId." );
+               }
+
+               $oldtext = $this->getNativeData();
+               $text = $with->getNativeData();
+
+               if ( $section === '' ) {
+                       return $with; # XXX: copy first?
+               } if ( $section == 'new' ) {
+                       # Inserting a new section
+                       $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
+                               ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\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 = wfMessage( 'newsectionheaderdefaultlevel' )
+                               ->inContentLanguage()->params( $header )->text();
+               $text .= "\n\n";
+               $text .= $this->getNativeData();
+
+               return new WikitextContent( $text );
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied using
+        * Parser::preSaveTransform().
+        *
+        * @param $title Title
+        * @param $user User
+        * @param $popts ParserOptions
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               global $wgParser;
+
+               $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 $popts ParserOptions
+        * @return Content
+        */
+       public function preloadTransform( Title $title, ParserOptions $popts ) {
+               global $wgParser;
+
+               $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;
+       }
+
+       /**
+        * @see   Content::updateRedirect()
+        *
+        * This implementation replaces the first link on the page with the given new target
+        * if this Content object is a redirect. Otherwise, this method returns $this.
+        *
+        * @since WD.1
+        *
+        * @param Title $target
+        *
+        * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
+        */
+       public function updateRedirect( Title $target ) {
+               if ( !$this->isRedirect() ) {
+                       return $this;
+               }
+
+               # Fix the text
+               # Remember that redirect pages can have categories, templates, etc.,
+               # so the regex has to be fairly general
+               $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
+                       '[[' . $target->getFullText() . ']]',
+                       $this->getNativeData(), 1 );
+
+               return new WikitextContent( $newText );
+       }
+
+       /**
+        * Returns true if this content is not a redirect, and this content's text
+        * is countable according to the criteria defined by $wgArticleCountMethod.
+        *
+        * @param $hasLinks Bool  if it is known whether this content contains
+        *    links, provide this information here, to avoid redundant parsing to
+        *    find out.
+        * @param $title null|\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;
+       }
+
+
+       /**
+        * 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 $revId null
+        * @param $options null|ParserOptions
+        * @param $generateHtml bool
+        *
+        * @internal param \IContextSource|null $context
+        * @return ParserOutput representing the HTML form of the text
+        */
+       public function getParserOutput( Title $title,
+               $revId = null,
+               ParserOptions $options = null, $generateHtml = true
+       ) {
+               global $wgParser;
+
+               if ( !$options ) {
+                       $options = new ParserOptions();
+               }
+
+               $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
+               return $po;
+       }
+
+       protected function getHtml() {
+               throw new MWException(
+                       "getHtml() not implemented for wikitext. "
+                               . "Use getParserOutput()->getText()."
+               );
+       }
+
+       /**
+        * @see  Content::matchMagicWord()
+        *
+        * This implementation calls $word->match() on the this TextContent object's text.
+        *
+        * @param MagicWord $word
+        *
+        * @return bool whether this Content object matches the given magic word.
+        */
+       public function matchMagicWord( MagicWord $word ) {
+               return $word->match( $this->getNativeData() );
+       }
+}
+
+/**
+ * Wrapper allowing us to handle a system message as a Content object. Note that this is generally *not* used
+ * to represent content from the MediaWiki namespace, and that there is no MessageContentHandler. MessageContent
+ * is just intended as glue for wrapping a message programatically.
+ *
+ * @since WD.1
+ */
+class MessageContent extends AbstractContent {
+
+       /**
+        * @var Message
+        */
+       protected $mMessage;
+
+       /**
+        * @param Message|String $msg    A Message object, or a message key
+        * @param array|null     $params An optional array of message parameters
+        */
+       public function __construct( $msg, $params = null ) {
+               # XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
+               parent::__construct( CONTENT_MODEL_WIKITEXT );
+
+               if ( is_string( $msg ) ) {
+                       $this->mMessage = wfMessage( $msg );
+               } else {
+                       $this->mMessage = clone $msg;
+               }
+
+               if ( $params ) {
+                       $this->mMessage = $this->mMessage->params( $params );
+               }
+       }
+
+       /**
+        * Returns the message as rendered HTML
+        *
+        * @return string The message text, parsed into html
+        */
+       public function getHtml() {
+               return $this->mMessage->parse();
+       }
+
+       /**
+        * Returns the message as rendered HTML
+        *
+        * @return string The message text, parsed into html
+        */
+       public function getWikitext() {
+               return $this->mMessage->text();
+       }
+
+       /**
+        * Returns the message object, with any parameters already substituted.
+        *
+        * @return Message The message object.
+        */
+       public function getNativeData() {
+               //NOTE: Message objects are mutable. Cloning here makes MessageContent immutable.
+               return clone $this->mMessage;
+       }
+
+       /**
+        * @see Content::getTextForSearchIndex
+        */
+       public function getTextForSearchIndex() {
+               return $this->mMessage->plain();
+       }
+
+       /**
+        * @see Content::getWikitextForTransclusion
+        */
+       public function getWikitextForTransclusion() {
+               return $this->getWikitext();
+       }
+
+       /**
+        * @see Content::getTextForSummary
+        */
+       public function getTextForSummary( $maxlength = 250 ) {
+               return substr( $this->mMessage->plain(), 0, $maxlength );
+       }
+
+       /**
+        * @see Content::getSize
+        *
+        * @return int
+        */
+       public function getSize() {
+               return strlen( $this->mMessage->plain() );
+       }
+
+       /**
+        * @see Content::copy
+        *
+        * @return Content. A copy of this object
+        */
+       public function copy() {
+               // MessageContent is immutable (because getNativeData() returns a clone of the Message object)
+               return $this;
+       }
+
+       /**
+        * @see Content::isCountable
+        *
+        * @return bool false
+        */
+       public function isCountable( $hasLinks = null ) {
+               return false;
+       }
+
+       /**
+        * @see Content::getParserOutput
+        *
+        * @return ParserOutput
+        */
+       public function getParserOutput(
+               Title $title, $revId = null,
+               ParserOptions $options = null, $generateHtml = true
+       ) {
+
+               if ( $generateHtml ) {
+                       $html = $this->getHtml();
+               } else {
+                       $html = '';
+               }
+
+               $po = new ParserOutput( $html );
+               return $po;
+       }
+}
+
+/**
+ * @since WD.1
+ */
+class JavaScriptContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct( $text, CONTENT_MODEL_JAVASCRIPT );
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied using
+        * Parser::preSaveTransform().
+        *
+        * @param Title $title
+        * @param User $user
+        * @param ParserOptions $popts
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               global $wgParser;
+               // @todo: make pre-save transformation optional for script pages
+               // See bug #32858
+
+               $text = $this->getNativeData();
+               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+               return new JavaScriptContent( $pst );
+       }
+
+
+       protected function getHtml( ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+               $html .= $this->getHighlightHtml( );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+}
+
+/**
+ * @since WD.1
+ */
+class CssContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct( $text, CONTENT_MODEL_CSS );
+       }
+
+       /**
+        * Returns a Content object with pre-save transformations applied using
+        * Parser::preSaveTransform().
+        *
+        * @param $title Title
+        * @param $user User
+        * @param $popts ParserOptions
+        * @return Content
+        */
+       public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
+               global $wgParser;
+               // @todo: make pre-save transformation optional for script pages
+
+               $text = $this->getNativeData();
+               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+               return new CssContent( $pst );
+       }
+
+
+       protected function getHtml( ) {
+               $html = "";
+               $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+               $html .= $this->getHighlightHtml( );
+               $html .= "\n</pre>\n";
+
+               return $html;
+       }
+}
diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php
new file mode 100644 (file)
index 0000000..bf1349a
--- /dev/null
@@ -0,0 +1,1136 @@
+<?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 a.k.a. MIME type) and is unserialized into its native
+ * PHP representation (the content model), which is wrapped in an instance of
+ * the appropriate subclass of Content.
+ *
+ * ContentHandler instances are stateless singletons that serve, among other
+ * things, as a factory for Content objects. Generally, there is one subclass
+ * of ContentHandler and one subclass of Content for every type of content model.
+ *
+ * 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
+        * @deprecated since WD.1. Always try to use the content object.
+        *
+        * @static
+        * @param $content Content|null
+        * @return null|string the textual form of $content, if available
+        * @throws MWException if $content is not an instance of TextContent and
+        *   $wgContentHandlerTextFallback was set to 'fail'.
+        */
+       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 $text string the textual representation, will be
+        *    unserialized to create the Content object
+        * @param $title null|Title the title of the page this text belongs to.
+        *    Required if $modelId is not provided.
+        * @param $modelId null|string the model to deserialize to. If not provided,
+        *    $title->getContentModel() is used.
+        * @param $format null|string the format to use for deserialization. If not
+        *    given, the model's default format is used.
+        *
+        * @return Content a Content object representing $text
+        *
+        * @throw MWException if $model or $format is not supported or if $text can
+        *    not be unserialized using $format.
+        */
+       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 by default for the page is determined based
+        * on several factors:
+        * - The global setting $wgNamespaceContentModels specifies a content model
+        *   per namespace.
+        * - The hook DefaultModelFor may be used to override the page's default
+        *   model.
+        * - Pages in NS_MEDIAWIKI and NS_USER default to the CSS or JavaScript
+        *   model if they end in .js or .css, respectively.
+        * - Pages in NS_MEDIAWIKI default to the wikitext model otherwise.
+        * - The hook TitleIsCssOrJsPage may be used to force a page to use the CSS
+        *   or JavaScript model if they end in .js or .css, respectively.
+        * - The hook TitleIsWikitextPage may be used to force a page to use the
+        *   wikitext model.
+        *
+        * If none of the above applies, the wikitext model is used.
+        *
+        * 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 initialize the mContentModel member.
+
+               $ns = $title->getNamespace();
+
+               $ext = false;
+               $m = null;
+               $model = null;
+
+               if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+                       $model = $wgNamespaceContentModels[ $ns ];
+               }
+
+               // Hook can determine 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 it 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 );
+       }
+
+       /**
+        * @var Array A Cache of ContentHandler instances by model id
+        */
+       static $handlers;
+
+       /**
+        * Returns the ContentHandler singleton for the given model ID. Use the
+        * CONTENT_MODEL_XXX constants to identify the desired content model.
+        *
+        * ContentHandler singletons are taken from the global $wgContentHandlers
+        * array. Keys in that array are model names, the values are either
+        * ContentHandler singleton objects, or strings specifying the appropriate
+        * subclass of ContentHandler.
+        *
+        * If a class name is encountered when looking up the singleton for a given
+        * model name, the class is instantiated and the class name is replaced by
+        * the resulting singleton in $wgContentHandlers.
+        *
+        * If no ContentHandler is defined for the desired $modelId, the
+        * ContentHandler may be provided by the ContentHandlerForModelID hook.
+        * If no ContentHandler can be determined, an MWException is raised.
+        *
+        * @since WD.1
+        *
+        * @static
+        * @param $modelId String The ID of the content model for which to get a
+        *    handler. Use CONTENT_MODEL_XXX constants.
+        * @return ContentHandler The ContentHandler singleton for handling the
+        *    model given by $modelId
+        * @throws MWException if no handler is known for $modelId.
+        */
+       public static function getForModelID( $modelId ) {
+               global $wgContentHandlers;
+
+               if ( isset( ContentHandler::$handlers[$modelId] ) ) {
+                       return ContentHandler::$handlers[$modelId];
+               }
+
+               if ( empty( $wgContentHandlers[$modelId] ) ) {
+                       $handler = null;
+
+                       wfRunHooks( 'ContentHandlerForModelID', array( $modelId, &$handler ) );
+
+                       if ( $handler === null ) {
+                               throw new MWException( "No handler for model #$modelId registered in \$wgContentHandlers" );
+                       }
+
+                       if ( !( $handler instanceof ContentHandler ) ) {
+                               throw new MWException( "ContentHandlerForModelID must supply a ContentHandler instance" );
+                       }
+               } else {
+                       $class = $wgContentHandlers[$modelId];
+                       $handler = new $class( $modelId );
+
+                       if ( !( $handler instanceof ContentHandler ) ) {
+                               throw new MWException( "$class from \$wgContentHandlers is not compatible with ContentHandler" );
+                       }
+               }
+
+               ContentHandler::$handlers[$modelId] = $handler;
+               return ContentHandler::$handlers[$modelId];
+       }
+
+       /**
+        * Returns the localized name for a given content model.
+        *
+        * Model names are localized using system messages. Message keys
+        * have the form content-model-$name, where $name is getContentModelName( $id ).
+        *
+        * @static
+        * @param $name String The content model ID, as given by a CONTENT_MODEL_XXX
+        *    constant or returned by Revision::getContentModel().
+        *
+        * @return string The content format's localized name.
+        * @throws MWException if the model id isn't known.
+        */
+       public static function getLocalizedName( $name ) {
+               $key = "content-model-$name";
+
+               if ( wfEmptyMsg( $key ) ) return $name;
+               else return wfMsg( $key );
+       }
+
+       public static function getContentModels() {
+               global $wgContentHandlers;
+
+               return array_keys( $wgContentHandlers );
+       }
+
+       public static function getAllContentFormats() {
+               global $wgContentHandlers;
+
+               $formats = array();
+
+               foreach ( $wgContentHandlers as $model => $class ) {
+                       $handler = ContentHandler::getForModelID( $model );
+                       $formats = array_merge( $formats, $handler->getSupportedFormats() );
+               }
+
+               $formats = array_unique( $formats );
+               return $formats;
+       }
+
+       // ------------------------------------------------------------------------
+
+       protected $mModelID;
+       protected $mSupportedFormats;
+
+       /**
+        * Constructor, initializing the ContentHandler instance with its model ID
+        * and a list of supported formats. Values for the parameters are typically
+        * provided as literals by subclass's constructors.
+        *
+        * @param $modelId String (use CONTENT_MODEL_XXX constants).
+        * @param $formats array List for supported serialization formats
+        *    (typically as MIME types)
+        */
+       public function __construct( $modelId, $formats ) {
+               $this->mModelID = $modelId;
+               $this->mSupportedFormats = $formats;
+
+               $this->mModelName = preg_replace( '/(Content)?Handler$/', '', get_class( $this ) );
+               $this->mModelName = preg_replace( '/[_\\\\]/', '', $this->mModelName );
+               $this->mModelName = strtolower( $this->mModelName );
+       }
+
+       /**
+        * Serializes a Content object of the type supported by this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @abstract
+        * @param $content Content The Content object to serialize
+        * @param $format null|String The desired serialization format
+        * @return string Serialized form of the content
+        */
+       public abstract function serializeContent( Content $content, $format = null );
+
+       /**
+        * Unserializes a Content object of the type supported by this ContentHandler.
+        *
+        * @since WD.1
+        *
+        * @abstract
+        * @param $blob string serialized form of the content
+        * @param $format null|String the format used for serialization
+        * @return Content the Content object created by deserializing $blob
+        */
+       public abstract function unserializeContent( $blob, $format = null );
+
+       /**
+        * Creates an empty Content object of the type supported by this
+        * ContentHandler.
+        *
+        * @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 String 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 String $model_id The model to check
+        *
+        * @throws MWException
+        */
+       protected function checkModelID( $model_id ) {
+               if ( $model_id !== $this->mModelID ) {
+                       throw new MWException( "Bad content model: " .
+                               "expected {$this->mModelID} " .
+                               "but got $model_id." );
+               }
+       }
+
+       /**
+        * Returns a list of serialization formats supported by the
+        * serializeContent() and unserializeContent() methods of this
+        * ContentHandler.
+        *
+        * @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 by 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 $format string 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 $format string the serialization format to check
+        *
+        * @throws MWException
+        */
+       protected function checkFormat( $format ) {
+               if ( !$this->isSupportedFormat( $format ) ) {
+                       throw new MWException(
+                               "Format $format is not supported for content model "
+                               . $this->getModelID()
+                       );
+               }
+       }
+
+       /**
+        * 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 for creating an appropriate DifferenceEngine for this content model.
+        *
+        * @since WD.1
+        *
+        * @param $context IContextSource context to use, anything else will be
+        *    ignored
+        * @param $old Integer Old ID we want to show and diff with.
+        * @param $new int|string String either 'prev' or 'next'.
+        * @param $rcid Integer ??? FIXME (default 0)
+        * @param $refreshCache boolean If set, refreshes the diff cache
+        * @param $unhide boolean If set, allow viewing deleted revs
+        *
+        * @return DifferenceEngine
+        */
+       public function createDifferenceEngine( IContextSource $context,
+               $old = 0, $new = 0,
+               $rcid = 0, # FIXME: use everywhere!
+               $refreshCache = false, $unhide = false
+       ) {
+               $this->checkModelID( $context->getTitle()->getContentModel() );
+
+               $diffEngineClass = $this->getDiffEngineClass();
+
+               return new $diffEngineClass( $context, $old, $new, $rcid, $refreshCache, $unhide );
+       }
+
+       /**
+        * Get the language in which the content of the given page is written.
+        *
+        * This default implementation just returns $wgContLang (except for pages in the MediaWiki namespace)
+        *
+        * Note that the pages language is not cacheable, since it may in some cases depend on user settings.
+        *
+        * Also note that the page language may or may not depend on the actual content of the page,
+        * that is, this method may load the content in order to determine the language.
+        *
+        * @since 1.WD
+        *
+        * @param Title        $title the page to determine the language for.
+        * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+        *
+        * @return Language the page's language
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               global $wgContLang;
+
+               if ( $title->getNamespace() == NS_MEDIAWIKI ) {
+                       // Parse mediawiki messages with correct target language
+                       list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $title->getText() );
+                       return wfGetLangObj( $lang );
+               }
+
+               return $wgContLang;
+       }
+
+       /**
+        * Get the language in which the content of this page is written when
+        * viewed by user. Defaults to $this->getPageLanguage(), but if the user
+        * specified a preferred variant, the variant will be used.
+        *
+        * This default implementation just returns $this->getPageLanguage( $title, $content ) unless
+        * the user specified a preferred variant.
+        *
+        * Note that the pages view language is not cacheable, since it depends on user settings.
+        *
+        * Also note that the page language may or may not depend on the actual content of the page,
+        * that is, this method may load the content in order to determine the language.
+        *
+        * @since 1.WD
+        *
+        * @param Title        $title the page to determine the language for.
+        * @param Content|null $content the page's content, if you have it handy, to avoid reloading it.
+        *
+        * @return Language the page's language for viewing
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               $pageLang = $this->getPageLanguage( $title, $content );
+
+               if ( $title->getNamespace() !== NS_MEDIAWIKI ) {
+                       // If the user chooses a variant, the content is actually
+                       // in a language whose code is the variant code.
+                       $variant = $pageLang->getPreferredVariant();
+                       if ( $pageLang->getCode() !== $variant ) {
+                               $pageLang = Language::factory( $variant );
+                       }
+               }
+
+               return $pageLang;
+       }
+
+       /**
+        * Determines whether the content type handled by this ContentHandler
+        * can be used on the given page.
+        *
+        * This default implementation always returns true.
+        * Subclasses may override this to restrict the use of this content model to specific locations,
+        * typically based on the namespace or some other aspect of the title, such as a special suffix
+        * (e.g. ".svg" for SVG content).
+        *
+        * @param Title $title the page's title.
+        *
+        * @return bool true if content of this kind can be used on the given page, false otherwise.
+        */
+       public function canBeUsedOn( Title $title ) {
+               return true;
+       }
+
+       /**
+        * Returns the name of the diff engine to use.
+        *
+        * @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 $oldContent Content|string  String
+        * @param $myContent Content|string   String
+        * @param $yourContent Content|string 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 ) ) {
+                       if ( !is_object( $ot )
+                               || !$rt->equals( $ot )
+                               || $ot->getFragment() != $rt->getFragment() )
+                       {
+                               $truncatedtext = $newContent->getTextForSummary(
+                                       250
+                                               - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
+                                               - strlen( $rt->getFullText() ) );
+
+                               return wfMessage( 'autoredircomment', $rt->getFullText() )
+                                               ->rawParams( $truncatedtext )->inContentLanguage()->text();
+                       }
+               }
+
+               // New page auto-summaries
+               if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
+                       // If they're making a new article, give its text, truncated, in
+                       // the summary.
+
+                       $truncatedtext = $newContent->getTextForSummary(
+                               200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) );
+
+                       return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
+                                       ->inContentLanguage()->text();
+               }
+
+               // Blanking auto-summaries
+               if ( !empty( $oldContent ) && $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+                       return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
+               } elseif ( !empty( $oldContent )
+                       && $oldContent->getSize() > 10 * $newContent->getSize()
+                       && $newContent->getSize() < 500 )
+               {
+                       // Removing more than 90% of the article
+
+                       $truncatedtext = $newContent->getTextForSummary(
+                               200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) );
+
+                       return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
+                                       ->inContentLanguage()->text();
+               }
+
+               // If we reach this point, there's no applicable auto-summary for our
+               // case, so our auto-summary is empty.
+               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 = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
+               } else {
+                       if ( $onlyAuthor ) {
+                               $reason = wfMessage(
+                                       'excontentauthor',
+                                       '$1',
+                                       $onlyAuthor
+                               )->inContentLanguage()->text();
+                       } else {
+                               $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
+                       }
+               }
+
+               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;
+       }
+
+       /**
+        * 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 true if this content model supports sections.
+        *
+        * This default implementation returns false.
+        *
+        * @return boolean whether sections are supported.
+        */
+       public function supportsSections() {
+               return false;
+       }
+
+       /**
+        * Call a legacy hook that uses text instead of Content objects.
+        * Will log a warning when a matching hook function is registered.
+        * If the textual representation of the content is changed by the
+        * hook function, a new Content object is constructed from the new
+        * text.
+        *
+        * @param $event String: event name
+        * @param $args Array: parameters passed to hook functions
+        * @param $warn bool: whether to log a warning (default: true). Should generally be true,
+        *                    may be set to false for testing.
+        *
+        * @return Boolean True if no handler aborted the hook
+        */
+       public static function runLegacyHooks( $event, $args = array(), $warn = true ) {
+               if ( !Hooks::isRegistered( $event ) ) {
+                       return true; // nothing to do here
+               }
+
+               if ( $warn ) {
+                       wfWarn( "Using obsolete hook $event" );
+               }
+
+               // convert Content objects to text
+               $contentObjects = array();
+               $contentTexts = array();
+
+               foreach ( $args as $k => $v ) {
+                       if ( $v instanceof Content ) {
+                               /* @var Content $v */
+
+                               $contentObjects[$k] = $v;
+
+                               $v = $v->serialize();
+                               $contentTexts[ $k ] = $v;
+                               $args[ $k ] = $v;
+                       }
+               }
+
+               // call the hook functions
+               $ok = wfRunHooks( $event, $args );
+
+               // see if the hook changed the text
+               foreach ( $contentTexts as $k => $orig ) {
+                       /* @var Content $content */
+
+                       $modified = $args[ $k ];
+                       $content = $contentObjects[$k];
+
+                       if ( $modified !== $orig ) {
+                               // text was changed, create updated Content object
+                               $content = $content->getContentHandler()->unserializeContent( $modified );
+                       }
+
+                       $args[ $k ] = $content;
+               }
+
+               return $ok;
+       }
+}
+
+/**
+ * @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 $format string|null
+        * @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 $oldContent \Content|string  String
+        * @param $myContent \Content|string   String
+        * @param $yourContent \Content|string 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;
+       }
+
+}
+
+/**
+ * @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 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( '' );
+       }
+
+       /**
+        * Returns the english language, because JS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageLanguage()
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
+
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageViewLanguage()
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
+}
+
+/**
+ * @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( '' );
+       }
+
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageLanguage()
+        */
+       public function getPageLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
+
+       /**
+        * Returns the english language, because CSS is english, and should be handled as such.
+        *
+        * @return Language wfGetLangObj( 'en' )
+        *
+        * @see ContentHandler::getPageViewLanguage()
+        */
+       public function getPageViewLanguage( Title $title, Content $content = null ) {
+               return wfGetLangObj( 'en' );
+       }
+}
index 2fa0c57..da4d0ed 100644 (file)
@@ -732,6 +732,16 @@ $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
+);
+
 /**
  * Resizing can be done using PHP's internal image libraries or using
  * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -6223,6 +6233,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;
+
 /**
  * Whether the user must enter their password to change their e-mail address
  *
index be9f981..b589493 100644 (file)
@@ -261,7 +261,7 @@ define( 'APCOND_BLOCKED', 8 );
 define( 'APCOND_ISBOT', 9 );
 /**@}*/
 
-/**
+/** @{
  * Protocol constants for wfExpandUrl()
  */
 define( 'PROTO_HTTP', 'http://' );
@@ -270,3 +270,34 @@ define( 'PROTO_RELATIVE', '//' );
 define( 'PROTO_CURRENT', null );
 define( 'PROTO_CANONICAL', 1 );
 define( 'PROTO_INTERNAL', 2 );
+/**@}*/
+
+/**@{
+ * Content model ids, used by Content and ContentHandler.
+ * These IDs will be exposed in the API and XML dumps.
+ *
+ * Extensions that define their own content model IDs should take
+ * care to avoid conflicts. Using the extension name as a prefix is recommended.
+ */
+define( 'CONTENT_MODEL_WIKITEXT', 'wikitext' );
+define( 'CONTENT_MODEL_JAVASCRIPT', 'javascript' );
+define( 'CONTENT_MODEL_CSS', 'css' );
+define( 'CONTENT_MODEL_TEXT', 'text' );
+/**@}*/
+
+/**@{
+ * Content formats, used by Content and ContentHandler.
+ * These should be MIME types, and will be exposed in the API and XML dumps.
+ *
+ * Extensions are free to use the below formats, or define their own.
+ * It is recommended to stick with the conventions for MIME types.
+ */
+define( 'CONTENT_FORMAT_WIKITEXT', 'text/x-wiki' ); // wikitext
+define( 'CONTENT_FORMAT_JAVASCRIPT', 'text/javascript' ); // for js pages
+define( 'CONTENT_FORMAT_CSS', 'text/css' );  // for css pages
+define( 'CONTENT_FORMAT_TEXT', 'text/plain' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_HTML', 'text/html' ); // for future use, e.g. with some plain-html messages.
+define( 'CONTENT_FORMAT_SERIALIZED', 'application/vnd.php.serialized' ); // for future use with the api and for extensions
+define( 'CONTENT_FORMAT_JSON', 'application/json' ); // for future use with the api, and for use by extensions
+define( 'CONTENT_FORMAT_XML', 'application/xml' ); // for future use with the api, and for use by extensions
+/**@}*/
index 4d7b968..d48bd0b 100644 (file)
@@ -27,7 +27,7 @@
  */
 
 class DeprecatedGlobal extends StubObject {
-        // The m's are to stay consistent with parent class.
+       // The m's are to stay consistent with parent class.
        protected $mRealValue, $mVersion;
 
        function __construct( $name, $realValue, $version = false ) {
index 2115eb0..fe0e95e 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;
@@ -233,12 +239,24 @@ class EditPage {
 
        public $suppressIntro = false;
 
+       /**
+        * Set to true to allow editing of non-text content types.
+        *
+        * @var bool
+        */
+       public $allowNonTextContent = false;
+
        /**
         * @param $article Article
         */
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
+
+               $this->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 +468,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 +485,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 = $this->toEditText( $content );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
 
-               $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
+               $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
 
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
@@ -711,10 +730,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 +773,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 = $this->toEditText( $content );
+
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -776,33 +805,56 @@ class EditPage {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
+        * @deprecated since 1.WD
+        * @todo: deprecated, replace usage everywhere
         */
-       function getContent( $def_text = '' ) {
-               global $wgOut, $wgRequest, $wgParser;
+       function getContent( $def_text = false ) {
+               wfDeprecated( __METHOD__, '1.WD' );
+
+               if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+                       $def_content = $this->toEditContent( $def_text );
+               } else {
+                       $def_content = false;
+               }
+
+               $content = $this->getContentObject( $def_content );
+
+               // Note: EditPage should only be used with text based content anyway.
+               return $this->toEditText( $content );
+       }
+
+       private function getContentObject( $def_content = null ) {
+               global $wgOut, $wgRequest;
 
                wfProfileIn( __METHOD__ );
 
-               $text = false;
+               $content = false;
 
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
-                               $text = $this->mTitle->getDefaultMessageText();
+                               $msg = $this->mTitle->getDefaultMessageText();
+
+                               $content = $this->toEditContent( $msg );
                        }
-                       if ( $text === false ) {
+                       if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-                               $text = $this->getPreloadedText( $preload );
+
+                               $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
-                               $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
+                               $orig = $this->getOriginalContent();
+                               $content = $orig ? $orig->getSection( $this->section ) : null;
+
+                               if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
@@ -818,15 +870,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 {
@@ -859,14 +912,14 @@ class EditPage {
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
 
-                               if ( $text === false ) {
-                                       $text = $this->getOriginalContent();
+                               if ( $content === false ) {
+                                       $content = $this->getOriginalContent();
                                }
                        }
                }
 
                wfProfileOut( __METHOD__ );
-               return $text;
+               return $content;
        }
 
        /**
@@ -885,39 +938,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;
+                       # nasty side-effect, but needed for consistency
+                       $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 = $this->toEditContent( $text );
+
+               $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;
        }
 
        /**
@@ -925,23 +1008,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 = $this->toEditText( $content );
+
+               return $text;
+       }
 
-               if ( !empty( $this->mPreloadText ) ) {
-                       return $this->mPreloadText;
+       /**
+        * 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 );
@@ -949,13 +1056,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 );
        }
 
        /**
@@ -1003,6 +1112,10 @@ class EditPage {
                        case self::AS_HOOK_ERROR:
                                return false;
 
+                       case self::AS_PARSE_ERROR:
+                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
+                               return true;
+
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
@@ -1095,9 +1208,19 @@ class EditPage {
                        return $status;
                }
 
+               try {
+                       # Construct Content object
+                       $textbox_content = $this->toEditContent( $this->textbox1 );
+               } catch (MWContentSerializationException $ex) {
+                       $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                       $status->value = self::AS_PARSE_ERROR;
+                       wfProfileOut( __METHOD__ );
+                       return $status;
+               }
+
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
-                       Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+                       $textbox_content->isRedirect() &&
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
@@ -1239,13 +1362,13 @@ class EditPage {
                                return $status;
                        }
 
-                       $text = $this->textbox1;
+                       $content = $textbox_content;
+
                        $result['sectionanchor'] = '';
                        if ( $this->section == 'new' ) {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
-                                       $text = wfMessage( 'newsectionheaderdefaultlevel', $this->sectiontitle )
-                                               ->inContentLanguage()->text() . "\n\n" . $text;
+                                       $content = $content->addSectionHeader( $this->sectiontitle );
 
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
@@ -1256,12 +1379,11 @@ class EditPage {
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
                                                $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
-                                                       ->inContentLanguage()->text();
+                                                       ->inContentLanguage()->text() ;
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
-                                       $text = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )
-                                               ->inContentLanguage()->text() . "\n\n" . $text;
+                                       $content = $content->addSectionHeader( $this->summary );
 
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
@@ -1275,10 +1397,13 @@ class EditPage {
 
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 
-               } else {
+               } else { # not $new
 
                        # Article exists. Check for edit conflict.
+
+                       $this->mArticle->clear(); # Force reload of dates, etc.
                        $timestamp = $this->mArticle->getTimestamp();
+
                        wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 
                        if ( $timestamp != $this->edittime ) {
@@ -1310,26 +1435,33 @@ class EditPage {
                                $sectionTitle = $this->summary;
                        }
 
+                       $content = null;
+
                        if ( $this->isConflict ) {
-                               wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
-                               $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
+                               wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
+                                               . " (article time '{$timestamp}')\n" );
+
+                               $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content,
+                                                                                                                                       $sectionTitle, $this->edittime );
                        } else {
-                               wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
-                               $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
+                               wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
+                               $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
                        }
-                       if ( is_null( $text ) ) {
+
+                       if ( is_null( $content ) ) {
                                wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
                                $this->isConflict = true;
-                               $text = $this->textbox1; // do not try to merge here!
+                               $content = $textbox_content; // do not try to merge here!
                        } elseif ( $this->isConflict ) {
                                # Attempt merge
-                               if ( $this->mergeChangesInto( $text ) ) {
+                               if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
                                        // Successful merge! Maybe we should tell the user the good news?
                                        $this->isConflict = false;
+                                       $content = $textbox_content;
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
                                } else {
                                        $this->section = '';
-                                       $this->textbox1 = $text;
+                                       #$this->textbox1 = $text; #redundant, nothing to do here?
                                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
                                }
                        }
@@ -1341,7 +1473,10 @@ class EditPage {
                        }
 
                        // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
+                       $hook_args = array( $this, $content, &$this->hookError, $this->summary );
+
+                       if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args )
+                               || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) {
                                # Error messages etc. could be handled within the hook...
                                $status->fatal( 'hookaborted' );
                                $status->value = self::AS_HOOK_ERROR;
@@ -1357,8 +1492,8 @@ class EditPage {
 
                        # Handle the user preference to force summaries here, but not for null edits
                        if ( $this->section != 'new' && !$this->allowBlankSummary
-                               && $this->getOriginalContent() != $text
-                               && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
+                               && !$content->equals( $this->getOriginalContent() )
+                               && !$content->isRedirect() ) # check if it's not a redirect
                        {
                                if ( md5( $this->summary ) == $this->autoSumm ) {
                                        $this->missingSummary = true;
@@ -1428,14 +1563,14 @@ class EditPage {
                        // merged the section into full text. Clear the section field
                        // so that later submission of conflict forms won't try to
                        // replace that into a duplicated mess.
-                       $this->textbox1 = $text;
+                       $this->textbox1 = $this->toEditText( $content );
                        $this->section = '';
 
                        $status->value = self::AS_SUCCESS_UPDATE;
                }
 
                // Check for length errors again now that the section is merged in
-               $this->kblength = (int)( strlen( $text ) / 1024 );
+                       $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
                if ( $this->kblength > $wgMaxArticleSize ) {
                        $this->tooBig = true;
                        $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
@@ -1448,10 +1583,11 @@ class EditPage {
                        ( ( $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;
+                               $result['redirect'] = $content->isRedirect();
                        $this->commitWatch();
                        wfProfileOut( __METHOD__ );
                        return $status;
@@ -1496,8 +1632,33 @@ class EditPage {
         * @param $editText string
         *
         * @return bool
+        * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
-       function mergeChangesInto( &$editText ) {
+       function mergeChangesInto( &$editText ){
+               wfDebug( __METHOD__, "1.WD" );
+
+               $editContent = $this->toEditContent( $editText );
+
+               $ok = $this->mergeChangesIntoContent( $editContent );
+
+               if ( $ok ) {
+                       $editText = $this->toEditText( $editContent );
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * @private
+        * @todo document
+        *
+        * @parma $editText string
+        *
+        * @return bool
+        * @since since 1.WD
+        */
+       private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
 
                $db = wfGetDB( DB_MASTER );
@@ -1508,7 +1669,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 );
@@ -1516,11 +1677,14 @@ class EditPage {
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $currentText = $currentRevision->getText();
+               $currentContent = $currentRevision->getContent();
+
+               $handler = ContentHandler::getForModelID( $baseContent->getModel() );
+
+               $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 
-               $result = '';
-               if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
-                       $editText = $result;
+               if ( $result ) {
+                       $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
@@ -1719,6 +1883,54 @@ class EditPage {
                }
        }
 
+       /**
+        * Gets an editable textual representation of the given Content object.
+        * The textual representation can be turned by into a Content object by the
+        * toEditContent() method.
+        *
+        * If the given Content object is not of a type that can be edited using the text base EditPage,
+        * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+        * content.
+        *
+        * @param Content $content
+        * @return String the editable text form of the content.
+        *
+        * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true.
+        */
+       protected function toEditText( Content $content ) {
+               if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+                       throw new MWException( "This content model can not be edited as text: "
+                                                               . ContentHandler::getLocalizedName( $content->getModel() ) );
+               }
+
+               return $content->serialize( $this->content_format );
+       }
+
+       /**
+        * Turns the given text into a Content object by unserializing it.
+        *
+        * If the resulting Content object is not of a type that can be edited using the text base EditPage,
+        * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
+        * content.
+        *
+        * @param String $text Text to unserialize
+        * @return Content the content object created from $text
+        *
+        * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent
+        *          and $this->allowNonTextContent is not true.
+        */
+       protected function toEditContent( $text ) {
+               $content = ContentHandler::makeContent( $text, $this->getTitle(),
+                       $this->content_model, $this->content_format );
+
+               if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
+                       throw new MWException( "This content model can not be edited as text: "
+                               . ContentHandler::getLocalizedName( $content->getModel() ) );
+               }
+
+               return $content;
+       }
+
        /**
         * Send the edit form and related headers to $wgOut
         * @param $formCallback Callback that takes an OutputPage parameter; will be called
@@ -1767,6 +1979,8 @@ class EditPage {
                        }
                }
 
+               //@todo: add EditForm plugin interface and use it here!
+               //       search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
@@ -1831,6 +2045,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 ) );
@@ -1848,7 +2065,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 = $this->toEditText( $content );
 
                        $this->showTextbox1();
                } else {
@@ -1874,7 +2093,13 @@ class EditPage {
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
 
                if ( $this->isConflict ) {
-                       $this->showConflict();
+                       try {
+                               $this->showConflict();
+                       } catch ( MWContentSerializationException $ex ) {
+                               // this can't really happen, but be nice if it does.
+                               $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
+                       }
                }
 
                $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
@@ -1948,7 +2173,7 @@ class EditPage {
 
                        if ( $this->section != '' && $this->section != 'new' ) {
                                if ( !$this->summary && !$this->preview && !$this->diff ) {
-                                       $sectionTitle = self::extractSectionTitle( $this->textbox1 );
+                                       $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
                                        if ( $sectionTitle !== false ) {
                                                $this->summary = "/* $sectionTitle */ ";
                                        }
@@ -2255,10 +2480,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.
@@ -2302,7 +2527,12 @@ HTML
                $wgOut->addHTML( '</div>' );
 
                if ( $this->formtype == 'diff' ) {
-                       $this->showDiff();
+                       try {
+                               $this->showDiff();
+                       } catch ( MWContentSerializationException $ex ) {
+                               $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
+                       }
                }
        }
 
@@ -2342,24 +2572,33 @@ HTML
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
+                               $oldContent = $this->toEditContent( $oldtext );
+                       } else {
+                               $oldContent = null;
                        }
                } else {
-                       $oldtext = $this->mArticle->getRawText();
+                       $oldContent = $this->getOriginalContent();
                }
-               $newtext = $this->mArticle->replaceSection(
-                       $this->section, $this->textbox1, $this->summary, $this->edittime );
 
-               wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
+               $textboxContent = $this->toEditContent( $this->textbox1 );
+
+               $newContent = $this->mArticle->replaceSectionContent(
+                                                                                       $this->section, $textboxContent,
+                                                                                       $this->summary, $this->edittime );
+
+               ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
+               wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-               $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
+               $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
 
-               if ( $oldtext !== false  || $newtext != '' ) {
+               if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMessage( $oldtitlemsg )->parse();
                        $newtitle = wfMessage( 'yourtext' )->parse();
 
-                       $de = new DifferenceEngine( $this->mArticle->getContext() );
-                       $de->setText( $oldtext, $newtext );
+                       $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $oldContent, $newContent );
+
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
@@ -2476,9 +2715,13 @@ 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 );
-                       $de->showDiff(
+                       $content1 = $this->toEditContent( $this->textbox1 );
+                       $content2 = $this->toEditContent( $this->textbox2 );
+
+                       $handler = ContentHandler::getForModelID( $this->content_model );
+                       $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $content2, $content1 );
+                       $de->showDiff( 
                                wfMessage( 'yourtext' )->parse(),
                                wfMessage( 'storedversion' )->text()
                        );
@@ -2600,82 +2843,95 @@ HTML
                        return $parsedNote;
                }
 
-               if ( $this->mTriedSave && !$this->mTokenOk ) {
-                       if ( $this->mTokenOkExceptSuffix ) {
-                               $note = wfMessage( 'token_suffix_mismatch' )->plain();
-                       } else {
-                               $note = wfMessage( 'session_fail_preview' )->plain();
-                       }
-               } elseif ( $this->incompleteForm ) {
-                       $note = wfMessage( 'edit_form_incomplete' )->plain();
-               } else {
-                       $note = wfMessage( 'previewnote' )->plain() .
-                               ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
-               }
+               $note = '';
 
-               $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+               try {
+                       $content = $this->toEditContent( $this->textbox1 );
 
-               $parserOptions->setEditSection( false );
-               $parserOptions->setIsPreview( true );
-               $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
+                       if ( $this->mTriedSave && !$this->mTokenOk ) {
+                               if ( $this->mTokenOkExceptSuffix ) {
+                                       $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
 
-               # don't parse non-wikitext pages, show message about preview
-               if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
-                       if ( $this->mTitle->isCssJsSubpage() ) {
-                               $level = 'user';
-                       } elseif ( $this->mTitle->isCssOrJsPage() ) {
-                               $level = 'site';
-                       } else {
-                               $level = false;
-                       }
-
-                       # Used messages to make sure grep find them:
-                       # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
-                       $class = 'mw-code';
-                       if ( $level ) {
-                               if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
-                                       $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
-                                       $class .= " mw-css";
-                               } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
-                                       $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
-                                       $class .= " mw-js";
                                } else {
-                                       throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
+                                       $note = wfMessage( 'session_fail_preview' )->plain() ;
                                }
-                               $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
-                               $previewHTML = $parserOutput->getText();
+                       } elseif ( $this->incompleteForm ) {
+                               $note = wfMessage( 'edit_form_incomplete' )->plain() ;
                        } else {
-                               $previewHTML = '';
-                       }
+                               $note = wfMessage( 'previewnote' )->plain() .
+                                       ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
+                       }
+
+                       $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
+                       $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->isCssOrJsPage() ) {
+                               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 = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\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'>" . wfMessage( "{$level}{$format}preview" )->text()  . "</div>";
+                               } else {
+                                       $note = wfMessage( 'previewnote' )->text() ;
+                               }
+                       } else {
+                               $note = wfMessage( 'previewnote' )->text() ;
                        }
 
-                       wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
-
-                       $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
-                       $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
-
-                       $rt = Title::newFromRedirectArray( $this->textbox1 );
+                       $rt = $content->getRedirectChain();
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
-                               $previewHTML = $parserOutput->getText();
-                       }
 
-                       $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputNoText( $parserOutput );
+                               # If we're adding a comment, we need to show the
+                               # summary as the headline
+                               if ( $this->section == "new" && $this->summary != "" ) {
+                                       $content = $content->addSectionHeader( $this->summary );
+                               }
+
+                               $hook_args = array( $this, &$content );
+                               ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
+                               wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
+
+                               $parserOptions->enableLimitReport();
+
+                               # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
+                               # But it's now deprecated, so never mind
 
-                       if ( count( $parserOutput->getWarnings() ) ) {
-                               $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+                               $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
+
+                               $previewHTML = $parserOutput->getText();
+                               $this->mParserOutput = $parserOutput;
+                               $wgOut->addParserOutputNoText( $parserOutput );
+
+                               if ( count( $parserOutput->getWarnings() ) ) {
+                                       $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                               }
                        }
+               } catch (MWContentSerializationException $ex) {
+                       $m = wfMessage('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                       $note .= "\n\n" . $m->parse();
+                       $previewHTML = '';
                }
 
                if ( $this->isConflict ) {
index f01fb23..f2b7e11 100644 (file)
@@ -63,7 +63,7 @@ class WikiExporter {
         * @return string
         */
        public static function schemaVersion() {
-               return "0.7";
+               return "0.7"; #FIXME: bump this when pushing ContentHandler additions.
        }
 
        /**
@@ -498,7 +498,7 @@ class XmlDumpWriter {
                        'xmlns'              => "http://www.mediawiki.org/xml/export-$ver/",
                        'xmlns:xsi'          => "http://www.w3.org/2001/XMLSchema-instance",
                        'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
-                                               "http://www.mediawiki.org/xml/export-$ver.xsd",
+                                               "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there?
                        'version'            => $ver,
                        'xml:lang'           => $wgLanguageCode ),
                        null ) .
@@ -657,12 +657,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";
@@ -679,6 +673,34 @@ 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 = strval( $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 );
+               }
+
+               $out .= "      " . Xml::element('model', null, strval( $content_model ) ) . "\n";
+
+               if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
+                       $content_format = strval( $row->rev_content_format );
+               } else {
+                       // probably using $wgContentHandlerUseDB = false;
+                       // @todo: test!
+                       $content_handler = ContentHandler::getForModelID( $content_model );
+                       $content_format = $content_handler->getDefaultFormat();
+               }
+
+               $out .= "      " . Xml::element('format', null, strval( $content_format ) ) . "\n";
+
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
 
                $out .= "    </revision>\n";
index 11b2675..b0a0e2b 100644 (file)
@@ -138,13 +138,23 @@ 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 );
-                               $diffText = $de->getDiff(
-                                       wfMessage( 'previousrevision' )->text(), // hack
-                                       wfMessage( 'revisionasof',
-                                       $wgLang->timeanddate( $timestamp ),
-                                       $wgLang->date( $timestamp ),
-                                       $wgLang->time( $timestamp ) )->text() );
+                               $rev = Revision::newFromId( $oldid );
+
+                               if ( !$rev ) {
+                                       $diffText = false;
+                               } else {
+                                       $context = clone RequestContext::getMain();
+                                       $context->setTitle( $title );
+
+                                       $contentHandler = $rev->getContentHandler();
+                                       $de = $contentHandler->createDifferenceEngine( $context, $oldid, $newid );
+                                       $diffText = $de->getDiff(
+                                               wfMessage( 'previousrevision' )->text(), // hack
+                                               wfMessage( 'revisionasof',
+                                                       $wgLang->timeanddate( $timestamp ),
+                                                       $wgLang->date( $timestamp ),
+                                                       $wgLang->time( $timestamp ) )->text() );
+                               }
                        }
 
                        if ( $wgFeedDiffCutoff <= 0 || ( strlen( $diffText ) > $wgFeedDiffCutoff ) ) {
@@ -162,16 +172,36 @@ class FeedUtils {
                } else {
                        $rev = Revision::newFromId( $newid );
                        if( $wgFeedDiffCutoff <= 0 || is_null( $rev ) ) {
-                               $newtext = '';
+                               $newContent = ContentHandler::getForTitle( $title )->makeEmptyContent();
+                       } else {
+                               $newContent = $rev->getContent();
+                       }
+
+                       if ( $newContent instanceof TextContent ) {
+                               // only textual content has a "source view".
+                               $text = $newContent->getNativeData();
+
+                               if ( $wgFeedDiffCutoff <= 0 || strlen( $text ) > $wgFeedDiffCutoff ) {
+                                       $html = null;
+                               } else {
+                                       $html = nl2br( htmlspecialchars( $text ) );
+                               }
                        } else {
-                               $newtext = $rev->getText();
+                               //XXX: we could get an HTML representation of the content via getParserOutput, but that may
+                               //     contain JS magic and generally may not be suitable for inclusion in a feed.
+                               //     Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
+                               //Compare also ApiFeedContributions::feedItemDesc
+                               $html = null;
                        }
-                       if ( $wgFeedDiffCutoff <= 0 || strlen( $newtext ) > $wgFeedDiffCutoff ) {
+
+                       if ( $html === null ) {
+
                                // Omit large new page diffs, bug 29110
+                               // Also use diff link for non-textual content
                                $diffText = self::getDiffLink( $title, $newid );
                        } else {
                                $diffText = '<p><b>' . wfMessage( 'newpage' )->text() . '</b></p>' .
-                                       '<div>' . nl2br( htmlspecialchars( $newtext ) ) . '</div>';
+                                       '<div>' . $html . '</div>';
                        }
                }
                $completeText .= $diffText;
index 17e3f4d..d310879 100644 (file)
@@ -391,7 +391,7 @@ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
 
        $cgi = '';
        foreach ( $array1 as $key => $value ) {
-               if ( !is_null($value) && $value !== false ) {
+               if ( $value !== false ) {
                        if ( $cgi != '' ) {
                                $cgi .= '&';
                        }
@@ -412,8 +412,11 @@ function wfArrayToCgi( $array1, $array2 = null, $prefix = '' ) {
                        } else {
                                if ( is_object( $value ) ) {
                                        $value = $value->__toString();
+                               } elseif( !is_null( $value ) ) {
+                                       $cgi .= urlencode( $key ) . '=' . urlencode( $value );
+                               } else {
+                                       $cgi .= urlencode( $key );
                                }
-                               $cgi .= urlencode( $key ) . '=' . urlencode( $value );
                        }
                }
        }
@@ -440,14 +443,15 @@ function wfCgiToArray( $query ) {
                        continue;
                }
                if ( strpos( $bit, '=' ) === false ) {
-                       // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does)
-                       $key = $bit;
-                       $value = '';
+                       // Pieces like &qwerty become 'qwerty' => null
+                       $key = urldecode( $bit );
+                       $value = null;
                } else {
                        list( $key, $value ) = explode( '=', $bit );
+                       $key = urldecode( $key );
+                       $value = urldecode( $value );
                }
-               $key = urldecode( $key );
-               $value = urldecode( $value );
+
                if ( strpos( $key, '[' ) !== false ) {
                        $keys = array_reverse( explode( '[', $key ) );
                        $key = array_pop( $keys );
@@ -472,23 +476,15 @@ function wfCgiToArray( $query ) {
  * Append a query string to an existing URL, which may or may not already
  * have query string parameters already. If so, they will be combined.
  *
+ * @deprecated in 1.20. Use Uri class.
  * @param $url String
  * @param $query Mixed: string or associative array
  * @return string
  */
 function wfAppendQuery( $url, $query ) {
-       if ( is_array( $query ) ) {
-               $query = wfArrayToCgi( $query );
-       }
-       if( $query != '' ) {
-               if( false === strpos( $url, '?' ) ) {
-                       $url .= '?';
-               } else {
-                       $url .= '&';
-               }
-               $url .= $query;
-       }
-       return $url;
+       $obj = new Uri( $url );
+       $obj->extendQuery( $query );
+       return $obj->toString();
 }
 
 /**
@@ -576,49 +572,13 @@ function wfExpandUrl( $url, $defaultProto = PROTO_CURRENT ) {
  * @todo Need to integrate this into wfExpandUrl (bug 32168)
  *
  * @since 1.19
+ * @deprecated
  * @param $urlParts Array URL parts, as output from wfParseUrl
  * @return string URL assembled from its component parts
  */
 function wfAssembleUrl( $urlParts ) {
-       $result = '';
-
-       if ( isset( $urlParts['delimiter'] ) ) {
-               if ( isset( $urlParts['scheme'] ) ) {
-                       $result .= $urlParts['scheme'];
-               }
-
-               $result .= $urlParts['delimiter'];
-       }
-
-       if ( isset( $urlParts['host'] ) ) {
-               if ( isset( $urlParts['user'] ) ) {
-                       $result .= $urlParts['user'];
-                       if ( isset( $urlParts['pass'] ) ) {
-                               $result .= ':' . $urlParts['pass'];
-                       }
-                       $result .= '@';
-               }
-
-               $result .= $urlParts['host'];
-
-               if ( isset( $urlParts['port'] ) ) {
-                       $result .= ':' . $urlParts['port'];
-               }
-       }
-
-       if ( isset( $urlParts['path'] ) ) {
-               $result .= $urlParts['path'];
-       }
-
-       if ( isset( $urlParts['query'] ) ) {
-               $result .= '?' . $urlParts['query'];
-       }
-
-       if ( isset( $urlParts['fragment'] ) ) {
-               $result .= '#' . $urlParts['fragment'];
-       }
-
-       return $result;
+       $obj = new Uri( $urlParts );
+       return $obj->toString();
 }
 
 /**
@@ -765,6 +725,7 @@ function wfUrlProtocolsWithoutProtRel() {
  * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
  * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
  *
+ * @deprecated
  * @param $url String: a URL to parse
  * @return Array: bits of the URL in an associative array, per PHP docs
  */
@@ -1064,6 +1025,7 @@ function wfLogDBError( $text ) {
                } else {
                        $d = date_create( "now", $logDBErrorTimeZoneObject );
                }
+               $date = $d->format( 'D M j G:i:s T Y' );
 
                $date = $d->format( 'D M j G:i:s T Y' );
 
@@ -2402,6 +2364,7 @@ define( 'TS_ISO_8601_BASIC', 9 );
 /**
  * Get a timestamp string in one of various formats
  *
+ * @deprecated
  * @param $outputtype Mixed: A timestamp in one of the supported formats, the
  *                    function will autodetect which format is supplied and act
  *                    accordingly.
index 6f1b1a1..316e1c9 100644 (file)
@@ -157,7 +157,9 @@ class ImagePage extends Article {
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getHtmlCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
+
                        parent::view();
+
                        $out->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
@@ -272,18 +274,18 @@ class ImagePage extends Article {
        }
 
        /**
-        * Overloading Article's getContent method.
+        * Overloading Article's getContentObject method.
         *
         * Omit noarticletext if sharedupload; text will be fetched from the
         * shared upload server if possible.
         * @return string
         */
-       public function getContent() {
+       public function getContentObject() {
                $this->loadFile();
                if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
-                       return '';
+                       return null;
                }
-               return parent::getContent();
+               return parent::getContentObject();
        }
 
        protected function openShowImage() {
index 11f3795..3035005 100644 (file)
@@ -611,7 +611,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;
 
@@ -656,6 +656,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'] ) ) {
@@ -1008,7 +1014,10 @@ class WikiRevision {
        var $timestamp = "20010115000000";
        var $user = 0;
        var $user_text = "";
+       var $model = null;
+       var $format = null;
        var $text = "";
+       var $content = null;
        var $comment = "";
        var $minor = false;
        var $type = "";
@@ -1064,6 +1073,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
         */
@@ -1187,11 +1210,54 @@ class WikiRevision {
 
        /**
         * @return string
+        *
+        * @deprecated Since 1.WD, use getContent() instead.
         */
        function getText() {
+               wfDeprecated( "Use getContent() instead." );
+
                return $this->text;
        }
 
+       /**
+        * @return Content
+        */
+       function getContent() {
+               if ( is_null( $this->content ) ) {
+                       $this->content =
+                               ContentHandler::makeContent(
+                                       $this->text,
+                                       $this->getTitle(),
+                                       $this->getModel(),
+                                       $this->getFormat()
+                               );
+               }
+
+               return $this->content;
+       }
+
+       /**
+        * @return String
+        */
+       function getModel() {
+               if ( is_null( $this->model ) ) {
+                       $this->model = $this->getTitle()->getContentModel();
+               }
+
+               return $this->model;
+       }
+
+       /**
+        * @return String
+        */
+       function getFormat() {
+               if ( is_null( $this->model ) ) {
+                       $this->format = ContentHandler::getForTitle( $this->getTitle() )->getDefaultFormat();
+               }
+
+               return $this->format;
+       }
+
        /**
         * @return string
         */
@@ -1331,7 +1397,9 @@ class WikiRevision {
                # Insert the row
                $revision = new Revision( array(
                        'page'       => $pageId,
-                       'text'       => $this->getText(),
+                       'content_model'  => $this->getModel(),
+                       'content_format' => $this->getFormat(),
+                       'text'       => $this->getContent()->serialize( $this->getFormat() ), //XXX: just set 'content' => $this->getContent()?
                        'comment'    => $this->getComment(),
                        'user'       => $userId,
                        'user_text'  => $userText,
index 214f495..b19f6c4 100644 (file)
 class LinkFilter {
 
        /**
-        * Check whether $text contains a link to $filterEntry
+        * Check whether $content contains a link to $filterEntry
         *
-        * @param $text String: text to check
+        * @param $content Content: content to check
         * @param $filterEntry String: domainparts, see makeRegex() for more details
         * @return Integer: 0 if no match or 1 if there's at least one match
         */
-       static function matchEntry( $text, $filterEntry ) {
+       static function matchEntry( Content $content, $filterEntry ) {
+               if ( !( $content instanceof TextContent ) ) {
+                       //TODO: handle other types of content too.
+                       //      Maybe create ContentHandler::matchFilter( LinkFilter ).
+                       //      Think about a common base class for LinkFilter and MagicWord.
+                       return 0;
+               }
+
+               $text = $content->getNativeData();
+
                $regex = LinkFilter::makeRegex( $filterEntry );
                return preg_match( $regex, $text );
        }
index 87db4d6..e2e860a 100644 (file)
@@ -71,6 +71,7 @@ class LinksUpdate extends SqlDataUpdate {
                }
 
                $this->mParserOutput = $parserOutput;
+
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
@@ -828,6 +829,10 @@ class LinksDeletionUpdate extends SqlDataUpdate {
                parent::__construct( false ); // no implicit transaction
 
                $this->mPage = $page;
+
+               if ( !$page->getId() ) {
+                       throw new MWException( "Page ID not known, perhaps the page doesn't exist?" );
+               }
        }
 
        /**
@@ -879,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 776a52f..1e60bae 100644 (file)
@@ -202,6 +202,11 @@ class Message {
         */
        protected $title = null;
 
+       /**
+        * Content object representing the message
+        */
+       protected $content = null;
+
        /**
         * @var string
         */
@@ -404,6 +409,18 @@ class Message {
                return $this;
        }
 
+       /**
+        * Returns the message as a Content object.
+        * @return Content
+        */
+       public function content() {
+               if ( !$this->content ) {
+                       $this->content = new MessageContent( $this->key );
+               }
+
+               return $this->content;
+       }
+
        /**
         * Returns the message parsed from wikitext to HTML.
         * @since 1.17
index 2e2b8d6..866e468 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 2c32c6a..926fbe3 100644 (file)
@@ -1994,7 +1994,11 @@ class OutputPage extends ContextSource {
                wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
 
                $this->sendCacheControl();
+
+               wfRunHooks( 'AfterFinalPageOutput', array( &$this ) );
+
                ob_end_flush();
+
                wfProfileOut( __METHOD__ );
        }
 
index 20cc8f5..31a96ad 100644 (file)
@@ -40,6 +40,10 @@ class Revision implements IDBAccessObject {
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
+       protected $mContentModel;
+       protected $mContentFormat;
+       protected $mContent;
+       protected $mContentHandler;
 
        // Revision deletion constants
        const DELETED_TEXT = 1;
@@ -138,6 +142,8 @@ class Revision implements IDBAccessObject {
         * @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,
@@ -150,7 +156,15 @@ class Revision implements IDBAccessObject {
                        '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_' );
@@ -358,7 +372,9 @@ class Revision implements IDBAccessObject {
         * @return array
         */
        public static function selectFields() {
-               return array(
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
@@ -370,8 +386,15 @@ class Revision implements IDBAccessObject {
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
-                       'rev_sha1'
+                       'rev_sha1',
                );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'rev_content_format';
+                       $fields[] = 'rev_content_model';
+               }
+
+               return $fields;
        }
 
        /**
@@ -475,6 +498,18 @@ class Revision implements IDBAccessObject {
                                $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 = strval( $row->rev_content_model );
+                       }
+
+                       if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
+                               $this->mContentFormat = null; # determine on demand if needed
+                       } else {
+                               $this->mContentFormat = strval( $row->rev_content_format );
+                       }
+
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
@@ -496,6 +531,21 @@ class Revision implements IDBAccessObject {
                        // 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'] ) ) {
+                               //@todo: when is that set? test with external store setup! check out insertOn() [dk]
+                               if ( !empty( $row['text_id'] ) ) {
+                                       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;
@@ -508,21 +558,59 @@ class Revision implements IDBAccessObject {
                        $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']  )  ? strval( $row['content_model'] )  : null;
+                       $this->mContentFormat    = isset( $row['content_format']  ) ? strval( $row['content_format'] ) : null;
+
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
 
-                       $this->mTitle     = null; # Load on demand if needed
+                       $this->mTitle     = isset( $row['title']      ) ? $row['title'] : 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 );
+                       }
+
+                       // if we have a Title object, override mPage. Useful for testing and convenience.
+                       if ( isset( $row['title'] ) ) {
+                               $this->mTitle     = $row['title'];
+                               $this->mPage      = $this->mTitle->getArticleID();
+                       } else {
+                               $this->mTitle     = null; // Load on demand if needed
+                       }
+
+                       // @todo: XXX: really? we are about to create a revision. it will usually then be the current one.
                        $this->mCurrent   = false;
-                       # If we still have no length, see it we have the text to figure it out
+
+                       // 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
+
+                       // Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
+
+                       // force lazy init
+                       $this->getContentModel();
+                       $this->getContentFormat();
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
@@ -595,7 +683,7 @@ class Revision implements IDBAccessObject {
                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' ),
@@ -607,6 +695,11 @@ class Revision implements IDBAccessObject {
                                $this->mTitle = Title::newFromRow( $row );
                        }
                }
+
+               if ( !$this->mTitle && !is_null( $this->mPage ) && $this->mPage > 0 ) {
+                       $this->mTitle = Title::newFromID( $this->mPage );
+               }
+
                return $this->mTitle;
        }
 
@@ -789,15 +882,39 @@ class Revision implements IDBAccessObject {
         *      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
+        *
+        * @deprecated in 1.WD, use getContent() instead
+        * @todo: replace usage in core
         * @return String
         */
        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
+        * @since 1.WD
+        * @return Content
+        */
+       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();
                }
        }
 
@@ -816,15 +933,110 @@ class Revision implements IDBAccessObject {
         * 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
+        *
+        * @since 1.WD
+        * @return String
+        */
+       public function getSerializedData() {
                return $this->mText;
        }
 
+       /**
+        * Gets the content object for the revision
+        *
+        * @since 1.WD
+        * @return Content
+        */
+       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->copy(); // NOTE: copy() will return $this for immutable content objects
+       }
+
+       /**
+        * 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 String 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 String 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 ) ) {
+                               throw new MWException( "Oops, the content format $format is not supported for this content model, $model" );
+                       }
+               }
+
+               return $this->mContentHandler;
+       }
+
        /**
         * @return String
         */
@@ -1007,10 +1219,12 @@ class Revision implements IDBAccessObject {
         * @return Integer
         */
        public function insertOn( $dbw ) {
-               global $wgDefaultExternalStore;
+               global $wgDefaultExternalStore, $wgContentHandlerUseDB;
 
                wfProfileIn( __METHOD__ );
 
+               $this->checkContentModel();
+
                $data = $this->mText;
                $flags = self::compressRevisionText( $data );
 
@@ -1046,27 +1260,47 @@ class Revision implements IDBAccessObject {
                $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 )
-                                       ? self::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 ) {
+                       //NOTE: Store null for the default model and format, to save space.
+                       //XXX: Makes the DB sensitive to changed defaults. Make this behaviour optional? Only in miser mode?
+
+                       $model = $this->getContentModel();
+                       $format = $this->getContentFormat();
+
+                       $title = $this->getTitle();
+
+                       if ( $title === null ) {
+                               throw new MWException( "Insufficient information to determine the title of the revision's page!" );
+                       }
+
+                       $defaultModel = ContentHandler::getDefaultModelFor( $title );
+                       $defaultFormat = ContentHandler::getForModelID( $defaultModel )->getDefaultFormat();
+
+                       $row[ 'rev_content_model' ] = ( $model === $defaultModel ) ? null : $model;
+                       $row[ 'rev_content_format' ] = ( $format === $defaultFormat ) ? null : $format;
+               }
+
+               $dbw->insert( 'revision', $row, __METHOD__ );
+
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
 
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
@@ -1075,6 +1309,52 @@ class Revision implements IDBAccessObject {
                return $this->mId;
        }
 
+       protected function checkContentModel() {
+               global $wgContentHandlerUseDB;
+
+               $title = $this->getTitle(); //note: may return 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();
+
+                       throw new MWException( "Can't use format $format with content model $model 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 ) {
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: "
+                                                                               . "model is $model , default for $t is $defaultModel" );
+                       }
+
+                       if ( $this->getContentFormat() != $defaultFormat ) {
+                               $t = $title->getPrefixedDBkey();
+
+                               throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: "
+                                                                               . "format is $format, default for $t is $defaultFormat" );
+                       }
+               }
+
+               $content = $this->getContent( Revision::RAW );
+
+               if ( !$content->isValid() ) {
+                       $t = $title->getPrefixedDBkey();
+
+                       throw new MWException( "Content of $t is not valid! Content model is $model" );
+               }
+       }
+
        /**
         * Get the base 36 SHA-1 value for a string of text
         * @param $text String
@@ -1159,12 +1439,21 @@ class Revision implements IDBAccessObject {
         * @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',
@@ -1172,7 +1461,7 @@ class Revision implements IDBAccessObject {
                        __METHOD__ );
 
                if( $current ) {
-                       $revision = new Revision( array(
+                       $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
@@ -1180,7 +1469,14 @@ class Revision implements IDBAccessObject {
                                '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;
@@ -1328,4 +1624,4 @@ class Revision implements IDBAccessObject {
                }
                return true;
        }
-}
\ No newline at end of file
+}
index 52c9be0..985bb95 100644 (file)
@@ -95,7 +95,7 @@ abstract class SqlDataUpdate extends DataUpdate {
         * Abort the database transaction started via beginTransaction (if any).
         */
        public function abortTransaction() {
-               if ( $this->mHasTransaction ) {
+               if ( $this->mHasTransaction ) { //XXX: actually... maybe always?
                        $this->mDb->rollback( get_class( $this ) . '::abortTransaction' );
                        $this->mHasTransaction = false;
                }
index 1b5e21d..b9f432f 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;
@@ -199,6 +200,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
         *
@@ -210,10 +232,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__
                );
@@ -239,10 +258,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__
                );
@@ -282,11 +298,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 = strval( $row->page_content_model );
+                       else
+                               $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+                       $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
 
@@ -312,6 +333,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;
        }
 
@@ -362,9 +384,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();
        }
 
        /**
@@ -375,10 +399,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();
        }
 
        /**
@@ -389,71 +414,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();
        }
 
        /**
@@ -701,6 +666,38 @@ class Title {
                return $this->mNamespace;
        }
 
+       /**
+        * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+        *
+        * @return String: Content model id
+        */
+       public function getContentModel() {
+               if ( !$this->mContentModel ) {
+                       $linkCache = LinkCache::singleton();
+                       $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+               }
+
+               if ( !$this->mContentModel ) {
+                       $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+               }
+
+               if( !$this->mContentModel ) {
+                       throw new MWException( "failed to determin content model!" );
+               }
+
+               return $this->mContentModel;
+       }
+
+       /**
+        * Convenience method for checking a title's content model name
+        *
+        * @param int $id
+        * @return Boolean true if $this->getContentModel() == $id
+        */
+       public function hasContentModel( $id ) {
+               return $this->getContentModel() == $id;
+       }
+
        /**
         * Get the namespace text
         *
@@ -934,6 +931,8 @@ class Title {
         * @return Bool
         */
        public function isConversionTable() {
+               //@todo: ConversionTable should become a separate content model.
+
                return $this->getNamespace() == NS_MEDIAWIKI &&
                        strpos( $this->getText(), 'Conversiontable/' ) === 0;
        }
@@ -944,22 +943,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;
        }
 
        /**
@@ -967,7 +975,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 ) ) );
        }
 
        /**
@@ -990,7 +1000,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 ) );
        }
 
        /**
@@ -999,7 +1010,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 ) );
        }
 
        /**
@@ -2826,8 +2838,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;
        }
@@ -2848,7 +2868,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;
        }
@@ -2868,7 +2895,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;
        }
@@ -2897,6 +2931,7 @@ class Title {
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
+               $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
 
@@ -3135,7 +3170,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(),
@@ -3185,6 +3220,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
@@ -3201,9 +3238,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,
@@ -3771,10 +3811,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' )
@@ -3783,6 +3829,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 ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
@@ -3827,24 +3874,25 @@ 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;
        }
 
        /**
@@ -4543,17 +4591,13 @@ class Title {
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
-               } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
-                       // css/js should always be LTR and is, in fact, English
-                       return wfGetLangObj( 'en' );
-               } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
-                       // Parse mediawiki messages with correct target language
-                       list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
-                       return wfGetLangObj( $lang );
                }
-               global $wgContLang;
-               // If nothing special, it should be in the wiki content language
-               $pageLang = $wgContLang;
+
+               //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request.
+               //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
+               $contentHandler = ContentHandler::getForTitle( $this );
+               $pageLang = $contentHandler->getPageLanguage( $this );
+
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
@@ -4568,19 +4612,23 @@ class Title {
         * @return Language
         */
        public function getPageViewLanguage() {
-               $pageLang = $this->getPageLanguage();
-               // If this is nothing special (so the content is converted when viewed)
-               if ( !$this->isSpecialPage()
-                       && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
-                       && $this->getNamespace() !== NS_MEDIAWIKI
-               ) {
+               global $wgLang;
+
+               if ( $this->isSpecialPage() ) {
                        // If the user chooses a variant, the content is actually
                        // in a language whose code is the variant code.
-                       $variant = $pageLang->getPreferredVariant();
-                       if ( $pageLang->getCode() !== $variant ) {
-                               $pageLang = Language::factory( $variant );
+                       $variant = $wgLang->getPreferredVariant();
+                       if ( $wgLang->getCode() !== $variant ) {
+                               return Language::factory( $variant );
                        }
+
+                       return $wgLang;
                }
+
+               //NOTE: can't be cached persistently, depends on user settings
+               //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
+               $contentHandler = ContentHandler::getForTitle( $this );
+               $pageLang = $contentHandler->getPageViewLanguage( $this );
                return $pageLang;
        }
 }
diff --git a/includes/Uri.php b/includes/Uri.php
new file mode 100644 (file)
index 0000000..f6cf06f
--- /dev/null
@@ -0,0 +1,335 @@
+<?php
+/**
+ * Class for simple URI parsing and manipulation.
+ * Intended to simplify things that were using wfParseUrl and
+ * had to do manual concatenation for various needs.
+ * Built to match our JS mw.Uri in naming patterns.
+ * @file
+ * @author Daniel Friesen
+ * @since 1.20
+ */
+
+class Uri {
+
+       /**
+        * The parsed components of the URI
+        */
+       protected $components;
+
+       protected static $validComponents = array( 'scheme', 'delimiter', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment' );
+       protected static $componentAliases = array( 'protocol' => 'scheme', 'password' => 'pass' );
+
+       /**
+        * parse_url() work-alike, but non-broken.  Differences:
+        *
+        * 1) Does not raise warnings on bad URLs (just returns false)
+        * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
+        * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
+        *
+        * @param $url String: a URL to parse
+        * @return Array: bits of the URL in an associative array, per PHP docs
+        */
+       protected static function parseUri( $url ) {
+               global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
+
+               // Protocol-relative URLs are handled really badly by parse_url(). It's so bad that the easiest
+               // way to handle them is to just prepend 'http:' and strip the protocol out later
+               $wasRelative = substr( $url, 0, 2 ) == '//';
+               if ( $wasRelative ) {
+                       $url = "http:$url";
+               }
+               wfSuppressWarnings();
+               $bits = parse_url( $url );
+               wfRestoreWarnings();
+               // parse_url() returns an array without scheme for some invalid URLs, e.g.
+               // parse_url("%0Ahttp://example.com") == array( 'host' => '%0Ahttp', 'path' => 'example.com' )
+               if ( !$bits ||
+                    !isset( $bits['scheme'] ) && strpos( $url, "://" ) !== false ) {
+                       wfDebug( __METHOD__ . ": Invalid URL: $url" );
+                       return false;
+               } else {
+                       $scheme = isset( $bits['scheme'] ) ? $bits['scheme'] : null;
+               }
+
+               // most of the protocols are followed by ://, but mailto: and sometimes news: not, check for it
+               if ( in_array( $scheme . '://', $wgUrlProtocols ) ) {
+                       $bits['delimiter'] = '://';
+               } elseif ( !is_null( $scheme ) && !in_array( $scheme . ':', $wgUrlProtocols ) ) {
+                       wfDebug( __METHOD__ . ": Invalid scheme in URL: $scheme" );
+                       return false;
+               } elseif( !is_null( $scheme ) ) {
+                       if( !in_array( $scheme . ':', $wgUrlProtocols ) ) {
+                               // For URLs that don't have a scheme, but do have a user:password, parse_url
+                               // detects the user as the scheme.
+                               unset( $bits['scheme'] );
+                               $bits['user'] = $scheme;
+                       } else {
+                               $bits['delimiter'] = ':';
+                               // parse_url detects for news: and mailto: the host part of an url as path
+                               // We have to correct this wrong detection
+                               if ( isset( $bits['path'] ) ) {
+                                       $bits['host'] = $bits['path'];
+                                       $bits['path'] = '';
+                               }
+                       }
+               }
+
+               /* Provide an empty host for eg. file:/// urls (see bug 28627) */
+               if ( !isset( $bits['host'] ) && $scheme == "file" ) {
+                       $bits['host'] = '';
+
+                       /* parse_url loses the third / for file:///c:/ urls (but not on variants) */
+                       if ( isset( $bits['path'] ) && substr( $bits['path'], 0, 1 ) !== '/' ) {
+                               $bits['path'] = '/' . $bits['path'];
+                       }
+               }
+
+               // If the URL was protocol-relative, fix scheme and delimiter
+               if ( $wasRelative ) {
+                       $bits['scheme'] = '';
+                       $bits['delimiter'] = '//';
+               }
+               return $bits;
+       }
+
+       /**
+        *
+        * @param $uri mixed URI string or array
+        */
+       public function __construct( $uri ) {
+               $this->components = array();
+               $this->setUri( $uri );
+       }
+
+       /**
+        * Set the Uri to the value of some other URI.
+        *
+        * @param $uri mixed URI string or array
+        */
+       public function setUri( $uri ) {
+               if ( is_string( $uri ) ) {
+                       $parsed = self::parseUri( $uri );
+                       if( $parsed === false ) {
+                               return false;
+                       }
+                       $this->setComponents( $parsed );
+               } elseif ( is_array( $uri ) ) {
+                       $this->setComponents( $uri );
+               } elseif ( $uri instanceof Uri ) {
+                       $this->setComponents( $uri->getComponents() );
+               } else {
+                       throw new MWException( __METHOD__ . ': $uri is not of a valid type.' );
+               }
+       }
+
+       /**
+        * Set the components of this array.
+        * Will output warnings when invalid components or aliases are found.
+        *
+        * @param $components Array The components to set on this Uri.
+        */
+       public function setComponents( array $components ) {
+               foreach ( $components as $name => $value ) {
+                       if ( isset( self::$componentAliases[$name] ) ) {
+                               $canonical = self::$componentAliases[$name];
+                               wfDebug( __METHOD__ . ": Converting alias $name to canonical $canonical." );
+                               $components[$canonical] = $value;
+                               unset( $components[$name] );
+                       } elseif ( !in_array( $name, self::$validComponents ) ) {
+                               throw new MWException( __METHOD__ . ": $name is not a valid component." );
+                       }
+               }
+
+               $this->components = $components;
+       }
+
+       /**
+        * Return the components for this Uri
+        * @return Array
+        */
+       public function getComponents() {
+               return $this->components;
+       }
+
+       /**
+        * Return the value of a specific component
+        *
+        * @param $name string The name of the component to return
+        * @param string|null
+        */
+       public function getComponent( $name ) {
+               if ( isset( self::$componentAliases[$name] ) ) {
+                       // Component is an alias. Get the actual name.
+                       $alias = $name;
+                       $name = self::$componentAliases[$name];
+                       wfDebug( __METHOD__ . ": Converting alias $alias to canonical $name." );
+               }
+
+               if( !in_array( $name, self::$validComponents ) ) {
+                       // Component is invalid
+                       throw new MWException( __METHOD__ . ": $name is not a valid component." );
+               } elseif( !empty( $this->components[$name] ) ) {
+                       // Component is valid and has a value.
+                       return $this->components[$name];
+               } else {
+                       // Component is empty
+                       return null;
+               }
+       }
+
+       /**
+        * Set a component for this Uri
+        * @param $name string The name of the component to set
+        * @param $value string|null The value to set
+        */
+       public function setComponent( $name, $value ) {
+               if ( isset( self::$componentAliases[$name] ) ) {
+                       $alias = $name;
+                       $name = self::$componentAliases[$name];
+                       wfDebug( __METHOD__ . ": Converting alias $alias to canonical $name." );
+               } elseif ( !in_array( $name, self::$validComponents ) ) {
+                       throw new MWException( __METHOD__ . ": $name is not a valid component." );
+               }
+               $this->components[$name] = $value;
+       }
+
+       public function getProtocol() { return $this->getComponent( 'scheme' ); }
+       public function getUser() { return $this->getComponent( 'user' ); }
+       public function getPassword() { return $this->getComponent( 'pass' ); }
+       public function getHost() { return $this->getComponent( 'host' ); }
+       public function getPort() { return $this->getComponent( 'port' ); }
+       public function getPath() { return $this->getComponent( 'path' ); }
+       public function getQueryString() { return $this->getComponent( 'query' ); }
+       public function getFragment() { return $this->getComponent( 'fragment' ); }
+
+       public function setProtocol( $scheme ) { $this->setComponent( 'scheme', $scheme ); }
+       public function setUser( $user ) { $this->setComponent( 'user', $user ); }
+       public function setPassword( $pass ) { $this->setComponent( 'pass', $pass ); }
+       public function setHost( $host ) { $this->setComponent( 'host', $host ); }
+       public function setPort( $port ) { $this->setComponent( 'port', $port ); }
+       public function setPath( $path ) { $this->setComponent( 'path', $path ); }
+       public function setFragment( $fragment ) { $this->setComponent( 'fragment', $fragment ); }
+
+       /**
+        * Gets the protocol-authority delimiter of a URI (:// or //).
+        * @return string|null
+        */
+       public function getDelimiter() {
+               $delimiter = $this->getComponent( 'delimiter' );
+               if ( $delimiter ) {
+                       // A specific delimiter is set, so return it.
+                       return $delimiter;
+               }
+               if ( $this->getAuthority() && $this->getProtocol() ) {
+                       // If the URI has a protocol and a body (i.e., some sort of host, etc.)
+                       // the default delimiter is "://", e.g., "http://test.com".
+                       return '://';
+               }
+               return null;
+       }
+
+       /**
+        * Gets query portion of a URI in array format.
+        * @return string
+        */
+       public function getQuery() {
+               return wfCgiToArray( $this->getQueryString() );
+       }
+
+       /**
+        * Gets query portion of a URI.
+        * @param string|array $query
+        */
+       public function setQuery( $query ) {
+               if ( is_array( $query ) ) {
+                       $query = wfArrayToCGI( $query );
+               }
+               $this->setComponent( 'query', $query );
+       }
+
+       /**
+        * Extend the query -- supply query parameters to override or add to ours
+        * @param Array|string $parameters query parameters to override or add
+        * @return Uri this URI object
+        */
+       public function extendQuery( $parameters ) {
+               if ( !is_array( $parameters ) ) {
+                       $parameters = wfCgiToArray( $parameters );
+               }
+
+               $query = $this->getQuery();
+               foreach( $parameters as $key => $value ) {
+                       $query[$key] = $value;
+               }
+
+               $this->setQuery( $query );
+               return $this;
+       }
+
+       /**
+        * Returns user and password portion of a URI.
+        * @return string
+        */
+       public function getUserInfo() {
+               $user = $this->getComponent( 'user' );
+               $pass = $this->getComponent( 'pass' );
+               return $pass ? "$user:$pass" : $user;
+       }
+
+       /**
+        * Gets host and port portion of a URI.
+        * @return string
+        */
+       public function getHostPort() {
+               $host = $this->getComponent( 'host' );
+               $port = $this->getComponent( 'port' );
+               return $port ? "$host:$port" : $host;
+       }
+
+       /**
+        * Returns the userInfo and host and port portion of the URI.
+        * In most real-world URLs, this is simply the hostname, but it is more general.
+        * @return string
+        */
+       public function getAuthority() {
+               $userinfo = $this->getUserInfo();
+               $hostinfo = $this->getHostPort();
+               return $userinfo ? "$userinfo@$hostinfo" : $hostinfo;
+       }
+
+       /**
+        * Returns everything after the authority section of the URI
+        * @return String
+        */
+       public function getRelativePath() {
+               $path = $this->getComponent( 'path' );
+               $query = $this->getComponent( 'query' );
+               $fragment = $this->getComponent( 'fragment' );
+
+               $retval = $path;
+               if( $query ) {
+                       $retval .= "?$query";
+               }
+               if( $fragment ) {
+                       $retval .= "#$fragment";
+               }
+               return $retval;
+       }
+
+       /**
+        * Gets the entire URI string. May not be precisely the same as input due to order of query arguments.
+        * @return String the URI string
+        */
+       public function toString() {
+               return $this->getComponent( 'scheme' ) . $this->getDelimiter() . $this->getAuthority() . $this->getRelativePath();
+       }
+
+       /**
+        * Gets the entire URI string. May not be precisely the same as input due to order of query arguments.
+        * @return String the URI string
+        */
+       public function __toString() {
+               return $this->toString();
+       }
+
+}
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 bc8766d..a032028 100644 (file)
@@ -187,7 +187,21 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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() );
        }
 
        /**
@@ -231,7 +245,9 @@ class WikiPage extends Page implements IDBAccessObject {
         * @return array
         */
        public static function selectFields() {
-               return array(
+               global $wgContentHandlerUseDB;
+
+               $fields = array(
                        'page_id',
                        'page_namespace',
                        'page_title',
@@ -244,6 +260,12 @@ class WikiPage extends Page implements IDBAccessObject {
                        'page_latest',
                        'page_len',
                );
+
+               if ( $wgContentHandlerUseDB ) {
+                       $fields[] = 'page_content_model';
+               }
+
+               return $fields;
        }
 
        /**
@@ -418,21 +440,42 @@ class WikiPage extends Page implements IDBAccessObject {
        }
 
        /**
-        * 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 String
+        *
+        * @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 {
+                               $title = $this->mTitle->getPrefixedDBkey();
+                               wfWarn( "Page $title exists but has no (visible) revisions!" );
+                       }
                }
+
+               # use the default model for this page
+               return $this->mTitle->getContentModel();
        }
 
        /**
@@ -554,6 +597,25 @@ class WikiPage extends Page implements IDBAccessObject {
                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...
         *
@@ -561,9 +623,12 @@ class WikiPage extends Page implements IDBAccessObject {
         *      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 );
@@ -575,13 +640,12 @@ class WikiPage extends Page implements IDBAccessObject {
         * 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 );
        }
 
        /**
@@ -723,32 +787,34 @@ class WikiPage extends Page implements IDBAccessObject {
                        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 );
        }
 
        /**
@@ -794,7 +860,8 @@ class WikiPage extends Page implements IDBAccessObject {
         */
        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;
                }
@@ -990,7 +1057,7 @@ class WikiPage extends Page implements IDBAccessObject {
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
-                       && $this->mTitle->isWikitextPage();
+                       && $this->getContentHandler()->isParserCacheSupported();
        }
 
        /**
@@ -1001,6 +1068,7 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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 ) {
@@ -1078,8 +1146,16 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                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;
                        }
@@ -1144,11 +1220,13 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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() );
 
@@ -1158,14 +1236,20 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                $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__ );
 
@@ -1177,7 +1261,8 @@ class WikiPage extends Page implements IDBAccessObject {
                        $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__ );
@@ -1260,6 +1345,21 @@ class WikiPage extends Page implements IDBAccessObject {
                return $ret;
        }
 
+    /**
+     * Get the content that needs to be saved in order to undo all revisions
+     * between $undo and $undoafter. Revisions must belong to the same page,
+     * must exist and must not be deleted
+     * @param $undo Revision
+     * @param $undoafter Revision Must be an earlier revision than $undo
+     * @return mixed string on success, false on failure
+     * @since 1.WD
+     * Before we had the Content object, this was done in getUndoText
+     */
+    public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
+        $handler = $undo->getContentHandler();
+        return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
+    }
+
        /**
         * Get the text that needs to be saved in order to undo all revisions
         * between $undo and $undoafter. Revisions must belong to the same page,
@@ -1267,27 +1367,29 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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;
        }
 
        /**
@@ -1295,18 +1397,67 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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 ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
+                       // Whole-page edit; let the whole text through
+                       return $text;
+               }
+
+               if ( !$this->supportsSections() ) {
+                       throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
+               }
+
+               # could even make section title, but that's not required.
+               $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
+
+               $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 ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
+                       $newContent = $sectionContent;
                } else {
+                       if ( !$this->supportsSections() ) {
+                               throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
+                       }
+
                        // 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;
@@ -1322,28 +1473,14 @@ class WikiPage extends Page implements IDBAccessObject {
                                        return null;
                                }
 
-                               $oldtext = $rev->getText();
+                               $oldContent = $rev->getContent();
                        }
 
-                       if ( $section == 'new' ) {
-                               # Inserting a new section
-                               $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
-                                       ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\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;
        }
 
        /**
@@ -1409,8 +1546,66 @@ class WikiPage extends Page implements IDBAccessObject {
         *     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 ) {
+               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, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
 
                # Low-level sanity check
@@ -1420,6 +1615,13 @@ class WikiPage extends Page implements IDBAccessObject {
 
                wfProfileIn( __METHOD__ );
 
+               if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
+                       wfProfileOut( __METHOD__ );
+                       return Status::newFatal( 'content-not-allowed-here',
+                               ContentHandler::getLocalizedName( $content->getModel() ),
+                               $this->getTitle()->getPrefixedText() );
+               }
+
                $user = is_null( $user ) ? $wgUser : $user;
                $status = Status::newGood( array() );
 
@@ -1430,10 +1632,14 @@ class WikiPage extends Page implements IDBAccessObject {
 
                $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" );
+               # handle hook
+               $hook_args = array( &$this, &$user, &$content, &$summary,
+                                                       $flags & EDIT_MINOR, null, null, &$flags, &$status );
+
+               if ( !wfRunHooks( 'ArticleContentSave', $hook_args )
+                       || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
+
+                       wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
 
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
@@ -1447,20 +1653,25 @@ class WikiPage extends Page implements IDBAccessObject {
                $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();
@@ -1477,7 +1688,7 @@ class WikiPage extends Page implements IDBAccessObject {
 
                                wfProfileOut( __METHOD__ );
                                return $status;
-                       } elseif ( $oldtext === false ) {
+                       } elseif ( !$old_content ) {
                                # Sanity check for bug 37225
                                wfProfileOut( __METHOD__ );
                                throw new MWException( "Could not find text for current revision {$oldid}." );
@@ -1487,20 +1698,35 @@ class WikiPage extends Page implements IDBAccessObject {
                                '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
-                       ) );
-                       # Bug 37225: use accessor to get the text as Revision may trim it.
-                       # After trimming, the text may be a duplicate of the current text.
-                       $text = $revision->getText(); // sanity; EditPage should trim already
+                               '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__ );
+
+                               $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
+                               $status->merge( $prepStatus );
+
+                               if ( !$status->isOK() ) {
+                                       $dbw->rollback();
+
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               }
+
                                $revisionId = $revision->insertOn( $dbw );
 
                                # Update page
@@ -1548,8 +1774,14 @@ class WikiPage extends Page implements IDBAccessObject {
                        }
 
                        # 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' );
@@ -1564,6 +1796,18 @@ class WikiPage extends Page implements IDBAccessObject {
 
                        $dbw->begin( __METHOD__ );
 
+                       $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
+                       $status->merge( $prepStatus );
+
+                       if ( !$status->isOK() ) {
+                               $dbw->rollback();
+
+                               wfProfileOut( __METHOD__ );
+                               return $status;
+                       }
+
+                       $status->merge( $prepStatus );
+
                        # Add the page record; stake our claim on this title!
                        # This will return false if the article already exists
                        $newid = $this->insertOn( $dbw );
@@ -1581,15 +1825,18 @@ class WikiPage extends Page implements IDBAccessObject {
                                '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 );
 
                        # Bug 37225: use accessor to get the text as Revision may trim it
-                       $text = $revision->getText(); // sanity; EditPage should trim already
+                       $content = $revision->getContent(); // sanity; get normalized version
 
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
@@ -1603,7 +1850,7 @@ class WikiPage extends Page implements IDBAccessObject {
                                        $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 ) {
@@ -1616,8 +1863,11 @@ class WikiPage extends Page implements IDBAccessObject {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
 
-                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
-                               $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+                       $hook_args = array( &$this, &$user, $content, $summary,
+                                                               $flags & EDIT_MINOR, null, null, &$flags, $revision );
+
+                       ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
+                       wfRunHooks( 'ArticleContentInsertComplete', $hook_args );
                }
 
                # Do updates right now unless deferral was requested
@@ -1628,8 +1878,11 @@ class WikiPage extends Page implements IDBAccessObject {
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
 
-               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+               $hook_args = array( &$this, &$user, $content, $summary,
+                                                       $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
+
+               ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
+               wfRunHooks( 'ArticleContentSaveComplete', $hook_args );
 
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
@@ -1662,6 +1915,7 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                if ( $this->getTitle()->isConversionTable() ) {
+                       //@todo: ConversionTable should become a separate content model.
                        $options->disableContentConversion();
                }
 
@@ -1674,15 +1928,39 @@ class WikiPage extends Page implements IDBAccessObject {
        /**
         * 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???
+               //XXX: 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;
@@ -1693,11 +1971,21 @@ class WikiPage extends Page implements IDBAccessObject {
 
                $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;
 
@@ -1710,7 +1998,6 @@ class WikiPage extends Page implements IDBAccessObject {
         * 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:
@@ -1727,13 +2014,13 @@ class WikiPage extends Page implements IDBAccessObject {
                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;
@@ -1746,7 +2033,7 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                # Update the links tables and other secondary data
-               $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
+               $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
 
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
@@ -1791,7 +2078,8 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                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
@@ -1817,7 +2105,11 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
-                       MessageCache::singleton()->replace( $shortTitle, $text );
+                       #XXX: could skip pseudo-messages like js/css here, based on content model.
+                       $msgtext = $content->getWikitextForTransclusion();
+                       if ( $msgtext === false || $msgtext === null ) $msgtext = '';
+
+                       MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
 
                if( $options['created'] ) {
@@ -1838,17 +2130,40 @@ class WikiPage extends Page implements IDBAccessObject {
         * @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 );
 
@@ -2142,7 +2457,7 @@ class WikiPage extends Page implements IDBAccessObject {
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
-               global $wgUser;
+               global $wgUser, $wgContentHandlerUseDB;
 
                wfDebug( __METHOD__ . "\n" );
 
@@ -2183,6 +2498,9 @@ class WikiPage extends Page implements IDBAccessObject {
                        $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.
@@ -2195,25 +2513,34 @@ class WikiPage extends Page implements IDBAccessObject {
                //
                // 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__
@@ -2229,7 +2556,7 @@ class WikiPage extends Page implements IDBAccessObject {
                        return $status;
                }
 
-               $this->doDeleteUpdates( $id );
+               $this->doDeleteUpdates( $id, $content );
 
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
@@ -2254,13 +2581,15 @@ class WikiPage extends Page implements IDBAccessObject {
         * 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
@@ -2273,16 +2602,6 @@ class WikiPage extends Page implements IDBAccessObject {
                $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
@@ -2455,7 +2774,12 @@ class WikiPage extends Page implements IDBAccessObject {
                }
 
                # 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 ( !$status->isOK() ) {
+                       return $status->getErrorsArray();
+               }
+
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
@@ -2601,60 +2925,23 @@ class WikiPage extends Page implements IDBAccessObject {
 
        /**
        * 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.
+               # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
 
-               # 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, 255
-                                       - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
-                                       - strlen( $rt->getFullText() )
-                               ) );
-                       return wfMessage( 'autoredircomment', $rt->getFullText() )
-                               ->rawParams( $truncatedtext )->inContentLanguage()->text();
-               }
-
-               # New page autosummaries
-               if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
-                       # If they're making a new article, give its text, truncated, in the summary.
-
-                       $truncatedtext = $wgContLang->truncate(
-                               str_replace( "\n", ' ', $newtext ),
-                               max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) );
-
-                       return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
-                               ->inContentLanguage()->text();
-               }
+               wfDeprecated( __METHOD__, '1.WD' );
 
-               # Blanking autosummaries
-               if ( $oldtext != '' && $newtext == '' ) {
-                       return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
-               } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
-                       # Removing more than 90% of the article
+               $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
+               $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
+               $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
 
-                       $truncatedtext = $wgContLang->truncate(
-                               $newtext,
-                               max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
-
-                       return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
-                               ->inContentLanguage()->text();
-               }
-
-               # 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 );
        }
 
        /**
@@ -2665,95 +2952,7 @@ class WikiPage extends Page implements IDBAccessObject {
         *    if no revision occurred
         */
        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 );
-
-               // 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 = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
-               } else {
-                       if ( $onlyAuthor ) {
-                               $reason = wfMessage(
-                                       'excontentauthor',
-                                       '$1',
-                                       $onlyAuthor
-                               )->inContentLanguage()->text();
-                       } else {
-                               $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
-                       }
-               }
-
-               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 );
-
-               return $reason;
+               return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
 
        /**
@@ -2995,6 +3194,31 @@ class WikiPage extends Page implements IDBAccessObject {
                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 information
+        * 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 );
+               }
+
+               if ( !$content ) {
+                       $updates = array();
+               } else {
+                       $updates = $content->getDeletionUpdates( $this );
+               }
+
+               wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
+               return $updates;
+       }
+
 }
 
 class PoolWorkArticleView extends PoolCounterWork {
@@ -3020,9 +3244,9 @@ class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
 
        /**
-        * @var string|null
+        * @var Content|null
         */
-       private $text;
+       private $content = null;
 
        /**
         * @var ParserOutput|bool
@@ -3046,14 +3270,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 );
        }
@@ -3089,25 +3319,30 @@ 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();
+                       #XXX: why use RAW audience here, and PUBLIC (default) below?
+                       $content = $this->page->getContent( Revision::RAW );
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
-                       $text = $rev->getText();
+
+                       #XXX: why use PUBLIC audience here (default), and RAW above?
+                       $content = $rev->getContent();
                }
 
                $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
@@ -3176,3 +3411,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..394625f 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 requested page uses the content model `"
+                                                                                                               . $content->getModel() . "` which is not supported via this interface." );
+                                       die();
+                               }
+
                                $section = $request->getIntOrNull( 'section' );
                                if ( $section !== null ) {
-                                       $text = $wgParser->getSection( $text, $section );
+                                       $content = $content->getSection( $section );
                                }
+
+                               $text = $content->getNativeData();
                        }
                }
 
index 0d9a902..8d9127e 100644 (file)
@@ -71,25 +71,18 @@ class RollbackAction extends FormlessAction {
                        return;
                }
 
-               # Display permissions errors before read-only message -- there's no
-               # point in misleading the user into thinking the inability to rollback
-               # is only temporary.
-               if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
-                       # array_diff is completely broken for arrays of arrays, sigh.
-                       # Remove any 'readonlytext' error manually.
-                       $out = array();
-                       foreach ( $result as $error ) {
-                               if ( $error != array( 'readonlytext' ) ) {
-                                       $out [] = $error;
-                               }
-                       }
-                       throw new PermissionsError( 'rollback', $out );
-               }
+               #NOTE: Permission errors already handled by Action::checkExecute.
 
                if ( $result == array( array( 'readonlytext' ) ) ) {
                        throw new ReadOnlyError;
                }
 
+               #XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object.
+               #     Right now, we only show the first error
+               foreach ( $result as $error ) {
+                       throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) );
+               }
+
                $current = $details['current'];
                $target = $details['target'];
                $newId = $details['newid'];
@@ -109,7 +102,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 = $current->getContentHandler();
+                       $de = $contentHandler->createDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
                        $de->showDiff( '', '' );
                }
        }
index ed72b29..6741259 100644 (file)
@@ -35,7 +35,15 @@ 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(),
+               $revision = Revision::newFromId( $rev1 );
+
+               if ( !$revision ) {
+                       $this->dieUsage( 'The diff cannot be retrieved, ' .
+                               'one revision does not exist or you do not have permission to view it.', 'baddiff' );
+               }
+
+               $contentHandler = $revision->getContentHandler();
+               $de = $contentHandler->createDifferenceEngine( $this->getContext(),
                        $rev1,
                        $rev2,
                        null, // rcid
index 2d36f19..7e1d1b1 100644 (file)
@@ -116,7 +116,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 ); 
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
index 0963fe7..506a546 100644 (file)
@@ -54,17 +54,37 @@ class ApiEditPage extends ApiBase {
                        $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
 
+               if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
+                       $contentHandler = $pageObj->getContentHandler();
+               } else {
+                       $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
+               }
+
+               // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such
+
+               if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
+                       $params['contentformat'] = $contentHandler->getDefaultFormat();
+               }
+
+               $contentFormat = $params['contentformat'];
+
+               if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
+                       $name = $titleObj->getPrefixedDBkey();
+                       $model = $contentHandler->getModelID();
+
+                       $this->dieUsage( "The requested format $contentFormat is not supported for content model ".
+                                                       " $model used by $name", 'badformat' );
+               }
+
                $apiResult = $this->getResult();
 
                if ( $params['redirect'] ) {
                        if ( $titleObj->isRedirect() ) {
                                $oldTitle = $titleObj;
 
-                               $titles = Title::newFromRedirectArray(
-                                       Revision::newFromTitle(
-                                               $oldTitle, false, Revision::READ_LATEST
-                                       )->getText( Revision::FOR_THIS_USER )
-                               );
+                               $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
+                                                       ->getContent( Revision::FOR_THIS_USER )
+                                                       ->getRedirectChain();
                                // array_shift( $titles );
 
                                $redirValues = array();
@@ -103,31 +123,58 @@ class ApiEditPage extends ApiBase {
                        $this->dieUsageMsg( $errors[0] );
                }
 
-               $articleObj = Article::newFromTitle( $titleObj, $this->getContext() );
-
                $toMD5 = $params['text'];
                if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) )
                {
-                       // For non-existent pages, Article::getContent()
-                       // returns an interface message rather than ''
-                       // We do want getContent()'s behavior for non-existent
-                       // MediaWiki: pages, though
-                       if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
-                               $content = '';
-                       } else {
-                               $content = $articleObj->getContent();
+                       $content = $pageObj->getContent();
+
+                       // @todo: Add support for appending/prepending to the Content interface
+
+                       if ( !( $content instanceof TextContent ) ) {
+                               $mode = $contentHandler->getModelID();
+                               $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
+                       }
+
+                       if ( !$content ) {
+                               # If this is a MediaWiki:x message, then load the messages
+                               # and return the message value for x.
+                               if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
+                                       $text = $titleObj->getDefaultMessageText();
+                                       if ( $text === false ) {
+                                               $text = '';
+                                       }
+
+                                       try {
+                                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
+                                       } catch ( MWContentSerializationException $ex ) {
+                                               $this->dieUsage( $ex->getMessage(), 'parseerror' );
+                                               return;
+                                       }
+                               }
                        }
 
                        if ( !is_null( $params['section'] ) ) {
+                               if ( !$contentHandler->supportsSections() ) {
+                                       $modelName = $contentHandler->getModelID();
+                                       $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' );
+                               }
+
                                // Process the content for section edits
-                               global $wgParser;
                                $section = intval( $params['section'] );
-                               $content = $wgParser->getSection( $content, $section, false );
-                               if ( $content === false ) {
+                               $content = $content->getSection( $section );
+
+                               if ( !$content ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
-                       $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+
+                       if ( !$content ) {
+                               $text = '';
+                       } else {
+                               $text = $content->serialize( $contentFormat );
+                       }
+
+                       $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
 
@@ -151,18 +198,21 @@ class ApiEditPage extends ApiBase {
                                $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) );
                        }
 
-                       if ( $undoRev->getPage() != $articleObj->getID() ) {
+                       if ( $undoRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) );
                        }
-                       if ( $undoafterRev->getPage() != $articleObj->getID() ) {
+                       if ( $undoafterRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
                        }
 
-                       $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev );
-                       if ( $newtext === false ) {
+                       $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev );
+
+                       if ( !$newContent ) {
                                $this->dieUsageMsg( 'undo-failure' );
                        }
-                       $params['text'] = $newtext;
+
+                       $params['text'] = $newContent->serialize( $params['contentformat'] );
+
                        // If no summary was given and we only undid one rev,
                        // use an autosummary
                        if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) {
@@ -179,6 +229,8 @@ class ApiEditPage extends ApiBase {
                // That interface kind of sucks, but it's workable
                $requestArray = array(
                        'wpTextbox1' => $params['text'],
+                       'format' => $contentFormat,
+                       'model' => $contentHandler->getModelID(),
                        'wpEditToken' => $params['token'],
                        'wpIgnoreBlankSummary' => ''
                );
@@ -196,7 +248,7 @@ class ApiEditPage extends ApiBase {
                if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) {
                        $requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] );
                } else {
-                       $requestArray['wpEdittime'] = $articleObj->getTimestamp();
+                       $requestArray['wpEdittime'] = $pageObj->getTimestamp();
                }
 
                if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) {
@@ -244,7 +296,12 @@ class ApiEditPage extends ApiBase {
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
 
-               $ep = new EditPage( $articleObj );
+               $articleObject = new Article( $titleObj );
+               $ep = new EditPage( $articleObject );
+
+               // allow editing of non-textual content.
+               $ep->allowNonTextContent = true;
+
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
 
@@ -262,7 +319,7 @@ class ApiEditPage extends ApiBase {
                }
 
                // Do the actual save
-               $oldRevId = $articleObj->getRevIdFetched();
+               $oldRevId = $articleObject->getRevIdFetched();
                $result = null;
                // Fake $wgRequest for some hooks inside EditPage
                // @todo FIXME: This interface SUCKS
@@ -278,6 +335,9 @@ class ApiEditPage extends ApiBase {
                        case EditPage::AS_HOOK_ERROR_EXPECTED:
                                $this->dieUsageMsg( 'hookaborted' );
 
+                       case EditPage::AS_PARSE_ERROR:
+                               $this->dieUsage( $status->getMessage(), 'parseerror' );
+
                        case EditPage::AS_IMAGE_REDIRECT_ANON:
                                $this->dieUsageMsg( 'noimageredirect-anon' );
 
@@ -329,14 +389,15 @@ class ApiEditPage extends ApiBase {
                                $r['result'] = 'Success';
                                $r['pageid'] = intval( $titleObj->getArticleID() );
                                $r['title'] = $titleObj->getPrefixedText();
-                               $newRevId = $articleObj->getLatest();
+                               $r['contentmodel'] = $titleObj->getContentModel();
+                               $newRevId = $articleObject->getLatest();
                                if ( $newRevId == $oldRevId ) {
                                        $r['nochange'] = '';
                                } else {
                                        $r['oldrevid'] = intval( $oldRevId );
                                        $r['newrevid'] = intval( $newRevId );
                                        $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
-                                               $articleObj->getTimestamp() );
+                                               $pageObj->getTimestamp() );
                                }
                                break;
 
@@ -380,6 +441,7 @@ class ApiEditPage extends ApiBase {
                                array( 'undo-failure' ),
                                array( 'hashcheckfailed' ),
                                array( 'hookaborted' ),
+                               array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
                                array( 'noimageredirect-anon' ),
                                array( 'noimageredirect-logged' ),
                                array( 'spamdetected', 'spam' ),
@@ -397,6 +459,13 @@ class ApiEditPage extends ApiBase {
                                array( 'unknownerror', 'retval' ),
                                array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
                                array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
+                               array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ),
+                               array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using '
+                                                                                                                               . 'the text based edit API.' ),
+                               array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending '
+                                                                                                                               . 'or prepending text.' ),
+                               array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to '
+                                                                                                               . 'the page\'s content model' ),
                                array( 'customcssprotected' ),
                                array( 'customjsprotected' ),
                        )
@@ -460,6 +529,12 @@ class ApiEditPage extends ApiBase {
                                ApiBase::PARAM_TYPE => 'boolean',
                                ApiBase::PARAM_DFLT => false,
                        ),
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ),
+                       'contentmodel' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                       )
                );
        }
 
@@ -498,6 +573,8 @@ class ApiEditPage extends ApiBase {
                        'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext",
                        'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
                        'redirect' => 'Automatically resolve redirects',
+                       'contentformat' => 'Content serialization format used for the input text',
+                       'contentmodel' => 'Content model of the new content',
                );
        }
 
index 1cf760a..fb6a06f 100644 (file)
@@ -130,10 +130,22 @@ class ApiFeedContributions extends ApiBase {
        protected function feedItemDesc( $revision ) {
                if( $revision ) {
                        $msg = wfMessage( 'colon-separator' )->inContentLanguage()->text();
+                       $content = $revision->getContent();
+
+                       if ( $content instanceof TextContent ) {
+                               // only textual content has a "source view".
+                               $html = nl2br( htmlspecialchars( $content->getNativeData() ) );
+                       } else {
+                               //XXX: we could get an HTML representation of the content via getParserOutput, but that may
+                               //     contain JS magic and generally may not be suitable for inclusion in a feed.
+                               //     Perhaps Content should have a getDescriptiveHtml method and/or a getSourceText method.
+                               //Compare also FeedUtils::formatDiffRow.
+                               $html = '';
+                       }
+
                        return '<p>' . htmlspecialchars( $revision->getUserText() ) . $msg .
                                htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
-                               "</p>\n<hr />\n<div>" .
-                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+                               "</p>\n<hr />\n<div>" . $html . "</div>";
                }
                return '';
        }
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 35febd9..70ef5e6 100644 (file)
@@ -105,6 +105,7 @@ class ApiMain extends ApiBase {
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
+               'none' => 'ApiFormatNone',
        );
 
        /**
index 2fcdc38..7c61291 100644 (file)
  * @ingroup API
  */
 class ApiParse extends ApiBase {
-       private $section, $text, $pstText = null;
+
+       /** @var String $section */
+       private $section = null;
+
+       /** @var Content $content */
+       private $content = null;
+
+       /** @var Content $pstContent */
+       private $pstContent = null;
 
        public function __construct( $main, $action ) {
                parent::__construct( $main, $action );
@@ -44,6 +52,9 @@ class ApiParse extends ApiBase {
                $pageid = $params['pageid'];
                $oldid = $params['oldid'];
 
+               $model = $params['contentmodel'];
+               $format = $params['contentformat'];
+
                if ( !is_null( $page ) && ( !is_null( $text ) || $title != 'API' ) ) {
                        $this->dieUsage( 'The page parameter cannot be used together with the text and title parameters', 'params' );
                }
@@ -93,17 +104,18 @@ class ApiParse extends ApiBase {
                                // If for some reason the "oldid" is actually the current revision, it may be cached
                                if ( $rev->isCurrent() )  {
                                        // May get from/save to parser cache
-                                       $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
-                                                isset( $prop['wikitext'] ) ) ;
+                                       $pageObj = WikiPage::factory( $titleObj );
+                                       $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, 
+                                               isset( $prop['wikitext'] ) ) ;
                                } else { // This is an old revision, so get the text differently
-                                       $this->text = $rev->getText( Revision::FOR_THIS_USER, $this->getUser() );
+                                       $this->content = $rev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 
                                        if ( $this->section !== false ) {
-                                               $this->text = $this->getSectionText( $this->text, 'r' . $rev->getId() );
+                                               $this->content = $this->getSectionContent( $this->content, 'r' . $rev->getId() );
                                        }
 
                                        // Should we save old revision parses to the parser cache?
-                                       $p_result = $wgParser->parse( $this->text, $titleObj, $popts );
+                                       $p_result = $this->content->getParserOutput( $titleObj, $popts );
                                }
                        } else { // Not $oldid, but $pageid or $page
                                if ( $params['redirects'] ) {
@@ -142,19 +154,15 @@ class ApiParse extends ApiBase {
                                        $oldid = $pageObj->getLatest();
                                }
 
+
                                $popts = $pageObj->makeParserOptions( $this->getContext() );
                                $popts->enableLimitReport( !$params['disablepp'] );
 
                                // Potentially cached
-                               $p_result = $this->getParsedSectionOrText( $pageObj, $popts, $pageid,
-                                        isset( $prop['wikitext'] ) ) ;
+                               $p_result = $this->getParsedContent( $pageObj, $popts, $pageid, 
+                                       isset( $prop['wikitext'] ) ) ;
                        }
                } else { // Not $oldid, $pageid, $page. Hence based on $text
-
-                       if ( is_null( $text ) ) {
-                               $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
-                       }
-                       $this->text = $text;
                        $titleObj = Title::newFromText( $title );
                        if ( !$titleObj ) {
                                $this->dieUsageMsg( array( 'invalidtitle', $title ) );
@@ -165,27 +173,42 @@ class ApiParse extends ApiBase {
                        $popts = $pageObj->makeParserOptions( $this->getContext() );
                        $popts->enableLimitReport( !$params['disablepp'] );
 
+                       if ( is_null( $text ) ) {
+                               $this->dieUsage( 'The text parameter should be passed with the title parameter. Should you be using the "page" parameter instead?', 'params' );
+                       }
+
+                       try {
+                               $this->content = ContentHandler::makeContent( $text, $titleObj, $model, $format );
+                       } catch ( MWContentSerializationException $ex ) {
+                               $this->dieUsage( $ex->getMessage(), 'parseerror' );
+                       }
+
                        if ( $this->section !== false ) {
-                               $this->text = $this->getSectionText( $this->text, $titleObj->getText() );
+                               $this->content = $this->getSectionContent( $this->content, $titleObj->getText() );
                        }
 
                        if ( $params['pst'] || $params['onlypst'] ) {
-                               $this->pstText = $wgParser->preSaveTransform( $this->text, $titleObj, $this->getUser(), $popts );
+                               $this->pstContent = $this->content->preSaveTransform( $titleObj, $this->getUser(), $popts );
                        }
                        if ( $params['onlypst'] ) {
                                // Build a result and bail out
                                $result_array = array();
                                $result_array['text'] = array();
-                               $result->setContent( $result_array['text'], $this->pstText );
+                               $result->setContent( $result_array['text'], $this->pstContent->serialize( $format ) );
                                if ( isset( $prop['wikitext'] ) ) {
                                        $result_array['wikitext'] = array();
-                                       $result->setContent( $result_array['wikitext'], $this->text );
+                                       $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
                                }
                                $result->addValue( null, $this->getModuleName(), $result_array );
                                return;
                        }
+
                        // Not cached (save or load)
-                       $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
+                       if ( $params['pst'] ) {
+                               $p_result = $this->pstContent->getParserOutput( $titleObj, $popts );
+                       } else {
+                               $p_result = $this->content->getParserOutput( $titleObj, $popts );
+                       }
                }
 
                $result_array = array();
@@ -275,10 +298,10 @@ class ApiParse extends ApiBase {
 
                if ( isset( $prop['wikitext'] ) ) {
                        $result_array['wikitext'] = array();
-                       $result->setContent( $result_array['wikitext'], $this->text );
-                       if ( !is_null( $this->pstText ) ) {
+                       $result->setContent( $result_array['wikitext'], $this->content->serialize( $format ) );
+                       if ( !is_null( $this->pstContent ) ) {
                                $result_array['psttext'] = array();
-                               $result->setContent( $result_array['psttext'], $this->pstText );
+                               $result->setContent( $result_array['psttext'], $this->pstContent->serialize( $format ) );
                        }
                }
                if ( isset( $prop['properties'] ) ) {
@@ -286,8 +309,12 @@ class ApiParse extends ApiBase {
                }
 
                if ( $params['generatexml'] ) {
+                       if ( $this->content->getModel() != CONTENT_MODEL_WIKITEXT ) {
+                               $this->dieUsage( "generatexml is only supported for wikitext content", "notwikitext" );
+                       }
+
                        $wgParser->startExternalParse( $titleObj, $popts, OT_PREPROCESS );
-                       $dom = $wgParser->preprocessToDom( $this->text );
+                       $dom = $wgParser->preprocessToDom( $this->content->getNativeData() );
                        if ( is_callable( array( $dom, 'saveXML' ) ) ) {
                                $xml = $dom->saveXML();
                        } else {
@@ -325,15 +352,15 @@ class ApiParse extends ApiBase {
         * @param $getWikitext Bool
         * @return ParserOutput
         */
-       private function getParsedSectionOrText( $page, $popts, $pageId = null, $getWikitext = false ) {
-               global $wgParser;
+       private function getParsedContent( WikiPage $page, $popts, $pageId = null, $getWikitext = false ) {
+               $this->content = $page->getContent( Revision::RAW ); //XXX: really raw?
 
                if ( $this->section !== false ) {
-                       $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
-                                       ? 'page id ' . $pageId : $page->getTitle()->getPrefixedText() );
+                       $this->content = $this->getSectionContent( $this->content, !is_null( $pageId )
+                                                                                                               ? 'page id ' . $pageId : $page->getTitle()->getText() );
 
                        // Not cached (save or load)
-                       return $wgParser->parse( $this->text, $page->getTitle(), $popts );
+                       return $this->content->getParserOutput( $page->getTitle(), $popts );
                } else {
                        // Try the parser cache first
                        // getParserOutput will save to Parser cache if able
@@ -342,20 +369,23 @@ 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 );
                        }
                        return $pout;
                }
        }
 
-       private function getSectionText( $text, $what ) {
-               global $wgParser;
+       private function getSectionContent( Content $content, $what ) {
                // Not cached (save or load)
-               $text = $wgParser->getSection( $text, $this->section, false );
-               if ( $text === false ) {
+               $section = $content->getSection( $this->section );
+               if ( $section === false ) {
                        $this->dieUsage( "There is no section {$this->section} in " . $what, 'nosuchsection' );
                }
-               return $text;
+               if ( $section === null ) {
+                       $this->dieUsage( "Sections are not supported by " . $what, 'nosuchsection' );
+                       $section = false;
+               }
+               return $section;
        }
 
        private function formatLangLinks( $links ) {
@@ -548,6 +578,12 @@ class ApiParse extends ApiBase {
                        'section' => null,
                        'disablepp' => false,
                        'generatexml' => false,
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                       ),
+                       'contentmodel' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
+                       )
                );
        }
 
@@ -593,6 +629,8 @@ class ApiParse extends ApiBase {
                        'section' => 'Only retrieve the content of this section number',
                        'disablepp' => 'Disable the PP Report from the parser output',
                        'generatexml' => 'Generate XML parse tree',
+                       'contentformat' => 'Content serialization format used for the input text',
+                       'contentmodel' => 'Content model of the new content',
                );
        }
 
@@ -613,6 +651,8 @@ class ApiParse extends ApiBase {
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section sectionnumber in page' ),
                        array( 'nosuchpageid' ),
                        array( 'invalidtitle', 'title' ),
+                       array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
+                       array( 'code' => 'notwikitext', 'info' => 'The requested operation is only supported on wikitext content.' ),
                ) );
        }
 
index 9fedaf1..3cf32ad 100644 (file)
@@ -86,14 +86,17 @@ class ApiPurge extends ApiBase {
 
                        if( $forceLinkUpdate ) {
                                if ( !$user->pingLimiter() ) {
-                                       global $wgParser, $wgEnableParserCache;
+                                       global $wgEnableParserCache;
 
                                        $popts = $page->makeParserOptions( 'canonical' );
-                                       $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
-                                               true, true, $page->getLatest() );
+                                       $popts->setTidy( true );
+
+                                       # Parse content; note that HTML generation is only needed if we want to cache the result.
+                                       $content = $page->getContent( Revision::RAW );
+                                       $p_result = $content->getParserOutput( $title, $page->getLatest(), $popts, $wgEnableParserCache );
 
                                        # Update the links tables
-                                       $updates = $p_result->getSecondaryDataUpdates( $title );
+                                       $updates = $content->getSecondaryDataUpdates( $title, null, true, $p_result );
                                        DataUpdate::runUpdates( $updates );
 
                                        $r['linkupdate'] = '';
index b89a8ea..d6b556d 100644 (file)
 class ApiQueryRevisions extends ApiQueryBase {
 
        private $diffto, $difftotext, $expandTemplates, $generateXML, $section,
-               $token, $parseContent;
+               $token, $parseContent, $contentFormat;
 
        public function __construct( $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'rv' );
        }
 
-       private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
+       private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false,
                        $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
-                       $fld_content = false, $fld_tags = false;
+                       $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
 
        private $tokenFunctions;
 
@@ -155,10 +155,15 @@ class ApiQueryRevisions extends ApiQueryBase {
                $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
                $this->fld_size = isset ( $prop['size'] );
                $this->fld_sha1 = isset ( $prop['sha1'] );
+               $this->fld_contentmodel = isset ( $prop['contentmodel'] );
                $this->fld_userid = isset( $prop['userid'] );
                $this->fld_user = isset ( $prop['user'] );
                $this->token = $params['token'];
 
+               if ( !empty( $params['contentformat'] ) ) {
+                       $this->contentFormat = $params['contentformat'];
+               }
+
                // Possible indexes used
                $index = array();
 
@@ -441,6 +446,10 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                }
 
+               if ( $this->fld_contentmodel ) {
+                       $vals['contentmodel'] = $revision->getContentModel();
+               }
+
                if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
@@ -479,39 +488,79 @@ class ApiQueryRevisions extends ApiQueryBase {
                        }
                }
 
-               $text = null;
+               $content = null;
                global $wgParser;
                if ( $this->fld_content || !is_null( $this->difftotext ) ) {
-                       $text = $revision->getText();
+                       $content = $revision->getContent();
                        // Expand templates after getting section content because
                        // template-added sections don't count and Parser::preprocess()
                        // will have less input
                        if ( $this->section !== false ) {
-                               $text = $wgParser->getSection( $text, $this->section, false );
-                               if ( $text === false ) {
+                               $content = $content->getSection( $this->section, false );
+                               if ( !$content ) {
                                        $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' );
                                }
                        }
                }
                if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
+                       $text = null;
+
                        if ( $this->generateXML ) {
-                               $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
-                               $dom = $wgParser->preprocessToDom( $text );
-                               if ( is_callable( array( $dom, 'saveXML' ) ) ) {
-                                       $xml = $dom->saveXML();
+                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                                       $t = $content->getNativeData(); # note: don't set $text
+
+                                       $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
+                                       $dom = $wgParser->preprocessToDom( $t );
+                                       if ( is_callable( array( $dom, 'saveXML' ) ) ) {
+                                               $xml = $dom->saveXML();
+                                       } else {
+                                               $xml = $dom->__toString();
+                                       }
+                                       $vals['parsetree'] = $xml;
                                } else {
-                                       $xml = $dom->__toString();
+                                       $this->setWarning( "Conversion to XML is supported for wikitext only, " .
+                                                                               $title->getPrefixedDBkey() .
+                                                                               " uses content model " . $content->getModel() . ")" );
                                }
-                               $vals['parsetree'] = $xml;
-
                        }
+
                        if ( $this->expandTemplates && !$this->parseContent ) {
-                               $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
+                               #XXX: implement template expansion for all content types in ContentHandler?
+                               if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
+                                       $text = $content->getNativeData();
+
+                                       $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
+                               } else {
+                                       $this->setWarning( "Template expansion is supported for wikitext only, " .
+                                               $title->getPrefixedDBkey() .
+                                               " uses content model " . $content->getModel() . ")" );
+
+                                       $text = false;
+                               }
                        }
                        if ( $this->parseContent ) {
-                               $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText();
+                               $po = $content->getParserOutput( $title, ParserOptions::newFromContext( $this->getContext() ) );
+                               $text = $po->getText();
+                       }
+
+                       if ( $text === null ) {
+                               $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
+
+                               if ( !$content->isSupportedFormat( $format ) ) {
+                                       $model = $content->getModel();
+                                       $name = $title->getPrefixedDBkey();
+
+                                       $this->dieUsage( "The requested format {$this->contentFormat} is not supported ".
+                                                                       "for content model $model used by $name", 'badformat' );
+                               }
+
+                               $text = $content->serialize( $format );
+                               $vals['contentformat'] = $format;
+                       }
+
+                       if ( $text !== false ) {
+                               ApiResult::setContent( $vals, $text );
                        }
-                       ApiResult::setContent( $vals, $text );
                } elseif ( $this->fld_content ) {
                        $vals['texthidden'] = '';
                }
@@ -523,11 +572,26 @@ class ApiQueryRevisions extends ApiQueryBase {
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
+                               $handler = $revision->getContentHandler();
+
                                if ( !is_null( $this->difftotext ) ) {
-                                       $engine = new DifferenceEngine( $context );
-                                       $engine->setText( $text, $this->difftotext );
+                                       $model = $title->getContentModel();
+
+                                       if ( $this->contentFormat
+                                               && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) {
+
+                                               $name = $title->getPrefixedDBkey();
+
+                                               $this->dieUsage( "The requested format {$this->contentFormat} is not supported for ".
+                                                                                       "content model $model used by $name", 'badformat' );
+                                       }
+
+                                       $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat );
+
+                                       $engine = $handler->createDifferenceEngine( $context );
+                                       $engine->setContent( $content, $difftocontent );
                                } 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();
                                }
@@ -567,6 +631,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                        'userid',
                                        'size',
                                        'sha1',
+                                       'contentmodel',
                                        'comment',
                                        'parsedcomment',
                                        'content',
@@ -616,6 +681,10 @@ class ApiQueryRevisions extends ApiQueryBase {
                        'continue' => null,
                        'diffto' => null,
                        'difftotext' => null,
+                       'contentformat' => array(
+                               ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
+                               ApiBase::PARAM_DFLT => null
+                       ),
                );
        }
 
@@ -631,6 +700,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                                ' userid         - User id of revision creator',
                                ' size           - Length (bytes) of the revision',
                                ' sha1           - SHA-1 (base 16) of the revision',
+                               ' contentmodel   - Content model id',
                                ' comment        - Comment by the user for revision',
                                ' parsedcomment  - Parsed comment by the user for the revision',
                                ' content        - Text of the revision',
@@ -655,6 +725,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                        'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.',
                                "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ),
                        'tag' => 'Only list revisions tagged with this tag',
+                       'contentformat' => 'Serialization format used for difftotext and expected for output of content',
                );
        }
 
@@ -732,13 +803,18 @@ class ApiQueryRevisions extends ApiQueryBase {
        public function getPossibleErrors() {
                return array_merge( parent::getPossibleErrors(), array(
                        array( 'nosuchrevid', 'diffto' ),
-                       array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).' ),
-                       array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.' ),
+                       array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options '
+                                                                                                       . '(limit, startid, endid, dirNewer, start, end).' ),
+                       array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, '
+                                                                                                       . ' but the limit, startid, endid, dirNewer, user, excludeuser, '
+                                                                                                       . 'start and end parameters may only be used on a single page.' ),
                        array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ),
                        array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ),
                        array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ),
                        array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ),
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ),
+                       array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied '
+                                                                                                       . ' to the page\'s content model' ),
                ) );
        }
 
index f759c02..623f545 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 ) ? strval( $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 b854a2e..3ae26d0 100644 (file)
@@ -770,16 +770,30 @@ class MessageCache {
                        Title::makeTitle( NS_MEDIAWIKI, $title ), false, Revision::READ_LATEST
                );
                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 );
+                               #XXX: Is this the right way to turn a Content object into a message?
+                               $message = $content->getWikitextForTransclusion();
+
+                               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 c7156fb..a275763 100644 (file)
@@ -38,7 +38,7 @@ class DifferenceEngine extends ContextSource {
         * @private
         */
        var $mOldid, $mNewid;
-       var $mOldtext, $mNewtext;
+       var $mOldContent, $mNewContent;
        protected $mDiffLang;
 
        /**
@@ -224,6 +224,10 @@ class DifferenceEngine extends ContextSource {
                # we'll use the application/x-external-editor interface to call
                # an external diff tool like kompare, kdiff3, etc.
                if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
+                       //TODO: come up with a good solution for non-text content here.
+                       //      at least, the content format needs to be passed to the client somehow.
+                       //      Currently, action=raw will just fail for non-text content.
+
                        $urls = array(
                                'File' => array( 'Extension' => 'wiki', 'URL' =>
                                        # This should be mOldPage, but it may not be set, see below.
@@ -510,19 +514,21 @@ class DifferenceEngine extends ContextSource {
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
 
+                       // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                        if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
                                // 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 ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+                                       // NOTE: deprecated 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 ) ) ) {
+                       } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+                               // Handled by extension
+                       } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+                               // NOTE: deprecated hook, B/C only
                                // Handled by extension
                        } else {
                                // Normal page
@@ -536,13 +542,7 @@ class DifferenceEngine extends ContextSource {
                                        $wikiPage = WikiPage::factory( $this->mNewPage );
                                }
 
-                               $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
-
-                               if ( !$this->mNewRev->isCurrent() ) {
-                                       $parserOptions->setEditSection( false );
-                               }
-
-                               $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
+                               $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
 
                                # WikiPage::getParserOutput() should not return false, but just in case
                                if( $parserOutput ) {
@@ -556,6 +556,17 @@ class DifferenceEngine extends ContextSource {
                wfProfileOut( __METHOD__ );
        }
 
+       protected function getParserOutput( WikiPage $page, Revision $rev ) {
+               $parserOptions = $page->makeParserOptions( $this->getContext() );
+
+               if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
+                       $parserOptions->setEditSection( false );
+               }
+
+               $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
+               return $parserOutput;
+       }
+
        /**
         * Get the diff text, send it to the OutputPage object
         * Returns false if the diff could not be generated, otherwise returns true
@@ -652,7 +663,7 @@ class DifferenceEngine extends ContextSource {
                        return false;
                }
 
-               $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+               $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
 
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
@@ -689,14 +700,56 @@ 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 ) {
+               if ( !( $old instanceof TextContent ) ) {
+                       throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
+                                                               . "override generateContentDiffBody to fix this." );
+               }
+
+               if ( !( $new instanceof TextContent ) ) {
+                       throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
+                               . "override generateContentDiffBody to fix this." );
+               }
+
+               $otext = $old->serialize();
+               $ntext = $new->serialize();
+
+               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
-        * @return bool|string
+        * @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 generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
 
                wfProfileIn( __METHOD__ );
@@ -859,7 +912,7 @@ class DifferenceEngine extends ContextSource {
         *        the visibility of the revision and a link to edit the page.
         * @return String HTML fragment
         */
-       private function getRevisionHeader( Revision $rev, $complete = '' ) {
+       protected function getRevisionHeader( Revision $rev, $complete = '' ) {
                $lang = $this->getLanguage();
                $user = $this->getUser();
                $revtimestamp = $rev->getTimestamp();
@@ -951,10 +1004,25 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Use specified text instead of loading from the database
+        * @deprecated since 1.WD, use setContent() instead.
         */
        function setText( $oldText, $newText ) {
-               $this->mOldtext = $oldText;
-               $this->mNewtext = $newText;
+               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;
        }
@@ -1082,14 +1150,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;
                        }
                }
@@ -1110,7 +1178,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 695c4e9..449cc45 100644 (file)
@@ -1186,8 +1186,9 @@ class LocalFile extends File {
                } else {
                        # New file; create the description page.
                        # There's already a log entry, so don't make a second RC entry
-                       # Squid and file cache for the description page are purged by doEdit.
-                       $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
+                       # Squid and file cache for the description page are purged by doEditContent.
+                       $content = ContentHandler::makeContent( $pageText, $descTitle );
+                       $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
 
                        if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
                                $dbw->begin();
@@ -1475,9 +1476,9 @@ class LocalFile extends File {
                global $wgParser;
                $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
                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 ac5dbd7..8140527 100644 (file)
@@ -1594,13 +1594,16 @@ abstract class Installer {
                $status = Status::newGood();
                try {
                        $page = WikiPage::factory( Title::newMainPage() );
-                       $page->doEdit( wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
-                                       wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text(),
-                                       '',
-                                       EDIT_NEW,
-                                       false,
-                                       User::newFromName( 'MediaWiki default' )
+                       $content = new WikitextContent (
+                               wfMessage( 'mainpagetext' )->inContentLanguage()->text() . "\n\n" .
+                               wfMessage( 'mainpagedocfooter' )->inContentLanguage()->text()
                        );
+
+                       $page->doEditContent( $content,
+                                                       '',
+                                                       EDIT_NEW,
+                                                       false,
+                                                       User::newFromName( 'MediaWiki default' ) );
                } catch (MWException $e) {
                        //using raw, because $wgShowExceptionDetails can not be set yet
                        $status->fatal( 'config-install-mainpage-failed', $e->getMessage() );
index bd55cb4..d92ab4a 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
@@ -214,6 +215,13 @@ class MysqlUpdater extends DatabaseUpdater {
                        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' ),
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.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 a741f26..d16de06 100644 (file)
@@ -71,6 +71,13 @@ class OracleUpdater extends DatabaseUpdater {
                        array( 'addIndex', 'ipblocks', 'i05', 'patch-ipblocks_i05_index.sql' ),
                        array( 'addIndex', 'revision', 'i05', 'patch-revision_i05_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' ),
+
                        // KEEP THIS AT THE BOTTOM!!
                        array( 'doRebuildDuplicateFunction' ),
 
index bc5fcac..1486683 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
@@ -93,6 +94,13 @@ class SqliteUpdater extends DatabaseUpdater {
                        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' ),
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.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 08af997..ebbd834 100644 (file)
@@ -94,8 +94,8 @@ 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;
@@ -103,7 +103,7 @@ class DoubleRedirectJob extends Job {
 
                # Check for a suppression tag (used e.g. in periodically archived discussions)
                $mw = MagicWord::get( 'staticredirect' );
-               if ( $mw->match( $text ) ) {
+               if ( $content->matchMagicWord( $mw ) ) {
                        wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
                        return true;
                }
@@ -125,14 +125,10 @@ class DoubleRedirectJob extends Job {
                        $currentDest->getFragment() );
 
                # Fix the text
-               # Remember that redirect pages can have categories, templates, etc.,
-               # so the regex has to be fairly general
-               $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
-                       '[[' . $newTitle->getFullText() . ']]',
-                       $text, 1 );
-
-               if ( $newText === $text ) {
-                       $this->setLastError( 'Text unchanged???' );
+               $newContent = $content->updateRedirect( $newTitle );
+
+               if ( $newContent->equals( $content ) ) {
+                       $this->setLastError( 'Content unchanged???' );
                        return false;
                }
 
@@ -144,7 +140,7 @@ class DoubleRedirectJob extends Job {
                $reason = wfMessage( 'double-redirect-fixed-' . $this->reason,
                        $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
                )->inContentLanguage()->text();
-               $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() );
+               $article->doEditContent( $newContent, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() );
                $wgUser = $oldUser;
 
                return true;
index b23951c..c4370f4 100644 (file)
@@ -70,16 +70,16 @@ class RefreshLinksJob extends Job {
        }
 
        public static function runForTitleInternal( Title $title, Revision $revision, $fname ) {
-               global $wgParser, $wgContLang;
+               global $wgContLang;
 
                wfProfileIn( $fname . '-parse' );
                $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
-               $parserOutput = $wgParser->parse(
-                       $revision->getText(), $title, $options, true, true, $revision->getId() );
+               $content = $revision->getContent();
+               $parserOutput = $content->getParserOutput( $title, $revision->getId(), $options, false );
                wfProfileOut( $fname . '-parse' );
 
                wfProfileIn( $fname . '-update' );
-               $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
+               $updates = $content->getSecondaryDataUpdates( $title, null, false, $parserOutput  );
                DataUpdate::runUpdates( $updates );
                wfProfileOut( $fname . '-update' );
        }
index 2a24bee..61b4239 100644 (file)
@@ -1948,6 +1948,7 @@ class Parser {
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
                                if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
+                                       // FIXME: the above check prevents links to sites with identifiers that are not language codes
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
                                        $s = rtrim( $s . $prefix );
                                        $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
@@ -3588,7 +3589,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();
@@ -3596,16 +3603,17 @@ class Parser {
                                        $text = false;
                                        break;
                                }
+                               $content = $message->content();
                                $text = $message->plain();
                        } else {
                                break;
                        }
-                       if ( $text === false ) {
+                       if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
-                       $title = Title::newFromRedirect( $text );
+                       $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
index 41b4a38..cf8b4a9 100644 (file)
@@ -50,7 +50,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 DataUpdate, used to save info from the page somewhere else.
 
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
@@ -375,9 +375,13 @@ 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.
+        *
         * @since 1.20
         *
-        * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
+        * @param $title Title The title of the page we're updating. If not given, a title object will be created
+        *                      based on $this->getTitleText()
         * @param $recursive Boolean: queue jobs for recursive updates?
         *
         * @return Array. An array of instances of DataUpdate
index 623a269..b3aabec 100644 (file)
@@ -81,7 +81,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 2ccb6d3..acff026 100644 (file)
@@ -804,11 +804,14 @@ 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 ) {
+                               //TODO: if we could plug in some code that knows about special content models *and* about
+                               //      special features of the search engine, the search could benefit.
+                               $content = $this->mRevision->getContent();
+                               $this->mText = $content->getTextForSearchIndex();
+                       } else { // TODO: can we fetch raw wikitext for commons images?
                                $this->mText = '';
-
+                       }
                }
        }
 
@@ -819,6 +822,8 @@ class SearchResult {
        function getTextSnippet( $terms ) {
                global $wgUser, $wgAdvancedSearchHighlighting;
                $this->initText();
+
+               // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
                list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
                $h = new SearchHighlighter();
                if ( $wgAdvancedSearchHighlighting )
index bf7de3f..fb65326 100644 (file)
@@ -144,8 +144,17 @@ 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, false, Revision::READ_NORMAL );
-                       $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $rev->getText() ) );
-                       return true;
+                       $content = $rev->getContent();
+
+                       if ( $content instanceof TextContent ) {
+                               //XXX: in the future, this could be stored as structured data, defining a list of book sources
+
+                               $text = $content->getNativeData();
+                               $this->getOutput()->addWikiText( str_replace( 'MAGICNUMBER', $this->isbn, $text ) );
+                               return true;
+                       } else {
+                               throw new MWException( "Unexpected content type for book sources: " . $content->getModel() );
+                       }
                }
 
                # Fall back to the defaults given in the language file
index 9e3c52b..2b7036c 100644 (file)
@@ -111,14 +111,19 @@ class SpecialComparePages extends SpecialPage {
                $rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] );
 
                if( $rev1 && $rev2 ) {
-                       $de = new DifferenceEngine( $form->getContext(),
-                               $rev1,
-                               $rev2,
-                               null, // rcid
-                               ( $data['Action'] == 'purge' ),
-                               ( $data['Unhide'] == '1' )
-                       );
-                       $de->showDiffPage( true );
+                       $revision = Revision::newFromId( $rev1 );
+
+                       if ( $revision ) { // NOTE: $rev1 was already checked, should exist.
+                               $contentHandler = $revision->getContentHandler();
+                               $de = $contentHandler->createDifferenceEngine( $form->getContext(),
+                                       $rev1,
+                                       $rev2,
+                                       null, // rcid
+                                       ( $data['Action'] == 'purge' ),
+                                       ( $data['Unhide'] == '1' )
+                               );
+                               $de->showDiffPage( true );
+                       }
                }
        }
 
index 8e15d55..b202e6b 100644 (file)
@@ -459,11 +459,12 @@ class SpecialNewpages extends IncludableSpecialPage {
        protected function feedItemDesc( $row ) {
                $revision = Revision::newFromId( $row->rev_id );
                if( $revision ) {
+                       //XXX: include content model/type in feed item?
                        return '<p>' . htmlspecialchars( $revision->getUserText() ) .
                                $this->msg( 'colon-separator' )->inContentLanguage()->escaped() .
                                htmlspecialchars( FeedItem::stripComment( $revision->getComment() ) ) .
                                "</p>\n<hr />\n<div>" .
-                               nl2br( htmlspecialchars( $revision->getText() ) ) . "</div>";
+                               nl2br( htmlspecialchars( $revision->getContent()->serialize() ) ) . "</div>";
                }
                return '';
        }
index d8e0b97..1ef1124 100644 (file)
@@ -32,7 +32,16 @@ class PageArchive {
         * @var Title
         */
        protected $title;
-       var $fileStatus;
+
+       /**
+        * @var Status
+        */
+       protected $fileStatus;
+
+       /**
+        * @var Status
+        */
+       protected $revisionStatus;
 
        function __construct( $title ) {
                if( is_null( $title ) ) {
@@ -112,12 +121,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() ),
                        __METHOD__,
@@ -174,28 +193,38 @@ 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 ) ),
                        __METHOD__ );
                if( $row ) {
-                       return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) );
+                       return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) );
                } else {
                        return null;
                }
@@ -341,7 +370,7 @@ class PageArchive {
                if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
                        $img = wfLocalFile( $this->title );
                        $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
-                       if ( !$this->fileStatus->isOk() ) {
+                       if ( !$this->fileStatus->isOK() ) {
                                return false;
                        }
                        $filesRestored = $this->fileStatus->successCount;
@@ -350,10 +379,12 @@ class PageArchive {
                }
 
                if( $restoreText ) {
-                       $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
-                       if( $textRestored === false ) { // It must be one of UNDELETE_*
+                       $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
+                       if( !$this->revisionStatus->isOK() ) {
                                return false;
                        }
+
+                       $textRestored = $this->revisionStatus->getValue();
                } else {
                        $textRestored = 0;
                }
@@ -401,11 +432,13 @@ class PageArchive {
         * @param $comment String
         * @param $unsuppress Boolean: remove all ar_deleted/fa_deleted restrictions of seletected revs
         *
-        * @return Mixed: number of revisions restored or false on failure
+        * @return Status, containing the number of revisions restored on success
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
+               global $wgContentHandlerNoDB;
+
                if ( wfReadOnly() ) {
-                       return false;
+                       throw new ReadOnlyError();
                }
                $restoreAll = empty( $timestamps );
 
@@ -435,9 +468,14 @@ class PageArchive {
                        $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
                                array( 'rev_id' => $previousRevId ),
                                __METHOD__ );
+
                        if( $previousTimestamp === false ) {
                                wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
-                               return 0;
+
+                               $status = Status::newGood( 0 );
+                               $status->warning( 'undeleterevision-missing' );
+
+                               return $status;
                        }
                } else {
                        # Have to create a new article...
@@ -457,24 +495,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(),
@@ -486,17 +531,38 @@ class PageArchive {
                $rev_count = $dbw->numRows( $result );
                if( !$rev_count ) {
                        wfDebug( __METHOD__ . ": no revisions to restore\n" );
-                       return false; // ???
+
+                       $status = Status::newGood( 0 );
+                       $status->warning( "undelete-no-results" );
+                       return $status;
                }
 
                $ret->seek( $rev_count - 1 ); // move to last
                $row = $ret->fetchObject(); // get newest archived rev
                $ret->seek( 0 ); // move back
 
+               // grab the content to check consistency with global state before restoring the page.
+               $revision = Revision::newFromArchiveRow( $row,
+                       array(
+                               'title' => $article->getTitle(), // used to derive default content model
+                       ) );
+
+               $m = $revision->getContentModel();
+
+               $user = User::newFromName( $revision->getRawUserText(), false );
+               $content = $revision->getContent( Revision::RAW );
+
+               //NOTE: article ID may not be known yet. prepareSave() should not modify the database.
+               $status = $content->prepareSave( $article, 0, -1, $user );
+
+               if ( !$status->isOK() ) {
+                       return $status;
+               }
+
                if( $makepage ) {
                        // Check the state of the newest to-be version...
                        if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
-                               return false; // we can't leave the current revision like this!
+                               return Status::newFatal( "undeleterevdel" );
                        }
                        // Safe to insert now...
                        $newid  = $article->insertOn( $dbw );
@@ -506,7 +572,7 @@ class PageArchive {
                        if( $row->ar_timestamp > $previousTimestamp ) {
                                // Check the state of the newest to-be version...
                                if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
-                                       return false; // we can't leave the current revision like this!
+                                       return Status::newFatal( "undeleterevdel" );
                                }
                        }
                }
@@ -527,7 +593,7 @@ class PageArchive {
                        // unless we are specifically removing all restrictions...
                        $revision = Revision::newFromArchiveRow( $row,
                                array(
-                                       'page' => $pageId,
+                                       'title' => $this->title,
                                        'deleted' => $unsuppress ? 0 : $row->ar_deleted
                                ) );
 
@@ -546,7 +612,7 @@ class PageArchive {
 
                // Was anything restored at all?
                if ( $restored == 0 ) {
-                       return 0;
+                       return Status::newGood( 0 );
                }
 
                $created = (bool)$newid;
@@ -566,13 +632,18 @@ class PageArchive {
                        $update->doUpdate();
                }
 
-               return $restored;
+               return Status::newGood( $restored );
        }
 
        /**
         * @return Status
         */
        function getFileStatus() { return $this->fileStatus; }
+
+       /**
+        * @return Status
+        */
+       function getRevisionStatus() { return $this->revisionStatus; }
 }
 
 /**
@@ -780,11 +851,13 @@ class SpecialUndelete extends SpecialPage {
 
        private function showRevision( $timestamp ) {
                if( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
-                       return 0;
+                       return;
                }
 
                $archive = new PageArchive( $this->mTargetObj );
-               wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) );
+               if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) {
+                       return;
+               }
                $rev = $archive->getRevision( $timestamp );
 
                $out = $this->getOutput();
@@ -834,7 +907,11 @@ class SpecialUndelete extends SpecialPage {
                $t = $lang->userTime( $timestamp, $user );
                $userLink = Linker::revUserTools( $rev );
 
-               if( $this->mPreview ) {
+               $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
+
+               $isText = ( $content instanceof TextContent );
+
+               if( $this->mPreview || $isText ) {
                        $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
                } else {
                        $openDiv = '<div id="mw-undelete-revision">';
@@ -851,23 +928,48 @@ class SpecialUndelete extends SpecialPage {
 
                $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
                        $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
-               wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
 
-               if( $this->mPreview ) {
+               if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) {
+                       return;
+               }
+
+               if( $this->mPreview || !$isText ) {
+                       // NOTE: non-text content has no source view, so always use rendered preview
+
                        // Hide [edit]s
                        $popts = $out->parserOptions();
                        $popts->setEditSection( false );
-                       $out->parserOptions( $popts );
-                       $out->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER, $user ), $this->mTargetObj, true );
+
+                       $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
+                       $out->addParserOutput( $pout );
                }
 
+               if ( $isText ) {
+                       // source view for textual content
+                       $sourceView = Xml::element( 'textarea', array(
+                               'readonly' => 'readonly',
+                               'cols' => intval( $user->getOption( 'cols' ) ),
+                               'rows' => intval( $user->getOption( 'rows' ) ) ),
+                               $content->getNativeData() . "\n" );
+
+                       $previewButton = Xml::element( 'input', array(
+                               'type' => 'submit',
+                               'name' => 'preview',
+                               'value' => $this->msg( 'showpreview' )->text() ) );
+               } else {
+                       $sourceView = '';
+                       $previewButton = '';
+               }
+
+               $diffButton = Xml::element( 'input', array(
+                       'name' => 'diff',
+                       'type' => 'submit',
+                       'value' => $this->msg( 'showdiff' )->text() ) );
+
                $out->addHTML(
-                       Xml::element( 'textarea', array(
-                                       'readonly' => 'readonly',
-                                       'cols' => intval( $user->getOption( 'cols' ) ),
-                                       'rows' => intval( $user->getOption( 'rows' ) ) ),
-                               $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
-                       Xml::openElement( 'div' ) .
+                       $sourceView .
+                       Xml::openElement( 'div', array(
+                               'style' => 'clear: both' ) ) .
                        Xml::openElement( 'form', array(
                                'method' => 'post',
                                'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
@@ -883,14 +985,8 @@ class SpecialUndelete extends SpecialPage {
                                'type' => 'hidden',
                                'name' => 'wpEditToken',
                                'value' => $user->getEditToken() ) ) .
-                       Xml::element( 'input', array(
-                               'type' => 'submit',
-                               'name' => 'preview',
-                               'value' => $this->msg( 'showpreview' )->text() ) ) .
-                       Xml::element( 'input', array(
-                               'name' => 'diff',
-                               'type' => 'submit',
-                               'value' => $this->msg( 'showdiff' )->text() ) ) .
+                       $previewButton .
+                       $diffButton .
                        Xml::closeElement( 'form' ) .
                        Xml::closeElement( 'div' ) );
        }
@@ -904,7 +1000,11 @@ class SpecialUndelete extends SpecialPage {
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
-               $diffEngine = new DifferenceEngine( $this->getContext() );
+               $diffContext = clone $this->getContext();
+               $diffContext->setTitle( $currentRev->getTitle() );
+               $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
+
+               $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
@@ -921,9 +1021,9 @@ class SpecialUndelete extends SpecialPage {
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
-                       $diffEngine->generateDiffBody(
-                               $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
-                               $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
+                       $diffEngine->generateContentDiffBody(
+                               $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
+                               $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) .
                        "</table>" .
                        "</div>\n"
                );
@@ -1180,7 +1280,10 @@ class SpecialUndelete extends SpecialPage {
 
        private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
                $rev = Revision::newFromArchiveRow( $row,
-                       array( 'page' => $this->mTargetObj->getArticleID() ) );
+                       array(
+                               'title' => $this->mTargetObj
+                       ) );
+
                $revTextSize = '';
                $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
                // Build checkboxen...
@@ -1417,11 +1520,15 @@ class SpecialUndelete extends SpecialPage {
                        $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
                } else {
                        $out->setPageTitle( $this->msg( 'undelete-error' ) );
-                       $out->addWikiMsg( 'cannotundelete' );
-                       $out->addWikiMsg( 'undeleterevdel' );
                }
 
-               // Show file deletion warnings and errors
+               // Show revision undeletion warnings and errors
+               $status = $archive->getRevisionStatus();
+               if( $status && !$status->isGood() ) {
+                       $out->addWikiText( '<div class="error">' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '</div>' );
+               }
+
+               // Show file undeletion warnings and errors
                $status = $archive->getFileStatus();
                if( $status && !$status->isGood() ) {
                        $out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' );
index c11d719..fdd2b1a 100644 (file)
@@ -70,13 +70,14 @@ class UploadFromUrl extends UploadBase {
                if ( !count( $wgCopyUploadsDomains ) ) {
                        return true;
                }
-               $parsedUrl = wfParseUrl( $url );
-               if ( !$parsedUrl ) {
+               $uri = new Uri( $url );
+               $parsedDomain = $uri->getHost();
+               if ( $parsedDomain === null ) {
                        return false;
                }
                $valid = false;
                foreach( $wgCopyUploadsDomains as $domain ) {
-                       if ( $parsedUrl['host'] === $domain ) {
+                       if ( $parsedDomain === $domain ) {
                                $valid = true;
                                break;
                        }
index 40d1f36..472871f 100644 (file)
@@ -419,6 +419,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 8b7d6cb..a211562 100644 (file)
@@ -925,7 +925,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 107b280..cbd5534 100644 (file)
@@ -2603,7 +2603,8 @@ Der aktuelle Text der gelöschten Seite ist nur Administratoren zugänglich.',
 'undeletedrevisions' => '{{PLURAL:$1|1 Version wurde|$1 Versionen wurden}} wiederhergestellt',
 'undeletedrevisions-files' => '{{PLURAL:$1|1 Version|$1 Versionen}} und {{PLURAL:$2|1 Datei|$2 Dateien}} wurden wiederhergestellt',
 'undeletedfiles' => '{{PLURAL:$1|1 Datei wurde|$1 Dateien wurden}} wiederhergestellt',
-'cannotundelete' => 'Wiederherstellung fehlgeschlagen; jemand anderes hat die Seite bereits wiederhergestellt.',
+'cannotundelete' => 'Wiederherstellung fehlgeschlagen:
+$1',
 'undeletedpage' => "'''„$1“''' wurde wiederhergestellt.
 
 Im [[Special:Log/delete|Lösch-Logbuch]] findest du eine Übersicht der gelöschten und wiederhergestellten Seiten.",
index a97dfe7..e96418d 100644 (file)
@@ -895,6 +895,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.',
@@ -1430,7 +1431,7 @@ If you save it, any changes made since this revision will be lost.",
 'yourdiff'                         => 'Differences',
 'copyrightwarning'                 => "Please note that all contributions to {{SITENAME}} are considered to be released under the $2 (see $1 for details).
 If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.<br />
-You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
+You are also promising us that you wrote this yourself, or copied editpageit from a public domain or similar free resource.
 '''Do not submit copyrighted work without permission!'''",
 'copyrightwarning2'                => "Please note that all contributions to {{SITENAME}} may be edited, altered, or removed by other contributors.
 If you do not want your writing to be edited mercilessly, then do not submit it here.<br />
@@ -1487,6 +1488,8 @@ It already exists.',
 'addsection-preload'               => '', # do not translate or duplicate this message to other languages
 'addsection-editintro'             => '', # do not translate or duplicate this message to other languages
 'defaultmessagetext'               => 'Default message text',
+'invalid-content-data'             => 'Invalid content data',
+'content-not-allowed-here'         => '"$1" content is not allowed on page [[$2]]',
 
 # Parser/template warnings
 'expensive-parserfunction-warning'        => "'''Warning:''' This page contains too many expensive parser function calls.
@@ -3070,8 +3073,8 @@ You may have a bad link, or the revision may have been restored or removed from
 'undeletedrevisions'           => '{{PLURAL:$1|1 revision|$1 revisions}} restored',
 'undeletedrevisions-files'     => '{{PLURAL:$1|1 revision|$1 revisions}} and {{PLURAL:$2|1 file|$2 files}} restored',
 'undeletedfiles'               => '{{PLURAL:$1|1 file|$1 files}} restored',
-'cannotundelete'               => 'Undelete failed;
-someone else may have undeleted the page first.',
+'cannotundelete'               => 'Undelete failed:
+$1',
 'undeletedpage'                => "'''$1 has been restored'''
 
 Consult the [[Special:Log/delete|deletion log]] for a record of recent deletions and restorations.",
@@ -4944,4 +4947,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 ContentHandler::getContentModel()
+'content-model-wikitext' => 'wikitext',
+'content-model-javascript' => 'JavaScript',
+'content-model-css' => 'CSS',
+'content-model-text' => 'plain text',
+
 );
index 522a976..1cdba1e 100644 (file)
@@ -1773,6 +1773,9 @@ iarradh sábháil. Is dócha gur nasc chuig suíomh seachtrach ba chúis leis.',
 'spamprotectionmatch' => 'Truicear ár scagaire dramhála ag an téacs seo a leanas: $1',
 'spambot_username' => 'MediaWiki turscar glanadh',
 
+# Info page
+'pageinfo-subjectpage' => 'Leathanach',
+
 # Skin names
 'skinname-standard' => 'Clasaiceach',
 'skinname-nostalgia' => 'Sean-nós',
index 858ea56..a1e4a73 100644 (file)
@@ -1053,6 +1053,10 @@ Please report at [[Support]] if you are unable to properly translate this messag
 'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
 'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
 'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
+'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.',
+'content-not-allowed-here'         => 'Error message indicating that the desired content model is not supported in given localtion.
+* $1 is the human readable name of the content model
+* $1 is the title of the page in question.',
 
 # Parser/template warnings
 'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@ -2882,6 +2886,8 @@ Options for the duration of the page protection. Example: See e.g. [[MediaWiki:P
 {{Identical|Reset}}',
 'undeleteinvert' => '{{Identical|Invert selection}}',
 'undeletecomment' => '{{Identical|Reason}}',
+'cannotundelete' => 'Message shown when undeletion failed for some reason.
+* <code>$1</code> is the combined wikitext of messages for all errors that caused the failure.',
 'undelete-search-title' => 'Page title when showing the search form in Special:Undelete',
 'undelete-search-submit' => '{{Identical|Search}}',
 'undelete-error' => 'Page title when a page could not be undeleted',
@@ -4883,4 +4889,10 @@ $4 is the gender of the target user.',
 'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
 'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
 
+# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
+'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
+'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
+'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
+'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
+
 );
index 4151723..cff77b2 100644 (file)
@@ -1144,7 +1144,8 @@ abstract class Maintenance {
                        $title = $titleObj->getPrefixedDBkey();
                        $this->output( "$title..." );
                        # Update searchindex
-                       $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() );
+                       # TODO: pass the Content object to SearchUpdate, let the search engine decide how to deal with it.
+                       $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent()->getTextForSearchIndex() );
                        $u->doUpdate();
                        $this->output( "\n" );
                }
diff --git a/maintenance/archives/patch-archive-ar_content_format.sql b/maintenance/archives/patch-archive-ar_content_format.sql
new file mode 100644 (file)
index 0000000..81f9fca
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-archive-ar_content_model.sql b/maintenance/archives/patch-archive-ar_content_model.sql
new file mode 100644 (file)
index 0000000..1a8b630
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-page-page_content_model.sql b/maintenance/archives/patch-page-page_content_model.sql
new file mode 100644 (file)
index 0000000..30434d9
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/page
+  ADD page_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_format.sql b/maintenance/archives/patch-revision-rev_content_format.sql
new file mode 100644 (file)
index 0000000..22aeb8a
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_model.sql b/maintenance/archives/patch-revision-rev_content_model.sql
new file mode 100644 (file)
index 0000000..1ba0572
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_model varbinary(32) DEFAULT NULL;
index 670b93d..4ba7e66 100644 (file)
@@ -50,7 +50,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 e20bcd8..9838569 100644 (file)
@@ -103,7 +103,8 @@ class CleanupSpam extends Maintenance {
                $rev = Revision::newFromTitle( $title );
                $currentRevId = $rev->getId();
 
-               while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT ) || LinkFilter::matchEntry( $rev->getText() , $domain ) ) ) {
+               while ( $rev && ( $rev->isDeleted( Revision::DELETED_TEXT )
+                                               || LinkFilter::matchEntry( $rev->getContent( Revision::RAW ), $domain ) ) ) {
                        $rev = $rev->getPrevious();
                }
 
@@ -117,8 +118,10 @@ class CleanupSpam extends Maintenance {
                        $page = WikiPage::factory( $title );
                        if ( $rev ) {
                                // Revert to this revision
+                               $content = $rev->getContent( Revision::RAW );
+
                                $this->output( "reverting\n" );
-                               $page->doEdit( $rev->getText(), wfMessage( 'spam_reverting', $domain )->inContentLanguage()->text(),
+                               $page->doEditContent( $content, wfMessage( 'spam_reverting', $domain )->inContentLanguage()->text(),
                                        EDIT_UPDATE, $rev->getId() );
                        } elseif ( $this->hasOption( 'delete' ) ) {
                                // Didn't find a non-spammy revision, blank the page
@@ -126,8 +129,11 @@ class CleanupSpam extends Maintenance {
                                $page->doDeleteArticle( wfMessage( 'spam_deleting', $domain )->inContentLanguage()->text() );
                        } else {
                                // Didn't find a non-spammy revision, blank the page
+                               $handler = ContentHandler::getForTitle( $title );
+                               $content = $handler->makeEmptyContent();
+
                                $this->output( "blanking\n" );
-                               $page->doEdit( '', wfMessage( 'spam_blanking', $domain )->inContentLanguage()->text() );
+                               $page->doEditContent( $content, wfMessage( 'spam_blanking', $domain )->inContentLanguage()->text() );
                        }
                        $dbw->commit( __METHOD__ );
                }
index a333717..1f3ac1c 100644 (file)
@@ -114,15 +114,24 @@ class CompareParsers extends DumpIterator {
                $parser1 = new $parser1Name();
                $parser2 = new $parser2Name();
 
-               $output1 = $parser1->parse( $rev->getText(), $title, $this->options );
-               $output2 = $parser2->parse( $rev->getText(), $title, $this->options );
+               $content = $rev->getContent();
+
+               if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) {
+                       $this->error( "Page {$title->getPrefixedText()} does not contain wikitext but {$content->getModel()}\n" );
+                       return;
+               }
+
+               $text = strval( $content->getNativeData() );
+
+               $output1 = $parser1->parse( $text, $title, $this->options );
+               $output2 = $parser2->parse( $text, $title, $this->options );
 
                if ( $output1->getText() != $output2->getText() ) {
                        $this->failed++;
                        $this->error( "Parsing for {$title->getPrefixedText()} differs\n" );
 
                        if ( $this->saveFailed ) {
-                               file_put_contents( $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt", $rev->getText());
+                               file_put_contents( $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt", $text );
                        }
                        if ( $this->showDiff ) {
                                $this->output( wfDiff( $this->stripParameters( $output1->getText() ), $this->stripParameters( $output2->getText() ), '' ) );
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 3657f96..870d632 100644 (file)
@@ -168,7 +168,7 @@ class SearchDump extends DumpIterator {
         * @param $rev Revision
         */
        public function processRevision( $rev ) {
-               if ( preg_match( $this->getOption( 'regex' ), $rev->getText() ) ) {
+               if ( preg_match( $this->getOption( 'regex' ), $rev->getContent()->getTextForSearchIndex() ) ) {
                        $this->output( $rev->getTitle() . " matches at edit from " . $rev->getTimestamp() . "\n" );
                }
        }
index 59df5e8..ad4c12f 100644 (file)
@@ -68,10 +68,11 @@ class EditCLI extends Maintenance {
 
                # Read the text
                $text = $this->getStdin( Maintenance::STDIN_ALL );
+               $content = ContentHandler::makeContent( $text, $wgTitle );
 
                # Do the edit
                $this->output( "Saving... " );
-               $status = $page->doEdit( $text, $summary,
+               $status = $page->doEditContent( $content, $summary,
                        ( $minor ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 ) |
                        ( $autoSummary ? EDIT_AUTOSUMMARY : 0 ) |
index 3e2f854..f6adfe2 100644 (file)
@@ -52,12 +52,12 @@ class GetTextMaint extends Maintenance {
                        $titleText = $title->getPrefixedText();
                        $this->error( "Page $titleText does not exist.\n", true );
                }
-               $text = $rev->getText( $this->hasOption( 'show-private' ) ? Revision::RAW : Revision::FOR_PUBLIC );
-               if ( $text === false ) {
+               $content = $rev->getContent( $this->hasOption( 'show-private' ) ? Revision::RAW : Revision::FOR_PUBLIC );
+               if ( $content === false ) {
                        $titleText = $title->getPrefixedText();
                        $this->error( "Couldn't extract the text from $titleText.\n", true );
                }
-               $this->output( $text );
+               $this->output( $content->serialize() );
        }
 }
 
old mode 100755 (executable)
new mode 100644 (file)
old mode 100755 (executable)
new mode 100644 (file)
index e369cb1..e25ee2b 100644 (file)
@@ -56,13 +56,17 @@ class ImportSiteScripts extends Maintenance {
                        }
 
                        $this->output( "Importing $page\n" );
-                       $url = wfAppendQuery( $baseUrl, array(
+                       $uri = new Uri( $baseUrl );
+                       $uri->extendQuery( array(
                                'action' => 'raw',
                                'title' => "MediaWiki:{$page}" ) );
+                       $url = $uri->toString();
+
                        $text = Http::get( $url );
 
                        $wikiPage = WikiPage::factory( $title );
-                       $wikiPage->doEdit( $text, "Importing from $url", 0, false, $user );
+                       $content = ContentHandler::makeContent( $text, $wikiPage->getTitle() );
+                       $wikiPage->doEditContent( $content, "Importing from $url", 0, false, $user );
                }
 
        }
@@ -79,7 +83,9 @@ class ImportSiteScripts extends Maintenance {
                $pages = array();
 
                do {
-                       $url = wfAppendQuery( $baseUrl, $data );
+                       $uri = new Uri( $baseUrl );
+                       $uri->extendQuery( $data );
+                       $url = $uri->toString();
                        $strResult = Http::get( $url );
                        //$result = FormatJson::decode( $strResult ); // Still broken
                        $result = unserialize( $strResult );
index adb5063..c04989c 100644 (file)
@@ -56,7 +56,8 @@ if ( count( $args ) < 1 || isset( $options['help'] ) ) {
 
                                        echo( "\nPerforming edit..." );
                                        $page = WikiPage::factory( $title );
-                                       $page->doEdit( $text, $comment, $flags, false, $user );
+                                       $content = ContentHandler::makeContent( $text, $title );
+                                       $page->doEditContent( $content, $comment, $flags, false, $user );
                                        echo( "done.\n" );
 
                                } else {
index 6c835f4..07c395f 100644 (file)
@@ -74,16 +74,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 2e14d31..382b7be 100644 (file)
@@ -143,14 +143,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,
@@ -174,10 +174,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 5952fd9..87fc997 100644 (file)
@@ -78,8 +78,14 @@ class PreprocessDump extends DumpIterator {
         * @param $rev Revision
         */
        public function processRevision( $rev ) {
+               $content = $rev->getContent( Revision::RAW );
+
+               if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) {
+                       return;
+               }
+
                try {
-                       $this->mPreprocessor->preprocessToObj( $rev->getText(), 0 );
+                       $this->mPreprocessor->preprocessToObj( strval( $content->getNativeData() ), 0 );
                }
                catch(Exception $e) {
                        $this->error("Caught exception " . $e->getMessage() . " in " . $rev->getTitle()->getPrefixedText() );
index 3d9270b..535808d 100644 (file)
@@ -216,18 +216,16 @@ 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();
+               $updates = $content->getSecondaryDataUpdates( $page->getTitle() );
+               DataUpdate::runUpdates( $updates );
 
                $dbw->commit( __METHOD__ );
        }
index 24bedfa..ad9a380 100644 (file)
@@ -100,10 +100,10 @@ class DumpRenderer extends Maintenance {
                $this->output( sprintf( "%s\n", $filename, $display ) );
 
                $user = new User();
-               $parser = new $wgParserConf['class']();
                $options = ParserOptions::newFromUser( $user );
 
-               $output = $parser->parse( $rev->getText(), $title, $options );
+               $content = $rev->getContent();
+               $output = $content->getParserOutput( $title, null, $options );
 
                file_put_contents( $filename,
                        "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" " .
diff --git a/maintenance/storage/drop_content_model_info.sql b/maintenance/storage/drop_content_model_info.sql
new file mode 100644 (file)
index 0000000..7bd9aba
--- /dev/null
@@ -0,0 +1,7 @@
+ALTER TABLE /*$wgDBprefix*/archive  DROP COLUMN ar_content_model;
+ALTER TABLE /*$wgDBprefix*/archive  DROP COLUMN ar_content_format;
+
+ALTER TABLE /*$wgDBprefix*/revision  DROP COLUMN rev_content_model;
+ALTER TABLE /*$wgDBprefix*/revision  DROP COLUMN rev_content_format;
+
+ALTER TABLE /*$wgDBprefix*/page  DROP COLUMN page_content_model;
old mode 100755 (executable)
new mode 100644 (file)
index 998ebe4..1efe75a 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 062052f..89e6ea5 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 cd59833..47d3e71 100644 (file)
@@ -1204,7 +1204,7 @@ class ParserTest {
                        }
                }
 
-               $page->doEdit( $text, '', EDIT_NEW );
+               $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
 
                $wgCapitalLinks = $oldCapitalLinks;
        }
index 1cc45e0..d3dd389 100644 (file)
@@ -19,6 +19,16 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
        protected $reuseDB = false;
        protected $tablesUsed = array(); // tables with data
 
+       protected $restoreGlobals = array( // global variables to restore for each test
+               'wgLang',
+               'wgContLang',
+               'wgLanguageCode',
+               'wgUser',
+               'wgTitle',
+       );
+
+       private $savedGlobals = array();
+
        private static $dbSetup = false;
 
        /**
@@ -119,7 +129,21 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                return $fname;
        }
 
-       protected function tearDown() {
+       protected function setup() {
+               parent::setup();
+
+               foreach ( $this->restoreGlobals as $var ) {
+                       $v = $GLOBALS[ $var ];
+
+                       if ( is_object( $v ) || is_array( $v ) ) {
+                               $v = clone $v;
+                       }
+
+                       $this->savedGlobals[ $var ] = $v;
+               }
+       }
+
+       protected function teardown() {
                // Cleaning up temporary files
                foreach ( $this->tmpfiles as $fname ) {
                        if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
@@ -136,7 +160,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                        }
                }
 
-               parent::tearDown();
+               // restore saved globals
+               foreach ( $this->savedGlobals as $k => $v ) {
+                       $GLOBALS[ $k ] = $v;
+               }
+
+               parent::teardown();
        }
 
        function dbPrefix() {
@@ -212,11 +241,12 @@ abstract class MediaWikiTestCase extends PHPUnit_Framework_TestCase {
                //Make 1 page with 1 revision
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                if ( !$page->getId() == 0 ) {
-                       $page->doEdit( 'UTContent',
-                                                       'UTPageSummary',
-                                                       EDIT_NEW,
-                                                       false,
-                                                       User::newFromName( 'UTSysop' ) );
+                       $page->doEditContent(
+                               new WikitextContent( 'UTContent' ),
+                               'UTPageSummary',
+                               EDIT_NEW,
+                               false,
+                               User::newFromName( 'UTSysop' ) );
                }
        }
 
index 17cee6e..967ffa1 100644 (file)
@@ -16,14 +16,14 @@ class ArticleTablesTest extends MediaWikiLangTestCase {
                $wgContLang = Language::factory( 'es' );
 
                $wgLang = Language::factory( 'fr' );
-               $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', 0, false, $user );
+               $status = $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', 0, false, $user );
                $templates1 = $title->getTemplateLinksFrom();
 
                $wgLang = Language::factory( 'de' );
                $page->mPreparedEdit = false; // In order to force the rerendering of the same wikitext
 
                // We need an edit, a purge is not enough to regenerate the tables
-               $status = $page->doEdit( '{{:{{int:history}}}}', 'Test code for bug 14404', EDIT_UPDATE, false, $user );
+               $status = $page->doEditContent( new WikitextContent( '{{:{{int:history}}}}' ), 'Test code for bug 14404', EDIT_UPDATE, false, $user );
                $templates2 = $title->getTemplateLinksFrom();
 
                $this->assertEquals( $templates1, $templates2 );
index 846d2b8..744417b 100644 (file)
@@ -54,6 +54,9 @@ class ArticleTest extends MediaWikiTestCase {
         * Checks for the existence of the backwards compatibility static functions (forwarders to WikiPage class)
         */
        function testStaticFunctions() {
+               $this->hideDeprecated( 'Article::getAutosummary' );
+               $this->hideDeprecated( 'WikiPage::getAutosummary' );
+
                $this->assertEquals( WikiPage::selectFields(), Article::selectFields(),
                        "Article static functions" );
                $this->assertEquals( true, is_callable( "Article::onArticleCreate" ),
diff --git a/tests/phpunit/includes/ContentHandlerTest.php b/tests/phpunit/includes/ContentHandlerTest.php
new file mode 100644 (file)
index 0000000..633f72d
--- /dev/null
@@ -0,0 +1,415 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @note: Declare that we are using the database, because otherwise we'll fail in the "databaseless" test run.
+ * This is because the LinkHolderArray used by the parser needs database access.
+ *
+ * @group Database
+ */
+class ContentHandlerTest extends MediaWikiTestCase {
+
+       public function setup() {
+               parent::setup();
+
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = "testing";
+               $wgContentHandlers[ "testing" ] = '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[ "testing" ] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
+               parent::teardown();
+       }
+
+       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 dataGetLocalizedName() {
+               return array(
+                       array( null, null ),
+                       array( "xyzzy", null ),
+
+                       array( CONTENT_MODEL_JAVASCRIPT, '/javascript/i' ), //XXX: depends on content language
+               );
+       }
+
+       /**
+        * @dataProvider dataGetLocalizedName
+        */
+       public function testGetLocalizedName( $id, $expected ) {
+               $name = ContentHandler::getLocalizedName( $id );
+
+               if ( $expected ) {
+                       $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" );
+               } else {
+                       $this->assertEquals( $id, $name, "localization of unknown model $id should have "
+                                                                                       . "fallen back to use the model id directly." );
+               }
+       }
+
+       public function dataGetPageLanguage() {
+               global $wgLanguageCode;
+
+               return array(
+                       array( "Main", $wgLanguageCode ),
+                       array( "Dummy:Foo", $wgLanguageCode ),
+                       array( "MediaWiki:common.js", 'en' ),
+                       array( "User:Foo/common.js", 'en' ),
+                       array( "MediaWiki:common.css", 'en' ),
+                       array( "User:Foo/common.css", 'en' ),
+                       array( "User:Foo", $wgLanguageCode ),
+
+                       array( CONTENT_MODEL_JAVASCRIPT, 'javascript' ),
+               );
+       }
+
+       /**
+        * @dataProvider dataGetPageLanguage
+        */
+       public function testGetPageLanguage( $title, $expected ) {
+               if ( is_string( $title ) ) {
+                       $title = Title::newFromText( $title );
+               }
+
+               $expected = wfGetLangObj( $expected );
+
+               $handler = ContentHandler::getForTitle( $title );
+               $lang = $handler->getPageLanguage( $title );
+
+               $this->assertEquals( $expected->getCode(), $lang->getCode() );
+       }
+
+       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, "testing", '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, "testing", "testing", '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, "testing", null, null, true ),
+                       array( 'hallo', 'MediaWiki:Test.js', CONTENT_MODEL_CSS, "testing", null, null, true ),
+                       array( 'hallo', 'Dummy:Test', CONTENT_MODEL_JAVASCRIPT, "testing", 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 testSupportsSections() {
+               $this->markTestIncomplete( "not yet implemented" );
+       }
+
+       public function testRunLegacyHooks() {
+               Hooks::register( 'testRunLegacyHooks', __CLASS__ . '::dummyHookHandler' );
+
+               $content = new WikitextContent( 'test text' );
+               $ok = ContentHandler::runLegacyHooks( 'testRunLegacyHooks', array( 'foo', &$content, 'bar' ), false );
+
+               $this->assertTrue( $ok, "runLegacyHooks should have returned true" );
+               $this->assertEquals( "TEST TEXT", $content->getNativeData() );
+       }
+
+       public static function dummyHookHandler( $foo, &$text, $bar ) {
+               if ( $text === null || $text === false ) {
+                       return false;
+               }
+
+               $text = strtoupper( $text );
+
+               return true;
+       }
+}
+
+class DummyContentHandlerForTesting extends ContentHandler {
+
+       public function __construct( $dataModel ) {
+               parent::__construct( $dataModel, array( "testing" ) );
+       }
+
+       /**
+        * Serializes Content object of the type supported by this ContentHandler.
+        *
+        * @param Content $content the Content object to serialize
+        * @param null $format the desired serialization format
+        * @return String serialized form of the content
+        */
+       public function serializeContent( Content $content, $format = null )
+       {
+          return $content->serialize();
+       }
+
+       /**
+        * Unserializes a Content object of the type supported by this ContentHandler.
+        *
+        * @param $blob String serialized form of the content
+        * @param null $format the format used for serialization
+        * @return Content the Content object created by deserializing $blob
+        */
+       public function unserializeContent( $blob, $format = null )
+       {
+               $d = unserialize( $blob );
+               return new DummyContentForTesting( $d );
+       }
+
+       /**
+        * Creates an empty Content object of the type supported by this ContentHandler.
+        *
+        */
+       public function makeEmptyContent()
+       {
+               return new DummyContentForTesting( '' );
+       }
+}
+
+class DummyContentForTesting extends AbstractContent {
+
+       public function __construct( $data ) {
+               parent::__construct( "testing" );
+
+               $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;
+       }
+
+       /**
+        * @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( Title $title, $revId = null, ParserOptions $options = NULL, $generateHtml = true )
+       {
+               return new ParserOutput( $this->getNativeData() );
+       }
+}
diff --git a/tests/phpunit/includes/CssContentTest.php b/tests/phpunit/includes/CssContentTest.php
new file mode 100644 (file)
index 0000000..163ce48
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @group Database
+ *        ^--- needed, because we do need the database to test link updates
+ */
+class CssContentTest extends JavascriptContentTest {
+
+       public function newContent( $text ) {
+               return new CssContent( $text );
+       }
+
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("MediaWiki:Test.css", "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 ),
+               );
+       }
+
+}
index 9097d30..129f271 100644 (file)
@@ -108,7 +108,7 @@ class GlobalTest extends MediaWikiTestCase {
                        array( array( 'foo' => 1 ), 'foo=1' ), // number test
                        array( array( 'foo' => true ), 'foo=1' ), // true test
                        array( array( 'foo' => false ), '' ), // false test
-                       array( array( 'foo' => null ), '' ), // null test
+                       array( array( 'foo' => null ), 'foo' ), // null test
                        array( array( 'foo' => 'A&B=5+6@!"\'' ), 'foo=A%26B%3D5%2B6%40%21%22%27' ), // urlencoding test
                        array( array( 'foo' => 'bar', 'baz' => 'is', 'asdf' => 'qwerty' ), 'foo=bar&baz=is&asdf=qwerty' ), // multi-item test
                        array( array( 'foo' => array( 'bar' => 'baz' ) ), 'foo%5Bbar%5D=baz' ),
diff --git a/tests/phpunit/includes/JavascriptContentTest.php b/tests/phpunit/includes/JavascriptContentTest.php
new file mode 100644 (file)
index 0000000..f8a9bdf
--- /dev/null
@@ -0,0 +1,259 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @group Database
+ *        ^--- needed, because we do need the database to test link updates
+ */
+class JavascriptContentTest extends WikitextContentTest {
+
+       public function newContent( $text ) {
+               return new JavascriptContent( $text );
+       }
+
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("MediaWiki:Test.js", "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 ) );
+       }
+
+       // XXX: currently, preSaveTransform is applied to scripts. this may change or become optional.
+       /*
+       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 testMatchMagicWord( ) {
+               $mw = MagicWord::get( "staticredirect" );
+
+               $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+               $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word, since it's not wikitext" );
+       }
+
+       public function testUpdateRedirect( ) {
+               $target = Title::newFromText( "testUpdateRedirect_target" );
+
+               $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+               $newContent = $content->updateRedirect( $target );
+
+               $this->assertTrue( $content->equals( $newContent ), "content should be unchanged since it's not wikitext" );
+       }
+
+       # =================================================================================================================
+
+       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 4946200..a50a223 100644 (file)
@@ -146,7 +146,9 @@ class LinksUpdateTest extends MediaWikiTestCase {
        protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) {
                $update = new LinksUpdate( $title, $parserOutput );
 
+               $update->beginTransaction();
                $update->doUpdate();
+               $update->commitTransaction();
 
                $this->assertSelect( $table, $fields, $condition, $expectedRows );
        }
index 8a7face..4786e3d 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
  *
@@ -11,6 +12,9 @@
  */
 class RevisionStorageTest extends MediaWikiTestCase {
 
+       /**
+        * @var WikiPage $the_page
+        */
        var $the_page;
 
        function  __construct( $name = null, array $data = array(), $dataName = '' ) {
@@ -35,11 +39,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();
 
@@ -63,7 +91,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;
        }
@@ -75,6 +104,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() );
        }
 
@@ -158,7 +189,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $page = $this->createPage( 'RevisionStorageTest_testFetchRevision', 'one' );
                $id1 = $page->getRevision()->getId();
 
-               $page->doEdit( 'two', 'second rev' );
+               $page->doEditContent( new WikitextContent( 'two' ), 'second rev' );
                $id2 = $page->getRevision()->getId();
 
                $res = Revision::fetchRevision( $page->getTitle() );
@@ -179,12 +210,23 @@ class RevisionStorageTest extends MediaWikiTestCase {
         */
        public function testSelectFields()
        {
+               global $wgContentHandlerUseDB;
+
                $fields = Revision::selectFields();
 
                $this->assertTrue( in_array( 'rev_id', $fields ), 'missing rev_id in list of fields');
                $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
                $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
                $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
+
+               if ( $wgContentHandlerUseDB ) {
+                       $this->assertTrue( in_array( 'rev_content_model', $fields ),
+                                                               'missing rev_content_model in list of fields');
+                       $this->assertTrue( in_array( 'rev_content_format', $fields ),
+                                                               'missing rev_content_format in list of fields');
+               } else {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
        }
 
        /**
@@ -205,12 +247,25 @@ class RevisionStorageTest extends MediaWikiTestCase {
         */
        public function testGetText()
        {
+               $this->hideDeprecated( 'Revision::getText' );
+
                $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
                $rev = Revision::newFromId( $orig->getId() );
 
                $this->assertEquals( 'hello hello.', $rev->getText() );
        }
 
+       /**
+        * @covers Revision::getContent
+        */
+       public function testGetContent()
+       {
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
+       }
+
        /**
         * @covers Revision::revText
         */
@@ -228,11 +283,51 @@ class RevisionStorageTest extends MediaWikiTestCase {
         */
        public function testGetRawText()
        {
+               $this->hideDeprecated( 'Revision::getRawText' );
+
                $orig = $this->makeRevision( array( 'text' => 'hello hello raw.' ) );
                $rev = Revision::newFromId( $orig->getId() );
 
                $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
        }
+
+       /**
+        * @covers Revision::getContentModel
+        */
+       public function testGetContentModel()
+       {
+               global $wgContentHandlerUseDB;
+
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+                                                                                       'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       /**
+        * @covers Revision::getContentFormat
+        */
+       public function testGetContentFormat()
+       {
+               global $wgContentHandlerUseDB;
+
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+
+               $orig = $this->makeRevision( array( 'text' => 'hello hello.',
+                                                                                       'content_model' => CONTENT_MODEL_JAVASCRIPT,
+                                                                                       'content_format' => CONTENT_FORMAT_JAVASCRIPT ) );
+               $rev = Revision::newFromId( $orig->getId() );
+
+               $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
+       }
+
        /**
         * @covers Revision::isCurrent
         */
@@ -247,7 +342,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
@@ -270,7 +365,8 @@ 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() );
@@ -287,7 +383,8 @@ 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() );
@@ -305,9 +402,11 @@ class RevisionStorageTest extends MediaWikiTestCase {
                $dbw = wfGetDB( DB_MASTER );
                $rev = Revision::newNullRevision( $dbw, $page->getId(), 'a null revision', false );
 
-               $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' );
-               $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' );
-               $this->assertEquals( 'some testing text', $rev->getText() );
+               $this->assertNotEquals( $orig->getId(), $rev->getId(),
+                                                               'new null revision shold have a different id from the original revision' );
+               $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
+                                                               'new null revision shold have the same text id as the original revision' );
+               $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
        }
 
        public function dataUserWasLastToEdit() {
@@ -351,6 +450,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                # zero
                $revisions[0] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(), // we need the title to determine the page's default content model
                        'timestamp' => '20120101000000',
                        'user' => $userA->getId(),
                        'text' => 'zero',
@@ -361,6 +461,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                # one
                $revisions[1] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(), // still need the title, because $page->getId() is 0 (there's no entry in the page table)
                        'timestamp' => '20120101000100',
                        'user' => $userA->getId(),
                        'text' => 'one',
@@ -371,6 +472,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                # two
                $revisions[2] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000200',
                        'user' => $userB->getId(),
                        'text' => 'two',
@@ -381,6 +483,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                # three
                $revisions[3] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000300',
                        'user' => $userA->getId(),
                        'text' => 'three',
@@ -391,6 +494,7 @@ class RevisionStorageTest extends MediaWikiTestCase {
                # four
                $revisions[4] = new Revision( array(
                        'page' => $page->getId(),
+                       'title' => $page->getTitle(),
                        'timestamp' => '20120101000200',
                        'user' => $userA->getId(),
                        'text' => 'zero',
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..3778914 100644 (file)
@@ -1,25 +1,54 @@
 <?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 ] = "testing";
+               $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting';
+               $wgContentHandlers[ "RevisionTestModifyableContent" ] = 'RevisionTestModifyableContentHandler';
+
+               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 +149,293 @@ 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, "testing" ),
+               );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetContentModel
+        */
+       function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedModel, $rev->getContentModel() );
+       }
+
+       function dataGetContentFormat() {
+               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, "testing" ),
+               );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetContentFormat
+        */
+       function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
+       }
+
+       function dataGetContentHandler() {
+               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' ),
+               );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetContentHandler
+        */
+       function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
+       }
+
+       function dataGetContent() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, serialize('hello world') ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, serialize('hello world') ),
+               );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetContent
+        */
+       function testGetContent( $text, $title, $model, $format, $audience, $expectedSerialization ) {
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+               $content = $rev->getContent( $audience );
+
+               $this->assertEquals( $expectedSerialization, is_null( $content ) ? null : $content->serialize( $format ) );
+       }
+
+       function dataGetText() {
+               return array(
+                       array( 'hello world', 'Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ),
+                       array( serialize('hello world'), 'Hello', "testing", null, Revision::FOR_PUBLIC, null ),
+                       array( serialize('hello world'), 'Dummy:Hello', null, null, Revision::FOR_PUBLIC, null ),
+               );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetText
+        */
+       function testGetText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $this->hideDeprecated( 'Revision::getText' );
+
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedText, $rev->getText( $audience ) );
+       }
+
+       /**
+        * @group Database
+        * @dataProvider dataGetText
+        */
+       function testGetRawText( $text, $title, $model, $format, $audience, $expectedText ) {
+               $this->hideDeprecated( 'Revision::getRawText' );
+
+               $rev = $this->newTestRevision( $text, $title, $model, $format );
+
+               $this->assertEquals( $expectedText, $rev->getRawText( $audience ) );
+       }
+
+
+       public function dataGetSize( ) {
+               return array(
+                       array( "hello world.", null, 12 ),
+                       array( serialize( "hello world." ), "testing", 12 ),
+               );
+       }
+
+       /**
+        * @covers Revision::getSize
+        * @group Database
+        * @dataProvider dataGetSize
+        */
+       public function testGetSize( $text, $model, $expected_size )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
+               $this->assertEquals( $expected_size, $rev->getSize() );
+       }
+
+       public function dataGetSha1( ) {
+               return array(
+                       array( "hello world.", null, Revision::base36Sha1( "hello world." ) ),
+                       array( serialize( "hello world." ), "testing", Revision::base36Sha1( serialize( "hello world." ) ) ),
+               );
+       }
+
+       /**
+        * @covers Revision::getSha1
+        * @group Database
+        * @dataProvider dataGetSha1
+        */
+       public function testGetSha1( $text, $model, $expected_hash )
+       {
+               $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
+               $this->assertEquals( $expected_hash, $rev->getSha1() );
+       }
+
+       public function testConstructWithText() {
+               $this->hideDeprecated( "Revision::getText" );
+
+               $rev = new Revision( array(
+                                         'text' => 'hello world.',
+                                         'content_model' => CONTENT_MODEL_JAVASCRIPT
+                                    ));
+
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       public function testConstructWithContent() {
+               $this->hideDeprecated( "Revision::getText" );
+
+               $title = Title::newFromText( 'RevisionTest_testConstructWithContent' );
+
+               $rev = new Revision( array(
+                                         'content' => ContentHandler::makeContent( 'hello world.', $title, CONTENT_MODEL_JAVASCRIPT ),
+                                    ));
+
+               $this->assertNotNull( $rev->getText(), 'no content text' );
+               $this->assertNotNull( $rev->getContent(), 'no content object available' );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContent()->getModel() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
+       }
+
+       /**
+        * Tests whether $rev->getContent() returns a clone when needed.
+        *
+        * @group Database
+        */
+       function testGetContentClone( ) {
+               $content = new RevisionTestModifyableContent( "foo" );
+
+               $rev = new Revision(
+                       array(
+                               'id'         => 42,
+                               'page'       => 23,
+                               'title'      => Title::newFromText( "testGetContentClone_dummy" ),
+
+                               'content'    => $content,
+                               'length'     => $content->getSize(),
+                               'comment'    => "testing",
+                               'minor_edit' => false,
+                       )
+               );
+
+               $content = $rev->getContent( Revision::RAW );
+               $content->setText( "bar" );
+
+               $content2 = $rev->getContent( Revision::RAW );
+               $this->assertNotSame( $content, $content2, "expected a clone" ); // content is mutable, expect clone
+               $this->assertEquals( "foo", $content2->getText() ); // clone should contain the original text
+
+               $content2->setText( "bla bla" );
+               $this->assertEquals( "bar", $content->getText() ); // clones should be independent
+       }
+
+
+       /**
+        * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
+        *
+        * @group Database
+        */
+       function testGetContentUncloned() {
+               $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
+               $content = $rev->getContent( Revision::RAW );
+               $content2 = $rev->getContent( Revision::RAW );
+
+               // for immutable content like wikitext, this should be the same object
+               $this->assertSame( $content, $content2 );
+       }
+
+}
+
+class RevisionTestModifyableContent extends TextContent {
+       public function __construct( $text ) {
+               parent::__construct( $text, "RevisionTestModifyableContent" );
+       }
+
+       public function copy( ) {
+               return new RevisionTestModifyableContent( $this->mText );
+       }
+
+       public function getText() {
+               return $this->mText;
+       }
+
+       public function setText( $text ) {
+               $this->mText = $text;
+       }
+
 }
 
+class RevisionTestModifyableContentHandler extends TextContentHandler {
 
+       public function __construct( ) {
+               parent::__construct( "RevisionTestModifyableContent", array( CONTENT_FORMAT_TEXT ) );
+       }
+
+       public function unserializeContent( $text, $format = null ) {
+               $this->checkFormat( $format );
+
+               return new RevisionTestModifyableContent( $text );
+       }
+
+       public function makeEmptyContent() {
+               return new RevisionTestModifyableContent( '' );
+       }
+}
index 39ce6e3..03b94ae 100644 (file)
@@ -13,14 +13,14 @@ class TemplateCategoriesTest extends MediaWikiLangTestCase {
                $user = new User();
                $user->mRights = array( 'createpage', 'edit', 'purge' );
 
-               $status = $page->doEdit( '{{Categorising template}}', 'Create a page with a template', 0, false, $user );
+               $status = $page->doEditContent( new WikitextContent( '{{Categorising template}}' ), 'Create a page with a template', 0, false, $user );
                $this->assertEquals(
                        array()
                        , $title->getParentCategories()
                );
 
                $template = WikiPage::factory( Title::newFromText( 'Template:Categorising template' ) );
-               $status = $template->doEdit( '[[Category:Solved bugs]]', 'Add a category through a template', 0, false, $user );
+               $status = $template->doEditContent( new WikitextContent( '[[Category:Solved bugs]]' ), 'Add a category through a template', 0, false, $user );
 
                // Run the job queue
                $jobs = new RunJobs;
index 231228f..fd4ceb7 100644 (file)
@@ -43,6 +43,13 @@ class TimestampTest extends MediaWikiTestCase {
         * Test human readable timestamp format.
         */
        function testHumanOutput() {
+               global $wgLang;
+
+               $wgLang = Language::factory( 'es' );
+               $timestamp = new MWTimestamp( time() - 3600 );
+               $this->assertEquals( "hace una hora", $timestamp->getHumanTimestamp()->toString() );
+
+               $wgLang = Language::factory( 'en' );
                $timestamp = new MWTimestamp( time() - 3600 );
                $this->assertEquals( "1 hour ago", $timestamp->getHumanTimestamp()->toString() );
        }
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 f61652d..2a8ec44 100644 (file)
@@ -1,5 +1,10 @@
 <?php
 
+/**
+ *
+ * @group Database
+ *        ^--- needed for language cache stuff
+ */
 class TitleTest extends MediaWikiTestCase {
 
        function testLegalChars() {
diff --git a/tests/phpunit/includes/UriTest.php b/tests/phpunit/includes/UriTest.php
new file mode 100644 (file)
index 0000000..3b78f47
--- /dev/null
@@ -0,0 +1,164 @@
+<?php
+
+class UriTest extends MediaWikiTestCase {
+
+       function setUp() {
+               AutoLoader::loadClass( 'Uri' );
+       }
+
+       function dataUris() {
+               return array(
+                       array(
+                               'http://example.com/',
+                               array(
+                                       'scheme'    => 'http',
+                                       'delimiter' => '://',
+                                       'user'      => null,
+                                       'pass'      => null,
+                                       'host'      => 'example.com',
+                                       'port'      => null,
+                                       'path'      => '/',
+                                       'query'     => null,
+                                       'fragment'  => null,
+                               ),
+                       ),
+                       array(
+                               '//mediawiki.org/wiki/Main_Page',
+                               array(
+                                       'scheme'    => null,
+                                       'delimiter' => '//',
+                                       'user'      => null,
+                                       'pass'      => null,
+                                       'host'      => 'mediawiki.org',
+                                       'port'      => null,
+                                       'path'      => '/wiki/Main_Page',
+                                       'query'     => null,
+                                       'fragment'  => null,
+                               ),
+                       ),
+                       array(
+                               'http://user:pass@example.com/',
+                               array(
+                                       'scheme'    => 'http',
+                                       'delimiter' => '://',
+                                       'user'      => 'user',
+                                       'pass'      => 'pass',
+                                       'host'      => 'example.com',
+                                       'port'      => null,
+                                       'path'      => '/',
+                                       'query'     => null,
+                                       'fragment'  => null,
+                               ),
+                       ),
+                       array(
+                               '/?asdf=asdf',
+                               array(
+                                       'scheme'    => null,
+                                       'delimiter' => null,
+                                       'user'      => null,
+                                       'pass'      => null,
+                                       'host'      => null,
+                                       'port'      => null,
+                                       'path'      => '/',
+                                       'query'     => 'asdf=asdf',
+                                       'fragment'  => null,
+                               ),
+                       ),
+                       array(
+                               '?asdf=asdf#asdf',
+                               array(
+                                       'scheme'    => null,
+                                       'delimiter' => null,
+                                       'user'      => null,
+                                       'pass'      => null,
+                                       'host'      => null,
+                                       'port'      => null,
+                                       'path'      => null,
+                                       'query'     => 'asdf=asdf',
+                                       'fragment'  => 'asdf',
+                               ),
+                       )
+               );
+       }
+
+       /**
+        * Ensure that get* methods properly match the appropriate getComponent( key ) value
+        * @dataProvider dataUris
+        */
+       function testGetters( $uri ) {
+               $uri = new Uri( $uri );
+               $getterMap = array(
+                       'getProtocol' => 'scheme',
+                       'getUser' => 'user',
+                       'getPassword' => 'pass',
+                       'getHost' => 'host',
+                       'getPort' => 'port',
+                       'getPath' => 'path',
+                       'getQueryString' => 'query',
+                       'getFragment' => 'fragment',
+               );
+               foreach ( $getterMap as $fn => $c ) {
+                       $this->assertSame( $uri->{$fn}(), $uri->getComponent( $c ), "\$uri->{$fn}(); matches \$uri->getComponent( '$c' );" );
+               }
+       }
+
+       /**
+        * Ensure that Uri has the proper components for our example uris
+        * @dataProvider dataUris
+        */
+       function testComponents( $uri, $components ) {
+               $uri = new Uri( $uri );
+
+               $this->assertSame( $components['scheme'], $uri->getProtocol(), 'Correct scheme' );
+               $this->assertSame( $components['delimiter'], $uri->getDelimiter(), 'Correct delimiter' );
+               $this->assertSame( $components['user'], $uri->getUser(), 'Correct user' );
+               $this->assertSame( $components['pass'], $uri->getPassword(), 'Correct pass' );
+               $this->assertSame( $components['host'], $uri->getHost(), 'Correct host' );
+               $this->assertSame( $components['port'], $uri->getPort(), 'Correct port' );
+               $this->assertSame( $components['path'], $uri->getPath(), 'Correct path' );
+               $this->assertSame( $components['query'], $uri->getQueryString(), 'Correct query' );
+               $this->assertSame( $components['fragment'], $uri->getFragment(), 'Correct fragment' );
+       }
+
+       /**
+        * Ensure that the aliases work for various components.
+        */
+       function testAliases() {
+               $url = "//myuser@test.com";
+               $uri = new Uri( $url );
+
+               // Set the aliases.
+               $uri->setComponent( 'protocol', 'https' );
+               $uri->setComponent( 'password', 'mypass' );
+
+               // Now try getting them.
+               $this->assertSame( 'https', $uri->getComponent( 'protocol' ), 'Correct protocol (alias for scheme)' );
+               $this->assertSame( 'mypass', $uri->getComponent( 'password' ), 'Correct password (alias for pass)' );
+
+               // Finally check their actual names.
+               $this->assertSame( 'https', $uri->getProtocol(), 'Alias for scheme works' );
+               $this->assertSame( 'mypass', $uri->getPassword(), 'Alias for pass works' );
+       }
+
+       /**
+        * Ensure that Uri's helper methods return the correct data
+        */
+       function testHelpers() {
+               $uri = new Uri( 'http://a:b@example.com:8080/path?query=value' );
+
+               $this->assertSame( 'a:b', $uri->getUserInfo(), 'Correct getUserInfo' );
+               $this->assertSame( 'example.com:8080', $uri->getHostPort(), 'Correct getHostPort' );
+               $this->assertSame( 'a:b@example.com:8080', $uri->getAuthority(), 'Correct getAuthority' );
+               $this->assertSame( '/path?query=value', $uri->getRelativePath(), 'Correct getRelativePath' );
+               $this->assertSame( 'http://a:b@example.com:8080/path?query=value', $uri->toString(), 'Correct toString' );
+       }
+
+       /**
+        * Ensure that Uri's extend method properly overrides keys
+        */
+       function testExtend() {
+               $uri = new Uri( 'http://example.org/?a=b&hello=world' );
+               $uri->extendQuery( 'a=c&foo=bar' );
+               $this->assertSame( 'a=c&hello=world&foo=bar', $uri->getQueryString() );
+       }
+}
index 2949a3a..49b27af 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,16 +66,89 @@ 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() {
+               $this->hideDeprecated( "WikiPage::doEdit" );
+               $this->hideDeprecated( "WikiPage::getText" );
+               $this->hideDeprecated( "Revision::getText" );
+
                $title = Title::newFromText( "WikiPageTest_testDoEdit" );
 
                $page = $this->newPage( $title );
@@ -76,13 +156,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 );
 
@@ -113,6 +203,8 @@ class WikiPageTest extends MediaWikiLangTestCase {
        public function testDoQuickEdit() {
                global $wgUser;
 
+               $this->hideDeprecated( "WikiPage::doQuickEdit" );
+
                $page = $this->createPage( "WikiPageTest_testDoQuickEdit", "original text" );
 
                $text = "quick text";
@@ -123,13 +215,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,10 +279,25 @@ 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() {
+               $this->hideDeprecated( "WikiPage::getText" );
+
                $page = $this->newPage( "WikiPageTest_testGetText" );
 
                $text = $page->getText();
@@ -188,6 +311,8 @@ class WikiPageTest extends MediaWikiLangTestCase {
        }
 
        public function testGetRawText() {
+               $this->hideDeprecated( "WikiPage::getRawText" );
+
                $page = $this->newPage( "WikiPageTest_testGetRawText" );
 
                $text = $page->getRawText();
@@ -200,7 +325,32 @@ class WikiPageTest extends MediaWikiLangTestCase {
                $this->assertEquals( "some text", $text );
        }
 
-       
+       public function testGetContentModel() {
+               global $wgContentHandlerUseDB;
+
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+
+               $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
+       }
+
+       public function testGetContentHandler() {
+               global $wgContentHandlerUseDB;
+
+               if ( !$wgContentHandlerUseDB ) {
+                       $this->markTestSkipped( '$wgContentHandlerUseDB is disabled' );
+               }
+
+               $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
+
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
+       }
+
        public function testExists() {
                $page = $this->newPage( "WikiPageTest_testExists" );
                $this->assertFalse( $page->exists() );
@@ -259,6 +409,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 +525,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\"" );
@@ -448,7 +606,9 @@ more stuff
                               "2",
                               "== TEST ==\nmore fun",
                               null,
-                              trim( preg_replace( '/^== test ==.*== foo ==/sm', "== TEST ==\nmore fun\n\n== foo ==", WikiPageTest::$sections ) )
+                              trim( preg_replace( '/^== test ==.*== foo ==/sm',
+                                                  "== TEST ==\nmore fun\n\n== foo ==",
+                                                  WikiPageTest::$sections ) )
                        ),
                        array( 'WikiPageTest_testReplaceSection',
                               WikiPageTest::$sections,
@@ -471,6 +631,8 @@ more stuff
         * @dataProvider dataReplaceSection
         */
        public function testReplaceSection( $title, $text, $section, $with, $sectionTitle, $expected ) {
+               $this->hideDeprecated( "WikiPage::replaceSection" );
+
                $page = $this->createPage( $title, $text );
                $text = $page->replaceSection( $section, $with, $sectionTitle );
                $text = trim( $text );
@@ -478,6 +640,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() ) );
+       }
+       
        /* @todo FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
@@ -538,19 +712,22 @@ 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.
@@ -577,8 +754,9 @@ 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( $rev2->getSha1(), $page->getRevision()->getSha1(),
+                                                               "rollback did not revert to the correct revision" );
+               $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
 
        /**
@@ -590,14 +768,16 @@ 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...
@@ -609,8 +789,9 @@ 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( $rev1->getSha1(), $page->getRevision()->getSha1(),
+                                                       "rollback did not revert to the correct revision" );
+               $this->assertEquals( "one", $page->getContent()->getNativeData() );
        }
 
        public function dataGetAutosummary( ) {
@@ -658,11 +839,14 @@ more stuff
         * @dataProvider dataGetAutoSummary
         */
        public function testGetAutosummary( $old, $new, $flags, $expected ) {
+               $this->hideDeprecated( "WikiPage::getAutosummary" );
+
                $page = $this->newPage( "WikiPageTest_testGetAutosummary" );
 
                $summary = $page->getAutosummary( $old, $new, $flags );
 
-               $this->assertTrue( (bool)preg_match( $expected, $summary ), "Autosummary didn't match expected pattern $expected: $summary" );
+               $this->assertTrue( (bool)preg_match( $expected, $summary ),
+                                                       "Autosummary didn't match expected pattern $expected: $summary" );
        }
 
        public function dataGetAutoDeleteReason( ) {
@@ -739,7 +923,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;
                }
@@ -747,9 +933,11 @@ more stuff
                $reason = $page->getAutoDeleteReason( $hasHistory );
 
                if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) $this->assertEquals( $expectedResult, $reason );
-               else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ), "Autosummary didn't match expected pattern $expectedResult: $reason" );
+               else $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
+                                                               "Autosummary didn't match expected pattern $expectedResult: $reason" );
 
-               $this->assertEquals( $expectedHistory, $hasHistory, "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
+               $this->assertEquals( $expectedHistory, $hasHistory,
+                                                       "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
 
                $page->doDeleteArticle( "done" );
        }
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..7a7ba91
--- /dev/null
@@ -0,0 +1,552 @@
+<?php
+
+/**
+ * @group ContentHandler
+ *
+ * @group Database
+ *        ^--- needed, because we do need the database to test link updates
+ */
+class WikitextContentTest extends MediaWikiTestCase {
+
+       public function setup() {
+               global $wgUser;
+
+               // anon user
+               $wgUser = new User();
+               $wgUser->setName( '127.0.0.1' );
+
+               $this->context = new RequestContext( new FauxRequest() );
+               $this->context->setTitle( Title::newFromText( "Test" ) );
+               $this->context->setUser( $wgUser );
+       }
+
+       public function newContent( $text ) {
+               return new WikitextContent( $text );
+       }
+
+
+       public function dataGetParserOutput() {
+               return array(
+                       array("WikitextContentTest_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 );
+               $content = ContentHandler::makeContent( $text, $title );
+
+               $po = $content->getParserOutput( $title );
+
+               $this->assertEquals( $expectedHtml, $po->getText() );
+               // @todo: assert more properties
+       }
+
+       public function dataGetSecondaryDataUpdates() {
+               return array(
+                       array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
+                               array( 'LinksUpdate' => array(  'mRecursive' => true,
+                                                               'mLinks' => array() ) )
+                       ),
+                       array("WikitextContentTest_testGetSecondaryDataUpdates_2", "hello [[world test 21344]]\n",
+                               array( 'LinksUpdate' => array(  'mRecursive' => true,
+                                                               'mLinks' => array( array( 'World_test_21344' => 0 ) ) ) )
+                       ),
+                       // @todo: more...?
+               );
+       }
+
+       /**
+        * @dataProvider dataGetSecondaryDataUpdates
+        * @group Database
+        */
+       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 = $content->getSecondaryDataUpdates( $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" );
+                       }
+               }
+       }
+
+
+       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 $wgContLang;
+
+               $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $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 $wgContLang;
+               $options = ParserOptions::newFromUserAndLang( $this->context->getUser(), $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
+        * @group Database
+        */
+       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 testMatchMagicWord( ) {
+               $mw = MagicWord::get( "staticredirect" );
+
+               $content = $this->newContent( "#REDIRECT [[FOO]]\n__STATICREDIRECT__" );
+               $this->assertTrue( $content->matchMagicWord( $mw ), "should have matched magic word" );
+
+               $content = $this->newContent( "#REDIRECT [[FOO]]" );
+               $this->assertFalse( $content->matchMagicWord( $mw ), "should not have matched magic word" );
+       }
+
+       public function testUpdateRedirect( ) {
+               $target = Title::newFromText( "testUpdateRedirect_target" );
+
+               // test with non-redirect page
+               $content = $this->newContent( "hello world." );
+               $newContent = $content->updateRedirect( $target );
+
+               $this->assertTrue( $content->equals( $newContent ), "content should be unchanged" );
+
+               // test with actual redirect
+               $content = $this->newContent( "#REDIRECT [[Someplace]]" );
+               $newContent = $content->updateRedirect( $target );
+
+               $this->assertFalse( $content->equals( $newContent ), "content should have changed" );
+               $this->assertTrue( $newContent->isRedirect(), "new content should be a redirect" );
+
+               $this->assertEquals( $target->getFullText(), $newContent->getRedirectTarget()->getFullText() );
+       }
+
+       # =================================================================================================================
+
+       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 ) );
+       }
+
+       public function dataGetDeletionUpdates() {
+               return array(
+                       array("WikitextContentTest_testGetSecondaryDataUpdates_1", "hello ''world''\n",
+                               array( 'LinksDeletionUpdate' => array( ) )
+                       ),
+                       array("WikitextContentTest_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 = $content->getDeletionUpdates( WikiPage::factory( $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" );
+                       }
+               }
+       }
+
+}
index 5297d6d..f02c6d7 100644 (file)
  */
 class ApiEditPageTest extends ApiTestCase {
 
-       function setUp() {
-               parent::setUp();
+       public function setup() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               parent::setup();
+
+               $wgExtraNamespaces[ 12312 ] = 'Dummy';
+               $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
+
+               $wgNamespaceContentModels[ 12312 ] = "testing";
+               $wgContentHandlers[ "testing" ] = 'DummyContentHandlerForTesting';
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
                $this->doLogin();
        }
 
+       public function teardown() {
+               global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+               unset( $wgExtraNamespaces[ 12312 ] );
+               unset( $wgExtraNamespaces[ 12313 ] );
+
+               unset( $wgNamespaceContentModels[ 12312 ] );
+               unset( $wgContentHandlers[ "testing" ] );
+
+               MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
+               $wgContLang->resetNamespaces(); # reset namespace cache
+
+               parent::teardown();
+       }
+
        function testEdit( ) {
                $name = 'ApiEditPageTest_testEdit';
 
@@ -25,7 +52,7 @@ class ApiEditPageTest extends ApiTestCase {
                                'text' => 'some text', ) );
                $apiResult = $apiResult[0];
 
-               # Validate API result data
+               // Validate API result data
                $this->assertArrayHasKey( 'edit', $apiResult );
                $this->assertArrayHasKey( 'result', $apiResult['edit'] );
                $this->assertEquals( 'Success', $apiResult['edit']['result'] );
@@ -66,6 +93,33 @@ class ApiEditPageTest extends ApiTestCase {
                );
        }
 
+       function testNonTextEdit( ) {
+               $name = 'Dummy:ApiEditPageTest_testNonTextEdit';
+               $data = serialize( 'some bla bla text' );
+
+               // -- test new page --------------------------------------------
+               $apiResult = $this->doApiRequestWithToken( array(
+                       'action' => 'edit',
+                       'title' => $name,
+                       'text' => $data, ) );
+               $apiResult = $apiResult[0];
+
+               // Validate API result data
+               $this->assertArrayHasKey( 'edit', $apiResult );
+               $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+               $this->assertEquals( 'Success', $apiResult['edit']['result'] );
+
+               $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+               $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+               $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+               // validate resulting revision
+               $page = WikiPage::factory( Title::newFromText( $name ) );
+               $this->assertEquals( "testing", $page->getContentModel() );
+               $this->assertEquals( $data, $page->getContent()->serialize() );
+       }
+
        function testEditAppend() {
                $this->markTestIncomplete( "not yet implemented" );
        }
index a2dc5c6..1364d8b 100644 (file)
@@ -1,6 +1,9 @@
 <?php
 
 /**
+ * @group medium
+ * ^---- causes phpunit to use a higher timeout threshold
+ * 
  * @group FileRepo
  * @group FileBackend
  * @group medium
index 957907c..4522e50 100644 (file)
@@ -94,7 +94,7 @@ class SearchEngineTest extends MediaWikiTestCase {
                LinkCache::singleton()->clear();
 
                $page = WikiPage::factory( $title );
-               $page->doEdit( $text, $comment, 0, false, $user );
+               $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
 
                $this->pageList[] = array( $title, $page->getId() );
 
old mode 100755 (executable)
new mode 100644 (file)
index d134438..aceedf9 100644 (file)
@@ -35,7 +35,7 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
         * @throws MWExcepion
         */
        protected function addRevision( Page $page, $text, $summary ) {
-               $status = $page->doEdit( $text, $summary );
+               $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary );
                if ( $status->isGood() ) {
                        $value = $status->getValue();
                        $revision = $value['revision'];
@@ -295,9 +295,12 @@ 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 String: the expected content model id (default: CONTENT_MODEL_WIKITEXT)
+        * @param $format String: the expected format model id (default: CONTENT_FORMAT_WIKITEXT)
         * @param $parentid int|false: (optional) id of the parent revision
         */
-       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) {
+       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false,
+                                               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) {
 
                $this->assertNodeStart( "revision" );
                $this->skipWhitespace();
@@ -315,9 +318,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,
@@ -344,9 +371,5 @@ abstract class DumpTestCase extends MediaWikiLangTestCase {
                        $this->assertNodeEnd( "text" );
                        $this->skipWhitespace();
                }
-
-               $this->assertNodeEnd( "revision" );
-               $this->skipWhitespace();
        }
-
 }
index 8ff8557..5178154 100644 (file)
@@ -199,6 +199,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP1Summary1</comment>
       <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
       <text xml:space="preserve">BackupDumperTestP1Text1</text>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
   </page>
 ';
@@ -216,6 +218,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP2Summary1</comment>
       <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
       <text xml:space="preserve">BackupDumperTestP2Text1</text>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
     <revision>
       <id>5</id>
@@ -227,6 +231,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       <comment>BackupDumperTestP2Summary4 extra</comment>
       <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
       <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
     </revision>
   </page>
 ';
@@ -243,6 +249,8 @@ class BaseDumpTest extends MediaWikiTestCase {
       </contributor>
       <comment>Talk BackupDumperTestP1 Summary1</comment>
       <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+      <model name="wikitext">1</model>
+      <format mime="text/x-wiki">1</format>
       <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
     </revision>
   </page>
index a0bbadf..b17284e 100644 (file)
@@ -383,7 +383,7 @@ class TextPassDumperTest extends DumpTestCase {
                $this->assertEmpty( $files, "Remaining unchecked files" );
 
                // ... and have dealt with more than one checkpoint file
-               $this->assertGreaterThan( 1, $checkpointFiles, "# of checkpoint files" );
+               $this->assertGreaterThan( 1, $checkpointFiles, "expected more than 1 checkpoint to have been created. Checkpoint interval is $checkpointAfter seconds, maybe your computer is too fast?" );
 
                $this->expectETAOutput();
        }
@@ -481,6 +481,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP1Summary1</comment>
       <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId1_1 . '" bytes="23" />
     </revision>
   </page>
@@ -497,6 +499,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary1</comment>
       <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId2_1 . '" bytes="23" />
     </revision>
     <revision>
@@ -508,6 +512,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary2</comment>
       <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId2_2 . '" bytes="23" />
     </revision>
     <revision>
@@ -519,6 +525,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary3</comment>
       <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId2_3 . '" bytes="23" />
     </revision>
     <revision>
@@ -530,6 +538,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>BackupDumperTestP2Summary4 extra</comment>
       <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId2_4 . '" bytes="44" />
     </revision>
   </page>
@@ -548,6 +558,8 @@ class TextPassDumperTest extends DumpTestCase {
       </contributor>
       <comment>Talk BackupDumperTestP1 Summary1</comment>
       <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
+      <model>wikitext</model>
+      <format>text/x-wiki</format>
       <text id="' . $this->textId4_1 . '" bytes="35" />
     </revision>
   </page>
index e7ffa01..fd6db0a 100644 (file)
@@ -111,7 +111,7 @@ class FetchTextTest extends MediaWikiTestCase {
         * @throws MWExcepion
         */
        private function addRevision( $page, $text, $summary ) {
-               $status = $page->doEdit( $text, $summary );
+               $status = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), $summary );
                if ( $status->isGood() ) {
                        $value = $status->getValue();
                        $revision = $value['revision'];
index f286fa1..f4ee500 100644 (file)
@@ -11,8 +11,8 @@
                 timeoutForSmallTests="2"
                 timeoutForMediumTests="10"
                 timeoutForLargeTests="60"
-         strict="true"
-                verbose="true">
+                strict="false"
+                verbose="false">
        <testsuites>
                <testsuite name="includes">
                        <directory>includes</directory>
index 2edc280..bdb24f9 100644 (file)
--- a/thumb.php
+++ b/thumb.php
@@ -71,10 +71,9 @@ function wfThumbHandle404() {
        }
        # Just get the URI path (REDIRECT_URL/REQUEST_URI is either a full URL or a path)
        if ( substr( $uriPath, 0, 1 ) !== '/' ) {
-               $bits = wfParseUrl( $uriPath );
-               if ( $bits && isset( $bits['path'] ) ) {
-                       $uriPath = $bits['path'];
-               } else {
+               $uri = new Uri( $uriPath );
+               $uriPath = $uri->getPath();
+               if ( $uriPath === null ) {
                        wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
                        return;
                }