merged master
authorjeroendedauw <jeroendedauw@gmail.com>
Wed, 6 Jun 2012 14:42:18 +0000 (16:42 +0200)
committerjeroendedauw <jeroendedauw@gmail.com>
Wed, 6 Jun 2012 14:44:15 +0000 (16:44 +0200)
Change-Id: I4cf7b0f87cd571a6b50f66995dd9ad987a6ecdf8

15 files changed:
1  2 
includes/Article.php
includes/AutoLoader.php
includes/Export.php
includes/ImagePage.php
includes/LinksUpdate.php
includes/Revision.php
includes/WikiPage.php
includes/actions/RawAction.php
includes/api/ApiPurge.php
includes/specials/SpecialUndelete.php
languages/Language.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/maintenance/DumpTestCase.php

diff --combined includes/Article.php
@@@ -57,17 -57,10 +57,17 @@@ class Article extends Page 
        public $mParserOptions;
  
        /**
 -       * Content of the revision we are working on
 +       * Text of the revision we are working on
         * @var string $mContent
         */
 -      var $mContent;                    // !<
 +      var $mContent;                    // !< #BC cruft
 +
 +      /**
 +       * Content of the revision we are working on
 +       * @var Content
 +       * @since 1.WD
 +       */
 +      var $mContentObject;              // !<
  
        /**
         * Is the content ($mContent) already loaded?
         * 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() );
                }
        }
  
                        $this->doDelete( $reason, $suppress );
  
                        if ( $request->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) {
-                               $this->doWatch();
+                               WatchAction::doWatch( $title, $user );
                        } elseif ( $title->userIsWatching() ) {
-                               $this->doUnwatch();
+                               WatchAction::doUnwatch( $title, $user );
                        }
  
                        return;
                // 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
         * @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
@@@ -71,8 -71,8 +71,8 @@@ $wgAutoloadLocalClasses = array
        'DeferredUpdates' => 'includes/DeferredUpdates.php',
        'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
        'DerivativeRequest' => 'includes/WebRequest.php',
-       'DeviceDetection' => 'includes/DeviceDetection.php',
-       'DeviceProperties' => 'includes/DeviceDetection.php',
+       'DeviceDetection' => 'includes/mobile/DeviceDetection.php',
+       'DeviceProperties' => 'includes/mobile/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',
+       'IDeviceProperties' => 'includes/mobile/DeviceDetection.php',
+       'IDeviceDetector' => 'includes/mobile/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',
        'DBConnectionError' => 'includes/db/DatabaseError.php',
        'DBError' => 'includes/db/DatabaseError.php',
        'DBObject' => 'includes/db/DatabaseUtility.php',
+       'IORMRow' => 'includes/db/IORMRow.php',
+       'IORMTable' => 'includes/db/IORMTable.php',
        'DBMasterPos' => 'includes/db/DatabaseUtility.php',
        'DBQueryError' => 'includes/db/DatabaseError.php',
        'DBUnexpectedError' => 'includes/db/DatabaseError.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',
diff --combined includes/Export.php
@@@ -468,7 -468,7 +468,7 @@@ class XmlDumpWriter 
         * @return string
         */
        function schemaVersion() {
-               return "0.6";
+               return "0.7";
        }
  
        /**
                        $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
  
 -              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 -                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 -              } else {
 -                      $out .= "      <sha1/>\n";
 +              if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
 +                      $name = ContentHandler::getContentModelName( $row->rev_content_model );
 +                      $out .= "      " . Xml::element('model', array( 'name' => $name ), strval( $row->rev_content_model ) ) . "\n";
 +              }
 +
 +              if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
 +                      $mime = ContentHandler::getContentFormatMimeType( $row->rev_content_format );
 +                      $out .= "      " . Xml::element('format', array( 'mime' => $mime ), strval( $row->rev_content_format ) ) . "\n";
                }
  
                $text = '';
                                "" ) . "\n";
                }
  
-               if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
-                       $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
-               } else {
-                       $out .= "      <sha1/>\n";
-               }
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
  
                $out .= "    </revision>\n";
        function writeLogItem( $row ) {
                wfProfileIn( __METHOD__ );
  
-               $out  = "    <logitem>\n";
-               $out .= "      " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
+               $out  = "  <logitem>\n";
+               $out .= "    " . Xml::element( 'id', null, strval( $row->log_id ) ) . "\n";
  
-               $out .= $this->writeTimestamp( $row->log_timestamp );
+               $out .= $this->writeTimestamp( $row->log_timestamp, "    " );
  
                if ( $row->log_deleted & LogPage::DELETED_USER ) {
-                       $out .= "      " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
+                       $out .= "    " . Xml::element( 'contributor', array( 'deleted' => 'deleted' ) ) . "\n";
                } else {
-                       $out .= $this->writeContributor( $row->log_user, $row->user_name );
+                       $out .= $this->writeContributor( $row->log_user, $row->user_name, "    " );
                }
  
                if ( $row->log_deleted & LogPage::DELETED_COMMENT ) {
-                       $out .= "      " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
+                       $out .= "    " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
                } elseif ( $row->log_comment != '' ) {
-                       $out .= "      " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
+                       $out .= "    " . Xml::elementClean( 'comment', null, strval( $row->log_comment ) ) . "\n";
                }
  
-               $out .= "      " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
-               $out .= "      " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
+               $out .= "    " . Xml::element( 'type', null, strval( $row->log_type ) ) . "\n";
+               $out .= "    " . Xml::element( 'action', null, strval( $row->log_action ) ) . "\n";
  
                if ( $row->log_deleted & LogPage::DELETED_ACTION ) {
-                       $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
+                       $out .= "    " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                } else {
                        $title = Title::makeTitle( $row->log_namespace, $row->log_title );
-                       $out .= "      " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
-                       $out .= "      " . Xml::elementClean( 'params',
+                       $out .= "    " . Xml::elementClean( 'logtitle', null, self::canonicalTitle( $title ) ) . "\n";
+                       $out .= "    " . Xml::elementClean( 'params',
                                array( 'xml:space' => 'preserve' ),
                                strval( $row->log_params ) ) . "\n";
                }
  
-               $out .= "    </logitem>\n";
+               $out .= "  </logitem>\n";
  
                wfProfileOut( __METHOD__ );
                return $out;
         * @param $timestamp string
         * @return string
         */
