merge latest master into Wikidata branch
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 8 Oct 2012 11:58:54 +0000 (13:58 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Mon, 8 Oct 2012 11:58:54 +0000 (13:58 +0200)
Change-Id: Id4e0f40c03679c13d8934a6add99b5cd86d0437d

45 files changed:
1  2 
RELEASE-NOTES-1.21
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Export.php
includes/GlobalFunctions.php
includes/Message.php
includes/OutputPage.php
includes/SqlDataUpdate.php
includes/Title.php
includes/WikiPage.php
includes/actions/RollbackAction.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiQueryRevisions.php
includes/content/WikitextContent.php
includes/diff/DifferenceEngine.php
includes/filerepo/file/LocalFile.php
includes/installer/MysqlUpdater.php
includes/installer/SqliteUpdater.php
includes/job/DoubleRedirectJob.php
includes/parser/Parser.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/search/SearchEngine.php
includes/specials/SpecialUndelete.php
includes/upload/UploadFromUrl.php
languages/Language.php
languages/LanguageConverter.php
languages/messages/MessagesDe.php
languages/messages/MessagesEn.php
languages/messages/MessagesGa.php
languages/messages/MessagesQqq.php
maintenance/Maintenance.php
maintenance/storage/testCompression.php
maintenance/tables.sql
tests/parser/parserTest.inc
tests/phpunit/MediaWikiTestCase.php
tests/phpunit/includes/LinksUpdateTest.php
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/includes/search/SearchEngineTest.php
thumb.php

diff --combined RELEASE-NOTES-1.21
@@@ -11,25 -11,33 +11,37 @@@ MediaWiki 1.21 is an alpha-quality bran
  production.
  
  === Configuration changes in 1.21 ===
+ * (bug 29374) $wgVectorUseSimpleSearch is now enabled by default.
+ * Deprecated $wgAllowRealName is removed. Use $wgHiddenPrefs[] = 'realname' instead
  
  === New features in 1.21 ===
 +* Added ContentHandler facility to allow extensions to support other content than wikitext.
 +  See docs/contenthandler.txt for details.
  
  === Bug fixes in 1.21 ===
+ * (bug 40353) SpecialDoubleRedirect should support interwiki redirects.
+ * (bug 40352) fixDoubleRedirects.php should support interwiki redirects.
+ * (bug 9237) SpecialBrokenRedirect should not list interwiki redirects.
+ * (bug 34960) Drop unused fields rc_moved_to_ns and rc_moved_to_title from recentchanges table.
+ * (bug 32951) Do not register internal externals with absolute protocol,
+   when server has relative protocol.
  
  === API changes in 1.21 ===
 +* prop=revisions can now report the contentmodel and contentformat, see docs/contenthandler.txt
 +* action=edit and action=parse now support contentmodel and contentformat parameters to control the interpretation of
 +  page content; See docs/contenthandler.txt for details.
+ * (bug 35693) ApiQueryImageInfo now suppresses errors when unserializing metadata.
  
  === Languages updated in 1.21 ===
  
 -
+ MediaWiki supports over 350 languages. Many localisations are updated
+ regularly. Below only new and removed languages are listed, as well as
+ changes to languages because of Bugzilla reports.
  === Other changes in 1.21 ===
  
  == Compatibility ==
  
Since version 1.20, MediaWiki requires PHP 5.3.2. PHP 4 is no longer supported.
MediaWiki 1.21 requires PHP 5.3.2. PHP 4 is no longer supported.
  
  MySQL is the recommended DBMS. PostgreSQL or SQLite can also be used, but
  support for them is somewhat less mature. There is experimental support for IBM
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.21
 +       */
 +      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.21; use getContentObject() instead
 +       *
         * @return string Return the text of this revision
         */
        public function getContent() {
 +              wfDeprecated( __METHOD__, '1.21' );
 +              $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.21
 +       *
 +       * @todo: FIXME: this should really be protected, all callers should be changed to use WikiPage::getContent() instead.
 +       */
 +      public function getContentObject() {
 +              global $wgUser;
                wfProfileIn( __METHOD__ );
  
                if ( $this->mPage->getID() === 0 ) {
                                if ( $text === false ) {
                                        $text = '';
                                }
 +
 +                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
                                $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon';
 -                              $text = wfMessage( $message )->text();
 +                              $content = new MessageContent( $message, null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * 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.21, use WikiPage::getContent() instead
         */
 -      function fetchContent() {
 -              if ( $this->mContentLoaded ) {
 +      function fetchContent() { #BC cruft!
 +              wfDeprecated( __METHOD__, '1.21' );
 +
 +              if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
  
                wfProfileIn( __METHOD__ );
  
 +              $content = $this->fetchContentObject();
 +
 +              $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
 +              ContentHandler::runLegacyHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.21
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # Pre-fill content with error message so that if something
                # fails we'll have something telling us what we intended.
 -              $this->mContent = wfMessage( 'missing-revision', $oldid )->plain();
 +              //XXX: this isn't page content but a UI message. horrible.
 +              $this->mContentObject = new MessageContent( 'missing-revision', array( $oldid ), array() ) ;
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $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->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // Loads if user is allowed
++              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER, $this->getContext()->getUser() ); // 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( !ContentHandler::runLegacyHooks( 'ArticleViewCustom',
 +                                                                                                                              array( $this->fetchContentObject(), $this->getTitle(),
 +                                                                                                                                              $outputPage ) ) ) {
 +
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid,
 +                                                                                                                                                              $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
                                        # 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();
        /**
         * Show a diff page according to current request variables. For use within
         * Article::view() only, other callers should use the DifferenceEngine class.
 +       *
 +       * @todo: make protected
         */
        public function showDiffPage() {
                $request = $this->getContext()->getRequest();
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $rev = $this->getRevisionFetched();
 +
 +              if ( !$rev ) {
 +                      $this->getContext()->getOutput()->setPageTitle( wfMessage( 'errorpagetitle' )->text() );
 +                      $this->getContext()->getOutput()->addWikiMsg( 'difference-missing-revision', $oldid, 1 );
 +                      return;
 +              }
 +
 +              $contentHandler = $rev->getContentHandler();
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->fetchContentObject(), $this->getTitle(), $wgOut ) ) ) {
 +                      $po = $this->mContentObject->getParserOutput( $this->getTitle() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
                // 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 ParserOutput or false if the given revsion ID is not found
         */
        public function getParserOutput( $oldid = null, User $user = null ) {
 +              //XXX: bypasses mParserOptions and thus setParserOptions()
 +
                if ( $user === null ) {
                        $parserOptions = $this->getParserOptions();
                } else {
                return $this->mPage->getParserOutput( $parserOptions, $oldid );
        }
  
 +      /**
 +       * Override the ParserOptions used to render the primary article wikitext.
 +       *
 +       * @param ParserOptions $options
 +       * @throws MWException if the parser options where already initialized.
 +       */
 +      public function setParserOptions( ParserOptions $options ) {
 +              if ( $this->mParserOptions ) {
 +                      throw new MWException( "can't change parser options after they have already been set" );
 +              }
 +
 +              // clone, so if $options is modified later, it doesn't confuse the parser cache.
 +              $this->mParserOptions = clone $options;
 +      }
 +
        /**
         * Get parser options suitable for rendering the primary article wikitext
         * @return ParserOptions
                wfDeprecated( __METHOD__, '1.18' );
                if ( $noRedir ) {
                        $query = 'redirect=no';
-                       if ( $extraQuery )
+                       if ( $extraQuery ) {
                                $query .= "&$extraQuery";
+                       }
                } else {
                        $query = $extraQuery;
                }
         * @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.21, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --combined includes/AutoLoader.php
@@@ -246,6 -246,7 +246,7 @@@ $wgAutoloadLocalClasses = array
        'StubUserLang' => 'includes/StubObject.php',
        'TablePager' => 'includes/Pager.php',
        'MWTimestamp' => 'includes/Timestamp.php',
+       'TimestampException' => 'includes/Timestamp.php',
        'Title' => 'includes/Title.php',
        'TitleArray' => 'includes/TitleArray.php',
        'TitleArrayFromResult' => 'includes/TitleArray.php',
        'UnlistedSpecialPage' => 'includes/SpecialPage.php',
        'UploadSourceAdapter' => 'includes/Import.php',
        'UppercaseCollation' => 'includes/Collation.php',
 +      'Uri' => 'includes/Uri.php',
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
        'UserArrayFromResult' => 'includes/UserArray.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
        'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
  
 +      # content handler
 +      'Content' => 'includes/content/Content.php',
 +      'AbstractContent' => 'includes/content/AbstractContent.php',
 +      'ContentHandler' => 'includes/content/ContentHandler.php',
 +      'CssContent' => 'includes/content/CssContent.php',
 +      'TextContentHandler' => 'includes/content/ContentHandler.php',
 +      'CssContentHandler' => 'includes/content/ContentHandler.php',
 +      'JavaScriptContent' => 'includes/content/JavaScriptContent.php',
 +      'JavaScriptContentHandler' => 'includes/content/ContentHandler.php',
 +      'MessageContent' => 'includes/content/MessageContent.php',
 +      'TextContent' => 'includes/content/TextContent.php',
 +      'WikitextContent' => 'includes/content/WikitextContent.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',
        'Blob' => 'includes/db/DatabaseUtility.php',
        'ChronologyProtector' => 'includes/db/LBFactory.php',
        'CloneDatabase' => 'includes/db/CloneDatabase.php',
-       'Database' => 'includes/db/DatabaseMysql.php',
        'DatabaseBase' => 'includes/db/Database.php',
        'DatabaseIbm_db2' => 'includes/db/DatabaseIbm_db2.php',
        'DatabaseMssql' => 'includes/db/DatabaseMssql.php',
        'FakeConverter' => 'languages/Language.php',
        'Language' => 'languages/Language.php',
        'LanguageConverter' => 'languages/LanguageConverter.php',
+       'CLDRPluralRuleConverter' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+       'CLDRPluralRuleConverter_Expression' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+       'CLDRPluralRuleConverter_Fragment' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+       'CLDRPluralRuleConverter_Operator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
        'CLDRPluralRuleEvaluator' => 'languages/utils/CLDRPluralRuleEvaluator.php',
+       'CLDRPluralRuleEvaluator_Range' => 'languages/utils/CLDRPluralRuleEvaluator.php',
        'CLDRPluralRuleError' => 'languages/utils/CLDRPluralRuleEvaluator.php',
  
        # maintenance
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
  
 +      # tests/phpunit
 +      'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
 +      'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
 +      'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
 +      'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
 +      'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +      'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
        # tests/phpunit/includes
        'GenericArrayObjectTest' => 'tests/phpunit/includes/libs/GenericArrayObjectTest.php',
  
@@@ -59,7 -59,7 +59,7 @@@ if( !defined( 'MEDIAWIKI' ) ) 
  $wgConf = new SiteConfiguration;
  
  /** MediaWiki version number */
- $wgVersion = '1.20alpha';
+ $wgVersion = '1.21alpha';
  
  /** Name of the site. It must be changed in LocalSettings.php */
  $wgSitename = 'MediaWiki';
@@@ -530,6 -530,12 +530,12 @@@ $wgAllowAsyncCopyUploads = false
   */
  $wgCopyUploadsDomains = array();
  
+ /**
+  * Proxy to use for copy upload requests.
+  * @since 1.20
+  */
+ $wgCopyUploadProxy = false;
  /**
   * Max size for uploads, in bytes. If not set to an array, applies to all
   * uploads. If set to an array, per upload type maximums can be set, using the
@@@ -732,16 -738,6 +738,16 @@@ $wgMediaHandlers = array
        'image/x-djvu'   => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model id to a class name
 + */
 +$wgContentHandlers = array(
 +      CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +      CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +      CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -2724,11 -2720,11 +2730,11 @@@ $wgFooterIcons = array
  $wgUseCombinedLoginLink = false;
  
  /**
-  * Search form behavior for Vector skin only.
+  * Search form look for Vector skin only.
   *  - true = use an icon search button
   *  - false = use Go & Search buttons
   */
- $wgVectorUseSimpleSearch = false;
+ $wgVectorUseSimpleSearch = true;
  
  /**
   * Watch and unwatch as an icon rather than a link for Vector skin only.
@@@ -3234,8 -3230,13 +3240,13 @@@ $wgMaxTocLevel = 999
  $wgMaxPPNodeCount = 1000000;
  
  /**
-  * A complexity limit on template expansion: the maximum number of nodes 
-  * generated by Preprocessor::preprocessToObj()
+  * A complexity limit on template expansion: the maximum number of elements
+  * generated by Preprocessor::preprocessToObj(). This allows you to limit the
+  * amount of memory used by the Preprocessor_DOM node cache: testing indicates
+  * that each element uses about 160 bytes of memory on a 64-bit processor, so
+  * this default corresponds to about 155 MB.
+  *
+  * When the limit is exceeded, an exception is thrown.
   */
  $wgMaxGeneratedPPNodeCount = 1000000;
  
@@@ -3593,13 -3594,6 +3604,6 @@@ $wgDefaultUserOptions = array
        'wllimit'                 => 250,
  );
  
- /**
-  * Whether or not to allow and use real name fields.
-  * @deprecated since 1.16, use $wgHiddenPrefs[] = 'realname' below to disable real
-  * names
-  */
- $wgAllowRealName = true;
  /** An array of preferences to not show for the user */
  $wgHiddenPrefs = array();
  
@@@ -4275,8 -4269,18 +4279,18 @@@ $wgProxyScriptPath = "$IP/maintenance/p
  $wgProxyMemcExpiry = 86400;
  /** This should always be customised in LocalSettings.php */
  $wgSecretKey = false;
- /** big list of banned IP addresses, in the keys not the values */
+ /**
+  * Big list of banned IP addresses.
+  *
+  * This can have the following formats:
+  * - An array of addresses, either in the values
+  *   or the keys (for backward compatibility)
+  * - A string, in that case this is the path to a file
+  *   containing the list of IP addresses, one per line
+  */
  $wgProxyList = array();
  /** deprecated */
  $wgProxyKey = false;
  
@@@ -4693,17 -4697,11 +4707,11 @@@ $wgCountTotalSearchHits = false
   */
  $wgOpenSearchTemplate = false;
  
- /**
-  * Enable suggestions while typing in search boxes
-  * (results are passed around in OpenSearch format)
-  * Requires $wgEnableOpenSearchSuggest = true;
-  */
- $wgEnableMWSuggest = false;
  /**
   * Enable OpenSearch suggestions requested by MediaWiki. Set this to
-  * false if you've disabled MWSuggest or another suggestion script and
-  * want reduce load caused by cached scripts pulling suggestions.
+  * false if you've disabled scripts that use api?action=opensearch and
+  * want reduce load caused by cached scripts still pulling suggestions.
+  * It will let the API fallback by responding with an empty array.
   */
  $wgEnableOpenSearchSuggest = true;
  
   */
  $wgSearchSuggestCacheExpiry = 1200;
  
- /**
-  *  Template for internal MediaWiki suggestion engine, defaults to API action=opensearch
-  *
-  *  Placeholders: {searchTerms}, {namespaces}, {dbname}
-  *
-  */
- $wgMWSuggestTemplate = false;
  /**
   * If you've disabled search semi-permanently, this also disables updates to the
   * table. If you ever re-enable, be sure to rebuild the search table.
@@@ -6233,31 -6223,6 +6233,31 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
 +/**
 + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
 + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
 + * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
 + */
 +$wgNamespaceContentModels = array();
 +
 +/**
 + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
 + *
 + * * 'ignore': return null
 + * * 'fail': throw an MWException
 + * * 'serializeContent': serializeContent to default format
 + */
 +$wgContentHandlerTextFallback = 'ignore';
 +
 +/**
 + * Compatibility switch for running ContentHandler code withoput a schema update.
 + * Set to false to disable use of the database fields introduced by the ContentHandler facility.
 + *
 + * @deprecated this is only here to allow code deployment without a database schema update on large sites.
 + *             get rid of it in the next version.
 + */
 +$wgContentHandlerUseDB = true;
 +
  /**
   * Whether the user must enter their password to change their e-mail address
   *
diff --combined includes/EditPage.php
@@@ -155,11 -155,6 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
  
        public $suppressIntro = false;
  
 +      /**
 +       * Set to true to allow editing of non-text content types.
 +       *
 +       * @var bool
 +       */
 +      public $allowNonTextContent = false;
 +
        /**
         * @param $article Article
         */
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModel();
 +
 +              $handler = ContentHandler::getForModelID( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                                wfProfileOut( __METHOD__ );
                                return;
                        }
-                       if ( !$this->mTitle->getArticleID() )
+                       if ( !$this->mTitle->getArticleID() ) {
                                wfRunHooks( 'EditFormPreloadText', array( &$this->textbox1, &$this->mTitle ) );
-                       else
+                       }
+                       else {
                                wfRunHooks( 'EditFormInitialText', array( $this ) );
+                       }
                }
  
                $this->showEditForm();
                        return;
                }
  
 -              $content = $this->getContent();
 +              $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
 -              if ( $this->firsttime && $content === '' ) {
 +              if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
 -                      $content = $this->textbox1;
 +                      $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
 +                      $text = $this->toEditText( $content );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
 -              $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
 +              $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                                // modified by subclasses
                                wfProfileIn( get_class( $this ) . "::importContentFormData" );
                                $textbox1 = $this->importContentFormData( $request );
-                               if ( isset( $textbox1 ) )
+                               if ( isset( $textbox1 ) ) {
                                        $this->textbox1 = $textbox1;
+                               }
                                wfProfileOut( get_class( $this ) . "::importContentFormData" );
                        }
  
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
 -              $this->textbox1 = $this->getContent( false );
 +
 +              $content = $this->getContentObject( false ); #TODO: track content object?!
 +              $this->textbox1 = $this->toEditText( $content );
 +
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.21
 +       * @todo: deprecated, replace usage everywhere
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) {
 +              wfDeprecated( __METHOD__, '1.21' );
 +
 +              if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
 +                      $def_content = $this->toEditContent( $def_text );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              // Note: EditPage should only be used with text based content anyway.
 +              return $this->toEditText( $content );
 +      }
 +
 +      private function getContentObject( $def_content = null ) {
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = $this->toEditContent( $msg );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
 -                                      # Otherwise, $text will be left as-is.
 +                                      # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
 -                                              $text = $this->mArticle->getUndoText( $undorev, $oldrev );
 -                                              if ( $text === false ) {
 +                                              $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
 +
 +                                              if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.21
         * @return string
         */
 -      private function getCurrentText() {
 -              $text = $this->mArticle->getRawText();
 -              if ( $text === false ) {
 -                      return '';
 +      private function getCurrentContent() {
 +              $rev = $this->mArticle->getRevision();
 +              $content = $rev ? $rev->getContent( Revision::RAW ) : null;
 +
 +              if ( $content  === false || $content === null ) {
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                } else {
 -                      return $text;
 +                      # nasty side-effect, but needed for consistency
 +                      $this->content_model = $rev->getContentModel();
 +                      $this->content_format = $rev->getContentFormat();
 +
 +                      return $content;
                }
        }
  
 +
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
 +       * @deprecated since 1.21
         */
        public function setPreloadedText( $text ) {
 -              $this->mPreloadText = $text;
 +              wfDeprecated( __METHOD__, "1.21" );
 +
 +              $content = $this->toEditContent( $text );
 +
 +              $this->setPreloadedContent( $content );
 +      }
 +
 +      /**
 +       * Use this method before edit() to preload some content into the edit box
 +       *
 +       * @param $content Content
 +       *
 +       * @since 1.21
 +       */
 +      public function setPreloadedContent( Content $content ) {
 +              $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
 +       *
         * @return String
 +       *
 +       * @deprecated since 1.21, use getPreloadedContent() instead
         */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.21" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $this->toEditText( $content );
 +
 +              return $text;
 +      }
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +      /**
 +       * Get the contents to be preloaded into the box, either set by
 +       * an earlier setPreloadText() or by loading the given page.
 +       *
 +       * @param $preload String: representing the title to preload from.
 +       *
 +       * @return Content
 +       *
 +       * @since 1.21
 +       */
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
 +
 +              if ( !empty( $this->mPreloadContent ) ) {
 +                      return $this->mPreloadContent;
                }
  
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
                if ( $preload === '' ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
-               if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+               if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                if ( $page->isRedirect() ) {
                        $title = $page->getRedirectTarget();
                        # Same as before
-                       if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
+                       if ( $title === null || !$title->exists() || !$title->userCan( 'read', $wgUser ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 +              $content = $page->getContent( Revision::RAW );
 +
 +              return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                        case self::AS_HOOK_ERROR:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              return true;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
                        return $status;
                }
  
 +              try {
 +                      # Construct Content object
 +                      $textbox_content = $this->toEditContent( $this->textbox1 );
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
 +                      wfProfileOut( __METHOD__ );
 +                      return $status;
 +              }
 +
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      $textbox_content->isRedirect() &&
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
  
                if ( $new ) {
                        // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create' ) ) {
+                       if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
                                $status->fatal( 'nocreatetext' );
                                $status->value = self::AS_NO_CREATE_PERMISSION;
                                wfDebug( __METHOD__ . ": no create permission\n" );
                                return $status;
                        }
  
 -                      $text = $this->textbox1;
 +                      $content = $textbox_content;
 +
                        $result['sectionanchor'] = '';
                        if ( $this->section == 'new' ) {
                                if ( $this->sectiontitle !== '' ) {
                                        // Insert the section title above the content.
 -                                      $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->sectiontitle )
 -                                              ->inContentLanguage()->text() . "\n\n" . $text;
 +                                      $content = $content->addSectionHeader( $this->sectiontitle );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
                                        // passed.
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
-                                                       ->inContentLanguage()->text() ;
+                                               $this->summary = wfMessage( 'newsectionsummary' )
 -                                                      ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
++                                                      ->rawParams( $cleanSectionTitle )->inContentLanguage()->text() ;
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        // Insert the section title above the content.
 -                                      $text = wfMessage( 'newsectionheaderdefaultlevel' )->rawParams( $this->summary )
 -                                              ->inContentLanguage()->text() . "\n\n" . $text;
 +                                      $content = $content->addSectionHeader( $this->summary );
  
                                        // Jump to the new section
                                        $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
  
                                        // Create a link to the new section from the edit summary.
                                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary', $cleanSummary )
-                                               ->inContentLanguage()->text();
+                                       $this->summary = wfMessage( 'newsectionsummary' )
+                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
                                }
                        }
  
                        $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +              } else { # not $new
  
                        # Article exists. Check for edit conflict.
 +
 +                      $this->mArticle->clear(); # Force reload of dates, etc.
                        $timestamp = $this->mArticle->getTimestamp();
 +
                        wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
  
                        if ( $timestamp != $this->edittime ) {
                                $sectionTitle = $this->summary;
                        }
  
 +                      $content = null;
 +
                        if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 +                              wfDebug( __METHOD__ . ": conflict! getting section '{$this->section}' for time '{$this->edittime}'"
 +                                              . " (article time '{$timestamp}')\n" );
 +
 +                              $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content,
 +                                                                                                                                      $sectionTitle, $this->edittime );
                        } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 +                              wfDebug( __METHOD__ . ": getting section '{$this->section}'\n" );
 +                              $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
                        }
 -                      if ( is_null( $text ) ) {
 +
 +                      if ( is_null( $content ) ) {
                                wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
                                $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 +                              $content = $textbox_content; // do not try to merge here!
                        } elseif ( $this->isConflict ) {
                                # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 +                              if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
                                        // Successful merge! Maybe we should tell the user the good news?
                                        $this->isConflict = false;
 +                                      $content = $textbox_content;
                                        wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
                                } else {
                                        $this->section = '';
 -                                      $this->textbox1 = $text;
 +                                      #$this->textbox1 = $text; #redundant, nothing to do here?
                                        wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
                                }
                        }
                        }
  
                        // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 +                      $hook_args = array( $this, $content, &$this->hookError, $this->summary );
 +
 +                      if ( !ContentHandler::runLegacyHooks( 'EditFilterMerged', $hook_args )
 +                              || !wfRunHooks( 'EditFilterMergedContent', $hook_args ) ) {
                                # Error messages etc. could be handled within the hook...
                                $status->fatal( 'hookaborted' );
                                $status->value = self::AS_HOOK_ERROR;
  
                        # Handle the user preference to force summaries here, but not for null edits
                        if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 +                              && !$content->equals( $this->getOriginalContent() )
 +                              && !$content->isRedirect() ) # check if it's not a redirect
                        {
                                if ( md5( $this->summary ) == $this->autoSumm ) {
                                        $this->missingSummary = true;
                                        // passed.
                                        if ( $this->summary === '' ) {
                                                $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
-                                               $this->summary = wfMessage( 'newsectionsummary', $cleanSectionTitle )
-                                                       ->inContentLanguage()->text();
+                                               $this->summary = wfMessage( 'newsectionsummary' )
+                                                       ->rawParams( $cleanSectionTitle )->inContentLanguage()->text();
                                        }
                                } elseif ( $this->summary !== '' ) {
                                        $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
                                        # This is a new section, so create a link to the new section
                                        # in the revision summary.
                                        $cleanSummary = $wgParser->stripSectionName( $this->summary );
-                                       $this->summary = wfMessage( 'newsectionsummary', $cleanSummary )
-                                               ->inContentLanguage()->text();
+                                       $this->summary = wfMessage( 'newsectionsummary' )
+                                               ->rawParams( $cleanSummary )->inContentLanguage()->text();
                                }
                        } elseif ( $this->section != '' ) {
                                # Try to get a section anchor from the section source, redirect to edited section if header found
                        // merged the section into full text. Clear the section field
                        // so that later submission of conflict forms won't try to
                        // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 +                      $this->textbox1 = $this->toEditText( $content );
                        $this->section = '';
  
                        $status->value = self::AS_SUCCESS_UPDATE;
                }
  
                // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 +                      $this->kblength = (int)( strlen( $this->toEditText( $content ) ) / 1024 );
                if ( $this->kblength > $wgMaxArticleSize ) {
                        $this->tooBig = true;
                        $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
                        ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
                        ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                      $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags,
 +                                                                                                                      false, null, $this->content_format );
  
                if ( $doEditStatus->isOK() ) {
-                               $result['redirect'] = $content->isRedirect();
-                       $this->commitWatch();
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
++                      $result['redirect'] = $content->isRedirect();
+                       $this->updateWatchlist();
                        wfProfileOut( __METHOD__ );
                        return $status;
                } else {
        }
  
        /**
-        * Commit the change of watch status
+        * Register the change of watch status
         */
-       protected function commitWatch() {
+       protected function updateWatchlist() {
                global $wgUser;
                if ( $wgUser->isLoggedIn() && $this->watchthis != $wgUser->isWatched( $this->mTitle ) ) {
+                       $fname = __METHOD__;
+                       $title = $this->mTitle;
+                       $watch = $this->watchthis;
+                       // Do this in its own transaction to reduce contention...
                        $dbw = wfGetDB( DB_MASTER );
-                       $dbw->begin( __METHOD__ );
-                       if ( $this->watchthis ) {
-                               WatchAction::doWatch( $this->mTitle, $wgUser );
-                       } else {
-                               WatchAction::doUnwatch( $this->mTitle, $wgUser );
-                       }
-                       $dbw->commit( __METHOD__ );
+                       $dbw->onTransactionIdle( function() use ( $dbw, $title, $watch, $wgUser, $fname ) {
+                               $dbw->begin( $fname );
+                               if ( $watch ) {
+                                       WatchAction::doWatch( $title, $wgUser );
+                               } else {
+                                       WatchAction::doUnwatch( $title, $wgUser );
+                               }
+                               $dbw->commit( $fname );
+                       } );
                }
        }
  
         * @param $editText string
         *
         * @return bool
 +       * @deprecated since 1.21, use mergeChangesIntoContent() instead
         */
 -      function mergeChangesInto( &$editText ) {
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.21" );
 +
 +              $editContent = $this->toEditContent( $editText );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $this->toEditText( $editContent );
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.21
 +       */
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $baseText = $baseRevision->getText();
 +              $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
 +
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                $wgOut->addModules( 'mediawiki.action.edit' );
  
                if ( $wgUser->getOption( 'uselivepreview', false ) ) {
-                       $wgOut->addModules( 'mediawiki.legacy.preview' );
+                       $wgOut->addModules( 'mediawiki.action.edit.preview' );
                }
                // Bug #19334: textarea jumps when editing articles in IE8
                $wgOut->addStyle( 'common/IE80Fixes.css', 'screen', 'IE 8' );
                }
        }
  
 +      /**
 +       * Gets an editable textual representation of the given Content object.
 +       * The textual representation can be turned by into a Content object by the
 +       * toEditContent() method.
 +       *
 +       * If the given Content object is not of a type that can be edited using the text base EditPage,
 +       * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
 +       * content.
 +       *
 +       * @param Content $content
 +       * @return String the editable text form of the content.
 +       *
 +       * @throws MWException if $content is not an instance of TextContent and $this->allowNonTextContent is not true.
 +       */
 +      protected function toEditText( Content $content ) {
 +              if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
 +                      throw new MWException( "This content model can not be edited as text: "
 +                                                              . ContentHandler::getLocalizedName( $content->getModel() ) );
 +              }
 +
 +              return $content->serialize( $this->content_format );
 +      }
 +
 +      /**
 +       * Turns the given text into a Content object by unserializing it.
 +       *
 +       * If the resulting Content object is not of a type that can be edited using the text base EditPage,
 +       * an exception will be raised. Set $this->allowNonTextContent to true to allow editing of non-textual
 +       * content.
 +       *
 +       * @param String $text Text to unserialize
 +       * @return Content the content object created from $text
 +       *
 +       * @throws MWException if unserializing the text results in a Content object that is not an instance of TextContent
 +       *          and $this->allowNonTextContent is not true.
 +       */
 +      protected function toEditContent( $text ) {
 +              $content = ContentHandler::makeContent( $text, $this->getTitle(),
 +                      $this->content_model, $this->content_format );
 +
 +              if ( !$this->allowNonTextContent && !( $content instanceof TextContent ) ) {
 +                      throw new MWException( "This content model can not be edited as text: "
 +                              . ContentHandler::getLocalizedName( $content->getModel() ) );
 +              }
 +
 +              return $content;
 +      }
 +
        /**
         * Send the edit form and related headers to $wgOut
         * @param $formCallback Callback that takes an OutputPage parameter; will be called
                        }
                }
  
 +              //@todo: add EditForm plugin interface and use it here!
 +              //       search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $this->toEditText( $content );
  
                        $this->showTextbox1();
                } else {
                        Linker::formatHiddenCategories( $this->mArticle->getHiddenCategories() ) ) );
  
                if ( $this->isConflict ) {
 -                      $this->showConflict();
 +                      try {
 +                              $this->showConflict();
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              // this can't really happen, but be nice if it does.
 +                              $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                              $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
 +                      }
                }
  
                $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
  
                        if ( $this->section != '' && $this->section != 'new' ) {
                                if ( !$this->summary && !$this->preview && !$this->diff ) {
 -                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 );
 +                                      $sectionTitle = self::extractSectionTitle( $this->textbox1 ); //FIXME: use Content object
                                        if ( $sectionTitle !== false ) {
                                                $this->summary = "/* $sectionTitle */ ";
                                        }
                                if ( $revision ) {
                                        // Let sysop know that this will make private content public if saved
  
-                                       if ( !$revision->userCan( Revision::DELETED_TEXT ) ) {
+                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
                                                $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-permission' );
                                        } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
                                                $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", 'rev-deleted-text-view' );
                                        $wgOut->wrapWikiMsg( "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>", array( 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ) );
                                }
                                if ( $this->formtype !== 'preview' ) {
-                                       if ( $this->isCssSubpage )
+                                       if ( $this->isCssSubpage ) {
                                                $wgOut->wrapWikiMsg( "<div id='mw-usercssyoucanpreview'>\n$1\n</div>", array( 'usercssyoucanpreview' ) );
-                                       if ( $this->isJsSubpage )
+                                       }
+                                       if ( $this->isJsSubpage ) {
                                                $wgOut->wrapWikiMsg( "<div id='mw-userjsyoucanpreview'>\n$1\n</div>", array( 'userjsyoucanpreview' ) );
+                                       }
                                }
                        }
                }
         * @return String
         */
        protected function getSummaryPreview( $isSubjectPreview, $summary = "" ) {
-               if ( !$summary || ( !$this->preview && !$this->diff ) )
+               if ( !$summary || ( !$this->preview && !$this->diff ) ) {
                        return "";
+               }
  
                global $wgParser;
  
-               if ( $isSubjectPreview )
+               if ( $isSubjectPreview ) {
                        $summary = wfMessage( 'newsectionsummary', $wgParser->stripSectionName( $summary ) )
                                ->inContentLanguage()->text();
+               }
  
                $message = $isSubjectPreview ? 'subject-preview' : 'summary-preview';
  
  
  HTML
                );
-               if ( !$this->checkUnicodeCompliantBrowser() )
+               if ( !$this->checkUnicodeCompliantBrowser() ) {
                        $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
+               }
        }
  
        protected function showFormAfterText() {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
        protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
                global $wgOut;
                $classes = array();
-               if ( $isOnTop )
+               if ( $isOnTop ) {
                        $classes[] = 'ontop';
+               }
  
                $attribs = array( 'id' => 'wikiPreview', 'class' => implode( ' ', $classes ) );
  
-               if ( $this->formtype != 'preview' )
+               if ( $this->formtype != 'preview' ) {
                        $attribs['style'] = 'display: none;';
+               }
  
                $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
  
                $wgOut->addHTML( '</div>' );
  
                if ( $this->formtype == 'diff' ) {
 -                      $this->showDiff();
 +                      try {
 +                              $this->showDiff();
 +                      } catch ( MWContentSerializationException $ex ) {
 +                              $msg = wfMessage( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                              $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>');
 +                      }
                }
        }
  
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = $this->toEditContent( $oldtext );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 -              wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
 +              $textboxContent = $this->toEditContent( $this->textbox1 );
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                      $this->section, $textboxContent,
 +                                                                                      $this->summary, $this->edittime );
 +
 +              ContentHandler::runLegacyHooks( 'EditPageGetDiffText', array( $this, &$newContent ) );
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
  
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMessage( $oldtitlemsg )->parse();
                        $newtitle = wfMessage( 'yourtext' )->parse();
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 -                      $de->showDiff(
 +                      $content1 = $this->toEditContent( $this->textbox1 );
 +                      $content2 = $this->toEditContent( $this->textbox2 );
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
 +                      $de->showDiff( 
                                wfMessage( 'yourtext' )->parse(),
                                wfMessage( 'storedversion' )->text()
                        );
                );
                // Quick paranoid permission checks...
                if ( is_object( $data ) ) {
-                       if ( $data->log_deleted & LogPage::DELETED_USER )
+                       if ( $data->log_deleted & LogPage::DELETED_USER ) {
                                $data->user_name = wfMessage( 'rev-deleted-user' )->escaped();
-                       if ( $data->log_deleted & LogPage::DELETED_COMMENT )
+                       }
+                       if ( $data->log_deleted & LogPage::DELETED_COMMENT ) {
                                $data->log_comment = wfMessage( 'rev-deleted-comment' )->escaped();
+                       }
                }
                return $data;
        }
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMessage( 'token_suffix_mismatch' )->plain();
 -                      } else {
 -                              $note = wfMessage( 'session_fail_preview' )->plain();
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMessage( 'edit_form_incomplete' )->plain();
 -              } else {
 -                      $note = wfMessage( 'previewnote' )->plain() .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
 +              try {
 +                      $content = $this->toEditContent( $this->textbox1 );
  
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMessage( 'token_suffix_mismatch' )->plain() ;
  
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 -
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMessage( "{$level}csspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMessage( "{$level}jspreview" )->text() . "\n</div>";
 -                                      $class .= " mw-js";
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMessage( 'session_fail_preview' )->plain() ;
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMessage( 'edit_form_incomplete' )->plain() ;
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMessage( 'previewnote' )->plain() .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMessage( 'continue-editing' )->text() . ']]';
 +                      }
 +
 +                      $parserOptions = $this->mArticle->makeParserOptions( $this->mArticle->getContext() );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      # don't parse non-wikitext pages, show message about preview
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMessage( 'newsectionheaderdefaultlevel', $this->summary )->inContentLanguage()->text() . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMessage( "{$level}{$format}preview" )->text()  . "</div>";
 +                              } else {
 +                                      $note = wfMessage( 'previewnote' )->text() ;
 +                              }
 +                      } else {
 +                              $note = wfMessage( 'previewnote' )->text() ;
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 -
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
 +                      $rt = $content->getRedirectChain();
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $hook_args = array( $this, &$content );
 +                              ContentHandler::runLegacyHooks( 'EditPageGetPreviewText', $hook_args );
 +                              wfRunHooks( 'EditPageGetPreviewContent', $hook_args );
 +
 +                              $parserOptions->enableLimitReport();
 +
 +                              # For CSS/JS pages, we should have called the ShowRawCssJs hook here.
 +                              # But it's now deprecated, so never mind
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $m = wfMessage('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $note .= "\n\n" . $m->parse();
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
diff --combined includes/Export.php
@@@ -63,7 -63,7 +63,7 @@@ class WikiExporter 
         * @return string
         */
        public static function schemaVersion() {
 -              return "0.7";
 +              return "0.8";
        }
  
        /**
@@@ -83,9 -83,9 +83,9 @@@
         * @param $buffer Int: one of WikiExporter::BUFFER or WikiExporter::STREAM
         * @param $text Int: one of WikiExporter::TEXT or WikiExporter::STUB
         */
-       function __construct( &$db, $history = WikiExporter::CURRENT,
+       function __construct( $db, $history = WikiExporter::CURRENT,
                        $buffer = WikiExporter::BUFFER, $text = WikiExporter::TEXT ) {
-               $this->db =& $db;
+               $this->db = $db;
                $this->history = $history;
                $this->buffer  = $buffer;
                $this->writer  = new XmlDumpWriter();
@@@ -498,7 -498,7 +498,7 @@@ class XmlDumpWriter 
                        'xmlns'              => "http://www.mediawiki.org/xml/export-$ver/",
                        'xmlns:xsi'          => "http://www.w3.org/2001/XMLSchema-instance",
                        'xsi:schemaLocation' => "http://www.mediawiki.org/xml/export-$ver/ " .
 -                                              "http://www.mediawiki.org/xml/export-$ver.xsd",
 +                                              "http://www.mediawiki.org/xml/export-$ver.xsd", #TODO: how do we get a new version up there?
                        'version'            => $ver,
                        'xml:lang'           => $wgLanguageCode ),
                        null ) .
                        $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
  
 -              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 -                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 -              } else {
 -                      $out .= "      <sha1/>\n";
 -              }
 -
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                                "" ) . "\n";
                }
  
 +              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 +                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 +              } else {
 +                      $out .= "      <sha1/>\n";
 +              }
 +
 +              if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
 +                      $content_model = strval( $row->rev_content_model );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $title = Title::makeTitle( $row->page_namespace, $row->page_title );
 +                      $content_model = ContentHandler::getDefaultModelFor( $title );
 +              }
 +
 +              $out .= "      " . Xml::element('model', null, strval( $content_model ) ) . "\n";
 +
 +              if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
 +                      $content_format = strval( $row->rev_content_format );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $content_handler = ContentHandler::getForModelID( $content_model );
 +                      $content_format = $content_handler->getDefaultFormat();
 +              }
 +
 +              $out .= "      " . Xml::element('format', null, strval( $content_format ) ) . "\n";
 +
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
  
                $out .= "    </revision>\n";
