Merge branch 'master' into Wikidata
authorJens Ohlig <jens.ohlig@wikimedia.de>
Wed, 11 Apr 2012 12:24:29 +0000 (14:24 +0200)
committerJens Ohlig <jens.ohlig@wikimedia.de>
Wed, 11 Apr 2012 12:24:29 +0000 (14:24 +0200)
Conflicts:
.gitreview
includes/Article.php
includes/AutoLoader.php
includes/EditPage.php
includes/LinksUpdate.php
includes/WikiPage.php
includes/installer/Ibm_db2Updater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/SqliteUpdater.php
maintenance/refreshLinks.php

45 files changed:
.gitreview
includes/Article.php
includes/AutoLoader.php
includes/Content.php [new file with mode: 0644]
includes/ContentHandler.php [new file with mode: 0644]
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/FeedUtils.php
includes/ImagePage.php
includes/LinksUpdate.php
includes/Revision.php
includes/SecondaryDBDataUpdate.php [new file with mode: 0644]
includes/SecondaryDataUpdate.php [new file with mode: 0644]
includes/Title.php
includes/WikiPage.php
includes/actions/EditAction.php
includes/actions/RawAction.php
includes/actions/RollbackAction.php
includes/api/ApiComparePages.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiParse.php
includes/api/ApiPurge.php
includes/api/ApiQueryRevisions.php
includes/diff/DairikiDiff.php
includes/diff/DifferenceEngine.php
includes/installer/Ibm_db2Updater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/SqliteUpdater.php
includes/job/RefreshLinksJob.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/specials/SpecialComparePages.php
includes/specials/SpecialUndelete.php
languages/messages/MessagesEn.php
maintenance/archives/patch-archive-ar_content_format.sql [new file with mode: 0644]
maintenance/archives/patch-archive-ar_content_model.sql [new file with mode: 0644]
maintenance/archives/patch-page-page_content_model.sql [new file with mode: 0644]
maintenance/archives/patch-revision-rev_content_format.sql [new file with mode: 0644]
maintenance/archives/patch-revision-rev_content_model.sql [new file with mode: 0644]
maintenance/populateRevisionLength.php
maintenance/refreshLinks.php
maintenance/tables.sql

index f6438d5..7e1473a 100644 (file)
@@ -2,4 +2,4 @@
 host=gerrit.wikimedia.org
 port=29418
 project=mediawiki/core.git