-       function writeTimestamp( $timestamp ) {
+       function writeTimestamp( $timestamp, $indent = "      " ) {
                $ts = wfTimestamp( TS_ISO_8601, $timestamp );
-               return "      " . Xml::element( 'timestamp', null, $ts ) . "\n";
+               return $indent . Xml::element( 'timestamp', null, $ts ) . "\n";
        }
  
        /**
         * @param $text string
         * @return string
         */
-       function writeContributor( $id, $text ) {
-               $out = "      <contributor>\n";
+       function writeContributor( $id, $text, $indent = "      " ) {
+               $out = $indent . "<contributor>\n";
                if ( $id || !IP::isValid( $text ) ) {
-                       $out .= "        " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
-                       $out .= "        " . Xml::element( 'id', null, strval( $id ) ) . "\n";
+                       $out .= $indent . "  " . Xml::elementClean( 'username', null, strval( $text ) ) . "\n";
+                       $out .= $indent . "  " . Xml::element( 'id', null, strval( $id ) ) . "\n";
                } else {
-                       $out .= "        " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
+                       $out .= $indent . "  " . Xml::elementClean( 'ip', null, strval( $text ) ) . "\n";
                }
-               $out .= "      </contributor>\n";
+               $out .= $indent . "</contributor>\n";
                return $out;
        }
  
                } else {
                        $contents = '';
                }
+               if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
+                       $comment = Xml::element( 'comment', array( 'deleted' => 'deleted' ) );
+               } else {
+                       $comment = Xml::elementClean( 'comment', null, $file->getDescription() );
+               }
                return "    <upload>\n" .
                        $this->writeTimestamp( $file->getTimestamp() ) .
                        $this->writeContributor( $file->getUser( 'id' ), $file->getUser( 'text' ) ) .
-                       "      " . Xml::elementClean( 'comment', null, $file->getDescription() ) . "\n" .
+                       "      " . $comment . "\n" .
                        "      " . Xml::element( 'filename', null, $file->getName() ) . "\n" .
                        $archiveName .
                        "      " . Xml::element( 'src', null, $file->getCanonicalUrl() ) . "\n" .
diff --combined includes/ImagePage.php
@@@ -157,10 -157,8 +157,10 @@@ class ImagePage extends Article 
                        $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
 -                      parent::view();
 -                      $out->addHTML( Xml::closeElement( 'div' ) );
 +
 +            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
 +
 +                      $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
                        $out->setArticleFlag( true );
                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;
@@@ -992,7 -990,7 +992,7 @@@ class ImageHistoryList extends ContextS
                $img = $iscur ? $file->getName() : $file->getArchiveName();
                $userId = $file->getUser( 'id' );
                $userText = $file->getUser( 'text' );