@@@ -391,7 -391,7 +391,7 @@@ function wfArrayToCgi( $array1, $array
  
        $cgi = '';
        foreach ( $array1 as $key => $value ) {
 -              if ( !is_null($value) && $value !== false ) {
 +              if ( $value !== false ) {
                        if ( $cgi != '' ) {
                                $cgi .= '&';
                        }
                        } else {
                                if ( is_object( $value ) ) {
                                        $value = $value->__toString();
 +                              } elseif( !is_null( $value ) ) {
 +                                      $cgi .= urlencode( $key ) . '=' . urlencode( $value );
 +                              } else {
 +                                      $cgi .= urlencode( $key );
                                }
 -                              $cgi .= urlencode( $key ) . '=' . urlencode( $value );
                        }
                }
        }
@@@ -443,15 -440,14 +443,15 @@@ function wfCgiToArray( $query ) 
                        continue;
                }
                if ( strpos( $bit, '=' ) === false ) {
 -                      // Pieces like &qwerty become 'qwerty' => '' (at least this is what php does)
 -                      $key = $bit;
 -                      $value = '';
 +                      // Pieces like &qwerty become 'qwerty' => null
 +                      $key = urldecode( $bit );
 +                      $value = null;
                } else {
                        list( $key, $value ) = explode( '=', $bit );
 +                      $key = urldecode( $key );
 +                      $value = urldecode( $value );
                }
 -              $key = urldecode( $key );
 -              $value = urldecode( $value );
 +
                if ( strpos( $key, '[' ) !== false ) {
                        $keys = array_reverse( explode( '[', $key ) );
                        $key = array_pop( $keys );
   * Append a query string to an existing URL, which may or may not already
   * have query string parameters already. If so, they will be combined.
   *
 + * @deprecated in 1.20. Use Uri class.
   * @param $url String
   * @param $query Mixed: string or associative array
   * @return string
   */
  function wfAppendQuery( $url, $query ) {
 -      if ( is_array( $query ) ) {
 -              $query = wfArrayToCgi( $query );
 -      }
 -      if( $query != '' ) {
 -              if( false === strpos( $url, '?' ) ) {
 -                      $url .= '?';
 -              } else {
 -                      $url .= '&';
 -              }
 -              $url .= $query;
 -      }
 -      return $url;
 +      $obj = new Uri( $url );
 +      $obj->extendQuery( $query );
 +      return $obj->toString();
  }
  
  /**
@@@ -572,13 -576,49 +572,13 @@@ function wfExpandUrl( $url, $defaultPro
   * @todo Need to integrate this into wfExpandUrl (bug 32168)
   *
   * @since 1.19
 + * @deprecated
   * @param $urlParts Array URL parts, as output from wfParseUrl
   * @return string URL assembled from its component parts
   */
  function wfAssembleUrl( $urlParts ) {
 -      $result = '';
 -
 -      if ( isset( $urlParts['delimiter'] ) ) {
 -              if ( isset( $urlParts['scheme'] ) ) {
 -                      $result .= $urlParts['scheme'];
 -              }
 -
 -              $result .= $urlParts['delimiter'];
 -      }
 -
 -      if ( isset( $urlParts['host'] ) ) {
 -              if ( isset( $urlParts['user'] ) ) {
 -                      $result .= $urlParts['user'];
 -                      if ( isset( $urlParts['pass'] ) ) {
 -                              $result .= ':' . $urlParts['pass'];
 -                      }
 -                      $result .= '@';
 -              }
 -
 -              $result .= $urlParts['host'];
 -
 -              if ( isset( $urlParts['port'] ) ) {
 -                      $result .= ':' . $urlParts['port'];
 -              }
 -      }
 -
 -      if ( isset( $urlParts['path'] ) ) {
 -              $result .= $urlParts['path'];
 -      }
 -
 -      if ( isset( $urlParts['query'] ) ) {
 -              $result .= '?' . $urlParts['query'];
 -      }
 -
 -      if ( isset( $urlParts['fragment'] ) ) {
 -              $result .= '#' . $urlParts['fragment'];
 -      }
 -
 -      return $result;
 +      $obj = new Uri( $urlParts );
 +      return $obj->toString();
  }
  
  /**
@@@ -725,7 -765,6 +725,7 @@@ function wfUrlProtocolsWithoutProtRel(
   * 2) Handles protocols that don't use :// (e.g., mailto: and news: , as well as protocol-relative URLs) correctly
   * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2))
   *
 + * @deprecated
   * @param $url String: a URL to parse
   * @return Array: bits of the URL in an associative array, per PHP docs
   */
@@@ -1025,7 -1064,6 +1025,7 @@@ function wfLogDBError( $text ) 
                } else {
                        $d = date_create( "now", $logDBErrorTimeZoneObject );
                }
 +              $date = $d->format( 'D M j G:i:s T Y' );
  
                $date = $d->format( 'D M j G:i:s T Y' );
  
@@@ -2364,7 -2402,6 +2364,7 @@@ define( 'TS_ISO_8601_BASIC', 9 )
  /**
   * Get a timestamp string in one of various formats
   *
 + * @deprecated
   * @param $outputtype Mixed: A timestamp in one of the supported formats, the
   *                    function will autodetect which format is supplied and act
   *                    accordingly.
@@@ -3514,16 -3551,6 +3514,6 @@@ function wfBoolToStr( $value ) 
        return $value ? 'true' : 'false';
  }
  
- /**
-  * Load an extension messages file
-  *
-  * @deprecated since 1.16, warnings in 1.18, remove in 1.20
-  * @codeCoverageIgnore
-  */
- function wfLoadExtensionMessages() {
-       wfDeprecated( __FUNCTION__, '1.16' );
- }
  /**
   * Get a platform-independent path to the null file, e.g. /dev/null
   *
diff --combined includes/Message.php
@@@ -202,11 -202,6 +202,11 @@@ class Message 
         */
        protected $title = null;
  
 +      /**
 +       * Content object representing the message
 +       */
 +      protected $content = null;
 +
        /**
         * @var string
         */
                return $this;
        }
  
 +      /**
 +       * Returns the message as a Content object.
 +       * @return Content
 +       */
 +      public function content() {
 +              if ( !$this->content ) {
 +                      $this->content = new MessageContent( $this->key );
 +              }
 +
 +              return $this->content;
 +      }
 +
        /**
         * Returns the message parsed from wikitext to HTML.
         * @since 1.17
        /**
         * Check whether a message does not exist, is an empty string, or is "-"
         * @since 1.18
-        * @return Bool: true if is is and false if not
+        * @return Bool: true if it is and false if not
         */
        public function isDisabled() {
                $message = $this->fetchMessage();
diff --combined includes/OutputPage.php
@@@ -255,7 -255,7 +255,7 @@@ class OutputPage extends ContextSource 
        function __construct( IContextSource $context = null ) {
                if ( $context === null ) {
                        # Extensions should use `new RequestContext` instead of `new OutputPage` now.
-                       wfDeprecated( __METHOD__ );
+                       wfDeprecated( __METHOD__, '1.18' );
                } else {
                        $this->setContext( $context );
                }
                $this->getContext()->setTitle( $t );
        }
  
        /**
         * Replace the subtile with $str
         *
-        * @param $str String|Message: new value of the subtitle
+        * @param $str String|Message: new value of the subtitle. String should be safe HTML.
         */
        public function setSubtitle( $str ) {
                $this->clearSubtitle();
        /**
         * Add $str to the subtitle
         *
-        * @param $str String|Message to add to the subtitle
+        * @param $str String|Message to add to the subtitle. String should be safe HTML.
         */
        public function addSubtitle( $str ) {
                if ( $str instanceof Message ) {
         * @deprecated since 1.18 Use HttpStatus::getMessage() instead.
         */
        public static function getStatusMessage( $code ) {
-               wfDeprecated( __METHOD__ );
+               wfDeprecated( __METHOD__, '1.18' );
                return HttpStatus::getMessage( $code );
        }
  
                wfRunHooks( 'AfterFinalPageOutput', array( $this ) );
  
                $this->sendCacheControl();
 +
 +              wfRunHooks( 'AfterFinalPageOutput', array( &$this ) );
 +
                ob_end_flush();
 +
                wfProfileOut( __METHOD__ );
        }
  
@@@ -2347,11 -2342,20 +2346,20 @@@ $template
         * @param $title Title to link
         * @param $query Array query string parameters
         * @param $text String text of the link (input is not escaped)
+        * @param $options Options array to pass to Linker
         */
-       public function addReturnTo( $title, $query = array(), $text = null ) {
-               $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullURL() ) );
+       public function addReturnTo( $title, $query = array(), $text = null, $options = array() ) {
+               if( in_array( 'http', $options ) ) {
+                       $proto = PROTO_HTTP;
+               } elseif( in_array( 'https', $options ) ) {
+                       $proto = PROTO_HTTPS;
+               } else {
+                       $proto = PROTO_RELATIVE;
+               }
+               $this->addLink( array( 'rel' => 'next', 'href' => $title->getFullURL( '', false, $proto ) ) );
                $link = $this->msg( 'returnto' )->rawParams(
-                       Linker::link( $title, $text, array(), $query ) )->escaped();
+                       Linker::link( $title, $text, array(), $query, $options ) )->escaped();
                $this->addHTML( "<p id=\"mw-returnto\">{$link}</p>\n" );
        }
  
         */
        private function addDefaultModules() {
                global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil, $wgUseAjax,
-                       $wgAjaxWatch, $wgEnableMWSuggest;
+                       $wgAjaxWatch;
  
                // Add base resources
                $this->addModules( array(
                                $this->addModules( 'mediawiki.page.watch.ajax' );
                        }
  
-                       if ( $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
-                               $this->addModules( 'mediawiki.legacy.mwsuggest' );
+                       if ( !$this->getUser()->getOption( 'disablesuggest', false ) ) {
+                               $this->addModules( 'mediawiki.searchSuggest' );
                        }
                }
  
         * @return array
         */
        public function getJSVars() {
-               global $wgUseAjax, $wgEnableMWSuggest, $wgContLang;
+               global $wgUseAjax, $wgContLang;
  
                $latestRevID = 0;
                $pageID = 0;
                foreach ( $title->getRestrictionTypes() as $type ) {
                        $vars['wgRestriction' . ucfirst( $type )] = $title->getRestrictions( $type );
                }
-               if ( $wgUseAjax && $wgEnableMWSuggest && !$this->getUser()->getOption( 'disablesuggest', false ) ) {
-                       $vars['wgSearchNamespaces'] = SearchEngine::userNamespaces( $this->getUser() );
-               }
                if ( $title->isMainPage() ) {
                        $vars['wgIsMainPage'] = true;
                }
@@@ -95,7 -95,7 +95,7 @@@ abstract class SqlDataUpdate extends Da
         * Abort the database transaction started via beginTransaction (if any).
         */
        public function abortTransaction() {
 -              if ( $this->mHasTransaction ) {
 +              if ( $this->mHasTransaction ) { //XXX: actually... maybe always?
                        $this->mDb->rollback( get_class( $this ) . '::abortTransaction' );
                        $this->mHasTransaction = false;
                }
         * @param $namespace Integer
         * @param $dbkeys Array
         */
-       protected function invalidatePages( $namespace, Array $dbkeys ) {
+       protected function invalidatePages( $namespace, array $dbkeys ) {
                if ( !count( $dbkeys ) ) {
                        return;
                }
diff --combined includes/Title.php
@@@ -65,7 -65,6 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
 +      var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
                }
        }
  
 +      /**
 +       * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
 +       * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
 +       *
 +       * @return array
 +       */
 +      protected static function getSelectFields() {
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
 +                      'page_namespace', 'page_title', 'page_id',
 +                      'page_len', 'page_is_redirect', 'page_latest',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
 +      }
 +
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = strval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
 +       * @deprecated since 1.21, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
 -              return self::newFromRedirectInternal( $text );
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Title
 +       * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
 -              $titles = self::newFromRedirectArray( $text );
 -              return $titles ? array_pop( $titles ) : null;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getUltimateRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
 +       * @deprecated since 1.21, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
 -              global $wgMaxRedirects;
 -              $title = self::newFromRedirectInternal( $text );
 -              if ( is_null( $title ) ) {
 -                      return null;
 -              }
 -              // recursive check to follow double redirects
 -              $recurse = $wgMaxRedirects;
 -              $titles = array( $title );
 -              while ( --$recurse > 0 ) {
 -                      if ( $title->isRedirect() ) {
 -                              $page = WikiPage::factory( $title );
 -                              $newtitle = $page->getRedirectTarget();
 -                      } else {
 -                              break;
 -                      }
 -                      // Redirects to some special pages are not permitted
 -                      if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
 -                              // the new title passes the checks, so make that our current title so that further recursion can be checked
 -                              $title = $newtitle;
 -                              $titles[] = $newtitle;
 -                      } else {
 -                              break;
 -                      }
 -              }
 -              return $titles;
 -      }
 -
 -      /**
 -       * Really extract the redirect destination
 -       * Do not call this function directly, use one of the newFromRedirect* functions above
 -       *
 -       * @param $text String Text with possible redirect
 -       * @return Title
 -       */
 -      protected static function newFromRedirectInternal( $text ) {
 -              global $wgMaxRedirects;
 -              if ( $wgMaxRedirects < 1 ) {
 -                      //redirects are disabled, so quit early
 -                      return null;
 -              }
 -              $redir = MagicWord::get( 'redirect' );
 -              $text = trim( $text );
 -              if ( $redir->matchStartAndRemove( $text ) ) {
 -                      // Extract the first link and see if it's usable
 -                      // Ensure that it really does come directly after #REDIRECT
 -                      // Some older redirects included a colon, so don't freak about that!
 -                      $m = array();
 -                      if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
 -                              // Strip preceding colon used to "escape" categories, etc.
 -                              // and URL-decode links
 -                              if ( strpos( $m[1], '%' ) !== false ) {
 -                                      // Match behavior of inline link parsing here;
 -                                      $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
 -                              }
 -                              $title = Title::newFromText( $m[1] );
 -                              // If the title is a redirect to bad special pages or is invalid, return null
 -                              if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
 -                                      return null;
 -                              }
 -                              return $title;
 -                      }
 -              }
 -              return null;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectChain();
        }
  
        /**
                return $this->mNamespace;
        }
  
 +      /**
 +       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 +       *
 +       * @return String: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $linkCache = LinkCache::singleton();
 +                      $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 +              }
 +
 +              if ( !$this->mContentModel ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              if( !$this->mContentModel ) {
 +                      throw new MWException( "failed to determin content model!" );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Convenience method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return Boolean true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         * @return Bool
         */
        public function isConversionTable() {
 +              //@todo: ConversionTable should become a separate content model.
 +
                return $this->getNamespace() == NS_MEDIAWIKI &&
                        strpos( $this->getText(), 'Conversiontable/' ) === 0;
        }
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +       * or CONTENT_MODEL_JAVASCRIPT.
 +       *
 +       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +       *
 +       * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +              $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +                      && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                              || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +              #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +              #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +              return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
        }
  
        /**
-        * Get the base page name, i.e. the leftmost part before any slashes
+        * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
+        *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
+        * # returns: 'Foo'
+        * @endcode
+        *
+        * @return String Root name
+        * @since 1.20
+        */
+       public function getRootText() {
+               if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
+                       return $this->getText();
+               }
+               return strtok( $this->getText(), '/' );
+       }
+       /**
+        * Get the root page name title, i.e. the leftmost part before any slashes
+        *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
+        * # returns: Title{User:Foo}
+        * @endcode
+        *
+        * @return Title Root title
+        * @since 1.20
+        */
+       public function getRootTitle() {
+               return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
+       }
+       /**
+        * Get the base page name without a namespace, i.e. the part before the subpage name
+        *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
+        * # returns: 'Foo/Bar'
+        * @endcode
         *
         * @return String Base name
         */
                return implode( '/', $parts );
        }
  
+       /**
+        * Get the base page name title, i.e. the part before the subpage name
+        *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
+        * # returns: Title{User:Foo/Bar}
+        * @endcode
+        *
+        * @return Title Base title
+        * @since 1.20
+        */
+       public function getBaseTitle() {
+               return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
+       }
        /**
         * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
         *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
+        * # returns: "Baz"
+        * @endcode
+        *
         * @return String Subpage name
         */
        public function getSubpageText() {
                return( $parts[count( $parts ) - 1] );
        }
  
+       /**
+        * Get the title for a subpage of the current page
+        *
+        * @par Example:
+        * @code
+        * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
+        * # returns: Title{User:Foo/Bar/Baz/Asdf}
+        * @endcode
+        *
+        * @param $text String The subpage name to add to the title
+        * @return Title Subpage title
+        * @since 1.20
+        */
+       public function getSubpage( $text ) {
+               return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
+       }
        /**
         * Get the HTML-escaped displayable text form.
         * Used for the title field in <a> tags.
         *
         * See getLocalURL for the arguments.
         *
+        * @param $proto Protocol to use; setting this will cause a full URL to be used
         * @see self::getLocalURL
         * @return String the URL
         */
-       public function getLinkURL( $query = '', $query2 = false ) {
+       public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
                wfProfileIn( __METHOD__ );
-               if ( $this->isExternal() ) {
-                       $ret = $this->getFullURL( $query, $query2 );
+               if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
+                       $ret = $this->getFullURL( $query, $query2, $proto );
                } elseif ( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) {
                        $ret = $this->getFragmentForURL();
                } else {
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
 +
                $linkCache = LinkCache::singleton();
 -              $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mRedirect = (bool)$cached;
  
                return $this->mRedirect;
        }
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLength = intval( $cached );
  
                return $this->mLength;
        }
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLatestID = intval( $cached );
  
                return $this->mLatestID;
        }
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
 +              $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
  
  
                $res = $db->select(
                        array( 'page', $table ),
 -                      array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
 +              global $wgContentHandlerUseDB;
 +
                $id = $this->getArticleID();
  
                # If the page doesn't exist; there can't be any link from this page
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
  
 +              $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $res = $db->select(
                        array( $table, 'page' ),
 -                      array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
         * @return Mixed True on success, getUserPermissionsErrors()-like array on failure
         */
        public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
  
                $errors = array();
                if ( !$nt ) {
                        $errors[] = array( 'badarticleerror' );
                }
  
 +              // Content model checks
 +              if ( !$wgContentHandlerUseDB &&
 +                              $this->getContentModel() !== $nt->getContentModel() ) {
 +                      // can't move a page if that would change the page's content model
 +                      $errors[] = array( 'bad-target-model',
 +                                                      ContentHandler::getLocalizedName( $this->getContentModel() ),
 +                                                      ContentHandler::getLocalizedName( $nt->getContentModel() ) );
 +              }
 +
                // Image-specific checks
                if ( $this->getNamespace() == NS_FILE ) {
                        $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
                        $logType = 'move';
                }
  
 -              $redirectSuppressed = !$createRedirect;
 +              if ( $createRedirect ) {
 +                      $contentHandler = ContentHandler::getForTitle( $this );
 +                      $redirectContent = $contentHandler->makeRedirectContent( $nt );
 +
 +                      // NOTE: If this page's content model does not support redirects, $redirectContent will be null.
 +              } else {
 +                      $redirectContent = null;
 +              }
  
                $logEntry = new ManualLogEntry( 'move', $logType );
                $logEntry->setPerformer( $wgUser );
                $logEntry->setComment( $reason );
                $logEntry->setParameters( array(
                        '4::target' => $nt->getPrefixedText(),
 -                      '5::noredir' => $redirectSuppressed ? '1': '0',
 +                      '5::noredir' => $redirectContent ? '0': '1',
                ) );
  
                $formatter = LogFormatter::newFromEntry( $logEntry );
                }
  
                # Recreate the redirect, this time in the other direction.
 -              if ( $redirectSuppressed ) {
 +              if ( !$redirectContent ) {
                        WikiPage::onArticleDelete( $this );
                } else {
 -                      $mwRedir = MagicWord::get( 'redirect' );
 -                      $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n";
                        $redirectArticle = WikiPage::factory( $this );
                        $newid = $redirectArticle->insertOn( $dbw );
                        if ( $newid ) { // sanity
                                $redirectRevision = new Revision( array(
                                        'page'    => $newid,
                                        'comment' => $comment,
 -                                      'text'    => $redirectText ) );
 +                                      'content'    => $redirectContent ) );
                                $redirectRevision->insertOn( $dbw );
                                $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 );
  
         * @return Bool
         */
        public function isSingleRevRedirect() {
 +              global $wgContentHandlerUseDB;
 +
                $dbw = wfGetDB( DB_MASTER );
 +
                # Is it a redirect?
 +              $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $row = $dbw->selectRow( 'page',
 -                      array( 'page_is_redirect', 'page_latest', 'page_id' ),
 +                      $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
 +              $this->mContentModel = $row && isset( $row->page_content_model ) ? strval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
                if( !is_object( $rev ) ){
                        return false;
                }
 -              $text = $rev->getText();
 +              $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
 -              $m = array();
 -              if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
 -                      $redirTitle = Title::newFromText( $m[1] );
 -                      if ( !is_object( $redirTitle ) ||
 -                              ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 -                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
 +              $redirTitle = $content->getRedirectTarget();
 +
 +              if ( $redirTitle ) {
 +                      if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 +                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
 +                      } else {
 +                              return true;
                        }
                } else {
 -                      # Fail safe
 -                      wfDebug( __METHOD__ . ": failsafe\n" );
 +                      # Fail safe (not a redirect after all. strange.)
 +                      wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
 +                                              " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
 -              return true;
        }
  
        /**
                if ( $this->isSpecialPage() ) {
                        // special pages are in the user language
                        return $wgLang;
 -              } elseif ( $this->isCssOrJsPage() || $this->isCssJsSubpage() ) {
 -                      // css/js should always be LTR and is, in fact, English
 -                      return wfGetLangObj( 'en' );
 -              } elseif ( $this->getNamespace() == NS_MEDIAWIKI ) {
 -                      // Parse mediawiki messages with correct target language
 -                      list( /* $unused */, $lang ) = MessageCache::singleton()->figureMessage( $this->getText() );
 -                      return wfGetLangObj( $lang );
                }
 -              global $wgContLang;
 -              // If nothing special, it should be in the wiki content language
 -              $pageLang = $wgContLang;
 +
 +              //TODO: use the LinkCache to cache this! Note that this may depend on user settings, so the cache should be only per-request.
 +              //NOTE: ContentHandler::getPageLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageLanguage( $this );
 +
                // Hook at the end because we don't want to override the above stuff
                wfRunHooks( 'PageContentLanguage', array( $this, &$pageLang, $wgLang ) );
                return wfGetLangObj( $pageLang );
         * @return Language
         */
        public function getPageViewLanguage() {
 -              $pageLang = $this->getPageLanguage();
 -              // If this is nothing special (so the content is converted when viewed)
 -              if ( !$this->isSpecialPage()
 -                      && !$this->isCssOrJsPage() && !$this->isCssJsSubpage()
 -                      && $this->getNamespace() !== NS_MEDIAWIKI
 -              ) {
 +              global $wgLang;
 +
 +              if ( $this->isSpecialPage() ) {
                        // If the user chooses a variant, the content is actually
                        // in a language whose code is the variant code.
 -                      $variant = $pageLang->getPreferredVariant();
 -                      if ( $pageLang->getCode() !== $variant ) {
 -                              $pageLang = Language::factory( $variant );
 +                      $variant = $wgLang->getPreferredVariant();
 +                      if ( $wgLang->getCode() !== $variant ) {
 +                              return Language::factory( $variant );
                        }
 +
 +                      return $wgLang;
                }
 +
 +              //NOTE: can't be cached persistently, depends on user settings
 +              //NOTE: ContentHandler::getPageViewLanguage() may need to load the content to determine the page language!
 +              $contentHandler = ContentHandler::getForTitle( $this );
 +              $pageLang = $contentHandler->getPageViewLanguage( $this );
                return $pageLang;
        }
  }
diff --combined includes/WikiPage.php
@@@ -187,21 -187,7 +187,21 @@@ class WikiPage extends Page implements 
         * @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.21
 +       */
 +      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;
        }
  
        /**
        }
  
        /**
 -       * Tests if the article text represents a redirect
 +       * Tests if the article content represents a redirect
         *
 -       * @param $text mixed string containing article contents, or boolean
         * @return bool
         */
 -      public function isRedirect( $text = false ) {
 -              if ( $text === false ) {
 -                      if ( !$this->mDataLoaded ) {
 -                              $this->loadPageData();
 -                      }
 +      public function isRedirect( ) {
 +              $content = $this->getContent();
 +              if ( !$content ) return false;
  
 -                      return (bool)$this->mIsRedirect;
 -              } else {
 -                      return Title::newFromRedirect( $text ) !== null;
 +              return $content->isRedirect();
 +      }
 +
 +      /**
 +       * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
 +       *
 +       * Will use the revisions actual content model if the page exists,
 +       * and the page's default if the page doesn't exist yet.
 +       *
 +       * @return String
 +       *
 +       * @since 1.21
 +       */
 +      public function getContentModel() {
 +              if ( $this->exists() ) {
 +                      # look at the revision's actual content model
 +                      $rev = $this->getRevision();
 +
 +                      if ( $rev !== null ) {
 +                              return $rev->getContentModel();
 +                      } else {
 +                              $title = $this->mTitle->getPrefixedDBkey();
 +                              wfWarn( "Page $title exists but has no (visible) revisions!" );
 +                      }
                }
 +
 +              # use the default model for this page
 +              return $this->mTitle->getContentModel();
        }
  
        /**
                return null;
        }
  
-       public function getContent( $audience = Revision::FOR_PUBLIC ) {
 +      /**
 +       * 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
++       * @param $user User object to check for, only if FOR_THIS_USER is passed
++       *              to the $audience parameter
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.21
 +       */
++      public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 +              $this->loadLastEdit();
 +              if ( $this->mLastRevision ) {
 +                      return $this->mLastRevision->getContent( $audience );
 +              }
 +              return null;
 +      }
 +
        /**
         * Get the text 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::FOR_THIS_USER    to be displayed to the given user
         *      Revision::RAW              get the text regardless of permissions
 -       * @return String|bool The text of the current revision. False on failure
+        * @param $user User object to check for, only if FOR_THIS_USER is passed
+        *              to the $audience parameter
 +       * @return String|false The text of the current revision
 +       * @deprecated as of 1.21, getContent() should be used instead.
         */
-       public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 -      public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
++      public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.21' );
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getText( $audience );
+                       return $this->mLastRevision->getText( $audience, $user );
                }
                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.21, getContent() should be used instead.
         */
        public function getRawText() {
 -              $this->loadLastEdit();
 -              if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getRawText();
 -              }
 -              return false;
 +              wfDeprecated( __METHOD__, '1.21' );
 +
 +              return $this->getText( Revision::RAW );
        }
  
        /**
        /**
         * @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 int user ID for the user that made the last article revision
         */
-       public function getUser( $audience = Revision::FOR_PUBLIC ) {
+       public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getUser( $audience );
+                       return $this->mLastRevision->getUser( $audience, $user );
                } else {
                        return -1;
                }
         * Get the User object of the user who created the page
         * @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 User|null
         */