-defaultbranch=master
+defaultbranch=Wikidata
index 393f770..d516ce9 100644 (file)
@@ -37,7 +37,13 @@ class Article extends Page {
         */
        public $mParserOptions;
 
-       var $mContent;                    // !<
+       var $mContent;                    // !< #BC cruft
+
+       /**
+        * @var Content
+        */
+       var $mContentObject;
+
        var $mContentLoaded = false;      // !<
        var $mOldId;                      // !<
 
@@ -112,13 +118,14 @@ class Article extends Page {
                if ( !$page ) {
                        switch( $title->getNamespace() ) {
                                case NS_FILE:
-                                       $page = new ImagePage( $title );
+                                       $page = new ImagePage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                case NS_CATEGORY:
-                                       $page = new CategoryPage( $title );
+                                       $page = new CategoryPage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                default:
-                                       $page = new Article( $title );
+                                       $handler = ContentHandler::getForTitle( $title );
+                                       $page = $handler->createArticle( $title );
                        }
                }
                $page->setContext( $context );
@@ -188,9 +195,27 @@ class Article extends Page {
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
-        * @return string Return the text of this revision
+        * @deprecated in 1.20; use getContentObject() instead
+        *
+        * @return string The text of this revision
         */
        public function getContent() {
+               wfDeprecated( __METHOD__, '1.20' );
+               $content = $this->getContentObject();
+               return ContentHandler::getContentText( $content );
+       }
+
+       /**
+        * 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
+        */
+   public function getContentObject() {
                global $wgUser;
 
                wfProfileIn( __METHOD__ );
@@ -203,17 +228,19 @@ class Article extends Page {
                                if ( $text === false ) {
                                        $text = '';
                                }
+
+                               $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
-                               $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
+                               $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
 
-                       return $text;
+                       return $content;
                } else {
-                       $this->fetchContent();
+                       $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
 
-                       return $this->mContent;
+                       return $this->mContentObject;
                }
        }
 
@@ -296,15 +323,44 @@ class Article extends Page {
         * Does *NOT* follow redirects.
         *
         * @return mixed string containing article contents, or false if null
+        * @deprecated in 1.20, use getContentObject() instead
         */
-       function fetchContent() {
-               if ( $this->mContentLoaded ) {
+       protected function fetchContent() { #BC cruft!
+               wfDeprecated( __METHOD__, '1.20' );
+
+               if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
 
                wfProfileIn( __METHOD__ );
 
+               $content = $this->fetchContentObject();
+
+               $this->mContent = ContentHandler::getContentText( $content ); #FIXME: get rid of mContent everywhere!
+               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft!
+
+               wfProfileOut( __METHOD__ );
+
+               return $this->mContent;
+       }
+
+
+       /**
+        * Get text content object
+        * Does *NOT* follow redirects.
+        * TODO: when is this null?
+        *
+        * @return Content|null
+        */
+       protected function fetchContentObject() {
+               if ( $this->mContentLoaded ) {
+                       return $this->mContentObject;
+               }
+
+               wfProfileIn( __METHOD__ );
+
                $this->mContentLoaded = true;
+               $this->mContent = null;
 
                $oldid = $this->getOldID();
 
@@ -312,7 +368,7 @@ class Article extends Page {
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
-               $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
+               $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ;
 
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
@@ -332,6 +388,7 @@ class Article extends Page {
                        }
 
                        $this->mRevision = $this->mPage->getRevision();
+
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
@@ -341,14 +398,14 @@ class Article extends Page {
 
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
-               $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
+               $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
 
-               wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
+               wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) ); #FIXME: register new hook
 
                wfProfileOut( __METHOD__ );
 
-               return $this->mContent;
+               return $this->mContentObject;
        }
 
        /**
@@ -381,7 +438,7 @@ class Article extends Page {
         * @return Revision|null
         */
        public function getRevisionFetched() {
-               $this->fetchContent();
+               $this->fetchContentObject();
 
                return $this->mRevision;
        }
@@ -540,7 +597,7 @@ class Article extends Page {
                                        break;
                                case 3:
                                        # This will set $this->mRevision if needed
-                                       $this->fetchContent();
+                                       $this->fetchContentObject();
 
                                        # Are we looking at an old revision
                                        if ( $oldid && $this->mRevision ) {
@@ -564,18 +621,21 @@ class Article extends Page {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
-                                       } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
+                                       } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) { #FIXME: document new hook!
+                                               # Allow extensions do their own custom view for certain pages
+                                               $outputDone = true;
+                                       } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated! #FIXME: deprecate hook!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
-                                               $text = $this->getContent();
-                                               $rt = Title::newFromRedirectArray( $text );
+                                               $content = $this->getContentObject();
+                                               $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $wgOut->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
-                                                       $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
+                                                       $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions );
                                                        $wgOut->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
@@ -586,7 +646,7 @@ class Article extends Page {
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
 
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
-                                               $this->getRevIdFetched(), $useParserCache, $this->getContent() );
+                                               $this->getRevIdFetched(), $useParserCache, $this->getContentObject() );
 
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
@@ -680,7 +740,9 @@ class Article extends Page {
                $unhide = $wgRequest->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
 
-               $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+               $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+               $de = $contentHandler->getDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
+
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
@@ -698,23 +760,21 @@ class Article extends Page {
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
-       protected function showCssOrJsPage() {
+       protected function showCssOrJsPage( $showCacheHint = true ) {
                global $wgOut;
 
-               $dir = $this->getContext()->getLanguage()->getDir();
-               $lang = $this->getContext()->getLanguage()->getCode();
+               if ( $showCacheHint ) {
+                       $dir = $this->getContext()->getLanguage()->getDir();
+                       $lang = $this->getContext()->getLanguage()->getCode();
 
-               $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
-                       'clearyourcache' );
+                       $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
+                               'clearyourcache' );
+               }
 
                // Give hooks a chance to customise the output
-               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $wgOut ) ) ) {
-                       // Wrap the whole lot in a <pre> and don't parse
-                       $m = array();
-                       preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
-                       $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                       $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
-                       $wgOut->addHTML( "\n</pre>\n" );
+               if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated #FIXME: hook is deprecated
+                       $po = $this->mContentObject->getParserOutput();
+                       $wgOut->addHTML( $po->getText() );
                }
        }
 
@@ -1349,7 +1409,13 @@ class Article extends Page {
                // Generate deletion reason
                $hasHistory = false;
                if ( !$reason ) {
-                       $reason = $this->generateReason( $hasHistory );
+                       try {
+                               $reason = $this->generateReason( $hasHistory );
+                       } catch (MWException $e) {
+                               # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
+                               wfDebug("Error while building auto delete summary: $e");
+                               $reason = '';
+                       }
                }
 
                // If the page has a history, insert a warning
@@ -1866,7 +1932,9 @@ class Article extends Page {
         * @return mixed
         */
        public function generateReason( &$hasHistory ) {
-               return $this->mPage->getAutoDeleteReason( $hasHistory );
+               $title = $this->mPage->getTitle();
+               $handler = ContentHandler::getForTitle( $title );
+               return $handler->getAutoDeleteReason( $title, $hasHistory );
        }
 
        // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
@@ -1904,6 +1972,7 @@ class Article extends Page {
         * @param $newtext
         * @param $flags
         * @return string
+        * @deprecated since 1.20, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
index a1bbc3c..02ad3db 100644 (file)
@@ -195,6 +195,8 @@ $wgAutoloadLocalClasses = array(
        'RevisionList' => 'includes/RevisionList.php',
        'RSSFeed' => 'includes/Feed.php',
        'Sanitizer' => 'includes/Sanitizer.php',
+    'SecondaryDataUpdate' => 'includes/SecondaryDataUpdate.php',
+    'SecondaryDBDataUpdate' => 'includes/SecondaryDBDataUpdate.php',
        'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
        'SiteConfiguration' => 'includes/SiteConfiguration.php',
        'SiteStats' => 'includes/SiteStats.php',
@@ -259,6 +261,18 @@ $wgAutoloadLocalClasses = array(
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
 
+    # content handler
+    'Content' => 'includes/Content.php',
+    'ContentHandler' => 'includes/ContentHandler.php',
+    'CssContent' => 'includes/Content.php',
+    'CssContentHandler' => 'includes/ContentHandler.php',
+    'JavaScriptContent' => 'includes/Content.php',
+    'JavaScriptContentHandler' => 'includes/ContentHandler.php',
+    'MessageContent' => 'includes/Content.php',
+    'TextContent' => 'includes/Content.php',
+    'WikitextContent' => 'includes/Content.php',
+    'WikitextContentHandler' => 'includes/ContentHandler.php',
+
        # includes/actions
        'CreditsAction' => 'includes/actions/CreditsAction.php',
        'DeleteAction' => 'includes/actions/DeleteAction.php',
diff --git a/includes/Content.php b/includes/Content.php
new file mode 100644 (file)
index 0000000..913eb06
--- /dev/null
@@ -0,0 +1,612 @@
+<?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.
+ * Content objects are imutable.
+ *
+ */
+abstract class Content {
+    
+    public function __construct( $modelName = null ) {
+        $this->mModelName = $modelName;
+    }
+
+    public function getModelName() {
+        return $this->mModelName;
+    }
+
+    protected function checkModelName( $modelName ) {
+        if ( $modelName !== $this->mModelName ) {
+            throw new MWException( "Bad content model: expected " . $this->mModelName . " but got found " . $modelName );
+        }
+    }
+
+    public function getContentHandler() {
+        return ContentHandler::getForContent( $this );
+    }
+
+    public function getDefaultFormat() {
+        return $this->getContentHandler()->getDefaultFormat();
+    }
+
+    public function getSupportedFormats() {
+        return $this->getContentHandler()->getSupportedFormats();
+    }
+
+    public function isSupportedFormat( $format ) {
+        if ( !$format ) return true; # this means "use the default"
+
+        return $this->getContentHandler()->isSupportedFormat( $format );
+    }
+
+    protected function checkFormat( $format ) {
+        if ( !$this->isSupportedFormat( $format ) ) {
+            throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
+        }
+    }
+
+    public function serialize( $format = null ) {
+        return $this->getContentHandler()->serialize( $this, $format );
+    }
+
+    /**
+     * @return String a string representing the content in a way useful for building a full text search index.
+     *         If no useful representation exists, this method returns an empty string.
+     */
+    public abstract function getTextForSearchIndex( );
+
+    /**
+     * @return String the wikitext to include when another page includes this  content, or false if the content is not
+     *         includable in a wikitext page.
+     */
+    #TODO: allow native handling, bypassing wikitext representation, like for includable special pages.
+    public abstract function getWikitextForTransclusion( ); #FIXME: use in parser, etc!
+
+    /**
+     * Returns a textual representation of the content suitable for use in edit summaries and log messages.
+     *
+     * @param int $maxlength maximum length of the summary text
+     * @return String the summary text
+     */
+    public abstract function getTextForSummary( $maxlength = 250 );
+
+    /**
+     * Returns native represenation of the data. Interpretation depends on the data model used,
+     * as given by getDataModel().
+     *
+     * @return mixed the native representation of the content. Could be a string, a nested array
+     *         structure, an object, a binary blob... anything, really.
+     */
+    public abstract function getNativeData( ); #FIXME: review all calls carefully, caller must be aware of content model!
+
+    /**
+     * returns the content's nominal size in bogo-bytes.
+     *
+     * @return int
+     */
+    public abstract function getSize( );
+
+    public function isEmpty() {
+        return $this->getSize() == 0;
+    }
+
+    public function equals( Content $that ) {
+        if ( empty( $that ) ) return false;
+        if ( $that === $this ) return true;
+        if ( $that->getModelName() !== $this->getModelName() ) return false;
+
+        return $this->getNativeData() == $that->getNativeData();
+    }
+
+    /**
+     * 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.
+     */
+    public abstract function isCountable( $hasLinks = null ) ;
+
+    /**
+     * @param null|Title $title
+     * @param null $revId
+     * @param null|ParserOptions $options
+     * @return ParserOutput
+     */
+    public abstract function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = 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).
+     *
+     * @return Array of Titles, with the destination last
+     */
+    public function getRedirectChain() {
+        return null;
+    }
+
+    /**
+     * Construct the redirect destination from this content and return an
+     * array of Titles, or null if this content doesn't represent a redirect.
+     * This will only return the immediate redirect target, useful for
+     * the redirect table and other checks that don't need full recursion.
+     *
+     * @return Title: The corresponding Title
+     */
+    public function getRedirectTarget() {
+        return null;
+    }
+
+    /**
+     * Construct the redirect destination from this content and return the
+     * Title, or null if this content doesn't represent a redirect.
+     * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
+     * in order to provide (hopefully) the Title of the final destination instead of another redirect.
+     *
+     * @return Title
+     */
+    public function getUltimateRedirectTarget() {
+        return null;
+    }
+
+    public function isRedirect() {
+        return $this->getRedirectTarget() != null;
+    }
+
+    /**
+     * Returns the section with the given id.
+     *
+     * The default implementation returns null.
+     *
+     * @param String $sectionId the section's id
+     * @return Content|Boolean|null the section, or false if no such section exist, or null if sections are not supported
+     */
+    public function getSection( $sectionId ) {
+        return null;
+    }
+
+    /**
+     * Replaces a section of the content and returns a Content object with the section replaced.
+     *
+     * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
+     * @param $with Content: new content of the section
+     * @param $sectionTitle String: new section's subject, only if $section is 'new'
+     * @return string Complete article text, or null if error
+     */
+    public function replaceSection( $section, Content $with, $sectionTitle = ''  ) {
+        return $this;
+    }
+
+    /**
+     * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+     *
+     * @param Title $title
+     * @param User $user
+     * @param null|ParserOptions $popts
+     * @return Content
+     */
+    public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
+        return $this;
+    }
+
+    /**
+     * Returns a new WikitextContent object with the given section heading prepended, if supported.
+     * The default implementation just returns this Content object unmodified, ignoring the section header.
+     *
+     * @param $header String
+     * @return Content
+     */
+    public function addSectionHeader( $header ) {
+        return $this;
+    }
+
+    /**
+     * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+     *
+     * @param Title $title
+     * @param null|ParserOptions $popts
+     * @return Content
+     */
+    public function preloadTransform( Title $title, ParserOptions $popts = null ) {
+        return $this;
+    }
+
+    # TODO: minimize special cases for CSS/JS; how to handle extra message for JS/CSS previews??
+    # TODO: handle ImagePage and CategoryPage
+    # TODO: hook into dump generation to serialize and record model and format!
+
+    # TODO: make sure we cover lucene search / wikisearch.
+    # TODO: make sure ReplaceTemplates 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
+
+    # TODO: make sure we cover the external editor interface (does anyone actually use that?!)
+
+    # TODO: tie into API to provide contentModel for Revisions
+    # TODO: tie into API to provide serialized version and contentFormat for Revisions
+    # TODO: tie into API edit interface
+    # TODO: make EditForm plugin for EditPage
+
+    # XXX: isCacheable( ) # can/should we do this here?
+}
+
+/**
+ * Content object implementation for representing flat text. The
+ */
+abstract class TextContent extends Content {
+    public function __construct( $text, $modelName = null ) {
+        parent::__construct($modelName);
+
+        $this->mText = $text;
+    }
+
+    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 content's nominal size in bogo-bytes.
+     */
+    public function getSize( ) { #FIXME: use! replace strlen in WikiPage.
+        $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.
+     */
+    public function isCountable( $hasLinks = null ) {
+        global $wgArticleCountMethod;
+
+        if ( $this->isRedirect( ) ) {
+            return false;
+        }
+
+        if (  $wgArticleCountMethod === 'any' ) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the text represented by this Content object, as a string.
+     *
+     * @return String the raw text
+     */
+    public function getNativeData( ) {
+        $text = $this->mText;
+        return $text;
+    }
+
+    /**
+     * Returns the text represented by this Content object, as a string.
+     *
+     * @return String the raw text
+     */
+    public function getTextForSearchIndex( ) { #FIXME: use!
+        return $this->getNativeData();
+    }
+
+    /**
+     * Returns the text represented by this Content object, as a string.
+     *
+     * @return String the raw text
+     */
+    public function getWikitextForTransclusion( ) { #FIXME: use!
+        return $this->getNativeData();
+    }
+
+    /**
+     * Returns a generic ParserOutput object, wrapping the HTML returned by getHtml().
+     *
+     * @return ParserOutput representing the HTML form of the text
+     */
+    public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
+        # generic implementation, relying on $this->getHtml()
+
+        $html = $this->getHtml( $options );
+        $po = new ParserOutput( $html );
+
+        return $po;
+    }
+
+    protected abstract function getHtml( );
+
+}
+
+class WikitextContent extends TextContent {
+    public function __construct( $text ) {
+        parent::__construct($text, CONTENT_MODEL_WIKITEXT);
+
+        $this->mDefaultParserOptions = null; #TODO: use per-class static member?!
+    }
+
+    protected function getHtml( ) {
+        throw new MWException( "getHtml() not implemented for wikitext. Use getParserOutput()->getText()." );
+    }
+
+    public function getDefaultParserOptions() {
+        global $wgUser, $wgContLang;
+
+        if ( !$this->mDefaultParserOptions ) { #TODO: use per-class static member?!
+            $this->mDefaultParserOptions = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+        }
+
+        return $this->mDefaultParserOptions;
+    }
+
+    /**
+     * Returns a ParserOutput object reesulting from parsing the content's text using $wgParser
+     *
+     * @return ParserOutput representing the HTML form of the text
+     */
+    public function getParserOutput( Title $title = null, $revId = null, ParserOptions $options = null ) {
+        global $wgParser;
+
+        if ( !$options ) {
+            $options = $this->getDefaultParserOptions();
+        }
+
+        $po = $wgParser->parse( $this->mText, $title, $options, true, true, $revId );
+
+        return $po;
+    }
+
+    /**
+     * Returns the section with the given id.
+     *
+     * @param String $sectionId the section's id
+     * @return Content|false|null the section, or false if no such section exist, or null if sections are not supported
+     */
+    public function getSection( $section ) {
+        global $wgParser;
+
+        $text = $this->getNativeData();
+        $sect = $wgParser->getSection( $text, $section, false );
+
+        return  new WikitextContent( $sect );
+    }
+
+    /**
+     * Replaces a section in the wikitext
+     *
+     * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...), or "new"
+     * @param $with Content: new content of the section
+     * @param $sectionTitle String: new section's subject, only if $section is 'new'
+     * @return string Complete article text, or null if error
+     */
+    public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
+        global $wgParser;
+
+        wfProfileIn( __METHOD__ );
+
+        $myModelName = $this->getModelName();
+        $sectionModelName = $with->getModelName();
+
+        if ( $sectionModelName != $myModelName  ) {
+            throw new MWException( "Incompatible content model for section: document uses $myModelName, section uses $sectionModelName." );
+        }
+
+        $oldtext = $this->getNativeData();
+        $text = $with->getNativeData();
+
+        if ( $section == 'new' ) {
+            # Inserting a new section
+            $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
+            if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
+                $text = strlen( trim( $oldtext ) ) > 0
+                    ? "{$oldtext}\n\n{$subject}{$text}"
+                    : "{$subject}{$text}";
+            }
+        } else {
+            # Replacing an existing section; roll out the big guns
+            global $wgParser;
+
+            $text = $wgParser->replaceSection( $oldtext, $section, $text );
+        }
+
+        $newContent = new WikitextContent( $text );
+
+        wfProfileOut( __METHOD__ );
+        return $newContent;
+    }
+
+    /**
+     * Returns a new WikitextContent object with the given section heading prepended.
+     *
+     * @param $header String
+     * @return Content
+     */
+    public function addSectionHeader( $header ) {
+        $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $this->getNativeData();
+
+        return new WikitextContent( $text );
+    }
+
+    /**
+     * Returns a Content object with pre-save transformations applied (or this object if no transformations apply).
+     *
+     * @param Title $title
+     * @param User $user
+     * @param null|ParserOptions $popts
+     * @return Content
+     */
+    public function preSaveTransform( Title $title, User $user, ParserOptions $popts = null ) {
+        global $wgParser;
+
+        if ( $popts == null ) $popts = $this->getDefaultParserOptions();
+
+        $text = $this->getNativeData();
+        $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+        return new WikitextContent( $pst );
+    }
+
+    /**
+     * Returns a Content object with preload transformations applied (or this object if no transformations apply).
+     *
+     * @param Title $title
+     * @param null|ParserOptions $popts
+     * @return Content
+     */
+    public function preloadTransform( Title $title, ParserOptions $popts = null ) {
+        global $wgParser;
+
+        if ( $popts == null ) $popts = $this->getDefaultParserOptions();
+
+        $text = $this->getNativeData();
+        $plt = $wgParser->getPreloadText( $text, $title, $popts );
+
+        return new WikitextContent( $plt );
+    }
+
+    public function getRedirectChain() {
+        $text = $this->getNativeData();
+        return Title::newFromRedirectArray( $text );
+    }
+
+    public function getRedirectTarget() {
+        $text = $this->getNativeData();
+        return Title::newFromRedirect( $text );
+    }
+
+    public function getUltimateRedirectTarget() {
+        $text = $this->getNativeData();
+        return Title::newFromRedirectRecurse( $text );
+    }
+
+    /**
+     * Returns true if this content is not a redirect, and this content's text is countable according to
+     * the criteria defiend by $wgArticleCountMethod.
+     *
+     * @param $hasLinks Bool: if it is known whether this content contains links, provide this information here,
+     *                        to avoid redundant parsing to find out.
+     */
+    public function isCountable( $hasLinks = null ) {
+        global $wgArticleCountMethod;
+
+        if ( $this->isRedirect( ) ) {
+            return false;
+        }
+
+        $text = $this->getNativeData();
+
+        switch ( $wgArticleCountMethod ) {
+            case 'any':
+                return true;
+            case 'comma':
+                if ( $text === false ) {
+                    $text = $this->getRawText();
+                }
+                return strpos( $text,  ',' ) !== false;
+            case 'link':
+                if ( $hasLinks === null ) { # not know, find out
+                    $po = $this->getParserOutput();
+                    $links = $po->getLinks();
+                    $hasLinks = !empty( $links );
+                }
+
+                return $hasLinks;
+        }
+    }
+
+    public function getTextForSummary( $maxlength = 250 ) {
+        $truncatedtext = parent::getTextForSummary( $maxlength );
+
+        #clean up unfinished links
+        #XXX: make this optional? wasn't there in autosummary, but required for deletion summary.
+        $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
+
+        return $truncatedtext;
+    }
+
+}
+
+class MessageContent extends TextContent {
+    public function __construct( $msg_key, $params = null, $options = null ) {
+        parent::__construct(null, CONTENT_MODEL_WIKITEXT); #XXX: messages may be wikitext, html or plain text! and maybe even something else entirely.
+
+        $this->mMessageKey = $msg_key;
+
+        $this->mParameters = $params;
+
+        if ( !$options ) $options = array();
+        $this->mOptions = $options;
+
+        $this->mHtmlOptions = null;
+    }
+
+    /**
+     * Returns the message as rendered HTML, using the options supplied to the constructor plus "parse".
+     */
+    protected function getHtml(  ) {
+        $opt = array_merge( $this->mOptions, array('parse') );
+
+        return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
+    }
+
+
+    /**
+     * Returns the message as raw text, using the options supplied to the constructor minus "parse" and "parseinline".
+     */
+    public function getNativeData( ) {
+        $opt = array_diff( $this->mOptions, array('parse', 'parseinline') );
+
+        return wfMsgExt( $this->mMessageKey, $this->mParameters, $opt );
+    }
+
+}
+
+
+class JavaScriptContent extends TextContent {
+    public function __construct( $text ) {
+        parent::__construct($text, CONTENT_MODEL_JAVASCRIPT);
+    }
+
+    protected function getHtml( ) {
+        $html = "";
+        $html .= "<pre class=\"mw-code mw-js\" dir=\"ltr\">\n";
+        $html .= htmlspecialchars( $this->getNativeData() );
+        $html .= "\n</pre>\n";
+
+        return $html;
+    }
+
+}
+
+class CssContent extends TextContent {
+    public function __construct( $text ) {
+        parent::__construct($text, CONTENT_MODEL_CSS);
+    }
+
+    protected function getHtml( ) {
+        $html = "";
+        $html .= "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n";
+        $html .= htmlspecialchars( $this->getNativeData() );
+        $html .= "\n</pre>\n";
+
+        return $html;
+    }
+}
+
+#FUTURE: special type for redirects?!
+#FUTURE: MultipartMultipart < WikipageContent (Main + Links + X)
+#FUTURE: LinksContent < LanguageLinksContent, CategoriesContent
+#EXAMPLE: CoordinatesContent
+#EXAMPLE: WikidataContent
diff --git a/includes/ContentHandler.php b/includes/ContentHandler.php
new file mode 100644 (file)
index 0000000..699e2fd
--- /dev/null
@@ -0,0 +1,566 @@
+<?php
+
+class MWContentSerializationException extends MWException {
+
+}
+
+
+/**
+ * A content handler knows how do deal with a specific type of content on a wiki page.
+ * Content is stored in the database in a serialized form (using a serialization format aka mime type)
+ * and is be unserialized into it's native PHP represenation (the content model).
+ * 
+ * Some content types have a flat model, that is, their native represenation is the
+ * same as their serialized form. Examples would be JavaScript and CSS code. As of now,
+ * this also applies to wikitext (mediawiki's default content type), but wikitext
+ * content may be represented by a DOM or AST structure in the future.
+ *  
+ */
+abstract class ContentHandler {
+
+    public static function getContentText( Content $content = null ) {
+        global $wgContentHandlerTextFallback;
+
+        if ( is_null( $content ) ) {
+                       return '';
+               }
+
+        if ( $content instanceof TextContent ) {
+            return $content->getNativeData();
+        }
+
+        if ( $wgContentHandlerTextFallback == 'fail' ) {
+                       throw new MWException( "Attempt to get text from Content with model " . $content->getModelName() );
+               }
+
+        if ( $wgContentHandlerTextFallback == 'serialize' ) {
+                       return $content->serialize();
+               }
+
+        return null;
+    }
+
+    public static function makeContent( $text, Title $title, $modelName = null, $format = null ) {
+
+        if ( is_null( $modelName ) ) {
+            $modelName = $title->getContentModelName();
+        }
+
+        $handler = ContentHandler::getForModelName( $modelName );
+        return $handler->unserialize( $text, $format );
+    }
+
+    public static function getDefaultModelFor( Title $title ) {
+        global $wgNamespaceContentModels;
+
+        // NOTE: this method must not rely on $title->getContentModelName() directly or indirectly,
+        //       because it is used to initialized the mContentModelName memebr.
+
+        $ns = $title->getNamespace();
+
+        $ext = false;
+        $m = null;
+        $model = null;
+
+        if ( !empty( $wgNamespaceContentModels[ $ns ] ) ) {
+            $model = $wgNamespaceContentModels[ $ns ];
+        }
+
+        // hook can determin default model
+        if ( !wfRunHooks( 'DefaultModelFor', array( $title, &$model ) ) ) { #FIXME: document new hook!
+            if ( !is_null( $model ) ) {
+                               return $model;
+                       }
+        }
+
+        // Could this page contain custom CSS or JavaScript, based on the title?
+        $isCssOrJsPage = NS_MEDIAWIKI == $ns && preg_match( '!\.(css|js)$!u', $title->getText(), $m );
+        if ( $isCssOrJsPage ) {
+                       $ext = $m[1];
+               }
+
+        // hook can force js/css
+        wfRunHooks( 'TitleIsCssOrJsPage', array( $title, &$isCssOrJsPage ) );
+
+        // Is this a .css subpage of a user page?
+        $isJsCssSubpage = NS_USER == $ns && !$isCssOrJsPage && preg_match( "/\\/.*\\.(js|css)$/", $title->getText(), $m );
+        if ( $isJsCssSubpage ) {
+                       $ext = $m[1];
+               }
+
+        // is this wikitext, according to $wgNamespaceContentModels or the DefaultModelFor hook?
+        $isWikitext = is_null( $model ) || $model == CONTENT_MODEL_WIKITEXT;
+        $isWikitext = $isWikitext && !$isCssOrJsPage && !$isJsCssSubpage;
+
+        // hook can override $isWikitext
+        wfRunHooks( 'TitleIsWikitextPage', array( $title, &$isWikitext ) );
+
+        if ( !$isWikitext ) {
+                       switch ( $ext ) {
+                               case 'js':
+                                       return CONTENT_MODEL_JAVASCRIPT;
+                               case 'css':
+                                       return CONTENT_MODEL_CSS;
+                               default:
+                                       return is_null( $model ) ? CONTENT_MODEL_TEXT : $model;
+                       }
+        }
+
+        // we established that is must be wikitext
+
+        return CONTENT_MODEL_WIKITEXT;
+    }
+
+    public static function getForTitle( Title $title ) {
+        $modelName = $title->getContentModelName();
+        return ContentHandler::getForModelName( $modelName );
+    }
+
+    public static function getForContent( Content $content ) {
+        $modelName = $content->getModelName();
+        return ContentHandler::getForModelName( $modelName );
+    }
+
+    /**
+     * @static
+     * @param $modelName String the name of the content model for which to get a handler. Use CONTENT_MODEL_XXX constants.
+     * @return ContentHandler
+     * @throws MWException
+     */
+    public static function getForModelName( $modelName ) {
+        global $wgContentHandlers;
+
+        if ( empty( $wgContentHandlers[$modelName] ) ) {
+            $handler = null;
+
+                       // TODO: document new hook
+            wfRunHooks( 'ContentHandlerForModelName', array( $modelName, &$handler ) );
+
+            if ( $handler ) { // NOTE: may be a string or an object, either is fine!
+                $wgContentHandlers[$modelName] = $handler;
+            } else {
+                throw new MWException( "No handler for model $modelName registered in \$wgContentHandlers" );
+            }
+        }
+
+        if ( is_string( $wgContentHandlers[$modelName] ) ) {
+            $class = $wgContentHandlers[$modelName];
+            $wgContentHandlers[$modelName] = new $class( $modelName );
+        }
+
+        return $wgContentHandlers[$modelName];
+    }
+
+    // ----------------------------------------------------------------------------------------------------------
+    public function __construct( $modelName, $formats ) {
+        $this->mModelName = $modelName;
+        $this->mSupportedFormats = $formats;
+    }
+
+    public function getModelName() {
+        // for wikitext: wikitext; in the future: wikiast, wikidom?
+        // for wikidata: wikidata
+        return $this->mModelName;
+    }
+
+    protected function checkModelName( $modelName ) {
+        if ( $modelName !== $this->mModelName ) {
+            throw new MWException( "Bad content model: expected " . $this->mModelName . " but got found " . $modelName );
+        }
+    }
+
+    public function getSupportedFormats() {
+        // for wikitext: "text/x-mediawiki-1", "text/x-mediawiki-2", etc
+        // for wikidata: "application/json", "application/x-php", etc
+        return $this->mSupportedFormats;
+    }
+
+    public function getDefaultFormat() {
+        return $this->mSupportedFormats[0];
+    }
+
+    public function isSupportedFormat( $format ) {
+
+        if ( !$format ) {
+                       return true; // this means "use the default"
+               }
+
+        return in_array( $format, $this->mSupportedFormats );
+    }
+
+    protected function checkFormat( $format ) {
+        if ( !$this->isSupportedFormat( $format ) ) {
+            throw new MWException( "Format $format is not supported for content model " . $this->getModelName() );
+        }
+    }
+
+    /**
+     * @abstract
+     * @param Content $content
+     * @param null $format
+     * @return String
+     */
+    public abstract function serialize( Content $content, $format = null );
+
+    /**
+     * @abstract
+     * @param $blob String
+     * @param null $format
+     * @return Content
+     */
+    public abstract function unserialize( $blob, $format = null );
+
+    public abstract function emptyContent();
+
+    /**
+     * Return an Article object suitable for viewing the given object
+     *
+     * NOTE: does *not* do special handling for Image and Category pages!
+     *       Use Article::newFromTitle() for that!
+     *
+     * @param Title $title
+     * @return Article
+     * @todo Article is being refactored into an action class, keep track of that
+     */
+    public function createArticle( Title $title ) {
+        $this->checkModelName( $title->getContentModelName() );
+
+        $article = new Article($title);
+        return $article;
+    }
+
+    /**
+     * Return an EditPage object suitable for editing the given object
+     *
+     * @param Article $article
+     * @return EditPage
+     */
+    public function createEditPage( Article $article ) {
+        $this->checkModelName( $article->getContentModelName() );
+
+        $editPage = new EditPage( $article );
+        return $editPage;
+    }
+
+    /**
+     * Return an ExternalEdit object suitable for editing the given object
+     *
+     * @param IContextSource $context
+     * @return ExternalEdit
+     */
+    public function createExternalEdit( IContextSource $context ) {
+        $this->checkModelName( $context->getTitle()->getModelName() );
+
+        $externalEdit = new ExternalEdit( $context );
+        return $externalEdit;
+    }
+
+    /**
+     * Factory
+     * @param $context IContextSource context to use, anything else will be ignored
+     * @param $old Integer old ID we want to show and diff with.
+     * @param $new String either 'prev' or 'next'.
+     * @param $rcid Integer ??? FIXME (default 0)
+     * @param $refreshCache boolean If set, refreshes the diff cache
+     * @param $unhide boolean If set, allow viewing deleted revs
+        *
+        * @return DifferenceEngine
+     */
+    public function getDifferenceEngine( IContextSource $context, $old = 0, $new = 0, $rcid = 0, #FIMXE: use everywhere!
+                                         $refreshCache = false, $unhide = false ) {
+
+        $this->checkModelName( $context->getTitle()->getModelName() );
+
+        return new DifferenceEngine( $context, $old, $new, $rcid, $refreshCache, $unhide );
+    }
+
+    /**
+     * attempts to merge differences between three versions.
+     * Returns a new Content object for a clean merge and false for failure or a conflict.
+     *
+     * This default implementation always returns false.
+     *
+     * @param $oldContent String
+     * @param $myContent String
+     * @param $yourContent String
+     * @return Content|Bool
+     */
+    public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+        return false;
+    }
+
+    /**
+     * Return an applicable autosummary if one exists for the given edit.
+     *
+     * @param $oldContent Content|null: the previous text of the page.
+     * @param $newContent Content|null: The submitted text of the page.
+     * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
+     *
+     * @return string An appropriate autosummary, or an empty string.
+     */
+    public function getAutosummary( Content $oldContent = null, Content $newContent = null, $flags ) {
+        global $wgContLang;
+
+        // Decide what kind of autosummary is needed.
+
+        // Redirect autosummaries
+
+        $ot = !empty( $ot ) ? $oldContent->getRedirectTarget() : false;
+        $rt = !empty( $rt ) ? $newContent->getRedirectTarget() : false;
+
+        if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
+
+            $truncatedtext = $newContent->getTextForSummary(
+                250
+                    - strlen( wfMsgForContent( 'autoredircomment' ) )
+                    - strlen( $rt->getFullText() ) );
+
+            return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
+        }
+
+        // New page autosummaries
+        if ( $flags & EDIT_NEW && $newContent->getSize() > 0 ) {
+            // If they're making a new article, give its text, truncated, in the summary.
+
+            $truncatedtext = $newContent->getTextForSummary(
+                200 - strlen( wfMsgForContent( 'autosumm-new' ) ) );
+
+            return wfMsgForContent( 'autosumm-new', $truncatedtext );
+        }
+
+        // Blanking autosummaries
+        if ( $oldContent->getSize() > 0 && $newContent->getSize() == 0 ) {
+            return wfMsgForContent( 'autosumm-blank' );
+        } elseif ( $oldContent->getSize() > 10 * $newContent->getSize() && $newContent->getSize() < 500 ) {
+            // Removing more than 90% of the article
+
+            $truncatedtext = $newContent->getTextForSummary(
+                200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) );
+
+            return wfMsgForContent( 'autosumm-replace', $truncatedtext );
+        }
+
+        // If we reach this point, there's no applicable autosummary for our case, so our
+        // autosummary is empty.
+
+        return '';
+    }
+
+    /**
+     * Auto-generates a deletion reason
+     *
+     * @param $title Title: the page's title
+     * @param &$hasHistory Boolean: whether the page has a history
+     * @return mixed String containing deletion reason or empty string, or boolean false
+     *    if no revision occurred
+     */
+    public function getAutoDeleteReason( Title $title, &$hasHistory ) {
+        $dbw = wfGetDB( DB_MASTER );
+
+        // Get the last revision
+        $rev = Revision::newFromTitle( $title );
+
+        if ( is_null( $rev ) ) {
+            return false;
+        }
+
+        // Get the article's contents
+        $content = $rev->getContent();
+        $blank = false;
+
+        // If the page is blank, use the text from the previous revision,
+        // which can only be blank if there's a move/import/protect dummy revision involved
+        if ( $content->getSize() == 0 ) {
+            $prev = $rev->getPrevious();
+
+            if ( $prev )       {
+                $content = $rev->getContent();
+                $blank = true;
+            }
+        }
+
+        // Find out if there was only one contributor
+        // Only scan the last 20 revisions
+        $res = $dbw->select( 'revision', 'rev_user_text',
+            array( 'rev_page' => $title->getArticleID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
+            __METHOD__,
+            array( 'LIMIT' => 20 )
+        );
+
+        if ( $res === false ) {
+            // This page has no revisions, which is very weird
+            return false;
+        }
+
+        $hasHistory = ( $res->numRows() > 1 );
+        $row = $dbw->fetchObject( $res );
+
+        if ( $row ) { // $row is false if the only contributor is hidden
+            $onlyAuthor = $row->rev_user_text;
+            // Try to find a second contributor
+            foreach ( $res as $row ) {
+                if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
+                    $onlyAuthor = false;
+                    break;
+                }
+            }
+        } else {
+            $onlyAuthor = false;
+        }
+
+        // Generate the summary with a '$1' placeholder
+        if ( $blank ) {
+            // The current revision is blank and the one before is also
+            // blank. It's just not our lucky day
+            $reason = wfMsgForContent( 'exbeforeblank', '$1' );
+        } else {
+            if ( $onlyAuthor ) {
+                $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
+            } else {
+                $reason = wfMsgForContent( 'excontent', '$1' );
+            }
+        }
+
+        if ( $reason == '-' ) {
+            // Allow these UI messages to be blanked out cleanly
+            return '';
+        }
+
+        // Max content length = max comment length - length of the comment (excl. $1)
+        $text = $content->getTextForSummary( 255 - ( strlen( $reason ) - 2 ) );
+
+        // Now replace the '$1' placeholder
+        $reason = str_replace( '$1', $text, $reason );
+
+        return $reason;
+    }
+
+    /**
+     * Get the Content object that needs to be saved in order to undo all revisions
+     * between $undo and $undoafter. Revisions must belong to the same page,
+     * must exist and must not be deleted
+     * @param $undo Revision
+     * @param $undoafter null|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 = null ) {
+        $cur_content = $current->getContent();
+
+        if ( empty( $cur_content ) ) {
+            return false; // no page
+        }
+
+        $undo_content = $undo->getContent();
+        $undoafter_content = $undoafter->getContent();
+
+        if ( $cur_content->equals( $undo_content ) ) {
+            // No use doing a merge if it's just a straight revert.
+            return $undoafter_content;
+        }
+
+        $undone_content = $this->merge3( $undo_content, $undoafter_content, $cur_content );
+
+        return $undone_content;
+    }
+}
+
+
+abstract class TextContentHandler extends ContentHandler {
+
+    public function __construct( $modelName, $formats ) {
+        parent::__construct( $modelName, $formats );
+    }
+
+    public function serialize( 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.
+     *
+     * This text-based implementation uses wfMerge().
+     *
+     * @param $oldContent String
+     * @param $myContent String
+     * @param $yourContent String
+     * @return Content|Bool
+     */
+    public function merge3( Content $oldContent, Content $myContent, Content $yourContent ) {
+        $this->checkModelName( $oldContent->getModelName() );
+        #TODO: check that all Content objects have the same content model! #XXX: what to do if they don't?
+
+        $format = $this->getDefaultFormat();
+
+        $old = $this->serialize( $oldContent, $format );
+        $mine = $this->serialize( $myContent, $format );
+        $yours = $this->serialize( $yourContent, $format );
+
+        $ok = wfMerge( $old, $mine, $yours, $result );
+
+        if ( !$ok ) {
+                       return false;
+               }
+
+        if ( !$result ) {
+                       return $this->emptyContent();
+               }
+
+        $mergedContent = $this->unserialize( $result, $format );
+        return $mergedContent;
+    }
+
+
+}
+class WikitextContentHandler extends TextContentHandler {
+
+    public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+        parent::__construct( $modelName, array( 'application/x-wikitext' ) ); #FIXME: mime
+    }
+
+    public function unserialize( $text, $format = null ) {
+        $this->checkFormat( $format );
+
+        return new WikitextContent( $text );
+    }
+
+    public function emptyContent() {
+        return new WikitextContent( '' );
+    }
+
+
+}
+
+#TODO: make ScriptContentHandler base class with plugin interface for syntax highlighting!
+
+class JavaScriptContentHandler extends TextContentHandler {
+
+    public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+        parent::__construct( $modelName, array( 'text/javascript' ) ); #XXX: or use $wgJsMimeType? this is for internal storage, not HTTP...
+    }
+
+    public function unserialize( $text, $format = null ) {
+        return new JavaScriptContent( $text );
+    }
+
+    public function emptyContent() {
+        return new JavaScriptContent( '' );
+    }
+}
+
+class CssContentHandler extends TextContentHandler {
+
+    public function __construct( $modelName = CONTENT_MODEL_WIKITEXT ) {
+        parent::__construct( $modelName, array( 'text/css' ) );
+    }
+
+    public function unserialize( $text, $format = null ) {
+        return new CssContent( $text );
+    }
+
+    public function emptyContent() {
+        return new CssContent( '' );
+    }
+
+}
index 6788984..277507f 100644 (file)
@@ -640,6 +640,17 @@ $wgMediaHandlers = array(
        'image/x-djvu' => 'DjVuHandler', // compat
 );
 