-               $description = $file->getDescription();
+               $description = $file->getDescription( File::FOR_THIS_USER, $user );
  
                $local = $this->current->isLocal();
                $row = $selected = '';
                $row .= '<td>';
                if ( $iscur ) {
                        $row .= wfMsgHtml( 'filehist-current' );
-               } elseif ( $local && $user->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
+               } elseif ( $local && $this->title->quickUserCan( 'edit' )
+                       && $this->title->quickUserCan( 'upload' )
+               ) {
                        if ( $file->isDeleted( File::DELETED_FILE ) ) {
                                $row .= wfMsgHtml( 'filehist-revert' );
                        } else {
diff --combined includes/LinksUpdate.php
@@@ -39,6 -39,8 +39,6 @@@ class LinksUpdate extends SqlDataUpdat
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
 -              $mDb,            //!< Database connection reference
 -              $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
  
        /**
  
                $this->mTitle = $title;
                $this->mId = $title->getArticleID();
 +              assert( $this->mId > 0 );
  
+               if ( !$this->mId ) {
+                       throw new MWException( "The Title object did not provide an article ID. Perhaps the page doesn't exist?" );
+               }
                $this->mParserOutput = $parserOutput;
 +
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
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;
        }
  
        /**
                        'page_namespace',
                        'page_title',
                        'page_id',
-                       'page_latest'
+                       'page_latest',
+                       'page_is_redirect',
+                       'page_len',
                );
        }
  
                                $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
        }
  
        /**
         * Get revision ID
         *
-        * @return Integer
+        * @return Integer|null
         */
        public function getId() {
                return $this->mId;
        /**
         * Get text row ID
         *
-        * @return Integer
+        * @return Integer|null
         */
        public function getTextId() {
                return $this->mTextId;
        /**
         * Returns the length of the text in this revision, or null if unknown.
         *
-        * @return Integer
+        * @return Integer|null
         */
        public function getSize() {
                return $this->mSize;
        /**
         * Returns the base36 sha1 of the text in this revision, or null if unknown.
         *
-        * @return String
+        * @return String|null
         */
        public function getSha1() {
                return $this->mSha1;
        }
  
        /**
-        * Returns the title of the page associated with this entry.
+        * Returns the title of the page associated with this entry or null.
         *
-        * @return Title
+        * Will do a query, when title is not set and id is given.
+        *
+        * @return Title|null
         */
        public function getTitle() {
                if( isset( $this->mTitle ) ) {
                        return $this->mTitle;
                }
-               $dbr = wfGetDB( DB_SLAVE );
-               $row = $dbr->selectRow(
-                       array( 'page', 'revision' ),
-                       self::selectPageFields(),
-                       array( 'page_id=rev_page',
-                                  'rev_id' => $this->mId ),
-                       __METHOD__ );
-               if ( $row ) {
-                       $this->mTitle = Title::newFromRow( $row );
+               if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
+                       $dbr = wfGetDB( DB_SLAVE );
+                       $row = $dbr->selectRow(
+                               array( 'page', 'revision' ),
+                               self::selectPageFields(),
+                               array( 'page_id=rev_page',
+                                          'rev_id' => $this->mId ),
+                               __METHOD__ );
+                       if ( $row ) {
+                               $this->mTitle = Title::newFromRow( $row );
+                       }
                }
                return $this->mTitle;
        }
        /**
         * Get the page ID
         *
-        * @return Integer
+        * @return Integer|null
         */
        public function getPage() {
                return $this->mPage;
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
-        *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *      Revision::FOR_THIS_USER    to be displayed to the given user
         *      Revision::RAW              get the ID regardless of permissions
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
-        *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *      Revision::FOR_THIS_USER    to be displayed to the given user
         *      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
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
-        *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *      Revision::FOR_THIS_USER    to be displayed to the given user
         *      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
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
-        *      Revision::FOR_THIS_USER    to be displayed to $wgUser
+        *      Revision::FOR_THIS_USER    to be displayed to the given user
         *      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 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', '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,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
                                'sha1'       => $current->rev_sha1
 -                              ) );
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
                        $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
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();
        }
  
        /**
                }
  
                wfProfileOut( __METHOD__ );
-               if ( $row ) {
-                       return Revision::newFromRow( $row );
-               } else {
-                       return null;
-               }
+               return $row ? Revision::newFromRow( $row ) : null;
        }
  
        /**
        }
  
        /**
 -       * 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 ) {
 +      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
 +      }
 +
 +      /**
 +       * Change an existing article or create a new article. Updates RC and all necessary caches,
 +       * optionally via the deferred update array.
 +       *
 +       * @param $content Content: new content
 +       * @param $summary String: edit summary
 +       * @param $flags Integer bitfield:
 +       *      EDIT_NEW
 +       *          Article is known or assumed to be non-existent, create a new one
 +       *      EDIT_UPDATE
 +       *          Article is known or assumed to be pre-existing, update it
 +       *      EDIT_MINOR
 +       *          Mark this edit minor, if the user is allowed to do so
 +       *      EDIT_SUPPRESS_RC
 +       *          Do not log the change in recentchanges
 +       *      EDIT_FORCE_BOT
 +       *          Mark the edit a "bot" edit regardless of user rights
 +       *      EDIT_DEFER_UPDATES
 +       *          Defer some of the updates until the end of index.php
 +       *      EDIT_AUTOSUMMARY
 +       *          Fill in blank summaries with generated text where possible
 +       *
 +       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
 +       * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
 +       * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
 +       * edit-already-exists error will be returned. These two conditions are also possible with
 +       * auto-detection due to MediaWiki's performance-optimised locking strategy.
 +       *
 +       * @param $baseRevId the revision ID this edit was based off, if any
 +       * @param $user User the user doing the edit
 +       * @param $serialisation_format String: format for storing the content in the database
 +       *
 +       * @return Status object. Possible errors:
 +       *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
 +       *     edit-gone-missing:       In update mode, but the article didn't exist
 +       *     edit-conflict:           In update mode, the article changed unexpectedly
 +       *     edit-no-change:          Warning that the text was the same as before
 +       *     edit-already-exists:     In creation mode, but the article already exists
 +       *
 +       *  Extensions may define additional errors.
 +       *
 +       *  $return->value will contain an associative array with members as follows:
 +       *     new:                     Boolean indicating if the function attempted to create a new article
 +       *     revision:                The revision object for the inserted revision, or null
 +       *
 +       * @since 1.WD
 +       */
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
  
                # Low-level sanity check
  
                $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__
                $this->mTitle->resetArticleID( 0 );
        }
  
 -      public function getDeletionUpdates() {
 -              $updates = array(
 -                      new LinksDeletionUpdate( $this ),
 -              );
 -
 -              //@todo: make a hook to add update objects
 -              //NOTE: deletion updates will be determined by the ContentHandler in the future
 -              return $updates;
 -      }
 -
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
                }
  
                # 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 );
 -              }
 -
 -              # New page autosummaries
 -              if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
 -                      # If they're making a new article, give its text, truncated, in the summary.
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
 -
 -                      return wfMsgForContent( 'autosumm-new', $truncatedtext );
 -              }
 +              # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
  
 -              # 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
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
 +              $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-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
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
 +
 +      public function getDeletionUpdates() {
 +              $updates = $this->getContentHandler()->getDeletionUpdates( $this );
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, &$updates ) );
 +              return $updates;
 +      }