-       public function getCreator( $audience = Revision::FOR_PUBLIC ) {
+       public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
                $revision = $this->getOldestRevision();
                if ( $revision ) {
-                       $userName = $revision->getUserText( $audience );
+                       $userName = $revision->getUserText( $audience, $user );
                        return User::newFromName( $userName, false );
                } else {
                        return null;
        /**
         * @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 username of the user that made the last article revision
         */
-       public function getUserText( $audience = Revision::FOR_PUBLIC ) {
+       public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getUserText( $audience );
+                       return $this->mLastRevision->getUserText( $audience, $user );
                } else {
                        return '';
                }
        /**
         * @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 Comment stored for the last article revision
         */
-       public function getComment( $audience = Revision::FOR_PUBLIC ) {
+       public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
-                       return $this->mLastRevision->getComment( $audience );
+                       return $this->mLastRevision->getComment( $audience, $user );
                } else {
                        return '';
                }
                        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 ) {
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 +                      //@todo: move this logic to MessageCache
 +
                        if ( $this->mTitle->exists() ) {
 -                              $text = $this->getRawText();
 +                              // NOTE: use transclusion text for messages.
 +                              //       This is consistent with  MessageCache::getMsgFromNamespace()
 +
 +                              $content = $this->getContent();
 +                              $text = $content === null ? null : $content->getWikitextForTransclusion();
 +
 +                              if ( $text === null ) $text = false;
                        } else {
                                $text = false;
                        }
         * @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__ );
  
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        # Update the LinkCache.
 -                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
 +                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect,
 +                                                                                                      $this->mLatest, $revision->getContentModel() );
                }
  
                wfProfileOut( __METHOD__ );
                return $ret;
        }
  
 +    /**
 +     * Get the content that needs to be saved in order to undo all revisions
 +     * between $undo and $undoafter. Revisions must belong to the same page,
 +     * must exist and must not be deleted
 +     * @param $undo Revision
 +     * @param $undoafter Revision Must be an earlier revision than $undo
 +     * @return mixed string on success, false on failure
 +     * @since 1.21
 +     * Before we had the Content object, this was done in getUndoText
 +     */
 +    public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
 +        $handler = $undo->getContentHandler();
 +        return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
 +    }
 +
        /**
         * Get the text that needs to be saved in order to undo all revisions
         * between $undo and $undoafter. Revisions must belong to the same page,
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
 +       * @deprecated since 1.21: 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.21' );
  
 -              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.21, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
 +              wfDeprecated( __METHOD__, '1.21' );
 +
 +              if ( strval( $section ) == '' ) { //NOTE: keep condition in sync with condition in replaceSectionContent!
 +                      // Whole-page edit; let the whole text through
 +                      return $text;
 +              }
 +
 +              if ( !$this->supportsSections() ) {
 +                      throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +              }
 +
 +              # could even make section title, but that's not required.
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent );
 +      }
 +
 +      /**
 +       * Returns true iff this page's content model supports sections.
 +       *
 +       * @return boolean whether sections are supported.
 +       *
 +       * @todo: the skin should check this and not offer section functionality if sections are not supported.
 +       * @todo: the EditPage should check this and not offer section functionality if sections are not supported.
 +       */
 +      public function supportsSections() {
 +              return $this->getContentHandler()->supportsSections();
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.21
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
 +                      if ( !$this->supportsSections() ) {
 +                              throw new MWException( "sections not supported for content model " . $this->getContentHandler()->getModelID() );
 +                      }
 +
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
 -                                      ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
 -                              if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 -                                      $text = strlen( trim( $oldtext ) ) > 0
 -                                              ? "{$oldtext}\n\n{$subject}{$text}"
 -                                              : "{$subject}{$text}";
 -                              }
 -                      } else {
 -                              # Replacing an existing section; roll out the big guns
 -                              global $wgParser;
 -
 -                              $text = $wgParser->replaceSection( $oldtext, $section, $text );
 -                      }
 +                      $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     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.21: use doEditContent() instead.
         */
        public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 +              wfDeprecated( __METHOD__, '1.21' );
 +
 +              $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.21
 +       */
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
  
                # Low-level sanity check
  
                wfProfileIn( __METHOD__ );
  
 +              if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
 +                      wfProfileOut( __METHOD__ );
 +                      return Status::newFatal( 'content-not-allowed-here',
 +                              ContentHandler::getLocalizedName( $content->getModel() ),
 +                              $this->getTitle()->getPrefixedText() );
 +              }
 +
                $user = is_null( $user ) ? $wgUser : $user;
                $status = Status::newGood( array() );
  
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # handle hook
 +              $hook_args = array( &$this, &$user, &$content, &$summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, &$status );
 +
 +              if ( !wfRunHooks( 'ArticleContentSave', $hook_args )
 +                      || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
 +
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $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();
  
                                wfProfileOut( __METHOD__ );
                                return $status;
 -                      } elseif ( $oldtext === false ) {
 +                      } elseif ( !$old_content ) {
                                # Sanity check for bug 37225
                                wfProfileOut( __METHOD__ );
                                throw new MWException( "Could not find text for current revision {$oldid}." );
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 -                      ) );
 -                      # Bug 37225: use accessor to get the text as Revision may trim it.
 -                      # After trimming, the text may be a duplicate of the current text.
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
 +                      ) ); #XXX: pass content object?!
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              if ( !$content->isValid() ) {
 +                                      throw new MWException( "New content failed validity check!" );
 +                              }
 +
                                $dbw->begin( __METHOD__ );
 +
 +                              $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                              $status->merge( $prepStatus );
 +
 +                              if ( !$status->isOK() ) {
 +                                      $dbw->rollback();
 +
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
 +
                                $revisionId = $revision->insertOn( $dbw );
  
                                # Update page
                        }
  
                        # 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' );
  
                        $dbw->begin( __METHOD__ );
  
 +                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
 +                      $status->merge( $prepStatus );
 +
 +                      if ( !$status->isOK() ) {
 +                              $dbw->rollback();
 +
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
 +
 +                      $status->merge( $prepStatus );
 +
                        # Add the page record; stake our claim on this title!
                        # This will return false if the article already exists
                        $newid = $this->insertOn( $dbw );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                        # Bug 37225: use accessor to get the text as Revision may trim it
 -                      $text = $revision->getText(); // sanity; EditPage should trim already
 +                      $content = $revision->getContent(); // sanity; get normalized version
  
                        # Update the page record with revision data
                        $this->updateRevisionOn( $dbw, $revision, 0 );
                                        $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,
 -                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +                      $hook_args = array( &$this, &$user, $content, $summary,
 +                                                              $flags & EDIT_MINOR, null, null, &$flags, $revision );
 +
 +                      ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
 +                      wfRunHooks( 'ArticleContentInsertComplete', $hook_args );
                }
  
                # Do updates right now unless deferral was requested
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +              $hook_args = array( &$this, &$user, $content, $summary,
 +                                                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
 +
 +              ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
 +              wfRunHooks( 'ArticleContentSaveComplete', $hook_args );
  
                # Promote user to any groups they meet the criteria for
                $user->addAutopromoteOnceGroups( 'onEdit' );
        /**
         * Get parser options suitable for rendering the primary article wikitext
         *
 +       * @see ContentHandler::makeParserOptions
 +       *
         * @param IContextSource|User|string $context One of the following:
         *        - IContextSource: Use the User and the Language of the provided
         *          context
         * @return ParserOptions
         */
        public function makeParserOptions( $context ) {
 -              global $wgContLang;
 -
 -              if ( $context instanceof IContextSource ) {
 -                      $options = ParserOptions::newFromContext( $context );
 -              } elseif ( $context instanceof User ) { // settings per user (even anons)
 -                      $options = ParserOptions::newFromUser( $context );
 -              } else { // canonical settings
 -                      $options = ParserOptions::newFromUserAndLang( new User, $wgContLang );
 -              }
 +              $options = $this->getContentHandler()->makeParserOptions( $context );
  
                if ( $this->getTitle()->isConversionTable() ) {
 +                      //@todo: ConversionTable should become a separate content model, so we don't need special cases like this one.
                        $options->disableContentConversion();
                }
  
 -              $options->enableLimitReport(); // show inclusion/loop reports
 -              $options->setTidy( true ); // fix bad HTML
 -
                return $options;
        }
  
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.21: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.21' );
 +              $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.21
 +       */
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
 -              // @TODO fixme: check $user->getId() here???
 +              //XXX: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $edit = (object)array();
                $edit->revid = $revid;
 -              $edit->newText = $text;
 -              $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
 +
 +              $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 +              $edit->pst = $edit->pstContent->serialize( $serialization_format ); #XXX: do we need this??
 +              $edit->format = $serialization_format;
 +
                $edit->popts = $this->makeParserOptions( 'canonical' );
 -              $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
 -              $edit->oldText = $this->getRawText();
 +
 +              $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
 +
 +              $edit->newContent = $content;
 +              $edit->oldContent = $this->getContent( Revision::RAW );
 +
 +              #NOTE: B/C for hooks! don't use these fields!
 +              $edit->newText = ContentHandler::getContentText( $edit->newContent );
 +              $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
  
                $this->mPreparedEdit = $edit;
  
         * 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 = $content->getSecondaryDataUpdates( $this->getTitle(), null, true, $editInfo->output );
                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() ) );
 +              #@TODO: let the search engine decide what to do with the content object
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      #XXX: could skip pseudo-messages like js/css here, based on content model.
 +                      $msgtext = $content->getWikitextForTransclusion();
 +                      if ( $msgtext === false || $msgtext === null ) $msgtext = '';
 +
 +                      MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
  
                if( $options['created'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.21, use doEditContent() instead.
         */
        public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
 +              wfDeprecated( __METHOD__, "1.21" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->doQuickEditContent( $content, $user, $comment , $minor );
 +      }
 +
 +      /**
 +       * Edit an article without doing all that other stuff
 +       * The article must already exist; link tables etc
 +       * are not updated, caches are not flushed.
 +       *
 +       * @param $content Content: content submitted
 +       * @param $user User The relevant user
 +       * @param $comment String: comment submitted
 +       * @param $serialisation_format String: format for storing the content in the database
 +       * @param $minor Boolean: whereas it's a minor modification
 +       */
 +      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
  
 +              $serialized = $content->serialize( $serialisation_format );
 +
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
 -                      'text'       => $text,
 +                      'text'       => $serialized,
 +                      'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
 -              ) );
 +              ) ); #XXX: set the content object?
                $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
  
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
  
                wfDebug( __METHOD__ . "\n" );
  
                        $bitfield = 'rev_deleted';
                }
  
 +              // we need to remember the old content so we can use it to generate all deletion updates.
 +              $content = $this->getContent( Revision::RAW );
 +
                $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
                // For now, shunt the revision data into the archive table.
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
 +
 +              $row = array(
 +                      'ar_namespace'  => 'page_namespace',
 +                      'ar_title'      => 'page_title',
 +                      'ar_comment'    => 'rev_comment',
 +                      'ar_user'       => 'rev_user',
 +                      'ar_user_text'  => 'rev_user_text',
 +                      'ar_timestamp'  => 'rev_timestamp',
 +                      'ar_minor_edit' => 'rev_minor_edit',
 +                      'ar_rev_id'     => 'rev_id',
 +                      'ar_parent_id'  => 'rev_parent_id',
 +                      'ar_text_id'    => 'rev_text_id',
 +                      'ar_text'       => '\'\'', // Be explicit to appease
 +                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 +                      'ar_len'        => 'rev_len',
 +                      'ar_page_id'    => 'page_id',
 +                      'ar_deleted'    => $bitfield,
 +                      'ar_sha1'       => 'rev_sha1',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'ar_content_model' ] = 'rev_content_model';
 +                      $row[ 'ar_content_format' ] = 'rev_content_format';
 +              }
 +
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
 +                      $row,
                        array(
 -                              'ar_namespace'  => 'page_namespace',
 -                              'ar_title'      => 'page_title',
 -                              'ar_comment'    => 'rev_comment',
 -                              'ar_user'       => 'rev_user',
 -                              'ar_user_text'  => 'rev_user_text',
 -                              'ar_timestamp'  => 'rev_timestamp',
 -                              'ar_minor_edit' => 'rev_minor_edit',
 -                              'ar_rev_id'     => 'rev_id',
 -                              'ar_parent_id'  => 'rev_parent_id',
 -                              'ar_text_id'    => 'rev_text_id',
 -                              'ar_text'       => '\'\'', // Be explicit to appease
 -                              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                              'ar_len'        => 'rev_len',
 -                              'ar_page_id'    => 'page_id',
 -                              'ar_deleted'    => $bitfield,
 -                              'ar_sha1'       => 'rev_sha1'
 -                      ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
                        return $status;
                }
  
 -              $this->doDeleteUpdates( $id );
 +              $this->doDeleteUpdates( $id, $content );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
                        $dbw->commit( __METHOD__ );
                }
  
 -              wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id ) );
 +              wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) );
                $status->value = $logid;
                return $status;
        }
         * Do some database updates after deletion
         *
         * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
 +       * @param $content Content: optional page content to be used when determining the required updates.
 +       *        This may be needed because $this->getContent() may already return null when the page proper was deleted.
         */
 -      public function doDeleteUpdates( $id ) {
 +      public function doDeleteUpdates( $id, Content $content = null ) {
                # update site status
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
  
                # remove secondary indexes, etc
 -              $updates = $this->getDeletionUpdates( );
 +              $updates = $this->getDeletionUpdates( $content );
                DataUpdate::runUpdates( $updates );
  
                # Clear caches
                $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 ( !$status->isOK() ) {
 +                      return $status->getErrorsArray();
 +              }
 +
                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.21, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
 -              global $wgContLang;
 +              # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
  
 -              # Decide what kind of autosummary is needed.
 +              wfDeprecated( __METHOD__, '1.21' );
  
 -              # Redirect autosummaries
 -              $ot = Title::newFromRedirect( $oldtext );
 -              $rt = Title::newFromRedirect( $newtext );
 -
 -              if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 255
 -                                      - strlen( wfMessage( 'autoredircomment' )->inContentLanguage()->text() )
 -                                      - strlen( $rt->getFullText() )
 -                              ) );
 -                      return wfMessage( 'autoredircomment', $rt->getFullText() )
 -                              ->rawParams( $truncatedtext )->inContentLanguage()->text();
 -              }
 -
 -              # New page autosummaries
 -              if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
 -                      # If they're making a new article, give its text, truncated, in the summary.
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMessage( 'autosumm-new' )->inContentLanguage()->text() ) ) );
 -
 -                      return wfMessage( 'autosumm-new' )->rawParams( $truncatedtext )
 -                              ->inContentLanguage()->text();
 -              }
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -              # Blanking autosummaries
 -              if ( $oldtext != '' && $newtext == '' ) {
 -                      return wfMessage( 'autosumm-blank' )->inContentLanguage()->text();
 -              } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
 -                      # Removing more than 90% of the article
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMessage( 'autosumm-replace' )->inContentLanguage()->text() ) ) );
 -
 -                      return wfMessage( 'autosumm-replace' )->rawParams( $truncatedtext )
 -                              ->inContentLanguage()->text();
 -              }
 -
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         *    if no revision occurred
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 -              global $wgContLang;
 -
 -              // Get the last revision
 -              $rev = $this->getRevision();
 -
 -              if ( is_null( $rev ) ) {
 -                      return false;
 -              }
 -
 -              // Get the article's contents
 -              $contents = $rev->getText();
 -              $blank = false;
 -
 -              // If the page is blank, use the text from the previous revision,
 -              // which can only be blank if there's a move/import/protect dummy revision involved
 -              if ( $contents == '' ) {
 -                      $prev = $rev->getPrevious();
 -
 -                      if ( $prev )    {
 -                              $contents = $prev->getText();
 -                              $blank = true;
 -                      }
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              // Find out if there was only one contributor
 -              // Only scan the last 20 revisions
 -              $res = $dbw->select( 'revision', 'rev_user_text',
 -                      array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
 -                      __METHOD__,
 -                      array( 'LIMIT' => 20 )
 -              );
 -
 -              if ( $res === false ) {
 -                      // This page has no revisions, which is very weird
 -                      return false;
 -              }
 -
 -              $hasHistory = ( $res->numRows() > 1 );
 -              $row = $dbw->fetchObject( $res );
 -
 -              if ( $row ) { // $row is false if the only contributor is hidden
 -                      $onlyAuthor = $row->rev_user_text;
 -                      // Try to find a second contributor
 -                      foreach ( $res as $row ) {
 -                              if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
 -                                      $onlyAuthor = false;
 -                                      break;
 -                              }
 -                      }
 -              } else {
 -                      $onlyAuthor = false;
 -              }
 -
 -              // Generate the summary with a '$1' placeholder
 -              if ( $blank ) {
 -                      // The current revision is blank and the one before is also
 -                      // blank. It's just not our lucky day
 -                      $reason = wfMessage( 'exbeforeblank', '$1' )->inContentLanguage()->text();
 -              } else {
 -                      if ( $onlyAuthor ) {
 -                              $reason = wfMessage(
 -                                      'excontentauthor',
 -                                      '$1',
 -                                      $onlyAuthor
 -                              )->inContentLanguage()->text();
 -                      } else {
 -                              $reason = wfMessage( 'excontent', '$1' )->inContentLanguage()->text();
 -                      }
 -              }
 -
 -              if ( $reason == '-' ) {
 -                      // Allow these UI messages to be blanked out cleanly
 -                      return '';
 -              }
 -
 -              // Replace newlines with spaces to prevent uglyness
 -              $contents = preg_replace( "/[\n\r]/", ' ', $contents );
 -              // Calculate the maximum amount of chars to get
 -              // Max content length = max comment length - length of the comment (excl. $1)
 -              $maxLength = 255 - ( strlen( $reason ) - 2 );
 -              $contents = $wgContLang->truncate( $contents, $maxLength );
 -              // Remove possible unfinished links
 -              $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
 -              // Now replace the '$1' placeholder
 -              $reason = str_replace( '$1', $contents, $reason );
 -
 -              return $reason;
 +              return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
  
        /**
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
 +
 +      /**
 +       * Returns a list of updates to be performed when this page is deleted. The updates should remove any information
 +       * about this page from secondary data stores such as links tables.
 +       *
 +       * @param Content|null $content optional Content object for determining the necessary updates
 +       * @return Array an array of DataUpdates objects
 +       */
 +      public function getDeletionUpdates( Content $content = null ) {
 +              if ( !$content ) {
 +                      // load content object, which may be used to determine the necessary updates
 +                      // XXX: the content may not be needed to determine the updates, then this would be overhead.
 +                      $content = $this->getContent( Revision::RAW );
 +              }
 +
 +              if ( !$content ) {
 +                      $updates = array();
 +              } else {
 +                      $updates = $content->getDeletionUpdates( $this );
 +              }
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        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();
 +                      #XXX: why use RAW audience here, and PUBLIC (default) below?
 +                      $content = $this->page->getContent( Revision::RAW );
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +
 +                      #XXX: why use PUBLIC audience here (default), and RAW above?
 +                      $content = $rev->getContent();
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
@@@ -71,39 -71,38 +71,32 @@@ class RollbackAction extends FormlessAc
                        return;
                }
  
 -              # Display permissions errors before read-only message -- there's no
 -              # point in misleading the user into thinking the inability to rollback
 -              # is only temporary.
 -              if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
 -                      # array_diff is completely broken for arrays of arrays, sigh.
 -                      # Remove any 'readonlytext' error manually.
 -                      $out = array();
 -                      foreach ( $result as $error ) {
 -                              if ( $error != array( 'readonlytext' ) ) {
 -                                      $out [] = $error;
 -                              }
 -                      }
 -                      throw new PermissionsError( 'rollback', $out );
 -              }
 +              #NOTE: Permission errors already handled by Action::checkExecute.
  
                if ( $result == array( array( 'readonlytext' ) ) ) {
                        throw new ReadOnlyError;
                }
  
 +              #XXX: Would be nice if ErrorPageError could take multiple errors, and/or a status object.
 +              #     Right now, we only show the first error
 +              foreach ( $result as $error ) {
 +                      throw new ErrorPageError( 'rollbackfailed', $error[0], array_slice( $error, 1 ) );
 +              }
 +
                $current = $details['current'];
                $target = $details['target'];
                $newId = $details['newid'];
                $this->getOutput()->setPageTitle( $this->msg( 'actioncomplete' ) );
                $this->getOutput()->setRobotPolicy( 'noindex,nofollow' );
  
-               if ( $current->getUserText() === '' ) {
-                       $old = $this->msg( 'rev-deleted-user' )->escaped();
-               } else {
-                       $old = Linker::userLink( $current->getUser(), $current->getUserText() )
-                               . Linker::userToolLinks( $current->getUser(), $current->getUserText() );
-               }
-               $new = Linker::userLink( $target->getUser(), $target->getUserText() )
-                       . Linker::userToolLinks( $target->getUser(), $target->getUserText() );
+               $old = Linker::revUserTools( $current );
+               $new = Linker::revUserTools( $target );
                $this->getOutput()->addHTML( $this->msg( 'rollback-success' )->rawParams( $old, $new )->parseAsBlock() );
                $this->getOutput()->returnToMain( false, $this->getTitle() );
  
                if ( !$request->getBool( 'hidediff', false ) && !$this->getUser()->getBoolOption( 'norollbackdiff', false ) ) {
 -                      $de = new DifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
 +                      $contentHandler = $current->getContentHandler();
 +                      $de = $contentHandler->createDifferenceEngine( $this->getContext(), $current->getId(), $newId, false, true );
                        $de->showDiff( '', '' );
                }
        }
@@@ -61,6 -61,9 +61,9 @@@ class ApiDelete extends ApiBase 
                        $status = self::delete( $pageObj, $user, $params['token'], $reason );
                }
  
+               if ( is_array( $status ) ) {
+                       $this->dieUsageMsg( $status[0] );
+               }
                if ( !$status->isGood() ) {
                        $errors = $status->getErrorsArray();
                        $this->dieUsageMsg( $errors[0] ); // We don't care about multiple errors, just report one of them
        /**
         * We have our own delete() function, since Article.php's implementation is split in two phases
         *
-        * @param $page WikiPage object to work on
+        * @param $page Page|WikiPage object to work on
         * @param $user User doing the action
-        * @param $token String: delete token (same as edit token)
-        * @param $reason String: reason for the deletion. Autogenerated if NULL
-        * @return Status
+        * @param $token String delete token (same as edit token)
+        * @param $reason String|null reason for the deletion. Autogenerated if NULL
+        * @return Status|array
         */
        public static function delete( Page $page, User $user, $token, &$reason = null ) {
                $title = $page->getTitle();
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
 -                      $reason = $page->getAutoDeleteReason( $hasHistory );
 +                      $reason = $page->getAutoDeleteReason( $hasHistory ); 
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
        }
  
        /**
-        * @param $page WikiPage object to work on
+        * @param $page WikiPage|Page object to work on
         * @param $user User doing the action
         * @param $token
         * @param $oldimage
         * @param $reason
         * @param $suppress bool
-        * @return Status
+        * @return Status|array
         */
        public static function deleteFile( Page $page, User $user, $token, $oldimage, &$reason = null, $suppress = false ) {
                $title = $page->getTitle();
                if ( is_null( $reason ) ) { // Log and RC don't like null reasons
                        $reason = '';
                }
-               return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress );
+               return FileDeleteForm::doDelete( $title, $file, $oldimage, $reason, $suppress, $user );
        }
  
        public function mustBePosted() {
@@@ -54,37 -54,17 +54,37 @@@ class ApiEditPage extends ApiBase 
                        $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
  
 +              if ( !isset( $params['contentmodel'] ) || $params['contentmodel'] == '' ) {
 +                      $contentHandler = $pageObj->getContentHandler();
 +              } else {
 +                      $contentHandler = ContentHandler::getForModelID( $params['contentmodel'] );
 +              }
 +
 +              // @todo ask handler whether direct editing is supported at all! make allowFlatEdit() method or some such
 +
 +              if ( !isset( $params['contentformat'] ) || $params['contentformat'] == '' ) {
 +                      $params['contentformat'] = $contentHandler->getDefaultFormat();
 +              }
 +
 +              $contentFormat = $params['contentformat'];
 +
 +              if ( !$contentHandler->isSupportedFormat( $contentFormat ) ) {
 +                      $name = $titleObj->getPrefixedDBkey();
 +                      $model = $contentHandler->getModelID();
 +
 +                      $this->dieUsage( "The requested format $contentFormat is not supported for content model ".
 +                                                      " $model used by $name", 'badformat' );
 +              }
 +
                $apiResult = $this->getResult();
  
                if ( $params['redirect'] ) {
                        if ( $titleObj->isRedirect() ) {
                                $oldTitle = $titleObj;
  
 -                              $titles = Title::newFromRedirectArray(
 -                                      Revision::newFromTitle(
 -                                              $oldTitle, false, Revision::READ_LATEST
 -                                      )->getText( Revision::FOR_THIS_USER, $user )
 -                              );
 +                              $titles = Revision::newFromTitle( $oldTitle, false, Revision::READ_LATEST )
-                                                       ->getContent( Revision::FOR_THIS_USER )
++                                                      ->getContent( Revision::FOR_THIS_USER, $user )
 +                                                      ->getRedirectChain();
                                // array_shift( $titles );
  
                                $redirValues = array();
                        $this->dieUsageMsg( $errors[0] );
                }
  
 -              $articleObj = Article::newFromTitle( $titleObj, $this->getContext() );
 -
                $toMD5 = $params['text'];
                if ( !is_null( $params['appendtext'] ) || !is_null( $params['prependtext'] ) )
                {
 -                      // For non-existent pages, Article::getContent()
 -                      // returns an interface message rather than ''
 -                      // We do want getContent()'s behavior for non-existent
 -                      // MediaWiki: pages, though
 -                      if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
 -                              $content = '';
 -                      } else {
 -                              $content = $articleObj->getContent();
 +                      $content = $pageObj->getContent();
 +
 +                      // @todo: Add support for appending/prepending to the Content interface
 +
 +                      if ( !( $content instanceof TextContent ) ) {
 +                              $mode = $contentHandler->getModelID();
 +                              $this->dieUsage( "Can't append to pages using content model $mode", 'appendnotsupported' );
 +                      }
 +
 +                      if ( !$content ) {
 +                              # If this is a MediaWiki:x message, then load the messages
 +                              # and return the message value for x.
 +                              if ( $titleObj->getNamespace() == NS_MEDIAWIKI ) {
 +                                      $text = $titleObj->getDefaultMessageText();
 +                                      if ( $text === false ) {
 +                                              $text = '';
 +                                      }
 +
 +                                      try {
 +                                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +                                      } catch ( MWContentSerializationException $ex ) {
 +                                              $this->dieUsage( $ex->getMessage(), 'parseerror' );
 +                                              return;
 +                                      }
 +                              }
                        }
  
                        if ( !is_null( $params['section'] ) ) {
 +                              if ( !$contentHandler->supportsSections() ) {
 +                                      $modelName = $contentHandler->getModelID();
 +                                      $this->dieUsage( "Sections are not supported for this content model: $modelName.", 'sectionsnotsupported' );
 +                              }
 +
                                // Process the content for section edits
 -                              global $wgParser;
                                $section = intval( $params['section'] );
 -                              $content = $wgParser->getSection( $content, $section, false );
 -                              if ( $content === false ) {
 +                              $content = $content->getSection( $section );
 +
 +                              if ( !$content ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
 -                      $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
 +
 +                      if ( !$content ) {
 +                              $text = '';
 +                      } else {
 +                              $text = $content->serialize( $contentFormat );
 +                      }
 +
 +                      $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
  
                                $this->dieUsageMsg( array( 'nosuchrevid', $params['undoafter'] ) );
                        }
  
 -                      if ( $undoRev->getPage() != $articleObj->getID() ) {
 +                      if ( $undoRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoRev->getID(), $titleObj->getPrefixedText() ) );
                        }
 -                      if ( $undoafterRev->getPage() != $articleObj->getID() ) {
 +                      if ( $undoafterRev->getPage() != $pageObj->getID() ) {
                                $this->dieUsageMsg( array( 'revwrongpage', $undoafterRev->getID(), $titleObj->getPrefixedText() ) );
                        }
  
 -                      $newtext = $articleObj->getUndoText( $undoRev, $undoafterRev );
 -                      if ( $newtext === false ) {
 +                      $newContent = $contentHandler->getUndoContent( $pageObj->getRevision(), $undoRev, $undoafterRev );
 +
 +                      if ( !$newContent ) {
                                $this->dieUsageMsg( 'undo-failure' );
                        }
 -                      $params['text'] = $newtext;
 +
 +                      $params['text'] = $newContent->serialize( $params['contentformat'] );
 +
                        // If no summary was given and we only undid one rev,
                        // use an autosummary
                        if ( is_null( $params['summary'] ) && $titleObj->getNextRevisionID( $undoafterRev->getID() ) == $params['undo'] ) {
                // That interface kind of sucks, but it's workable
                $requestArray = array(
                        'wpTextbox1' => $params['text'],
 +                      'format' => $contentFormat,
 +                      'model' => $contentHandler->getModelID(),
                        'wpEditToken' => $params['token'],
                        'wpIgnoreBlankSummary' => ''
                );
                if ( !is_null( $params['basetimestamp'] ) && $params['basetimestamp'] != '' ) {
                        $requestArray['wpEdittime'] = wfTimestamp( TS_MW, $params['basetimestamp'] );
                } else {
 -                      $requestArray['wpEdittime'] = $articleObj->getTimestamp();
 +                      $requestArray['wpEdittime'] = $pageObj->getTimestamp();
                }
  
                if ( !is_null( $params['starttimestamp'] ) && $params['starttimestamp'] != '' ) {
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
  
 -              $ep = new EditPage( $articleObj );
 +              $articleObject = new Article( $titleObj );
 +              $ep = new EditPage( $articleObject );
 +
 +              // allow editing of non-textual content.
 +              $ep->allowNonTextContent = true;
 +
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
  
                }
  
                // Do the actual save
 -              $oldRevId = $articleObj->getRevIdFetched();
 +              $oldRevId = $articleObject->getRevIdFetched();
                $result = null;
                // Fake $wgRequest for some hooks inside EditPage
                // @todo FIXME: This interface SUCKS
                        case EditPage::AS_HOOK_ERROR_EXPECTED:
                                $this->dieUsageMsg( 'hookaborted' );
  
 +                      case EditPage::AS_PARSE_ERROR:
 +                              $this->dieUsage( $status->getMessage(), 'parseerror' );
 +
                        case EditPage::AS_IMAGE_REDIRECT_ANON:
                                $this->dieUsageMsg( 'noimageredirect-anon' );
  
                                $r['result'] = 'Success';
                                $r['pageid'] = intval( $titleObj->getArticleID() );
                                $r['title'] = $titleObj->getPrefixedText();
 -                              $newRevId = $articleObj->getLatest();
 +                              $r['contentmodel'] = $titleObj->getContentModel();
 +                              $newRevId = $articleObject->getLatest();
                                if ( $newRevId == $oldRevId ) {
                                        $r['nochange'] = '';
                                } else {
                                        $r['oldrevid'] = intval( $oldRevId );
                                        $r['newrevid'] = intval( $newRevId );
                                        $r['newtimestamp'] = wfTimestamp( TS_ISO_8601,
 -                                              $articleObj->getTimestamp() );
 +                                              $pageObj->getTimestamp() );
                                }
                                break;
  
                                array( 'undo-failure' ),
                                array( 'hashcheckfailed' ),
                                array( 'hookaborted' ),
 +                              array( 'code' => 'parseerror', 'info' => 'Failed to parse the given text.' ),
                                array( 'noimageredirect-anon' ),
                                array( 'noimageredirect-logged' ),
                                array( 'spamdetected', 'spam' ),
                                array( 'unknownerror', 'retval' ),
                                array( 'code' => 'nosuchsection', 'info' => 'There is no section section.' ),
                                array( 'code' => 'invalidsection', 'info' => 'The section parameter must be set to an integer or \'new\'' ),
 +                              array( 'code' => 'sectionsnotsupported', 'info' => 'Sections are not supported for this type of page.' ),
 +                              array( 'code' => 'editnotsupported', 'info' => 'Editing of this type of page is not supported using '
 +                                                                                                                              . 'the text based edit API.' ),
 +                              array( 'code' => 'appendnotsupported', 'info' => 'This type of page can not be edited by appending '
 +                                                                                                                              . 'or prepending text.' ),
 +                              array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied to '
 +                                                                                                              . 'the page\'s content model' ),
                                array( 'customcssprotected' ),
                                array( 'customjsprotected' ),
                        )
                                ApiBase::PARAM_TYPE => 'boolean',
                                ApiBase::PARAM_DFLT => false,
                        ),
 +                      'contentformat' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
 +                      ),
 +                      'contentmodel' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getContentModels(),
 +                      )
                );
        }
  
                        'undo' => "Undo this revision. Overrides {$p}text, {$p}prependtext and {$p}appendtext",
                        'undoafter' => 'Undo all revisions from undo to this one. If not set, just undo one revision',
                        'redirect' => 'Automatically resolve redirects',
 +                      'contentformat' => 'Content serialization format used for the input text',
 +                      'contentmodel' => 'Content model of the new content',
                );
        }
  
diff --combined includes/api/ApiMain.php
@@@ -105,7 -105,6 +105,7 @@@ class ApiMain extends ApiBase 
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
  
        private $mCacheMode = 'private';
        private $mCacheControl = array();