+/**
+ * Plugins for page content model handling.
+ * Each entry in the array maps a model name type to a class name
+ */
+$wgContentHandlers = array(
+    CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
+    CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
+    CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
+    CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
+);
+
 /**
  * Resizing can be done using PHP's internal image libraries or using
  * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@ -5807,6 +5818,22 @@ $wgSeleniumConfigFile = null;
 $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
 $wgDBtestpassword = '';
 
+/**
+ * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
+ * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
+ * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
+ */
+$wgNamespaceContentModels = array();
+
+/**
+ * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
+ *
+ * * 'ignore': return null
+ * * 'fail': throw an MWException
+ * * 'serialize': serialize to default format
+ */
+$wgContentHandlerTextFallback = 'ignore';
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index e40c9b2..610d0a5 100644 (file)
@@ -253,3 +253,12 @@ define( 'PROTO_RELATIVE', '//' );
 define( 'PROTO_CURRENT', null );
 define( 'PROTO_CANONICAL', 1 );
 define( 'PROTO_INTERNAL', 2 );
+
+/**
+ * Content model names, used by Content and ContentHandler
+ */
+define('CONTENT_MODEL_WIKITEXT', 'wikitext');
+define('CONTENT_MODEL_JAVASCRIPT', 'javascript');
+define('CONTENT_MODEL_CSS', 'css');
+define('CONTENT_MODEL_TEXT', 'text');
+
index 69187e4..afe2821 100644 (file)
@@ -144,6 +144,11 @@ class EditPage {
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
 
+       /**
+        * Status: can't parse content
+        */
+       const AS_PARSE_ERROR                = 240;
+
        /**
         * @var Article
         */
@@ -198,6 +203,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
@@ -209,7 +215,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;
@@ -223,6 +229,11 @@ class EditPage {
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
+
+               $this->content_model = $this->mTitle->getContentModelName();
+
+               $handler = ContentHandler::getForModelName( $this->content_model );
+               $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
 
        /**
@@ -434,10 +445,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 );
@@ -451,13 +462,14 @@ class EditPage {
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
-                       $content = $this->textbox1;
+                       $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
+                       $text = $content->serialize( $this->content_format );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
 
-               $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
+               $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
 
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
@@ -568,9 +580,9 @@ class EditPage {
                                // Skip this if wpTextbox2 has input, it indicates that we came
                                // from a conflict page with raw page text, not a custom form
                                // modified by subclasses
-                               wfProfileIn( get_class( $this ) . "::importContentFormData" );
-                               $textbox1 = $this->importContentFormData( $request );
-                               if ( isset( $textbox1 ) )
+                               wfProfileIn( get_class($this)."::importContentFormData" );
+                               $textbox1 = $this->importContentFormData( $request ); #FIXME: what should this return??
+                               if ( isset($textbox1) )
                                        $this->textbox1 = $textbox1;
                                wfProfileOut( get_class( $this ) . "::importContentFormData" );
                        }
@@ -663,7 +675,7 @@ class EditPage {
                } else {
                        # Not a posted form? Start with nothing.
                        wfDebug( __METHOD__ . ": Not a posted form.\n" );
-                       $this->textbox1     = '';
+                       $this->textbox1     = ''; #FIXME: track content object
                        $this->summary      = '';
                        $this->sectiontitle = '';
                        $this->edittime     = '';
@@ -695,10 +707,17 @@ class EditPage {
                        }
                }
 
+               $this->oldid = $request->getInt( 'oldid' );
+
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
 
-               $this->oldid = $request->getInt( 'oldid' );
+               $content_handler = ContentHandler::getForTitle( $this->mTitle );
+               $this->content_model = $request->getText( 'model', $content_handler->getModelName() ); #may be overridden by revision
+               $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
+
+               #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
+               #TODO: check if the desired content model supports the given content format!
 
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
@@ -731,7 +750,10 @@ class EditPage {
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
-               $this->textbox1 = $this->getContent( false );
+
+               $content = $this->getContentObject( false ); #TODO: track content object?!
+               $this->textbox1 = $content->serialize( $this->content_format );
+
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
@@ -760,33 +782,52 @@ class EditPage {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
+        * @deprecated since 1.20
         */
-       function getContent( $def_text = '' ) {
-               global $wgOut, $wgRequest, $wgParser;
+       function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
+               if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
+                       $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
+               } else {
+                       $def_content = false;
+               }
+
+               $content = $this->getContentObject( $def_content );
+
+               return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
+       }
+
+       private function getContentObject( $def_content = null ) { #FIXME: use this!
+               global $wgOut, $wgRequest;
 
                wfProfileIn( __METHOD__ );
 
-               $text = false;
+               $content = false;
 
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
-                               $text = $this->mTitle->getDefaultMessageText();
+                               $msg = $this->mTitle->getDefaultMessageText();
+
+                               $content = new WikitextContent($msg); //XXX: really hardcode wikitext here?
                        }
-                       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' );
@@ -802,15 +843,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 {
@@ -842,14 +884,14 @@ class EditPage {
                                                wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
                                }
 
-                               if ( $text === false ) {
-                                       $text = $this->getOriginalContent();
+                               if ( $content === false ) {
+                                       $content = $this->getOriginalContent();
                                }
                        }
                }
 
                wfProfileOut( __METHOD__ );
-               return $text;
+               return $content;
        }
 
        /**
@@ -866,31 +908,45 @@ class EditPage {
         * @since 1.19
         * @return string
         */
-       private function getOriginalContent() {
+       private function getOriginalContent() { #FIXME: use Content! set content_model and content_format!
                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()->getContentModelName();
+                       $handler = ContentHandler::getForModelName( $this->content_model );
+
+                       return $handler->emptyContent();
                }
-               return $this->mArticle->getContent();
+
+               $content = $this->mArticle->getContentObject();
+               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.20
         * @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()->getContentModelName();
+                       $handler = ContentHandler::getForModelName( $this->content_model );
+
+                       return $handler->emptyContent();
                } else {
-                       return $text;
+                       #FIXME: nasty side-effect!
+                       $this->content_model = $rev->getContentModelName();
+                       $this->content_format = $rev->getContentFormat();
+
+                       return $content;
                }
        }
 
