merged master
authordaniel <daniel.kinzler@wikimedia.de>
Tue, 29 May 2012 15:01:13 +0000 (17:01 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Tue, 29 May 2012 15:01:13 +0000 (17:01 +0200)
18 files changed:
1  2 
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/Defines.php
includes/EditPage.php
includes/ImagePage.php
includes/Revision.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiQueryRevisions.php
includes/resourceloader/ResourceLoaderWikiModule.php
languages/Language.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
tests/phpunit/includes/WikiPageTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php

diff --combined includes/Article.php
@@@ -1,6 -1,22 +1,22 @@@
  <?php
  /**
-  * File for articles
+  * User interface for page actions.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   */
  
@@@ -9,7 -25,7 +25,7 @@@
   *
   * This maintains WikiPage functions for backwards compatibility.
   *
-  * @TODO: move and rewrite code to an Action class
+  * @todo move and rewrite code to an Action class
   *
   * See design.txt for an overview.
   * Note: edit user interface and cache support functions have been
@@@ -23,49 -39,69 +39,76 @@@ class Article extends Page 
         */
  
        /**
-        * @var IContextSource
+        * The context this Article is executed in
+        * @var IContextSource $mContext
         */
        protected $mContext;
  
        /**
-        * @var WikiPage
+        * The WikiPage object of this instance
+        * @var WikiPage $mPage
         */
        protected $mPage;
  
        /**
-        * @var ParserOptions: ParserOptions object for $wgUser articles
+        * ParserOptions object for $wgUser articles
+        * @var ParserOptions $mParserOptions
         */
        public $mParserOptions;
  
 -       * Content of the revision we are working on
+       /**
 -      var $mContent;                    // !<
++       * Text of the revision we are working on
+        * @var string $mContent
+        */
-       var $mContentObject;
 +      var $mContent;                    // !< #BC cruft
 +
 +      /**
++       * Content of the revision we are working on
 +       * @var Content
 +       * @since 1.WD
 +       */
++      var $mContentObject;              // !<
  
+       /**
+        * Is the content ($mContent) already loaded?
+        * @var bool $mContentLoaded
+        */
        var $mContentLoaded = false;      // !<
+       /**
+        * The oldid of the article that is to be shown, 0 for the
+        * current revision
+        * @var int|null $mOldId
+        */
        var $mOldId;                      // !<
  
        /**
-        * @var Title
+        * Title from which we were redirected here
+        * @var Title $mRedirectedFrom
         */
        var $mRedirectedFrom = null;
  
        /**
-        * @var mixed: boolean false or URL string
+        * URL to redirect to or false if none
+        * @var string|false $mRedirectUrl
         */
        var $mRedirectUrl = false;        // !<
+       /**
+        * Revision ID of revision we are working on
+        * @var int $mRevIdFetched
+        */
        var $mRevIdFetched = 0;           // !<
  
        /**
-        * @var Revision
+        * Revision we are working on
+        * @var Revision $mRevision
         */
        var $mRevision = null;
  
        /**
-        * @var ParserOutput
+        * ParserOutput object
+        * @var ParserOutput $mParserOutput
         */
        var $mParserOutput;
  
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
 +       * @deprecated in 1.WD; use getContentObject() instead
 +       *
         * @return string Return the text of this revision
         */
        public function getContent() {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = $this->getContentObject();
 +              return ContentHandler::getContentText( $content );
 +      }
 +
 +      /**
 +       * Returns a Content object representing the pages effective display content,
 +     * not necessarily the revision's content!
 +     *
 +     * Note that getContent/loadContent do not follow redirects anymore.
 +       * If you need to fetch redirectable content easily, try
 +       * the shortcut in WikiPage::getRedirectTarget()
 +       *
 +       * This function has side effects! Do not use this function if you
 +       * only want the real revision text if any.
 +       *
 +       * @return Content Return the content of this revision
 +       *
 +       * @since 1.WD
 +       */
 +   protected function getContentObject() {
 +              global $wgUser;
                wfProfileIn( __METHOD__ );
  
                if ( $this->mPage->getID() === 0 ) {
                                if ( $text === false ) {
                                        $text = '';
                                }
 +
 +                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
 -                              $text = wfMsgExt( $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
 +                              $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * Get text of an article from database
         * Does *NOT* follow redirects.
         *
 +       * @protected
 +       * @note this is really internal functionality that should really NOT be used by other functions. For accessing
 +       *       article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
 +       *       uses this method to retrieve page text from the database, so the function has to remain public for now.
 +       *
         * @return mixed string containing article contents, or false if null
 +       * @deprecated in 1.WD, use WikiPage::getContent() instead
         */
 -      function fetchContent() {
 -              if ( $this->mContentLoaded ) {
 +      function fetchContent() { #BC cruft!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
  
                wfProfileIn( __METHOD__ );
  
 +              $content = $this->fetchContentObject();
 +
 +              $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
 +              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft! #XXX: can we deprecate that hook?
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.WD
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
 -              $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
 +              $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; // @todo: this isn't page content but a UI message. horrible.
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
 +              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
 -              return $this->mContent;
 +              return $this->mContentObject;
        }
  
        /**
         * @return Revision|null
         */
        public function getRevisionFetched() {
 -              $this->fetchContent();
 +              $this->fetchContentObject();
  
                return $this->mRevision;
        }
                                        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 ) {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
 -                                      } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 +                                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
 +                                              # Allow extensions do their own custom view for certain pages
 +                                              $outputDone = true;
 +                                      } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $outputPage ) ) ) { #FIXME: fetchContent() is deprecated!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
  
 +                                      // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this?
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
 -                                              $this->getRevIdFetched(), $useParserCache, $this->getContent() );
 +                                              $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() );
  
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
 +                      $po = $this->mContentObject->getParserOutput( $this->getTitle() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
         * merging of several policies using array_merge().
         * @param $policy Mixed, returns empty array on null/false/'', transparent
         *            to already-converted arrays, converts String.
-        * @return Array: 'index' => <indexpolicy>, 'follow' => <followpolicy>
+        * @return Array: 'index' => \<indexpolicy\>, 'follow' => \<followpolicy\>
         */
        public static function formatRobotPolicy( $policy ) {
                if ( is_array( $policy ) ) {
                // 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
        /**
         * Handle action=purge
         * @deprecated since 1.19
+        * @return Action|bool|null false if the action is disabled, null if it is not recognised
         */
        public function purge() {
                return Action::factory( 'purge', $this )->show();
         * @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 ) ****** //
         * @param $newtext
         * @param $flags
         * @return string
 +       * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --combined includes/AutoLoader.php
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * This defines autoloading handler for whole MediaWiki framework
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   */
  
@@@ -57,6 -72,7 +72,7 @@@ $wgAutoloadLocalClasses = array
        'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
        'DerivativeRequest' => 'includes/WebRequest.php',
        'DeviceDetection' => 'includes/DeviceDetection.php',
+       'DeviceProperties' => 'includes/DeviceDetection.php',
        'DiffHistoryBlob' => 'includes/HistoryBlob.php',
        'DoubleReplacer' => 'includes/StringUtils.php',
        'DummyLinker' => 'includes/Linker.php',
        'HttpRequest' => 'includes/HttpFunctions.old.php',
        'ICacheHelper' => 'includes/CacheHelper.php',
        'IcuCollation' => 'includes/Collation.php',
+       'IDeviceProperties' => 'includes/DeviceDetection.php',
+       'IDeviceDetector' => 'includes/DeviceDetection.php',
        'IdentityCollation' => 'includes/Collation.php',
        'ImageGallery' => 'includes/ImageGallery.php',
        'ImageHistoryList' => 'includes/ImagePage.php',
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
  
 +    # content handler
 +    'Content' => 'includes/Content.php',
 +    'ContentHandler' => 'includes/ContentHandler.php',
 +    'CssContent' => 'includes/Content.php',
 +    'CssContentHandler' => 'includes/ContentHandler.php',
 +    'JavaScriptContent' => 'includes/Content.php',
 +    'JavaScriptContentHandler' => 'includes/ContentHandler.php',
 +    'MessageContent' => 'includes/Content.php',
 +    'TextContent' => 'includes/Content.php',
 +    'WikitextContent' => 'includes/Content.php',
 +    'WikitextContentHandler' => 'includes/ContentHandler.php',
 +
        # includes/actions
        'CachedAction' => 'includes/actions/CachedAction.php',
        'CreditsAction' => 'includes/actions/CreditsAction.php',
        'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
        'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
        'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
 +        'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
        'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
        'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
  
 +      # tests/phpunit
 +      'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
 +      'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
 +      'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
 +      'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
 +      'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +      'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +
        # tests/parser
        'ParserTest' => 'tests/parser/parserTest.inc',
        'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php',
@@@ -1077,6 -1074,7 +1095,7 @@@ class AutoLoader 
         * Sanitizer that have define()s outside of their class definition. Of course
         * this wouldn't be necessary if everything in MediaWiki was class-based. Sigh.
         *
+        * @param $class string
         * @return Boolean Return the results of class_exists() so we know if we were successful
         */
        static function loadClass( $class ) {
@@@ -1,6 -1,7 +1,7 @@@
  <?php
  /**
-  * @file
+  * Default values for configuration settings.
+  *
   *
   *                 NEVER EDIT THIS FILE
   *
   *
   * Documentation is in the source and on:
   * http://www.mediawiki.org/wiki/Manual:Configuration_settings
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
+  * @file
   */
  
  /**
@@@ -310,9 -328,11 +328,11 @@@ $wgImgAuthPublicTest = true
   *   - zones            Associative array of zone names that each map to an array with:
   *                          container : backend container name the zone is in
   *                          directory : root path within container for the zone
-  *                      Zones default to using <repo name>-<zone> as the
-  *                      container name and the container root as the zone directory.
-  *   - url              Base public URL
+  *                          url       : base URL to the root of the zone
+  *                      Zones default to using <repo name>-<zone name> as the container name
+  *                      and default to using the container root as the zone's root directory.
+  *                      Nesting of zone locations within other zones should be avoided.
+  *   - url              Public zone URL. The 'zones' settings take precedence.
   *   - hashLevels       The number of directory levels for hash-based division of files
   *   - thumbScriptUrl   The URL for thumb.php (optional, not recommended)
   *   - transformVia404  Whether to skip media file transformation on parse and rely on a 404
@@@ -640,40 -660,6 +660,40 @@@ $wgMediaHandlers = array
        'image/x-djvu' => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model id to a class name
 + */
 +$wgContentHandlers = array(
 +    CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +    CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
 +);
 +
 +/**
 + * Mime types for content formats.
 + * Each entry in the array maps a content format to a mime type.
 + *
 + * Extensions that define their own content formats can register
 + * the appropriate mime types in this array.
 + *
 + * Such extensions shall use content format IDs
 + * larger than 100 and register the ids they use at
 + * <http://mediawiki.org/ContentHandler/registry>
 + * to avoid conflicts with other extensions.
 + */
 +$wgContentFormatMimeTypes = array(
 +      CONTENT_FORMAT_WIKITEXT => 'text/x-wiki',
 +      CONTENT_FORMAT_JAVASCRIPT => 'text/javascript',
 +      CONTENT_FORMAT_CSS => 'text/css',
 +      CONTENT_FORMAT_TEXT => 'text/plain',
 +      CONTENT_FORMAT_HTML => 'text/html',
 +      CONTENT_FORMAT_XML => 'application/xml',
 +      CONTENT_FORMAT_JSON => 'application/json',
 +      CONTENT_FORMAT_SERIALIZED => 'application/vnd.php.serialized',
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -1332,6 -1318,7 +1352,7 @@@ $wgSharedTables = array( 'user', 'user_
   *                  - DBO_TRX -- wrap entire request in a transaction
   *                  - DBO_IGNORE -- ignore errors (not useful in LocalSettings.php)
   *                  - DBO_NOBUFFER -- turn off buffering (not useful in LocalSettings.php)
+  *                  - DBO_PERSISTENT -- enables persistent database connections
   *
   *   - max lag:     (optional) Maximum replication lag before a slave will taken out of rotation
   *   - max threads: (optional) Maximum number of running threads
@@@ -5775,6 -5762,19 +5796,19 @@@ $wgCompiledFiles = array()
  /** @} */ # End of HipHop compilation }
  
  
+ /************************************************************************//**
+  * @name   Mobile support
+  * @{
+  */
+ /**
+  * Name of the class used for mobile device detection, must be inherited from
+  * IDeviceDetector.
+  */
+ $wgDeviceDetectionClass = 'DeviceDetection';
+ /** @} */ # End of Mobile support }
  /************************************************************************//**
   * @name   Miscellaneous
   * @{
@@@ -5853,31 -5853,6 +5887,31 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
 +/**
 + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
 + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
 + * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
 + */
 +$wgNamespaceContentModels = array();
 +
 +/**
 + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
 + *
 + * * 'ignore': return null
 + * * 'fail': throw an MWException
 + * * 'serializeContent': serializeContent to default format
 + */
 +$wgContentHandlerTextFallback = 'ignore';
 +
 +/**
 + * Compatibility switch for running ContentHandler code withoput a schema update.
 + * Set to false to disable use of the database fields introduced by the ContentHandler facility.
 + *
 + * @deprecated this is only here to allow code deployment without a database schema update on large sites.
 + *             get rid of it in the next version.
 + */
 +$wgContentHandlerUseDB = true;
 +
  /**
   * For really cool vim folding this needs to be at the end:
   * vim: foldmarker=@{,@} foldmethod=marker
diff --combined includes/Defines.php
@@@ -6,6 -6,21 +6,21 @@@
   * since this file will not be executed during request startup for a compiled
   * MediaWiki.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   */
  
@@@ -244,7 -259,7 +259,7 @@@ define( 'APCOND_BLOCKED', 8 )
  define( 'APCOND_ISBOT', 9 );
  /**@}*/
  
 -/**
 +/** @{
   * Protocol constants for wfExpandUrl()
   */
  define( 'PROTO_HTTP', 'http://' );
@@@ -253,40 -268,3 +268,40 @@@ define( 'PROTO_RELATIVE', '//' )
  define( 'PROTO_CURRENT', null );
  define( 'PROTO_CANONICAL', 1 );
  define( 'PROTO_INTERNAL', 2 );
 +/**@}*/
 +
 +/**@{
 + * Content model ids, used by Content and ContentHandler
 + *
 + * Extensions that define their own content models shall use IDs
 + * larger than 100 and register the ids they use at
 + * <http://mediawiki.org/ContentHandler/registry>
 + * to avoid conflicts with other extensions.
 + */
 +define( 'CONTENT_MODEL_WIKITEXT', 1 );
 +define( 'CONTENT_MODEL_JAVASCRIPT', 2 );
 +define( 'CONTENT_MODEL_CSS', 3 );
 +define( 'CONTENT_MODEL_TEXT', 4 );
 +/**@}*/
 +
 +/**@{
 + * Content format ids, used by Content and ContentHandler.
 + * Use ContentHander::getFormatMimeType() to get the associated mime type.
 + * Register mime types in $wgContentFormatMimeTypes.
 + *
 + * Extensions that define their own content formats shall use IDs
 + * larger than 100 and register the ids they use at
 + * <http://mediawiki.org/ContentHandler/registry>
 + * to avoid conflicts with other extensions.
 + */
 +define( 'CONTENT_FORMAT_WIKITEXT', 1 ); // wikitext
 +define( 'CONTENT_FORMAT_JAVASCRIPT', 2 ); // for js pages
 +define( 'CONTENT_FORMAT_CSS', 3 );  // for css pages
 +define( 'CONTENT_FORMAT_TEXT', 4 ); // for future use, e.g. with some plain-html messages.
 +define( 'CONTENT_FORMAT_HTML', 5 ); // for future use, e.g. with some plain-html messages.
 +define( 'CONTENT_FORMAT_SERIALIZED', 11 ); // for future use with the api, and for use by extensions
 +define( 'CONTENT_FORMAT_JSON', 12 ); // for future use with the api, and for use by extensions
 +define( 'CONTENT_FORMAT_XML', 13 ); // for future use with the api, and for use by extensions
 +/**@}*/
 +
 +
diff --combined includes/EditPage.php
@@@ -1,6 -1,22 +1,22 @@@
  <?php
  /**
-  * Contains the EditPage class
+  * Page edition user interface.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   */
  
@@@ -139,11 -155,6 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        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
        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;
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModel();
 +
 +              $handler = ContentHandler::getForModelID( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                        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 );
                # 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() ) ) );
                } 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     = '';
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        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' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.WD
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
 +                      $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
 +      }
 +
 +      private function getContentObject( $def_content = null ) { #FIXME: use this!
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = ContentHandler::makeContent( $msg, $this->mTitle );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # 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 {
                                                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;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.WD
         * @return string
         */
 -      private function getCurrentText() {
 -              $text = $this->mArticle->getRawText();
 -              if ( $text === false ) {
 -                      return '';
 +      private function getCurrentContent() {
 +              $rev = $this->mArticle->getRevision();
 +              $content = $rev ? $rev->getContent( Revision::RAW ) : null;
 +
 +              if ( $content  === false || $content === null ) {
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                } else {
 -                      return $text;
 +                      #FIXME: nasty side-effect!
 +                      $this->content_model = $rev->getContentModel();
 +                      $this->content_format = $rev->getContentFormat();
 +
 +                      return $content;
                }
        }
  
 +
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
 +       * @deprecated since 1.WD
         */
        public function setPreloadedText( $text ) {
 -              $this->mPreloadText = $text;
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              $this->setPreloadedContent( $content );
 +      }
 +
 +      /**
 +       * Use this method before edit() to preload some content into the edit box
 +       *
 +       * @param $content Content
 +       *
 +       * @since 1.WD
 +       */
 +      public function setPreloadedContent( Content $content ) {
 +              $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
 +       *
         * @return String
 +       *
 +       * @deprecated since 1.WD, use getPreloadedContent() instead
         */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +              return $text;
 +      }
 +
 +      /**
 +       * Get the contents to be preloaded into the box, either set by
 +       * an earlier setPreloadText() or by loading the given page.
 +       *
 +       * @param $preload String: representing the title to preload from.
 +       *
 +       * @return Content
 +       *
 +       * @since 1.WD
 +       */
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
 +
 +              if ( !empty( $this->mPreloadContent ) ) {
 +                      return $this->mPreloadContent;
                }
  
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
                if ( $preload === '' ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 +              $content = $page->getContent( Revision::RAW );
 +
 +              return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
                $status = $this->internalAttemptSave( $resultDetails, $bot );
                // FIXME: once the interface for internalAttemptSave() is made nicer, this should use the message in $status
                if ( $status->value == self::AS_SUCCESS_UPDATE || $status->value == self::AS_SUCCESS_NEW_ARTICLE ) {
                        $this->didSave = true;
                }
                        case self::AS_HOOK_ERROR:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              #FIXME: cause editform to be shown again, not just an error!
 +                              return false;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
                                $permission = $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage';
                                throw new PermissionsError( $permission );
  
+                       default:
+                               // We don't recognize $status->value. The only way that can happen
+                               // is if an extension hook aborted from inside ArticleSave.
+                               // Render the status object into $this->hookError
+                               // FIXME this sucks, we should just use the Status object throughout
+                               $this->hookError = '<div class="error">' . $status->getWikitext() .
+                                       '</div>';
+                               return true;
                }
                return false;
        }
  
                # 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 );
                $this->mArticle->loadPageData( 'forupdate' );
                $new = !$this->mArticle->exists();
  
 -              if ( $new ) {
 -                      // Late check for create permission, just in case *PARANOIA*
 -                      if ( !$this->mTitle->userCan( 'create' ) ) {
 -                              $status->fatal( 'nocreatetext' );
 -                              $status->value = self::AS_NO_CREATE_PERMISSION;
 -                              wfDebug( __METHOD__ . ": no create permission\n" );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +              try {
 +                      if ( $new ) {
 +                              // Late check for create permission, just in case *PARANOIA*
 +                              if ( !$this->mTitle->userCan( 'create' ) ) {
 +                                      $status->fatal( 'nocreatetext' );
 +                                      $status->value = self::AS_NO_CREATE_PERMISSION;
 +                                      wfDebug( __METHOD__ . ": no create permission\n" );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      # Don't save a new article if it's blank.
 -                      if ( $this->textbox1 == '' ) {
 -                              $status->setResult( false, self::AS_BLANK_ARTICLE );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              # Don't save a new article if it's blank.
 +                              if ( $this->textbox1 == '' ) {
 +                                      $status->setResult( false, self::AS_BLANK_ARTICLE );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      $text = $this->textbox1;
 -                      $result['sectionanchor'] = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                              $result['sectionanchor'] = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
  
 -                                      // Create a link to the new section from the edit summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +
 +                                              // Create a link to the new section from the edit summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
                                }
 -                      }
  
 -                      $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 +                              $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +                      } else { # not $new
  
 -                      # Article exists. Check for edit conflict.
 -                      $timestamp = $this->mArticle->getTimestamp();
 -                      wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +                              # Article exists. Check for edit conflict.
  
 -                      if ( $timestamp != $this->edittime ) {
 -                              $this->isConflict = true;
 -                              if ( $this->section == 'new' ) {
 -                                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 -                                              $this->mArticle->getComment() == $this->summary ) {
 -                                              // Probably a duplicate submission of a new comment.
 -                                              // This can happen when squid resends a request after
 -                                              // a timeout but the first one actually went through.
 -                                              wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 -                                      } else {
 -                                              // New comment; suppress conflict.
 +                              $this->mArticle->clear(); # Force reload of dates, etc.
 +                              $timestamp = $this->mArticle->getTimestamp();
 +
 +                              wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +
 +                              if ( $timestamp != $this->edittime ) {
 +                                      $this->isConflict = true;
 +                                      if ( $this->section == 'new' ) {
 +                                              if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 +                                                      $this->mArticle->getComment() == $this->summary ) {
 +                                                      // Probably a duplicate submission of a new comment.
 +                                                      // This can happen when squid resends a request after
 +                                                      // a timeout but the first one actually went through.
 +                                                      wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 +                                              } else {
 +                                                      // New comment; suppress conflict.
 +                                                      $this->isConflict = false;
 +                                                      wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 +                                              }
 +                                      } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 +                                              # Suppress edit conflict with self, except for section edits where merging is required.
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                                $this->isConflict = false;
 -                                              wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
 -                              } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 -                                      # Suppress edit conflict with self, except for section edits where merging is required.
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 -                                      $this->isConflict = false;
                                }
 -                      }
 -
 -                      // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 -                      // backwards compatibility with old forms/bots).
 -                      if ( $this->sectiontitle !== '' ) {
 -                              $sectionTitle = $this->sectiontitle;
 -                      } else {
 -                              $sectionTitle = $this->summary;
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 -                      } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 -                      }
 -                      if ( is_null( $text ) ) {
 -                              wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 -                              $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 -                      } elseif ( $this->isConflict ) {
 -                              # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 -                                      // Successful merge! Maybe we should tell the user the good news?
 -                                      $this->isConflict = false;
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                              // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 +                              // backwards compatibility with old forms/bots).
 +                              if ( $this->sectiontitle !== '' ) {
 +                                      $sectionTitle = $this->sectiontitle;
                                } else {
 -                                      $this->section = '';
 -                                      $this->textbox1 = $text;
 -                                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      $sectionTitle = $this->summary;
                                }
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              $status->setResult( false, self::AS_CONFLICT_DETECTED );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                              $content = null;
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              if ( $this->isConflict ) {
 +                                      wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
 +                              } else {
 +                                      wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
 +                              }
  
 -                      # Handle the user preference to force summaries here, but not for null edits
 -                      if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 -                      {
 -                              if ( md5( $this->summary ) == $this->autoSumm ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' );
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 +                              if ( is_null( $content ) ) {
 +                                      wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 +                                      $this->isConflict = true;
 +                                      $content = $textbox_content; // do not try to merge here!
 +                              } elseif ( $this->isConflict ) {
 +                                      # Attempt merge
 +                                      if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
 +                                              // Successful merge! Maybe we should tell the user the good news?
 +                                              $this->isConflict = false;
 +                                              $content = $textbox_content;
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                                      } else {
 +                                              $this->section = '';
 +                                              #$this->textbox1 = $text; #redundant, nothing to do here?
 +                                              wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      }
                                }
 -                      }
  
 -                      # And a similar thing for new sections
 -                      if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 -                              if ( trim( $this->summary ) == '' ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 +                              if ( $this->isConflict ) {
 +                                      $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                      }
  
 -                      # All's well
 -                      wfProfileIn( __METHOD__ . '-sectionanchor' );
 -                      $sectionanchor = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->textbox1 == '' ) {
 -                                      $this->missingComment = true;
 -                                      $status->fatal( 'missingcommenttext' );
 -                                      $status->value = self::AS_TEXTBOX_EMPTY;
 -                                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
 +                                              || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                              # Handle the user preference to force summaries here, but not for null edits
 +                              if ( $this->section != 'new' && !$this->allowBlankSummary
 +                                      && !$content->equals( $this->getOriginalContent() )
 +                                      && !$content->isRedirect() ) # check if it's not a redirect
 +                              {
 +                                      if ( md5( $this->summary ) == $this->autoSumm ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' );
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
                                        }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -                                      # This is a new section, so create a link to the new section
 -                                      # in the revision summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
                                }
 -                      } elseif ( $this->section != '' ) {
 -                              # Try to get a section anchor from the section source, redirect to edited section if header found
 -                              # XXX: might be better to integrate this into Article::replaceSection
 -                              # for duplicate heading checking and maybe parsing
 -                              $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 -                              # we can't deal with anchors, includes, html etc in the header for now,
 -                              # headline would need to be parsed to improve this
 -                              if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +
 +                              # And a similar thing for new sections
 +                              if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 +                                      if ( trim( $this->summary ) == '' ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
                                }
 -                      }
 -                      $result['sectionanchor'] = $sectionanchor;
 -                      wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      // Save errors may fall down to the edit form, but we've now
 -                      // merged the section into full text. Clear the section field
 -                      // so that later submission of conflict forms won't try to
 -                      // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 -                      $this->section = '';
 +                              # All's well
 +                              wfProfileIn( __METHOD__ . '-sectionanchor' );
 +                              $sectionanchor = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->textbox1 == '' ) {
 +                                              $this->missingComment = true;
 +                                              $status->fatal( 'missingcommenttext' );
 +                                              $status->value = self::AS_TEXTBOX_EMPTY;
 +                                              wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                                              # This is a new section, so create a link to the new section
 +                                              # in the revision summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
 +                              } elseif ( $this->section != '' ) {
 +                                      # Try to get a section anchor from the section source, redirect to edited section if header found
 +                                      # XXX: might be better to integrate this into Article::replaceSection
 +                                      # for duplicate heading checking and maybe parsing
 +                                      $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 +                                      # we can't deal with anchors, includes, html etc in the header for now,
 +                                      # headline would need to be parsed to improve this
 +                                      if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +                                      }
 +                              }
 +                              $result['sectionanchor'] = $sectionanchor;
 +                              wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      $status->value = self::AS_SUCCESS_UPDATE;
 -              }
 +                              // Save errors may fall down to the edit form, but we've now
 +                              // merged the section into full text. Clear the section field
 +                              // so that later submission of conflict forms won't try to
 +                              // replace that into a duplicated mess.
 +                                      $this->textbox1 = $content->serialize( $this->content_format );
 +                              $this->section = '';
  
 -              // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 -              if ( $this->kblength > $wgMaxArticleSize ) {
 -                      $this->tooBig = true;
 -                      $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              }
 +                              $status->value = self::AS_SUCCESS_UPDATE;
 +                      }
 +
 +                      // Check for length errors again now that the section is merged in
 +                              $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
 +                      if ( $this->kblength > $wgMaxArticleSize ) {
 +                              $this->tooBig = true;
 +                              $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
  
 -              $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 -                      ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 -                      ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 -                      ( $bot ? EDIT_FORCE_BOT : 0 );
 +                      $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 +                              ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 +                              ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 +                              ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                              $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
  
 -              if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 -                      $this->commitWatch();
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              } else {
 -                      // Failure from doEdit()
 -                      // Show the edit conflict page for certain recognized errors from doEdit(),
 -                      // but don't show it for errors from extension hooks
 -                      $errors = $doEditStatus->getErrorsArray();
 -                      if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
 -                              'edit-already-exists' ) ) )
 -                      {
 -                              $this->isConflict = true;
 -                              // Destroys data doEdit() put in $status->value but who cares
 -                              $doEditStatus->value = self::AS_END;
 +                      if ( $doEditStatus->isOK() ) {
 +                                      $result['redirect'] = $content->isRedirect();
 +                              $this->commitWatch();
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      } else {
-                               $this->isConflict = true;
-                               $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
++                              // Failure from doEdit()
++                              // Show the edit conflict page for certain recognized errors from doEdit(),
++                              // but don't show it for errors from extension hooks
++                              $errors = $doEditStatus->getErrorsArray();
++                              if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
++                                      'edit-already-exists' ) ) )
++                              {
++                                      $this->isConflict = true;
++                                      // Destroys data doEdit() put in $status->value but who cares
++                                      $doEditStatus->value = self::AS_END;
++                              }
 +                              wfProfileOut( __METHOD__ );
 +                              return $doEditStatus;
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
 -                      return $doEditStatus;
 +                      return $status;
                }
        }
  
         * @parma $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
 -      function mergeChangesInto( &$editText ) {
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.WD" );
 +
 +              $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
 +       */
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        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 );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                        }
                }
  
 +              #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $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 ) );
                        // 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 {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 +              $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                                      $this->section, $textboxContent,
 +                                                                                                      $this->summary, $this->edittime );
 +
 +              # hanlde legacy text-based hook
 +              $newtext_orig = $newContent->serialize( $this->content_format );
 +              $newtext = $newtext_orig; #clone
                wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
  
 +              if ( $newtext != $newtext_orig ) {
 +                                              #if the hook changed the text, create a new Content object accordingly.
 +                                              $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModel() ); #XXX: handle parse errors ?
 +              }
 +
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 +
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
  
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMsg( 'token_suffix_mismatch' );
 -                      } else {
 -                              $note = wfMsg( 'session_fail_preview' );
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMsg( 'edit_form_incomplete' );
 -              } else {
 -                      $note = wfMsg( 'previewnote' ) .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setTidy( true );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 -
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 +              try {
 +                      $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
 -                                      $class .= " mw-js";
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMsg( 'token_suffix_mismatch' );
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMsg( 'session_fail_preview' );
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMsg( 'edit_form_incomplete' );
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMsg( 'previewnote' ) .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 +                      }
 +
 +                      $parserOptions = ParserOptions::newFromUser( $wgUser );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              # don't parse non-wikitext pages, show message about preview
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
 +                              } else {
 +                                      $note = wfMsg( 'previewnote' );
 +                              }
 +                      } else {
 +                              $note = wfMsg( 'previewnote' );
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 +                      $rt = $content->getRedirectChain();
  
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $toparse_orig = $content->serialize( $this->content_format );
 +                              $toparse = $toparse_orig;
 +                              wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +
 +                              if ( $toparse !== $toparse_orig ) {
 +                                      #hook changed the text, create new Content object
 +                                      $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
 +                              }
 +
 +                              wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) );
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $parserOptions->enableLimitReport();
 +
 +                              #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +
 +                              // TODO: might be a saner way to get a meaningfull context here?
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
diff --combined includes/ImagePage.php
@@@ -96,7 -96,7 +96,7 @@@ class ImagePage extends Article 
         * Include body text only; none of the image extras
         */
        public function render() {
-               $this->getContext()->setArticleBodyOnly( true );
+               $this->getContext()->getOutput()->setArticleBodyOnly( true );
                parent::view();
        }
  
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
 -                      parent::view();
 -                      $out->addHTML( Xml::closeElement( 'div' ) );
 +
 +            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
 +
 +                      $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
                        $out->setArticleFlag( true );
                return $r;
        }
  
 -      /**
 -       * Overloading Article's getContent method.
 -       *
 -       * Omit noarticletext if sharedupload; text will be fetched from the
 -       * shared upload server if possible.
 -       * @return string
 -       */
 -      public function getContent() {
 -              $this->loadFile();
 -              if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 -                      return '';
 -              }
 -              return parent::getContent();
 -      }
 +    /**
 +     * Overloading Article's getContentObject method.
 +     *
 +     * Omit noarticletext if sharedupload; text will be fetched from the
 +     * shared upload server if possible.
 +     * @return string
 +     */
 +    public function getContentObject() {
 +        $this->loadFile();
 +        if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 +            return null;
 +        }
 +        return parent::getContentObject();
 +    }
  
        protected function openShowImage() {
                global $wgImageLimits, $wgEnableUploads, $wgSend404Code;
                                                # Note that $height <= $maxHeight now, but might not be identical
                                                # because of rounding.
                                        }