+       private $mParamsUsed = array();
  
        /**
         * Constructs an instance of ApiMain that utilizes the module and format specified by $request.
                        // Remove all modules other than login
                        global $wgUser;
  
-                       if ( $this->getRequest()->getVal( 'callback' ) !== null ) {
+                       if ( $this->getVal( 'callback' ) !== null ) {
                                // JSON callback allows cross-site reads.
                                // For safety, strip user credentials.
                                wfDebug( "API: stripping user credentials for JSON callback\n" );
                // clear the output buffer and print just the error information
                ob_start();
  
+               $t = microtime( true );
                try {
                        $this->executeAction();
                } catch ( Exception $e ) {
                        $this->printResult( true );
                }
  
+               // Log the request whether or not there was an error
+               $this->logRequest( microtime( true ) - $t);
                // Send cache headers after any code which might generate an error, to
                // avoid sending public cache headers for errors.
                $this->sendCacheHeaders();
                $module->profileOut();
  
                if ( !$this->mInternalMode ) {
+                       // Report unused params
+                       $this->reportUnusedParams();
                        //append Debug information
                        MWDebug::appendDebugInfoToApiResult( $this->getContext(), $this->getResult() );
  
                }
        }
  
+       /**
+        * Log the preceding request
+        * @param $time Time in seconds
+        */
+       protected function logRequest( $time ) {
+               $request = $this->getRequest();
+               $milliseconds = $time === null ? '?' : round( $time * 1000 );
+               $s = 'API' . 
+                       ' ' . $request->getMethod() .
+                       ' ' . wfUrlencode( str_replace( ' ', '_', $this->getUser()->getName() ) ) .
+                       ' ' . $request->getIP() .
+                       ' T=' . $milliseconds .'ms';
+               foreach ( $this->getParamsUsed() as $name ) {
+                       $value = $request->getVal( $name );
+                       if ( $value === null ) {
+                               continue;
+                       }
+                       $s .= ' ' . $name . '=';
+                       if ( strlen( $value ) > 256 ) {
+                               $encValue = $this->encodeRequestLogValue( substr( $value, 0, 256 ) );
+                               $s .= $encValue . '[...]';
+                       } else {
+                               $s .= $this->encodeRequestLogValue( $value );
+                       }
+               }
+               $s .= "\n";
+               wfDebugLog( 'api', $s, false );
+       }
+       /**
+        * Encode a value in a format suitable for a space-separated log line.
+        */
+       protected function encodeRequestLogValue( $s ) {
+               static $table;
+               if ( !$table ) {
+                       $chars = ';@$!*(),/:';
+                       for ( $i = 0; $i < strlen( $chars ); $i++ ) {
+                               $table[ rawurlencode( $chars[$i] ) ] = $chars[$i];
+                       }
+               }
+               return strtr( rawurlencode( $s ), $table );
+       }
+       /**
+        * Get the request parameters used in the course of the preceding execute() request
+        */
+       protected function getParamsUsed() {
+               return array_keys( $this->mParamsUsed );
+       }
+       /**
+        * Get a request value, and register the fact that it was used, for logging.
+        */
+       public function getVal( $name, $default = null ) {
+               $this->mParamsUsed[$name] = true;
+               return $this->getRequest()->getVal( $name, $default );
+       }
+       /**
+        * Get a boolean request value, and register the fact that the parameter
+        * was used, for logging.
+        */
+       public function getCheck( $name ) {
+               $this->mParamsUsed[$name] = true;
+               return $this->getRequest()->getCheck( $name );          
+       }
+       /**
+        * Report unused parameters, so the client gets a hint in case it gave us parameters we don't know,
+        * for example in case of spelling mistakes or a missing 'g' prefix for generators.
+        */
+       protected function reportUnusedParams() {
+               $paramsUsed = $this->getParamsUsed();
+               $allParams = $this->getRequest()->getValueNames();
+               $unusedParams = array_diff( $allParams, $paramsUsed );
+               if( count( $unusedParams ) ) {
+                       $s = count( $unusedParams ) > 1 ? 's' : '';
+                       $this->setWarning( "Unrecognized parameter$s: '" . implode( $unusedParams, "', '" ) . "'" );
+               }
+       }
        /**
         * Print results using the current printer
         *
  class ApiQueryRevisions extends ApiQueryBase {
  
        private $diffto, $difftotext, $expandTemplates, $generateXML, $section,
 -              $token, $parseContent;
 +              $token, $parseContent, $contentFormat;
  
        public function __construct( $query, $moduleName ) {
                parent::__construct( $query, $moduleName, 'rv' );
        }
  
 -      private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false,
 +      private $fld_ids = false, $fld_flags = false, $fld_timestamp = false, $fld_size = false, $fld_sha1 = false,
                        $fld_comment = false, $fld_parsedcomment = false, $fld_user = false, $fld_userid = false,
 -                      $fld_content = false, $fld_tags = false;
 +                      $fld_content = false, $fld_tags = false, $fld_contentmodel = false;
  
        private $tokenFunctions;
  
                $this->fld_parsedcomment = isset ( $prop['parsedcomment'] );
                $this->fld_size = isset ( $prop['size'] );
                $this->fld_sha1 = isset ( $prop['sha1'] );
 +              $this->fld_contentmodel = isset ( $prop['contentmodel'] );
                $this->fld_userid = isset( $prop['userid'] );
                $this->fld_user = isset ( $prop['user'] );
                $this->token = $params['token'];
  
 +              if ( !empty( $params['contentformat'] ) ) {
 +                      $this->contentFormat = $params['contentformat'];
 +              }
 +
                // Possible indexes used
                $index = array();
  
  
                if ( isset( $prop['content'] ) || !is_null( $this->difftotext ) ) {
                        // For each page we will request, the user must have read rights for that page
+                       $user = $this->getUser();
                        foreach ( $pageSet->getGoodTitles() as $title ) {
-                               if ( !$title->userCan( 'read' ) ) {
+                               if ( !$title->userCan( 'read', $user ) ) {
                                        $this->dieUsage(
                                                'The current user is not allowed to read ' . $title->getPrefixedText(),
                                                'accessdenied' );
                        }
                }
  
 +              if ( $this->fld_contentmodel ) {
 +                      $vals['contentmodel'] = $revision->getContentModel();
 +              }
 +
                if ( $this->fld_comment || $this->fld_parsedcomment ) {
                        if ( $revision->isDeleted( Revision::DELETED_COMMENT ) ) {
                                $vals['commenthidden'] = '';
                        }
                }
  
 -              $text = null;
 +              $content = null;
                global $wgParser;
                if ( $this->fld_content || !is_null( $this->difftotext ) ) {
 -                      $text = $revision->getText();
 +                      $content = $revision->getContent();
                        // Expand templates after getting section content because
                        // template-added sections don't count and Parser::preprocess()
                        // will have less input
                        if ( $this->section !== false ) {
 -                              $text = $wgParser->getSection( $text, $this->section, false );
 -                              if ( $text === false ) {
 +                              $content = $content->getSection( $this->section, false );
 +                              if ( !$content ) {
                                        $this->dieUsage( "There is no section {$this->section} in r" . $revision->getId(), 'nosuchsection' );
                                }
                        }
                }
                if ( $this->fld_content && !$revision->isDeleted( Revision::DELETED_TEXT ) ) {
 +                      $text = null;
 +
                        if ( $this->generateXML ) {
 -                              $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
 -                              $dom = $wgParser->preprocessToDom( $text );
 -                              if ( is_callable( array( $dom, 'saveXML' ) ) ) {
 -                                      $xml = $dom->saveXML();
 +                              if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
 +                                      $t = $content->getNativeData(); # note: don't set $text
 +
 +                                      $wgParser->startExternalParse( $title, ParserOptions::newFromContext( $this->getContext() ), OT_PREPROCESS );
 +                                      $dom = $wgParser->preprocessToDom( $t );
 +                                      if ( is_callable( array( $dom, 'saveXML' ) ) ) {
 +                                              $xml = $dom->saveXML();
 +                                      } else {
 +                                              $xml = $dom->__toString();
 +                                      }
 +                                      $vals['parsetree'] = $xml;
                                } else {
 -                                      $xml = $dom->__toString();
 +                                      $this->setWarning( "Conversion to XML is supported for wikitext only, " .
 +                                                                              $title->getPrefixedDBkey() .
 +                                                                              " uses content model " . $content->getModel() . ")" );
                                }
 -                              $vals['parsetree'] = $xml;
 -
                        }
 +
                        if ( $this->expandTemplates && !$this->parseContent ) {
 -                              $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              #XXX: implement template expansion for all content types in ContentHandler?
 +                              if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
 +                                      $text = $content->getNativeData();
 +
 +                                      $text = $wgParser->preprocess( $text, $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              } else {
 +                                      $this->setWarning( "Template expansion is supported for wikitext only, " .
 +                                              $title->getPrefixedDBkey() .
 +                                              " uses content model " . $content->getModel() . ")" );
 +
 +                                      $text = false;
 +                              }
                        }
                        if ( $this->parseContent ) {
 -                              $text = $wgParser->parse( $text, $title, ParserOptions::newFromContext( $this->getContext() ) )->getText();
 +                              $po = $content->getParserOutput( $title, ParserOptions::newFromContext( $this->getContext() ) );
 +                              $text = $po->getText();
 +                      }
 +
 +                      if ( $text === null ) {
 +                              $format = $this->contentFormat ? $this->contentFormat : $content->getDefaultFormat();
 +
 +                              if ( !$content->isSupportedFormat( $format ) ) {
 +                                      $model = $content->getModel();
 +                                      $name = $title->getPrefixedDBkey();
 +
 +                                      $this->dieUsage( "The requested format {$this->contentFormat} is not supported ".
 +                                                                      "for content model $model used by $name", 'badformat' );
 +                              }
 +
 +                              $text = $content->serialize( $format );
 +                              $vals['contentformat'] = $format;
 +                      }
 +
 +                      if ( $text !== false ) {
 +                              ApiResult::setContent( $vals, $text );
                        }
 -                      ApiResult::setContent( $vals, $text );
                } elseif ( $this->fld_content ) {
                        $vals['texthidden'] = '';
                }
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
 +                              $handler = $revision->getContentHandler();
 +
                                if ( !is_null( $this->difftotext ) ) {
 -                                      $engine = new DifferenceEngine( $context );
 -                                      $engine->setText( $text, $this->difftotext );
 +                                      $model = $title->getContentModel();
 +
 +                                      if ( $this->contentFormat
 +                                              && !ContentHandler::getForModelID( $model )->isSupportedFormat( $this->contentFormat ) ) {
 +
 +                                              $name = $title->getPrefixedDBkey();
 +
 +                                              $this->dieUsage( "The requested format {$this->contentFormat} is not supported for ".
 +                                                                                      "content model $model used by $name", 'badformat' );
 +                                      }
 +
 +                                      $difftocontent = ContentHandler::makeContent( $this->difftotext, $title, $model, $this->contentFormat );
 +
 +                                      $engine = $handler->createDifferenceEngine( $context );
 +                                      $engine->setContent( $content, $difftocontent );
                                } else {
 -                                      $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
 +                                      $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
                                        'userid',
                                        'size',
                                        'sha1',
 +                                      'contentmodel',
                                        'comment',
                                        'parsedcomment',
                                        'content',
                        'continue' => null,
                        'diffto' => null,
                        'difftotext' => null,
 +                      'contentformat' => array(
 +                              ApiBase::PARAM_TYPE => ContentHandler::getAllContentFormats(),
 +                              ApiBase::PARAM_DFLT => null
 +                      ),
                );
        }
  
                                ' userid         - User id of revision creator',
                                ' size           - Length (bytes) of the revision',
                                ' sha1           - SHA-1 (base 16) of the revision',
 +                              ' contentmodel   - Content model id',
                                ' comment        - Comment by the user for revision',
                                ' parsedcomment  - Parsed comment by the user for the revision',
                                ' content        - Text of the revision',
                        'difftotext' => array( 'Text to diff each revision to. Only diffs a limited number of revisions.',
                                "Overrides {$p}diffto. If {$p}section is set, only that section will be diffed against this text" ),
                        'tag' => 'Only list revisions tagged with this tag',
 +                      'contentformat' => 'Serialization format used for difftotext and expected for output of content',
                );
        }
  
        public function getPossibleErrors() {
                return array_merge( parent::getPossibleErrors(), array(
                        array( 'nosuchrevid', 'diffto' ),
 -                      array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options (limit, startid, endid, dirNewer, start, end).' ),
 -                      array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, but the limit, startid, endid, dirNewer, user, excludeuser, start and end parameters may only be used on a single page.' ),
 +                      array( 'code' => 'revids', 'info' => 'The revids= parameter may not be used with the list options '
 +                                                                                                      . '(limit, startid, endid, dirNewer, start, end).' ),
 +                      array( 'code' => 'multpages', 'info' => 'titles, pageids or a generator was used to supply multiple pages, '
 +                                                                                                      . ' but the limit, startid, endid, dirNewer, user, excludeuser, '
 +                                                                                                      . 'start and end parameters may only be used on a single page.' ),
                        array( 'code' => 'diffto', 'info' => 'rvdiffto must be set to a non-negative number, "prev", "next" or "cur"' ),
                        array( 'code' => 'badparams', 'info' => 'start and startid cannot be used together' ),
                        array( 'code' => 'badparams', 'info' => 'end and endid cannot be used together' ),
                        array( 'code' => 'badparams', 'info' => 'user and excludeuser cannot be used together' ),
                        array( 'code' => 'nosuchsection', 'info' => 'There is no section section in rID' ),
 +                      array( 'code' => 'badformat', 'info' => 'The requested serialization format can not be applied '
 +                                                                                                      . ' to the page\'s content model' ),
                ) );
        }
  
index 465a402,0000000..9526520
mode 100644,000000..100644
--- /dev/null
@@@ -1,289 -1,0 +1,289 @@@
-                               ->inContentLanguage()->params( $header )->text();
 +<?php
 +/**
 + * @since 1.21
 + */
 +class WikitextContent extends TextContent {
 +
 +      public function __construct( $text ) {
 +              parent::__construct( $text, CONTENT_MODEL_WIKITEXT );
 +      }
 +
 +      /**
 +       * @see Content::getSection()
 +       */
 +      public function getSection( $section ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $sect = $wgParser->getSection( $text, $section, false );
 +
 +              return  new WikitextContent( $sect );
 +      }
 +
 +      /**
 +       * @see Content::replaceSection()
 +       */
 +      public function replaceSection( $section, Content $with, $sectionTitle = '' ) {
 +              wfProfileIn( __METHOD__ );
 +
 +              $myModelId = $this->getModel();
 +              $sectionModelId = $with->getModel();
 +
 +              if ( $sectionModelId != $myModelId  ) {
 +                      throw new MWException( "Incompatible content model for section: " .
 +                              "document uses $myModelId but " .
 +                              "section uses $sectionModelId." );
 +              }
 +
 +              $oldtext = $this->getNativeData();
 +              $text = $with->getNativeData();
 +
 +              if ( $section === '' ) {
 +                      return $with; # XXX: copy first?
 +              } if ( $section == 'new' ) {
 +                      # Inserting a new section
 +                      $subject = $sectionTitle ? wfMessage( 'newsectionheaderdefaultlevel' )
 +                              ->rawParams( $sectionTitle )->inContentLanguage()->text() . "\n\n" : '';
 +                      if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 +                              $text = strlen( trim( $oldtext ) ) > 0
 +                                      ? "{$oldtext}\n\n{$subject}{$text}"
 +                                      : "{$subject}{$text}";
 +                      }
 +              } else {
 +                      # Replacing an existing section; roll out the big guns
 +                      global $wgParser;
 +
 +                      $text = $wgParser->replaceSection( $oldtext, $section, $text );
 +              }
 +
 +              $newContent = new WikitextContent( $text );
 +
 +              wfProfileOut( __METHOD__ );
 +              return $newContent;
 +      }
 +
 +      /**
 +       * Returns a new WikitextContent object with the given section heading
 +       * prepended.
 +       *
 +       * @param $header string
 +       * @return Content
 +       */
 +      public function addSectionHeader( $header ) {
 +              $text = wfMessage( 'newsectionheaderdefaultlevel' )
++                      ->rawParams( $header )->inContentLanguage()->text();
 +              $text .= "\n\n";
 +              $text .= $this->getNativeData();
 +
 +              return new WikitextContent( $text );
 +      }
 +
 +      /**
 +       * Returns a Content object with pre-save transformations applied using
 +       * Parser::preSaveTransform().
 +       *
 +       * @param $title Title
 +       * @param $user User
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
 +
 +              return new WikitextContent( $pst );
 +      }
 +
 +      /**
 +       * Returns a Content object with preload transformations applied (or this
 +       * object if no transformations apply).
 +       *
 +       * @param $title Title
 +       * @param $popts ParserOptions
 +       * @return Content
 +       */
 +      public function preloadTransform( Title $title, ParserOptions $popts ) {
 +              global $wgParser;
 +
 +              $text = $this->getNativeData();
 +              $plt = $wgParser->getPreloadText( $text, $title, $popts );
 +
 +              return new WikitextContent( $plt );
 +      }
 +
 +      /**
 +       * Implement redirect extraction for wikitext.
 +       *
 +       * @return null|Title
 +       *
 +       * @note: migrated here from Title::newFromRedirectInternal()
 +       *
 +       * @see Content::getRedirectTarget
 +       * @see AbstractContent::getRedirectTarget
 +       */
 +      public function getRedirectTarget() {
 +              global $wgMaxRedirects;
 +              if ( $wgMaxRedirects < 1 ) {
 +                      // redirects are disabled, so quit early
 +                      return null;
 +              }
 +              $redir = MagicWord::get( 'redirect' );
 +              $text = trim( $this->getNativeData() );
 +              if ( $redir->matchStartAndRemove( $text ) ) {
 +                      // Extract the first link and see if it's usable
 +                      // Ensure that it really does come directly after #REDIRECT
 +                      // Some older redirects included a colon, so don't freak about that!
 +                      $m = array();
 +                      if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
 +                              // Strip preceding colon used to "escape" categories, etc.
 +                              // and URL-decode links
 +                              if ( strpos( $m[1], '%' ) !== false ) {
 +                                      // Match behavior of inline link parsing here;
 +                                      $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
 +                              }
 +                              $title = Title::newFromText( $m[1] );
 +                              // If the title is a redirect to bad special pages or is invalid, return null
 +                              if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
 +                                      return null;
 +                              }
 +                              return $title;
 +                      }
 +              }
 +              return null;
 +      }
 +
 +      /**
 +       * @see   Content::updateRedirect()
 +       *
 +       * This implementation replaces the first link on the page with the given new target
 +       * if this Content object is a redirect. Otherwise, this method returns $this.
 +       *
 +       * @since 1.21
 +       *
 +       * @param Title $target
 +       *
 +       * @return Content a new Content object with the updated redirect (or $this if this Content object isn't a redirect)
 +       */
 +      public function updateRedirect( Title $target ) {
 +              if ( !$this->isRedirect() ) {
 +                      return $this;
 +              }
 +
 +              # Fix the text
 +              # Remember that redirect pages can have categories, templates, etc.,
 +              # so the regex has to be fairly general
 +              $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
 +                      '[[' . $target->getFullText() . ']]',
 +                      $this->getNativeData(), 1 );
 +
 +              return new WikitextContent( $newText );
 +      }
 +
 +      /**
 +       * Returns true if this content is not a redirect, and this content's text
 +       * is countable according to the criteria defined by $wgArticleCountMethod.
 +       *
 +       * @param $hasLinks Bool  if it is known whether this content contains
 +       *    links, provide this information here, to avoid redundant parsing to
 +       *    find out.
 +       * @param $title null|\Title
 +       *
 +       * @internal param \IContextSource $context context for parsing if necessary
 +       *
 +       * @return bool True if the content is countable
 +       */
 +      public function isCountable( $hasLinks = null, Title $title = null ) {
 +              global $wgArticleCountMethod;
 +
 +              if ( $this->isRedirect( ) ) {
 +                      return false;
 +              }
 +
 +              $text = $this->getNativeData();
 +
 +              switch ( $wgArticleCountMethod ) {
 +                      case 'any':
 +                              return true;
 +                      case 'comma':
 +                              return strpos( $text,  ',' ) !== false;
 +                      case 'link':
 +                              if ( $hasLinks === null ) { # not known, find out
 +                                      if ( !$title ) {
 +                                              $context = RequestContext::getMain();
 +                                              $title = $context->getTitle();
 +                                      }
 +
 +                                      $po = $this->getParserOutput( $title, null, null, false );
 +                                      $links = $po->getLinks();
 +                                      $hasLinks = !empty( $links );
 +                              }
 +
 +                              return $hasLinks;
 +              }
 +
 +              return false;
 +      }
 +
 +      public function getTextForSummary( $maxlength = 250 ) {
 +              $truncatedtext = parent::getTextForSummary( $maxlength );
 +
 +              # clean up unfinished links
 +              # XXX: make this optional? wasn't there in autosummary, but required for
 +              # deletion summary.
 +              $truncatedtext = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $truncatedtext );
 +
 +              return $truncatedtext;
 +      }
 +
 +      /**
 +       * Returns a ParserOutput object resulting from parsing the content's text
 +       * using $wgParser.
 +       *
 +       * @since    1.21
 +       *
 +       * @param $content Content the content to render
 +       * @param $title \Title
 +       * @param $revId null
 +       * @param $options null|ParserOptions
 +       * @param $generateHtml bool
 +       *
 +       * @internal param \IContextSource|null $context
 +       * @return ParserOutput representing the HTML form of the text
 +       */
 +      public function getParserOutput( Title $title,
 +              $revId = null,
 +              ParserOptions $options = null, $generateHtml = true
 +      ) {
 +              global $wgParser;
 +
 +              if ( !$options ) {
 +                      //NOTE: use canonical options per default to produce cacheable output
 +                      $options = $this->getContentHandler()->makeParserOptions( 'canonical' );
 +              }
 +
 +              $po = $wgParser->parse( $this->getNativeData(), $title, $options, true, true, $revId );
 +              return $po;
 +      }
 +
 +      protected function getHtml() {
 +              throw new MWException(
 +                      "getHtml() not implemented for wikitext. "
 +                              . "Use getParserOutput()->getText()."
 +              );
 +      }
 +
 +      /**
 +       * @see  Content::matchMagicWord()
 +       *
 +       * This implementation calls $word->match() on the this TextContent object's text.
 +       *
 +       * @param MagicWord $word
 +       *
 +       * @return bool whether this Content object matches the given magic word.
 +       */
 +      public function matchMagicWord( MagicWord $word ) {
 +              return $word->match( $this->getNativeData() );
 +      }
 +}
@@@ -38,7 -38,7 +38,7 @@@ class DifferenceEngine extends ContextS
         * @private
         */
        var $mOldid, $mNewid;
 -      var $mOldtext, $mNewtext;
 +      var $mOldContent, $mNewContent;
        protected $mDiffLang;
  
        /**
                # we'll use the application/x-external-editor interface to call
                # an external diff tool like kompare, kdiff3, etc.
                if ( ExternalEdit::useExternalEngine( $this->getContext(), 'diff' ) ) {
 +                      //TODO: come up with a good solution for non-text content here.
 +                      //      at least, the content format needs to be passed to the client somehow.
 +                      //      Currently, action=raw will just fail for non-text content.
 +
                        $urls = array(
                                'File' => array( 'Extension' => 'wiki', 'URL' =>
                                        # This should be mOldPage, but it may not be set, see below.
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
  
 +                      // NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                        if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
 -                              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 -                                      // Wrap the whole lot in a <pre> and don't parse
 -                                      $m = array();
 -                                      preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
 -                                      $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                                      $out->addHTML( htmlspecialchars( $this->mNewtext ) );
 -                                      $out->addHTML( "\n</pre>\n" );
 +                              if ( ContentHandler::runLegacyHooks( 'ShowRawCssJs', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                                      // NOTE: deprecated hook, B/C only
 +                                      // use the content object's own rendering
 +                                      $po = $this->mNewRev->getContent()->getParserOutput( $this->mNewRev->getTitle(), $this->mNewRev->getId() );
 +                                      $out->addHTML( $po->getText() );
                                }
 -                      } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 +                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                              // Handled by extension
 +                      } elseif( !ContentHandler::runLegacyHooks( 'ArticleViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                              // NOTE: deprecated hook, B/C only
                                // Handled by extension
                        } else {
                                // Normal page
                                        $wikiPage = WikiPage::factory( $this->mNewPage );
                                }
  
 -                              $parserOptions = $wikiPage->makeParserOptions( $this->getContext() );
 -
 -                              if ( !$this->mNewRev->isCurrent() ) {
 -                                      $parserOptions->setEditSection( false );
 -                              }
 -
 -                              $parserOutput = $wikiPage->getParserOutput( $parserOptions, $this->mNewid );
 +                              $parserOutput = $this->getParserOutput( $wikiPage, $this->mNewRev );
  
                                # WikiPage::getParserOutput() should not return false, but just in case
                                if( $parserOutput ) {
                wfProfileOut( __METHOD__ );
        }
  
 +      protected function getParserOutput( WikiPage $page, Revision $rev ) {
 +              $parserOptions = $page->makeParserOptions( $this->getContext() );
 +
 +              if ( !$rev->isCurrent() || !$rev->getTitle()->quickUserCan( "edit" ) ) {
 +                      $parserOptions->setEditSection( false );
 +              }
 +
 +              $parserOutput = $page->getParserOutput( $parserOptions, $rev->getId() );
 +              return $parserOutput;
 +      }
 +
        /**
         * Get the diff text, send it to the OutputPage object
         * Returns false if the diff could not be generated, otherwise returns true
                        return false;
                }
  
 -              $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
 +              $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
  
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
                }
        }
  
 +      /**
 +       * Generate a diff, no caching.
 +       *
 +       * Subclasses may override this to provide a
 +       *
 +       * @param $old Content: old content
 +       * @param $new Content: new content
 +       *
 +       * @since 1.21
 +       */
 +      function generateContentDiffBody( Content $old, Content $new ) {
 +              if ( !( $old instanceof TextContent ) ) {
 +                      throw new MWException( "Diff not implemented for " . get_class( $old ) . "; "
 +                                                              . "override generateContentDiffBody to fix this." );
 +              }
 +
 +              if ( !( $new instanceof TextContent ) ) {
 +                      throw new MWException( "Diff not implemented for " . get_class( $new ) . "; "
 +                              . "override generateContentDiffBody to fix this." );
 +              }
 +
 +              $otext = $old->serialize();
 +              $ntext = $new->serialize();
 +
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
        /**
         * Generate a diff, no caching
         *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
 -       * @return bool|string
 +       * @deprecated since 1.21, use generateContentDiffBody() instead!
         */
        function generateDiffBody( $otext, $ntext ) {
 +              wfDeprecated( __METHOD__, "1.21" );
 +
 +              return $this->generateTextDiffBody( $otext, $ntext );
 +      }
 +
 +      /**
 +       * Generate a diff, no caching
 +       *
 +       * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
 +       *
 +       * @param $otext String: old text, must be already segmented
 +       * @param $ntext String: new text, must be already segmented
 +       * @return bool|string
 +       */
 +      function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
  
                wfProfileIn( __METHOD__ );
         *        the visibility of the revision and a link to edit the page.
         * @return String HTML fragment
         */
 -      private function getRevisionHeader( Revision $rev, $complete = '' ) {
 +      protected function getRevisionHeader( Revision $rev, $complete = '' ) {
                $lang = $this->getLanguage();
                $user = $this->getUser();
                $revtimestamp = $rev->getTimestamp();
  
        /**
         * Use specified text instead of loading from the database
 +       * @deprecated since 1.21, use setContent() instead.
         */
        function setText( $oldText, $newText ) {
 -              $this->mOldtext = $oldText;
 -              $this->mNewtext = $newText;
 +              wfDeprecated( __METHOD__, "1.21" );
 +
 +              $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
 +              $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
 +
 +              $this->setContent( $oldContent, $newContent );
 +      }
 +
 +      /**
 +       * Use specified text instead of loading from the database
 +       * @since 1.21
 +       */
 +      function setContent( Content $oldContent, Content $newContent ) {
 +              $this->mOldContent = $oldContent;
 +              $this->mNewContent = $newContent;
 +
                $this->mTextLoaded = 2;
                $this->mRevisionsLoaded = true;
        }
                        return false;
                }
                if ( $this->mOldRev ) {
-                       $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
 -                      $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
 -                      if ( $this->mOldtext === false ) {
++                      $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 +                      if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
-                       $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 -                      $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
 -                      if ( $this->mNewtext === false ) {
++                      $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
 +                      if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
-               $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 -              $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER, $this->getUser() );
++              $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER, $this->getUser() );
                return true;
        }
  }
@@@ -1006,7 -1006,7 +1006,7 @@@ class LocalFile extends File 
        {
                $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
  
-               if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
+               if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp ) ) {
                        return false;
                }
  
                } else {
                        # New file; create the description page.
                        # There's already a log entry, so don't make a second RC entry
 -                      # Squid and file cache for the description page are purged by doEdit.
 -                      $status = $wikiPage->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
 +                      # Squid and file cache for the description page are purged by doEditContent.
 +                      $content = ContentHandler::makeContent( $pageText, $descTitle );
 +                      $status = $wikiPage->doEditContent( $content, $comment, EDIT_NEW | EDIT_SUPPRESS_RC, false, $user );
  
                        if ( isset( $status->value['revision'] ) ) { // XXX; doEdit() uses a transaction
                                $dbw->begin();
                global $wgParser;
                $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
                if ( !$revision ) return false;
 -              $text = $revision->getText();
 -              if ( !$text ) return false;
 -              $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
 +              $content = $revision->getContent();
 +              if ( !$content ) return false;
 +              $pout = $content->getParserOutput( $this->title, null, new ParserOptions() );
                return $pout->getText();
        }
  
@@@ -206,7 -206,6 +206,7 @@@ class MysqlUpdater extends DatabaseUpda
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
 +
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ufg_group-length-increase.sql' ),
  
                        // 1.20
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
  
                        // 1.21
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
+                       array( 'dropField', 'site_stats',   'ss_admins',        'patch-drop-ss_admins.sql' ),
+                       array( 'dropField', 'recentchanges', 'rc_moved_to_title',            'patch-rc_moved.sql' ),
                );
        }
  
@@@ -85,7 -85,6 +85,7 @@@ class SqliteUpdater extends DatabaseUpd
                        array( 'modifyField', 'user_groups', 'ug_group', 'patch-ug_group-length-increase.sql' ),
                        array( 'addField',      'uploadstash',  'us_chunk_inx',         'patch-uploadstash_chunk.sql' ),
                        array( 'addfield', 'job',           'job_timestamp',    'patch-jobs-add-timestamp.sql' ),
 +
                        array( 'modifyField', 'user_former_groups', 'ufg_group', 'patch-ug_group-length-increase.sql' ),
  
                        // 1.20
                        array( 'dropField', 'category',     'cat_hidden',       'patch-cat_hidden.sql' ),
  
                        // 1.21
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
+                       array( 'dropField', 'site_stats',   'ss_admins',        'patch-drop-ss_admins.sql' ),
+                       array( 'dropField', 'recentchanges', 'rc_moved_to_title', 'patch-rc_moved.sql' ),
                );
        }
  