@@ -898,9 +954,23 @@ class EditPage {
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
+        * @deprecated since 1.20
+        */
+       public function setPreloadedText( $text ) { #FIXME: deprecated, use setPreloadedContent()
+               wfDeprecated( __METHOD__, "1.20" );
+
+               $content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+               $this->setPreloadedContent( $content );
+       }
+
+       /**
+        * Use this method before edit() to preload some content into the edit box
+        *
+        * @param $content Content
         */
-       public function setPreloadedText( $text ) {
-               $this->mPreloadText = $text;
+       public function setPreloadedContent( Content $content ) { #FIXME: use this!
+               $this->mPreloadedContent = $content;
        }
 
        /**
@@ -909,22 +979,34 @@ class EditPage {
         *
         * @param $preload String: representing the title to preload from.
         * @return String
+        * @deprecated since 1.20
         */
-       protected function getPreloadedText( $preload ) {
-               global $wgUser, $wgParser;
+       protected function getPreloadedText( $preload ) { #FIXME: B/C only, replace usage!
+               wfDeprecated( __METHOD__, "1.20" );
 
-               if ( !empty( $this->mPreloadText ) ) {
-                       return $this->mPreloadText;
+               $content = $this->getPreloadedContent( $preload );
+               $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
+
+               return $text;
+       }
+
+       protected function getPreloadedContent( $preload ) { #FIXME: use this!
+               global $wgUser;
+
+               if ( !empty( $this->mPreloadContent ) ) {
+                       return $this->mPreloadContent;
                }
 
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+
                if ( $preload === '' ) {
-                       return '';
+                       return $handler->emptyContent();
                }
 
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                       return '';
+                       return $handler->emptyContent();
                }
 
                $page = WikiPage::factory( $title );
@@ -932,13 +1014,15 @@ class EditPage {
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
-                               return '';
+                               return $handler->emptyContent();
                        }
                        $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 );
        }
 
        /**
@@ -988,6 +1072,11 @@ class EditPage {
                        case self::AS_FILTERING:
                                return false;
 
+                       case self::AS_PARSE_ERROR:
+                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
+                               #FIXME: cause editform to be shown again, not just an error!
+                               return false;
+
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
@@ -1076,7 +1165,7 @@ class EditPage {
 
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
-                       Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
+                       Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
@@ -1192,38 +1281,52 @@ class EditPage {
                $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
                $new = ( $aid == 0 );
 
-               if ( $new ) {
-                       // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create' ) ) {
-                               $status->fatal( 'nocreatetext' );
-                               $status->value = self::AS_NO_CREATE_PERMISSION;
-                               wfDebug( __METHOD__ . ": no create permission\n" );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+               try {
+                       if ( $new ) {
+                               // Late check for create permission, just in case *PARANOIA*
+                               if ( !$this->mTitle->userCan( 'create' ) ) {
+                                       $status->fatal( 'nocreatetext' );
+                                       $status->value = self::AS_NO_CREATE_PERMISSION;
+                                       wfDebug( __METHOD__ . ": no create permission\n" );
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               }
 
-                       # Don't save a new article if it's blank.
-                       if ( $this->textbox1 == '' ) {
-                               $status->setResult( false, self::AS_BLANK_ARTICLE );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+                               # Don't save a new article if it's blank.
+                               if ( $this->textbox1 == '' ) {
+                                       $status->setResult( false, self::AS_BLANK_ARTICLE );
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               }
 
-                       // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
-                               # Error messages etc. could be handled within the hook...
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       } elseif ( $this->hookError != '' ) {
-                               # ...or the hook could be expecting us to produce an error
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR_EXPECTED;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+<<<<<<< HEAD
+                               // Run post-section-merge edit filter
+                               if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
+                                       # Error messages etc. could be handled within the hook...
+                                       $status->fatal( 'hookaborted' );
+                                       $status->value = self::AS_HOOK_ERROR;
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               } elseif ( $this->hookError != '' ) {
+                                       # ...or the hook could be expecting us to produce an error
+                                       $status->fatal( 'hookaborted' );
+                                       $status->value = self::AS_HOOK_ERROR_EXPECTED;
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               }
+
+                $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 
+                               # Handle the user preference to force summaries here. Check if it's not a redirect.
+                               if ( !$this->allowBlankSummary && !$content->isRedirect() ) {
+                                       if ( md5( $this->summary ) == $this->autoSumm ) {
+                                               $this->missingSummary = true;
+                                               $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
+                                               $status->value = self::AS_SUMMARY_NEEDED;
+                                               wfProfileOut( __METHOD__ );
+                                               return $status;
+                                       }
+=======
                        $text = $this->textbox1;
                        $result['sectionanchor'] = '';
                        if ( $this->section == 'new' ) {
@@ -1251,39 +1354,80 @@ class EditPage {
                                        // Create a link to the new section from the edit summary.
                                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
                                        $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+>>>>>>> master
                                }
-                       }
 
-                       $status->value = self::AS_SUCCESS_NEW_ARTICLE;
+                               $result['sectionanchor'] = '';
+                               if ( $this->section == 'new' ) {
+                                       if ( $this->sectiontitle !== '' ) {
+                                               // Insert the section title above the content.
+                                               $content = $content->addSectionHeader( $this->sectiontitle );
+
+                                               // Jump to the new section
+                                               $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+
+                                               // If no edit summary was specified, create one automatically from the section
+                                               // title and have it link to the new section. Otherwise, respect the summary as
+                                               // passed.
+                                               if ( $this->summary === '' ) {
+                                                       $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                                                       $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+                                               }
+                                       } elseif ( $this->summary !== '' ) {
+                                               // Insert the section title above the content.
+                                               $content = $content->addSectionHeader( $this->sectiontitle );
+
+                                               // Jump to the new section
+                                               $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 
-               } else {
+                                               // Create a link to the new section from the edit summary.
+                                               $cleanSummary = $wgParser->stripSectionName( $this->summary );
+                                               $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+                                       }
+                               }
 
-                       # Article exists. Check for edit conflict.
+                               $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 
-                       $this->mArticle->clear(); # Force reload of dates, etc.
-                       $timestamp = $this->mArticle->getTimestamp();
+                       } else {
 
-                       wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
+                               # Article exists. Check for edit conflict.
 
-                       if ( $timestamp != $this->edittime ) {
-                               $this->isConflict = true;
-                               if ( $this->section == 'new' ) {
-                                       if ( $this->mArticle->getUserText() == $wgUser->getName() &&
-                                               $this->mArticle->getComment() == $this->summary ) {
-                                               // Probably a duplicate submission of a new comment.
-                                               // This can happen when squid resends a request after
-                                               // a timeout but the first one actually went through.
-                                               wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
-                                       } else {
-                                               // New comment; suppress conflict.
+                               $this->mArticle->clear(); # Force reload of dates, etc.
+                               $timestamp = $this->mArticle->getTimestamp();
+
+                               wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
+
+                               if ( $timestamp != $this->edittime ) {
+                                       $this->isConflict = true;
+                                       if ( $this->section == 'new' ) {
+                                               if ( $this->mArticle->getUserText() == $wgUser->getName() &&
+                                                       $this->mArticle->getComment() == $this->summary ) {
+                                                       // Probably a duplicate submission of a new comment.
+                                                       // This can happen when squid resends a request after
+                                                       // a timeout but the first one actually went through.
+                                                       wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
+                                               } else {
+                                                       // New comment; suppress conflict.
+                                                       $this->isConflict = false;
+                                                       wfDebug( __METHOD__ .": conflict suppressed; new section\n" );
+                                               }
+                                       } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
+                                               # Suppress edit conflict with self, except for section edits where merging is required.
+                                               wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                                $this->isConflict = false;
+<<<<<<< HEAD
+=======
                                                wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
+>>>>>>> master
                                        }
-                               } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
-                                       # Suppress edit conflict with self, except for section edits where merging is required.
-                                       wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
-                                       $this->isConflict = false;
                                }
+<<<<<<< HEAD
+
+                               // If sectiontitle is set, use it, otherwise use the summary as the section title (for
+                               // backwards compatibility with old forms/bots).
+                               if ( $this->sectiontitle !== '' ) {
+                                       $sectionTitle = $this->sectiontitle;
+=======
                        }
 
                        // If sectiontitle is set, use it, otherwise use the summary as the section title (for
@@ -1311,137 +1455,173 @@ class EditPage {
                                        // Successful merge! Maybe we should tell the user the good news?
                                        $this->isConflict = false;
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
+>>>>>>> master
                                } else {
-                                       $this->section = '';
-                                       $this->textbox1 = $text;
-                                       wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
+                                       $sectionTitle = $this->summary;
                                }
-                       }
 
-                       if ( $this->isConflict ) {
-                               $status->setResult( false, self::AS_CONFLICT_DETECTED );
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+                               $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+                               $content = false;
 
-                       // Run post-section-merge edit filter
-                       if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
-                               # Error messages etc. could be handled within the hook...
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       } elseif ( $this->hookError != '' ) {
-                               # ...or the hook could be expecting us to produce an error
-                               $status->fatal( 'hookaborted' );
-                               $status->value = self::AS_HOOK_ERROR_EXPECTED;
-                               wfProfileOut( __METHOD__ );
-                               return $status;
-                       }
+                               if ( $this->isConflict ) {
+                                       wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 
-                       # Handle the user preference to force summaries here, but not for null edits
-                       if ( $this->section != 'new' && !$this->allowBlankSummary
-                               && $this->getOriginalContent() != $text
-                               && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
-                       {
-                               if ( md5( $this->summary ) == $this->autoSumm ) {
-                                       $this->missingSummary = true;
-                                       $status->fatal( 'missingsummary' );
-                                       $status->value = self::AS_SUMMARY_NEEDED;
-                                       wfProfileOut( __METHOD__ );
-                                       return $status;
+                                       $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
+                               } else {
+                                       wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
+
+                                       $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
                                }
-                       }
 
-                       # And a similar thing for new sections
-                       if ( $this->section == 'new' && !$this->allowBlankSummary ) {
-                               if ( trim( $this->summary ) == '' ) {
-                                       $this->missingSummary = true;
-                                       $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
-                                       $status->value = self::AS_SUMMARY_NEEDED;
+                               if ( is_null( $content ) ) {
+                                       wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
+                                       $this->isConflict = true;
+                                       $content = $textbox_content; // do not try to merge here!
+                               } elseif ( $this->isConflict ) {
+                                       # Attempt merge
+                                       if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
+                                               // Successful merge! Maybe we should tell the user the good news?
+                                               $content = $textbox_content;
+                                               $this->isConflict = false;
+                                               wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
+                                       } else {
+                                               $this->section = '';
+                                               #$this->textbox1 = $text; #redundant, nothing to do here?
+                                               wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
+                                       }
+                               }
+
+                               if ( $this->isConflict ) {
+                                       $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
-                       }
 
-                       # All's well
-                       wfProfileIn( __METHOD__ . '-sectionanchor' );
-                       $sectionanchor = '';
-                       if ( $this->section == 'new' ) {
-                               if ( $this->textbox1 == '' ) {
-                                       $this->missingComment = true;
-                                       $status->fatal( 'missingcommenttext' );
-                                       $status->value = self::AS_TEXTBOX_EMPTY;
-                                       wfProfileOut( __METHOD__ . '-sectionanchor' );
+                               // Run post-section-merge edit filter
+                               if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
+                                       || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) { #FIXME: document new hook
+                                       # Error messages etc. could be handled within the hook...
+                                       $status->fatal( 'hookaborted' );
+                                       $status->value = self::AS_HOOK_ERROR;
+                                       wfProfileOut( __METHOD__ );
+                                       return $status;
+                               } elseif ( $this->hookError != '' ) {
+                                       # ...or the hook could be expecting us to produce an error
+                                       $status->fatal( 'hookaborted' );
+                                       $status->value = self::AS_HOOK_ERROR_EXPECTED;
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
-                               if ( $this->sectiontitle !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
-                                       // If no edit summary was specified, create one automatically from the section
-                                       // title and have it link to the new section. Otherwise, respect the summary as
-                                       // passed.
-                                       if ( $this->summary === '' ) {
-                                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+
+                               # Handle the user preference to force summaries here, but not for null edits
+                               if ( $this->section != 'new' && !$this->allowBlankSummary
+                                       && !$content->equals( $this->getOriginalContent() )
+                                       && !$content->isRedirect() ) # check if it's not a redirect
+                               {
+                                       if ( md5( $this->summary ) == $this->autoSumm ) {
+                                               $this->missingSummary = true;
+                                               $status->fatal( 'missingsummary' );
+                                               $status->value = self::AS_SUMMARY_NEEDED;
+                                               wfProfileOut( __METHOD__ );
+                                               return $status;
                                        }
-                               } elseif ( $this->summary !== '' ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
-                                       # This is a new section, so create a link to the new section
-                                       # in the revision summary.
-                                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
                                }
-                       } elseif ( $this->section != '' ) {
-                               # Try to get a section anchor from the section source, redirect to edited section if header found
-                               # XXX: might be better to integrate this into Article::replaceSection
-                               # for duplicate heading checking and maybe parsing
-                               $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
-                               # we can't deal with anchors, includes, html etc in the header for now,
-                               # headline would need to be parsed to improve this
-                               if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
-                                       $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
+
+                               # And a similar thing for new sections
+                               if ( $this->section == 'new' && !$this->allowBlankSummary ) {
+                                       if ( trim( $this->summary ) == '' ) {
+                                               $this->missingSummary = true;
+                                               $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
+                                               $status->value = self::AS_SUMMARY_NEEDED;
+                                               wfProfileOut( __METHOD__ );
+                                               return $status;
+                                       }
                                }
-                       }
-                       $result['sectionanchor'] = $sectionanchor;
-                       wfProfileOut( __METHOD__ . '-sectionanchor' );
 
-                       // Save errors may fall down to the edit form, but we've now
-                       // merged the section into full text. Clear the section field
-                       // so that later submission of conflict forms won't try to
-                       // replace that into a duplicated mess.
-                       $this->textbox1 = $text;
-                       $this->section = '';
+                               # All's well
+                               wfProfileIn( __METHOD__ . '-sectionanchor' );
+                               $sectionanchor = '';
+                               if ( $this->section == 'new' ) {
+                                       if ( $this->textbox1 == '' ) {
+                                               $this->missingComment = true;
+                                               $status->fatal( 'missingcommenttext' );
+                                               $status->value = self::AS_TEXTBOX_EMPTY;
+                                               wfProfileOut( __METHOD__ . '-sectionanchor' );
+                                               wfProfileOut( __METHOD__ );
+                                               return $status;
+                                       }
+                                       if ( $this->sectiontitle !== '' ) {
+                                               $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
+                                               // If no edit summary was specified, create one automatically from the section
+                                               // title and have it link to the new section. Otherwise, respect the summary as
+                                               // passed.
+                                               if ( $this->summary === '' ) {
+                                                       $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                                                       $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
+                                               }
+                                       } elseif ( $this->summary !== '' ) {
+                                               $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
+                                               # This is a new section, so create a link to the new section
+                                               # in the revision summary.
+                                               $cleanSummary = $wgParser->stripSectionName( $this->summary );
+                                               $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
+                                       }
+                               } elseif ( $this->section != '' ) {
+                                       # Try to get a section anchor from the section source, redirect to edited section if header found
+                                       # XXX: might be better to integrate this into Article::replaceSection
+                                       # for duplicate heading checking and maybe parsing
+                                       $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
+                                       # we can't deal with anchors, includes, html etc in the header for now,
+                                       # headline would need to be parsed to improve this
+                                       if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
+                                               $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
+                                       }
+                               }
+                               $result['sectionanchor'] = $sectionanchor;
+                               wfProfileOut( __METHOD__ . '-sectionanchor' );
 
-                       $status->value = self::AS_SUCCESS_UPDATE;
-               }
+                               // Save errors may fall down to the edit form, but we've now
+                               // merged the section into full text. Clear the section field
+                               // so that later submission of conflict forms won't try to
+                               // replace that into a duplicated mess.
+                               $this->textbox1 = $content->serialize( $this->content_format );
+                               $this->section = '';
 
-               // Check for length errors again now that the section is merged in
-               $this->kblength = (int)( strlen( $text ) / 1024 );
-               if ( $this->kblength > $wgMaxArticleSize ) {
-                       $this->tooBig = true;
-                       $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
-                       wfProfileOut( __METHOD__ );
-                       return $status;
-               }
+                               $status->value = self::AS_SUCCESS_UPDATE;
+                       }
+
+                       // Check for length errors again now that the section is merged in
+                       $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
+                       if ( $this->kblength > $wgMaxArticleSize ) {
+                               $this->tooBig = true;
+                               $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
+                               wfProfileOut( __METHOD__ );
+                               return $status;
+                       }
 
-               $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
-                       ( $new ? EDIT_NEW : EDIT_UPDATE ) |
-                       ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
-                       ( $bot ? EDIT_FORCE_BOT : 0 );
+                       $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
+                               ( $new ? EDIT_NEW : EDIT_UPDATE ) |
+                               ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
+                               ( $bot ? EDIT_FORCE_BOT : 0 );
 
-               $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
+                       $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
 
-               if ( $doEditStatus->isOK() ) {
-                       $result['redirect'] = Title::newFromRedirect( $text ) !== null;
-                       $this->commitWatch();
+                       if ( $doEditStatus->isOK() ) {
+                               $result['redirect'] = $content->isRedirect();
+                               $this->commitWatch();
+                               wfProfileOut( __METHOD__ );
+                               return $status;
+                       } else {
+                               $this->isConflict = true;
+                               $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
+                               wfProfileOut( __METHOD__ );
+                               return $doEditStatus;
+                       }
+               } catch (MWContentSerializationException $ex) {
+                       $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+                       $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
                        return $status;
-               } else {
-                       $this->isConflict = true;
-                       $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
-                       wfProfileOut( __METHOD__ );
-                       return $doEditStatus;
                }
        }
 
@@ -1498,8 +1678,33 @@ class EditPage {
         * @parma $editText string
         *
         * @return bool
+        * @deprecated since 1.20
+        */
+       function mergeChangesInto( &$editText ){
+               wfDebug( __METHOD__, "1.20" );
+
+               $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
+
+               $ok = $this->mergeChangesIntoContent( $editContent );
+
+               if ( $ok ) {
+                       $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
+                       return true;
+               } else {
+                       return false;
+               }
+       }
+
+       /**
+        * @private
+        * @todo document
+        *
+        * @parma $editText string
+        *
+        * @return bool
+        * @since since 1.20
         */
-       function mergeChangesInto( &$editText ) {
+       private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
 
                $db = wfGetDB( DB_MASTER );
@@ -1510,7 +1715,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 );
@@ -1518,11 +1723,14 @@ class EditPage {
                        wfProfileOut( __METHOD__ );
                        return false;
                }
-               $currentText = $currentRevision->getText();
+               $currentContent = $currentRevision->getContent();
 
-               $result = '';
-               if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
-                       $editText = $result;
+               $handler = ContentHandler::getForModelName( $baseContent->getModelName() );
+
+               $result = $handler->merge3( $baseContent, $editContent, $currentContent );
+
+               if ( $result ) {
+                       $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
@@ -1609,13 +1817,13 @@ class EditPage {
                } elseif ( $contextTitle->exists() && $this->section != '' ) {
                        $msg = $this->section == 'new' ? 'editingcomment' : 'editingsection';
                } else {
-                       $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ?\r
-                               'editing' : 'creating';\r
+                       $msg = $contextTitle->exists() || ( $contextTitle->getNamespace() == NS_MEDIAWIKI && $contextTitle->getDefaultMessageText() !== false ) ?
+                               'editing' : 'creating';
                }
                # Use the title defined by DISPLAYTITLE magic word when present
-               $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;\r
-               if ( $displayTitle === false ) {\r
-                       $displayTitle = $contextTitle->getPrefixedText();\r
+               $displayTitle = isset( $this->mParserOutput ) ? $this->mParserOutput->getDisplayTitle() : false;
+               if ( $displayTitle === false ) {
+                       $displayTitle = $contextTitle->getPrefixedText();
                }
                $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
        }
@@ -1769,6 +1977,7 @@ class EditPage {
                        }
                }
 
+               #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => 'editform', 'name' => 'editform',
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
@@ -1829,6 +2038,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 ) );
@@ -1846,7 +2058,9 @@ class EditPage {
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
-                       $this->textbox1 = $this->getCurrentText();
+
+                       $content = $this->getCurrentContent();
+                       $this->textbox1 = $content->serialize( $this->content_format );
 
                        $this->showTextbox1();
                } else {
@@ -2254,7 +2468,7 @@ 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 );
@@ -2333,8 +2547,16 @@ HTML
         * save and then make a comparison.
         */
        function showDiff() {
-               global $wgUser, $wgContLang, $wgParser, $wgOut;
+               global $wgUser, $wgContLang, $wgOut;
+
+               $oldContent = $this->getOriginalContent();
+
+               $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
+                                                                                                               $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 
+               $newContent = $this->mArticle->replaceSectionContent(
+                                                                                       $this->section, $textboxContent,
+                                                                                       $this->summary, $this->edittime );
                $oldtitlemsg = 'currentrev';
                # if message does not exist, show diff against the preloaded default
                if( $this->mTitle->getNamespace() == NS_MEDIAWIKI && !$this->mTitle->exists() ) {
@@ -2348,17 +2570,30 @@ HTML
                $newtext = $this->mArticle->replaceSection(
                        $this->section, $this->textbox1, $this->summary, $this->edittime );
 
+               # hanlde legacy text-based hook
+               $newtext_orig = $newContent->serialize( $this->content_format );
+               $newtext = $newtext_orig; #clone
                wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 
+               if ( $newtext != $newtext_orig ) {
+                       #if the hook changed the text, create a new Content object accordingly.
+                       $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModelName() ); #XXX: handle parse errors ?
+               }
+
+               wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) ); #FIXME: document new hook
+
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-               $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
+               $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
 
+               if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
+                       $oldtitle = wfMsgExt( 'currentrev', array( 'parseinline' ) );
                if ( $oldtext !== false  || $newtext != '' ) {
                        $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
 
-                       $de = new DifferenceEngine( $this->mArticle->getContext() );
-                       $de->setText( $oldtext, $newtext );
+                       $de = $oldContent->getContentHandler()->getDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $oldContent, $newContent );
+
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
@@ -2448,8 +2683,12 @@ HTML
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
 
-                       $de = new DifferenceEngine( $this->mArticle->getContext() );
-                       $de->setText( $this->textbox2, $this->textbox1 );
+                       $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
+                       $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
+
+                       $handler = ContentHandler::getForModelName( $this->content_model );
+                       $de = $handler->getDifferenceEngine( $this->mArticle->getContext() );
+                       $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
 
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
@@ -2569,6 +2808,95 @@ HTML
                        return $parsedNote;
                }
 