++
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        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;
        }
  }
 +
@@@ -7,7 -7,20 +7,20 @@@
   *
   * Based on HistoryPage and SpecialExport
   *
-  * License: GPL (http://www.gnu.org/copyleft/gpl.html)
+  * 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
   *
   * @author Gabriel Wicke <wicke@wikidev.net>
   * @file
@@@ -135,20 -148,11 +148,20 @@@ class RawAction extends FormlessAction 
                                $request->response()->header( "Last-modified: $lastmod" );
  
                                // Public-only due to cache headers
 -                              $text = $rev->getText();
 +                              $content = $rev->getContent();
 +
 +                              if ( !$content instanceof TextContent ) {
 +                                      wfHttpError( 406, "Not Acceptable", "The requeste page uses the content model `"
 +                                                                                                              . $content->getModel() . "` which is not supported via this interface." );
 +                                      die();
 +                              }
 +
                                $section = $request->getIntOrNull( 'section' );
                                if ( $section !== null ) {
 -                                      $text = $wgParser->getSection( $text, $section );
 +                                      $content = $content->getSection( $section );
                                }
 +
 +                              $text = $content->getNativeData();
                        }
                }
  
@@@ -91,7 -91,7 +91,7 @@@ class ApiPurge extends ApiBase 
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
                                        $popts->setTidy( true );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
 -                                              true, true, $page->getLatest() );
 +                                              true, true, $page->getLatest() ); #FIXME: content!
  
                                        # Update the links tables
                                        $updates = $p_result->getSecondaryDataUpdates( $title );
                                                $pcache->save( $p_result, $page, $popts );
                                        }
                                } else {
-                                       $this->setWarning( $this->parseMsg( array( 'actionthrottledtext' ) ) );
+                                       $error = $this->parseMsg( array( 'actionthrottledtext' ) );
+                                       $this->setWarning( $error['info'] );
                                        $forceLinkUpdate = false;
                                }
                        }