@@@ -27,7 -27,7 +27,7 @@@
   * @ingroup JobQueue
   */
  class DoubleRedirectJob extends Job {
-       var $reason, $redirTitle, $destTitleText;
+       var $reason, $redirTitle;
  
        /**
         * @var User
@@@ -77,7 -77,6 +77,6 @@@
                parent::__construct( 'fixDoubleRedirect', $title, $params, $id );
                $this->reason = $params['reason'];
                $this->redirTitle = Title::newFromText( $params['redirTitle'] );
-               $this->destTitleText = !empty( $params['destTitle'] ) ? $params['destTitle'] : '';
        }
  
        /**
@@@ -94,8 -93,8 +93,8 @@@
                        wfDebug( __METHOD__.": target redirect already deleted, ignoring\n" );
                        return true;
                }
 -              $text = $targetRev->getText();
 -              $currentDest = Title::newFromRedirect( $text );
 +              $content = $targetRev->getContent();
 +              $currentDest = $content->getRedirectTarget();
                if ( !$currentDest || !$currentDest->equals( $this->redirTitle ) ) {
                        wfDebug( __METHOD__.": Redirect has changed since the job was queued\n" );
                        return true;
  
                # Check for a suppression tag (used e.g. in periodically archived discussions)
                $mw = MagicWord::get( 'staticredirect' );
 -              if ( $mw->match( $text ) ) {
 +              if ( $content->matchMagicWord( $mw ) ) {
                        wfDebug( __METHOD__.": skipping: suppressed with __STATICREDIRECT__\n" );
                        return true;
                }
  
                # Preserve fragment (bug 14904)
                $newTitle = Title::makeTitle( $newTitle->getNamespace(), $newTitle->getDBkey(),
-                       $currentDest->getFragment() );
+                       $currentDest->getFragment(), $newTitle->getInterwiki() );
  
                # Fix the text
 -              # Remember that redirect pages can have categories, templates, etc.,
 -              # so the regex has to be fairly general
 -              $newText = preg_replace( '/ \[ \[  [^\]]*  \] \] /x',
 -                      '[[' . $newTitle->getFullText() . ']]',
 -                      $text, 1 );
 -
 -              if ( $newText === $text ) {
 -                      $this->setLastError( 'Text unchanged???' );
 +              $newContent = $content->updateRedirect( $newTitle );
 +
 +              if ( $newContent->equals( $content ) ) {
 +                      $this->setLastError( 'Content unchanged???' );
                        return false;
                }
  
                $reason = wfMessage( 'double-redirect-fixed-' . $this->reason,
                        $this->redirTitle->getPrefixedText(), $newTitle->getPrefixedText()
                )->inContentLanguage()->text();
 -              $article->doEdit( $newText, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() );
 +              $article->doEditContent( $newContent, $reason, EDIT_UPDATE | EDIT_SUPPRESS_RC, false, $this->getUser() );
                $wgUser = $oldUser;
  
                return true;
                        }
                        $seenTitles[$titleText] = true;
  
+                       if ( $title->getInterwiki() ) {
+                               // If the target is interwiki, we have to break early (bug 40352).
+                               // Otherwise it will look up a row in the local page table
+                               // with the namespace/page of the interwiki target which can cause
+                               // unexpected results (e.g. X -> foo:Bar -> Bar -> .. )
+                               break;
+                       }
                        $row = $dbw->selectRow(
                                array( 'redirect', 'page' ),
-                               array( 'rd_namespace', 'rd_title' ),
+                               array( 'rd_namespace', 'rd_title', 'rd_interwiki' ),
                                array(
                                        'rd_from=page_id',
                                        'page_namespace' => $title->getNamespace(),
                                # No redirect from here, chain terminates
                                break;
                        } else {
-                               $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title );
+                               $dest = $title = Title::makeTitle( $row->rd_namespace, $row->rd_title, '', $row->rd_interwiki );
                        }
                }
                return $dest;
@@@ -200,6 -200,13 +200,13 @@@ class Parser 
         */
        var $mUniqPrefix;
  
+       /**
+        * @var Array with the language name of each language link (i.e. the
+        * interwiki prefix) in the key, value arbitrary. Used to avoid sending
+        * duplicate language links to the ParserOutput.
+        */
+       var $mLangLinkLanguages;
        /**
         * Constructor
         *
                        $this->mRevisionId = $this->mRevisionUser = null;
                $this->mVarCache = array();
                $this->mUser = null;
+               $this->mLangLinkLanguages = array();
  
                /**
                 * Prefix for temporary replacement strings for the multipass parser.
                                $PFreport;
                        wfRunHooks( 'ParserLimitReport', array( $this, &$limitReport ) );
                        $text .= "\n<!-- \n$limitReport-->\n";
+                       if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
+                               wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
+                                       $this->mTitle->getPrefixedDBkey() );
+                       }
                }
                $this->mOutput->setText( $text );
  
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
                                if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && Language::fetchLanguageName( $iw, null, 'mw' ) ) {
-                                       // FIXME: the above check prevents links to sites with identifiers that are not language codes
-                                       $this->mOutput->addLanguageLink( $nt->getFullText() );
++                                      // XXX: the above check prevents links to sites with identifiers that are not language codes
++
+                                       # Bug 24502: filter duplicates
+                                       if ( !isset( $this->mLangLinkLanguages[$iw] ) ) {
+                                               $this->mLangLinkLanguages[$iw] = true;
+                                               $this->mOutput->addLanguageLink( $nt->getFullText() );
+                                       }
++
                                        $s = rtrim( $s . $prefix );
                                        $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
                                        wfProfileOut( __METHOD__."-interwiki" );
                        }
  
                        if ( $rev ) {
 -                              $text = $rev->getText();
 +                              $content = $rev->getContent();
 +                              $text = $content->getWikitextForTransclusion();
 +
 +                              if ( $text === false || $text === null ) {
 +                                      $text = false;
 +                                      break;
 +                              }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
                                        $text = false;
                                        break;
                                }
 +                              $content = $message->content();
                                $text = $message->plain();
                        } else {
                                break;
                        }
 -                      if ( $text === false ) {
 +                      if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
 -                      $title = Title::newFromRedirect( $text );
 +                      $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
                        return $obj->tc_contents;
                }
  
-               $text = Http::get( $url );
-               if ( !$text ) {
+               $req = MWHttpRequest::factory( $url );
+               $status = $req->execute(); // Status object
+               if ( $status->isOK() ) {
+                       $text = $req->getContent();
+               } elseif ( $req->getStatus() != 200 ) { // Though we failed to fetch the content, this status is useless.
+                       return wfMessage( 'scarytranscludefailed-httpstatus', $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+               } else {
                        return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
                }
  
                        $safeHeadline = $this->mStripState->unstripBoth( $safeHeadline );
  
                        # Strip out HTML (first regex removes any tag not allowed)
-                       # Allowed tags are <sup> and <sub> (bug 8393), <i> (bug 26375) and <b> (r105284)
-                       # We strip any parameter from accepted tags (second regex)
+                       # Allowed tags are:
+                       # * <sup> and <sub> (bug 8393)
+                       # * <i> (bug 26375)
+                       # * <b> (r105284)
+                       # * <span dir="rtl"> and <span dir="ltr"> (bug 35167)
+                       #
+                       # We strip any parameter from accepted tags (second regex), except dir="rtl|ltr" from <span>,
+                       # to allow setting directionality in toc items.
                        $tocline = preg_replace(
-                               array( '#<(?!/?(sup|sub|i|b)(?: [^>]*)?>).*?'.'>#', '#<(/?(sup|sub|i|b))(?: .*?)?'.'>#' ),
+                               array( '#<(?!/?(span|sup|sub|i|b)(?: [^>]*)?>).*?'.'>#', '#<(/?(?:span(?: dir="(?:rtl|ltr)")?|sup|sub|i|b))(?: .*?)?'.'>#' ),
                                array( '',                          '<$1>' ),
                                $safeHeadline
                        );
@@@ -50,7 -50,7 +50,7 @@@ class ParserOutput extends CacheTime 
                $mTimestamp;                  # Timestamp of the revision
                private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
                private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
 -              private $mSecondaryDataUpdates = array(); # List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
 +              private $mSecondaryDataUpdates = array(); # List of DataUpdate, used to save info from the page somewhere else.
  
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
  
                return (bool)$this->mNewSection;
        }
  
+       /**
+        * Checks, if a url is pointing to the own server
+        *
+        * @param $internal String the server to check against
+        * @param $url String the url to check
+        * @return bool
+        */
+       static function isLinkInternal( $internal, $url ) {
+               return (bool)preg_match( '/^' .
+                       # If server is proto relative, check also for http/https links
+                       ( substr( $internal, 0, 2 ) === '//' ? '(?:https?:)?' : '' ) .
+                       preg_quote( $internal, '/' ) .
+                       # check for query/path/anchor or end of link in each case
+                       '(?:[\?\/\#]|$)/i',
+                       $url
+               );
+       }
        function addExternalLink( $url ) {
                # We don't register links pointing to our own server, unless... :-)
                global $wgServer, $wgRegisterInternalExternals;
-               if( $wgRegisterInternalExternals or stripos($url,$wgServer.'/')!==0)
+               $registerExternalLink = true;
+               if( !$wgRegisterInternalExternals ) {
+                       $registerExternalLink = !self::isLinkInternal( $wgServer, $url );
+               }
+               if( $registerExternalLink ) {
                        $this->mExternalLinks[$url] = 1;
+               }
        }
  
        /**
         * extracted from the page's content, including a LinksUpdate object for all links stored in
         * this ParserOutput object.
         *
 +       * @note: Avoid using this method directly, use ContentHandler::getSecondaryDataUpdates() instead! The content
 +       *        handler may provide additional update objects.
 +       *
         * @since 1.20
         *
 -       * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
 +       * @param $title Title The title of the page we're updating. If not given, a title object will be created
 +       *                      based on $this->getTitleText()
         * @param $recursive Boolean: queue jobs for recursive updates?
         *
         * @return Array. An array of instances of DataUpdate
@@@ -68,12 -68,6 +68,6 @@@ abstract class ResourceLoaderWikiModul
         * @return null|string
         */
        protected function getContent( $title ) {
-               if ( $title->getNamespace() === NS_MEDIAWIKI ) {
-                       // The first "true" is to use the database, the second is to use the content langue
-                       // and the last one is to specify the message key already contains the language in it ("/de", etc.)
-                       $text = MessageCache::singleton()->get( $title->getDBkey(), true, true, true );
-                       return $text === false ? '' : $text;
-               }
                if ( !$title->isCssJsSubpage() && !$title->isCssOrJsPage() ) {
                        return null;
                }
                if ( !$revision ) {
                        return null;
                }
 -              return $revision->getRawText();
 +
 +              $content = $revision->getContent( Revision::RAW );
 +              $model = $content->getModel();
 +
 +              if ( $model !== CONTENT_MODEL_CSS && $model !== CONTENT_MODEL_JAVASCRIPT ) {
 +                      wfDebug( __METHOD__ . "bad content model #$model for JS/CSS page!\n" );
 +                      return null;
 +              }
 +
 +              return $content->getNativeData(); //NOTE: this is safe, we know it's JS or CSS
        }
  
        /* Methods */
                        }
                        $style = CSSMin::remap( $style, false, $wgScriptPath, true );
                        if ( !isset( $styles[$media] ) ) {
-                               $styles[$media] = '';
+                               $styles[$media] = array();
                        }
                        if ( strpos( $titleText, '*/' ) === false ) {
-                               $styles[$media] .=  "/* $titleText */\n";
+                               $style =  "/* $titleText */\n" . $style;
                        }
-                       $styles[$media] .= $style . "\n";
+                       $styles[$media][] = $style;
                }
                return $styles;
        }
@@@ -505,19 -505,6 +505,6 @@@ class SearchEngine 
                        return $wgCanonicalServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace=' . $ns;
                }
        }
-       /**
-        * Get internal MediaWiki Suggest template
-        *
-        * @return String
-        */
-       public static function getMWSuggestTemplate() {
-               global $wgMWSuggestTemplate, $wgServer;
-               if ( $wgMWSuggestTemplate )
-                       return $wgMWSuggestTemplate;
-               else
-                       return $wgServer . wfScript( 'api' ) . '?action=opensearch&search={searchTerms}&namespace={namespaces}&suggest';
-       }
  }
  
  /**
@@@ -804,14 -791,11 +791,14 @@@ class SearchResult 
         */
        protected function initText() {
                if ( !isset( $this->mText ) ) {
 -                      if ( $this->mRevision != null )
 -                              $this->mText = $this->mRevision->getText();
 -                      else // TODO: can we fetch raw wikitext for commons images?
 +                      if ( $this->mRevision != null ) {
 +                              //TODO: if we could plug in some code that knows about special content models *and* about
 +                              //      special features of the search engine, the search could benefit.
 +                              $content = $this->mRevision->getContent();
 +                              $this->mText = $content->getTextForSearchIndex();
 +                      } else { // TODO: can we fetch raw wikitext for commons images?
                                $this->mText = '';
 -
 +                      }
                }
        }
  
        function getTextSnippet( $terms ) {
                global $wgUser, $wgAdvancedSearchHighlighting;
                $this->initText();
 +
 +              // TODO: make highliter take a content object. Make ContentHandler a factory for SearchHighliter.
                list( $contextlines, $contextchars ) = SearchEngine::userHighlightPrefs( $wgUser );
                $h = new SearchHighlighter();
                if ( $wgAdvancedSearchHighlighting )
@@@ -32,16 -32,7 +32,16 @@@ class PageArchive 
         * @var Title
         */
        protected $title;
 -      var $fileStatus;
 +
 +      /**
 +       * @var Status
 +       */
 +      protected $fileStatus;
 +
 +      /**
 +       * @var Status
 +       */
 +      protected $revisionStatus;
  
        function __construct( $title ) {
                if( is_null( $title ) ) {
         * @return ResultWrapper
         */
        function listRevisions() {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 +                      'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $res = $dbr->select( 'archive',
 -                      array(
 -                              'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 -                              'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        __METHOD__,
         * @return Revision
         */
        function getRevision( $timestamp ) {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_len',
 +                      'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $row = $dbr->selectRow( 'archive',
 -                      array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_len',
 -                              'ar_sha1',
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
                        __METHOD__ );
                if( $row ) {
 -                      return Revision::newFromArchiveRow( $row, array( 'page' => $this->title->getArticleID() ) );
 +                      return Revision::newFromArchiveRow( $row, array( 'title' => $this->title ) );
                } else {
                        return null;
                }
         * on success, false on failure
         */
        function undelete( $timestamps, $comment = '', $fileVersions = array(), $unsuppress = false, User $user = null ) {
-               global $wgUser;
                // If both the set of text revisions and file revisions are empty,
                // restore everything. Otherwise, just restore the requested items.
                $restoreAll = empty( $timestamps ) && empty( $fileVersions );
                if( $restoreFiles && $this->title->getNamespace() == NS_FILE ) {
                        $img = wfLocalFile( $this->title );
                        $this->fileStatus = $img->restore( $fileVersions, $unsuppress );
 -                      if ( !$this->fileStatus->isOk() ) {
 +                      if ( !$this->fileStatus->isOK() ) {
                                return false;
                        }
                        $filesRestored = $this->fileStatus->successCount;
                }
  
                if( $restoreText ) {
 -                      $textRestored = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
 -                      if( $textRestored === false ) { // It must be one of UNDELETE_*
 +                      $this->revisionStatus = $this->undeleteRevisions( $timestamps, $unsuppress, $comment );
 +                      if( !$this->revisionStatus->isOK() ) {
                                return false;
                        }
 +
 +                      $textRestored = $this->revisionStatus->getValue();
                } else {
                        $textRestored = 0;
                }
                }
  
                if ( $user === null ) {
+                       global $wgUser;
                        $user = $wgUser;
                }
  
         * @param $comment String
         * @param $unsuppress Boolean: remove all ar_deleted/fa_deleted restrictions of seletected revs
         *
 -       * @return Mixed: number of revisions restored or false on failure
 +       * @return Status, containing the number of revisions restored on success
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
 +              global $wgContentHandlerNoDB;
 +
                if ( wfReadOnly() ) {
 -                      return false;
 +                      throw new ReadOnlyError();
                }
                $restoreAll = empty( $timestamps );
  
                        $previousTimestamp = $dbw->selectField( 'revision', 'rev_timestamp',
                                array( 'rev_id' => $previousRevId ),
                                __METHOD__ );
 +
                        if( $previousTimestamp === false ) {
                                wfDebug( __METHOD__.": existing page refers to a page_latest that does not exist\n" );
 -                              return 0;
 +
 +                              $status = Status::newGood( 0 );
 +                              $status->warning( 'undeleterevision-missing' );
 +
 +                              return $status;
                        }
                } else {
                        # Have to create a new article...
                        $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(),
                $rev_count = $dbw->numRows( $result );
                if( !$rev_count ) {
                        wfDebug( __METHOD__ . ": no revisions to restore\n" );
 -                      return false; // ???
 +
 +                      $status = Status::newGood( 0 );
 +                      $status->warning( "undelete-no-results" );
 +                      return $status;
                }
  
                $ret->seek( $rev_count - 1 ); // move to last
                $row = $ret->fetchObject(); // get newest archived rev
                $ret->seek( 0 ); // move back
  
 +              // grab the content to check consistency with global state before restoring the page.
 +              $revision = Revision::newFromArchiveRow( $row,
 +                      array(
 +                              'title' => $article->getTitle(), // used to derive default content model
 +                      ) );
 +
 +              $m = $revision->getContentModel();
 +
 +              $user = User::newFromName( $revision->getRawUserText(), false );
 +              $content = $revision->getContent( Revision::RAW );
 +
 +              //NOTE: article ID may not be known yet. prepareSave() should not modify the database.
 +              $status = $content->prepareSave( $article, 0, -1, $user );
 +
 +              if ( !$status->isOK() ) {
 +                      return $status;
 +              }
 +
                if( $makepage ) {
                        // Check the state of the newest to-be version...
                        if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
 -                              return false; // we can't leave the current revision like this!
 +                              return Status::newFatal( "undeleterevdel" );
                        }
                        // Safe to insert now...
                        $newid  = $article->insertOn( $dbw );
                        if( $row->ar_timestamp > $previousTimestamp ) {
                                // Check the state of the newest to-be version...
                                if( !$unsuppress && ( $row->ar_deleted & Revision::DELETED_TEXT ) ) {
 -                                      return false; // we can't leave the current revision like this!
 +                                      return Status::newFatal( "undeleterevdel" );
                                }
                        }
                }
                        // unless we are specifically removing all restrictions...
                        $revision = Revision::newFromArchiveRow( $row,
                                array(
 -                                      'page' => $pageId,
 +                                      'title' => $this->title,
                                        'deleted' => $unsuppress ? 0 : $row->ar_deleted
                                ) );
  
  
                // Was anything restored at all?
                if ( $restored == 0 ) {
 -                      return 0;
 +                      return Status::newGood( 0 );
                }
  
                $created = (bool)$newid;
                        $update->doUpdate();
                }
  
 -              return $restored;
 +              return Status::newGood( $restored );
        }
  
        /**
         * @return Status
         */
        function getFileStatus() { return $this->fileStatus; }
 +
 +      /**
 +       * @return Status
 +       */
 +      function getRevisionStatus() { return $this->revisionStatus; }
  }
  
  /**
@@@ -851,13 -779,11 +850,13 @@@ class SpecialUndelete extends SpecialPa
  
        private function showRevision( $timestamp ) {
                if( !preg_match( '/[0-9]{14}/', $timestamp ) ) {
 -                      return 0;
 +                      return;
                }
  
                $archive = new PageArchive( $this->mTargetObj );
 -              wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) );
 +              if ( !wfRunHooks( 'UndeleteForm::showRevision', array( &$archive, $this->mTargetObj ) ) ) {
 +                      return;
 +              }
                $rev = $archive->getRevision( $timestamp );
  
                $out = $this->getOutput();
                $t = $lang->userTime( $timestamp, $user );
                $userLink = Linker::revUserTools( $rev );
  
 -              if( $this->mPreview ) {
 +              $content = $rev->getContent( Revision::FOR_THIS_USER, $user );
 +
 +              $isText = ( $content instanceof TextContent );
 +
 +              if( $this->mPreview || $isText ) {
                        $openDiv = '<div id="mw-undelete-revision" class="mw-warning">';
                } else {
                        $openDiv = '<div id="mw-undelete-revision">';
  
                $out->addHTML( $this->msg( 'undelete-revision' )->rawParams( $link )->params(
                        $time )->rawParams( $userLink )->params( $d, $t )->parse() . '</div>' );
 -              wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) );
  
 -              if( $this->mPreview ) {
 +              if ( !wfRunHooks( 'UndeleteShowRevision', array( $this->mTargetObj, $rev ) ) ) {
 +                      return;
 +              }
 +
 +              if( $this->mPreview || !$isText ) {
 +                      // NOTE: non-text content has no source view, so always use rendered preview
 +
                        // Hide [edit]s
                        $popts = $out->parserOptions();
                        $popts->setEditSection( false );
 -                      $out->parserOptions( $popts );
 -                      $out->addWikiTextTitleTidy( $rev->getText( Revision::FOR_THIS_USER, $user ), $this->mTargetObj, true );
 +
 +                      $pout = $content->getParserOutput( $this->mTargetObj, $rev->getId(), $popts, true );
 +                      $out->addParserOutput( $pout );
                }
  
 +              if ( $isText ) {
 +                      // source view for textual content
 +                      $sourceView = Xml::element( 'textarea', array(
 +                              'readonly' => 'readonly',
 +                              'cols' => intval( $user->getOption( 'cols' ) ),
 +                              'rows' => intval( $user->getOption( 'rows' ) ) ),
 +                              $content->getNativeData() . "\n" );
 +
 +                      $previewButton = Xml::element( 'input', array(
 +                              'type' => 'submit',
 +                              'name' => 'preview',
 +                              'value' => $this->msg( 'showpreview' )->text() ) );
 +              } else {
 +                      $sourceView = '';
 +                      $previewButton = '';
 +              }
 +
 +              $diffButton = Xml::element( 'input', array(
 +                      'name' => 'diff',
 +                      'type' => 'submit',
 +                      'value' => $this->msg( 'showdiff' )->text() ) );
 +
                $out->addHTML(
 -                      Xml::element( 'textarea', array(
 -                                      'readonly' => 'readonly',
 -                                      'cols' => intval( $user->getOption( 'cols' ) ),
 -                                      'rows' => intval( $user->getOption( 'rows' ) ) ),
 -                              $rev->getText( Revision::FOR_THIS_USER, $user ) . "\n" ) .
 -                      Xml::openElement( 'div' ) .
 +                      $sourceView .
 +                      Xml::openElement( 'div', array(
 +                              'style' => 'clear: both' ) ) .
                        Xml::openElement( 'form', array(
                                'method' => 'post',
                                'action' => $this->getTitle()->getLocalURL( array( 'action' => 'submit' ) ) ) ) .
                                'type' => 'hidden',
                                'name' => 'wpEditToken',
                                'value' => $user->getEditToken() ) ) .
 -                      Xml::element( 'input', array(
 -                              'type' => 'submit',
 -                              'name' => 'preview',
 -                              'value' => $this->msg( 'showpreview' )->text() ) ) .
 -                      Xml::element( 'input', array(
 -                              'name' => 'diff',
 -                              'type' => 'submit',
 -                              'value' => $this->msg( 'showdiff' )->text() ) ) .
 +                      $previewButton .
 +                      $diffButton .
                        Xml::closeElement( 'form' ) .
                        Xml::closeElement( 'div' ) );
        }
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
 -              $diffEngine = new DifferenceEngine( $this->getContext() );
 +              $diffContext = clone $this->getContext();
 +              $diffContext->setTitle( $currentRev->getTitle() );
 +              $diffContext->setWikiPage( WikiPage::factory( $currentRev->getTitle() ) );
 +
 +              $diffEngine = $currentRev->getContentHandler()->createDifferenceEngine( $diffContext );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
-                       "<table width='98%' cellpadding='0' cellspacing='4' class='diff'>" .
+                       "<table style='width: 98%;' cellpadding='0' cellspacing='4' class='diff'>" .
                        "<col class='diff-marker' />" .
                        "<col class='diff-content' />" .
                        "<col class='diff-marker' />" .
                        "<col class='diff-content' />" .
                        "<tr>" .
-                               "<td colspan='2' width='50%' style='text-align: center' class='diff-otitle'>" .
+                               "<td colspan='2' style='width: 50%; text-align: center' class='diff-otitle'>" .
                                $this->diffHeader( $previousRev, 'o' ) .
                                "</td>\n" .
-                               "<td colspan='2' width='50%' style='text-align: center' class='diff-ntitle'>" .
+                               "<td colspan='2' style='width: 50%;  text-align: center' class='diff-ntitle'>" .
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
 -                      $diffEngine->generateDiffBody(
 -                              $previousRev->getText( Revision::FOR_THIS_USER, $this->getUser() ),
 -                              $currentRev->getText( Revision::FOR_THIS_USER, $this->getUser() ) ) .
 +                      $diffEngine->generateContentDiffBody(
 +                              $previousRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ),
 +                              $currentRev->getContent( Revision::FOR_THIS_USER, $this->getUser() ) ) .
                        "</table>" .
                        "</div>\n"
                );
  
        private function formatRevisionRow( $row, $earliestLiveTime, $remaining ) {
                $rev = Revision::newFromArchiveRow( $row,
 -                      array( 'page' => $this->mTargetObj->getArticleID() ) );
 +                      array(
 +                              'title' => $this->mTargetObj
 +                      ) );
 +
                $revTextSize = '';
                $ts = wfTimestamp( TS_MW, $row->ar_timestamp );
                // Build checkboxen...
                        $out->addHTML( $this->msg( 'undeletedpage' )->rawParams( $link )->parse() );
                } else {
                        $out->setPageTitle( $this->msg( 'undelete-error' ) );
 -                      $out->addWikiMsg( 'cannotundelete' );
 -                      $out->addWikiMsg( 'undeleterevdel' );
                }
  
 -              // Show file deletion warnings and errors
 +              // Show revision undeletion warnings and errors
 +              $status = $archive->getRevisionStatus();
 +              if( $status && !$status->isGood() ) {
 +                      $out->addWikiText( '<div class="error">' . $status->getWikiText( 'cannotundelete', 'cannotundelete' ) . '</div>' );
 +              }
 +
 +              // Show file undeletion warnings and errors
                $status = $archive->getFileStatus();
                if( $status && !$status->isGood() ) {
                        $out->addWikiText( '<div class="error">' . $status->getWikiText( 'undelete-error-short', 'undelete-error-long' ) . '</div>' );
@@@ -61,6 -61,8 +61,8 @@@ class UploadFromUrl extends UploadBase 
  
        /**
         * Checks whether the URL is for an allowed host
+        * The domains in the whitelist can include wildcard characters (*) in place
+        * of any of the domain levels, e.g. '*.flickr.com' or 'upload.*.gov.uk'.
         *
         * @param $url string
         * @return bool
                if ( !count( $wgCopyUploadsDomains ) ) {
                        return true;
                }
 -              $parsedUrl = wfParseUrl( $url );
 -              if ( !$parsedUrl ) {
 +              $uri = new Uri( $url );
 +              $parsedDomain = $uri->getHost();
 +              if ( $parsedDomain === null ) {
                        return false;
                }
                $valid = false;
                foreach( $wgCopyUploadsDomains as $domain ) {
-                       if ( $parsedDomain === $domain ) {
+                       // See if the domain for the upload matches this whitelisted domain
+                       $whitelistedDomainPieces = explode( '.', $domain );
+                       $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
+                       if ( count( $whitelistedDomainPieces ) === count( $uploadDomainPieces ) ) {
+                               $valid = true;
+                               // See if all the pieces match or not (excluding wildcards)
+                               foreach ( $whitelistedDomainPieces as $index => $piece ) {
+                                       if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
+                                               $valid = false;
+                                       }
+                               }
+                               if ( $valid ) {
+                                       // We found a match, so quit comparing against the list
+                                       break;
+                               }
+                       }
+                       /* Non-wildcard test
+                       if ( $parsedUrl['host'] === $domain ) {
                                $valid = true;
                                break;
                        }
+                       */
                }
                return $valid;
        }
                $this->mRemoveTempFile = true;
                $this->mFileSize = 0;
  
-               $req = MWHttpRequest::factory( $this->mUrl, array(
+               $options = array(
                        'followRedirects' => true
-               ) );
+               );
+               global $wgCopyUploadProxy;
+               if ( $wgCopyUploadProxy !== false ) {
+                       $options['proxy'] = $wgCopyUploadProxy;
+               }
+               $req = MWHttpRequest::factory( $this->mUrl, $options );
                $req->setCallback( array( $this, 'saveTempFileChunk' ) );
                $status = $req->execute();
  
diff --combined languages/Language.php
@@@ -48,7 -48,7 +48,7 @@@ class FakeConverter 
        /**
         * @var Language
         */
-       var $mLang;
+       public $mLang;
        function __construct( $langobj ) { $this->mLang = $langobj; }
        function autoConvertToAllVariants( $text ) { return array( $this->mLang->getCode() => $text ); }
        function convert( $t ) { return $t; }
@@@ -77,21 -77,21 +77,21 @@@ class Language 
        /**
         * @var LanguageConverter
         */
-       var $mConverter;
+       public $mConverter;
  
-       var $mVariants, $mCode, $mLoaded = false;
-       var $mMagicExtensions = array(), $mMagicHookDone = false;
+       public $mVariants, $mCode, $mLoaded = false;
+       public $mMagicExtensions = array(), $mMagicHookDone = false;
        private $mHtmlCode = null;
  
-       var $dateFormatStrings = array();
-       var $mExtendedSpecialPageAliases;
+       public $dateFormatStrings = array();
+       public $mExtendedSpecialPageAliases;
  
        protected $namespaceNames, $mNamespaceIds, $namespaceAliases;
  
        /**
         * ReplacementArray object caches
         */
-       var $transformData = array();
+       public $transformData = array();
  
        /**
         * @var LocalisationCache
         * @deprecated in 1.19
         */
        function getFallbackLanguageCode() {
-               wfDeprecated( __METHOD__ );
+               wfDeprecated( __METHOD__, '1.19' );
                return self::getFallbackFor( $this->mCode );
        }
  
         */
        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;
        }
  
        /**
                if ( !count( $forms ) ) {
                        return '';
                }
+               // Handle explicit 0= and 1= forms
+               foreach ( $forms as $index => $form ) {
+                       if ( isset( $form[1] ) && $form[1] === '=' ) {
+                               if ( $form[0] === (string) $count ) {
+                                       return substr( $form, 2 );
+                               }
+                               unset( $forms[$index] );
+                       }
+               }
+               $forms = array_values( $forms );
                $pluralForm = $this->getPluralForm( $count );
                $pluralForm = min( $pluralForm, count( $forms ) - 1 );
                return $forms[$pluralForm];
@@@ -47,28 -47,28 +47,28 @@@ class LanguageConverter 
                'zh',
        );
  
-       var $mMainLanguageCode;
-       var $mVariants, $mVariantFallbacks, $mVariantNames;
-       var $mTablesLoaded = false;
-       var $mTables;
+       public $mMainLanguageCode;
+       public $mVariants, $mVariantFallbacks, $mVariantNames;
+       public $mTablesLoaded = false;
+       public $mTables;
        // 'bidirectional' 'unidirectional' 'disable' for each variant
-       var $mManualLevel;
+       public $mManualLevel;
  
        /**
         * @var String: memcached key name
         */