-                                       $msgbig  = wfMsgHtml( 'show-big-image' );
+                                       $msgbig = wfMsgHtml( 'show-big-image' );
+                                       if ( $this->displayImg->getRepo()->canTransformVia404() ) {
+                                               $thumbSizes = $wgImageLimits;
+                                       } else {
+                                               # Creating thumb links triggers thumbnail generation.
+                                               # Just generate the thumb for the current users prefs.
+                                               $thumbOption = $user->getOption( 'thumbsize' );
+                                               $thumbSizes = array( isset( $wgImageLimits[$thumbOption] )
+                                                       ? $wgImageLimits[$thumbOption]
+                                                       : $wgImageLimits[User::getDefaultOption( 'thumbsize' )] );
+                                       }
+                                       # Generate thumbnails or thumbnail links as needed...
                                        $otherSizes = array();
-                                       foreach ( $wgImageLimits as $size ) {
-                                               if ( $size[0] < $width_orig && $size[1] < $height_orig &&
-                                                               $size[0] != $width && $size[1] != $height ) {
+                                       foreach ( $thumbSizes as $size ) {
+                                               if ( $size[0] < $width_orig && $size[1] < $height_orig
+                                                       && $size[0] != $width && $size[1] != $height )
+                                               {
                                                        $otherSizes[] = $this->makeSizeLink( $params, $size[0], $size[1] );
                                                }
                                        }
                                        $msgsmall = wfMessage( 'show-big-image-preview' )->
                                                rawParams( $this->makeSizeLink( $params, $width, $height ) )->
                                                parse();
-                                       if ( count( $otherSizes ) && $this->displayImg->getRepo()->canTransformVia404() ) {
+                                       if ( count( $otherSizes ) ) {
                                                $msgsmall .= ' ' .
                                                Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ),
                                                        wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )->
@@@ -1089,7 -1099,10 +1101,10 @@@ class ImageHistoryList extends ContextS
                // Image dimensions + size
                $row .= '<td>';
                $row .= htmlspecialchars( $file->getDimensionsString() );
-               $row .= ' <span style="white-space: nowrap;">(' . Linker::formatSize( $file->getSize() ) . ')</span>';
+               $row .= $this->getContext()->msg( 'word-separator' )->plain();
+               $row .= '<span style="white-space: nowrap;">';
+               $row .= $this->getContext()->msg( 'parentheses' )->rawParams( Linker::formatSize( $file->getSize() ) )->plain();
+               $row .= '</span>';
                $row .= '</td>';
  
                // Uploading user
diff --combined includes/Revision.php
@@@ -40,10 -40,6 +40,10 @@@ class Revision 
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
 +      protected $mContentModel;
 +      protected $mContentFormat;
 +      protected $mContent;
 +      protected $mContentHandler;
  
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
 +              global $wgContentHandlerUseDB;
 +
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
 +                      'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
 +                      'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      unset( $attribs['content_model'] );
 +                      unset( $attribs['content_format'] );
 +              }
 +
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
 -                      'rev_sha1'
 +                      'rev_sha1',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_format';
 +                      $fields[] = 'rev_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
                                $this->mTitle = null;
                        }
  
 +                      if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
 +                              $this->mContentModel = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentModel = intval( $row->rev_content_model );
 +                      }
 +
 +                      if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
 +                              $this->mContentFormat = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentFormat = intval( $row->rev_content_format );
 +                      }
 +
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
                        // 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']->getModel();
 +                              # note: mContentFormat is initializes later accordingly
 +                              # note: content is serialized later in this method!
 +                              # also set text to null?
 +                      }
 +
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
  
 +                      $this->mContentModel = isset( $row['content_model']  )  ? intval( $row['content_model'] )  : null;
 +                      $this->mContentFormat    = isset( $row['content_format']  ) ? intval( $row['content_format'] ) : null;
 +
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
  
 +                      # if we have a content object, override mText and mContentModel
 +                      if ( !empty( $row['content'] ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $row['content'];
 +
 +                              $this->mContentModel = $this->mContent->getModel();
 +                              $this->mContentHandler = null;
 +
 +                              $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
 +                      } elseif ( !is_null( $this->mText ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $handler->unserializeContent( $this->mText );
 +                      }
 +
                        $this->mTitle     = null; # Load on demand if needed
 -                      $this->mCurrent   = false;
 +                      $this->mCurrent   = false; #XXX: really? we are about to create a revision. it will usually then be the current one.
 +
                        # If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
 -                              $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
 +                              if ( !is_null( $this->mContent ) ) {
 +                                      $this->mSize = $this->mContent->getSize();
 +                              } else {
 +                                      #XXX: my be inconsistent with the notion of "size" use for the present content model
 +                                      #NOTE: should never happen if we have either text or content object!
 +                                      $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->getContentModel(); # force lazy init
 +                      $this->getContentFormat();    # force lazy init
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                $this->mUnpatrolled = null;
 +
 +              // @TODO: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API
 +              // @TODO: get rid of $mText
        }
  
        /**
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
 +       * @deprecated in 1.WD, use getContent() instead
 +       */
 +      public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage! #FIXME: used a LOT!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $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
 +       *
 +       * @since 1.WD
         */
 -      public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
 +      public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
 -                      return '';
 +                      return null;
                } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
 -                      return '';
 +                      return null;
                } else {
 -                      return $this->getRawText();
 +                      return $this->getContentInternal();
                }
        }
  
         *
         * @return String
         */
 -      public function getRawText() {
 -              if( is_null( $this->mText ) ) {
 -                      // Revision text is immutable. Load on demand:
 -                      $this->mText = $this->loadText();
 +      public function getRawText() { #FIXME: deprecated, replace usage!
 +              return $this->getText( self::RAW );
 +      }
 +
 +      protected function getContentInternal() {
 +              if( is_null( $this->mContent ) ) {
 +                      // Revision is immutable. Load on demand:
 +
 +                      $handler = $this->getContentHandler();
 +                      $format = $this->getContentFormat();
 +                      $title = $this->getTitle();
 +
 +                      if( is_null( $this->mText ) ) {
 +                              // Load text on demand:
 +                              $this->mText = $this->loadText();
 +                      }
 +
 +                      $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
 +              }
 +
 +              return $this->mContent;
 +      }
 +
 +      /**
 +       * Returns the content model for this revision.
 +       *
 +       * If no content model was stored in the database, $this->getTitle()->getContentModel() is
 +       * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
 +       * is used as a last resort.
 +       *
 +       * @return int the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
 +       **/
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $title = $this->getTitle();
 +                      $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
 +
 +                      assert( !empty( $this->mContentModel ) );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Returns the content format for this revision.
 +       *
 +       * If no content format was stored in the database, the default format for this
 +       * revision's content model is returned.
 +       *
 +       * @return int the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
 +       **/
 +      public function getContentFormat() {
 +              if ( !$this->mContentFormat ) {
 +                      $handler = $this->getContentHandler();
 +                      $this->mContentFormat = $handler->getDefaultFormat();
 +
 +                      assert( !empty( $this->mContentFormat ) );
 +              }
 +
 +              return $this->mContentFormat;
 +      }
 +
 +      /**
 +       * Returns the content handler appropriate for this revision's content model.
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler() {
 +              if ( !$this->mContentHandler ) {
 +                      $model = $this->getContentModel();
 +                      $this->mContentHandler = ContentHandler::getForModelID( $model );
 +
 +                      assert( $this->mContentHandler->isSupportedFormat( $this->getContentFormat() ) );
                }
 -              return $this->mText;
 +
 +              return $this->mContentHandler;
        }
  
        /**
         * @return Integer
         */
        public function insertOn( $dbw ) {
 -              global $wgDefaultExternalStore;
 +              global $wgDefaultExternalStore, $wgContentHandlerUseDB;
  
                wfProfileIn( __METHOD__ );
  
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
 -              $dbw->insert( 'revision',
 -                      array(
 -                              'rev_id'         => $rev_id,
 -                              'rev_page'       => $this->mPage,
 -                              'rev_text_id'    => $this->mTextId,
 -                              'rev_comment'    => $this->mComment,
 -                              'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 -                              'rev_user'       => $this->mUser,
 -                              'rev_user_text'  => $this->mUserText,
 -                              'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 -                              'rev_deleted'    => $this->mDeleted,
 -                              'rev_len'        => $this->mSize,
 -                              'rev_parent_id'  => is_null( $this->mParentId )
 -                                      ? $this->getPreviousRevisionId( $dbw )
 -                                      : $this->mParentId,
 -                              'rev_sha1'       => is_null( $this->mSha1 )
 -                                      ? Revision::base36Sha1( $this->mText )
 -                                      : $this->mSha1
 -                      ), __METHOD__
 +
 +              $row = array(
 +                      'rev_id'         => $rev_id,
 +                      'rev_page'       => $this->mPage,
 +                      'rev_text_id'    => $this->mTextId,
 +                      'rev_comment'    => $this->mComment,
 +                      'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 +                      'rev_user'       => $this->mUser,
 +                      'rev_user_text'  => $this->mUserText,
 +                      'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 +                      'rev_deleted'    => $this->mDeleted,
 +                      'rev_len'        => $this->mSize,
 +                      'rev_parent_id'  => is_null( $this->mParentId )
 +                              ? $this->getPreviousRevisionId( $dbw )
 +                              : $this->mParentId,
 +                      'rev_sha1'       => is_null( $this->mSha1 )
 +                              ? Revision::base36Sha1( $this->mText )
 +                              : $this->mSha1,
                );
  
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'rev_content_model' ] = $this->getContentModel();
 +                      $row[ 'rev_content_format' ] = $this->getContentFormat();
 +              }
 +
 +              $dbw->insert( 'revision', $row, __METHOD__ );
 +
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
  
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
-               $fields = array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' );
++              $fields = array( 'page_latest', 'page_namespace', 'page_title',
++                                              'rev_text_id', 'rev_len', 'rev_sha1' );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_model';
 +                      $fields[] = 'rev_content_format';
 +              }
 +
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
 -                      array( 'page_latest', 'page_namespace', 'page_title',
 -                              'rev_text_id', 'rev_len', 'rev_sha1' ),
 +                      $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
                        __METHOD__ );
  
                if( $current ) {
 -                      $revision = new Revision( array(
 +                      $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
                                'text_id'    => $current->rev_text_id,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
-                               'sha1'       => $current->rev_sha1,
+                               'sha1'       => $current->rev_sha1
 -                              ) );
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
+                       $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
                }