@@@ -112,22 -112,12 +112,22 @@@ class PageArchive 
         * @return ResultWrapper
         */
        function listRevisions() {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 +                      'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $res = $dbr->select( 'archive',
 -                      array(
 -                              'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 -                              'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        'PageArchive::listRevisions',
         * @return Revision
         */
        function getRevision( $timestamp ) {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_len',
 +                      'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $row = $dbr->selectRow( 'archive',
 -                      array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_len',
 -                              'ar_sha1',
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
         * @return Mixed: number of revisions restored or false on failure
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
 +              global $wgContentHandlerNoDB;
 +
                if ( wfReadOnly() ) {
                        return false;
                }
                        $oldones = "ar_timestamp IN ( {$oldts} )";
                }
  
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_page_id',
 +                      'ar_len',
 +                      'ar_sha1');
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
 -                      /* fields */ array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_page_id',
 -                              'ar_len',
 -                              'ar_sha1' ),
 +                      $fields,
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@@ -921,8 -892,7 +921,8 @@@ class SpecialUndelete extends SpecialPa
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
 -              $diffEngine = new DifferenceEngine( $this->getContext() );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $diffEngine = $contentHandler->createDifferenceEngine( $this->getContext() );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
 -                      $diffEngine->generateDiffBody(
 -                              $previousRev->getText(), $currentRev->getText() ) .
 +                      $diffEngine->generateContentDiffBody(
 +                              $previousRev->getContent(), $currentRev->getContent() ) .
                        "</table>" .
                        "</div>\n"
                );
        private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
                $rev = Revision::newFromArchiveRow( $row,
                        array( 'page' => $this->mTargetObj->getArticleID() ) );