+        try {
+            $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
+
+            if ( $this->mTriedSave && !$this->mTokenOk ) {
+                if ( $this->mTokenOkExceptSuffix ) {
+                    $note = wfMsg( 'token_suffix_mismatch' );
+                } else {
+                    $note = wfMsg( 'session_fail_preview' );
+                }
+            } elseif ( $this->incompleteForm ) {
+                $note = wfMsg( 'edit_form_incomplete' );
+            } elseif ( $this->isCssJsSubpage || $this->mTitle->isCssOrJsPage() ) {
+                # if this is a CSS or JS page used in the UI, show a special notice
+                # XXX: stupid php bug won't let us use $this->getContextTitle()->isCssJsSubpage() here -- This note has been there since r3530. Sure the bug was fixed time ago?
+
+                if( $this->mTitle->isCssJsSubpage() ) {
+                    $level = 'user';
+                } elseif( $this->mTitle->isCssOrJsPage() ) {
+                    $level = 'site';
+                } else {
+                    $level = false;
+                }
+
+                if ( $content->getModelName() == CONTENT_MODEL_CSS ) {
+                    $format = 'css';
+                } elseif ( $content->getModelName() == CONTENT_MODEL_JAVASCRIPT ) {
+                    $format = 'js';
+                } else {
+                    $format = false;
+                }
+
+                # Used messages to make sure grep find them:
+                # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
+                if( $level && $format ) {
+                    $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
+                } else {
+                    $note = wfMsg( 'previewnote' );
+                }
+            } else {
+                $note = wfMsg( 'previewnote' );
+            }
+
+            $parserOptions = ParserOptions::newFromUser( $wgUser );
+            $parserOptions->setEditSection( false );
+            $parserOptions->setTidy( true );
+            $parserOptions->setIsPreview( true );
+            $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
+
+            $rt = $content->getRedirectChain();
+
+            if ( $rt ) {
+                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
+            } else {
+
+                # If we're adding a comment, we need to show the
+                # summary as the headline
+                if ( $this->section == "new" && $this->summary != "" ) {
+                    $content = $content->addSectionHeader( $this->summary );
+                }
+
+                $toparse_orig = $content->serialize( $this->content_format );
+                $toparse = $toparse_orig;
+                wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
+
+                if ( $toparse !== $toparse_orig ) {
+                    #hook changed the text, create new Content object
+                    $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
+                }
+
+                wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) ); # FIXME: document new hook
+
+                $parserOptions->enableLimitReport();
+
+                #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
+                $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+                $parserOutput = $content->getParserOutput( $this->mTitle, null, $parserOptions );
+
+                $previewHTML = $parserOutput->getText();
+                $this->mParserOutput = $parserOutput;
+                $wgOut->addParserOutputNoText( $parserOutput );
+
+                if ( count( $parserOutput->getWarnings() ) ) {
+                    $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
+                }
+            }
+        } catch (MWContentSerializationException $ex) {
+            $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
+            $previewHTML = '';
+        }
                if ( $this->mTriedSave && !$this->mTokenOk ) {
                        if ( $this->mTokenOkExceptSuffix ) {
                                $note = wfMsg( 'token_suffix_mismatch' );
@@ -3078,7 +3406,14 @@ HTML
                $wgOut->addHTML( '</div>' );
 
                $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
-               $this->showDiff();
+
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+               $de = $handler->getDifferenceEngine( $this->mArticle->getContext() );
+
+               $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
+               $de->setContent( $this->getCurrentContent(), $content2 );
+
+               $de->showDiff( wfMsg( "storedversion" ), wfMsgExt( 'yourtext', 'parseinline' ) );
 
                $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                $this->showTextbox2();
@@ -3234,6 +3569,8 @@ HTML
                                // breaks one of the entities whilst editing.
                                if ( ( substr( $invalue, $i, 1 ) == ";" ) and ( strlen( $hexstring ) <= 6 ) ) {
                                        $codepoint = hexdec( $hexstring );
+                               if ( (substr($invalue,$i,1)==";") and (strlen($hexstring) <= 6) ) {
+                                       $codepoint = hexdec($hexstring);
                                        $result .= codepointToUtf8( $codepoint );
                                } else {
                                        $result .= "&#x" . $hexstring . substr( $invalue, $i, 1 );
index d280db5..bb47439 100644 (file)
@@ -117,7 +117,8 @@ class FeedUtils {
                        $diffText = '';
                        // Don't bother generating the diff if we won't be able to show it
                        if ( $wgFeedDiffCutoff > 0 ) {
-                               $de = new DifferenceEngine( $title, $oldid, $newid );
+                $contentHandler = ContentHandler::getForTitle( $title );
+                $de = $contentHandler->getDifferenceEngine( $title, $oldid, $newid );
                                $diffText = $de->getDiff(
                                        wfMsg( 'previousrevision' ), // hack
                                        wfMsg( 'revisionasof',
index 7453baa..5755bdc 100644 (file)
@@ -133,7 +133,9 @@ class ImagePage extends Article {
                        $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
-                       parent::view();
+
+            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
+
                        $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
@@ -244,20 +246,20 @@ class ImagePage extends Article {
                return $r;
        }
 
-       /**
-        * Overloading Article's getContent method.
-        *
-        * Omit noarticletext if sharedupload; text will be fetched from the
-        * shared upload server if possible.
-        * @return string
-        */
-       public function getContent() {
-               $this->loadFile();
-               if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
-                       return '';
-               }
-               return parent::getContent();
-       }
+    /**
+     * Overloading Article's getContentObject method.
+     *
+     * Omit noarticletext if sharedupload; text will be fetched from the
+     * shared upload server if possible.
+     * @return string
+     */
+    public function getContentObject() {
+        $this->loadFile();
+        if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
+            return null;
+        }
+        return parent::getContentObject();
+    }
 
        protected function openShowImage() {
                global $wgOut, $wgUser, $wgImageLimits, $wgRequest,
index 716e7d8..1973d95 100644 (file)
  *
  * @todo document (e.g. one-sentence top-level class description).
  */
-class LinksUpdate {
+class LinksUpdate extends SecondaryDBDataUpdate {
 
        /**@{{
         * @private
         */
        var $mId,            //!< Page ID of the article linked from
-               $mTitle,         //!< Title object of the article linked from
-               $mParserOutput,  //!< Parser output
-               $mLinks,         //!< Map of title strings to IDs for the links in the document
+        $mTitle,         //!< Title object of the article linked from
+        $mParserOutput,  //!< Whether to queue jobs for recursive update
+        $mLinks,         //!< Map of title strings to IDs for the links in the document
                $mImages,        //!< DB keys of the images used, in the array key only
                $mTemplates,     //!< Map of title strings to IDs for the template references, including broken ones
                $mExternals,     //!< URLs of external links, array key only
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
-               $mDb,            //!< Database connection reference
-               $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
        /**@}}*/
 
@@ -47,23 +45,22 @@ class LinksUpdate {
         * @param $recursive Boolean: queue jobs for recursive updates?
         */
        function __construct( $title, $parserOutput, $recursive = true ) {
-               global $wgAntiLockFlags;
+        parent::__construct( );
 
-               if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
-                       $this->mOptions = array();
-               } else {
-                       $this->mOptions = array( 'FOR UPDATE' );
-               }
-               $this->mDb = wfGetDB( DB_MASTER );
+        if ( !is_object( $title ) ) {
+            throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
+                "Please see Article::editUpdates() for an invocation example.\n" );
+        }
 
-               if ( !is_object( $title ) ) {
-                       throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
+               if ( !is_object( $title ) ) {
+                       throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
                                "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
-               }
-               $this->mTitle = $title;
-               $this->mId = $title->getArticleID();
+               }
+        $this->mTitle = $title;
+        $this->mId = $title->getArticleID();
+
+        $this->mParserOutput = $parserOutput;
 
-               $this->mParserOutput = $parserOutput;
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
@@ -253,51 +250,6 @@ class LinksUpdate {
                wfProfileOut( __METHOD__ );
        }
 
-       /**
-        * Invalidate the cache of a list of pages from a single namespace
-        *
-        * @param $namespace Integer
-        * @param $dbkeys Array
-        */
-       function invalidatePages( $namespace, $dbkeys ) {
-               if ( !count( $dbkeys ) ) {
-                       return;
-               }
-
-               /**
-                * Determine which pages need to be updated
-                * This is necessary to prevent the job queue from smashing the DB with
-                * large numbers of concurrent invalidations of the same page
-                */
-               $now = $this->mDb->timestamp();
-               $ids = array();
-               $res = $this->mDb->select( 'page', array( 'page_id' ),
-                       array(
-                               'page_namespace' => $namespace,
-                               'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-               foreach ( $res as $row ) {
-                       $ids[] = $row->page_id;
-               }
-               if ( !count( $ids ) ) {
-                       return;
-               }
-
-               /**
-                * Do the update
-                * We still need the page_touched condition, in case the row has changed since
-                * the non-locking select above.
-                */
-               $this->mDb->update( 'page', array( 'page_touched' => $now ),
-                       array(
-                               'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-       }
-
        /**
         * @param $cats
         */
@@ -324,20 +276,20 @@ class LinksUpdate {
                $this->invalidatePages( NS_FILE, array_keys( $images ) );
        }
 
-       /**
-        * @param $table
-        * @param $insertions
-        * @param $fromField
-        */
-       private function dumbTableUpdate( $table, $insertions, $fromField ) {
-               $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
-               if ( count( $insertions ) ) {
-                       # The link array was constructed without FOR UPDATE, so there may
-                       # be collisions.  This may cause minor link table inconsistencies,
-                       # which is better than crippling the site with lock contention.
-                       $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
-               }
-       }
+    /**
+     * @param $table
+     * @param $insertions
+     * @param $fromField
+     */
+    private function dumbTableUpdate( $table, $insertions, $fromField ) {
+        $this->mDb->delete( $table, array( $fromField => $this->mId ), __METHOD__ );
+        if ( count( $insertions ) ) {
+            # The link array was constructed without FOR UPDATE, so there may
+            # be collisions.  This may cause minor link table inconsistencies,
+            # which is better than crippling the site with lock contention.
+            $this->mDb->insert( $table, $insertions, __METHOD__, array( 'IGNORE' ) );
+        }
+    }
 
        /**
         * Update a table by doing a delete query then an insert query
@@ -803,22 +755,22 @@ class LinksUpdate {
                return $arr;
        }
 
-       /**
-        * Return the title object of the page being updated
-        * @return Title
-        */
-       public function getTitle() {
-               return $this->mTitle;
-       }
-
-       /**
-        * Returns parser output
-        * @since 1.19
-        * @return ParserOutput
-        */
-       public function getParserOutput() {
-               return $this->mParserOutput;
-       }
+    /**
+     * Return the title object of the page being updated
+     * @return Title
+     */
+    public function getTitle() {
+        return $this->mTitle;
+    }
+
+    /**
+     * Returns parser output
+     * @since 1.19
+     * @return ParserOutput
+     */
+    public function getParserOutput() {
+        return $this->mParserOutput;
+    }
 
        /**
         * Return the list of images used as generated by the parser
index 1147e6a..07ae586 100644 (file)
@@ -20,6 +20,10 @@ class Revision {
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
+    protected $mContentModelName;
+    protected $mContentFormat;
+    protected $mContent;
+    protected $mContentHandler;
 
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
@@ -125,6 +129,8 @@ class Revision {
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
+            'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
+            'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
@@ -336,7 +342,9 @@ class Revision {
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
-                       'rev_sha1'
+                       'rev_sha1',
+                       'rev_content_format',
+                       'rev_content_model'
                );
        }
 
@@ -416,6 +424,18 @@ class Revision {
                                $this->mTitle = null;
                        }
 
+            if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
+                $this->mContentModelName = null; # determine on demand if needed
+            } else {
+                $this->mContentModelName = strval( $row->rev_content_model );
+            }
+
+            if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
+                $this->mContentFormat = null; # determine on demand if needed
+            } else {
+                $this->mContentFormat = strval( $row->rev_content_format );
+            }
+
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
@@ -437,6 +457,19 @@ class Revision {
                        // Build a new revision to be saved...
                        global $wgUser; // ugh
 
+
+            # if we have a content object, use it to set the model and type
+            if ( !empty( $row['content'] ) ) {
+                if ( !empty( $row['text_id'] ) ) { #FIXME: when is that set? test with external store setup! check out insertOn()
+                    throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" );
+                }
+
+                $row['content_model'] = $row['content']->getModelName();
+                # note: mContentFormat is initializes later accordingly
+                # note: content is serialized later in this method!
+                # also set text to null?
+            }
+
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
@@ -449,6 +482,9 @@ class Revision {
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
 
+            $this->mContentModelName = isset( $row['content_model']  )  ? strval( $row['content_model'] ) : null;
+            $this->mContentFormat    = isset( $row['content_format']  )   ? strval( $row['content_format'] ) : null;
+
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
@@ -458,16 +494,29 @@ class Revision {
                        $this->mCurrent   = false;
                        # If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
+                #XXX: my be inconsistent with the notion of "size" use for the present content model
                                $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
                        }
                        # Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
+
+            $this->getContentModelName(); # force lazy init
+            $this->getContentFormat();      # force lazy init
+
+            # if we have a content object, serialize it, overriding mText
+            if ( !empty( $row['content'] ) ) {
+                $handler = $this->getContentHandler();
+                $this->mText = $handler->serialize( $row['content'], $this->getContentFormat() );
+            }
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                $this->mUnpatrolled = null;
+
+        #FIXME: add patch for ar_content_format, ar_content_model, rev_content_format, rev_content_model to installer
+        #FIXME: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API
        }
 
        /**
@@ -727,17 +776,38 @@ class Revision {
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
+     * @deprectaed in 1.20, use getContent() instead
         */
-       public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
-               if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
-                       return '';
-               } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
-                       return '';
-               } else {
-                       return $this->getRawText();
-               }
+       public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage! #FIXME: used a LOT!
+        wfDeprecated( __METHOD__, '1.20' );
+
+        $content = $this->getContent();
+        return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
        }
 
+    /**
+     * Fetch revision content if it's available to the specified audience.
+     * If the specified audience does not have the ability to view this
+     * revision, null will be returned.
+     *
+     * @param $audience Integer: one of:
+     *      Revision::FOR_PUBLIC       to be displayed to all users
+     *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+     *      Revision::RAW              get the text regardless of permissions
+     * @param $user User object to check for, only if FOR_THIS_USER is passed
+     *              to the $audience parameter
+     * @return Content
+     */
+    public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
+        if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
+            return null;
+        } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
+            return null;
+        } else {
+            return $this->getContentInternal();
+        }
+    }
+
        /**
         * Alias for getText(Revision::FOR_THIS_USER)
         *
@@ -754,14 +824,63 @@ class Revision {
         *
         * @return String
         */
-       public function getRawText() {
-               if( is_null( $this->mText ) ) {
-                       // Revision text is immutable. Load on demand:
-                       $this->mText = $this->loadText();
-               }
-               return $this->mText;
+       public function getRawText() { #FIXME: deprecated, replace usage!
+               return $this->getText( self::RAW );
        }
 
+    protected function getContentInternal() {
+        if( is_null( $this->mContent ) ) {
+            // Revision is immutable. Load on demand:
+
+            $handler = $this->getContentHandler();
+            $format = $this->getContentFormat();
+            $title = $this->getTitle();
+
+            if( is_null( $this->mText ) ) {
+                // Load text on demand:
+                $this->mText = $this->loadText();
+            }
+
+            $this->mContent = is_null( $this->mText ) ? null : $handler->unserialize( $this->mText, $format );
+        }
+
+        return $this->mContent;
+    }
+
+    public function getContentModelName() {
+        if ( !$this->mContentModelName ) {
+            $title = $this->getTitle();
+            $this->mContentModelName = ( $title ? $title->getContentModelName() : CONTENT_MODEL_WIKITEXT );
+        }
+
+        return $this->mContentModelName;
+    }
+
+    public function getContentFormat() {
+        if ( !$this->mContentFormat ) {
+            $handler = $this->getContentHandler();
+            $this->mContentFormat = $handler->getDefaultFormat();
+        }
+
+        return $this->mContentFormat;
+    }
+
+    public function getContentHandler() {
+        if ( !$this->mContentHandler ) {
+            $title = $this->getTitle();
+
+            if ( $title ) $model = $title->getContentModelName();
+            else $model = CONTENT_MODEL_WIKITEXT;
+
+            $this->mContentHandler = ContentHandler::getForModelName( $model );
+
+            #XXX: do we need to verify that mContentHandler supports mContentFormat?
+            #     otherwise, a fixed content format may cause problems on insert.
+        }
+
+        return $this->mContentHandler;
+    }
+
        /**
         * @return String
         */
@@ -983,26 +1102,29 @@ class Revision {
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
-               $dbw->insert( 'revision',
-                       array(
-                               'rev_id'         => $rev_id,
-                               'rev_page'       => $this->mPage,
-                               'rev_text_id'    => $this->mTextId,
-                               'rev_comment'    => $this->mComment,
-                               'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
-                               'rev_user'       => $this->mUser,
-                               'rev_user_text'  => $this->mUserText,
-                               'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
-                               'rev_deleted'    => $this->mDeleted,
-                               'rev_len'        => $this->mSize,
-                               'rev_parent_id'  => is_null( $this->mParentId )
-                                       ? $this->getPreviousRevisionId( $dbw )
-                                       : $this->mParentId,
-                               'rev_sha1'       => is_null( $this->mSha1 )
-                                       ? Revision::base36Sha1( $this->mText )
-                                       : $this->mSha1
-                       ), __METHOD__
-               );
+
+        $row = array(
+            'rev_id'         => $rev_id,
+            'rev_page'       => $this->mPage,
+            'rev_text_id'    => $this->mTextId,
+            'rev_comment'    => $this->mComment,
+            'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
+            'rev_user'       => $this->mUser,
+            'rev_user_text'  => $this->mUserText,
+            'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
+            'rev_deleted'    => $this->mDeleted,
+            'rev_len'        => $this->mSize,
+            'rev_parent_id'  => is_null( $this->mParentId )
+                ? $this->getPreviousRevisionId( $dbw )
+                : $this->mParentId,
+            'rev_sha1'       => is_null( $this->mSha1 )
+                ? Revision::base36Sha1( $this->mText )
+                : $this->mSha1,
+            'rev_content_model'       => $this->getContentModelName(),
+            'rev_content_format'        => $this->getContentFormat(),
+        );
+
+               $dbw->insert( 'revision', $row, __METHOD__ );
 
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
 
@@ -1100,7 +1222,8 @@ class Revision {
 
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
-                       array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ),
+                       array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1',
+                    'rev_content_model', 'rev_content_format' ),
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
@@ -1115,7 +1238,9 @@ class Revision {
                                'text_id'    => $current->rev_text_id,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
-                               'sha1'       => $current->rev_sha1
+                               'sha1'       => $current->rev_sha1,
+                               'content_model'  => $current->rev_content_model,
+                               'content_format'   => $current->rev_content_format
                                ) );
                } else {
                        $revision = null;
diff --git a/includes/SecondaryDBDataUpdate.php b/includes/SecondaryDBDataUpdate.php
new file mode 100644 (file)
index 0000000..1adb9a3
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that put some secondary data extracted
+ * from article content into the database.
+ */
+abstract class SecondaryDBDataUpdate extends SecondaryDataUpdate {
+
+       /**@{{
+        * @private
+        */
+       var $mDb,            //!< Database connection reference
+               $mOptions;       //!< SELECT options to be used (array)
+       /**@}}*/
+
+       /**
+        * Constructor
+     **/
+    public function __construct( ) {
+               global $wgAntiLockFlags;
+
+        parent::__construct( );
+
+               if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
+                       $this->mOptions = array();
+               } else {
+                       $this->mOptions = array( 'FOR UPDATE' );
+               }
+               $this->mDb = wfGetDB( DB_MASTER );
+       }
+
+       /**
+        * Invalidate the cache of a list of pages from a single namespace
+        *
+        * @param $namespace Integer
+        * @param $dbkeys Array
+        */
+       public function invalidatePages( $namespace, $dbkeys ) {
+               if ( !count( $dbkeys ) ) {
+                       return;
+               }
+
+               /**
+                * Determine which pages need to be updated
+                * This is necessary to prevent the job queue from smashing the DB with
+                * large numbers of concurrent invalidations of the same page
+                */
+               $now = $this->mDb->timestamp();
+               $ids = array();
+               $res = $this->mDb->select( 'page', array( 'page_id' ),
+                       array(
+                               'page_namespace' => $namespace,
+                               'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
+                               'page_touched < ' . $this->mDb->addQuotes( $now )
+                       ), __METHOD__
+               );
+               foreach ( $res as $row ) {
+                       $ids[] = $row->page_id;
+               }
+               if ( !count( $ids ) ) {
+                       return;
+               }
+
+               /**
+                * Do the update
+                * We still need the page_touched condition, in case the row has changed since
+                * the non-locking select above.
+                */
+               $this->mDb->update( 'page', array( 'page_touched' => $now ),
+                       array(
+                               'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
+                               'page_touched < ' . $this->mDb->addQuotes( $now )
+                       ), __METHOD__
+               );
+       }
+
+}
diff --git a/includes/SecondaryDataUpdate.php b/includes/SecondaryDataUpdate.php
new file mode 100644 (file)
index 0000000..eeee42f
--- /dev/null
@@ -0,0 +1,51 @@
+<?php
+/**
+ * See docs/deferred.txt
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * Abstract base class for update jobs that do something with some secondary
+ * data extracted from article.
+ */
+abstract class SecondaryDataUpdate {
+
+       /**
+        * Constructor
+        */
+    public function __construct( ) {
+        # noop
+       }
+
+       /**
+        * Perform update.
+        */
+       public abstract function doUpdate();
+
+    /**
+     * Conveniance method, calls doUpdate() on every element in the array.
+     *
+     * @static
+     * @param $updates array
+     */
+    public static function runUpdates( $updates ) {
+        if ( empty( $updates ) ) return; # nothing to do
+
+        foreach ( $updates as $update ) {
+            $update->doUpdate();
+        }
+    }
+
+}
index 769adb9..6152f1c 100644 (file)
@@ -271,12 +271,17 @@ class Title {
                        if ( isset( $row->page_is_redirect ) )
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
-                               $this->mLatestID = (int)$row->page_latest;
+                               $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model
+            if ( isset( $row->page_content_model ) )
+                $this->mContentModelName = $row->page_content_model;
+            else
+                $this->mContentModelName = null; # initialized lazily in getContentModelName()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
+            $this->mContentModelName = null; # initialized lazily in getContentModelName()
                }
        }
 
@@ -302,6 +307,7 @@ class Title {
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
+        $t->mContentModelName = null; # initialized lazily in getContentModelName()
                return $t;
        }
 
@@ -687,6 +693,29 @@ class Title {
                return $this->mNamespace;
        }
 
+    /**
+     * Get the page's content model name
+     *
+     * @return Integer: Namespace index
+     */
+    public function getContentModelName() {
+        if ( empty( $this->mContentModelName ) ) {
+            $this->mContentModelName = ContentHandler::getDefaultModelFor( $this );
+        }
+
+        return $this->mContentModelName;
+    }
+
+    /**
+     * Conveniance method for checking a title's content model name
+     *
+     * @param $name
+     * @return true if $this->getContentModelName() == $name
+     */
+    public function hasContentModel( $name ) {
+        return $this->getContentModelName() == $name;
+    }
+
        /**
         * Get the namespace text
         *
@@ -937,22 +966,29 @@ 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.
+     *
+     * 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;
        }
 
        /**
@@ -960,7 +996,8 @@ 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->isCssOrJsPage() );
        }
 
        /**
@@ -983,7 +1020,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 ) );
        }
 
        /**
@@ -992,7 +1030,8 @@ class Title {
         * @return Bool
         */
        public function isJsSubpage() {
-               return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+        return ( NS_USER == $this->mNamespace && $this->isSubpage()
+            && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
 
        /**
index 6cad466..5ea5d75 100644 (file)
@@ -185,6 +185,7 @@ class WikiPage extends Page {
                        'page_touched',
                        'page_latest',
                        'page_len',
+            'page_content_model',
                );
        }
 
@@ -323,17 +324,31 @@ class WikiPage extends Page {
         * @return bool
         */
        public function isRedirect( $text = false ) {
-               if ( $text === false ) {
-                       if ( !$this->mDataLoaded ) {
-                               $this->loadPageData();
-                       }
+        if ( $text === false ) $content = $this->getContent();
+        else $content = ContentHandler::makeContent( $text, $this->mTitle ); # TODO: allow model and format to be provided; or better, expect a Content object
 
-                       return (bool)$this->mIsRedirect;
-               } else {
-                       return Title::newFromRedirect( $text ) !== null;
-               }
+
+        if ( empty( $content ) ) return false;
+        else return $content->isRedirect();
        }
 
+    /**
+     * Returns the page's content model name. Will use the revisions actual content model if the page exists,
+     * and the page's default if the page doesn't exist yet.
+     *
+     * @return int
+     */
+    public function getContentModelName() {
+        if ( $this->exists() ) {
+            # look at the revision's actual content model
+            $content = $this->getContent();
+            return $content->getModelName();
+        } else {
+            # use the default model for this page
+            return $this->mTitle->getContentModelName();
+        }
+    }
+
        /**
         * Loads page_touched and returns a value indicating if it should be used
         * @return boolean true if not a redirect
@@ -407,6 +422,23 @@ class WikiPage extends Page {
                return null;
        }
 
+    /**
+     * Get the content of the current revision. No side-effects...
+     *
+     * @param $audience Integer: one of:
+     *      Revision::FOR_PUBLIC       to be displayed to all users
+     *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+     *      Revision::RAW              get the text regardless of permissions
+     * @return Content|null The content of the current revision
+     */
+    public function getContent( $audience = Revision::FOR_PUBLIC ) {
+        $this->loadLastEdit();
+        if ( $this->mLastRevision ) {
+            return $this->mLastRevision->getContent( $audience );
+        }
+        return false;
+    }
+
        /**
         * Get the text of the current revision. No side-effects...
         *
@@ -414,9 +446,11 @@ class WikiPage extends Page {
         *      Revision::FOR_PUBLIC       to be displayed to all users
         *      Revision::FOR_THIS_USER    to be displayed to $wgUser
         *      Revision::RAW              get the text regardless of permissions
-        * @return String|bool The text of the current revision. False on failure
+        * @return String|false The text of the current revision
+     * @deprecated as of 1.20, getContent() should be used instead.
         */
-       public function getText( $audience = Revision::FOR_PUBLIC ) {
+       public function getText( $audience = Revision::FOR_PUBLIC ) { #FIXME: deprecated, replace usage!
+        wfDeprecated( __METHOD__, '1.20' );
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
                        return $this->mLastRevision->getText( $audience );
@@ -429,14 +463,22 @@ class WikiPage extends Page {
         *
         * @return String|bool The text of the current revision. False on failure
         */
-       public function getRawText() {
-               $this->loadLastEdit();
-               if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getRawText();
-               }
-               return false;
+       public function getRawText() { #FIXME: deprecated, replace usage!
+               return $this->getText( Revision::RAW );
        }
 
+    /**
+     * Get the content of the current revision. No side-effects...
+     *
+     * @return Contet|false The text of the current revision
+     */
+    protected function getNativeData() { #FIXME: examine all uses carefully! caller must be aware of content model!
+        $content = $this->getContent( Revision::RAW );
+        if ( !$content ) return null;
+
+        return $content->getNativeData();
+    }
+
        /**
         * @return string MW timestamp of last article revision
         */
@@ -558,32 +600,35 @@ class WikiPage extends Page {
                        return false;
                }
 
-               $text = $editInfo ? $editInfo->pst : false;
+        if ( $editInfo ) {
+            $content = ContentHandler::makeContent( $editInfo->pst, $this->mTitle );
+            # TODO: take model and format from edit info!
+        } else {
+            $content = $this->getContent();
+        }
 
-               if ( $this->isRedirect( $text ) ) {
+               if ( !$content || $content->isRedirect( ) ) {
                        return false;
                }
 
-               switch ( $wgArticleCountMethod ) {
-               case 'any':
-                       return true;
-               case 'comma':
-                       if ( $text === false ) {
-                               $text = $this->getRawText();
-                       }
-                       return strpos( $text,  ',' ) !== false;
-               case 'link':
-                       if ( $editInfo ) {
-                               // ParserOutput::getLinks() is a 2D array of page links, so
-                               // to be really correct we would need to recurse in the array
-                               // but the main array should only have items in it if there are
-                               // links.
-                               return (bool)count( $editInfo->output->getLinks() );
-                       } else {
-                               return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
-                                       array( 'pl_from' => $this->getId() ), __METHOD__ );
-                       }
-               }
+        $hasLinks = null;
+
+        if ( $wgArticleCountMethod === 'link' ) {
+            # nasty special case to avoid re-parsing to detect links
+
+            if ( $editInfo ) {
+                // ParserOutput::getLinks() is a 2D array of page links, so
+                // to be really correct we would need to recurse in the array
+                // but the main array should only have items in it if there are
+                // links.
+                $hasLinks = (bool)count( $editInfo->output->getLinks() );
+            } else {
+                $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
+                    array( 'pl_from' => $this->getId() ), __METHOD__ );
+            }
+        }
+
+               return $content->isCountable( $hasLinks );
        }
 
        /**
@@ -629,7 +674,8 @@ class WikiPage extends Page {
         */
        public function insertRedirect() {
                // recurse through to only get the final target
-               $retval = Title::newFromRedirectRecurse( $this->getRawText() );
+        $content = $this->getContent();
+               $retval = $content ? $content->getUltimateRedirectTarget() : null;
                if ( !$retval ) {
                        return null;
                }
@@ -825,7 +871,7 @@ class WikiPage extends Page {
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
-                       && $this->mTitle->isWikitextPage();
+                       && $this->mTitle->isWikitextPage(); #FIXME: ask ContentHandler if cachable!
        }
 
        /**
@@ -914,7 +960,7 @@ class WikiPage extends Page {
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        if ( $this->mTitle->exists() ) {
-                               $text = $this->getRawText();
+                               $text = $this->getNativeData(); #FIXME: may not be a string. check Content model!
                        } else {
                                $text = false;
                        }
@@ -981,9 +1027,9 @@ class WikiPage extends Page {
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
                wfProfileIn( __METHOD__ );
 
-               $text = $revision->getText();
-               $len = strlen( $text );
-               $rt = Title::newFromRedirectRecurse( $text );
+        $content = $revision->getContent();
+               $len = $content->getSize();
+               $rt = $content->getUltimateRedirectTarget();
 
                $conditions = array( 'page_id' => $this->getId() );
 
@@ -1102,27 +1148,23 @@ class WikiPage extends Page {
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
+     * @deprecated since 1.20: use ContentHandler::getUndoContent() instead.
         */
-       public function getUndoText( Revision $undo, Revision $undoafter = null ) {
-               $cur_text = $this->getRawText();
-               if ( $cur_text === false ) {
-                       return false; // no page
-               }
-               $undo_text = $undo->getText();
-               $undoafter_text = $undoafter->getText();
+       public function getUndoText( Revision $undo, Revision $undoafter = null ) { #FIXME: replace usages.
+        $this->loadLastEdit();
 
-               if ( $cur_text == $undo_text ) {
-                       # No use doing a merge if it's just a straight revert.
-                       return $undoafter_text;
-               }
+        if ( $this->mLastRevision ) {
+            $handler = ContentHandler::getForTitle( $this->getTitle() );
+            $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
 
-               $undone_text = '';
+            if ( !$undone ) {
+                return false;
+            } else {
+                return ContentHandler::getContentText( $undone );
+            }
+        }
 
-               if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
-                       return false;
-               }
-
-               return $undone_text;
+        return false;
        }
 
        /**
@@ -1130,55 +1172,54 @@ class WikiPage extends Page {
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
-        * @return string Complete article text, or null if error
+        * @return Content new complete article content, or null if error
+     * @deprected since 1.20, use replaceSectionContent() instead
         */
-       public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
-               wfProfileIn( __METHOD__ );
+       public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) { #FIXME: use replaceSectionContent() instead!
+        wfDeprecated( __METHOD__, '1.20' );
 
-               if ( strval( $section ) == '' ) {
-                       // Whole-page edit; let the whole text through
-               } else {
-                       // Bug 30711: always use current version when adding a new section
-                       if ( is_null( $edittime ) || $section == 'new' ) {
-                               $oldtext = $this->getRawText();
-                               if ( $oldtext === false ) {
-                                       wfDebug( __METHOD__ . ": no page text\n" );
-                                       wfProfileOut( __METHOD__ );
-                                       return null;
-                               }
-                       } else {
-                               $dbw = wfGetDB( DB_MASTER );
-                               $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
+        $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); #XXX: could make section title, but that's not required.
 
-                               if ( !$rev ) {
-                                       wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
-                                               $this->getId() . "; section: $section; edittime: $edittime)\n" );
-                                       wfProfileOut( __METHOD__ );
-                                       return null;
-                               }
+        $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 
-                               $oldtext = $rev->getText();
-                       }
+               return ContentHandler::getContentText( $newContent ); #XXX: unclear what will happen for non-wikitext!
+       }
 
-                       if ( $section == 'new' ) {
-                               # Inserting a new section
-                               $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
-                               if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
-                                       $text = strlen( trim( $oldtext ) ) > 0
-                                               ? "{$oldtext}\n\n{$subject}{$text}"
-                                               : "{$subject}{$text}";
-                               }
-                       } else {
-                               # Replacing an existing section; roll out the big guns
-                               global $wgParser;
+    public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
+        wfProfileIn( __METHOD__ );
 
-                               $text = $wgParser->replaceSection( $oldtext, $section, $text );
-                       }
-               }
+        if ( strval( $section ) == '' ) {
+            // Whole-page edit; let the whole text through
+            $newContent = $sectionContent;
+        } else {
+            // Bug 30711: always use current version when adding a new section
+            if ( is_null( $edittime ) || $section == 'new' ) {
+                $oldContent = $this->getContent();
+                if ( ! $oldContent ) {
+                    wfDebug( __METHOD__ . ": no page text\n" );
+                    wfProfileOut( __METHOD__ );
+                    return null;
+                }
+            } else {
+                $dbw = wfGetDB( DB_MASTER );
+                $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
 
-               wfProfileOut( __METHOD__ );
-               return $text;
-       }
+                if ( !$rev ) {
+                    wfDebug( "WikiPage::replaceSection asked for bogus section (page: " .
+                        $this->getId() . "; section: $section; edittime: $edittime)\n" );
+                    wfProfileOut( __METHOD__ );
+                    return null;
+                }
+
+                $oldContent = $rev->getContent();
+            }
+
+            $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
+        }
+
+        wfProfileOut( __METHOD__ );
+        return $newContent;
+    }
 
        /**
         * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
@@ -1242,8 +1283,64 @@ class WikiPage extends Page {
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
-        */
-       public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
+     * @deprecated since 1.20: use doEditContent() instead.
+        */
+    public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #FIXME: use doEditContent() instead
+        #TODO: log use of deprecated function
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+
+        return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
+    }
+
+    /**
+     * Change an existing article or create a new article. Updates RC and all necessary caches,
+     * optionally via the deferred update array.
+     *
+     * @param $content Content: new content
+     * @param $summary String: edit summary
+     * @param $flags Integer bitfield:
+     *      EDIT_NEW
+     *          Article is known or assumed to be non-existent, create a new one
+     *      EDIT_UPDATE
+     *          Article is known or assumed to be pre-existing, update it
+     *      EDIT_MINOR
+     *          Mark this edit minor, if the user is allowed to do so
+     *      EDIT_SUPPRESS_RC
+     *          Do not log the change in recentchanges
+     *      EDIT_FORCE_BOT
+     *          Mark the edit a "bot" edit regardless of user rights
+     *      EDIT_DEFER_UPDATES
+     *          Defer some of the updates until the end of index.php
+     *      EDIT_AUTOSUMMARY
+     *          Fill in blank summaries with generated text where possible
+     *
+     * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
+     * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
+     * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
+     * edit-already-exists error will be returned. These two conditions are also possible with
+     * auto-detection due to MediaWiki's performance-optimised locking strategy.
+     *
+     * @param $baseRevId the revision ID this edit was based off, if any
+     * @param $user User the user doing the edit
+     * @param $serialisation_format String: format for storing the content in the database
+     *
+     * @return Status object. Possible errors:
+     *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
+     *     edit-gone-missing:       In update mode, but the article didn't exist
+     *     edit-conflict:           In update mode, the article changed unexpectedly
+     *     edit-no-change:          Warning that the text was the same as before
+     *     edit-already-exists:     In creation mode, but the article already exists
+     *
+     *  Extensions may define additional errors.
+     *
+     *  $return->value will contain an associative array with members as follows:
+     *     new:                     Boolean indicating if the function attempted to create a new article
+     *     revision:                The revision object for the inserted revision, or null
+     *
+     *  Compatibility note: this function previously returned a boolean value indicating success/failure
+     */
+       public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
+                                   User $user = null, $serialisation_format = null ) { #FIXME: use this
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
 
                # Low-level sanity check
@@ -1261,10 +1358,25 @@ class WikiPage extends Page {
 
                $flags = $this->checkFlags( $flags );
 
-               if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
-                       $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
-               {
-                       wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
+        # call legacy hook
+        $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary, #FIXME: document new hook!
+            $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+
+        if ( $hook_ok && !empty( $wgHooks['ArticleSave'] ) ) { # avoid serialization overhead if the hook isn't present
+            $content_text = $content->serialize();
+            $txt = $content_text; # clone
+
+            $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary, #FIXME: deprecate legacy hook!
+                $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
+
+            if ( $txt !== $content_text ) {
+                # if the text changed, unserialize the new version to create an updated Content object.
+                $content = $content->getContentHandler()->unserialize( $txt );
+            }
+        }
+
+               if ( !$hook_ok ) {
+                       wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
 
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
@@ -1278,20 +1390,25 @@ class WikiPage extends Page {
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
 
-               $oldtext = $this->getRawText(); // current revision
-               $oldsize = strlen( $oldtext );
+               $old_content = $this->getContent( Revision::RAW ); // current revision's content
+
+               $oldsize = $old_content ? $old_content->getSize() : 0;
                $oldid = $this->getLatest();
                $oldIsRedirect = $this->isRedirect();
                $oldcountable = $this->isCountable();
 
+        $handler = $content->getContentHandler();
+
                # Provide autosummaries if one is not provided and autosummaries are enabled.
                if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
-                       $summary = self::getAutosummary( $oldtext, $text, $flags );
+            if ( !$old_content ) $old_content = null;
+                       $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
 
-               $editInfo = $this->prepareTextForEdit( $text, null, $user );
-               $text = $editInfo->pst;
-               $newsize = strlen( $text );
+               $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
+               $serialized = $editInfo->pst;
+        $content = $editInfo->pstContent;
+               $newsize =  $content->getSize();
 
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
@@ -1319,14 +1436,17 @@ class WikiPage extends Page {
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
-                               'text'       => $text,
+                               'text'       => $serialized,
+                'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
-                               'timestamp'  => $now
+                               'timestamp'  => $now,
+                'content_model' => $content->getModelName(),
+                'content_format' => $serialisation_format,
                        ) );
 
-                       $changed = ( strcmp( $text, $oldtext ) != 0 );
+                       $changed = !$content->equals( $old_content );
 
                        if ( $changed ) {
                                $dbw->begin( __METHOD__ );
@@ -1424,10 +1544,13 @@ class WikiPage extends Page {
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
-                               'text'       => $text,
+                               'text'       => $serialized,
+                'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
-                               'timestamp'  => $now
+                               'timestamp'  => $now,
+                'content_model' => $content->getModelName(),
+                'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
 
@@ -1445,7 +1568,7 @@ class WikiPage extends Page {
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
-                                       '', strlen( $text ), $revisionId, $patrolled );
+                                       '', $content->getSize(), $revisionId, $patrolled );
 
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
@@ -1458,8 +1581,11 @@ class WikiPage extends Page {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
 
-                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
+                       wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary, #FIXME: deprecate legacy hook
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
+
+            wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook
+                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
 
                # Do updates right now unless deferral was requested
@@ -1470,9 +1596,12 @@ class WikiPage extends Page {
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
 
-               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
+               wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,  #FIXME: deprecate legacy hook
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 
+        wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary, #FIXME: document new hook
+            $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
+
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
 
@@ -1500,15 +1629,35 @@ class WikiPage extends Page {
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
-        * @return bool|object
-        */
-       public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
+     * @deprecated in 1.20: use prepareContentForEdit instead.
+        */
+    public function prepareTextForEdit( $text, $revid = null, User $user = null ) {  #FIXME: use prepareContentForEdit() instead #XXX: who uses this?!
+        #TODO: log use of deprecated function
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+        return $this->prepareContentForEdit( $content, $revid , $user );
+    }
+
+    /**
+     * Prepare content which is about to be saved.
+     * Returns a stdclass with source, pst and output members
+     *
+     * @param \Content $content
+     * @param null $revid
+     * @param null|\User $user
+     * @param null $serialization_format
+     * @return bool|object
+     */
+       public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) { #FIXME: use this #XXX: really public?!
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
+
                if ( $this->mPreparedEdit
-                       && $this->mPreparedEdit->newText == $text
+                       && $this->mPreparedEdit->newContent
+            && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
+            && $this->mPreparedEdit->format == $serialization_format
+            #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
@@ -1519,11 +1668,19 @@ class WikiPage extends Page {
 
                $edit = (object)array();
                $edit->revid = $revid;
-               $edit->newText = $text;
-               $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
+
+               $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
+        $edit->pst = $edit->pstContent->serialize( $serialization_format );
+        $edit->format = $serialization_format;
+
                $edit->popts = $this->makeParserOptions( 'canonical' );
-               $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
-               $edit->oldText = $this->getRawText();
+               $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
+
+        $edit->newContent = $content;
+               $edit->oldContent = $this->getContent( Revision::RAW );
+
+        $edit->newText = ContentHandler::getContentText( $edit->newContent ); #FIXME: B/C only! don't use this field!
+        $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; #FIXME: B/C only! don't use this field!
 
                $this->mPreparedEdit = $edit;
 
@@ -1553,13 +1710,13 @@ class WikiPage extends Page {
                wfProfileIn( __METHOD__ );
 
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
-               $text = $revision->getText();
+        $content = $revision->getContent();
 
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
-                       $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
+                       $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
@@ -1571,9 +1728,9 @@ class WikiPage extends Page {
                        $parserCache->save( $editInfo->output, $this, $editInfo->popts );
                }
 
-               # Update the links tables
-               $u = new LinksUpdate( $this->mTitle, $editInfo->output );
-               $u->doUpdate();
+               # Update the links tables and other secondary data
+        $updates = $editInfo->output->getLinksUpdateAndOtherUpdates( $this->mTitle );
+        SecondaryDataUpdate::runUpdates( $updates );
 
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
 
@@ -1617,7 +1774,7 @@ class WikiPage extends Page {
                }
 
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
-               DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
+        DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) );
 
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
@@ -1643,7 +1800,10 @@ class WikiPage extends Page {
                }
 
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
-                       MessageCache::singleton()->replace( $shortTitle, $text );
+            $msgtext = ContentHandler::getContentText( $content ); #XXX: could skip pseudo-messages like js/css here, based on content model.
+            if ( $msgtext === false || $msgtext === null ) $msgtext = '';
+
+                       MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
 
                if( $options['created'] ) {
@@ -1665,13 +1825,33 @@ class WikiPage extends Page {
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
         */
-       public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+    public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
+        #TODO: log use of deprecated function
+        $content = ContentHandler::makeContent( $text, $this->getTitle() );
+        return $this->doQuickEdit( $content, $user, $comment , $minor );
+    }
+
+    /**
+     * Edit an article without doing all that other stuff
+     * The article must already exist; link tables etc
+     * are not updated, caches are not flushed.
+     *
+     * @param $content Content: content submitted
+     * @param $user User The relevant user
+     * @param $comment String: comment submitted
+     * @param $serialisation_format String: format for storing the content in the database
+     * @param $minor Boolean: whereas it's a minor modification
+     */
+       public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
 
+        $serialized = $content->serialize( $serialisation_format );
+
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
-                       'text'       => $text,
+                       'text'       => $serialized,
+            'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
                ) );
@@ -2013,7 +2193,9 @@ class WikiPage extends Page {
                                'ar_len'        => 'rev_len',
                                'ar_page_id'    => 'page_id',
                                'ar_deleted'    => $bitfield,
-                               'ar_sha1'       => 'rev_sha1'
+                               'ar_sha1'       => 'rev_content_model',
+                               'ar_content_format'       => 'rev_content_format',
+                               'ar_content_format'       => 'rev_sha1'
                        ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
@@ -2276,7 +2458,7 @@ class WikiPage extends Page {
                }
 
                # Actually store the edit
-               $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
+               $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
@@ -2426,53 +2608,16 @@ class WikiPage extends Page {
        * @param $newtext String: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
+    * @deprecated since 1.20, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
-               global $wgContLang;
-
-               # Decide what kind of autosummary is needed.
-
-               # Redirect autosummaries
-               $ot = Title::newFromRedirect( $oldtext );
-               $rt = Title::newFromRedirect( $newtext );
-
-               if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
-                       $truncatedtext = $wgContLang->truncate(
-                               str_replace( "\n", ' ', $newtext ),
-                               max( 0, 250
-                                       - strlen( wfMsgForContent( 'autoredircomment' ) )
-                                       - strlen( $rt->getFullText() )
-                               ) );
-                       return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
-               }
+               # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
 
-               # New page autosummaries
-               if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
-                       # If they're making a new article, give its text, truncated, in the summary.
+        $handler = ContentHandler::getForModelName( CONTENT_MODEL_WIKITEXT );
+        $oldContent = $oldtext ? $handler->unserialize( $oldtext ) : null;
+        $newContent = $newtext ? $handler->unserialize( $newtext ) : null;
 
-                       $truncatedtext = $wgContLang->truncate(
-                               str_replace( "\n", ' ', $newtext ),
-                               max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
-
-                       return wfMsgForContent( 'autosumm-new', $truncatedtext );
-               }
-
-               # Blanking autosummaries
-               if ( $oldtext != '' && $newtext == '' ) {
-                       return wfMsgForContent( 'autosumm-blank' );
-               } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
-                       # Removing more than 90% of the article
-
-                       $truncatedtext = $wgContLang->truncate(
-                               $newtext,
-                               max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
-
-                       return wfMsgForContent( 'autosumm-replace', $truncatedtext );
-               }
-
-               # If we reach this point, there's no applicable autosummary for our case, so our
-               # autosummary is empty.
-               return '';
+        return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
 
        /**
@@ -2481,8 +2626,13 @@ class WikiPage extends Page {
         * @param &$hasHistory Boolean: whether the page has a history
         * @return mixed String containing deletion reason or empty string, or boolean false
         *    if no revision occurred
+     * @deprecated since 1.20, use ContentHandler::getAutoDeleteReason() instead
         */
        public function getAutoDeleteReason( &$hasHistory ) {
+        #NOTE: stub for backwards-compatibility.
+
+               $handler = ContentHandler::getForTitle( $this->getTitle() );
+        $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory );
                global $wgContLang;
 
                // Get the last revision
@@ -2679,6 +2829,7 @@ class WikiPage extends Page {
 
                if ( count( $templates_diff ) > 0 ) {
                        # Whee, link updates time.
+            # Note: we are only interested in links here. We don't need to get other SecondaryDataUpdate items from the parser output.
                        $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
                        $u->doUpdate();
                }
@@ -2858,14 +3009,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
+            $modelName = $page->getRevision()->getContentModelName();
+            $format = $page->getRevision()->getContentFormat();
+            $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelName, $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 );
        }
@@ -2905,18 +3062,21 @@ class PoolWorkArticleView extends PoolCounterWork {
 
                $isCurrent = $this->revid === $this->page->getLatest();
 
-               if ( $this->text !== null ) {
-                       $text = $this->text;
+               if ( $this->content !== null ) {
+                       $content = $this->content;
                } elseif ( $isCurrent ) {
-                       $text = $this->page->getRawText();
+            $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
-                       $text = $rev->getText();
+            $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
 
+               $time = - wfTime();
+               $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
+               $time += wfTime();
                $time = - microtime( true );
                $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
                        $this->parserOptions, true, true, $this->revid );
index 08a33f4..2888d24 100644 (file)
@@ -40,14 +40,16 @@ class EditAction extends FormlessAction {
                $context = $this->getContext();
 
                if ( wfRunHooks( 'CustomEditor', array( $page, $user ) ) ) {
+            $handler = ContentHandler::getForTitle( $page->getTitle() );
+
                        if ( ExternalEdit::useExternalEngine( $context, 'edit' )
                                && $this->getName() == 'edit' && !$request->getVal( 'section' )
                                && !$request->getVal( 'oldid' ) )
                        {
-                               $extedit = new ExternalEdit( $context );
+                               $extedit = $handler->createExternalEdit( $context );
                                $extedit->execute();
                        } else {
-                               $editor = new EditPage( $page );
+                               $editor = $handler->createEditPage( $page );
                                $editor->edit();
                        }
                }
index 5615ad5..f07b5b6 100644 (file)
@@ -135,11 +135,20 @@ class RawAction extends FormlessAction {
                                $request->response()->header( "Last-modified: $lastmod" );
 
                                // Public-only due to cache headers
-                               $text = $rev->getText();
+                               $content = $rev->getContent();
+
+                               if ( !$content instanceof TextContent ) {
+                                       wfHttpError( 406, "Not Acceptable", "The requeste page uses the content model `"
+                                                                                                               . $content->getModelName() . "` which is not supported via this interface." );
+                                       die();
+                               }
+
                                $section = $request->getIntOrNull( 'section' );
                                if ( $section !== null ) {
-                                       $text = $wgParser->getSection( $text, $section );
+                                       $content = $content->getSection( $section );
                                }
+
+                               $text = $content->getNativeData();
                        }
                }
 
index 0d9a902..5aba0b5 100644 (file)
@@ -109,7 +109,8 @@ class RollbackAction extends FormlessAction {
                $this->getOutput()->returnToMain( false, $this->getTitle() );
 
                if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) {
-                       $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
+            $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+            $de = $contentHandler->getDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
                        $de->showDiff( '', '' );
                }
        }
index 87f0967..1fac996 100644 (file)
@@ -35,7 +35,8 @@ class ApiComparePages extends ApiBase {
                $rev1 = $this->revisionOrTitleOrId( $params['fromrev'], $params['fromtitle'], $params['fromid'] );
                $rev2 = $this->revisionOrTitleOrId( $params['torev'], $params['totitle'], $params['toid'] );
 
-               $de = new DifferenceEngine( $this->getContext(),
+        $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() );
+        $de = $contentHandler->getDifferenceEngine( $this->getContext(),
                        $rev1,
                        $rev2,
                        null, // rcid
index 8a4a17f..42f9737 100644 (file)
@@ -123,7 +123,7 @@ class ApiDelete extends ApiBase {
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
-                       $reason = $page->getAutoDeleteReason( $hasHistory );
+                       $reason = $page->getAutoDeleteReason( $hasHistory ); #FIXME: use ContentHandler::getAutoDeleteReason()
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
index 796b049..133ca9c 100644 (file)
@@ -117,21 +117,23 @@ class ApiEditPage extends ApiBase {
                        // We do want getContent()'s behavior for non-existent
                        // MediaWiki: pages, though
                        if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
-                               $content = '';
+                               $content = null;
+                $text = '';
                        } else {
-                               $content = $articleObj->getContent();
+                $content = $articleObj->getContentObject();
+                $text = ContentHandler::getContentText( $content ); #FIXME: serialize?! get format from params?...
                        }
 
                        if ( !is_null( $params['section'] ) ) {
                                // Process the content for section edits
-                               global $wgParser;
                                $section = intval( $params['section'] );
-                               $content = $wgParser->getSection( $content, $section, false );
-                               if ( $content === false ) {
+                $sectionContent = $content->getSection( $section );
+                $text = ContentHandler::getContentText( $sectionContent ); #FIXME: serialize?! get format from params?...
+                               if ( $text === false || $text === null ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
-                       $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
+                       $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
 
@@ -248,7 +250,9 @@ class ApiEditPage extends ApiBase {
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
 
-               $ep = new EditPage( $articleObj );
+        $handler = ContentHandler::getForTitle( $titleObj );
+               $ep = $handler->createEditPage( $articleObj );
+
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
 
index 141f779..afff5fd 100644 (file)
@@ -318,9 +318,9 @@ class ApiParse extends ApiBase {
 
                $page = WikiPage::factory( $titleObj );
 
-               if ( $this->section !== false ) {
+               if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ...
                        $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
-                                       ? 'page id ' . $pageId : $titleObj->getText() );
+                                       ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section...
 
                        // Not cached (save or load)
                        return $wgParser->parse( $this->text, $titleObj, $popts );
@@ -329,13 +329,14 @@ class ApiParse extends ApiBase {
                        // getParserOutput will save to Parser cache if able
                        $pout = $page->getParserOutput( $popts );
                        if ( $getWikitext ) {
-                               $this->text = $page->getRawText();
+                $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
+                               $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
                        }
                        return $pout;
                }
        }
 
-       private function getSectionText( $text, $what ) {
+       private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection
                global $wgParser;
                // Not cached (save or load)
                $text = $wgParser->getSection( $text, $this->section, false );
index 9e9320f..77898cf 100644 (file)
@@ -90,11 +90,11 @@ class ApiPurge extends ApiBase {
 
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
-                                               true, true, $page->getLatest() );
+                                               true, true, $page->getLatest() ); #FIXME: content!
 
                                        # Update the links tables
-                                       $u = new LinksUpdate( $title, $p_result );
-                                       $u->doUpdate();
+                    $updates = $p_result->getLinksUpdateAndOtherUpdates( $title );
+                    SecondaryDataUpdate::runUpdates( $updates );
 
                                        $r['linkupdate'] = '';
 
index fa58bdf..11ea371 100644 (file)
@@ -114,7 +114,7 @@ class ApiQueryRevisions extends ApiQueryBase {
                }
 
                if ( !is_null( $params['difftotext'] ) ) {
-                       $this->difftotext = $params['difftotext'];
+                       $this->difftotext = $params['difftotext']; #FIXME: handle non-text content!
                } elseif ( !is_null( $params['diffto'] ) ) {
                        if ( $params['diffto'] == 'cur' ) {
                                $params['diffto'] = 0;
@@ -503,11 +503,13 @@ class ApiQueryRevisions extends ApiQueryBase {
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
+                $handler = ContentHandler::getForTitle( $title );
+
                                if ( !is_null( $this->difftotext ) ) {
-                                       $engine = new DifferenceEngine( $context );
-                                       $engine->setText( $text, $this->difftotext );
+                                       $engine = $handler->getDifferenceEngine( $context );
+                                       $engine->setText( $text, $this->difftotext ); #FIXME: use content objects!...
                                } else {
-                                       $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
+                                       $engine = $handler->getDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
index 72eb5d3..56cee2e 100644 (file)
@@ -15,7 +15,7 @@
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp {
+class _DiffOp { #FIXME: no longer private!
        var $type;
        var $orig;
        var $closing;
@@ -44,7 +44,7 @@ class _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Copy extends _DiffOp {
+class _DiffOp_Copy extends _DiffOp { #FIXME: no longer private!
        var $type = 'copy';
 
        function __construct( $orig, $closing = false ) {
@@ -68,7 +68,7 @@ class _DiffOp_Copy extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Delete extends _DiffOp {
+class _DiffOp_Delete extends _DiffOp { #FIXME: no longer private!
        var $type = 'delete';
 
        function __construct( $lines ) {
@@ -89,7 +89,7 @@ class _DiffOp_Delete extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Add extends _DiffOp {
+class _DiffOp_Add extends _DiffOp { #FIXME: no longer private!
        var $type = 'add';
 
        function __construct( $lines ) {
@@ -110,7 +110,7 @@ class _DiffOp_Add extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffOp_Change extends _DiffOp {
+class _DiffOp_Change extends _DiffOp { #FIXME: no longer private!
        var $type = 'change';
 
        function __construct( $orig, $closing ) {
@@ -150,7 +150,7 @@ class _DiffOp_Change extends _DiffOp {
  * @private
  * @ingroup DifferenceEngine
  */
-class _DiffEngine {
+class _DiffEngine { #FIXME: no longer private!
 
        const MAX_XREF_LENGTH =  10000;
 
@@ -637,7 +637,7 @@ class _DiffEngine {
  * @private
  * @ingroup DifferenceEngine
  */
-class Diff {
+class Diff extends DiffResult {
        var $edits;
 
        /**
@@ -647,11 +647,36 @@ class Diff {
         * @param $from_lines array An array of strings.
         *                (Typically these are lines from a file.)
         * @param $to_lines array An array of strings.
+        * @param $eng _DiffEngine|null The diff engine to use.
         */
-       function __construct( $from_lines, $to_lines ) {
-               $eng = new _DiffEngine;
-               $this->edits = $eng->diff( $from_lines, $to_lines );
-               // $this->_check($from_lines, $to_lines);
+       function __construct( $from_lines, $to_lines, $eng = null  ) {
+               if ( !$eng ) {
+                       $eng = new _DiffEngine();
+               }
+
+               $edits = $eng->diff( $from_lines, $to_lines );
+
+               parent::__construct( $edits );
+
+               //$this->_check( $from_lines, $to_lines );
+       }
+}
+
+/**
+ * Class representing the result of 'diffin' two sequences of strings.
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class DiffResult {
+
+       /**
+        * Constructor.
+        *
+        * @param $edits array An array of Edit.
+        */
+       function __construct( $edits  ) {
+               $this->edits = $edits;
        }
 
        /**
index e8f35f0..348f2dc 100644 (file)
@@ -23,7 +23,7 @@ class DifferenceEngine extends ContextSource {
         * @private
         */
        var $mOldid, $mNewid;
-       var $mOldtext, $mNewtext;
+       var $mOldContent, $mNewContent;
        protected $mDiffLang;
 
        /**
@@ -486,20 +486,21 @@ class DifferenceEngine extends ContextSource {
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
 
-                       if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
+                       if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { #NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
-                               if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
-                                       // Wrap the whole lot in a <pre> and don't parse
-                                       $m = array();
-                                       preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
-                                       $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                                       $out->addHTML( htmlspecialchars( $this->mNewtext ) );
-                                       $out->addHTML( "\n</pre>\n" );
+                               if ( !Hook::isRegistered( 'ShowRawCssJs' )
+                    || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
+                    // use the content object's own rendering
+                    $po = $this->mContentObject->getParserOutput();
+                    $out->addHTML( $po->getText() );
                                }
-                       } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
-                               // Handled by extension
+            } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
+                // Handled by extension
+            } elseif( Hooks::isRegistered( 'ArticleViewCustom' )
+                    && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
+                // Handled by extension
                        } else {
                                // Normal page
                                if ( $this->getTitle()->equals( $this->mNewPage ) ) {
@@ -630,7 +631,9 @@ class DifferenceEngine extends ContextSource {
                        return false;
                }
 
-               $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+        #TODO: make sure both Content objects have the same content model. What do we do if they don't?
+
+               $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
 
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
@@ -667,14 +670,48 @@ class DifferenceEngine extends ContextSource {
                }
        }
 
+    /**
+     * Generate a diff, no caching.
+     *
+     * Subclasses may override this to provide a
+     *
+     * @param $old Content: old content
+     * @param $new Content: new content
+     */
+    function generateContentDiffBody( Content $old, Content $new ) {
+        #XXX: generate a warning if $old or $new are not instances of TextContent?
+        #XXX: fail if $old and $new don't have the same content model? or what?
+
+        $otext = $old->serialize();
+        $ntext = $new->serialize();
+
+        #XXX: text should be "already segmented". what does that mean?
+        return $this->generateTextDiffBody( $otext, $ntext );
+    }
+
+    /**
+     * Generate a diff, no caching
+     *
+     * @param $otext String: old text, must be already segmented
+     * @param $ntext String: new text, must be already segmented
+     * @deprecated since 1.20, use generateContentDiffBody() instead!
+     */
+    function generateDiffBody( $otext, $ntext ) {
+        wfDeprecated( __METHOD__, "1.20" );
+
+        return $this->generateTextDiffBody( $otext, $ntext );
+    }
+
        /**
         * Generate a diff, no caching
         *
+     * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
+     *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
         * @return bool|string
         */
-       function generateDiffBody( $otext, $ntext ) {
+       function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
 
                wfProfileIn( __METHOD__ );
@@ -928,13 +965,28 @@ class DifferenceEngine extends ContextSource {
 
        /**
         * Use specified text instead of loading from the database
+     * @deprecated since 1.20
         */
-       function setText( $oldText, $newText ) {
-               $this->mOldtext = $oldText;
-               $this->mNewtext = $newText;
-               $this->mTextLoaded = 2;
-               $this->mRevisionsLoaded = true;
-       }
+       function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()!
+        wfDeprecated( __METHOD__, "1.20" );
+
+        $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
+        $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
+
+        $this->setContent( $oldContent, $newContent );
+    }
+
+    /**
+     * Use specified text instead of loading from the database
+     * @since 1.20
+     */
+    function setContent( Content $oldContent, Content $newContent ) {
+        $this->mOldContent = $oldContent;
+        $this->mNewContent = $newContent;
+
+        $this->mTextLoaded = 2;
+        $this->mRevisionsLoaded = true;
+    }
 
        /**
         * Set the language in which the diff text is written
@@ -1059,14 +1111,14 @@ class DifferenceEngine extends ContextSource {
                        return false;
                }
                if ( $this->mOldRev ) {
-                       $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
-                       if ( $this->mOldtext === false ) {
+                       $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
+                       if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
-                       $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
-                       if ( $this->mNewtext === false ) {
+                       $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
+                       if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
@@ -1087,7 +1139,7 @@ class DifferenceEngine extends ContextSource {
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
-               $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
+               $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
                return true;
        }
 }
index 02d7cb1..68573e0 100644 (file)
@@ -70,6 +70,13 @@ class Ibm_db2Updater extends DatabaseUpdater {
                        array( 'addField', 'revision',      'rev_sha1',         'patch-rev_sha1.sql' ),
                        array( 'addField', 'archive',       'ar_sha1',          'patch-ar_sha1.sql' ),
 
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        // 1.20
                        array( 'addTable', 'config',                            'patch-config.sql' ),
                );
index 5e6ae7e..8182733 100644 (file)
@@ -191,6 +191,14 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
+
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
 
                        // 1.20
index 73bbc57..a79d0b9 100644 (file)
@@ -52,6 +52,13 @@ class OracleUpdater extends DatabaseUpdater {
                        array( 'addField', 'job', 'job_timestamp', 'patch-job_timestamp_field.sql' ),
                        array( 'addIndex', 'job', 'i02', 'patch-job_timestamp_index.sql' ),
 
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        //1.20
                        array( 'addTable', 'config', 'patch-config.sql' ),
 
index a98c4db..8962043 100644 (file)
@@ -70,6 +70,14 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
+
+            // 1.20
+            // content model stuff for WikiData
+            array( 'addField', 'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
+            array( 'addField', 'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
+            array( 'addField', 'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
+            array( 'addField', 'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
+            array( 'addField', 'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
 
                        // 1.20
index 1aa206f..dcc5ab1 100644 (file)
@@ -46,9 +46,11 @@ class RefreshLinksJob extends Job {
                $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
                wfProfileOut( __METHOD__.'-parse' );
                wfProfileIn( __METHOD__.'-update' );
-               $update = new LinksUpdate( $this->title, $parserOutput, false );
-               $update->doUpdate();
-               wfProfileOut( __METHOD__.'-update' );
+
+        $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $this->title, false );
+        SecondaryDataUpdate::runUpdates( $updates );
+
+        wfProfileOut( __METHOD__.'-update' );
                wfProfileOut( __METHOD__ );
                return true;
        }
@@ -118,8 +120,10 @@ class RefreshLinksJob2 extends Job {
                        $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
                        wfProfileOut( __METHOD__.'-parse' );
                        wfProfileIn( __METHOD__.'-update' );
-                       $update = new LinksUpdate( $title, $parserOutput, false );
-                       $update->doUpdate();
+
+            $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $title, false );
+            SecondaryDataUpdate::runUpdates( $updates );
+
                        wfProfileOut( __METHOD__.'-update' );
                        wfWaitForSlaves();
                }
index 0d597e8..9cb247f 100644 (file)
@@ -30,7 +30,7 @@ class CacheTime {
         */
        function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
 
-       /**
+       /**abstract
         * Sets the number of seconds after which this object should expire.
         * This value is used with the ParserCache.
         * If called with a value greater than the value provided at any previous call,
@@ -141,8 +141,9 @@ class ParserOutput extends CacheTime {
                $mProperties = array(),       # Name/value pairs to be cached in the DB
                $mTOCHTML = '',               # HTML of the TOC
                $mTimestamp;                  # Timestamp of the revision
-       private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
-       private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
+           private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
+           private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
+        private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
 
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
@@ -449,4 +450,53 @@ class ParserOutput extends CacheTime {
         function recordOption( $option ) {
                 $this->mAccessedOptions[$option] = true;
         }
+
+    /**
+     * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to
+     * store any secondary information extracted from the page's content.
+     *
+     * @param SecondaryDataUpdate $update
+     */
+    public function addSecondaryDataUpdate( SecondaryDataUpdate $update ) {
+        $this->mSecondaryDataUpdates[] = $update;
+    }
+
+    /**
+     * Returns any SecondaryDataUpdate jobs to be executed in order to store secondary information
+     * extracted from the page's content.
+     *
+     * This does not automatically include an LinksUpdate object for the links in this ParserOutput instance.
+     * Use getLinksUpdateAndOtherUpdates() if you want that.
+     *
+     * @return array an array of instances of SecondaryDataUpdate
+     */
+    public function getSecondaryDataUpdates() {
+        return $this->mSecondaryDataUpdates;
+    }
+
+    /**
+     * Conveniance method that returns any SecondaryDataUpdate jobs to be executed in order
+     * to store secondary information extracted from the page's content, including the LinksUpdate object
+     * for all links stopred in this ParserOutput object.
+     *
+     * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
+     * @param $recursive Boolean: queue jobs for recursive updates?
+     *
+     * @return array an array of instances of SecondaryDataUpdate
+     */
+    public function getLinksUpdateAndOtherUpdates( Title $title = null, $recursive = true ) {
+        if ( empty( $title ) ) {
+            $title = Title::newFromText( $this->getTitleText() );
+        }
+
+        $linksUpdate = new LinksUpdate( $title, $this, $recursive );
+
+        if ( empty( $this->mSecondaryDataUpdates ) ) {
+            return array( $linksUpdate );
+        } else {
+            $updates = array_merge( $this->mSecondaryDataUpdates, array( $linksUpdate ) );
+        }
+
+        return $updates;
+    }
 }
index 91a51f8..631ca64 100644 (file)
@@ -80,7 +80,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
                if ( !$revision ) {
                        return null;
                }
-               return $revision->getRawText();
+               return $revision->getRawText(); #FIXME: get raw data from content object after checking the type;
        }
 
        /* Methods */
index 9e3c52b..878bda0 100644 (file)
@@ -111,7 +111,8 @@ class SpecialComparePages extends SpecialPage {
                $rev2 = self::revOrTitle( $data['Revision2'], $data['Page2'] );
 
                if( $rev1 && $rev2 ) {
-                       $de = new DifferenceEngine( $form->getContext(),
+            $contentHandler = ContentHandler::getForModelName( $rev1->getContentModelName() );
+                       $de = $contentHandler->getDifferenceEngine( $form->getContext(),
                                $rev1,
                                $rev2,
                                null, // rcid
index 06b578d..47e88d0 100644 (file)
@@ -116,7 +116,8 @@ class PageArchive {
                $res = $dbr->select( 'archive',
                        array(
                                'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
-                               'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
+                               'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
+                'ar_content_format', 'ar_content_model'
                        ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
@@ -189,6 +190,8 @@ class PageArchive {
                                'ar_deleted',
                                'ar_len',
                                'ar_sha1',
+                'ar_content_format',
+                'ar_content_model',
                        ),
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
@@ -462,7 +465,9 @@ class PageArchive {
                                'ar_deleted',
                                'ar_page_id',
                                'ar_len',
-                               'ar_sha1' ),
+                               'ar_sha1',
+                'ar_content_format',
+                'ar_content_model' ),
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@ -892,7 +897,8 @@ class SpecialUndelete extends SpecialPage {
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
-               $diffEngine = new DifferenceEngine( $this->getContext() );
+        $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
+               $diffEngine = $contentHandler->getDifferenceEngine( $this->getContext() );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
@@ -909,8 +915,8 @@ class SpecialUndelete extends SpecialPage {
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
-                       $diffEngine->generateDiffBody(
-                               $previousRev->getText(), $currentRev->getText() ) .
+                       $diffEngine->generateContentDiffBody(
+                               $previousRev->getContent(), $currentRev->getContent() ) .
                        "</table>" .
                        "</div>\n"
                );
index 6b07bc6..8fd966b 100644 (file)
@@ -887,6 +887,7 @@ $1',
 'portal-url'           => 'Project:Community portal',
 'privacy'              => 'Privacy policy',
 'privacypage'          => 'Project:Privacy policy',
+'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
 
 'badaccess'        => 'Permission error',
 'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
diff --git a/maintenance/archives/patch-archive-ar_content_format.sql b/maintenance/archives/patch-archive-ar_content_format.sql
new file mode 100644 (file)
index 0000000..81f9fca
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-archive-ar_content_model.sql b/maintenance/archives/patch-archive-ar_content_model.sql
new file mode 100644 (file)
index 0000000..1a8b630
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/archive
+  ADD ar_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-page-page_content_model.sql b/maintenance/archives/patch-page-page_content_model.sql
new file mode 100644 (file)
index 0000000..30434d9
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/page
+  ADD page_content_model varbinary(32) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_format.sql b/maintenance/archives/patch-revision-rev_content_format.sql
new file mode 100644 (file)
index 0000000..22aeb8a
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_format varbinary(64) DEFAULT NULL;
diff --git a/maintenance/archives/patch-revision-rev_content_model.sql b/maintenance/archives/patch-revision-rev_content_model.sql
new file mode 100644 (file)
index 0000000..1ba0572
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/revision
+  ADD rev_content_model varbinary(32) DEFAULT NULL;
index 6626cbc..cec91fb 100644 (file)
@@ -67,7 +67,7 @@ class PopulateRevisionLength extends LoggedUpdateMaintenance {
                        # Go through and update rev_len from these rows.
                        foreach ( $res as $row ) {
                                $rev = new Revision( $row );
-                               $text = $rev->getRawText();
+                               $text = $rev->getRawText(); #FIXME: go via Content object; #FIXME: get size via Content object
                                if ( !is_string( $text ) ) {
                                        # This should not happen, but sometimes does (bug 20757)
                                        $this->output( "Text of revision {$row->rev_id} unavailable!\n" );
index 7abbc90..dd8c8a7 100644 (file)
@@ -221,6 +221,12 @@ class RefreshLinks extends Maintenance {
 
                $options = new ParserOptions;
                $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
+
+        $updates = $parserOutput->getLinksUpdateAndOtherUpdates( $title, false );
+        SecondaryDataUpdate::runUpdates( $updates );
+
+        $dbw->commit();
+        // TODO: We don't know what happens here.
                $update = new LinksUpdate( $title, $parserOutput, false );
                $update->doUpdate();
                $dbw->commit( __METHOD__ );
index a848bf5..a0601f1 100644 (file)
@@ -260,7 +260,10 @@ CREATE TABLE /*_*/page (
   page_latest int unsigned NOT NULL,
 
   -- Uncompressed length in bytes of the page's current source text.
-  page_len int unsigned NOT NULL
+  page_len int unsigned NOT NULL,
+
+  -- content model
+  page_content_model  varbinary(32) default NULL
 ) /*$wgDBTableOptions*/;
 
 CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
@@ -316,7 +319,13 @@ CREATE TABLE /*_*/revision (
   rev_parent_id int unsigned default NULL,
 
   -- SHA-1 text content hash in base-36
-  rev_sha1 varbinary(32) NOT NULL default ''
+  rev_sha1 varbinary(32) NOT NULL default '',
+
+  -- content model
+  rev_content_model  varbinary(32) default NULL,
+
+  -- content format (mime type)
+  rev_content_format varbinary(64) default NULL
 
 ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
 -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit
@@ -427,7 +436,14 @@ CREATE TABLE /*_*/archive (
   ar_parent_id int unsigned default NULL,
 
   -- SHA-1 text content hash in base-36
-  ar_sha1 varbinary(32) NOT NULL default ''
+  ar_sha1 varbinary(32) NOT NULL default '',
+
+  -- content model
+  ar_content_model  varbinary(32) default NULL,
+
+  -- content format (mime type)
+  ar_content_format varbinary(64) default NULL
+
 ) /*$wgDBTableOptions*/;
 
 CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);