diff --combined includes/Title.php
@@@ -65,7 -65,6 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
 +      var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
         */
        public static function newFromID( $id, $flags = 0 ) {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
-               $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
+               $row = $db->selectRow(
+                       'page',
+                       array(
+                               'page_namespace', 'page_title', 'page_id',
+                               'page_len', 'page_is_redirect', 'page_latest',
+                       ),
+                       array( 'page_id' => $id ),
+                       __METHOD__
+               );
                if ( $row !== false ) {
                        $title = Title::newFromRow( $row );
                } else {
                        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->mContentModel = intval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = null; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = null; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = null; # initialized lazily in getContentModel()
                return $t;
        }
  
                return $this->mNamespace;
        }
  
 +      /**
 +       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 +       *
 +       * @return Integer: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( empty( $this->mContentModel ) ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              assert( !empty( $this->mContentModel ) );
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Conveniance method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +       * or CONTENT_MODEL_JAVASCRIPT.
 +       *
 +       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +       *
 +       * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +              $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +                      && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                              || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +              #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +              #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +              return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @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 ) );
        }
  
        /**
         * @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 ) );
        }
  
        /**
                                        $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
                                        $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
                                } else {
-                                       $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
+                                       $restriction = trim( $temp[1] );
+                                       if( $restriction != '' ) { //some old entries are empty
+                                               $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
+                                       }
                                }
                        }
  
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
 +
                $linkCache = LinkCache::singleton();
 -              $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              assert( $cached !== null ); # assert the assumption that the cache actually knows about this title
 +
 +              $this->mRedirect = (bool)$cached;
  
                return $this->mRedirect;
        }
                $protected = $this->isProtected();
  
                // Do the actual move
-               $err = $this->moveToInternal( $nt, $reason, $createRedirect );
-               if ( is_array( $err ) ) {
-                       # @todo FIXME: What about the File we have already moved?
-                       $dbw->rollback( __METHOD__ );
-                       return $err;
-               }
+               $this->moveToInternal( $nt, $reason, $createRedirect );
  
                // Refresh the sortkey for this row.  Be careful to avoid resetting
                // cl_timestamp, which may disturb time-based lists on some sites.
         * @param $reason String The reason for the move
         * @param $createRedirect Bool Whether to leave a redirect at the old title.  Ignored
         *   if the user doesn't have the suppressredirect right
+        * @throws MWException
         */
        private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
                global $wgUser, $wgContLang;
                $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
                        $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