-               $stxt = '';
+               $revTextSize = '';
                $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
                // Build checkboxen...
                if( $this->mAllowed ) {
                // Revision text size
                $size = $row->ar_len;
                if( !is_null( $size ) ) {
-                       $stxt = Linker::formatRevisionSize( $size );
+                       $revTextSize = Linker::formatRevisionSize( $size );
                }
                // Edit summary
                $comment = Linker::revComment( $rev );
                // Revision delete links
                $revdlink = Linker::getRevDeleteLink( $user, $rev, $this->mTargetObj );
-               return "<li>$checkBox $revdlink ($last) $pageLink . . $userLink $stxt $comment</li>";
+               $revisionRow = $this->msg( 'undelete-revisionrow' )->rawParams( $checkBox, $revdlink, $last, $pageLink , $userLink, $revTextSize, $comment )->escaped();
+               return "<li>$revisionRow</li>";
        }
  
        private function formatFileRow( $row ) {
diff --combined languages/Language.php
@@@ -284,10 -284,6 +284,6 @@@ class Language 
                }
  
                if ( !defined( 'MW_COMPILED' ) ) {
-                       // Preload base classes to work around APC/PHP5 bug
-                       if ( file_exists( "$IP/languages/classes/$class.deps.php" ) ) {
-                               include_once( "$IP/languages/classes/$class.deps.php" );
-                       }
                        if ( file_exists( "$IP/languages/classes/$class.php" ) ) {
                                include_once( "$IP/languages/classes/$class.php" );
                        }
         */
        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;
        }
  
        /**
                $mwNames = $wgExtraLanguageNames + $coreLanguageNames;
                foreach ( $mwNames as $mwCode => $mwName ) {
                        # - Prefer own MediaWiki native name when not using the hook
-                       #       TODO: prefer it always to make it consistent, but casing is different in CLDR
                        # - For other names just add if not added through the hook
-                       if ( ( $mwCode === $inLanguage && !$inLanguage ) || !isset( $names[$mwCode] ) ) {
+                       if ( $mwCode === $inLanguage || !isset( $names[$mwCode] ) ) {
                                $names[$mwCode] = $mwName;
                        }
                }
                        return $s;
                }
  
-               $isutf8 = preg_match( '/^([\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
-                               '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
+               if ( function_exists( 'mb_check_encoding' ) ) {
+                       $isutf8 = mb_check_encoding( $s, 'UTF-8' );
+               } else {
+                       $isutf8 = preg_match( '/^(?>[\x00-\x7f]|[\xc0-\xdf][\x80-\xbf]|' .
+                                       '[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xf7][\x80-\xbf]{3})+$/', $s );
+               }
                if ( $isutf8 ) {
                        return $s;
                }
@@@ -191,6 -191,7 +191,7 @@@ $bookstoreList = array
   * Customisable syntax for wikitext and elsewhere.
   *
   * IDs must be valid identifiers, they cannot contain hyphens.
+  * CASE is 0 to match all case variants, 1 for case-sensitive
   *
   * Note to translators:
   *   Please include the English words as synonyms.  This allows people
@@@ -758,6 -759,7 +759,7 @@@ XHTML id names
  'index-category'                 => 'Indexed pages',
  'noindex-category'               => 'Noindexed pages',
  'broken-file-category'           => 'Pages with broken file links',
+ 'categoryviewer-pagedlinks'      => '($1) ($2)',
  
  'linkprefix' => '/^(.*?)([a-zA-Z\\x80-\\xff]+)$/sD', # only translate this message to other languages if you have to change it
  
@@@ -887,7 -889,6 +889,7 @@@ $1'
  'portal-url'           => 'Project:Community portal',
  'privacy'              => 'Privacy policy',
  'privacypage'          => 'Project:Privacy policy',
 +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
  
  'badaccess'        => 'Permission error',
  'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
@@@ -1662,6 -1663,7 +1664,7 @@@ Note that using the navigation links wi
  'mergehistory-comment'             => 'Merged [[:$1]] into [[:$2]]: $3',
  'mergehistory-same-destination'    => 'Source and destination pages cannot be the same',
  'mergehistory-reason'              => 'Reason:',
+ 'mergehistory-revisionrow'         => '$1 ($2) $3 . . $4 $5 $6',
  
  # Merge log
  'mergelog'           => 'Merge log',
@@@ -2656,14 -2658,15 +2659,15 @@@ Please note that other web sites may li
  'pubmedurl' => '//www.ncbi.nlm.nih.gov/pubmed/$1?dopt=Abstract', # do not translate or duplicate this message to other languages
  
  # Special:Log
- 'specialloguserlabel'  => 'Performer:',
- 'speciallogtitlelabel' => 'Target (title or user):',
- 'log'                  => 'Logs',
- 'all-logs-page'        => 'All public logs',
- 'alllogstext'          => 'Combined display of all available logs of {{SITENAME}}.
+ 'specialloguserlabel'        => 'Performer:',
+ 'speciallogtitlelabel'       => 'Target (title or user):',
+ 'log'                        => 'Logs',
+ 'all-logs-page'              => 'All public logs',
+ 'alllogstext'                => 'Combined display of all available logs of {{SITENAME}}.
  You can narrow down the view by selecting a log type, the username (case-sensitive), or the affected page (also case-sensitive).',
- 'logempty'             => 'No matching items in log.',
- 'log-title-wildcard'   => 'Search titles starting with this text',
+ 'logempty'                   => 'No matching items in log.',
+ 'log-title-wildcard'         => 'Search titles starting with this text',
+ 'showhideselectedlogentries' => 'Show/hide selected log entries',
  
  # Special:AllPages
  'allpages'                => 'All pages',
@@@ -3053,6 -3056,7 +3057,7 @@@ It may have already been undeleted.'
  $1',
  'undelete-show-file-confirm'   => 'Are you sure you want to view the deleted revision of the file "<nowiki>$1</nowiki>" from $2 at $3?',
  'undelete-show-file-submit'    => 'Yes',
+ 'undelete-revisionrow'        => "$1 $2 $3 $4 . . $5 $6 $7",
  
  # Namespace form on various pages
  'namespace'                     => 'Namespace:',
@@@ -4427,6 -4431,7 +4432,7 @@@ Please confirm that you really want to 
  'ellipsis'            => '...', # only translate this message to other languages if you have to change it
  'percent'             => '$1%', # only translate this message to other languages if you have to change it
  'parentheses'         => '($1)', # only translate this message to other languages if you have to change it
+ 'brackets'            => '[$1]', # only translate this message to other languages if you have to change it
  
  # Multipage image navigation
  'imgmultipageprev' => '← previous page',
@@@ -4869,10 -4874,4 +4875,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',
 +
  );
@@@ -310,6 -310,9 +310,9 @@@ See http://test.wikipedia.org/wiki/Cate
  'index-category' => 'Name of the [[mw:Help:Tracking categories|tracking category]] where pages with the <nowiki>__INDEX__</nowiki> behaviour switch are listed. For description of this behaviour switch see [//www.mediawiki.org/wiki/Help:Magic_words#Behavior_switches mediawiki].',
  'noindex-category' => 'Name of the [[mw:Help:Tracking categories|tracking category]] where pages with the <nowiki>__NOINDEX__</nowiki> behaviour switch are listed. For description of this behaviour switch see [//www.mediawiki.org/wiki/Help:Magic_words#Behavior_switches mediawiki].',
  'broken-file-category' => 'Name of [[mw:Help:Tracking categories|tracking category]] where pages that embed files that do not exist ("broken images") are listed.',
+ 'categoryviewer-pagedlinks' => 'The pagination links in category viewer. Parameters:
+ * $1 is the previous link,
+ * $2 is the next link',
  
  'linkprefix' => '{{optional}}',
  
@@@ -729,7 -732,9 +732,9 @@@ $1 is a filename, I think.'
  'nologin' => 'A message shown in the log in form. $1 is a link to the account creation form, and the text of it is "[[MediaWiki:Nologinlink/{{SUBPAGENAME}}|{{int:nologinlink}}]]".',
  'nologinlink' => 'Text of the link to the account creation form. Before that link, the message [[MediaWiki:Nologin/{{SUBPAGENAME}}]] appears.
  {{Identical|Create an account}}',
- 'createaccount' => 'The title of Special:CreateAccount, where users can register a new account. Used on Special:SpecialPages, and also on the submit button in the form where you register a new account.
+ 'createaccount' => 'The title of Special:CreateAccount, where users can register a new account. Used on Special:SpecialPages and on the submit button in the form where you register a new account.
+ It is also used on the top of the page for logged out users, where it appears next to {{msg-mw|login}}, so consider making them similar.
  {{Identical|Create account}}',
  'gotaccount' => 'A message shown in the account creation form. $1 is a link to the log in form, and the text of it is "[[MediaWiki:Gotaccountlink/{{SUBPAGENAME}}|{{int:gotaccountlink}}]]".',
  'gotaccountlink' => 'Text of the link to the log in form. Before that link, the message [[MediaWiki:Gotaccount/{{SUBPAGENAME}}]] appears.
@@@ -795,7 -800,7 +800,7 @@@ Parameters
  *Parameter $4 is a URL to the wiki',
  'login-throttled' => 'Error message shown at [[Special:UserLogin]] after 5 wrong passwords. The hardcoded waiting time is 300 seconds.',
  'login-abort-generic' => 'The generic unsuccessful login message is used unless otherwise specified by hook writers',
- 'loginlanguagelabel' => 'Used on [[Special:UserLogin]] if $wgLoginLanguageSelector is true.
+ 'loginlanguagelabel' => 'Used on [[Special:UserLogin]] if $wgLoginLanguageSelector is true. $1 is a pipe-separated list built from the names that appear in the message {{msg-mw|Loginlanguagelinks}}.
  {{Identical|Language}}',
  
  # E-mail sending
@@@ -1241,6 -1246,13 +1246,13 @@@ Parameters
  *Parameter $3 is a log comment for the merge',
  'mergehistory-same-destination' => 'Error message shown on [[Special:MergeHistory]] when the user entered the same page title to both source and destination',
  'mergehistory-reason' => '{{Identical|Reason}}',
+ 'mergehistory-revisionrow' => 'A revision row in the merge history page. Parameters:
+ * $1 is a radio button to indicate a merge point,
+ * $2 is a link to the last revision of a page ({{msg-mw|last}}),
+ * $3 is a page link,
+ * $4 is a user link,
+ * $5 is a revision size,
+ * $6 is a revision comment',
  
  # Merge log
  'mergelog' => 'This is the name of a log of merge actions done on [[Special:MergeHistory]]. This special page and this log is not enabled by default.',
@@@ -1263,7 -1275,7 +1275,7 @@@ Please note that the parameters in a lo
  See also {{msg-mw|difference}}.',
  'lineno' => 'Message used when comparing different versions of a page (diff). $1 is a line number.',
  'compareselectedversions' => 'Used as button in history pages.',
- 'showhideselectedversions' => 'Text of the button which brings up the [[mw:RevisionDelete|RevisionDelete]] menu.',
+ 'showhideselectedversions' => 'Text of the button which brings up the [[mw:RevisionDelete|RevisionDelete]] menu on history pages.',
  'editundo' => 'Undo link when viewing diffs
  {{Identical|Undo}}
  
@@@ -2372,6 -2384,7 +2384,7 @@@ The title is {{msg-mw|nopagetitle}}.'
  'alllogstext' => 'Header of [[Special:Log]]',
  'log-title-wildcard' => '* Appears in: [[Special:Log]]
  * Description: A check box to enable prefix search option',
+ 'showhideselectedlogentries' => 'Text of the button which brings up the [[mw:RevisionDelete|RevisionDelete]] menu on [[Special:Log]].',
  
  # Special:AllPages
  'allpages' => 'First part of the navigation bar for the special page [[Special:AllPages]] and [[Special:PrefixIndex]]. The other parts are {{msg-mw|Prevpage}} and {{msg-mw|Nextpage}}.
@@@ -2789,6 -2802,14 +2802,14 @@@ This message was something like "unloc
  
  {{identical|Are you sure you want to view the deleted revision of the file...}}',
  'undelete-show-file-submit' => '{{Identical|Yes}}',
+ 'undelete-revisionrow' => "A revision row in the undelete page. Parameters:
+ * $1 is a checkBox to indicate whether to restore this specific revision
+ * $2 is a link to the revision
+ * $3 is a link to the last revision of a page ({{msg-mw|last}})
+ * $4 is a link to the page
+ * $5 is a link to the revision's user
+ * $6 is the revision size
+ * $7 is the revision comment",
  
  # Namespace form on various pages
  'namespace' => 'This message is located at [[Special:Contributions]].',
@@@ -3481,7 -3502,7 +3502,7 @@@ Part of variable $1 in {{msg-mw|Ago}
  *{{msg-mw|Days}}',
  
  # Bad image list
- 'bad_image_list' => 'This is only message appears to guide administrators to add links with right format. This will not appear anywhere else in Mediawiki.',
+ 'bad_image_list' => 'This message only appears to guide administrators to add links with the right format. This will not appear anywhere else in MediaWiki.',
  
  /*
  Short names for language variants used for language conversion links.
@@@ -4714,10 -4735,4 +4735,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,9 -1,6 +1,9 @@@
  <?php
  
  /**
 + * @group medium
 + * ^---- causes phpunit to use a higher timeout threshold
 + * 
   * @group FileRepo
   * @group FileBackend
   */