-       var $mCacheKey;
-       var $mLangObj;
-       var $mFlags;
-       var $mDescCodeSep = ':', $mDescVarSep = ';';
-       var $mUcfirst = false;
-       var $mConvRuleTitle = false;
-       var $mURLVariant;
-       var $mUserVariant;
-       var $mHeaderVariant;
-       var $mMaxDepth = 10;
-       var $mVarSeparatorPattern;
+       public $mCacheKey;
+       public $mLangObj;
+       public $mFlags;
+       public $mDescCodeSep = ':', $mDescVarSep = ';';
+       public $mUcfirst = false;
+       public $mConvRuleTitle = false;
+       public $mURLVariant;
+       public $mUserVariant;
+       public $mHeaderVariant;
+       public $mMaxDepth = 10;
+       public $mVarSeparatorPattern;
  
        const CACHE_VERSION_KEY = 'VERSION 6';
  
                        if ( $title && $title->exists() ) {
                                $revision = Revision::newFromTitle( $title );
                                if ( $revision ) {
 -                                      $txt = $revision->getRawText();
 +                                      if ( $revision->getContentModel() == CONTENT_MODEL_WIKITEXT ) {
 +                                              $txt = $revision->getContent( Revision::RAW )->getNativeData();
 +                                      }
 +
 +                                      //@todo: in the future, use a specialized content model, perhaps based on json!
                                }
                        }
                }
   * @author fdcn <fdcn64@gmail.com>, PhiLiP <philip.npc@gmail.com>
   */
  class ConverterRule {
-       var $mText; // original text in -{text}-
-       var $mConverter; // LanguageConverter object
-       var $mRuleDisplay = '';
-       var $mRuleTitle = false;
-       var $mRules = '';// string : the text of the rules
-       var $mRulesAction = 'none';
-       var $mFlags = array();
-       var $mVariantFlags = array();
-       var $mConvTable = array();
-       var $mBidtable = array();// array of the translation in each variant
-       var $mUnidtable = array();// array of the translation in each variant
+       public $mText; // original text in -{text}-
+       public $mConverter; // LanguageConverter object
+       public $mRuleDisplay = '';
+       public $mRuleTitle = false;
+       public $mRules = '';// string : the text of the rules
+       public $mRulesAction = 'none';
+       public $mFlags = array();
+       public $mVariantFlags = array();
+       public $mConvTable = array();
+       public $mBidtable = array();// array of the translation in each variant
+       public $mUnidtable = array();// array of the translation in each variant
  
        /**
         * Constructor
   * @author ChrisiPK
   * @author Church of emacs
   * @author DaSch
+  * @author Das Schäfchen
   * @author Duesentrieb
   * @author Filzstift
   * @author Geitost
   * @author Giftpflanze
+  * @author Hoo
   * @author Imre
   * @author Inkowik
   * @author Jan Luca
@@@ -450,7 -452,7 +452,7 @@@ $messages = array
  
  'underline-always' => 'immer',
  'underline-never' => 'nie',
- 'underline-default' => 'abhängig von Browsereinstellung',
+ 'underline-default' => 'abhängig von der Browsereinstellung',
  
  # Font style option in Special:Preferences
  'editfont-style' => 'Schriftart für den Text im Bearbeitungsfenster:',
  'qbbrowse' => 'Durchsuchen',
  'qbedit' => 'Bearbeiten',
  'qbpageoptions' => 'Seitenoptionen',
- 'qbpageinfo' => 'Seitendaten',
+ 'qbpageinfo' => 'Kontext',
  'qbmyoptions' => 'Meine Seiten',
  'qbspecialpages' => 'Spezialseiten',
- 'faq' => 'Häufige Fragen',
+ 'faq' => 'Häufig gestellte Fragen',
  'faqpage' => 'Project:FAQ',
  
  # Vector skin
  'vector-action-protect' => 'Schützen',
  'vector-action-undelete' => 'Wiederherstellen',
  'vector-action-unprotect' => 'Seitenschutz ändern',
- 'vector-simplesearch-preference' => 'Erweiterte Suchvorschläge aktivieren (nur Vector)',
+ 'vector-simplesearch-preference' => 'Vereinfachte Suchleiste aktivieren (nur Vector)',
  'vector-view-create' => 'Erstellen',
  'vector-view-edit' => 'Bearbeiten',
  'vector-view-history' => 'Versionsgeschichte',
@@@ -653,8 -655,8 +655,8 @@@ $1'
  'privacy' => 'Datenschutz',
  'privacypage' => 'Project:Datenschutz',
  
- 'badaccess' => 'Keine ausreichenden Rechte',
- 'badaccess-group0' => 'Du hast nicht die erforderliche Berechtigung für diese Aktion.',
+ 'badaccess' => 'Keine ausreichenden Benutzerrechte',
+ 'badaccess-group0' => 'Du hast nicht die erforderlichen Benutzerrechte für diese Aktion.',
  'badaccess-groups' => 'Diese Aktion ist auf Benutzer beschränkt, die {{PLURAL:$2|der Gruppe|einer der Gruppen}} „$1“ angehören.',
  
  'versionrequired' => 'Version $1 von MediaWiki ist erforderlich.',
@@@ -790,7 -792,7 +792,7 @@@ Nutze bitte [//translatewiki.net/ trans
  $2',
  'namespaceprotected' => "Du hast nicht die erforderliche Berechtigung, um Seiten im Namensraum '''$1''' bearbeiten zu können.",
  'customcssprotected' => 'Du hast nicht die Berechtigung, diese CSS enthaltende Seite zu bearbeiten, da sie die persönlichen Einstellungen eines anderen Benutzers enthält.',
- 'customjsprotected' => 'Du hast nicht die Berechtigung diese JavaScript enthaltende Seite zu bearbeiten, da sie die persönlichen Einstellungen eines anderen Benutzers enthält.',
+ 'customjsprotected' => 'Du hast nicht die Berechtigung, diese JavaScript enthaltende Seite zu bearbeiten, da es sich hierbei um die persönlichen Einstellungen eines anderen Benutzers handelt.',
  'ns-specialprotected' => 'Spezialseiten können nicht bearbeitet werden.',
  'titleprotected' => "Eine Seite mit diesem Namen kann nicht angelegt werden.
  Die Sperre wurde durch [[User:$1|$1]] mit der Begründung ''„$2“'' eingerichtet.",
@@@ -810,12 -812,12 +812,12 @@@ Der Administrator, der den Schreibzugri
  # Login and logout pages
  'logouttext' => "'''Du bist nun abgemeldet.'''
  
- Du kannst {{SITENAME}} jetzt anonym weiternutzen, oder dich erneut unter demselben oder einem anderen Benutzernamen [[Special:UserLogin|anmelden]].
+ Du kannst {{SITENAME}} jetzt anonym weiternutzen oder dich erneut unter dem selben oder einem anderen Benutzernamen <span class='plainlinks'>[$1 anmelden]</span>.
  Beachte, dass einige Seiten noch anzeigen können, dass du angemeldet bist, solange du nicht deinen Browsercache geleert hast.",
  'welcomecreation' => '== Willkommen, $1! ==
  
- Dein Benutzerkonto wurde eingerichtet.
- Vergiss nicht, deine [[Special:Preferences|{{SITENAME}}-Einstellungen]] anzupassen.',
+ Dein Benutzerkonto wurde soeben eingerichtet.
+ Vergiss nicht, deine [[Special:Preferences|Einstellungen]] für dieses Wiki anzupassen.',
  'yourname' => 'Benutzername:',
  'yourpassword' => 'Passwort:',
  'yourpasswordagain' => 'Passwort wiederholen:',
  'login' => 'Anmelden',
  'nav-login-createaccount' => 'Anmelden / Benutzerkonto erstellen',
  'loginprompt' => 'Zur Anmeldung müssen Cookies aktiviert sein.',
- 'userlogin' => 'Anmelden / Erstellen',
+ 'userlogin' => 'Anmelden / Benutzerkonto anlegen',
  'userloginnocreate' => 'Anmelden',
  'logout' => 'Abmelden',
  'userlogout' => 'Abmelden',
@@@ -907,7 -909,7 +909,7 @@@ Bitte warte, bevor du es erneut probier
  
  # E-mail sending
  'php-mail-error-unknown' => 'Unbekannter Fehler mit der Funktion mail() von PHP',
- 'user-mail-no-addy' => 'Versuchte eine E-Mail ohne Angabe einer E-Mail-Adresse zu versenden',
+ 'user-mail-no-addy' => 'Versuchte, eine E-Mail ohne Angabe einer E-Mail-Adresse zu versenden.',
  
  # Change password dialog
  'resetpass' => 'Passwort ändern',
@@@ -997,7 -999,7 +999,7 @@@ Temporäres Passwort: $2'
  'showpreview' => 'Vorschau zeigen',
  'showlivepreview' => 'Sofortige Vorschau',
  'showdiff' => 'Änderungen zeigen',
- 'anoneditwarning' => "Du bearbeitest diese Seite unangemeldet. Wenn du speicherst, wird deine aktuelle IP-Adresse in der Versionsgeschichte aufgezeichnet und ist damit unwiderruflich '''öffentlich''' einsehbar.",
+ 'anoneditwarning' => "Du bearbeitest diese Seite unangemeldet. Wenn du sie speicherst, wird deine aktuelle IP-Adresse in der Versionsgeschichte aufgezeichnet und ist damit unwiderruflich '''öffentlich''' einsehbar.",
  'anonpreviewwarning' => "''Du bist nicht angemeldet. Beim Speichern wird deine IP-Adresse in der Versionsgeschichte aufgezeichnet.''",
  'missingsummary' => "'''Hinweis:''' Du hast keine Zusammenfassung angegeben. Wenn du erneut auf „{{int:savearticle}}“ klickst, wird deine Änderung ohne Zusammenfassung übernommen.",
  'missingcommenttext' => 'Dein Abschnitt enthält keinen Text.',
  'blockedtitle' => 'Benutzer ist gesperrt',
  'blockedtext' => "'''Dein Benutzername oder deine IP-Adresse wurde gesperrt.'''
  
- Die Sperrung wurde von $1 durchgeführt.
+ Die Sperrung wurde vom Administrator $1 durchgeführt.
  Als Grund wurde ''$2'' angegeben.
  
  * Beginn der Sperre: $8
  
  Du kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.
  Du kannst die „E-Mail an diesen Benutzer“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.
- Deine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.
+ Deine aktuelle IP-Adresse ist $3 und die Sperrkennung lautet $5.
  Bitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
  'autoblockedtext' => "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.
  Als Grund wurde angegeben:
@@@ -1056,8 -1058,7 +1058,7 @@@ Du kannst sie <span class="plainlinks">
  ihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]]
  oder die zugehörigen <span class="plainlinks">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten]</span>.',
  'noarticletext-nopermission' => 'Diese Seite enthält momentan noch keinen Text.
- Du kannst ihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]]
- oder die zugehörigen <span class="plainlinks">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten].</span>',
+ Du kannst ihren Titel auf anderen Seiten [[Special:Search/{{PAGENAME}}|suchen]] oder die zugehörigen <span class="plainlinks">[{{fullurl:{{#special:Log}}|page={{FULLPAGENAMEE}}}} Logbücher betrachten].</span> Du bist allerdings nicht berechtigt diese Seite zu erstellen.',
  'missing-revision' => 'Die Version $1 der Seite namens „{{PAGENAME}}“ ist nicht vorhanden.
  
  Dieser Fehler wird normalerweise von einem veralteten Link zur Versionsgeschichte einer Seite verursacht, die zwischenzeitlich gelöscht wurde.
@@@ -1105,7 -1106,7 +1106,7 @@@ Eine Speicherung kann den Seiteninhalt 
  'editingsection' => 'Bearbeiten von „$1“ (Abschnitt)',
  'editingcomment' => 'Bearbeiten von „$1“ (Neuer Abschnitt)',
  'editconflict' => 'Bearbeitungskonflikt: $1',
- 'explainconflict' => "Jemand anders hat diese Seite geändert, nachdem du angefangen hast sie zu bearbeiten.
+ 'explainconflict' => "Jemand anders hat diese Seite geändert, nachdem du angefangen hast, sie zu bearbeiten.
  Das obere Textfeld enthält den aktuellen Bearbeitungsstand der Seite.
  Das untere Textfeld enthält deine Änderungen.
  Bitte füge deine Änderungen in das obere Textfeld ein.
  'yourdiff' => 'Unterschiede',
  'copyrightwarning' => "'''Bitte kopiere keine Webseiten, die nicht deine eigenen sind, benutze keine urheberrechtlich geschützten Werke ohne Erlaubnis des Urhebers!'''<br />
  Du gibst uns hiermit deine Zusage, dass du den Text '''selbst verfasst''' hast, dass der Text Allgemeingut '''(public domain)''' ist, oder dass der '''Urheber''' seine '''Zustimmung''' gegeben hat. Falls dieser Text bereits woanders veröffentlicht wurde, weise bitte auf der Diskussionsseite darauf hin.
- <i>Bitte beachte, dass alle {{SITENAME}}-Beiträge automatisch unter der „$2“ stehen (siehe $1 für Details). Falls du nicht möchtest, dass deine Arbeit hier von anderen verändert und verbreitet wird, dann drücke nicht auf „Seite speichern“.</i>",
+ <i>Bitte beachte, dass alle {{SITENAME}}-Beiträge automatisch unter der „$2“ stehen (siehe $1 für Einzelheiten). Falls du nicht möchtest, dass deine Arbeit hier von anderen verändert und verbreitet wird, dann klicke nicht auf „Seite speichern“.</i>",
  'copyrightwarning2' => "Bitte beachte, dass alle Beiträge zu {{SITENAME}} von anderen Mitwirkenden bearbeitet, geändert oder gelöscht werden können.
  Reiche hier keine Texte ein, falls du nicht willst, dass diese ohne Einschränkung geändert werden können.
  
  Du bestätigst hiermit auch, dass du diese Texte selbst geschrieben hast oder diese von einer gemeinfreien Quelle kopiert hast
- (siehe $1 für weitere Details). '''ÜBERTRAGE OHNE GENEHMIGUNG KEINE URHEBERRECHTLICH GESCHÜTZTEN INHALTE!'''",
+ (siehe $1 für weitere Einzelheiten). '''ÜBERTRAGE OHNE GENEHMIGUNG KEINE URHEBERRECHTLICH GESCHÜTZTEN INHALTE!'''",
  'longpageerror' => "'''Fehler: Der Text, den du zu speichern versuchst, ist {{PLURAL:$1|ein Kilobyte|$1 Kilobyte}} groß. Dies ist größer als das erlaubte Maximum von {{PLURAL:$2|ein Kilobyte|$2 Kilobyte}}.'''
  Er kann nicht gespeichert werden.",
  'readonlywarning' => "'''Achtung: Die Datenbank wurde für Wartungsarbeiten gesperrt, so dass deine Änderungen derzeit nicht gespeichert werden können.
@@@ -1188,7 -1189,7 +1189,7 @@@ Sie darf nicht mehr als $2 {{PLURAL:$2|
  
  # "Undo" feature
  'undo-success' => 'Die Bearbeitung kann rückgängig gemacht werden.
- Bitte prüfe den Vergleich unten um sicherzustellen, dass du dies tun möchtest, und speichere dann unten deine Änderungen, um die Bearbeitung rückgängig zu machen.',
+ Bitte prüfe den Vergleich unten, um sicherzustellen, dass du dies tun möchtest, und speichere dann unten deine Änderungen, um die Bearbeitung rückgängig zu machen.',
  'undo-failure' => 'Die Änderung konnte nicht rückgängig gemacht werden, da der betroffene Abschnitt zwischenzeitlich verändert wurde.',
  'undo-norev' => 'Die Bearbeitung konnte nicht rückgängig gemacht werden, da sie nicht vorhanden ist oder gelöscht wurde.',
  'undo-summary' => 'Änderung $1 von [[Special:Contributions/$2|$2]] ([[User talk:$2|Diskussion]]) rückgängig gemacht.',
@@@ -1417,8 -1418,6 +1418,6 @@@ Einzelheiten sind im [{{fullurl:{{#Spec
  'search-interwiki-caption' => 'Schwesterprojekte',
  'search-interwiki-default' => '$1 Ergebnisse:',
  'search-interwiki-more' => '(weitere)',
- 'search-mwsuggest-enabled' => 'mit Vorschlägen',
- 'search-mwsuggest-disabled' => 'keine Vorschläge',
  'search-relatedarticle' => 'Verwandte',
  'mwsuggest-disable' => 'Vorschläge per Ajax deaktivieren',
  'searcheverything-enable' => 'In allen Namensräumen suchen',
@@@ -1498,7 -1497,7 +1497,7 @@@ Hier ein zufällig generierter Wert, de
  'savedprefs' => 'Deine Einstellungen wurden gespeichert.',
  'timezonelegend' => 'Zeitzone:',
  'localtime' => 'Ortszeit:',
- 'timezoneuseserverdefault' => 'Standardzeit des Wikis nutzen ($1)',
+ 'timezoneuseserverdefault' => 'Standardzeit dieses Wikis nutzen ($1)',
  'timezoneuseoffset' => 'Andere (Unterschied angeben)',
  'timezoneoffset' => 'Unterschied¹:',
  'servertime' => 'Aktuelle Zeit auf dem Server:',
  'timezoneregion-indian' => 'Indischer Ozean',
  'timezoneregion-pacific' => 'Pazifischer Ozean',
  'allowemail' => 'E-Mail-Empfang von anderen Benutzern ermöglichen',
- 'prefs-searchoptions' => 'Suchoptionen',
+ 'prefs-searchoptions' => 'Suche',
  'prefs-namespaces' => 'Namensräume',
  'defaultns' => 'Anderenfalls in diesen Namensräumen suchen:',
  'default' => 'Voreinstellung',
@@@ -1807,7 -1806,7 +1806,7 @@@ Um ein '''Bild''' in einer Seite zu ver
  'ignorewarning' => 'Warnung ignorieren und Datei speichern',
  'ignorewarnings' => 'Warnungen ignorieren',
  'minlength1' => 'Dateinamen müssen mindestens einen Buchstaben lang sein.',
- 'illegalfilename' => 'Der Dateiname „$1“ enthält mindestens ein nicht erlaubtes Zeichen. Bitte benenne die Datei um und versuche sie erneut hochzuladen.',
+ 'illegalfilename' => 'Der Dateiname „$1“ enthält mindestens ein nicht erlaubtes Zeichen. Bitte benenne die Datei um und versuche, sie erneut hochzuladen.',
  'filename-toolong' => 'Dateinamen dürfen nicht größer als 240 Byte sein.',
  'badfilename' => 'Der Dateiname wurde in „$1“ geändert.',
  'filetype-mime-mismatch' => 'Dateierweiterung „.$1“ stimmt nicht mit dem MIME-Typ ($2) überein.',
  'filetype-banned-type' => "'''„.$1“''' {{PLURAL:$4|ist ein nicht erlaubter Dateityp|sind nicht erlaubte Dateitypen}}.
  {{PLURAL:$3|Erlaubter Dateityp ist|Erlaubte Dateitypen sind}} $2.",
  'filetype-missing' => 'Die hochzuladende Datei hat keine Erweiterung (z. B. „.jpg“).',
- 'empty-file' => 'Die übertragene Datei ist leer',
- 'file-too-large' => 'Die übertragene Datei ist zu groß',
- 'filename-tooshort' => 'Der Dateiname ist zu kurz',
+ 'empty-file' => 'Die von dir übertragene Datei hat keinen Inhalt.',
+ 'file-too-large' => 'Die hochgeladene Datei war zu groß.',
+ 'filename-tooshort' => 'Der Dateiname ist zu kurz.',
  'filetype-banned' => 'Diese Dateiendung ist gesperrt.',
  'verification-error' => 'Diese Datei hat die Dateiprüfung nicht bestanden.',
- 'hookaborted' => 'Der Versuch, die Änderung durchzuführen, ist aufgrund eines Extension-Hooks fehlgeschlagen',
- 'illegal-filename' => 'Der Dateiname ist nicht erlaubt',
- 'overwrite' => 'Das Überschreiben einer existierenden Datei ist nicht erlaubt',
+ 'hookaborted' => 'Der Versuch, die Änderung durchzuführen, wurde von einer Parsererweiterung abgebrochen.',
+ 'illegal-filename' => 'Der Dateiname ist nicht zulässig.',
+ 'overwrite' => 'Das Überschreiben einer bereits vorhandenen Datei ist nicht erlaubt.',
  'unknown-error' => 'Ein unbekannter Fehler ist aufgetreten.',
  'tmp-create-error' => 'Temporäre Datei konnte nicht erstellt werden',
  'tmp-write-error' => 'Fehler beim Schreiben der temporären Datei',
@@@ -1840,7 -1839,7 +1839,7 @@@ Die Beschreibungsseite musst du nach de
  [[$1|thumb]]',
  'fileexists-extension' => 'Eine Datei ähnlichen Namens ist bereits vorhanden: [[$2|thumb]]
  * Name der hochzuladenden Datei: <strong>[[:$1]]</strong>
- * Name der vorhandenen Datei: <strong>[[:$2]]</strong>
+ * Name der bereits vorhandenen Datei: <strong>[[:$2]]</strong>
  Bitte wähle einen anderen Namen.',
  'fileexists-thumbnail-yes' => "Bei der Datei scheint es sich um ein Bild verringerter Größe ''(Miniatur)'' zu handeln. [[$1|thumb]]
  Bitte prüfe die Datei <strong>[[:$1]]</strong>.
@@@ -1940,7 -1939,7 +1939,7 @@@ Wenn das Problem weiter besteht, inform
  'backend-fail-internal' => 'Im Speicher-Backend „$1“ ist ein unbekannter Fehler aufgetreten.',
  'backend-fail-contenttype' => 'Der Inhaltstyp, der im Pfad „$1“ zu speichernden Datei, konnte nicht bestimmt werden.',
  'backend-fail-batchsize' => 'Eine Stapelverarbeitungsdatei, die {{PLURAL:$1|eine Operation|$1 Operationen}} enthält, wurde an das Speicher-Backend gesandt. Die Begrenzung liegt allerdings bei {{PLURAL:$2|einer Operation|$2 Operationen}}.',
- 'backend-fail-usable' => 'Die Datei „$1“ konnte, entweder aufgrund eines nicht vorhandenen Verzeichnisses oder aufgrund unzureichender Berechtigungen, weder abgerufen noch gespeichert werden.',
+ 'backend-fail-usable' => 'Die Datei „$1“ konnte entweder aufgrund eines nicht vorhandenen Verzeichnisses oder wegen unzureichender Berechtigungen weder abgerufen noch gespeichert werden.',
  
  # File journal errors
  'filejournal-fail-dbconnect' => 'Es konnte keine Verbindung zur Journaldatenbank des Speicher-Backends „$1“ hergestellt werden.',
@@@ -1971,17 -1970,17 +1970,17 @@@ Sie kann daher keiner ordnungsgemäße
  'uploadstash-summary' => 'Diese Seite ermöglicht den Zugriff auf Dateien, die hochgeladen wurden, bzw. gerade hochgeladen werden, aber noch nicht auf dem Wiki publiziert wurden. Diese Dateien sind, der hochladende Benutzer ausgenommen, noch nicht öffentlich einsehbar.',
  'uploadstash-clear' => 'Die vorab gespeicherten Dateien entfernen',
  'uploadstash-nofiles' => 'Es sind keine vorab gespeicherten Dateien vorhanden.',
- 'uploadstash-badtoken' => 'Das Entfernen der vorab gespeicherten Dateien war erfolglos, vielleicht weil die Sitzungsdaten abgelaufen sind. Bitte erneut versuchen.',
+ 'uploadstash-badtoken' => 'Das Entfernen der vorab gespeicherten Dateien war erfolglos, vielleicht weil deine Sitzungsdaten abgelaufen sind. Bitte versuche es erneut.',
  'uploadstash-errclear' => 'Das Entfernen der vorab gespeicherten Dateien war erfolglos.',
  'uploadstash-refresh' => 'Liste der Dateien aktualisieren',
  'invalid-chunk-offset' => 'Ungültiger Startpunkt',
  
  # img_auth script messages
  'img-auth-accessdenied' => 'Zugriff verweigert',
- 'img-auth-nopathinfo' => 'PATH_INFO fehlt.
+ 'img-auth-nopathinfo' => 'Die Angabe PATH_INFO fehlt.
  Der Server ist nicht dafür eingerichtet, diese Information weiterzugeben.
- Sie könnte CGI-gestützt sein und kann daher img_auth nicht ermöglichen.
- Siehe hierzu die Seite https://www.mediawiki.org/wiki/Manual:Image_Authorization.',
+ Sie könnte CGI-gestützt sein und kann daher „img_auth“ (Authentifizierung des Dateiaufrufs) nicht unterstützen.
+ Siehe hierzu die Seite https://www.mediawiki.org/wiki/Manual:Image_Authorization (englisch) für weitere Informationen.',
  'img-auth-notindir' => 'Der gewünschte Pfad ist nicht im konfigurierten Uploadverzeichnis.',
  'img-auth-badtitle' => 'Aus „$1“ kann kein gültiger Titel erstellt werden.',
  'img-auth-nologinnWL' => 'Du bist nicht angemeldet und „$1“ ist nicht in der weißen Liste.',
@@@ -2009,11 -2008,11 +2008,11 @@@ Aus Sicherheitsgründen ist img_auth.ph
  'upload-curl-error6' => 'URL ist nicht erreichbar',
  'upload-curl-error6-text' => 'Die angegebene URL ist nicht erreichbar. Prüfe sowohl die URL auf Fehler als auch den Online-Status der Seite.',
  'upload-curl-error28' => 'Zeitüberschreitung beim Hochladen',
- 'upload-curl-error28-text' => 'Die Seite braucht zu lange für eine Antwort. Prüfe, ob die Seite online ist, warte einen kurzen Moment und versuche es dann erneut. Es kann sinnvoll sein, einen erneuten Versuch zu einem anderen Zeitpunkt zu probieren.',
+ 'upload-curl-error28-text' => 'Die Seite braucht zu lange, um zu antworten. Prüfe, ob die Seite online ist, warte einen kurzen Moment und versuche es dann erneut. Es kann sinnvoll sein, es zu einem anderen Zeitpunkt erneut zu versuchen.',
  
  'license' => 'Lizenz:',
  'license-header' => 'Lizenz',
- 'nolicense' => 'keine Vorauswahl',
+ 'nolicense' => 'Keine Vorauswahl',
  'license-nopreview' => '(es ist keine Vorschau verfügbar)',
  'upload_source_url' => ' (gültige, öffentlich zugängliche URL)',
  'upload_source_file' => ' (eine Datei auf deinem Computer)',
@@@ -2143,7 -2142,7 +2142,7 @@@ Vielleicht möchtest du die Beschreibun
  'statistics-edits' => 'Seitenbearbeitungen',
  'statistics-edits-average' => 'Bearbeitungen pro Seite im Durchschnitt',
  'statistics-views-total' => 'Seitenaufrufe gesamt',
- 'statistics-views-total-desc' => 'Aufrufe nicht vorhandener Seiten und von Spezialseiten werden nicht berücksichtigt',
+ 'statistics-views-total-desc' => 'Aufrufe nicht vorhandener Seiten und von Spezialseiten werden nicht berücksichtigt.',
  'statistics-views-peredit' => 'Seitenaufrufe pro Bearbeitung',
  'statistics-users' => 'Registrierte [[Special:ListUsers|Benutzer]]',
  'statistics-users-active' => 'Aktive Benutzer',
@@@ -2506,9 -2505,9 +2505,9 @@@ Die letzte Änderung stammt von [[User:
  
  # Edit tokens
  'sessionfailure-title' => 'Sitzungsfehler',
- 'sessionfailure' => 'Es gab ein Problem mit der Übertragung deiner Benutzerdaten.
+ 'sessionfailure' => 'Es gab ein Problem bei der Übertragung deiner Benutzerdaten.
  Diese Aktion wurde daher sicherheitshalber abgebrochen, um eine falsche Zuordnung deiner Änderungen zu einem anderen Benutzer zu verhindern.
- Bitte gehe zurück und versuche den Vorgang erneut auszuführen.',
+ Bitte gehe zurück zur vorherigen Seite, lade sie erneut und versuche, den Vorgang erneut auszuführen.',
  
  # Protect
  'protectlogpage' => 'Seitenschutz-Logbuch',
@@@ -2603,8 -2602,7 +2602,8 @@@ Der aktuelle Text der gelöschten Seit
  'undeletedrevisions' => '{{PLURAL:$1|1 Version wurde|$1 Versionen wurden}} wiederhergestellt',
  'undeletedrevisions-files' => '{{PLURAL:$1|1 Version|$1 Versionen}} und {{PLURAL:$2|1 Datei|$2 Dateien}} wurden wiederhergestellt',
  'undeletedfiles' => '{{PLURAL:$1|1 Datei wurde|$1 Dateien wurden}} wiederhergestellt',
 -'cannotundelete' => 'Wiederherstellung fehlgeschlagen; jemand anderes hat die Seite bereits wiederhergestellt.',
 +'cannotundelete' => 'Wiederherstellung fehlgeschlagen:
 +$1',
  'undeletedpage' => "'''„$1“''' wurde wiederhergestellt.
  
  Im [[Special:Log/delete|Lösch-Logbuch]] findest du eine Übersicht der gelöschten und wiederhergestellten Seiten.",
@@@ -2721,7 -2719,7 +2720,7 @@@ Bitte gib den Grund für die Sperre an.
  'blockipsuccesstext' => 'Der Benutzer / die IP-Adresse [[Special:Contributions/$1|$1]] wurde gesperrt.<br />
  Zur Aufhebung der Sperre siehe die [[Special:BlockList|Liste aller aktiven Sperren]].',
  'ipb-blockingself' => 'Du bist gerade dabei, dich selbst zu sperren! Möchtest du das wirklich tun?',
- 'ipb-confirmhideuser' => 'Du bist gerade dabei einen Benutzer im Modus „Benutzer verstecken“ zu sperren. Dies führt dazu, dass der Benutzername in allen Listen und Logbüchern unterdrückt wird. Möchtest du das wirklich tun?',
+ 'ipb-confirmhideuser' => 'Du bist gerade dabei, einen Benutzer im Modus „Benutzer verstecken“ zu sperren. Dies führt dazu, dass der Benutzername in allen Listen und Logbüchern unterdrückt wird. Möchtest du das wirklich tun?',
  'ipb-edit-dropdown' => 'Sperrgründe bearbeiten',
  'ipb-unblock-addr' => '„$1“ freigeben',
  'ipb-unblock' => 'IP-Adresse/Benutzer freigeben',
@@@ -2889,7 -2887,7 +2888,7 @@@ Bitte den '''neuen''' Titel unter '''Zi
  'movenosubpage' => 'Diese Seite hat keine Unterseiten.',
  'movereason' => 'Grund:',
  'revertmove' => 'zurück verschieben',
- 'delete_and_move' => 'Löschen und Verschieben',
+ 'delete_and_move' => 'Löschen und verschieben',
  'delete_and_move_text' => '== Löschung erforderlich ==
  
  Die Seite „[[:$1]]“ existiert bereits. Möchtest du diese löschen, um die Seite verschieben zu können?',
@@@ -3028,7 -3026,6 +3027,6 @@@ Diese auf dem lokalen Rechner speicher
  
  # JavaScriptTest
  'javascripttest' => 'JavaScript-Test',
- 'javascripttest-disabled' => 'Diese Funktion wurde in diesem Wiki nicht aktiviert.',
  'javascripttest-title' => '$1-Tests werden durchgeführt',
  'javascripttest-pagetext-noframework' => 'Diese Seite ist JavaSkript-Tests vorbehalten.',
  'javascripttest-pagetext-unknownframework' => 'Unbekanntes Framework „$1“.',
  'tooltip-ca-talk' => 'Diskussion zum Seiteninhalt',
  'tooltip-ca-edit' => 'Seite bearbeiten. Bitte vor dem Speichern die Vorschaufunktion benutzen.',
  'tooltip-ca-addsection' => 'Neuen Abschnitt beginnen',
- 'tooltip-ca-viewsource' => 'Diese Seite ist geschützt. Der Quelltext kann angesehen werden.',
+ 'tooltip-ca-viewsource' => 'Diese Seite ist geschützt. Ihr Quelltext kann dennoch angesehen und kopiert werden.',
  'tooltip-ca-history' => 'Frühere Versionen dieser Seite',
  'tooltip-ca-protect' => 'Diese Seite schützen',
  'tooltip-ca-unprotect' => 'Seitenschutz ändern',
@@@ -3193,10 -3190,10 +3191,10 @@@ Das liegt wahrscheinlich an einem Link 
  'pageinfo-authors' => 'Gesamtzahl unterschiedlicher Autoren',
  'pageinfo-recent-edits' => 'Anzahl der kürzlich erfolgten Bearbeitungen (innerhalb von $1)',
  'pageinfo-recent-authors' => 'Anzahl der unterschiedlichen Autoren',
- 'pageinfo-restriction' => 'Seitenschutz ({{lcfirst:$1}})',
  'pageinfo-magic-words' => '{{PLURAL:$1|Magisches Wort|Magische Wörter}} ($1)',
  'pageinfo-hidden-categories' => 'Versteckte {{PLURAL:$1|Kategorie|Kategorien}} ($1)',
  'pageinfo-templates' => 'Eingebundene {{PLURAL:$1|Vorlage|Vorlagen}} ($1)',
+ 'pageinfo-toolboxlink' => 'Informationen zur Seite',
  
  # Skin names
  'skinname-standard' => 'Klassik',
@@@ -3778,6 -3775,7 +3776,7 @@@ Dieser Bestätigungscode ist gültig bi
  # Scary transclusion
  'scarytranscludedisabled' => '[Interwiki-Einbindung ist deaktiviert]',
  'scarytranscludefailed' => '[Vorlageneinbindung für $1 ist gescheitert]',
+ 'scarytranscludefailed-httpstatus' => '[Vorlagenabruf fehlgeschlagen für $1: HTTP  $2]',
  'scarytranscludetoolong' => '[URL ist zu lang]',
  
  # Delete conflict
@@@ -4042,7 -4040,7 +4041,7 @@@ Eine [{{SERVER}}{{SCRIPTPATH}}/COPYING 
  'revdelete-restricted' => 'Einschränkungen gelten auch für Administratoren',
  'revdelete-unrestricted' => 'Einschränkungen für Administratoren aufgehoben',
  'logentry-move-move' => '$1 verschob Seite $3 nach $4',
- 'logentry-move-move-noredirect' => '$1 verschob Seite $3 nach $4 ohne dabei eine Weiterleitung anzulegen',
+ 'logentry-move-move-noredirect' => '$1 verschob Seite $3 nach $4, ohne dabei eine Weiterleitung anzulegen',
  'logentry-move-move_redir' => '$1 verschob Seite $3 nach $4 und überschrieb dabei eine Weiterleitung',
  'logentry-move-move_redir-noredirect' => '$1 verschob Seite $3 nach $4 und überschrieb dabei eine Weiterleitung ohne selbst eine Weiterleitung anzulegen',
  'logentry-patrol-patrol' => '$1 markierte Version $4 von Seite $3 als kontrolliert',
  'newuserlog-byemail' => 'das Passwort wurde per E-Mail versandt',
  
  # Feedback
- 'feedback-bugornote' => 'Sofern Du detailliert ein technisches Problem beschreiben möchtest, melde bitte [$1 einen Fehler].
- Anderenfalls kannst du auch das untenstehende einfache Formular nutzen. Dein Kommentar wird, zusammen mit deinem Benutzernamen und der Version des von Dir verwendeten Webbrowsers sowie Betriebssystems, auf der Seite „[$3 $2]“ hinzugefügt.',
+ 'feedback-bugornote' => 'Sofern du detailliert ein technisches Problem beschreiben möchtest, melde bitte [$1 einen Fehler].
+ Anderenfalls kannst du auch das untenstehende einfache Formular nutzen. Dein Kommentar wird, zusammen mit deinem Benutzernamen und der Version des von dir verwendeten Webbrowsers sowie Betriebssystems, auf der Seite „[$3 $2]“ hinzugefügt.',
  'feedback-subject' => 'Betreff:',
  'feedback-message' => 'Nachricht:',
  'feedback-cancel' => 'Abbrechen',
  'feedback-bugcheck' => 'Super! Bitte überprüfe noch, ob es sich hierbei nicht um einen bereits [$1 bekannten Fehler] handelt.',
  'feedback-bugnew' => 'Ich habe es überprüft. Den neuen Fehler melden.',
  
+ # Search suggestions
+ 'searchsuggest-search' => 'Suchen',
+ 'searchsuggest-containing' => 'enthält …',
  # API errors
  'api-error-badaccess-groups' => 'Du hast nicht die Berechtigung Dateien in dieses Wiki hochzuladen.',
  'api-error-badtoken' => 'Interner Fehler: Der Token ist fehlerhaft.',
  'api-error-filetype-banned' => 'Diese Dateiendung ist gesperrt.',
  'api-error-filetype-banned-type' => '$1 {{PLURAL:$4|ist ein nicht zulässiger Dateityp|sind nicht zulässige Dateitypen}}. {{PLURAL:$3|Ein zulässiger Dateityp ist|Zulässige Dateitypen sind}} $2.',
  'api-error-filetype-missing' => 'Die hochzuladende Datei hat keine Dateiendung.',
- 'api-error-hookaborted' => 'Die von dir vorgesehene Anpassung kann nicht durchgeführt werden (Unterbrechung durch eine Programmschnittstelle).',
+ 'api-error-hookaborted' => 'Der Versuch, die Änderung durchzuführen, wurde von einer Parsererweiterung (API) abgebrochen.',
  'api-error-http' => 'Interner Fehler: Es konnte keine Verbindung zum Server hergestellt werden.',
  'api-error-illegal-filename' => 'Der Dateiname ist nicht erlaubt.',
  'api-error-internal-error' => 'Interner Fehler: Ein unbekannter Fehler ist beim Hochladen der Datei ins Wiki aufgetreten.',
@@@ -118,7 -118,7 +118,7 @@@ $namespaceGenderAliases = array()
   * A list of date format preference keys which can be selected in user
   * preferences. New preference keys can be added, provided they are supported
   * by the language class's timeanddate(). Only the 5 keys listed below are
-  * supported by the wikitext converter (DateFormatter.php).
+  * supported by the wikitext converter (parser/DateFormatter.php).
   *
   * The special key "default" is an alias for either dmy or mdy depending on
   * $wgAmericanDates
@@@ -207,7 -207,6 +207,6 @@@ $magicWords = array
        'forcetoc'                => array( 0,    '__FORCETOC__' ),
        'toc'                     => array( 0,    '__TOC__' ),
        'noeditsection'           => array( 0,    '__NOEDITSECTION__' ),
-       'noheader'                => array( 0,    '__NOHEADER__' ),
        'currentmonth'            => array( 1,    'CURRENTMONTH', 'CURRENTMONTH2' ),
        'currentmonth1'           => array( 1,    'CURRENTMONTH1' ),
        'currentmonthname'        => array( 1,    'CURRENTMONTHNAME' ),
@@@ -442,7 -441,6 +441,6 @@@ $specialPageAliases = array
        'Recentchanges'             => array( 'RecentChanges' ),
        'Recentchangeslinked'       => array( 'RecentChangesLinked', 'RelatedChanges' ),
        'Revisiondelete'            => array( 'RevisionDelete' ),
-       'RevisionMove'              => array( 'RevisionMove' ),
        'Search'                    => array( 'Search' ),
        'Shortpages'                => array( 'ShortPages' ),
        'Specialpages'              => array( 'SpecialPages' ),
@@@ -800,7 -798,7 +798,7 @@@ XHTML id names
  'vector-action-protect'          => 'Protect',
  'vector-action-undelete'         => 'Undelete',
  'vector-action-unprotect'        => 'Change protection',
- 'vector-simplesearch-preference' => 'Enable enhanced search suggestions (Vector skin only)',
+ 'vector-simplesearch-preference' => 'Enable simplified search bar (Vector skin only)',
  'vector-view-create'             => 'Create',
  'vector-view-edit'               => 'Edit',
  'vector-view-history'            => 'View history',
@@@ -895,7 -893,6 +893,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.',
@@@ -915,8 -912,8 +913,8 @@@ See [[Special:Version|version page]].'
  'newmessagesdifflink'          => 'last change',
  'youhavenewmessagesfromusers'  => 'You have $1 from {{PLURAL:$3|another user|$3 users}} ($2).',
  'youhavenewmessagesmanyusers'  => 'You have $1 from many users ($2).',
- 'newmessageslinkplural'        => '{{PLURAL:$1|a new message|new messages}}', # don't rely on the value of $1, it's 1 for singular and 2 for "more than one"
- 'newmessagesdifflinkplural'    => 'last {{PLURAL:$1|change|changes}}', # don't rely on the value of $1, it's 1 for singular and 2 for "more than one"
+ 'newmessageslinkplural'        => '{{PLURAL:$1|a new message|new messages}}',
+ 'newmessagesdifflinkplural'    => 'last {{PLURAL:$1|change|changes}}',
  'youhavenewmessagesmulti'      => 'You have new messages on $1',
  'newtalkseparator'             => ',&#32;', # do not translate or duplicate this message to other languages
  'editsection'                  => 'edit',
@@@ -1068,7 -1065,7 +1066,7 @@@ The administrator who locked it offere
  # Login and logout pages
  'logouttext'                 => "'''You are now logged out.'''
  
- You can continue to use {{SITENAME}} anonymously, or you can [[Special:UserLogin|log in again]] as the same or as a different user.
+ You can continue to use {{SITENAME}} anonymously, or you can <span class='plainlinks'>[$1 log in again]</span> as the same or as a different user.
  Note that some pages may continue to be displayed as if you were still logged in, until you clear your browser cache.",
  'welcomecreation'            => '== Welcome, $1! ==
  Your account has been created.
@@@ -1364,8 -1361,7 +1362,7 @@@ You can [[Special:Search/{{PAGENAME}}|s
  <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs],
  or [{{fullurl:{{FULLPAGENAME}}|action=edit}} edit this page]</span>.',
  'noarticletext-nopermission'       => 'There is currently no text in this page.
- You can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages,
- or <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>.',
+ You can [[Special:Search/{{PAGENAME}}|search for this page title]] in other pages, or <span class="plainlinks">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} search the related logs]</span>, but you do not have permission to create this page.',
  'noarticletextanon'                => '{{int:noarticletext}}', # do not translate or duplicate this message to other languages
  'missing-revision'                 => 'The revision #$1 of the page named "{{PAGENAME}}" does not exist.
  
@@@ -1431,7 -1427,7 +1428,7 @@@ If you save it, any changes made since 
  'yourdiff'                         => 'Differences',
  'copyrightwarning'                 => "Please note that all contributions to {{SITENAME}} are considered to be released under the $2 (see $1 for details).
  If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.<br />
 -You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource.
 +You are also promising us that you wrote this yourself, or copied editpageit from a public domain or similar free resource.
  '''Do not submit copyrighted work without permission!'''",
  'copyrightwarning2'                => "Please note that all contributions to {{SITENAME}} may be edited, altered, or removed by other contributors.
  If you do not want your writing to be edited mercilessly, then do not submit it here.<br />
@@@ -1488,8 -1484,6 +1485,8 @@@ It already exists.'
  'addsection-preload'               => '', # do not translate or duplicate this message to other languages
  'addsection-editintro'             => '', # do not translate or duplicate this message to other languages
  'defaultmessagetext'               => 'Default message text',
 +'invalid-content-data'             => 'Invalid content data',
 +'content-not-allowed-here'         => '"$1" content is not allowed on page [[$2]]',
  
  # Parser/template warnings
  'expensive-parserfunction-warning'        => "'''Warning:''' This page contains too many expensive parser function calls.
@@@ -1757,8 -1751,6 +1754,6 @@@ Details can be found in the [{{fullurl:
  'search-interwiki-default'         => '$1 results:',
  'search-interwiki-custom'          => '', # do not translate or duplicate this message to other languages
  'search-interwiki-more'            => '(more)',
- 'search-mwsuggest-enabled'         => 'with suggestions',
- 'search-mwsuggest-disabled'        => 'no suggestions',
  'search-relatedarticle'            => 'Related',
  'mwsuggest-disable'                => 'Disable AJAX suggestions',
  'searcheverything-enable'          => 'Search in all namespaces',
@@@ -1875,7 -1867,7 +1870,7 @@@ Here's a randomly-generated value you c
  'timezoneregion-indian'         => 'Indian Ocean',
  'timezoneregion-pacific'        => 'Pacific Ocean',
  'allowemail'                    => 'Enable e-mail from other users',
- 'prefs-searchoptions'           => 'Search options',
+ 'prefs-searchoptions'           => 'Search',
  'prefs-namespaces'              => 'Namespaces',
  'defaultns'                     => 'Otherwise search in these namespaces:',
  'default'                       => 'default',
@@@ -2211,16 -2203,16 +2206,16 @@@ this file is $2.'
  This might be due to a typo in the filename.
  Please check whether you really want to upload this file.',
  'windows-nonascii-filename'   => 'This wiki does not support filenames with special characters.',
- 'fileexists'                  => "A file with this name exists already, please check <strong>[[:$1]]</strong> if you are not sure if you want to change it.
- [[$1|thumb]]",
- 'filepageexists'              => "The description page for this file has already been created at <strong>[[:$1]]</strong>, but no file with this name currently exists.
+ 'fileexists'                  => 'A file with this name exists already, please check <strong>[[:$1]]</strong> if you are not sure if you want to change it.
+ [[$1|thumb]]',
+ 'filepageexists'              => 'The description page for this file has already been created at <strong>[[:$1]]</strong>, but no file with this name currently exists.
  The summary you enter will not appear on the description page.
  To make your summary appear there, you will need to manually edit it.
- [[$1|thumb]]",
- 'fileexists-extension'        => "A file with a similar name exists: [[$2|thumb]]
+ [[$1|thumb]]',
+ 'fileexists-extension'        => 'A file with a similar name exists: [[$2|thumb]]
  * Name of the uploading file: <strong>[[:$1]]</strong>
  * Name of the existing file: <strong>[[:$2]]</strong>
- Please choose a different name.",
+ Please choose a different name.',
  'fileexists-thumbnail-yes'    => "The file seems to be an image of reduced size ''(thumbnail)''.
  [[$1|thumb]]
  Please check the file <strong>[[:$1]]</strong>.
@@@ -2799,38 -2791,38 +2794,38 @@@ There may be [[{{MediaWiki:Listgrouprig
  'listgrouprights-removegroup-self-all' => 'Remove all groups from own account',
  
  # E-mail user
- 'mailnologin'          => 'No send address',
- 'mailnologintext'      => 'You must be [[Special:UserLogin|logged in]] and have a valid e-mail address in your [[Special:Preferences|preferences]] to send e-mail to other users.',
- 'emailuser'            => 'E-mail this user',
- 'emailuser-title-target' => 'E-mail this {{GENDER:$1|user}}',
+ 'mailnologin'              => 'No send address',
+ 'mailnologintext'          => 'You must be [[Special:UserLogin|logged in]] and have a valid e-mail address in your [[Special:Preferences|preferences]] to send e-mail to other users.',
+ 'emailuser'                => 'E-mail this user',
+ 'emailuser-title-target'   => 'E-mail this {{GENDER:$1|user}}',
  'emailuser-title-notarget' => 'E-mail user',
- 'emailuser-summary'    => '', # do not translate or duplicate this message to other languages
- 'emailpage'            => 'E-mail user',
- 'emailpagetext'        => 'You can use the form below to send an e-mail message to this user.
+ 'emailuser-summary'        => '', # do not translate or duplicate this message to other languages
+ 'emailpage'                => 'E-mail user',
+ 'emailpagetext'            => 'You can use the form below to send an e-mail message to this user.
  The e-mail address you entered in [[Special:Preferences|your user preferences]] will appear as the "From" address of the e-mail, so the recipient will be able to reply directly to you.',
- 'usermailererror'      => 'Mail object returned error:',
- 'defemailsubject'      => '{{SITENAME}} e-mail from user "$1"',
- 'usermaildisabled'     => 'User e-mail disabled',
- 'usermaildisabledtext' => 'You cannot send e-mail to other users on this wiki',
- 'noemailtitle'         => 'No e-mail address',
- 'noemailtext'          => 'This user has not specified a valid e-mail address.',
- 'nowikiemailtitle'     => 'No e-mail allowed',
- 'nowikiemailtext'      => 'This user has chosen not to receive e-mail from other users.',
- 'emailnotarget'        => 'Non-existent or invalid username for recipient.',
- 'emailtarget'          => 'Enter username of recipient',
- 'emailusername'        => 'Username:',
- 'emailusernamesubmit'  => 'Submit',
- 'email-legend'         => 'Send an e-mail to another {{SITENAME}} user',
- 'emailfrom'            => 'From:',
- 'emailto'              => 'To:',
- 'emailsubject'         => 'Subject:',
- 'emailmessage'         => 'Message:',
- 'emailsend'            => 'Send',
- 'emailccme'            => 'E-mail me a copy of my message.',
- 'emailccsubject'       => 'Copy of your message to $1: $2',
- 'emailsent'            => 'E-mail sent',
- 'emailsenttext'        => 'Your e-mail message has been sent.',
- 'emailuserfooter'      => 'This e-mail was sent by $1 to $2 by the "E-mail user" function at {{SITENAME}}.',
+ 'usermailererror'          => 'Mail object returned error:',
+ 'defemailsubject'          => '{{SITENAME}} e-mail from user "$1"',
+ 'usermaildisabled'         => 'User e-mail disabled',
+ 'usermaildisabledtext'     => 'You cannot send e-mail to other users on this wiki',
+ 'noemailtitle'             => 'No e-mail address',
+ 'noemailtext'              => 'This user has not specified a valid e-mail address.',
+ 'nowikiemailtitle'         => 'No e-mail allowed',
+ 'nowikiemailtext'          => 'This user has chosen not to receive e-mail from other users.',
+ 'emailnotarget'            => 'Non-existent or invalid username for recipient.',
+ 'emailtarget'              => 'Enter username of recipient',
+ 'emailusername'            => 'Username:',
+ 'emailusernamesubmit'      => 'Submit',
+ 'email-legend'             => 'Send an e-mail to another {{SITENAME}} user',
+ 'emailfrom'                => 'From:',
+ 'emailto'                  => 'To:',
+ 'emailsubject'             => 'Subject:',
+ 'emailmessage'             => 'Message:',
+ 'emailsend'                => 'Send',
+ 'emailccme'                => 'E-mail me a copy of my message.',
+ 'emailccsubject'           => 'Copy of your message to $1: $2',
+ 'emailsent'                => 'E-mail sent',
+ 'emailsenttext'            => 'Your e-mail message has been sent.',
+ 'emailuserfooter'          => 'This e-mail was sent by $1 to $2 by the "E-mail user" function at {{SITENAME}}.',
  
  # User Messenger
  'usermessage-summary'  => 'Leaving system message.',
@@@ -3073,8 -3065,8 +3068,8 @@@ You may have a bad link, or the revisio
  'undeletedrevisions'           => '{{PLURAL:$1|1 revision|$1 revisions}} restored',
  'undeletedrevisions-files'     => '{{PLURAL:$1|1 revision|$1 revisions}} and {{PLURAL:$2|1 file|$2 files}} restored',
  'undeletedfiles'               => '{{PLURAL:$1|1 file|$1 files}} restored',
 -'cannotundelete'               => 'Undelete failed;
 -someone else may have undeleted the page first.',
 +'cannotundelete'               => 'Undelete failed:
 +$1',
  'undeletedpage'                => "'''$1 has been restored'''
  
  Consult the [[Special:Log/delete|deletion log]] for a record of recent deletions and restorations.",
@@@ -3396,7 -3388,6 +3391,7 @@@ cannot move a page over itself.'
  'immobile-target-namespace-iw' => 'Interwiki link is not a valid target for page move.',
  'immobile-source-page'         => 'This page is not movable.',
  'immobile-target-page'         => 'Cannot move to that destination title.',
 +'bad-target-model'             => 'The desired destination uses a different content model. Can not convert from $1 to $2.',
  'imagenocrossnamespace'        => 'Cannot move file to non-file namespace',
  'nonfile-cannot-move-to-file'  => 'Cannot move non-file to file namespace',
  'imagetypemismatch'            => 'The new file extension does not match its type',
@@@ -3530,7 -3521,6 +3525,6 @@@ Please try again.'
  # JavaScriptTest
  'javascripttest'                           => 'JavaScript testing',
  'javascripttest-backlink'                  => '< $1', # do not translate or duplicate this message to other languages
- 'javascripttest-disabled'                  => 'This function has not been enabled on this wiki.',
  'javascripttest-title'                     => 'Running $1 tests',
  'javascripttest-pagetext-noframework'      => 'This page is reserved for running JavaScript tests.',
  'javascripttest-pagetext-unknownframework' => 'Unknown testing framework "$1".',
@@@ -3757,7 -3747,7 +3751,7 @@@ This is probably caused by a link to a 
  'pageinfo-views'               => 'Number of views',
  'pageinfo-watchers'            => 'Number of page watchers',
  'pageinfo-redirects-name'      => 'Redirects to this page',
- 'pageinfo-redirects-value'     => '$1',
+ 'pageinfo-redirects-value'     => '$1', # only translate this message to other languages if you have to change it
  'pageinfo-subpages-name'       => 'Subpages of this page',
  'pageinfo-subpages-value'      => '$1 ($2 {{PLURAL:$2|redirect|redirects}}; $3 {{PLURAL:$3|non-redirect|non-redirects}})',
  'pageinfo-firstuser'           => 'Page creator',
  'pageinfo-authors'             => 'Total number of distinct authors',
  'pageinfo-recent-edits'        => 'Recent number of edits (within past $1)',
  'pageinfo-recent-authors'      => 'Recent number of distinct authors',
- 'pageinfo-restriction'         => 'Page protection ({{lcfirst:$1}})',
  'pageinfo-magic-words'         => 'Magic {{PLURAL:$1|word|words}} ($1)',
  'pageinfo-hidden-categories'   => 'Hidden {{PLURAL:$1|category|categories}} ($1)',
  'pageinfo-templates'           => 'Transcluded {{PLURAL:$1|template|templates}} ($1)',
  'pageinfo-footer'              => '-', # do not translate or duplicate this message to other languages
+ 'pageinfo-toolboxlink'         => 'Page information',
  
  # Skin names
  'skinname-standard'    => 'Classic', # only translate this message to other languages if you have to change it
  'nextdiff'     => 'Newer edit →',
  
  # Media information
- 'mediawarning'           => "'''Warning''': This file type may contain malicious code.
+ 'mediawarning'                => "'''Warning''': This file type may contain malicious code.
  By executing it, your system may be compromised.",
- 'imagemaxsize'           => "Image size limit:<br />''(for file description pages)''",
- 'thumbsize'              => 'Thumbnail size:',
- 'widthheight'            => '$1 × $2', # only translate this message to other languages if you have to change it
- 'widthheightpage'        => '$1 × $2, $3 {{PLURAL:$3|page|pages}}',
- 'file-info'              => 'file size: $1, MIME type: $2',
- 'file-info-size'         => '$1 × $2 pixels, file size: $3, MIME type: $4',
- 'file-info-size-pages'   => '$1 × $2 pixels, file size: $3, MIME type: $4, $5 {{PLURAL:$5|page|pages}}',
- 'file-nohires'           => 'No higher resolution available.',
- 'svg-long-desc'          => 'SVG file, nominally $1 × $2 pixels, file size: $3',
- 'svg-long-desc-animated' => 'Animated SVG file, nominally $1 × $2 pixels, file size: $3',
- 'show-big-image'         => 'Full resolution',
- 'show-big-image-preview' => 'Size of this preview: $1.',
- 'show-big-image-other'   => 'Other {{PLURAL:$2|resolution|resolutions}}: $1.',
- 'show-big-image-size'    => '$1 × $2 pixels',
- 'file-info-gif-looped'   => 'looped',
- 'file-info-gif-frames'   => '$1 {{PLURAL:$1|frame|frames}}',
- 'file-info-png-looped'   => 'looped',
- 'file-info-png-repeat'   => 'played $1 {{PLURAL:$1|time|times}}',
- 'file-info-png-frames'   => '$1 {{PLURAL:$1|frame|frames}}',
- 'file-no-thumb-animation'=> '\'\'\'Note: Due to technical limitations, thumbnails of this file will not be animated.\'\'\'',
- 'file-no-thumb-animation-gif' => '\'\'\'Note: Due to technical limitations, thumbnails of high resolution GIF images such as this one will not be animated.\'\'\'',
+ 'imagemaxsize'                => "Image size limit:<br />''(for file description pages)''",
+ 'thumbsize'                   => 'Thumbnail size:',
+ 'widthheight'                 => '$1 × $2', # only translate this message to other languages if you have to change it
+ 'widthheightpage'             => '$1 × $2, $3 {{PLURAL:$3|page|pages}}',
+ 'file-info'                   => 'file size: $1, MIME type: $2',
+ 'file-info-size'              => '$1 × $2 pixels, file size: $3, MIME type: $4',
+ 'file-info-size-pages'        => '$1 × $2 pixels, file size: $3, MIME type: $4, $5 {{PLURAL:$5|page|pages}}',
+ 'file-nohires'                => 'No higher resolution available.',
+ 'svg-long-desc'               => 'SVG file, nominally $1 × $2 pixels, file size: $3',
+ 'svg-long-desc-animated'      => 'Animated SVG file, nominally $1 × $2 pixels, file size: $3',
+ 'show-big-image'              => 'Full resolution',
+ 'show-big-image-preview'      => 'Size of this preview: $1.',
+ 'show-big-image-other'        => 'Other {{PLURAL:$2|resolution|resolutions}}: $1.',
+ 'show-big-image-size'         => '$1 × $2 pixels',
+ 'file-info-gif-looped'        => 'looped',
+ 'file-info-gif-frames'        => '$1 {{PLURAL:$1|frame|frames}}',
+ 'file-info-png-looped'        => 'looped',
+ 'file-info-png-repeat'        => 'played $1 {{PLURAL:$1|time|times}}',
+ 'file-info-png-frames'        => '$1 {{PLURAL:$1|frame|frames}}',
+ 'file-no-thumb-animation'     => "'''Note: Due to technical limitations, thumbnails of this file will not be animated.'''",
+ 'file-no-thumb-animation-gif' => "'''Note: Due to technical limitations, thumbnails of high resolution GIF images such as this one will not be animated.'''",
  
  # Special:NewFiles
  'newimages'             => 'Gallery of new files',
@@@ -4467,9 -4457,10 +4461,10 @@@ This confirmation code will expire at $
  'invalidateemail'           => 'Cancel e-mail confirmation',
  
  # Scary transclusion
- 'scarytranscludedisabled' => '[Interwiki transcluding is disabled]',
- 'scarytranscludefailed'   => '[Template fetch failed for $1]',
- 'scarytranscludetoolong'  => '[URL is too long]',
+ 'scarytranscludedisabled'          => '[Interwiki transcluding is disabled]',
+ 'scarytranscludefailed'            => '[Template fetch failed for $1]',
+ 'scarytranscludefailed-httpstatus' => '[Template fetch failed for $1: HTTP $2]',
+ 'scarytranscludetoolong'           => '[URL is too long]',
  
  # Delete conflict
  'deletedwhileediting'      => "'''Warning''': This page was deleted after you started editing!",
@@@ -4703,7 -4694,7 +4698,7 @@@ You can also [[Special:EditWatchlist|us
  'version-svn-revision'                  => '(r$2)', # only translate this message to other languages if you have to change it
  'version-license'                       => 'License',
  'version-poweredby-credits'             => "This wiki is powered by '''[//www.mediawiki.org/ MediaWiki]''', copyright © 2001-$1 $2.",
- 'version-poweredby-others'              => '[{{SERVER}}{{SCRIPTPATH}}/CREDITS others]',
+ 'version-poweredby-others'              => 'others',
  'version-license-info'                  => 'MediaWiki 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.
  
  MediaWiki 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.
@@@ -4882,7 -4873,7 +4877,7 @@@ This site is experiencing technical dif
  
  # Feedback
  'feedback-bugornote' => 'If you are ready to describe a technical problem in detail please [$1 report a bug].
- Otherwise, you can use the easy form below. Your comment will be added to the page "[$3 $2]", along with your username and what browser you are using.',
+ Otherwise, you can use the easy form below. Your comment will be added to the page "[$3 $2]", along with your username.',
  'feedback-subject'   => 'Subject:',
  'feedback-message'   => 'Message:',
  'feedback-cancel'    => 'Cancel',
  'feedback-bugcheck'  => 'Great! Just check that it is not already one of the [$1 known bugs].',
  'feedback-bugnew'    => 'I checked. Report a new bug',
  
+ # Search suggestions
+ 'searchsuggest-search'     => 'Search',
+ 'searchsuggest-containing' => 'containing...',
  # API errors
  'api-error-badaccess-groups'              => 'You are not permitted to upload files to this wiki.',
  'api-error-badtoken'                      => 'Internal error: Bad token.',
  'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
  'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
 +'content-model-wikitext' => 'wikitext',
 +'content-model-javascript' => 'JavaScript',
 +'content-model-css' => 'CSS',
 +'content-model-text' => 'plain text',
 +
  );
@@@ -476,7 -476,7 +476,7 @@@ An fáth ná ''$2''."
  # Login and logout pages
  'logouttext' => "'''Tá tú logáilte amach anois.'''
  
- Is féidir leat an {{SITENAME}} a úsáid fós gan ainm, nó is féidir leat [[Special:UserLogin|logáil isteach arís]] mar an úsáideoir céanna, nó mar úsáideoir eile.
+ Is féidir leat an {{SITENAME}} a úsáid fós gan ainm, nó is féidir leat <span class='plainlinks'>[$1 logáil isteach arís]</span> mar an úsáideoir céanna, nó mar úsáideoir eile.
  Tabhair faoi deara go taispeáinfear roinnt leathanaigh mar atá tú logáilte isteach fós, go dtí go ghlanfá amach do taisce líonleitheora.",
  'welcomecreation' => '== Tá fáilte romhat, $1! ==
  
@@@ -802,8 -802,6 +802,6 @@@ Treoir: (rth) = difríocht ón leagan r
  'search-interwiki-caption' => 'Comhthionscadail',
  'search-interwiki-default' => '$1 torthaí:',
  'search-interwiki-more' => '(níos mó)',
- 'search-mwsuggest-enabled' => 'le moltaí',
- 'search-mwsuggest-disabled' => 'gan mholtaí',
  'search-relatedarticle' => 'Gaolmhar',
  'mwsuggest-disable' => 'Díchumasaigh moltaí AJAX',
  'searchrelated' => 'gaolmhara',
@@@ -1773,9 -1771,6 +1771,9 @@@ iarradh sábháil. Is dócha gur nasc c
  'spamprotectionmatch' => 'Truicear ár scagaire dramhála ag an téacs seo a leanas: $1',
  'spambot_username' => 'MediaWiki turscar glanadh',
  
 +# Info page
 +'pageinfo-subjectpage' => 'Leathanach',
 +
  # Skin names
  'skinname-standard' => 'Clasaiceach',
  'skinname-nostalgia' => 'Sean-nós',
@@@ -2237,4 -2232,7 +2235,7 @@@ Rachaidh an cód deimhnithe seo as feid
  'feedback-message' => 'Teachtaireacht:',
  'feedback-cancel' => 'Cealaigh',
  
+ # Search suggestions
+ 'searchsuggest-search' => 'Cuardaigh',
  );
@@@ -82,6 -82,8 +82,8 @@@
   * @author Nemo bis
   * @author Niels
   * @author Nike
+  * @author Njardarlogar
+  * @author Nnemo
   * @author Node ue
   * @author Octahedron80
   * @author Od1n
@@@ -553,9 -555,9 +555,9 @@@ The format is: "{{int:youhavenewmessage
  {{Identical|New messages}}',
  'newmessagesdifflink' => 'This is the second link displayed in an orange rectangle when a user gets a message on his talk page. Used in message {{msg-mw|youhavenewmessages}} (as parameter $2).',
  'youhavenewmessagesfromusers' => 'New talk indicator message: the message appearing when someone edited your user talk page.
- The message takes three parameters; 
- *$1 {{msg-mw|newmessageslinkplural}}, 
- *$2 {{msg-mw|newmessagesdifflinkplural}}, and 
+ The message takes three parameters;
+ *$1 {{msg-mw|newmessageslinkplural}},
+ *$2 {{msg-mw|newmessagesdifflinkplural}}, and
  *$3 the number of authors who have edited the talk page since the owning user last viewed it.',
  'youhavenewmessagesmanyusers' => 'New talk indicator message: the message appearing when someone edited your user talk page. Used when more than 10 users edited the user talk page since the owning user last viewed it, similar to{{msg-mw|youhavenewmessages}}. Parameters:
  * $1 is {{msg-mw|newmessageslinkplural}},
@@@ -705,8 -707,12 +707,12 @@@ $1 is a filename, I think.'
  * $1: the protection type, e.g. "protect" for fully protected pages',
  'viewsourcetext' => 'The text shown when displaying the source of a page that the user has no permission to edit',
  'viewyourtext' => 'Same as {{msg-mw|viewsourcetext}} but when showing the text submitted by the user, this happens e.g. when the user was blocked while he is editing the page',
- 'protectedinterface' => 'Message shown if a user without the "editinterface" right tries to edit a page in the MediaWiki namespace.',
- 'editinginterface' => 'A message shown when editing pages in the namespace MediaWiki:.',
+ 'protectedinterface' => 'Message shown if a user without the "editinterface" right tries to edit a page in the MediaWiki namespace.
+ See also {{msg-mw|editinginterface}}.',
+ 'editinginterface' => 'A message shown when editing pages in the namespace MediaWiki:.
+ See also {{msg-mw|protectedinterface}}.',
  'ns-specialprotected' => 'Error message displayed when trying to edit a page in the Special namespace',
  'titleprotected' => 'Use $1 for GENDER.',
  'invalidtitle-knownnamespace' => 'Displayed when an invalid title was encountered (generally in a list), but the namespace number is known to exist.
  'exception-nologin-text' => 'Generic reason displayed on error page when a user is not logged in. Message used by the UserNotLoggedIn exception.',
  
  # Login and logout pages
- 'logouttext' => 'Log out message',
+ 'logouttext' => 'Log out message
+ * $1 is an URL to [[Special:Userlogin]] containing returnto and returntoquery parameters',
  'welcomecreation' => 'The welcome message users see after registering a user account. $1 is the username of the new user.',
  'yourname' => "In user preferences
  
@@@ -793,7 -800,7 +800,7 @@@ $1 is the minimum number of characters 
  'mailmypassword' => 'Shown at [[Special:UserLogin]]',
  'passwordremindertitle' => 'Title of e-mail which contains temporary password',
  'passwordremindertext' => 'This text is used in an e-mail sent when a user requests a new temporary password (he has forgotten his password) or when an sysop creates a new user account choosing to have password and username sent to the new user by e-mail.
- * $1 is an IP addres. Example: 123.123.123.123
+ * $1 is an IP address. Example: 123.123.123.123
  * $2 is a username. Example: Joe
  * $3 is a password. Example: er##@fdas!
  * $4 is a URL. Example: http://wiki.example.com
@@@ -1053,10 -1060,6 +1060,10 @@@ Please report at [[Support]] if you ar
  'moveddeleted-notice' => 'Shown on top of a deleted page in normal view modus ([http://translatewiki.net/wiki/Test example]).',
  'edit-conflict' => "An 'Edit conflict' happens when more than one edit is being made to a page at the same time. This would usually be caused by separate individuals working on the same page. However, if the system is slow, several edits from one individual could back up and attempt to apply simultaneously - causing the conflict.",
  'defaultmessagetext' => 'Caption above the default message text shown on the left-hand side of a diff displayed after clicking “Show changes” when creating a new page in the MediaWiki: namespace',
 +'invalid-content-data'             => 'Error message indicating that the page\'s content can not be saved because it is invalid. This may occurr for some non-text content types.',
 +'content-not-allowed-here'         => 'Error message indicating that the desired content model is not supported in given localtion.
 +* $1 is the human readable name of the content model
 +* $1 is the title of the page in question.',
  
  # Parser/template warnings
  'expensive-parserfunction-warning' => 'On some (expensive) [[MetaWikipedia:Help:ParserFunctions|parser functions]] (e.g. <code><nowiki>{{#ifexist:}}</nowiki></code>) there is a limit of how many times it may be used. This is an error message shown when the limit is exceeded.
@@@ -1934,7 -1937,7 +1941,7 @@@ Does not work under $wgMiserMode ([[mwr
  
  {{Identical|Upload file}}',
  'uploadnologin' => '{{Identical|Not logged in}}',
- 'uploadtext' => "{{doc-important|''thumb'' and ''left'' are magic words. Leave it untranslated!}}
+ 'uploadtext' => "{{doc-important|''thumb'' and ''left'' are magic words. Leave them untranslated!}}
  Text displayed when uploading a file using [[Special:Upload]].",
  'upload-permitted' => 'Used in [[Special:Upload]].',
  'upload-preferred' => 'Used in [[Special:Upload]].',
@@@ -2102,7 -2105,7 +2109,7 @@@ Used on [[Special:UploadWizard]].'
  'img-auth-accessdenied' => '[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Access Denied
  {{Identical|Access denied}}',
  'img-auth-nopathinfo' => '[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Missing PATH_INFO - see english description
* This is plain text. Do not use any wiki syntax.',
{{Doc-important|This is plain text. Do not use any wiki syntax.}}',
  'img-auth-notindir' => '[[mw:Manual:Image Authorization|Manual:Image Authorization]]: When the specified path is not in upload directory.',
  'img-auth-badtitle' => '[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Bad title, $1 is the invalid title',
  'img-auth-nologinnWL' => '[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Logged in and file not whitelisted. $1 is the file not in whitelist.',
@@@ -2886,8 -2889,6 +2893,8 @@@ Options for the duration of the page pr
  {{Identical|Reset}}',
  'undeleteinvert' => '{{Identical|Invert selection}}',
  'undeletecomment' => '{{Identical|Reason}}',
 +'cannotundelete' => 'Message shown when undeletion failed for some reason.
 +* <code>$1</code> is the combined wikitext of messages for all errors that caused the failure.',
  'undelete-search-title' => 'Page title when showing the search form in Special:Undelete',
  'undelete-search-submit' => '{{Identical|Search}}',
  'undelete-error' => 'Page title when a page could not be undeleted',
  
  # Namespace form on various pages
  'namespace' => 'This message is located at [[Special:Contributions]].',
- 'invert' => 'Displayed in [[Special:RecentChanges|RecentChanges]], [[Special:RecentChangesLinked|RecentChangesLinked]] and [[Special:Watchlist|Watchlist]]
+ 'invert' => 'Displayed in [[Special:RecentChanges|RecentChanges]], [[Special:RecentChangesLinked|RecentChangesLinked]] and [[Special:Watchlist|Watchlist]].
  
- {{Identical|Invert selection}}
+ This message means "Invert selection of namespace".
  
- This message has a tooltip {{msg-mw|tooltip-invert}}',
+ This message has a tooltip {{msg-mw|tooltip-invert}}
+ {{Identical|Invert selection}}',
  'tooltip-invert' => 'Used in [[Special:Recentchanges]] as a tooltip for the invert checkbox. See also the message {{msg-mw|invert}}',
  'namespace_association' => 'Used in [[Special:Recentchanges]] with a checkbox which selects the associated namespace to be added to the selected namespace, so that both are searched (or excluded depending on another checkbox selection). The association is between a namespace and its talk namespace.
  
@@@ -3191,10 -3193,6 +3199,10 @@@ Parameters
  'immobile-target-namespace-iw' => "This message appears when attempting to move a page, if a person has typed an interwiki link as a namespace prefix in the input box labelled 'To new title'.  The special page 'Movepage' cannot be used to move a page to another wiki.
  
  'Destination' can be used instead of 'target' in this message.",
 +'bad-target-model'             => "This message is shown when attempting to move a page, but the move would change the page's content model.
 +This may be the case when \$wgContentHandlerUseDB is set to false, because then a page's content model is derived from the page's title.
 +* $1: The localized name of the original page's content model.
 +* $2: The localized name of the content model used by the destination title.",
  'fix-double-redirects' => 'This is a checkbox in [[Special:MovePage]] which allows to move all redirects from the old title to the new title.',
  'protectedpagemovewarning' => 'Related message: [[MediaWiki:protectedpagewarning/{{#titleparts:{{PAGENAME}}|1|2}}]]
  {{Related|Semiprotectedpagewarning}}',
@@@ -3291,7 -3289,6 +3299,6 @@@ See also
  
  # JavaScriptTest
  'javascripttest' => 'Title of [[Special:JavaScriptTest|the special page]]',
- 'javascripttest-disabled' => 'Message displayed on [[Special:JavaScriptTest]] if this feature is disabled (it is disabled by default).',
  'javascripttest-title' => 'Title of the special page when running a test suite. Parameters:
  * $1 is the name of the framework, for example QUnit.',
  'javascripttest-pagetext-unknownframework' => 'Error message when given framework id is not found. $1 is the id of the framework.',
@@@ -3503,14 -3500,13 +3510,13 @@@ See also {{msg-mw|Anonuser}} and {{msg-
  'pageinfo-authors' => 'The total number of users who have edited the page.',
  'pageinfo-recent-edits' => 'The number of times the page has been edited recently. $1 is a localised duration (e.g. 9 days).',
  'pageinfo-recent-authors' => 'The number of users who have edited the page recently.',
- 'pageinfo-restriction' => 'Parameters:
- * $1 is the type of page protection (message restriction-$type, preferably in lowercase). If your language doesn\'t have small and capital letters, you can simply write <nowiki>$1</nowiki>.',
  'pageinfo-magic-words' => 'The list of magic words on the page. Parameters:
  * $1 is the number of magic words on the page.',
  'pageinfo-hidden-categories' => 'The list of hidden categories on the page. Parameters:
  * $1 is the number of hidden categories on the page.',
  'pageinfo-templates' => 'The list of templates transcluded within the page. Parameters:
  * $1 is the number of templates transcluded within the page.',
+ 'pageinfo-toolboxlink' => "Information link for the page (like 'What links here', but to action=info for the current page instead)",
  
  # Skin names
  'skinname-standard' => '{{optional}}
@@@ -4353,6 -4349,12 +4359,12 @@@ See also [[MediaWiki:Confirmemail_body_
  'confirmemail_invalidated' => 'This is the text of the special page [[Special:InvalidateEmail|InvalidateEmail]] (with the title in {{msg-mw|Invalidateemail}}) where user goes if he chooses the cancel e-mail confirmation link from the confirmation e-mail.',
  'invalidateemail' => "This is the '''name of the special page''' where user goes if he chooses the cancel e-mail confirmation link from the confirmation e-mail.",
  
+ # Scary transclusion
+ 'scarytranscludedisabled' => 'Shown when scary transclusion is disabled.',
+ 'scarytranscludefailed' => 'Shown when the HTTP request for the template failed.',
+ 'scarytranscludefailed-httpstatus' => 'Identical to {{msg-mw|scarytranscludefailed}}, but shows the HTTP error which was received.',
+ 'scarytranscludetoolong' => 'The URL was too long.',
  'unit-pixel' => '{{optional}}',
  
  # action=purge
@@@ -4624,7 -4626,8 +4636,8 @@@ This is being used in [[Special:Version
  'version-software-product' => 'Shown in [[Special:Version]]',
  'version-software-version' => '{{Identical|Version}}',
  'version-entrypoints' => 'Header on [[Special:Version]] above a table that lists the URLs of various entry points in this MediaWiki installation. Entry points are the "places" where the wiki\'s content and information can be accessed in various ways, for instance the standard index.php which shows normal pages, histories etc.',
- 'version-entrypoints-header-entrypoint' => 'ପ୍ରବେଶ ବିନ୍ଦୁ',
+ 'version-entrypoints-header-entrypoint' => 'Header for the first column in the entry points table on [[Special:Version]].
+ See also {{msg-mw|Version-entrypoints}}',
  'version-entrypoints-header-url' => 'Header for the second column in the entry points table on [[Special:Version]].',
  'version-entrypoints-articlepath' => 'A short description of the article path entry point. Links to the mediawiki.org documentation page for $wgArticlePath.',
  'version-entrypoints-scriptpath' => 'A short description of the script path entry point. Links to the mediawiki.org documentation page for $wgScriptPath.',
@@@ -4841,6 -4844,12 +4854,12 @@@ $4 is the gender of the target user.'
  'feedback-bugcheck' => 'Message that appears before the user submits a bug, reminding them to check for known bugs.',
  'feedback-bugnew' => 'Button label - asserts that the user has checked for existing bugs. When clicked will launch a bugzilla form to add a new bug in a new tab or window',
  
+ # Search suggestions
+ 'searchsuggest-search' => 'Greyed out default text in the simple search box in the Vector skin. (It disappears and lets the user enter the requested search terms when the search box receives focus.)
+ {{Identical|Search}}',
+ 'searchsuggest-containing' => 'Label used in the special item of the search suggestions list which gives the user an option to perform a full text search for the term.',
  # API errors
  'api-error-badaccess-groups' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-badtoken' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHandler::getContentModel()
 +'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
 +'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
 +'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
 +'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
 +
  );
@@@ -482,19 -482,11 +482,11 @@@ abstract class Maintenance 
                        $this->error( 'Cannot get command line arguments, register_argc_argv is set to false', true );
                }
  
-               if ( version_compare( phpversion(), '5.2.4' ) >= 0 ) {
-                       // Send PHP warnings and errors to stderr instead of stdout.
-                       // This aids in diagnosing problems, while keeping messages
-                       // out of redirected output.
-                       if ( ini_get( 'display_errors' ) ) {
-                               ini_set( 'display_errors', 'stderr' );
-                       }
-                       // Don't touch the setting on earlier versions of PHP,
-                       // as setting it would disable output if you'd wanted it.
-                       // Note that exceptions are also sent to stderr when
-                       // command-line mode is on, regardless of PHP version.
+               // Send PHP warnings and errors to stderr instead of stdout.
+               // This aids in diagnosing problems, while keeping messages
+               // out of redirected output.
+               if ( ini_get( 'display_errors' ) ) {
+                       ini_set( 'display_errors', 'stderr' );
                }
  
                $this->loadParamsAndArgs();
                        $title = $titleObj->getPrefixedDBkey();
                        $this->output( "$title..." );
                        # Update searchindex
 -                      $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getText() );
 +                      # TODO: pass the Content object to SearchUpdate, let the search engine decide how to deal with it.
 +                      $u = new SearchUpdate( $pageId, $titleObj->getText(), $rev->getContent()->getTextForSearchIndex() );
                        $u->doUpdate();
                        $this->output( "\n" );
                }
@@@ -1,5 -1,7 +1,7 @@@
  <?php
  /**
+  * Test revision text compression and decompression.
+  *
   * 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
@@@ -16,8 -18,7 +18,7 @@@
   * http://www.gnu.org/copyleft/gpl.html
   *
   * @file
-  * @ingroup Maintenance
-  * @see wfWaitForSlaves()
+  * @ingroup Maintenance ExternalStorage
   */
  
  $optionsWithArgs = array( 'start', 'limit', 'type' );
@@@ -65,7 -66,7 +66,7 @@@ $uncompressedSize = 0
  $t = -microtime( true );
  foreach ( $res as $row ) {
        $revision = new Revision( $row );
 -      $text = $revision->getText();
 +      $text = $revision->getSerializedData();
        $uncompressedSize += strlen( $text );
        $hashes[$row->rev_id] = md5( $text );
        $keys[$row->rev_id] = $blob->addItem( $text );
diff --combined maintenance/tables.sql
@@@ -260,10 -260,7 +260,10 @@@ CREATE TABLE /*_*/page 
    page_latest int unsigned NOT NULL,
  
    -- Uncompressed length in bytes of the page's current source text.
 -  page_len int unsigned NOT NULL
 +  page_len int unsigned NOT NULL,
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  page_content_model  int unsigned  default NULL
  ) /*$wgDBTableOptions*/;
  
  CREATE UNIQUE INDEX /*i*/name_title ON /*_*/page (page_namespace,page_title);
@@@ -319,13 -316,7 +319,13 @@@ CREATE TABLE /*_*/revision 
    rev_parent_id int unsigned default NULL,
  
    -- SHA-1 text content hash in base-36
 -  rev_sha1 varbinary(32) NOT NULL default ''
 +  rev_sha1 varbinary(32) NOT NULL default '',
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  rev_content_model  int unsigned  default NULL,
 +
 +  -- content format, see CONTENT_FORMAT_XXX constants
 +  rev_content_format int unsigned  default NULL
  
  ) /*$wgDBTableOptions*/ MAX_ROWS=10000000 AVG_ROW_LENGTH=1024;
  -- In case tables are created as MyISAM, use row hints for MySQL <5.0 to avoid 4GB limit