-                       $row = $db->selectRow( 'revision', '*',
+                       $row = $db->selectRow( 'revision', Revision::selectFields(),
                                array( 'rev_page' => $pageId ),
                                __METHOD__,
                                array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
diff --combined includes/WikiPage.php
@@@ -230,21 -230,7 +230,21 @@@ class WikiPage extends Page 
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentHandler() {
 +              return ContentHandler::getForModelID( $this->getContentModel() );
        }
  
        /**
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'page_id',
                        'page_namespace',
                        'page_title',
                        'page_latest',
                        'page_len',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
         * @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 id (see the CONTENT_MODEL_XXX constants).
 +       *
 +       * Will use the revisions actual content model if the page exists,
 +       * and the page's default if the page doesn't exist yet.
 +       *
 +       * @return int
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentModel() {
 +              if ( $this->exists() ) {
 +                      # look at the revision's actual content model
 +                      $rev = $this->getRevision();
 +
 +                      if ( $rev !== null ) {
 +                              return $rev->getContentModel();
 +                      } else {
 +                              wfWarn( "Page exists but has no revision!" );
 +                      }
                }
 +
 +              # use the default model for this page
 +              return $this->mTitle->getContentModel();
        }
  
        /**
        }
  
        /**
 -       * Get the text of the current revision. No side-effects...
 +       * 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 String|bool The text of the current revision. False on failure
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.WD
         */
 -      public function getText( $audience = Revision::FOR_PUBLIC ) {
 +      public function getContent( $audience = Revision::FOR_PUBLIC ) {
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getText( $audience );
 +                      return $this->mLastRevision->getContent( $audience );
                }
 -              return false;
 +              return null;
        }
  
        /**
         * Get the text of the current revision. No side-effects...
         *
 -       * @return String|bool The text of the current revision. False on failure
 +       * @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 String|false The text of the current revision
 +       * @deprecated as of 1.WD, getContent() should be used instead.
         */
 -      public function getRawText() {
 +      public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getRawText();
 +                      return $this->mLastRevision->getText( $audience );
                }
                return false;
        }
  
 +      /**
 +       * Get the text of the current revision. No side-effects...
 +       *
 +       * @return String|bool The text of the current revision. False on failure
 +       * @deprecated as of 1.WD, getContent() should be used instead.
 +       */
 +      public function getRawText() { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              return $this->getText( Revision::RAW );
 +      }
 +
        /**
         * @return string MW timestamp of last article revision
         */
                if ( !$this->mTimestamp ) {
                        $this->loadLastEdit();
                }
 -              
 +
                return wfTimestamp( TS_MW, $this->mTimestamp );
        }
  
                        return false;
                }
  
 -              $text = $editInfo ? $editInfo->pst : false;
 +              if ( $editInfo ) {
 +                      $content = $editInfo->pstContent;
 +              } else {
 +                      $content = $this->getContent();
 +              }
  
 -              if ( $this->isRedirect( $text ) ) {
 +              if ( !$content || $content->isRedirect( ) ) {
                        return false;
                }
  
 -              switch ( $wgArticleCountMethod ) {
 -              case 'any':
 -                      return true;
 -              case 'comma':
 -                      if ( $text === false ) {
 -                              $text = $this->getRawText();
 -                      }
 -                      return strpos( $text,  ',' ) !== false;
 -              case 'link':
 +              $hasLinks = null;
 +
 +              if ( $wgArticleCountMethod === 'link' ) {
 +                      # nasty special case to avoid re-parsing to detect links
 +
                        if ( $editInfo ) {
                                // ParserOutput::getLinks() is a 2D array of page links, so
                                // to be really correct we would need to recurse in the array
                                // but the main array should only have items in it if there are
                                // links.
 -                              return (bool)count( $editInfo->output->getLinks() );
 +                              $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
 -                              return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 +                              $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
                                        array( 'pl_from' => $this->getId() ), __METHOD__ );
                        }
                }
 +
 +              return $content->isCountable( $hasLinks );
        }
  
        /**
         */
        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;
                }
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
 -                      && $this->mTitle->isWikitextPage();
 +                      && $this->getContentHandler()->isParserCacheSupported();
        }
  
        /**
         * @param $parserOptions ParserOptions to use for the parse operation
         * @param $oldid Revision ID to get the text from, passing null or 0 will
         *               get the current revision (default value)
 +       *
         * @return ParserOutput or false if the revision was not found
         */
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
                        $oldid = $this->getLatest();
                }
  
 -              $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
 +              $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache, null );
                $pool->execute();
  
                wfProfileOut( __METHOD__ );
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        if ( $this->mTitle->exists() ) {
 -                              $text = $this->getRawText();
 +                              $text = ContentHandler::getContentText( $this->getContent() );
                        } else {
                                $text = false;
                        }
         * @private
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 -              $text = $revision->getText();
 -              $len = strlen( $text );
 -              $rt = Title::newFromRedirectRecurse( $text );
 +              $content = $revision->getContent();
 +              $len = $content->getSize();
 +              $rt = $content->getUltimateRedirectTarget();
  
                $conditions = array( 'page_id' => $this->getId() );
  
                }
  
                $now = wfTimestampNow();
 +              $row = array( /* SET */
 +                      'page_latest'      => $revision->getId(),
 +                      'page_touched'     => $dbw->timestamp( $now ),
 +                      'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 +                      'page_is_redirect' => $rt !== null ? 1 : 0,
 +                      'page_len'         => $len,
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'page_content_model' ] = $revision->getContentModel();
 +              }
 +
                $dbw->update( 'page',
 -                      array( /* SET */
 -                              'page_latest'      => $revision->getId(),
 -                              'page_touched'     => $dbw->timestamp( $now ),
 -                              'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 -                              'page_is_redirect' => $rt !== null ? 1 : 0,
 -                              'page_len'         => $len,
 -                      ),
 +                      $row,
                        $conditions,
                        __METHOD__ );
  
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
 +       * @deprecated since 1.WD: use ContentHandler::getUndoContent() instead.
         */
        public function getUndoText( Revision $undo, Revision $undoafter = null ) {
 -              $cur_text = $this->getRawText();
 -              if ( $cur_text === false ) {
 -                      return false; // no page
 -              }
 -              $undo_text = $undo->getText();
 -              $undoafter_text = $undoafter->getText();
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -              if ( $cur_text == $undo_text ) {
 -                      # No use doing a merge if it's just a straight revert.
 -                      return $undoafter_text;
 -              }
 +              $this->loadLastEdit();
  
 -              $undone_text = '';
 +              if ( $this->mLastRevision ) {
 +                      if ( is_null( $undoafter ) ) {
 +                              $undoafter = $undo->getPrevious();
 +                      }
  
 -              if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
 -                      return false;
 +                      $handler = $this->getContentHandler();
 +                      $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
 +
 +                      if ( !$undone ) {
 +                              return false;
 +                      } else {
 +                              return ContentHandler::getContentText( $undone );
 +                      }
                }
  
 -              return $undone_text;
 +              return false;
        }
  
        /**
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
 -       * @return string Complete article text, or null if error
 +       * @return String new complete article text, or null if error
 +       *
 +       * @deprecated since 1.WD, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); #XXX: could make section title, but that's not required.
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent ); #XXX: unclear what will happen for non-wikitext!
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.WD
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
 -                              if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 -                                      $text = strlen( trim( $oldtext ) ) > 0
 -                                              ? "{$oldtext}\n\n{$subject}{$text}"
 -                                              : "{$subject}{$text}";
 -                              }
 -                      } else {
 -                              # Replacing an existing section; roll out the big guns
 -                              global $wgParser;
 -
 -                              $text = $wgParser->replaceSection( $oldtext, $section, $text );
 -                      }
 +                      $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
 +       *
 +       * @deprecated since 1.WD: use doEditContent() instead.
 +       */
 +      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
 +      }
 +
 +      /**
 +       * Change an existing article or create a new article. Updates RC and all necessary caches,
 +       * optionally via the deferred update array.
 +       *
 +       * @param $content Content: new content
 +       * @param $summary String: edit summary
 +       * @param $flags Integer bitfield:
 +       *      EDIT_NEW
 +       *          Article is known or assumed to be non-existent, create a new one
 +       *      EDIT_UPDATE
 +       *          Article is known or assumed to be pre-existing, update it
 +       *      EDIT_MINOR
 +       *          Mark this edit minor, if the user is allowed to do so
 +       *      EDIT_SUPPRESS_RC
 +       *          Do not log the change in recentchanges
 +       *      EDIT_FORCE_BOT
 +       *          Mark the edit a "bot" edit regardless of user rights
 +       *      EDIT_DEFER_UPDATES
 +       *          Defer some of the updates until the end of index.php
 +       *      EDIT_AUTOSUMMARY
 +       *          Fill in blank summaries with generated text where possible
 +       *
 +       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
 +       * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
 +       * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
 +       * edit-already-exists error will be returned. These two conditions are also possible with
 +       * auto-detection due to MediaWiki's performance-optimised locking strategy.
 +       *
 +       * @param $baseRevId the revision ID this edit was based off, if any
 +       * @param $user User the user doing the edit
 +       * @param $serialisation_format String: format for storing the content in the database
 +       *
 +       * @return Status object. Possible errors:
 +       *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
 +       *     edit-gone-missing:       In update mode, but the article didn't exist
 +       *     edit-conflict:           In update mode, the article changed unexpectedly
 +       *     edit-no-change:          Warning that the text was the same as before
 +       *     edit-already-exists:     In creation mode, but the article already exists
 +       *
 +       *  Extensions may define additional errors.
 +       *
 +       *  $return->value will contain an associative array with members as follows:
 +       *     new:                     Boolean indicating if the function attempted to create a new article
 +       *     revision:                The revision object for the inserted revision, or null
 +       *
 +       * @since 1.WD
         */
 -      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
  
                # Low-level sanity check
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # call legacy hook
 +              $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +              if ( $hook_ok && !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,
 +                              $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +                      if ( $txt !== $content_text ) {
 +                              # if the text changed, unserialize the new version to create an updated Content object.
 +                              $content = $content->getContentHandler()->unserializeContent( $txt );
 +                      }
 +              }
 +
 +              if ( !$hook_ok ) {
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $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();
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              // TODO: validate!
 +                              if ( $content->isValid() ) {
 +
 +                              }
 +
                                $dbw->begin( __METHOD__ );
                                $revisionId = $revision->insertOn( $dbw );
  
                                return $status;
                        }
  
 +                      // TODO: create content diff to pass to update objects that might need it
 +
                        # Update links tables, site stats, etc.
 -                      $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
 -                              'oldcountable' => $oldcountable ) );
 +                      $this->doEditUpdates(
 +                              $revision,
 +                              $user,
 +                              array(
 +                                      'changed' => $changed,
 +                                      'oldcountable' => $oldcountable
 +                              )
 +                      );
  
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                                        $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 ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 +                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +
 +                      wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
  
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 +              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +
 +              wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
  
                # Promote user to any groups they meet the criteria for
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.WD: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->prepareContentForEdit( $content, $revid , $user );
 +      }
 +
 +      /**
 +       * Prepare content which is about to be saved.
 +       * Returns a stdclass with source, pst and output members
 +       *
 +       * @param \Content $content
 +       * @param null $revid
 +       * @param null|\User $user
 +       * @param null $serialization_format
 +       *
 +       * @return bool|object
 +       *
 +       * @since 1.WD
 +       */
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $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;
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
                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;
                }
  
                # Update the links tables and other secondary data
 -              $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
 +              $updates = $editInfo->output->getSecondaryDataUpdates( $this->getTitle() );
                DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
                }
  
                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
                }
  
                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'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.WD, use doEditContent() instead.
         */
        public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->doQuickEditContent( $content, $user, $comment , $minor );
 +      }
 +
 +      /**
 +       * Edit an article without doing all that other stuff
 +       * The article must already exist; link tables etc
 +       * are not updated, caches are not flushed.
 +       *
 +       * @param $content Content: content submitted
 +       * @param $user User The relevant user
 +       * @param $comment String: comment submitted
 +       * @param $serialisation_format String: format for storing the content in the database
 +       * @param $minor Boolean: whereas it's a minor modification
 +       */
 +      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
  
 +              $serialized = $content->serialize( $serialisation_format );
 +
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
 -                      'text'       => $text,
 +                      'text'       => $serialized,
 +                      'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
                ) );
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
  
                wfDebug( __METHOD__ . "\n" );
  
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
 +
 +              $row = array(
 +                      'ar_namespace'  => 'page_namespace',
 +                      'ar_title'      => 'page_title',
 +                      'ar_comment'    => 'rev_comment',
 +                      'ar_user'       => 'rev_user',
 +                      'ar_user_text'  => 'rev_user_text',
 +                      'ar_timestamp'  => 'rev_timestamp',
 +                      'ar_minor_edit' => 'rev_minor_edit',
 +                      'ar_rev_id'     => 'rev_id',
 +                      'ar_parent_id'  => 'rev_parent_id',
 +                      'ar_text_id'    => 'rev_text_id',
 +                      'ar_text'       => '\'\'', // Be explicit to appease
 +                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 +                      'ar_len'        => 'rev_len',
 +                      'ar_page_id'    => 'page_id',
 +                      'ar_deleted'    => $bitfield,
 +                      'ar_sha1'       => 'rev_sha1',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'ar_content_model' ] = 'rev_content_model';
 +                      $row[ 'ar_content_format' ] = 'rev_content_format';
 +              }
 +
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
 +                      $row,
                        array(
 -                              'ar_namespace'  => 'page_namespace',
 -                              'ar_title'      => 'page_title',
 -                              'ar_comment'    => 'rev_comment',
 -                              'ar_user'       => 'rev_user',
 -                              'ar_user_text'  => 'rev_user_text',
 -                              'ar_timestamp'  => 'rev_timestamp',
 -                              'ar_minor_edit' => 'rev_minor_edit',
 -                              'ar_rev_id'     => 'rev_id',
 -                              'ar_parent_id'  => 'rev_parent_id',
 -                              'ar_text_id'    => 'rev_text_id',
 -                              'ar_text'       => '\'\'', // Be explicit to appease
 -                              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                              'ar_len'        => 'rev_len',
 -                              'ar_page_id'    => 'page_id',
 -                              'ar_deleted'    => $bitfield,
 -                              'ar_sha1'       => 'rev_sha1'
 -                      ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
                        return WikiPage::DELETE_NO_REVISIONS;
                }
  