@@@ -1395,8 -1392,11 +1395,11 @@@ class FileBackendTest extends MediaWiki
  
        private function doTestGetFileList() {
                $backendName = $this->backendClass();
                $base = $this->baseStorePath();
+               // Should have no errors
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont-notexists" ) );
                $files = array(
                        "$base/unittest-cont1/test1.txt",
                        "$base/unittest-cont1/test2.txt",
@@@ -1,11 -1,9 +1,9 @@@
  <?php
- global $IP;
- require_once( "$IP/maintenance/backup.inc" );
  
  /**
   * Base TestCase for dumps
   */
- abstract class DumpTestCase extends MediaWikiTestCase {
+ abstract class DumpTestCase extends MediaWikiLangTestCase {
  
        /**
         * exception to be rethrown once in sound PHPUnit surrounding
@@@ -74,7 -72,7 +72,7 @@@
         *
         * Clears $wgUser, and reports errors from addDBData to PHPUnit
         */
-       protected function setUp() {
+       public function setUp() {
                global $wgUser;
  
                parent::setUp();
                $this->skipWhitespace();
  
                $this->assertTextNode( "comment", $summary );
 +              $this->skipWhitespace();
 +
 +              if ( $this->xml->name == "model" ) { // model tag is optional
 +                      $this->assertTextNode( "model", CONTENT_MODEL_WIKITEXT ); //@todo: make this a test parameter
 +                      $this->skipWhitespace();
 +              }
 +
 +
 +              if ( $this->xml->name == "format" ) { // format tag is optional
 +                      $this->assertTextNode( "format", CONTENT_FORMAT_WIKITEXT ); //@todo: make this a test parameter
 +                      $this->skipWhitespace();
 +              }
  
+               $this->assertTextNode( "sha1", $text_sha1 );
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
                        $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
                                "Attribute 'bytes' of revision " . $id );
                }
  
                if ( $text === false ) {
                        // Testing for a stub
                        $this->assertEquals( $this->xml->getAttribute( "id" ), $text_id,
                        $this->skipWhitespace();
                }
  
-               $this->assertTextNode( "sha1", $text_sha1 );
                $this->assertNodeEnd( "revision" );
                $this->skipWhitespace();
        }