@@@ -436,14 -427,7 +436,14 @@@ CREATE TABLE /*_*/archive 
    ar_parent_id int unsigned default NULL,
  
    -- SHA-1 text content hash in base-36
 -  ar_sha1 varbinary(32) NOT NULL default ''
 +  ar_sha1 varbinary(32) NOT NULL default '',
 +
 +  -- content model, see CONTENT_MODEL_XXX constants
 +  ar_content_model  int unsigned default NULL,
 +
 +  -- content format, see CONTENT_FORMAT_XXX constants
 +  ar_content_format int unsigned default NULL
 +
  ) /*$wgDBTableOptions*/;
  
  CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
@@@ -705,9 -689,6 +705,6 @@@ CREATE TABLE /*_*/site_stats 
    -- Number of users that still edit
    ss_active_users bigint default '-1',
  
-   -- Deprecated, no longer updated as of 1.5
-   ss_admins int default '-1',
    -- Number of images, equivalent to SELECT COUNT(*) FROM image
    ss_images int default 0
  ) /*$wgDBTableOptions*/;
@@@ -862,7 -843,7 +859,7 @@@ CREATE INDEX /*i*/img_size ON /*_*/imag
  -- Used by Special:Newimages and Special:ListFiles
  CREATE INDEX /*i*/img_timestamp ON /*_*/image (img_timestamp);
  -- Used in API and duplicate search
- CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1);
+ CREATE INDEX /*i*/img_sha1 ON /*_*/image (img_sha1(10));
  
  
  --
@@@ -900,7 -881,7 +897,7 @@@ CREATE INDEX /*i*/oi_usertext_timestam
  CREATE INDEX /*i*/oi_name_timestamp ON /*_*/oldimage (oi_name,oi_timestamp);
  -- oi_archive_name truncated to 14 to avoid key length overflow
  CREATE INDEX /*i*/oi_name_archive_name ON /*_*/oldimage (oi_name,oi_archive_name(14));
- CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1);
+ CREATE INDEX /*i*/oi_sha1 ON /*_*/oldimage (oi_sha1(10));
  
  
  --
@@@ -1061,10 -1042,6 +1058,6 @@@ CREATE TABLE /*_*/recentchanges 
    -- The type of change entry (RC_EDIT,RC_NEW,RC_LOG)
    rc_type tinyint unsigned NOT NULL default 0,
  
-   -- These may no longer be used, with the new move log.
-   rc_moved_to_ns tinyint unsigned NOT NULL default 0,
-   rc_moved_to_title varchar(255) binary NOT NULL default '',
    -- If the Recent Changes Patrol option is enabled,
    -- users may mark edits as having been reviewed to
    -- remove a warning flag on the RC list.
@@@ -1,23 -1,25 +1,25 @@@
  <?php
- # Copyright (C) 2004, 2010 Brion Vibber <brion@pobox.com>
- # http://www.mediawiki.org/
- #
- # 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
  /**
+  * Helper code for the MediaWiki parser test suite.
+  *
+  * Copyright © 2004, 2010 Brion Vibber <brion@pobox.com>
+  * http://www.mediawiki.org/
+  *
+  * 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
+  *
   * @todo Make this more independent of the configuration (and if possible the database)
   * @todo document
   * @file
@@@ -1204,7 -1206,7 +1206,7 @@@ class ParserTest 
                        }
                }
  
 -              $page->doEdit( $text, '', EDIT_NEW );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $title ), '', EDIT_NEW );
  
                $wgCapitalLinks = $oldCapitalLinks;
        }
@@@ -19,16 -19,6 +19,16 @@@ abstract class MediaWikiTestCase extend
        protected $reuseDB = false;
        protected $tablesUsed = array(); // tables with data
  
 +      protected $restoreGlobals = array( // global variables to restore for each test
 +              'wgLang',
 +              'wgContLang',
 +              'wgLanguageCode',
 +              'wgUser',
 +              'wgTitle',
 +      );
 +
 +      private $savedGlobals = array();
 +
        private static $dbSetup = false;
  
        /**
                return $fname;
        }
  
 -      protected function tearDown() {
 +      protected function setup() {
 +              parent::setup();
 +
 +              foreach ( $this->restoreGlobals as $var ) {
 +                      $v = $GLOBALS[ $var ];
 +
 +                      if ( is_object( $v ) || is_array( $v ) ) {
 +                              $v = clone $v;
 +                      }
 +
 +                      $this->savedGlobals[ $var ] = $v;
 +              }
 +      }
 +
 +      protected function teardown() {
                // Cleaning up temporary files
                foreach ( $this->tmpfiles as $fname ) {
                        if ( is_file( $fname ) || ( is_link( $fname ) ) ) {
                        }
                }
  
 -              parent::tearDown();
 +              // restore saved globals
 +              foreach ( $this->savedGlobals as $k => $v ) {
 +                      $GLOBALS[ $k ] = $v;
 +              }
 +
 +              parent::teardown();
        }
  
        function dbPrefix() {
                //Make 1 page with 1 revision
                $page = WikiPage::factory( Title::newFromText( 'UTPage' ) );
                if ( !$page->getId() == 0 ) {
 -                      $page->doEdit( 'UTContent',
 -                                                      'UTPageSummary',
 -                                                      EDIT_NEW,
 -                                                      false,
 -                                                      User::newFromName( 'UTSysop' ) );
 +                      $page->doEditContent(
 +                              new WikitextContent( 'UTContent' ),
 +                              'UTPageSummary',
 +                              EDIT_NEW,
 +                              false,
 +                              User::newFromName( 'UTSysop' ) );
                }
        }
  
         *         or list the tables under testing in $this->tablesUsed, or override the
         *         needsDB() method.
         */
-       protected function assertSelect( $table, $fields, $condition, Array $expectedRows ) {
+       protected function assertSelect( $table, $fields, $condition, array $expectedRows ) {
                if ( !$this->needsDB() ) {
                        throw new MWException( 'When testing database state, the test cases\'s needDB()' .
                                ' method should return true. Use @group Database or $this->tablesUsed.');
         * @param string $message
         */
        protected function assertType( $type, $actual, $message = '' ) {
-               if ( is_object( $actual ) ) {
+               if ( class_exists( $type ) || interface_exists( $type ) ) {
                        $this->assertInstanceOf( $type, $actual, $message );
                }
                else {
@@@ -10,30 -10,35 +10,35 @@@ class LinksUpdateTest extends MediaWiki
        function  __construct( $name = null, array $data = array(), $dataName = '' ) {
                parent::__construct( $name, $data, $dataName );
  
-               $this->tablesUsed = array_merge ( $this->tablesUsed,
-                                                                                       array( 'interwiki',
-                                                                                               'page_props',
-                                                                                               'pagelinks',
-                                                                                               'categorylinks',
-                                                                                               'langlinks',
-                                                                                               'externallinks',
-                                                                                               'imagelinks',
-                                                                                               'templatelinks',
-                                                                                               'iwlinks' ) );
+               $this->tablesUsed = array_merge( $this->tablesUsed,
+                       array(
+                               'interwiki',
+                               'page_props',
+                               'pagelinks',
+                               'categorylinks',
+                               'langlinks',
+                               'externallinks',
+                               'imagelinks',
+                               'templatelinks',
+                               'iwlinks'
+                       )
+               );
        }
  
        function setUp() {
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->replace( 'interwiki',
-                                               array('iw_prefix'),
-                                               array( 'iw_prefix' => 'linksupdatetest',
-                                                      'iw_url' => 'http://testing.com/wiki/$1',
-                                                      'iw_api' => 'http://testing.com/w/api.php',
-                                                      'iw_local' => 0,
-                                                      'iw_trans' => 0,
-                                                      'iw_wikiid' => 'linksupdatetest',
-                                               ) );
+               $dbw->replace(
+                       'interwiki',
+                       array( 'iw_prefix' ),
+                       array(
+                               'iw_prefix' => 'linksupdatetest',
+                               'iw_url' => 'http://testing.com/wiki/$1',
+                               'iw_api' => 'http://testing.com/w/api.php',
+                               'iw_local' => 0,
+                               'iw_trans' => 0,
+                               'iw_wikiid' => 'linksupdatetest',
+                       )
+               );
        }
  
        protected function makeTitleAndParserOutput( $name, $id ) {
  
        #@todo: test recursive, too!
  
-       protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, Array $expectedRows ) {
+       protected function assertLinksUpdate( Title $title, ParserOutput $parserOutput, $table, $fields, $condition, array $expectedRows ) {
                $update = new LinksUpdate( $title, $parserOutput );
  
 +              $update->beginTransaction();
                $update->doUpdate();
 +              $update->commitTransaction();
  
                $this->assertSelect( $table, $fields, $condition, $expectedRows );
        }
@@@ -1,10 -1,5 +1,10 @@@
  <?php
  
 +/**
 + *
 + * @group Database
 + *        ^--- needed for language cache stuff
 + */
  class TitleTest extends MediaWikiTestCase {
  
        function testLegalChars() {
@@@ -80,8 -75,7 +80,7 @@@
                        array( 'File:Test.jpg', 'Page', 'imagenocrossnamespace' )
                );
        }
-       
-       
        /**
         * @dataProvider provideCasesForGetpageviewlanguage
         */
  
                );
        }
+       /**
+        * @dataProvider provideBaseTitleCases
+        */
+       function testExtractingBaseTextFromTitle( $title, $expected, $msg='' ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expected,
+                       $title->getBaseText(),
+                       $msg
+               );
+       }
+       function provideBaseTitleCases() {
+               return array(
+                       # Title, expected base, optional message
+                       array('User:John_Doe/subOne/subTwo', 'John Doe/subOne' ),
+                       array('User:Foo/Bar/Baz', 'Foo/Bar' ),
+               );
+       }
+       /**
+        * @dataProvider provideRootTitleCases
+        */
+       function testExtractingRootTextFromTitle( $title, $expected, $msg='' ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expected,
+                       $title->getRootText(),
+                       $msg
+               );
+       }
+       function provideRootTitleCases() {
+               return array(
+                       # Title, expected base, optional message
+                       array('User:John_Doe/subOne/subTwo', 'John Doe' ),
+                       array('User:Foo/Bar/Baz', 'Foo' ),
+               );
+       }
+       /**
+        * @todo Handle $wgNamespacesWithSubpages cases
+        * @dataProvider provideSubpageTitleCases
+        */
+       function testExtractingSubpageTextFromTitle( $title, $expected, $msg='' ) {
+               $title = Title::newFromText( $title );
+               $this->assertEquals( $expected,
+                       $title->getSubpageText(),
+                       $msg
+               );
+       }
+       function provideSubpageTitleCases() {
+               return array(
+                       # Title, expected base, optional message
+                       array('User:John_Doe/subOne/subTwo', 'subTwo' ),
+                       array('User:John_Doe/subOne', 'subOne' ),
+               );
+       }
  }
@@@ -1,9 -1,6 +1,9 @@@
  <?php
  
  /**
 + * @group medium
 + * ^---- causes phpunit to use a higher timeout threshold
 + * 
   * @group FileRepo
   * @group FileBackend
   * @group medium
@@@ -53,7 -50,7 +53,7 @@@ class FileBackendTest extends MediaWiki
                        'parallelize' => 'implicit',
                        'backends'    => array(
                                array(
-                                       'name'          => 'localmutlitesting1',
+                                       'name'          => 'localmultitesting1',
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
@@@ -62,7 -59,7 +62,7 @@@
                                        'isMultiMaster' => false
                                ),
                                array(
-                                       'name'          => 'localmutlitesting2',
+                                       'name'          => 'localmultitesting2',
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
        private function doTestGetFileContents( $source, $content ) {
                $backendName = $this->backendClass();
  
-               $this->prepare( array( 'dir' => dirname( $source ) ) );
-               $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
-                       "Creation of file at $source succeeded with OK status ($backendName)." );
-               $newContents = $this->backend->getFileContents( array( 'src' => $source, 'latest' => 1 ) );
-               $this->assertNotEquals( false, $newContents,
-                       "Read of file at $source succeeded ($backendName)." );
+               $srcs = (array)$source;
+               $content = (array)$content;
+               foreach ( $srcs as $i => $src ) {
+                       $this->prepare( array( 'dir' => dirname( $src ) ) );
+                       $status = $this->backend->doOperation(
+                               array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+                       $this->assertGoodStatus( $status,
+                               "Creation of file at $src succeeded ($backendName)." );
+               }
  
-               $this->assertEquals( $content, $newContents,
-                       "Contents read match data at $source ($backendName)." );
+               if ( is_array( $source ) ) {
+                       $contents = $this->backend->getFileContentsMulti( array( 'srcs' => $source ) );
+                       foreach ( $contents as $path => $data ) {
+                               $this->assertNotEquals( false, $data, "Contents of $path exists ($backendName)." );
+                               $this->assertEquals( current( $content ), $data, "Contents of $path is correct ($backendName)." );
+                               next( $content );
+                       }
+                       $this->assertEquals( $source, array_keys( $contents ), "Contents in right order ($backendName)." );
+                       $this->assertEquals( count( $source ), count( $contents ), "Contents array size correct ($backendName)." );
+               } else {
+                       $data = $this->backend->getFileContents( array( 'src' => $source ) );
+                       $this->assertNotEquals( false, $data, "Contents of $source exists ($backendName)." );
+                       $this->assertEquals( $content[0], $data, "Contents of $source is correct ($backendName)." );
+               }
        }
  
        function provider_testGetFileContents() {
                $base = $this->baseStorePath();
                $cases[] = array( "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents" );
                $cases[] = array( "$base/unittest-cont1/e/b/some-other_file.txt", "more file contents" );
+               $cases[] = array(
+                       array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+                                "$base/unittest-cont1/e/a/z.txt" ),
+                       array( "contents xx", "contents xy", "contents xz" )
+               );
  
                return $cases;
        }
        private function doTestGetLocalCopy( $source, $content ) {
                $backendName = $this->backendClass();
  
-               $this->prepare( array( 'dir' => dirname( $source ) ) );
-               $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-               $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
-               $this->assertNotNull( $tmpFile,
-                       "Creation of local copy of $source succeeded ($backendName)." );
+               $srcs = (array)$source;
+               $content = (array)$content;
+               foreach ( $srcs as $i => $src ) {
+                       $this->prepare( array( 'dir' => dirname( $src ) ) );
+                       $status = $this->backend->doOperation(
+                               array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+                       $this->assertGoodStatus( $status,
+                               "Creation of file at $src succeeded ($backendName)." );
+               }
  
-               $contents = file_get_contents( $tmpFile->getPath() );
-               $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
+               if ( is_array( $source ) ) {
+                       $tmpFiles = $this->backend->getLocalCopyMulti( array( 'srcs' => $source ) );
+                       foreach ( $tmpFiles as $path => $tmpFile ) {
+                               $this->assertNotNull( $tmpFile,
+                                       "Creation of local copy of $path succeeded ($backendName)." );
+                               $contents = file_get_contents( $tmpFile->getPath() );
+                               $this->assertNotEquals( false, $contents, "Local copy of $path exists ($backendName)." );
+                               $this->assertEquals( current( $content ), $contents, "Local copy of $path is correct ($backendName)." );
+                               next( $content );
+                       }
+                       $this->assertEquals( $source, array_keys( $tmpFiles ), "Local copies in right order ($backendName)." );
+                       $this->assertEquals( count( $source ), count( $tmpFiles ), "Local copies array size correct ($backendName)." );
+               } else {
+                       $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
+                       $this->assertNotNull( $tmpFile,
+                               "Creation of local copy of $source succeeded ($backendName)." );
+                       $contents = file_get_contents( $tmpFile->getPath() );
+                       $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
+                       $this->assertEquals( $content[0], $contents, "Local copy of $source is correct ($backendName)." );
+               }
        }
  
        function provider_testGetLocalCopy() {
                $base = $this->baseStorePath();
                $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
                $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+               $cases[] = array(
+                       array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+                                "$base/unittest-cont1/e/a/z.txt" ),
+                       array( "contents xx", "contents xy", "contents xz" )
+               );
  
                return $cases;
        }
        private function doTestGetLocalReference( $source, $content ) {
                $backendName = $this->backendClass();
  
-               $this->prepare( array( 'dir' => dirname( $source ) ) );
-               $status = $this->create( array( 'content' => $content, 'dst' => $source ) );
-               $this->assertGoodStatus( $status,
-                       "Creation of file at $source succeeded ($backendName)." );
-               $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
-               $this->assertNotNull( $tmpFile,
-                       "Creation of local copy of $source succeeded ($backendName)." );
+               $srcs = (array)$source;
+               $content = (array)$content;
+               foreach ( $srcs as $i => $src ) {
+                       $this->prepare( array( 'dir' => dirname( $src ) ) );
+                       $status = $this->backend->doOperation(
+                               array( 'op' => 'create', 'content' => $content[$i], 'dst' => $src ) );
+                       $this->assertGoodStatus( $status,
+                               "Creation of file at $src succeeded ($backendName)." );
+               }
  
-               $contents = file_get_contents( $tmpFile->getPath() );
-               $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
+               if ( is_array( $source ) ) {
+                       $tmpFiles = $this->backend->getLocalReferenceMulti( array( 'srcs' => $source ) );
+                       foreach ( $tmpFiles as $path => $tmpFile ) {
+                               $this->assertNotNull( $tmpFile,
+                                       "Creation of local copy of $path succeeded ($backendName)." );
+                               $contents = file_get_contents( $tmpFile->getPath() );
+                               $this->assertNotEquals( false, $contents, "Local ref of $path exists ($backendName)." );
+                               $this->assertEquals( current( $content ), $contents, "Local ref of $path is correct ($backendName)." );
+                               next( $content );
+                       }
+                       $this->assertEquals( $source, array_keys( $tmpFiles ), "Local refs in right order ($backendName)." );
+                       $this->assertEquals( count( $source ), count( $tmpFiles ), "Local refs array size correct ($backendName)." );
+               } else {
+                       $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
+                       $this->assertNotNull( $tmpFile,
+                               "Creation of local copy of $source succeeded ($backendName)." );
+                       $contents = file_get_contents( $tmpFile->getPath() );
+                       $this->assertNotEquals( false, $contents, "Local ref of $source exists ($backendName)." );
+                       $this->assertEquals( $content[0], $contents, "Local ref of $source is correct ($backendName)." );
+               }
        }
  
        function provider_testGetLocalReference() {
                $base = $this->baseStorePath();
                $cases[] = array( "$base/unittest-cont1/e/a/z/some_file.txt", "some file contents" );
                $cases[] = array( "$base/unittest-cont1/e/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/e/a/\$odd&.txt", "test file contents" );
+               $cases[] = array(
+                       array( "$base/unittest-cont1/e/a/x.txt", "$base/unittest-cont1/e/a/y.txt",
+                                "$base/unittest-cont1/e/a/z.txt" ),
+                       array( "contents xx", "contents xy", "contents xz" )
+               );
  
                return $cases;
        }
                foreach ( $this->filesToPrune as $file ) {
                        @unlink( $file );
                }
-               $containers = array( 'unittest-cont1', 'unittest-cont2', 'unittest-cont3' );
+               $containers = array( 'unittest-cont1', 'unittest-cont2' );
                foreach ( $containers as $container ) {
                        $this->deleteFiles( $container );
                }
                $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) );
                if ( $iter ) {
                        foreach ( $iter as $file ) {
-                               $this->backend->delete( array( 'src' => "$base/$container/$file" ),
-                                       array( 'force' => 1, 'nonLocking' => 1 ) );
+                               $this->backend->quickDelete( array( 'src' => "$base/$container/$file" ) );
                        }
                }
                $this->backend->clean( array( 'dir' => "$base/$container", 'recursive' => 1 ) );
@@@ -1,7 -1,6 +1,6 @@@
  <?php
  
  /**
-  * This class is not directly tested. Instead it is extended by SearchDbTest.
   * @group Search
   * @group Database
   */
@@@ -94,7 -93,7 +93,7 @@@ class SearchEngineTest extends MediaWik
                LinkCache::singleton()->clear();
  
                $page = WikiPage::factory( $title );
 -              $page->doEdit( $text, $comment, 0, false, $user );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $title ), $comment, 0, false, $user );
  
                $this->pageList[] = array( $title, $page->getId() );
  
diff --combined thumb.php
+++ b/thumb.php
@@@ -71,9 -71,10 +71,9 @@@ function wfThumbHandle404() 
        }
        # Just get the URI path (REDIRECT_URL/REQUEST_URI is either a full URL or a path)
        if ( substr( $uriPath, 0, 1 ) !== '/' ) {
 -              $bits = wfParseUrl( $uriPath );
 -              if ( $bits && isset( $bits['path'] ) ) {
 -                      $uriPath = $bits['path'];
 -              } else {
 +              $uri = new Uri( $uriPath );
 +              $uriPath = $uri->getPath();
 +              if ( $uriPath === null ) {
                        wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
                        return;
                }
@@@ -125,7 -126,15 +125,15 @@@ function wfStreamThumb( array $params 
        $fileName = strtr( $fileName, '\\/', '__' );
  
        // Actually fetch the image. Method depends on whether it is archived or not.
-       if ( $isOld ) {
+       if ( $isTemp ) {
+               $repo = RepoGroup::singleton()->getLocalRepo()->getTempRepo();
+               $img = new UnregisteredLocalFile( null, $repo,
+                       # Temp files are hashed based on the name without the timestamp.
+                       # The thumbnails will be hashed based on the entire name however.
+                       # @TODO: fix this convention to actually be reasonable.
+                       $repo->getZonePath( 'public' ) . '/' . $repo->getTempHashPath( $fileName ) . $fileName
+               );
+       } elseif ( $isOld ) {
                // Format is <timestamp>!<name>
                $bits = explode( '!', $fileName, 2 );
                if ( count( $bits ) != 2 ) {
                        return;
                }
                $img = RepoGroup::singleton()->getLocalRepo()->newFromArchiveName( $title, $fileName );
-       } elseif ( $isTemp ) {
-               $repo = RepoGroup::singleton()->getLocalRepo()->getTempRepo();
-               // Format is <timestamp>!<name> or just <name>
-               $bits = explode( '!', $fileName, 2 );
-               // Get the name without the timestamp so hash paths are correctly computed
-               $title = Title::makeTitleSafe( NS_FILE, isset( $bits[1] ) ? $bits[1] : $fileName );
-               if ( !$title ) {
-                       wfThumbError( 404, wfMessage( 'badtitletext' )->text() );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-               $img = new UnregisteredLocalFile( $title, $repo,
-                       $repo->getZonePath( 'public' ) . '/' . $repo->getTempHashPath( $fileName ) . $fileName
-               );
        } else {
                $img = wfLocalFile( $fileName );
        }
                                if ( $wgVaryOnXFP ) {
                                        $varyHeader[] = 'X-Forwarded-Proto';
                                }
-                               $response->header( 'Vary: ' . implode( ', ', $varyHeader ) );
+                               if ( count( $varyHeader ) ) {
+                                       $response->header( 'Vary: ' . implode( ', ', $varyHeader ) );
+                               }
                                wfProfileOut( __METHOD__ );
                                return;
                        } else {
-                               wfThumbError( 404, 'The source file for the specified thumbnail does not exist.' );
+                               wfThumbError( 404, 'The given path of the specified thumbnail is incorrect.' );
                                wfProfileOut( __METHOD__ );
                                return;
                        }
                }
                $thumbPath = $img->getThumbPath( $thumbName );
                if ( $img->getRepo()->fileExists( $thumbPath ) ) {
-                       $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
+                       if ( count( $varyHeader ) ) {
+                               $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
+                       }
                        $img->getRepo()->streamFile( $thumbPath, $headers );
                        wfProfileOut( __METHOD__ );
                        return;
                wfProfileOut( __METHOD__ );
                return;
        }
-       $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
+       if ( count( $varyHeader ) ) {
+               $headers[] = 'Vary: ' . implode( ', ', $varyHeader );
+       }
  
        // Thumbnail isn't already there, so create the new thumbnail...
        try {
  function wfExtractThumbParams( $uriPath ) {
        $repo = RepoGroup::singleton()->getLocalRepo();
  
+       // Zone URL might be relative ("/images") or protocol-relative ("//lang.site/image")
        $zoneUriPath = $repo->getZoneHandlerUrl( 'thumb' )
                ? $repo->getZoneHandlerUrl( 'thumb' ) // custom URL
                : $repo->getZoneUrl( 'thumb' ); // default to main URL
-       // URL might be relative ("/images") or protocol-relative ("//lang.site/image")
        $bits = wfParseUrl( wfExpandUrl( $zoneUriPath, PROTO_INTERNAL ) );
        if ( $bits && isset( $bits['path'] ) ) {
                $zoneUriPath = $bits['path'];
        } else {
-               return null;
+               return null; // not a valid thumbnail URL
        }
  
-       $hashDirRegex = $subdirRegex = '';
+       $hashDirReg = $subdirReg = '';
        for ( $i = 0; $i < $repo->getHashLevels(); $i++ ) {
-               $subdirRegex .= '[0-9a-f]';
-               $hashDirRegex .= "$subdirRegex/";
+               $subdirReg .= '[0-9a-f]';
+               $hashDirReg .= "$subdirReg/";
+       }
+       $zoneReg = preg_quote( $zoneUriPath ); // regex for thumb zone URI
+       // Check if this is a thumbnail of an original in the local file repo
+       if ( preg_match( "!^$zoneReg/((archive/)?$hashDirReg([^/]*)/([^/]*))$!", $uriPath, $m ) ) {
+               list( /*all*/, $rel, $archOrTemp, $filename, $thumbname ) = $m;
+       // Check if this is a thumbnail of an temp file in the local file repo
+       } elseif ( preg_match( "!^$zoneReg/(temp/)($hashDirReg([^/]*)/([^/]*))$!", $uriPath, $m ) ) {
+               list( /*all*/, $archOrTemp, $rel, $filename, $thumbname ) = $m;
+       } else {
+               return null; // not a valid looking thumbnail request
        }
  
-       $thumbPathRegex = "!^" . preg_quote( $zoneUriPath ) .
-               "/((archive/|temp/)?$hashDirRegex([^/]*)/([^/]*))$!";
-       // Check if this is a valid looking thumbnail request...
-       if ( preg_match( $thumbPathRegex, $uriPath, $matches ) ) {
-               list( /* all */, $rel, $archOrTemp, $filename, $thumbname ) = $matches;
-               $filename = urldecode( $filename );
-               $thumbname = urldecode( $thumbname );
+       $filename = urldecode( $filename );
+       $thumbname = urldecode( $thumbname );
  
-               $params = array( 'f' => $filename, 'rel404' => $rel );
-               if ( $archOrTemp == 'archive/' ) {
-                       $params['archived'] = 1;
-               } elseif ( $archOrTemp == 'temp/' ) {
-                       $params['temp'] = 1;
-               }
+       $params = array( 'f' => $filename, 'rel404' => $rel );
+       if ( $archOrTemp === 'archive/' ) {
+               $params['archived'] = 1;
+       } elseif ( $archOrTemp === 'temp/' ) {
+               $params['temp'] = 1;
+       }
  
-               // Check if the parameters can be extracted from the thumbnail name...
-               if ( preg_match( '!^(page(\d*)-)*(\d*)px-[^/]*$!', $thumbname, $matches ) ) {
-                       list( /* all */, $pagefull, $pagenum, $size ) = $matches;
-                       $params['width'] = $size;
-                       if ( $pagenum ) {
-                               $params['page'] = $pagenum;
-                       }
-                       return $params; // valid thumbnail URL
-               // Hooks return false if they manage to *resolve* the parameters
-               } elseif ( !wfRunHooks( 'ExtractThumbParameters', array( $thumbname, &$params ) ) ) {
-                       return $params; // valid thumbnail URL (via extension or config)
+       // Check if the parameters can be extracted from the thumbnail name...
+       if ( preg_match( '!^(page(\d*)-)*(\d*)px-[^/]*$!', $thumbname, $matches ) ) {
+               list( /* all */, $pagefull, $pagenum, $size ) = $matches;
+               $params['width'] = $size;
+               if ( $pagenum ) {
+                       $params['page'] = $pagenum;
                }
+               return $params; // valid thumbnail URL
+       // Hooks return false if they manage to *resolve* the parameters
+       } elseif ( !wfRunHooks( 'ExtractThumbParameters', array( $thumbname, &$params ) ) ) {
+               return $params; // valid thumbnail URL (via extension or config)
        }
  
        return null; // not a valid thumbnail URL