-               # update site status
-               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
-               # remove secondary indexes, etc
-               $updates = $this->getDeletionUpdates( );
-               DataUpdate::runUpdates( $updates );
-               # Clear caches
-               WikiPage::onArticleDelete( $this->mTitle );
-               # Reset this object
-               $this->clear();
-               # Clear the cached article id so the interface doesn't act like we exist
-               $this->mTitle->resetArticleID( 0 );
+               $this->doDeleteUpdates( $id );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
                return WikiPage::DELETE_SUCCESS;
        }
  
+       /**
+        * Do some database updates after deletion
+        *
+        * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
+        */
+       public function doDeleteUpdates( $id ) {
+               # update site status
+               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
+               # remove secondary indexes, etc
+               $updates = $this->getDeletionUpdates( );
+               DataUpdate::runUpdates( $updates );
+               # Clear caches
+               WikiPage::onArticleDelete( $this->mTitle );
+               # Reset this object
+               $this->clear();
+               # Clear the cached article id so the interface doesn't act like we exist
+               $this->mTitle->resetArticleID( 0 );
+       }
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
                }
  
                # 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 {
  
        /**
        * Return an applicable autosummary if one exists for the given edit.
 -      * @param $oldtext String: the previous text of the page.
 -      * @param $newtext String: The submitted text of the page.
 +      * @param $oldtext String|null: the previous text of the page.
 +      * @param $newtext String|null: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
 +      * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
 -              global $wgContLang;
 -
 -              # Decide what kind of autosummary is needed.
 -
 -              # Redirect autosummaries
 -              $ot = Title::newFromRedirect( $oldtext );
 -              $rt = Title::newFromRedirect( $newtext );
 -
 -              if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 250
 -                                      - strlen( wfMsgForContent( 'autoredircomment' ) )
 -                                      - strlen( $rt->getFullText() )
 -                              ) );
 -                      return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
 -              }
 +              # 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.
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -                      return wfMsgForContent( 'autosumm-new', $truncatedtext );
 -              }
 -
 -              # Blanking autosummaries
 -              if ( $oldtext != '' && $newtext == '' ) {
 -                      return wfMsgForContent( 'autosumm-blank' );
 -              } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
 -                      # Removing more than 90% of the article
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
 -
 -                      return wfMsgForContent( 'autosumm-replace', $truncatedtext );
 -              }
 -
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         * @param &$hasHistory Boolean: whether the page has a history
         * @return mixed String containing deletion reason or empty string, or boolean false
         *    if no revision occurred
 +       * @deprecated since 1.WD, use ContentHandler::getAutoDeleteReason() instead
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 +              #NOTE: stub for backwards-compatibility.
 +
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +              $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory );
                global $wgContLang;
  
                // Get the last revision
        }
  
        public function getDeletionUpdates() {
 -              $updates = array(
 -                      new LinksDeletionUpdate( $this ),
 -              );
 +              $updates = $this->getContentHandler()->getDeletionUpdates( $this );
  
 -              //@todo: make a hook to add update objects
 -              //NOTE: deletion updates will be determined by the ContentHandler in the future
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, &$updates ) );
                return $updates;
        }
  }
@@@ -3220,9 -3000,9 +3229,9 @@@ class PoolWorkArticleView extends PoolC
        private $parserOptions;
  
        /**
 -       * @var string|null
 +       * @var Content|null
         */
 -      private $text;
 +      private $content = null;
  
        /**
         * @var ParserOutput|bool
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
 -       * @param $text String: text to parse or null to load it
 +       * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
 +              if ( is_string($content) ) { #BC: old style call
 +                      $modelId = $page->getRevision()->getContentModel();
 +                      $format = $page->getRevision()->getContentFormat();
 +                      $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +                      $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              // TODO: page might not have this method? Hard to tell what page is supposed to be here...
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
@@@ -109,23 -109,21 +109,23 @@@ class ApiEditPage extends ApiBase 
                        // We do want getContent()'s behavior for non-existent
                        // MediaWiki: pages, though
                        if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
 -                              $content = '';
 +                              $content = null;
 +                $text = '';
                        } else {
 -                              $content = $articleObj->getContent();
 +                $content = $articleObj->getContentObject();
 +                $text = ContentHandler::getContentText( $content ); #FIXME: serialize?! get format from params?...
                        }
  
                        if ( !is_null( $params['section'] ) ) {
                                // Process the content for section edits
 -                              global $wgParser;
                                $section = intval( $params['section'] );
 -                              $content = $wgParser->getSection( $content, $section, false );
 -                              if ( $content === false ) {
 +                $sectionContent = $content->getSection( $section );
 +                $text = ContentHandler::getContentText( $sectionContent ); #FIXME: serialize?! get format from params?...
 +                              if ( $text === false || $text === null ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
 -                      $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
 +                      $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
  
                // 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 );
  
                                $this->dieUsageMsg( 'summaryrequired' );
  
                        case EditPage::AS_END:
+                       default:
                                // $status came from WikiPage::doEdit()
                                $errors = $status->getErrorsArray();
                                $this->dieUsageMsg( $errors[0] ); // TODO: Add new errors to message map
                                break;
-                       default:
-                               if ( is_string( $status->value ) && strlen( $status->value ) ) {
-                                       $this->dieUsage( "An unknown return value was returned by Editpage. The code returned was \"{$status->value}\"" , $status->value );
-                               } else {
-                                       $this->dieUsageMsg( array( 'unknownerror', $status->value ) );
-                               }
                }
                $apiResult->addValue( null, $this->getModuleName(), $r );
        }
diff --combined includes/api/ApiMain.php
@@@ -104,7 -104,6 +104,7 @@@ class ApiMain extends ApiBase 
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
                $module->profileOut();
  
                if ( !$this->mInternalMode ) {
+                       //append Debug information
+                       MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
                        // Print result data
                        $this->printResult( false );
                }
@@@ -114,7 -114,7 +114,7 @@@ class ApiQueryRevisions extends ApiQuer
                }
  
                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;
                        }
                }
  
+               // add user name, if needed
+               if ( $this->fld_user ) {
+                       $this->addTables( 'user' );
+                       $this->addJoinConds( array( 'user' => Revision::userJoinCond() ) );
+                       $this->addFields( Revision::selectUserFields() );
+               }
                // Bug 24166 - API error when using rvprop=tags
                $this->addTables( 'revision' );
  
                        $this->addWhereFld( 'rev_id', array_keys( $revs ) );
  
                        if ( !is_null( $params['continue'] ) ) {
-                               $this->addWhere( "rev_id >= '" . intval( $params['continue'] ) . "'" );
+                               $this->addWhere( 'rev_id >= ' . intval( $params['continue'] ) );
                        }
                        $this->addOption( 'ORDER BY', 'rev_id' );
  
                                $pageid = intval( $cont[0] );
                                $revid = intval( $cont[1] );
                                $this->addWhere(
-                                       "rev_page > '$pageid' OR " .
-                                       "(rev_page = '$pageid' AND " .
-                                       "rev_id >= '$revid')"
+                                       "rev_page > $pageid OR " .
+                                       "(rev_page = $pageid AND " .
+                                       "rev_id >= $revid)"
                                );
                        }
                        $this->addOption( 'ORDER BY', array(
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
 +                $handler = ContentHandler::getForTitle( $title );
 +
                                if ( !is_null( $this->difftotext ) ) {
 -                                      $engine = new DifferenceEngine( $context );
 -                                      $engine->setText( $text, $this->difftotext );
 +                                      $engine = $handler->createDifferenceEngine( $context );
 +                                      $engine->setText( $text, $this->difftotext ); #FIXME: use content objects!...
                                } else {
 -                                      $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
 +                                      $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
@@@ -22,8 -22,6 +22,6 @@@
   * @author Roan Kattouw
   */
  
- defined( 'MEDIAWIKI' ) || die( 1 );
  /**
   * Abstraction for resource loader modules which pull from wiki pages
   *
@@@ -84,7 -82,7 +82,7 @@@ abstract class ResourceLoaderWikiModul
                if ( !$revision ) {
                        return null;
                }
 -              return $revision->getRawText();
 +              return $revision->getRawText(); #FIXME: get raw data from content object after checking the type;
        }
  
        /* Methods */
diff --combined languages/Language.php
@@@ -245,6 -245,17 +245,17 @@@ class Language 
         * @return bool
         */
        public static function isValidBuiltInCode( $code ) {
+               if( !is_string($code) ) {
+                       $type = gettype( $code );
+                       if( $type === 'object' ) {
+                               $addmsg = " of class " . get_class( $code );
+                       } else {
+                               $addmsg = '';
+                       }
+                       throw new MWException( __METHOD__ . " must be passed a string, $type given$addmsg" );
+               }
                return preg_match( '/^[a-z0-9-]+$/i', $code );
        }
  
         */
        public function setNamespaces( array $namespaces ) {
                $this->namespaceNames = $namespaces;
 +              $this->mNamespaceIds = null;
 +      }
 +
 +      /**
 +       * Resets all of the namespace caches. Mainly used for testing
 +       */
 +      public function resetNamespaces( ) {
 +              $this->namespaceNames = null;
 +              $this->mNamespaceIds = null;
 +              $this->namespaceAliases = null;
        }
  
        /**
         *              Use null for autonyms (native names)
         * @param $include string:
         *              'all' all available languages
-        *              'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames
+        *              'mw' only if the language is defined in MediaWiki or wgExtraLanguageNames (default)
         *              'mwfile' only if the language is in 'mw' *and* has a message file
-        * @return array|bool: language code => language name, false if $include is wrong
+        * @return array: language code => language name
         * @since 1.20
         */
        public static function fetchLanguageNames( $inLanguage = null, $include = 'mw' ) {
                        $returnMw[$coreCode] = $names[$coreCode];
                }
  
-               if( $include === 'mw' ) {
-                       return $returnMw;
-               } elseif( $include === 'mwfile' ) {
+               if( $include === 'mwfile' ) {
                        $namesMwFile = array();
                        # We do this using a foreach over the codes instead of a directory
                        # loop so that messages files in extensions will work correctly.
                        }
                        return $namesMwFile;
                }
-               return false;
+               # 'mw' option; default if it's not one of the other two options (all/mwfile)
+               return $returnMw;
        }
  
        /**
        }
  
        /**
-        * An arrow, depending on the language direction
+        * An arrow, depending on the language direction.
         *
+        * @param $direction String: the direction of the arrow: forwards (default), backwards, left, right, up, down.
         * @return string
         */
-       function getArrow() {
-               return $this->isRTL() ? '←' : '→';
+       function getArrow( $direction = 'forwards' ) {
+               switch ( $direction ) {
+               case 'forwards':
+                       return $this->isRTL() ? '←' : '→';
+               case 'backwards':
+                       return $this->isRTL() ? '→' : '←';
+               case 'left':
+                       return '←';
+               case 'right':
+                       return '→';
+               case 'up':
+                       return '↑';
+               case 'down':
+                       return '↓';
+               }
        }
  
        /**
@@@ -887,7 -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.',
@@@ -1792,6 -1791,7 +1792,7 @@@ Note that their indexes of {{SITENAME}
  'prefs-beta'                    => 'Beta features',
  'prefs-datetime'                => 'Date and time',
  'prefs-labs'                    => 'Labs features',
+ 'prefs-user-pages'              => 'User pages',
  'prefs-personal'                => 'User profile',
  'prefs-rc'                      => 'Recent changes',
  'prefs-watchlist'               => 'Watchlist',
@@@ -2062,7 -2062,8 +2063,8 @@@ Your e-mail address is not revealed whe
  'recentchanges'                     => 'Recent changes',
  'recentchanges-url'                 => 'Special:RecentChanges', # do not translate or duplicate this message to other languages
  'recentchanges-legend'              => 'Recent changes options',
- 'recentchangestext'                 => 'Track the most recent changes to the wiki on this page.',
+ 'recentchanges-summary'             => 'Track the most recent changes to the wiki on this page.',
+ 'recentchangestext'                 => '-', # do not translate or duplicate this message to other languages
  'recentchanges-feed-description'    => 'Track the most recent changes to the wiki in this feed.',
  'recentchanges-label-newpage'       => 'This edit created a new page',
  'recentchanges-label-minor'         => 'This is a minor edit',
@@@ -2284,8 -2285,9 +2286,9 @@@ If the problem persists, contact an [[S
  'backend-fail-writetemp'     => 'Could not write to temporary file.',
  'backend-fail-closetemp'     => 'Could not close temporary file.',
  'backend-fail-read'          => 'Could not read file $1.',
- 'backend-fail-create'        => 'Could not create file $1.',
- 'backend-fail-maxsize'       => 'Could not create file $1 because it is larger than {{PLURAL:$2|one byte|$2 bytes}}.',
+ 'backend-fail-create'        => 'Could not write file $1.',
+ 'backend-fail-maxsize'       => 'Could not write file $1 because it is larger than {{PLURAL:$2|one byte|$2 bytes}}.',
+ 'backend-fail-usable'        => 'Could not write file $1 due to insufficient permissions or missing directories/containers.',
  'backend-fail-readonly'      => 'The storage backend "$1" is currently read-only. The reason given is: "\'\'$2\'\'"',
  'backend-fail-synced'        => 'The file "$1" is in an inconsistent state within the internal storage backends',
  'backend-fail-connect'       => 'Could not connect to storage backend "$1".',
@@@ -2353,7 -2355,6 +2356,6 @@@ For optimal security, img_auth.php is d
  'http-curl-error'       => 'Error fetching URL: $1',
  'http-host-unreachable' => 'Could not reach URL.',
  'http-bad-status'       => 'There was a problem during the HTTP request: $1 $2',
- 'http-truncated-body'   => 'The request body was only partially received.',
  
  # Some likely curl errors. More could be added from <http://curl.haxx.se/libcurl/c/libcurl-errors.html>
  'upload-curl-error6'       => 'Could not reach URL',
@@@ -4867,10 -4868,4 +4869,10 @@@ Otherwise, you can use the easy form be
  'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
  'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
 +'content-model-1' => 'wikitext',
 +'content-model-2' => 'JavaScript',
 +'content-model-3' => 'CSS',
 +'content-model-4' => 'plain text',
 +
  );
@@@ -8,6 -8,7 +8,7 @@@
   * @file
   *
   * @author *Surak*
+  * @author Abanima
   * @author Ahonc
   * @author Aleator
   * @author AlexSm
   * @author Sherbrooke
   * @author Shirayuki
   * @author Shushruth
+  * @author Siddhartha Ghai
   * @author Siebrand
   * @author Singularity
   * @author Sionnach
@@@ -1392,6 -1394,7 +1394,7 @@@ This is a search result (and I guess se
  'prefs-beta' => "Header of a subsection at [[Special:Preferences]], tab ''{{int:prefs-editing}}'', listing features that are in beta but mostly suitable for general use",
  'prefs-datetime' => '{{Identical|Date}}',
  'prefs-labs' => "Header of a subsection at [[Special:Preferences]], tab ''{{int:prefs-editing}}'', listing features that are experimental",
+ 'prefs-user-pages' => "Header of a subsection at [[Special:Preferences]], tab ''{{int:prefs-misc}}'', listing features that are related to user pages",
  'prefs-personal' => 'Title of a tab in [[Special:Preferences]].',
  'prefs-rc' => 'Used in user preferences.
  
@@@ -1770,7 -1773,7 +1773,7 @@@ This action allows editing of all of th
  
  {{Identical|Recent changes}}',
  'recentchanges-legend' => 'Legend of the fieldset of [[Special:RecentChanges]]',
- 'recentchangestext' => 'Text in recent changes',
+ 'recentchanges-summary' => 'Summary of [[Special:RecentChanges]].',
  'recentchanges-label-newpage' => 'Tooltip for {{msg-mw|newpageletter}}',
  'recentchanges-label-minor' => 'Tooltip for {{msg-mw|newpageletter}}',
  'recentchanges-label-bot' => 'Tooltip for {{msg-mw|boteditletter}}',
@@@ -1966,6 -1969,8 +1969,8 @@@ Extensions making use of it
  Parameters:
  * $1 is the number of operations attempted at once in this case.
  * $2 is the maximum number of operations that can be attempted at once.',
+ 'backend-fail-usable' => 'Parameters:
+ * $1 is the file name, including the path, formatted for the storage backend used',
  
  # File journal errors
  'filejournal-fail-dbconnect' => 'Parameters:
@@@ -2032,7 -2037,6 +2037,6 @@@ Siebrand think this has to do with allo
  
  If \'scheme\' is difficult to translate, then you could use \'prefix\' instead.',
  'http-bad-status' => '$1 is an HTTP error code (e.g. 404), $2 is the HTTP error message (e.g. File Not Found)',
- 'http-truncated-body' => 'This is a standard HTTP error message. → Seems the connection closed prematurely. The HTTP response contained a content-length greater than the received body.',
  
  'license' => 'This appears in the upload form for the license drop-down. The header in the file description page is now at {{msg-mw|License-header}}.',
  'nolicense' => '{{Identical|None selected}}',
@@@ -2343,7 -2347,7 +2347,7 @@@ $1 is a page title"
  'nopagetitle' => 'Used as title of [[Special:MovePage]], when the oldtitle does not exist.
  
  The text is {{msg-mw|nopagetext}}.',
- 'nopagetext' => 'Used as text of [[Special:MovePage]], when the oldtitle does not exist.
+ 'nopagetext' => 'Used as text on special pages like [[Special:MovePage]] (when the oldtitle does not exist) or [[Special:PermaLink]].
  
  The title is {{msg-mw|nopagetitle}}.',
  'pager-newer-n' => "This is part of the navigation message on the top and bottom of Special pages which are lists of things in date order, e.g. the User's contributions page. It is passed as the second argument of {{msg-mw|Viewprevnext}}. $1 is the number of items shown per page.",
@@@ -3048,6 -3052,7 +3052,7 @@@ See also {{msg-mw|Movepagetext-noredire
  'movetalk' => 'The text of the checkbox to watch the associated talk page to the page you are moving. This only appears when the talk page is not empty.',
  'move-subpages' => 'The text of an option on the special page [[Special:MovePage|MovePage]]. If this option is ticked, any subpages will be moved with the main page to a new title.',
  'move-talk-subpages' => 'The text of an option on the special page [[Special:MovePage|MovePage]]. If this option is ticked, any talk subpages will be moved with the talk page to a new title.',
+ 'movepage-max-pages' => 'PROBABLY (A GUESS): when moving a page, you can select an option of moving its subpages, but there is a maximum that can be moved automatically.',
  'movelogpage' => 'Title of [[Special:Log/move]]. Used as heading on that page, and in the dropdown menu on log pages.',
  'movelogpagetext' => "Text on the special page 'Move log'.",
  'movesubpage' => "This is a section header on [[Special:MovePage]], below is a list of subpages.
@@@ -3058,6 -3063,7 +3063,7 @@@ Parameters
  
  {{Identical|Reason}}',
  'revertmove' => '{{Identical|Revert}}',
+ 'delete_and_move' => 'Button text on the move page when the target page already exists.',
  'delete_and_move_text' => 'Used when moving a page, but the destination page already exists and needs deletion. This message is to confirm that you really want to delete the page. See also {{msg|delete and move confirm}}.',
  'delete_and_move_confirm' => 'Used when moving a page, but the destination page already exists and needs deletion. This message is for a checkbox to confirm that you really want to delete the page. See also {{msg|delete and move text}}.',
  'delete_and_move_reason' => 'Shown as reason in content language in the deletion log. Parameter:
@@@ -4021,6 -4027,7 +4027,7 @@@ CW is an abbreviation for clockwise.'
  'exif-contrast-2' => '{{Identical|Hard}}',
  
  'exif-saturation-0' => '{{Identical|Normal}}',
+ 'exif-saturation-2' => 'Color saturation in picture EXIF data',
  
  'exif-sharpness-0' => '{{Identical|Normal}}',
  'exif-sharpness-1' => '{{Identical|Soft}}',
@@@ -4707,10 -4714,4 +4714,10 @@@ $4 is the gender of the target user.'
  'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
 +'content-model-1' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
 +'content-model-2' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
 +'content-model-3' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
 +'content-model-4' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
 +
  );
@@@ -1,11 -1,10 +1,11 @@@
  <?php
  /**
 +* @group ContentHandler
  * @group Database
  * ^--- important, causes temporary tables to be used instead of the real database
  **/
  
- class WikiPageTest extends MediaWikiTestCase {
+ class WikiPageTest extends MediaWikiLangTestCase {
  
        var $pages_to_delete;
  
                                                       'templatelinks',
                                                       'iwlinks' ) );
        }
 -
 +      
        public function setUp() {
+               parent::setUp();
                $this->pages_to_delete = array();
 +
 +              LinkCache::singleton()->clear(); # avoid cached redirect status, etc
        }
  
        public function tearDown() {
                                // fail silently
                        }
                }
+               parent::tearDown();
        }
  
 +      /**
 +       * @param Title $title
 +       * @return WikiPage
 +       */
        protected function newPage( $title ) {
                if ( is_string( $title ) ) $title = Title::newFromText( $title );
  
                return $p;
        }
  
 +
 +      /**
 +       * @param String|Title|WikiPage $page
 +       * @param String $text
 +       * @param int $model
 +       *
 +       * @return WikiPage
 +       */
        protected function createPage( $page, $text, $model = null ) {
                if ( is_string( $page ) ) $page = Title::newFromText( $page );
 -              if ( $page instanceof Title ) $page = $this->newPage( $page );
  
 -              $page->doEdit( $text, "testing", EDIT_NEW );
 +              if ( $page instanceof Title ) {
 +                      $title = $page;
 +                      $page = $this->newPage( $page );
 +              } else {
 +                      $title = null;
 +              }
 +
 +              $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
 +              $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
  
 +      public function testDoEditContent() {
 +              $title = Title::newFromText( "WikiPageTest_testDoEditContent" );
 +
 +              $page = $this->newPage( $title );
 +
 +              $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
 +                                                      . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "[[testing]] 1" );
 +
 +              $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
 +              $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
 +              $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
 +              $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
 +
 +              $id = $page->getId();
 +
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
 +                                                      . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "testing 2" );
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
 +      }
 +      
        public function testDoEdit() {
                $title = Title::newFromText( "WikiPageTest_testDoEdit" );
  
                $text = "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
                       . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.";
  
 -              $page->doEdit( $text, "testing 1" );
 +              $page->doEdit( $text, "[[testing]] 1" );
  
 +              $this->assertTrue( $title->getArticleID() > 0, "Title object should have new page id" );
 +              $this->assertTrue( $page->getId() > 0, "WikiPage should have new page id" );
                $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
                $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
  
                $id = $page->getId();
  
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 1, $n, 'pagelinks should contain one link from the page' );
 +
                # ------------------------
                $page = new WikiPage( $title );
  
                $this->assertEquals( $text, $page->getText() );
        }
  
 +      public function testDoQuickEditContent() {
 +              global $wgUser;
 +
 +              $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text" );
 +
 +              $content = ContentHandler::makeContent( "quick text", $page->getTitle() );
 +              $page->doQuickEditContent( $content, $wgUser, "testing q" );
 +
 +              # ---------------------
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertTrue( $content->equals( $page->getContent() ) );
 +      }
 +      
        public function testDoDeleteArticle() {
                $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
                $id = $page->getId();
  
                $page->doDeleteArticle( "testing deletion" );
  
 +              $this->assertFalse( $page->getTitle()->getArticleID() > 0, "Title object should now have page id 0" );
 +              $this->assertFalse( $page->getId() > 0, "WikiPage should now have page id 0" );
                $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
 +              $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
                $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
  
                $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
                $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
        }
  
+       public function testDoDeleteUpdates() {
+               $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
+               $id = $page->getId();
+               $page->doDeleteUpdates( $id );
+               # ------------------------
+               $dbr = wfGetDB( DB_SLAVE );
+               $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
+               $n = $res->numRows();
+               $res->free();
+               $this->assertEquals( 0, $n, 'pagelinks should contain no more links from the page' );
+       }
        public function testGetRevision() {
                $page = $this->newPage( "WikiPageTest_testGetRevision" );
  
                $rev = $page->getRevision();
  
                $this->assertEquals( $page->getLatest(), $rev->getId() );
 -              $this->assertEquals( "some text", $rev->getText() );
 +              $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
 +      }
 +
 +      public function testGetContent() {
 +              $page = $this->newPage( "WikiPageTest_testGetContent" );
 +
 +              $content = $page->getContent();
 +              $this->assertNull( $content );
 +
 +              # -----------------
 +              $this->createPage( $page, "some text" );
 +
 +              $content = $page->getContent();
 +              $this->assertEquals( "some text", $content->getNativeData() );
        }
  
        public function testGetText() {
                $this->assertEquals( "some text", $text );
        }
  
 -      
 +      public function testGetContentModel() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
 +      }
 +
 +      public function testGetContentHandler() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
 +      }
 +
        public function testExists() {
                $page = $this->newPage( "WikiPageTest_testExists" );
                $this->assertFalse( $page->exists() );
        public function testGetRedirectTarget( $title, $text, $target ) {
                $page = $this->createPage( $title, $text );
  
 +              # sanity check, because this test seems to fail for no reason for some people.
 +              $c = $page->getContent();
 +              $this->assertEquals( 'WikitextContent', get_class( $c ) );
 +              
                # now, test the actual redirect
                $t = $page->getRedirectTarget();
                $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
        public function testIsCountable( $title, $text, $mode, $expected ) {
                global $wgArticleCountMethod;
  
 -              $old = $wgArticleCountMethod;
 +              $oldArticleCountMethod = $wgArticleCountMethod;
                $wgArticleCountMethod = $mode;
  
                $page = $this->createPage( $title, $text );
 -              $editInfo = $page->prepareTextForEdit( $page->getText() );
 +              $hasLinks = wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 +                                      array( 'pl_from' => $page->getId() ), __METHOD__ );
 +
 +              $editInfo = $page->prepareContentForEdit( $page->getContent() );
  
                $v = $page->isCountable();
                $w = $page->isCountable( $editInfo );
 -              $wgArticleCountMethod = $old;
 +
 +              $wgArticleCountMethod = $oldArticleCountMethod;
  
                $this->assertEquals( $expected, $v, "isCountable( null ) returned unexpected value " . var_export( $v, true )
                                                    . " instead of " . var_export( $expected, true ) . " in mode `$mode` for text \"$text\"" );
@@@ -597,18 -478,6 +614,18 @@@ more stuf
                $this->assertEquals( $expected, $text );
        }
  
 +      /**
 +       * @dataProvider dataReplaceSection
 +       */
 +      public function testReplaceSectionContent( $title, $text, $section, $with, $sectionTitle, $expected ) {
 +              $page = $this->createPage( $title, $text );
 +
 +              $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
 +              $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
 +
 +              $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
 +      }
 +      
        /* @FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
  
                $user2 = new User();
                $user2->setName( "127.0.2.13" );
                $text .= "\n\nthree";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section three", 0, false, $user2 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three", 0, false, $user2 );
  
                # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
                # or not committed under some circumstances. so, make sure the last revision has the right user name.
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
 -              $this->assertEquals( "one\n\ntwo", $page->getText() );
 +              $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
  
        /**
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
                $rev1 = $page->getRevision();
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
  
                # now, try the rollback
                $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
 -              $this->assertEquals( "one", $page->getText() );
 +              $this->assertEquals( "one", $page->getContent()->getNativeData() );
        }
  
        public function dataGetAutosummary( ) {
                        if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
                        else $user = $wgUser;
  
 -                      $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
 +                      $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
 +
 +                      $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
  
                        $c += 1;
                }
@@@ -1,16 -1,12 +1,15 @@@
  <?php
  
  /**
 + * @group medium
 + * ^---- causes phpunit to use a higher timeout threshold
 + * 
   * @group FileRepo
   * @group FileBackend
   */
  class FileBackendTest extends MediaWikiTestCase {
        private $backend, $multiBackend;
        private $filesToPrune = array();
-       private $dirsToPrune = array();
        private static $backendToUse;
  
        function setUp() {
                return $cases;
        }
  
+       public function testDoQuickOperations() {
+               $this->backend = $this->singleBackend;
+               $this->doTestDoQuickOperations();
+               $this->tearDownFiles();
+               $this->backend = $this->multiBackend;
+               $this->doTestDoQuickOperations();
+               $this->tearDownFiles();
+       }
+       private function doTestDoQuickOperations() {
+               $backendName = $this->backendClass();
+               $base = $this->baseStorePath();
+               $files = array(
+                       "$base/unittest-cont1/fileA.a",
+                       "$base/unittest-cont1/fileB.a",
+                       "$base/unittest-cont1/fileC.a"
+               );
+               $ops = array();
+               $purgeOps = array();
+               foreach ( $files as $path ) {
+                       $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+                       $this->assertGoodStatus( $status,
+                               "Preparing $path succeeded without warnings ($backendName)." );
+                       $ops[] = array( 'op' => 'create', 'dst' => $path, 'content' => mt_rand(0,50000) );
+                       $purgeOps[] = array( 'op' => 'delete', 'src' => $path );
+               }
+               $purgeOps[] = array( 'op' => 'null' );
+               $status = $this->backend->doQuickOperations( $ops );
+               $this->assertGoodStatus( $status,
+                       "Creation of source files succeeded ($backendName)." );
+               foreach ( $files as $file ) {
+                       $this->assertTrue( $this->backend->fileExists( array( 'src' => $file ) ),
+                               "File $file exists." );
+               }
+               $status = $this->backend->doQuickOperations( $purgeOps );
+               $this->assertGoodStatus( $status,
+                       "Quick deletion of source files succeeded ($backendName)." );
+               foreach ( $files as $file ) {
+                       $this->assertFalse( $this->backend->fileExists( array( 'src' => $file ) ),
+                               "File $file purged." );
+               }
+       }
        /**
         * @dataProvider provider_testConcatenate
         */
  
                if ( $alreadyExists ) {
                        $this->prepare( array( 'dir' => dirname( $path ) ) );
-                       $status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) );
+                       $status = $this->create( array( 'dst' => $path, 'content' => $content ) );
                        $this->assertGoodStatus( $status,
                                "Creation of file at $path succeeded ($backendName)." );
  
  
                        $this->assertEquals( strlen( $content ), $size,
                                "Correct file size of '$path'" );
-                       $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5,
+                       $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
                                "Correct file timestamp of '$path'" );
  
                        $size = $stat['size'];
                        $time = $stat['mtime'];
                        $this->assertEquals( strlen( $content ), $size,
                                "Correct file size of '$path'" );
-                       $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5,
+                       $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10,
                                "Correct file timestamp of '$path'" );
                } else {
                        $size = $this->backend->getFileSize( array( 'src' => $path ) );
  
                $this->prepare( array( 'dir' => dirname( $source ) ) );
  
-               $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+               $status = $this->create( array( 'content' => $content, 'dst' => $source ) );
                $this->assertGoodStatus( $status,
                        "Creation of file at $source succeeded ($backendName)." );
  
                $this->tearDownFiles();
        }
  
-       function doTestRecursiveClean() {
+       private function doTestRecursiveClean() {
                $backendName = $this->backendClass();
  
                $base = $this->baseStorePath();
                $fileD = "$base/unittest-cont1/a/b/fileD.txt";
  
                $this->prepare( array( 'dir' => dirname( $fileA ) ) );
-               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
                $this->prepare( array( 'dir' => dirname( $fileB ) ) );
-               $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+               $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
                $this->prepare( array( 'dir' => dirname( $fileC ) ) );
-               $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+               $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
                $this->prepare( array( 'dir' => dirname( $fileD ) ) );
  
                $status = $this->backend->doOperations( array(
        }
  
        // concurrency orientated
-       function doTestDoOperations2() {
+       private function doTestDoOperations2() {
                $base = $this->baseStorePath();
  
                $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
                $fileD = "$base/unittest-cont1/a/b/fileD.txt";
  
                $this->prepare( array( 'dir' => dirname( $fileA ) ) );
-               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
                $this->prepare( array( 'dir' => dirname( $fileB ) ) );
                $this->prepare( array( 'dir' => dirname( $fileC ) ) );
                $this->prepare( array( 'dir' => dirname( $fileD ) ) );
                        "Correct file SHA-1 of $fileC" );
        }
  
-       function doTestDoOperationsFailing() {
+       private function doTestDoOperationsFailing() {
                $base = $this->baseStorePath();
  
                $fileA = "$base/unittest-cont2/a/b/fileA.txt";
                $fileD = "$base/unittest-cont2/a/b/fileD.txt";
  
                $this->prepare( array( 'dir' => dirname( $fileA ) ) );
-               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
                $this->prepare( array( 'dir' => dirname( $fileB ) ) );
-               $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+               $this->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
                $this->prepare( array( 'dir' => dirname( $fileC ) ) );
-               $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+               $this->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
  
                $status = $this->backend->doOperations( array(
                        array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
                        $this->prepare( array( 'dir' => dirname( $file ) ) );
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
-               $status = $this->backend->doOperations( $ops );
+               $status = $this->backend->doQuickOperations( $ops );
                $this->assertGoodStatus( $status,
                        "Creation of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        $this->prepare( array( 'dir' => dirname( $file ) ) );
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
-               $status = $this->backend->doOperations( $ops );
+               $status = $this->backend->doQuickOperations( $ops );
                $this->assertGoodStatus( $status,
                        "Creation of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
  
        // test helper wrapper for backend prepare() function
        private function prepare( array $params ) {
-               $this->dirsToPrune[] = $params['dir'];
                return $this->backend->prepare( $params );
        }
  
+       // test helper wrapper for backend prepare() function
+       private function create( array $params ) {
+               $params['op'] = 'create';
+               return $this->backend->doQuickOperations( array( $params ) );
+       }
        function tearDownFiles() {
                foreach ( $this->filesToPrune as $file ) {
                        @unlink( $file );
                foreach ( $containers as $container ) {
                        $this->deleteFiles( $container );
                }
-               foreach ( $this->dirsToPrune as $dir ) {
-                       $this->recursiveClean( $dir );
-               }
-               $this->filesToPrune = $this->dirsToPrune = array();
+               $this->filesToPrune = array();
        }
  
        private function deleteFiles( $container ) {
                                        array( 'force' => 1, 'nonLocking' => 1 ) );
                        }
                }
-       }
-       private function recursiveClean( $dir ) {
-               $this->backend->clean( array( 'dir' => $dir, 'recursive' => 1 ) );
+               $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
        }
  
        function assertGoodStatus( $status, $msg ) {