merged latest master
authordaniel <daniel.kinzler@wikimedia.de>
Mon, 14 May 2012 21:24:18 +0000 (23:24 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Mon, 14 May 2012 21:24:18 +0000 (23:24 +0200)
35 files changed:
1  2 
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/EditPage.php
includes/Export.php
includes/ImagePage.php
includes/LinksUpdate.php
includes/Namespace.php
includes/Revision.php
includes/Title.php
includes/WikiPage.php
includes/api/ApiDelete.php
includes/api/ApiEditPage.php
includes/api/ApiMain.php
includes/api/ApiParse.php
includes/api/ApiPurge.php
includes/api/ApiQueryRevisions.php
includes/diff/DifferenceEngine.php
includes/installer/Ibm_db2Updater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/SqliteUpdater.php
includes/job/RefreshLinksJob.php
includes/parser/ParserOutput.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/specials/SpecialUndelete.php
languages/messages/MessagesEn.php
languages/messages/MessagesQqq.php
maintenance/tables.sql
tests/phpunit/includes/RevisionStorageTest.php
tests/phpunit/includes/TitleMethodsTest.php
tests/phpunit/includes/WikiPageTest.php
tests/phpunit/includes/filerepo/FileBackendTest.php
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/phpunit.php

diff --combined includes/Article.php
@@@ -37,14 -37,7 +37,14 @@@ class Article extends Page 
         */
        public $mParserOptions;
  
 -      var $mContent;                    // !<
 +      var $mContent;                    // !< #BC cruft
 +
 +      /**
 +       * @var Content
 +       * @since 1.WD
 +       */
 +      var $mContentObject;
 +
        var $mContentLoaded = false;      // !<
        var $mOldId;                      // !<
  
                if ( !$page ) {
                        switch( $title->getNamespace() ) {
                                case NS_FILE:
 -                                      $page = new ImagePage( $title );
 +                                      $page = new ImagePage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                case NS_CATEGORY:
 -                                      $page = new CategoryPage( $title );
 +                                      $page = new CategoryPage( $title ); #FIXME: teach ImagePage to use ContentHandler
                                        break;
                                default:
 -                                      $page = new Article( $title );
 +                                      $handler = ContentHandler::getForTitle( $title );
 +                                      $page = $handler->createArticle( $title );
                        }
                }
                $page->setContext( $context );
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
-        * @return string The text of this revision
 +       * @deprecated in 1.WD; use getContentObject() instead
 +       *
+        * @return string Return the text of this revision
         */
        public function getContent() {
-        * @return Content
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = $this->getContentObject();
 +              return ContentHandler::getContentText( $content );
 +      }
 +
 +      /**
 +       * Returns a Content object representing the pages effective display content,
 +     * not necessarily the revision's content!
 +     *
 +     * Note that getContent/loadContent do not follow redirects anymore.
 +       * If you need to fetch redirectable content easily, try
 +       * the shortcut in WikiPage::getRedirectTarget()
 +       *
 +       * This function has side effects! Do not use this function if you
 +       * only want the real revision text if any.
 +       *
++       * @return Content Return the content of this revision
 +       *
 +       * @since 1.WD
 +       */
 +   protected function getContentObject() {
 +              global $wgUser;
                wfProfileIn( __METHOD__ );
  
                if ( $this->mPage->getID() === 0 ) {
                                if ( $text === false ) {
                                        $text = '';
                                }
 +
 +                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
 -                              $text = wfMsgExt( $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
 +                              $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * @return int The old id for the request
         */
        public function getOldIDFromRequest() {
-               global $wgRequest;
                $this->mRedirectUrl = false;
  
-               $oldid = $wgRequest->getIntOrNull( 'oldid' );
+               $request = $this->getContext()->getRequest();
+               $oldid = $request->getIntOrNull( 'oldid' );
  
                if ( $oldid === null ) {
                        return 0;
                        }
                }
  
-               if ( $wgRequest->getVal( 'direction' ) == 'next' ) {
+               if ( $request->getVal( 'direction' ) == 'next' ) {
                        $nextid = $this->getTitle()->getNextRevisionID( $oldid );
                        if ( $nextid ) {
                                $oldid = $nextid;
                        } else {
                                $this->mRedirectUrl = $this->getTitle()->getFullURL( 'redirect=no' );
                        }
-               } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) {
+               } elseif ( $request->getVal( 'direction' ) == 'prev' ) {
                        $previd = $this->getTitle()->getPreviousRevisionID( $oldid );
                        if ( $previd ) {
                                $oldid = $previd;
         * Does *NOT* follow redirects.
         *
         * @return mixed string containing article contents, or false if null
 +       * @deprecated in 1.WD, use getContentObject() instead
         */
 -      function fetchContent() {
 -              if ( $this->mContentLoaded ) {
 +      protected function fetchContent() { #BC cruft!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
  
                wfProfileIn( __METHOD__ );
  
 +              $content = $this->fetchContentObject();
 +
 +              $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
 +              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft! #XXX: can we deprecate that hook?
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.WD
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
 -              $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
 +              $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; // @todo: this isn't page content but a UI message. horrible.
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
 +              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
 -              return $this->mContent;
 +              return $this->mContentObject;
        }
  
        /**
         * @return Revision|null
         */
        public function getRevisionFetched() {
 -              $this->fetchContent();
 +              $this->fetchContentObject();
  
                return $this->mRevision;
        }
         * page of the given title.
         */
        public function view() {
-               global $wgUser, $wgOut, $wgRequest, $wgParser;
-               global $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
+               global $wgParser, $wgUseFileCache, $wgUseETag, $wgDebugToolbar;
  
                wfProfileIn( __METHOD__ );
  
                # the first call of this method even if $oldid is used way below.
                $oldid = $this->getOldID();
  
+               $user = $this->getContext()->getUser();
                # Another whitelist check in case getOldID() is altering the title
-               $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $wgUser );
+               $permErrors = $this->getTitle()->getUserPermissionsErrors( 'read', $user );
                if ( count( $permErrors ) ) {
                        wfDebug( __METHOD__ . ": denied on secondary read check\n" );
                        wfProfileOut( __METHOD__ );
                        throw new PermissionsError( 'read', $permErrors );
                }
  
+               $outputPage = $this->getContext()->getOutput();
                # getOldID() may as well want us to redirect somewhere else
                if ( $this->mRedirectUrl ) {
-                       $wgOut->redirect( $this->mRedirectUrl );
+                       $outputPage->redirect( $this->mRedirectUrl );
                        wfDebug( __METHOD__ . ": redirecting due to oldid\n" );
                        wfProfileOut( __METHOD__ );
  
                }
  
                # If we got diff in the query, we want to see a diff page instead of the article.
-               if ( $wgRequest->getCheck( 'diff' ) ) {
+               if ( $this->getContext()->getRequest()->getCheck( 'diff' ) ) {
                        wfDebug( __METHOD__ . ": showing diff page\n" );
                        $this->showDiffPage();
                        wfProfileOut( __METHOD__ );
                }
  
                # Set page title (may be overridden by DISPLAYTITLE)
-               $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
+               $outputPage->setPageTitle( $this->getTitle()->getPrefixedText() );
  
-               $wgOut->setArticleFlag( true );
+               $outputPage->setArticleFlag( true );
                # Allow frames by default
-               $wgOut->allowClickjacking();
+               $outputPage->allowClickjacking();
  
                $parserCache = ParserCache::singleton();
  
                $parserOptions = $this->getParserOptions();
                # Render printable version, use printable version cache
-               if ( $wgOut->isPrintable() ) {
+               if ( $outputPage->isPrintable() ) {
                        $parserOptions->setIsPrintable( true );
                        $parserOptions->setEditSection( false );
                } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit' ) ) {
                # Try client and file cache
                if ( !$wgDebugToolbar && $oldid === 0 && $this->mPage->checkTouched() ) {
                        if ( $wgUseETag ) {
-                               $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) );
+                               $outputPage->setETag( $parserCache->getETag( $this, $parserOptions ) );
                        }
  
                        # Is it client cached?
-                       if ( $wgOut->checkLastModified( $this->mPage->getTouched() ) ) {
+                       if ( $outputPage->checkLastModified( $this->mPage->getTouched() ) ) {
                                wfDebug( __METHOD__ . ": done 304\n" );
                                wfProfileOut( __METHOD__ );
  
                        } elseif ( $wgUseFileCache && $this->tryFileCache() ) {
                                wfDebug( __METHOD__ . ": done file cache\n" );
                                # tell wgOut that output is taken care of
-                               $wgOut->disable();
-                               $this->mPage->doViewUpdates( $wgUser );
+                               $outputPage->disable();
+                               $this->mPage->doViewUpdates( $user );
                                wfProfileOut( __METHOD__ );
  
                                return;
                # Should the parser cache be used?
                $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid );
                wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
-               if ( $wgUser->getStubThreshold() ) {
+               if ( $user->getStubThreshold() ) {
                        wfIncrStats( 'pcache_miss_stub' );
                }
  
                                                        } else {
                                                                wfDebug( __METHOD__ . ": showing parser cache contents\n" );
                                                        }
-                                                       $wgOut->addParserOutput( $this->mParserOutput );
+                                                       $outputPage->addParserOutput( $this->mParserOutput );
                                                        # Ensure that UI elements requiring revision ID have
                                                        # the correct version information.
-                                                       $wgOut->setRevisionId( $this->mPage->getLatest() );
+                                                       $outputPage->setRevisionId( $this->mPage->getLatest() );
                                                        # Preload timestamp to avoid a DB hit
                                                        $cachedTimestamp = $this->mParserOutput->getTimestamp();
                                                        if ( $cachedTimestamp !== null ) {
-                                                               $wgOut->setRevisionTimestamp( $cachedTimestamp );
+                                                               $outputPage->setRevisionTimestamp( $cachedTimestamp );
                                                                $this->mPage->setTimestamp( $cachedTimestamp );
                                                        }
                                                        $outputDone = true;
                                        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 ) {
  
                                        # Ensure that UI elements requiring revision ID have
                                        # the correct version information.
-                                       $wgOut->setRevisionId( $this->getRevIdFetched() );
+                                       $outputPage->setRevisionId( $this->getRevIdFetched() );
                                        # Preload timestamp to avoid a DB hit
-                                       $wgOut->setRevisionTimestamp( $this->getTimestamp() );
+                                       $outputPage->setRevisionTimestamp( $this->getTimestamp() );
  
                                        # Pages containing custom CSS or JavaScript get special treatment
                                        if ( $this->getTitle()->isCssOrJsPage() || $this->getTitle()->isCssJsSubpage() ) {
                                                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(), $wgOut ) ) ) {
 +                                              # Allow extensions do their own custom view for certain pages
 +                                              $outputDone = true;
 +                                      } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
-                                                       $wgOut->addHTML( $this->viewRedirect( $rt ) );
+                                                       $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 -                                                      $outputPage->addParserOutputNoText( $this->mParserOutput );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getContext(), $oldid, $parserOptions, false );
 +                                                      $wgOut->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();
                                                if ( $error ) {
-                                                       $wgOut->clearHTML(); // for release() errors
-                                                       $wgOut->enableClientCache( false );
-                                                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
+                                                       $outputPage->clearHTML(); // for release() errors
+                                                       $outputPage->enableClientCache( false );
+                                                       $outputPage->setRobotPolicy( 'noindex,nofollow' );
  
                                                        $errortext = $error->getWikiText( false, 'view-pool-error' );
-                                                       $wgOut->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
+                                                       $outputPage->addWikiText( '<div class="errorbox">' . $errortext . '</div>' );
                                                }
                                                # Connection or timeout error
                                                wfProfileOut( __METHOD__ );
                                        }
  
                                        $this->mParserOutput = $poolArticleView->getParserOutput();
-                                       $wgOut->addParserOutput( $this->mParserOutput );
+                                       $outputPage->addParserOutput( $this->mParserOutput );
  
                                        # Don't cache a dirty ParserOutput object
                                        if ( $poolArticleView->getIsDirty() ) {
-                                               $wgOut->setSquidMaxage( 0 );
-                                               $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
+                                               $outputPage->setSquidMaxage( 0 );
+                                               $outputPage->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
                                        }
  
                                        $outputDone = true;
                if ( $this->getTitle()->isMainPage() ) {
                        $msg = wfMessage( 'pagetitle-view-mainpage' )->inContentLanguage();
                        if ( !$msg->isDisabled() ) {
-                               $wgOut->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
+                               $outputPage->setHTMLTitle( $msg->title( $this->getTitle() )->text() );
                        }
                }
  
                # Check for any __NOINDEX__ tags on the page using $pOutput
                $policy = $this->getRobotPolicy( 'view', $pOutput );
-               $wgOut->setIndexPolicy( $policy['index'] );
-               $wgOut->setFollowPolicy( $policy['follow'] );
+               $outputPage->setIndexPolicy( $policy['index'] );
+               $outputPage->setFollowPolicy( $policy['follow'] );
  
                $this->showViewFooter();
-               $this->mPage->doViewUpdates( $wgUser );
+               $this->mPage->doViewUpdates( $user );
  
                wfProfileOut( __METHOD__ );
        }
         * @param $pOutput ParserOutput
         */
        public function adjustDisplayTitle( ParserOutput $pOutput ) {
-               global $wgOut;
                # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
                $titleText = $pOutput->getTitleText();
                if ( strval( $titleText ) !== '' ) {
-                       $wgOut->setPageTitle( $titleText );
+                       $this->getContext()->getOutput()->setPageTitle( $titleText );
                }
        }
  
         * Article::view() only, other callers should use the DifferenceEngine class.
         */
        public function showDiffPage() {
-               global $wgRequest, $wgUser;
-               $diff = $wgRequest->getVal( 'diff' );
-               $rcid = $wgRequest->getVal( 'rcid' );
-               $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
-               $purge = $wgRequest->getVal( 'action' ) == 'purge';
-               $unhide = $wgRequest->getInt( 'unhide' ) == 1;
+               $request = $this->getContext()->getRequest();
+               $user = $this->getContext()->getUser();
+               $diff = $request->getVal( 'diff' );
+               $rcid = $request->getVal( 'rcid' );
+               $diffOnly = $request->getBool( 'diffonly', $user->getOption( 'diffonly' ) );
+               $purge = $request->getVal( 'action' ) == 'purge';
+               $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
  
                if ( $diff == 0 || $diff == $this->mPage->getLatest() ) {
                        # Run view updates for current revision only
-                       $this->mPage->doViewUpdates( $wgUser );
+                       $this->mPage->doViewUpdates( $user );
                }
        }
  
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
 +                      $po = $this->mContentObject->getParserOutput( $this->getContext() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
         * TODO: actions other than 'view'
         */
        public function getRobotPolicy( $action, $pOutput ) {
-               global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies;
-               global $wgDefaultRobotPolicy, $wgRequest;
+               global $wgArticleRobotPolicies, $wgNamespaceRobotPolicies, $wgDefaultRobotPolicy;
  
                $ns = $this->getTitle()->getNamespace();
  
                                'index'  => 'noindex',
                                'follow' => 'nofollow'
                        );
-               } elseif ( $wgOut->isPrintable() ) {
+               } elseif ( $this->getContext()->getOutput()->isPrintable() ) {
                        # Discourage indexing of printable versions, but encourage following
                        return array(
                                'index'  => 'noindex',
                                'follow' => 'follow'
                        );
-               } elseif ( $wgRequest->getInt( 'curid' ) ) {
+               } elseif ( $this->getContext()->getRequest()->getInt( 'curid' ) ) {
                        # For ?curid=x urls, disallow indexing
                        return array(
                                'index'  => 'noindex',
  
        /**
         * If this request is a redirect view, send "redirected from" subtitle to
-        * $wgOut. Returns true if the header was needed, false if this is not a
-        * redirect view. Handles both local and remote redirects.
+        * the output. Returns true if the header was needed, false if this is not
+        * redirect view. Handles both local and remote redirects.
         *
         * @return boolean
         */
        public function showRedirectedFromHeader() {
-               global $wgOut, $wgRequest, $wgRedirectSources;
+               global $wgRedirectSources;
+               $outputPage = $this->getContext()->getOutput();
  
-               $rdfrom = $wgRequest->getVal( 'rdfrom' );
+               $rdfrom = $this->getContext()->getRequest()->getVal( 'rdfrom' );
  
                if ( isset( $this->mRedirectedFrom ) ) {
                        // This is an internally redirected page view.
                                        array( 'redirect' => 'no' )
                                );
  
-                               $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
+                               $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
  
                                // Set the fragment if one was specified in the redirect
                                if ( strval( $this->getTitle()->getFragment() ) != '' ) {
                                        $fragment = Xml::escapeJsString( $this->getTitle()->getFragmentForURL() );
-                                       $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
+                                       $outputPage->addInlineScript( "redirectToFragment(\"$fragment\");" );
                                }
  
                                // Add a <link rel="canonical"> tag
-                               $wgOut->addLink( array( 'rel' => 'canonical',
+                               $outputPage->addLink( array( 'rel' => 'canonical',
                                        'href' => $this->getTitle()->getLocalURL() )
                                );
  
-                               // Tell $wgOut the user arrived at this article through a redirect
-                               $wgOut->setRedirectedFrom( $this->mRedirectedFrom );
+                               // Tell the output object that the user arrived at this article through a redirect
+                               $outputPage->setRedirectedFrom( $this->mRedirectedFrom );
  
                                return true;
                        }
                        // If it was reported from a trusted site, supply a backlink.
                        if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
                                $redir = Linker::makeExternalLink( $rdfrom, $rdfrom );
-                               $wgOut->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
+                               $outputPage->addSubtitle( wfMessage( 'redirectedfrom' )->rawParams( $redir ) );
  
                                return true;
                        }
         * [[MediaWiki:Talkpagetext]]. For Article::view().
         */
        public function showNamespaceHeader() {
-               global $wgOut;
                if ( $this->getTitle()->isTalkPage() ) {
                        if ( !wfMessage( 'talkpageheader' )->isDisabled() ) {
-                               $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) );
+                               $this->getContext()->getOutput()->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1\n</div>", array( 'talkpageheader' ) );
                        }
                }
        }
         * Show the footer section of an ordinary page view
         */
        public function showViewFooter() {
-               global $wgOut;
                # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
                if ( $this->getTitle()->getNamespace() == NS_USER_TALK && IP::isValid( $this->getTitle()->getText() ) ) {
-                       $wgOut->addWikiMsg( 'anontalkpagetext' );
+                       $this->getContext()->getOutput()->addWikiMsg( 'anontalkpagetext' );
                }
  
                # If we have been passed an &rcid= parameter, we want to give the user a
         * desired, does nothing.
         */
        public function showPatrolFooter() {
-               global $wgOut, $wgRequest, $wgUser;
-               $rcid = $wgRequest->getVal( 'rcid' );
+               $request = $this->getContext()->getRequest();
+               $outputPage = $this->getContext()->getOutput();
+               $user = $this->getContext()->getUser();
+               $rcid = $request->getVal( 'rcid' );
  
                if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) {
                        return;
                }
  
-               $token = $wgUser->getEditToken( $rcid );
-               $wgOut->preventClickjacking();
+               $token = $user->getEditToken( $rcid );
+               $outputPage->preventClickjacking();
  
-               $wgOut->addHTML(
+               $outputPage->addHTML(
                        "<div class='patrollink'>" .
                                wfMsgHtml(
                                        'markaspatrolledlink',
         * namespace, show the default message text. To be called from Article::view().
         */
        public function showMissingArticle() {
-               global $wgOut, $wgRequest, $wgUser, $wgSend404Code;
+               global $wgSend404Code;
+               $outputPage = $this->getContext()->getOutput();
  
                # Show info in user (talk) namespace. Does the user exist? Is he blocked?
                if ( $this->getTitle()->getNamespace() == NS_USER || $this->getTitle()->getNamespace() == NS_USER_TALK ) {
                        $ip = User::isIP( $rootPart );
  
                        if ( !($user && $user->isLoggedIn()) && !$ip ) { # User does not exist
-                               $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
+                               $outputPage->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1\n</div>",
                                        array( 'userpage-userdoesnotexist-view', wfEscapeWikiText( $rootPart ) ) );
                        } elseif ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
                                LogEventsList::showLogExtract(
-                                       $wgOut,
+                                       $outputPage,
                                        'block',
                                        $user->getUserPage()->getPrefixedText(),
                                        '',
                wfRunHooks( 'ShowMissingArticle', array( $this ) );
  
                # Show delete and move logs
-               LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->getTitle()->getPrefixedText(), '',
+               LogEventsList::showLogExtract( $outputPage, array( 'delete', 'move' ), $this->getTitle()->getPrefixedText(), '',
                        array(  'lim' => 10,
                                'conds' => array( "log_action != 'revision'" ),
                                'showIfEmpty' => false,
                if ( !$this->mPage->hasViewableContent() && $wgSend404Code ) {
                        // If there's no backing content, send a 404 Not Found
                        // for better machine handling of broken links.
-                       $wgRequest->response()->header( "HTTP/1.1 404 Not Found" );
+                       $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" );
                }
  
                $hookResult = wfRunHooks( 'BeforeDisplayNoArticleText', array( $this ) );
                        // Use the default message text
                        $text = $this->getTitle()->getDefaultMessageText();
                } else {
-                       $createErrors = $this->getTitle()->getUserPermissionsErrors( 'create', $wgUser );
-                       $editErrors = $this->getTitle()->getUserPermissionsErrors( 'edit', $wgUser );
+                       $createErrors = $this->getTitle()->getUserPermissionsErrors( 'create', $this->getContext()->getUser() );
+                       $editErrors = $this->getTitle()->getUserPermissionsErrors( 'edit', $this->getContext()->getUser() );
                        $errors = array_merge( $createErrors, $editErrors );
  
                        if ( !count( $errors ) ) {
                }
                $text = "<div class='noarticletext'>\n$text\n</div>";
  
-               $wgOut->addWikiText( $text );
+               $outputPage->addWikiText( $text );
        }
  
        /**
         * If the revision requested for view is deleted, check permissions.
-        * Send either an error message or a warning header to $wgOut.
+        * Send either an error message or a warning header to the output.
         *
         * @return boolean true if the view is allowed, false if not.
         */
        public function showDeletedRevisionHeader() {
-               global $wgOut, $wgRequest;
                if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
                        // Not deleted
                        return true;
                }
  
+               $outputPage = $this->getContext()->getOutput();
                // If the user is not allowed to see it...
                if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
-                       $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+                       $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                'rev-deleted-text-permission' );
  
                        return false;
                // If the user needs to confirm that they want to see it...
-               } elseif ( $wgRequest->getInt( 'unhide' ) != 1 ) {
+               } elseif ( $this->getContext()->getRequest()->getInt( 'unhide' ) != 1 ) {
                        # Give explanation and add a link to view the revision...
                        $oldid = intval( $this->getOldID() );
                        $link = $this->getTitle()->getFullUrl( "oldid={$oldid}&unhide=1" );
                        $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
                                'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
-                       $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
+                       $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                array( $msg, $link ) );
  
                        return false;
                } else {
                        $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
                                'rev-suppressed-text-view' : 'rev-deleted-text-view';
-                       $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg );
+                       $outputPage->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1\n</div>\n", $msg );
  
                        return true;
                }
         *   Revision as of \<date\>; view current revision
         *   \<- Previous version | Next Version -\>
         *
-        * @param $oldid String: revision ID of this article revision
+        * @param $oldid int: revision ID of this article revision
         */
        public function setOldSubtitle( $oldid = 0 ) {
-               global $wgLang, $wgOut, $wgUser, $wgRequest;
                if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
                        return;
                }
  
-               $unhide = $wgRequest->getInt( 'unhide' ) == 1;
+               $unhide = $this->getContext()->getRequest()->getInt( 'unhide' ) == 1;
  
                # Cascade unhide param in links for easy deletion browsing
                $extraParams = array();
                $timestamp = $revision->getTimestamp();
  
                $current = ( $oldid == $this->mPage->getLatest() );
-               $td = $wgLang->timeanddate( $timestamp, true );
-               $tddate = $wgLang->date( $timestamp, true );
-               $tdtime = $wgLang->time( $timestamp, true );
+               $language = $this->getContext()->getLanguage();
+               $td = $language->timeanddate( $timestamp, true );
+               $tddate = $language->date( $timestamp, true );
+               $tdtime = $language->time( $timestamp, true );
  
                # Show user links if allowed to see them. If hidden, then show them only if requested...
                $userlinks = Linker::revUserTools( $revision, !$unhide );
                        ? 'revision-info-current'
                        : 'revision-info';
  
-               $wgOut->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg,
+               $outputPage = $this->getContext()->getOutput();
+               $outputPage->addSubtitle( "<div id=\"mw-{$infomsg}\">" . wfMessage( $infomsg,
                        $td )->rawParams( $userlinks )->params( $revision->getID(), $tddate,
                        $tdtime, $revision->getUser() )->parse() . "</div>" );
  
                                array( 'known', 'noclasses' )
                        );
  
-               $cdel = Linker::getRevDeleteLink( $wgUser, $revision, $this->getTitle() );
+               $cdel = Linker::getRevDeleteLink( $this->getContext()->getUser(), $revision, $this->getTitle() );
                if ( $cdel !== '' ) {
                        $cdel .= ' ';
                }
  
-               $wgOut->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel .
+               $outputPage->addSubtitle( "<div id=\"mw-revision-nav\">" . $cdel .
                        wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
                        $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>" );
        }
         * @return string containing HMTL with redirect link
         */
        public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
-               global $wgOut, $wgStylePath;
+               global $wgStylePath;
  
                if ( !is_array( $target ) ) {
                        $target = array( $target );
                $imageDir = $lang->getDir();
  
                if ( $appendSubtitle ) {
-                       $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
+                       $this->getContext()->getOutput()->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
                }
  
                // the loop prepends the arrow image before the link, so the first case needs to be outside
         * Handle action=render
         */
        public function render() {
-               global $wgOut;
-               $wgOut->setArticleBodyOnly( true );
+               $this->getContext()->getOutput()->setArticleBodyOnly( true );
                $this->view();
        }
  
         * UI entry point for page deletion
         */
        public function delete() {
-               global $wgOut, $wgRequest, $wgLang;
                # This code desperately needs to be totally rewritten
  
                $title = $this->getTitle();
                $conds = $title->pageCond();
                $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
                if ( $latest === false ) {
-                       $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) );
-                       $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
+                       $outputPage = $this->getContext()->getOutput();
+                       $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $title->getPrefixedText() ) );
+                       $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
                                        array( 'cannotdelete', wfEscapeWikiText( $title->getPrefixedText() ) )
                                );
-                       $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+                       $outputPage->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
                        LogEventsList::showLogExtract(
-                               $wgOut,
+                               $outputPage,
                                'delete',
                                $title->getPrefixedText()
                        );
                        return;
                }
  
-               $deleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
-               $deleteReason = $wgRequest->getText( 'wpReason' );
+               $request = $this->getContext()->getRequest();
+               $deleteReasonList = $request->getText( 'wpDeleteReasonList', 'other' );
+               $deleteReason = $request->getText( 'wpReason' );
  
                if ( $deleteReasonList == 'other' ) {
                        $reason = $deleteReason;
                        $reason = $deleteReasonList;
                }
  
-               if ( $wgRequest->wasPosted() && $user->matchEditToken( $wgRequest->getVal( 'wpEditToken' ),
+               if ( $request->wasPosted() && $user->matchEditToken( $request->getVal( 'wpEditToken' ),
                        array( 'delete', $this->getTitle()->getPrefixedText() ) ) )
                {
                        # Flag to hide all contents of the archived revisions
-                       $suppress = $wgRequest->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' );
+                       $suppress = $request->getVal( 'wpSuppress' ) && $user->isAllowed( 'suppressrevision' );
  
                        $this->doDelete( $reason, $suppress );
  
-                       if ( $wgRequest->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) {
+                       if ( $request->getCheck( 'wpWatch' ) && $user->isLoggedIn() ) {
                                $this->doWatch();
                        } elseif ( $title->userIsWatching() ) {
                                $this->doUnwatch();
                // 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
                if ( $hasHistory ) {
                        $revisions = $this->mTitle->estimateRevisionCount();
                        // @todo FIXME: i18n issue/patchwork message
-                       $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
-                               wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
+                       $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' .
+                               wfMsgExt( 'historywarning', array( 'parseinline' ), $this->getContext()->getLanguage()->formatNum( $revisions ) ) .
                                wfMsgHtml( 'word-separator' ) . Linker::link( $title,
                                        wfMsgHtml( 'history' ),
                                        array( 'rel' => 'archives' ),
  
                        if ( $this->mTitle->isBigDeletion() ) {
                                global $wgDeleteRevisionsLimit;
-                               $wgOut->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
-                                       array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
+                               $this->getContext()->getOutput()->wrapWikiMsg( "<div class='error'>\n$1\n</div>\n",
+                                       array( 'delete-warning-toobig', $this->getContext()->getLanguage()->formatNum( $wgDeleteRevisionsLimit ) ) );
                        }
                }
  
         * @param $reason String: prefilled reason
         */
        public function confirmDelete( $reason ) {
-               global $wgOut;
                wfDebug( "Article::confirmDelete\n" );
  
-               $wgOut->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) );
-               $wgOut->addBacklinkSubtitle( $this->getTitle() );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-               $wgOut->addWikiMsg( 'confirmdeletetext' );
+               $outputPage = $this->getContext()->getOutput();
+               $outputPage->setPageTitle( wfMessage( 'delete-confirm', $this->getTitle()->getPrefixedText() ) );
+               $outputPage->addBacklinkSubtitle( $this->getTitle() );
+               $outputPage->setRobotPolicy( 'noindex,nofollow' );
+               $outputPage->addWikiMsg( 'confirmdeletetext' );
  
-               wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
+               wfRunHooks( 'ArticleConfirmDelete', array( $this, $outputPage, &$reason ) );
  
                $user = $this->getContext()->getUser();
  
                                $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
                        }
  
-               $wgOut->addHTML( $form );
-               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
-               LogEventsList::showLogExtract( $wgOut, 'delete',
+               $outputPage->addHTML( $form );
+               $outputPage->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+               LogEventsList::showLogExtract( $outputPage, 'delete',
                        $this->getTitle()->getPrefixedText()
                );
        }
         * @param $suppress bool
         */
        public function doDelete( $reason, $suppress = false ) {
-               global $wgOut;
                $error = '';
+               $outputPage = $this->getContext()->getOutput();
                if ( $this->mPage->doDeleteArticle( $reason, $suppress, 0, true, $error ) ) {
                        $deleted = $this->getTitle()->getPrefixedText();
  
-                       $wgOut->setPageTitle( wfMessage( 'actioncomplete' ) );
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
+                       $outputPage->setPageTitle( wfMessage( 'actioncomplete' ) );
+                       $outputPage->setRobotPolicy( 'noindex,nofollow' );
  
                        $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
  
-                       $wgOut->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
-                       $wgOut->returnToMain( false );
+                       $outputPage->addWikiMsg( 'deletedtext', wfEscapeWikiText( $deleted ), $loglink );
+                       $outputPage->returnToMain( false );
                } else {
-                       $wgOut->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) );
+                       $outputPage->setPageTitle( wfMessage( 'cannotdelete-title', $this->getTitle()->getPrefixedText() ) );
                        if ( $error == '' ) {
-                               $wgOut->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
+                               $outputPage->wrapWikiMsg( "<div class=\"error mw-error-cannotdelete\">\n$1\n</div>",
                                        array( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) )
                                );
-                               $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
+                               $outputPage->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
  
                                LogEventsList::showLogExtract(
-                                       $wgOut,
+                                       $outputPage,
                                        'delete',
                                        $this->getTitle()->getPrefixedText()
                                );
                        } else {
-                               $wgOut->addHTML( $error );
+                               $outputPage->addHTML( $error );
                        }
                }
        }
         * @return ParserOutput or false if the given revsion ID is not found
         */
        public function getParserOutput( $oldid = null, User $user = null ) {
-               global $wgUser;
-               $user = is_null( $user ) ? $wgUser : $user;
+               $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
                $parserOptions = $this->mPage->makeParserOptions( $user );
  
                return $this->mPage->getParserOutput( $parserOptions, $oldid );
         * @return ParserOptions
         */
        public function getParserOptions() {
-               global $wgUser;
                if ( !$this->mParserOptions ) {
-                       $this->mParserOptions = $this->mPage->makeParserOptions( $wgUser );
+                       $this->mParserOptions = $this->mPage->makeParserOptions( $this->getContext()->getUser() );
                }
                // Clone to allow modifications of the return value without affecting cache
                return clone $this->mParserOptions;
        }
  
        /**
-        * Add this page to $wgUser's watchlist
+        * Add this page to the current user's watchlist
         *
         * This is safe to be called multiple times
         *
         * @deprecated since 1.18
         */
        public function doWatch() {
-               global $wgUser;
                wfDeprecated( __METHOD__, '1.18' );
-               return WatchAction::doWatch( $this->getTitle(), $wgUser );
+               return WatchAction::doWatch( $this->getTitle(), $this->getContext()->getUser() );
        }
  
        /**
         * @deprecated since 1.18
         */
        public function doUnwatch() {
-               global $wgUser;
                wfDeprecated( __METHOD__, '1.18' );
-               return WatchAction::doUnwatch( $this->getTitle(), $wgUser );
+               return WatchAction::doUnwatch( $this->getTitle(), $this->getContext()->getUser() );
        }
  
        /**
         * Output a redirect back to the article.
         * This is typically used after an edit.
         *
-        * @deprecated in 1.18; call $wgOut->redirect() directly
+        * @deprecated in 1.18; call OutputPage::redirect() directly
         * @param $noRedir Boolean: add redirect=no
         * @param $sectionAnchor String: section to redirect to, including "#"
         * @param $extraQuery String: extra query params
         */
        public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
                wfDeprecated( __METHOD__, '1.18' );
-               global $wgOut;
                if ( $noRedir ) {
                        $query = 'redirect=no';
                        if ( $extraQuery )
                        $query = $extraQuery;
                }
  
-               $wgOut->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor );
+               $this->getContext()->getOutput()->redirect( $this->getTitle()->getFullURL( $query ) . $sectionAnchor );
        }
  
        /**
         * @return array
         */
        public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails, User $user = null ) {
-               global $wgUser;
-               $user = is_null( $user ) ? $wgUser : $user;
+               $user = is_null( $user ) ? $this->getContext()->getUser() : $user;
                return $this->mPage->doRollback( $fromP, $summary, $token, $bot, $resultDetails, $user );
        }
  
         * @return array
         */
        public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser = null ) {
-               global $wgUser;
-               $guser = is_null( $guser ) ? $wgUser : $guser;
+               $guser = is_null( $guser ) ? $this->getContext()->getUser() : $guser;
                return $this->mPage->commitRollback( $fromP, $summary, $bot, $resultDetails, $guser );
        }
  
         * @return mixed
         */
        public function generateReason( &$hasHistory ) {
 -              return $this->mPage->getAutoDeleteReason( $hasHistory );
 +              $title = $this->mPage->getTitle();
 +              $handler = ContentHandler::getForTitle( $title );
 +              return $handler->getAutoDeleteReason( $title, $hasHistory );
        }
  
        // ****** B/C functions for static methods ( __callStatic is PHP>=5.3 ) ****** //
         * @param $newtext
         * @param $flags
         * @return string
 +       * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
         */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
                return WikiPage::getAutosummary( $oldtext, $newtext, $flags );
diff --combined includes/AutoLoader.php
@@@ -56,6 -56,7 +56,7 @@@ $wgAutoloadLocalClasses = array
        'DeferredUpdates' => 'includes/DeferredUpdates.php',
        'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
        'DerivativeRequest' => 'includes/WebRequest.php',
+       'DeviceDetection' => 'includes/DeviceDetection.php',
        'DiffHistoryBlob' => 'includes/HistoryBlob.php',
  
        'DoubleReplacer' => 'includes/StringUtils.php',
        'Linker' => 'includes/Linker.php',
        'LinkFilter' => 'includes/LinkFilter.php',
        'LinksUpdate' => 'includes/LinksUpdate.php',
 +      'LinksDeletionUpdate' => 'includes/LinksUpdate.php',
        'LocalisationCache' => 'includes/LocalisationCache.php',
        'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php',
        'MagicWord' => 'includes/MagicWord.php',
        'RevisionList' => 'includes/RevisionList.php',
        'RSSFeed' => 'includes/Feed.php',
        'Sanitizer' => 'includes/Sanitizer.php',
 +    'SecondaryDataUpdate' => 'includes/SecondaryDataUpdate.php',
 +    'SecondaryDBDataUpdate' => 'includes/SecondaryDBDataUpdate.php',
        'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
        'SiteConfiguration' => 'includes/SiteConfiguration.php',
        'SiteStats' => 'includes/SiteStats.php',
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
  
 +    # content handler
 +    'Content' => 'includes/Content.php',
 +    'ContentHandler' => 'includes/ContentHandler.php',
 +    'CssContent' => 'includes/Content.php',
 +    'CssContentHandler' => 'includes/ContentHandler.php',
 +    'JavaScriptContent' => 'includes/Content.php',
 +    'JavaScriptContentHandler' => 'includes/ContentHandler.php',
 +    'MessageContent' => 'includes/Content.php',
 +    'TextContent' => 'includes/Content.php',
 +    'WikitextContent' => 'includes/Content.php',
 +    'WikitextContentHandler' => 'includes/ContentHandler.php',
 +
        # includes/actions
        'CachedAction' => 'includes/actions/CachedAction.php',
        'CreditsAction' => 'includes/actions/CreditsAction.php',
        'ApiFormatDump' => 'includes/api/ApiFormatDump.php',
        'ApiFormatFeedWrapper' => 'includes/api/ApiFormatBase.php',
        'ApiFormatJson' => 'includes/api/ApiFormatJson.php',
 +      'ApiFormatNone' => 'includes/api/ApiFormatNone.php',
        'ApiFormatPhp' => 'includes/api/ApiFormatPhp.php',
        'ApiFormatRaw' => 'includes/api/ApiFormatRaw.php',
        'ApiFormatTxt' => 'includes/api/ApiFormatTxt.php',
        'ApiMain' => 'includes/api/ApiMain.php',
        'ApiMove' => 'includes/api/ApiMove.php',
        'ApiOpenSearch' => 'includes/api/ApiOpenSearch.php',
+       'ApiOptions' => 'includes/api/ApiOptions.php',
        'ApiPageSet' => 'includes/api/ApiPageSet.php',
        'ApiParamInfo' => 'includes/api/ApiParamInfo.php',
        'ApiParse' => 'includes/api/ApiParse.php',
        'FileBackendStoreShardDirIterator' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendStoreShardFileIterator' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
+       'FileBackendStoreOpHandle' => 'includes/filerepo/backend/FileBackendStore.php',
        'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendList' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendDirList' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
+       'FSFileOpHandle' => 'includes/filerepo/backend/FSFileBackend.php',
        'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendDirList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
+       'SwiftFileOpHandle' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
        'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
        'NullFileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
        'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php',
        'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
        'FileOp' => 'includes/filerepo/backend/FileOp.php',
+       'FileOpBatch' => 'includes/filerepo/backend/FileOpBatch.php',
        'StoreFileOp' => 'includes/filerepo/backend/FileOp.php',
        'CopyFileOp' => 'includes/filerepo/backend/FileOp.php',
        'MoveFileOp' => 'includes/filerepo/backend/FileOp.php',
        'TestFileIterator' => 'tests/testHelpers.inc',
        'TestRecorder' => 'tests/testHelpers.inc',
  
 +      # tests/phpunit
 +      'RevisionStorageTest' => 'tests/phpunit/includes/RevisionStorageTest.php',
 +      'WikiPageTest' => 'tests/phpunit/includes/WikiPageTest.php',
 +      'WikitextContentTest' => 'tests/phpunit/includes/WikitextContentTest.php',
 +      'JavascriptContentTest' => 'tests/phpunit/includes/JavascriptContentTest.php',
 +      'DummyContentHandlerForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +      'DummyContentForTesting' => 'tests/phpunit/includes/ContentHandlerTest.php',
 +
        # tests/parser
        'ParserTest' => 'tests/parser/parserTest.inc',
        'ParserTestParserHook' => 'tests/parser/parserTestsParserHook.php',
@@@ -640,40 -640,6 +640,40 @@@ $wgMediaHandlers = array
        'image/x-djvu' => 'DjVuHandler', // compat
  );
  
 +/**
 + * Plugins for page content model handling.
 + * Each entry in the array maps a model id to a class name
 + */
 +$wgContentHandlers = array(
 +    CONTENT_MODEL_WIKITEXT => 'WikitextContentHandler', // the usual case
 +    CONTENT_MODEL_JAVASCRIPT => 'JavaScriptContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_CSS => 'CssContentHandler', // dumb version, no syntax highlighting
 +    CONTENT_MODEL_TEXT => 'TextContentHandler', // dumb plain text in <pre>
 +);
 +
 +/**
 + * Mime types for content formats.
 + * Each entry in the array maps a content format to a mime type.
 + *
 + * Extensions that define their own content formats can register
 + * the appropriate mime types in this array.
 + *
 + * Such extensions shall use content format IDs
 + * larger than 100 and register the ids they use at
 + * <http://mediawiki.org/ContentHandler/registry>
 + * to avoid conflicts with other extensions.
 + */
 +$wgContentFormatMimeTypes = array(
 +      CONTENT_FORMAT_WIKITEXT => 'text/x-wiki',
 +      CONTENT_FORMAT_JAVASCRIPT => 'text/javascript',
 +      CONTENT_FORMAT_CSS => 'text/css',
 +      CONTENT_FORMAT_TEXT => 'text/plain',
 +      CONTENT_FORMAT_HTML => 'text/html',
 +      CONTENT_FORMAT_XML => 'application/xml',
 +      CONTENT_FORMAT_JSON => 'application/json',
 +      CONTENT_FORMAT_SERIALIZED => 'application/vnd.php.serialized',
 +);
 +
  /**
   * Resizing can be done using PHP's internal image libraries or using
   * ImageMagick or another third-party converter, e.g. GraphicMagick.
@@@ -1907,12 -1873,12 +1907,12 @@@ $wgMaxSquidPurgeTitles = 400
   * Routing configuration for HTCP multicast purging. Add elements here to
   * enable HTCP and determine which purges are sent where. If set to an empty
   * array, HTCP is disabled.
-  * 
+  *
   * Each key in this array is a regular expression to match against the purged
   * URL, or an empty string to match all URLs. The purged URL is matched against
   * the regexes in the order specified, and the first rule whose regex matches
   * is used.
-  * 
+  *
   * Example configuration to send purges for upload.wikimedia.org to one
   * multicast group and all other purges to another:
   * $wgHTCPMulticastRouting = array(
   *                 'port' => 4827,
   *         ),
   * );
-  * 
+  *
   * @see $wgHTCPMulticastTTL
   */
  $wgHTCPMulticastRouting = array();
   *
   * Note that MediaWiki uses the old non-RFC compliant HTCP format, which was
   * present in the earliest Squid implementations of the protocol.
-  * 
+  *
   * This setting is DEPRECATED in favor of $wgHTCPMulticastRouting , and kept
   * for backwards compatibility only. If $wgHTCPMulticastRouting is set, this
   * setting is ignored. If $wgHTCPMulticastRouting is not set and this setting
   * is, it is used to populate $wgHTCPMulticastRouting.
-  * 
+  *
   * @deprecated in favor of $wgHTCPMulticastRouting
   */
  $wgHTCPMulticastAddress = false;
@@@ -4175,6 -4141,11 +4175,11 @@@ $wgShowExceptionDetails = false
   */
  $wgShowDBErrorBacktrace = false;
  
+ /**
+  * If true, send the exception backtrace to the error log
+  */
+ $wgLogExceptionBacktrace = true;
  /**
   * Expose backend server host names through the API and various HTML comments
   */
@@@ -4256,6 -4227,14 +4261,14 @@@ $wgAggregateStatsID = false
   */
  $wgDisableCounters = false;
  
+ /**
+  * Set this to an integer to only do synchronous site_stats updates
+  * one every *this many* updates. The other requests go into pending
+  * delta values in $wgMemc. Make sure that $wgMemc is a global cache.
+  * If set to -1, updates *only* go to $wgMemc (useful for daemons).
+  */
+ $wgSiteStatsAsyncFactor = false;
  /**
   * Parser test suite files to be run by parserTests.php when no specific
   * filename is passed to it.
@@@ -4596,6 -4575,20 +4609,20 @@@ $wgReadOnlyFile = false
   */
  $wgUpgradeKey = false;
  
+ /**
+  * Map GIT repository URLs to viewer URLs to provide links in Special:Version
+  *
+  * Key is a pattern passed to preg_match() and preg_replace(),
+  * without the delimiters (which are #) and must match the whole URL.
+  * The value is the replacement for the key (it can contain $1, etc.)
+  * %h will be replaced by the short SHA-1 (7 first chars) and %H by the
+  * full SHA-1 of the HEAD revision.
+  */
+ $wgGitRepositoryViewers = array(
+     'https://gerrit.wikimedia.org/r/p/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H',
+     'ssh://(?:[a-z0-9_]+@)?gerrit.wikimedia.org:29418/(.*)' => 'https://gerrit.wikimedia.org/r/gitweb?p=$1;h=%H',
+ );
  /** @} */ # End of maintenance }
  
  /************************************************************************//**
@@@ -5825,31 -5818,6 +5852,31 @@@ $wgSeleniumConfigFile = null
  $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
  $wgDBtestpassword = '';
  
 +/**
 + * Associative array mapping namespace IDs to the name of the content model pages in that namespace should have by
 + * default (use the CONTENT_MODEL_XXX constants). If no special content type is defined for a given namespace,
 + * pages in that namespace will  use the CONTENT_MODEL_WIKITEXT (except for the special case of JS and CS pages).
 + */
 +$wgNamespaceContentModels = array();
 +
 +/**
 + * How to react if a plain text version of a non-text Content object is requested using ContentHandler::getContentText():
 + *
 + * * 'ignore': return null
 + * * 'fail': throw an MWException
 + * * 'serializeContent': serializeContent to default format
 + */
 +$wgContentHandlerTextFallback = 'ignore';
 +
 +/**
 + * Compatibility switch for running ContentHandler code withoput a schema update.
 + * Set to false to disable use of the database fields introduced by the ContentHandler facility.
 + *
 + * @deprecated this is only here to allow code deployment without a database schema update on large sites.
 + *             get rid of it in the next version.
 + */
 +$wgContentHandlerUseDB = true;
 +
  /**
   * For really cool vim folding this needs to be at the end:
   * vim: foldmarker=@{,@} foldmethod=marker
diff --combined includes/EditPage.php
@@@ -139,11 -139,6 +139,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
        public function __construct( Article $article ) {
                $this->mArticle = $article;
                $this->mTitle = $article->getTitle();
 +
 +              $this->content_model = $this->mTitle->getContentModel();
 +
 +              $handler = ContentHandler::getForModelID( $this->content_model );
 +              $this->content_format = $handler->getDefaultFormat(); #NOTE: should be overridden by format of actual revision
        }
  
        /**
                        return;
                }
  
 -              $content = $this->getContent();
 +              $content = $this->getContentObject();
  
                # Use the normal message if there's nothing to display
 -              if ( $this->firsttime && $content === '' ) {
 +              if ( $this->firsttime && $content->isEmpty() ) {
                        $action = $this->mTitle->exists() ? 'edit' :
                                ( $this->mTitle->isTalkPage() ? 'createtalk' : 'createpage' );
                        throw new PermissionsError( $action, $permErrors );
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
 -                      $content = $this->textbox1;
 +                      $text = $this->textbox1;
                        $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
 +                      $text = $content->serialize( $this->content_format );
                        $wgOut->addWikiMsg( 'viewsourcetext' );
                }
  
 -              $this->showTextbox( $content, 'wpTextbox1', array( 'readonly' ) );
 +              $this->showTextbox( $text, 'wpTextbox1', array( 'readonly' ) );
  
                $wgOut->addHTML( Html::rawElement( 'div', array( 'class' => 'templatesUsed' ),
                        Linker::formatTemplates( $this->getTemplates() ) ) );
                } else {
                        # Not a posted form? Start with nothing.
                        wfDebug( __METHOD__ . ": Not a posted form.\n" );
 -                      $this->textbox1     = '';
 +                      $this->textbox1     = ''; #FIXME: track content object
                        $this->summary      = '';
                        $this->sectiontitle = '';
                        $this->edittime     = '';
                        }
                }
  
 +              $this->oldid = $request->getInt( 'oldid' );
 +
                $this->bot = $request->getBool( 'bot', true );
                $this->nosummary = $request->getBool( 'nosummary' );
  
 -              $this->oldid = $request->getInt( 'oldid' );
 +              $content_handler = ContentHandler::getForTitle( $this->mTitle );
 +              $this->content_model = $request->getText( 'model', $content_handler->getModelID() ); #may be overridden by revision
 +              $this->content_format = $request->getText( 'format', $content_handler->getDefaultFormat() ); #may be overridden by revision
 +
 +              #TODO: check if the desired model is allowed in this namespace, and if a transition from the page's current model to the new model is allowed
 +              #TODO: check if the desired content model supports the given content format!
  
                $this->live = $request->getCheck( 'live' );
                $this->editintro = $request->getText( 'editintro',
        function initialiseForm() {
                global $wgUser;
                $this->edittime = $this->mArticle->getTimestamp();
 -              $this->textbox1 = $this->getContent( false );
 +
 +              $content = $this->getContentObject( false ); #TODO: track content object?!
 +              $this->textbox1 = $content->serialize( $this->content_format );
 +
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
                if ( $wgUser->getOption( 'watchdefault' ) ) {
         * @param $def_text string
         * @return mixed string on success, $def_text for invalid sections
         * @private
 +       * @deprecated since 1.WD
         */
 -      function getContent( $def_text = '' ) {
 -              global $wgOut, $wgRequest, $wgParser;
 +      function getContent( $def_text = false ) { #FIXME: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $def_text !== null && $def_text !== false && $def_text !== '' ) {
 +                      $def_content = ContentHandler::makeContent( $def_text, $this->getTitle() );
 +              } else {
 +                      $def_content = false;
 +              }
 +
 +              $content = $this->getContentObject( $def_content );
 +
 +              return $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?
 +      }
 +
 +      private function getContentObject( $def_content = null ) { #FIXME: use this!
 +              global $wgOut, $wgRequest;
  
                wfProfileIn( __METHOD__ );
  
 -              $text = false;
 +              $content = false;
  
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
                if ( !$this->mTitle->exists() || $this->section == 'new' ) {
                        if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI && $this->section != 'new' ) {
                                # If this is a system message, get the default text.
 -                              $text = $this->mTitle->getDefaultMessageText();
 +                              $msg = $this->mTitle->getDefaultMessageText();
 +
 +                              $content = ContentHandler::makeContent( $msg, $this->mTitle );
                        }
 -                      if ( $text === false ) {
 +                      if ( $content === false ) {
                                # If requested, preload some text.
                                $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
 -                              $text = $this->getPreloadedText( $preload );
 +
 +                              $content = $this->getPreloadedContent( $preload );
                        }
                // For existing pages, get text based on "undo" or section parameters.
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
 -                              $text = $wgParser->getSection( $this->getOriginalContent(), $this->section, $def_text );
 +                              $orig = $this->getOriginalContent();
 +                              $content = $orig ? $orig->getSection( $this->section ) : null;
 +
 +                              if ( !$content ) $content = $def_content;
                        } else {
                                $undoafter = $wgRequest->getInt( 'undoafter' );
                                $undo = $wgRequest->getInt( 'undo' );
  
                                        # Sanity check, make sure it's the right page,
                                        # the revisions exist and they were not deleted.
 -                                      # Otherwise, $text will be left as-is.
 +                                      # Otherwise, $content will be left as-is.
                                        if ( !is_null( $undorev ) && !is_null( $oldrev ) &&
                                                $undorev->getPage() == $oldrev->getPage() &&
                                                $undorev->getPage() == $this->mTitle->getArticleID() &&
                                                !$undorev->isDeleted( Revision::DELETED_TEXT ) &&
                                                !$oldrev->isDeleted( Revision::DELETED_TEXT ) ) {
  
 -                                              $text = $this->mArticle->getUndoText( $undorev, $oldrev );
 -                                              if ( $text === false ) {
 +                                              $content = $this->mArticle->getUndoContent( $undorev, $oldrev );
 +
 +                                              if ( $content === false ) {
                                                        # Warn the user that something went wrong
                                                        $undoMsg = 'failure';
                                                } else {
                                                wfMsgNoTrans( 'undo-' . $undoMsg ) . '</div>', true, /* interface */true );
                                }
  
 -                              if ( $text === false ) {
 -                                      $text = $this->getOriginalContent();
 +                              if ( $content === false ) {
 +                                      $content = $this->getOriginalContent();
                                }
                        }
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $content;
        }
  
        /**
         */
        private function getOriginalContent() {
                if ( $this->section == 'new' ) {
 -                      return $this->getCurrentText();
 +                      return $this->getCurrentContent();
                }
                $revision = $this->mArticle->getRevisionFetched();
                if ( $revision === null ) {
 -                      return '';
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                }
 -              return $this->mArticle->getContent();
 +              $content = $revision->getContent();
 +              return $content;
        }
  
        /**
 -       * Get the actual text of the page. This is basically similar to
 -       * WikiPage::getRawText() except that when the page doesn't exist an empty
 -       * string is returned instead of false.
 +       * Get the current content of the page. This is basically similar to
 +       * WikiPage::getContent( Revision::RAW ) except that when the page doesn't exist an empty
 +       * content object is returned instead of null.
         *
 -       * @since 1.19
 +       * @since 1.WD
         * @return string
         */
 -      private function getCurrentText() {
 -              $text = $this->mArticle->getRawText();
 -              if ( $text === false ) {
 -                      return '';
 +      private function getCurrentContent() {
 +              $rev = $this->mArticle->getRevision();
 +              $content = $rev ? $rev->getContent( Revision::RAW ) : null;
 +
 +              if ( $content  === false || $content === null ) {
 +                      if ( !$this->content_model ) $this->content_model = $this->getTitle()->getContentModel();
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +
 +                      return $handler->makeEmptyContent();
                } else {
 -                      return $text;
 +                      #FIXME: nasty side-effect!
 +                      $this->content_model = $rev->getContentModel();
 +                      $this->content_format = $rev->getContentFormat();
 +
 +                      return $content;
                }
        }
  
 +
        /**
         * Use this method before edit() to preload some text into the edit box
         *
         * @param $text string
 +       * @deprecated since 1.WD
         */
        public function setPreloadedText( $text ) {
 -              $this->mPreloadText = $text;
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              $this->setPreloadedContent( $content );
 +      }
 +
 +      /**
 +       * Use this method before edit() to preload some content into the edit box
 +       *
 +       * @param $content Content
 +       *
 +       * @since 1.WD
 +       */
 +      public function setPreloadedContent( Content $content ) {
 +              $this->mPreloadedContent = $content;
        }
  
        /**
         * an earlier setPreloadText() or by loading the given page.
         *
         * @param $preload String: representing the title to preload from.
 +       *
         * @return String
 +       *
 +       * @deprecated since 1.WD, use getPreloadedContent() instead
         */
 -      protected function getPreloadedText( $preload ) {
 -              global $wgUser, $wgParser;
 +      protected function getPreloadedText( $preload ) { #NOTE: B/C only, replace usage!
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = $this->getPreloadedContent( $preload );
 +              $text = $content->serialize( $this->content_format ); #XXX: really use serialized form? use ContentHandler::getContentText() instead?!
  
 -              if ( !empty( $this->mPreloadText ) ) {
 -                      return $this->mPreloadText;
 +              return $text;
 +      }
 +
 +      /**
 +       * Get the contents to be preloaded into the box, either set by
 +       * an earlier setPreloadText() or by loading the given page.
 +       *
 +       * @param $preload String: representing the title to preload from.
 +       *
 +       * @return Content
 +       *
 +       * @since 1.WD
 +       */
 +      protected function getPreloadedContent( $preload ) { #@todo: use this!
 +              global $wgUser;
 +
 +              if ( !empty( $this->mPreloadContent ) ) {
 +                      return $this->mPreloadContent;
                }
  
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +
                if ( $preload === '' ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $title = Title::newFromText( $preload );
                # Check for existence to avoid getting MediaWiki:Noarticletext
                if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                      return '';
 +                      return $handler->makeEmptyContent();
                }
  
                $page = WikiPage::factory( $title );
                        $title = $page->getRedirectTarget();
                        # Same as before
                        if ( $title === null || !$title->exists() || !$title->userCan( 'read' ) ) {
 -                              return '';
 +                              return $handler->makeEmptyContent();
                        }
                        $page = WikiPage::factory( $title );
                }
  
                $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              return $wgParser->getPreloadText( $page->getRawText(), $title, $parserOptions );
 +              $content = $page->getContent( Revision::RAW );
 +
 +              return $content->preloadTransform( $title, $parserOptions );
        }
  
        /**
                        case self::AS_HOOK_ERROR:
                                return false;
  
 +                      case self::AS_PARSE_ERROR:
 +                              $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>');
 +                              #FIXME: cause editform to be shown again, not just an error!
 +                              return false;
 +
                        case self::AS_SUCCESS_NEW_ARTICLE:
                                $query = $resultDetails['redirect'] ? 'redirect=no' : '';
                                $anchor = isset ( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
  
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
  
                wfProfileOut( __METHOD__ . '-checks' );
  
-               # If article is new, insert it.
-               $aid = $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
-               $new = ( $aid == 0 );
+               // Use SELECT FOR UPDATE here to avoid transaction collision in
+               // WikiPage::updateRevisionOn() and ending in the self::AS_END case.
+               $this->mArticle->loadPageData( 'forupdate' );
+               $new = !$this->mArticle->exists();
  
 -              if ( $new ) {
 -                      // Late check for create permission, just in case *PARANOIA*
 -                      if ( !$this->mTitle->userCan( 'create' ) ) {
 -                              $status->fatal( 'nocreatetext' );
 -                              $status->value = self::AS_NO_CREATE_PERMISSION;
 -                              wfDebug( __METHOD__ . ": no create permission\n" );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +              try {
 +                      if ( $new ) {
 +                              // Late check for create permission, just in case *PARANOIA*
 +                              if ( !$this->mTitle->userCan( 'create' ) ) {
 +                                      $status->fatal( 'nocreatetext' );
 +                                      $status->value = self::AS_NO_CREATE_PERMISSION;
 +                                      wfDebug( __METHOD__ . ": no create permission\n" );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      # Don't save a new article if it's blank.
 -                      if ( $this->textbox1 == '' ) {
 -                              $status->setResult( false, self::AS_BLANK_ARTICLE );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              # Don't save a new article if it's blank.
 +                              if ( $this->textbox1 == '' ) {
 +                                      $status->setResult( false, self::AS_BLANK_ARTICLE );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      $text = $this->textbox1;
 -                      $result['sectionanchor'] = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                              $result['sectionanchor'] = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
  
 -                                      // Create a link to the new section from the edit summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +
 +                                              // Create a link to the new section from the edit summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
                                }
 -                      }
  
 -                      $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 +                              $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +                      } else { # not $new
  
 -                      # Article exists. Check for edit conflict.
 -                      $timestamp = $this->mArticle->getTimestamp();
 -                      wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +                              # Article exists. Check for edit conflict.
  
 -                      if ( $timestamp != $this->edittime ) {
 -                              $this->isConflict = true;
 -                              if ( $this->section == 'new' ) {
 -                                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 -                                              $this->mArticle->getComment() == $this->summary ) {
 -                                              // Probably a duplicate submission of a new comment.
 -                                              // This can happen when squid resends a request after
 -                                              // a timeout but the first one actually went through.
 -                                              wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 -                                      } else {
 -                                              // New comment; suppress conflict.
 +                              $this->mArticle->clear(); # Force reload of dates, etc.
 +                              $timestamp = $this->mArticle->getTimestamp();
 +
 +                              wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +
 +                              if ( $timestamp != $this->edittime ) {
 +                                      $this->isConflict = true;
 +                                      if ( $this->section == 'new' ) {
 +                                              if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 +                                                      $this->mArticle->getComment() == $this->summary ) {
 +                                                      // Probably a duplicate submission of a new comment.
 +                                                      // This can happen when squid resends a request after
 +                                                      // a timeout but the first one actually went through.
 +                                                      wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 +                                              } else {
 +                                                      // New comment; suppress conflict.
 +                                                      $this->isConflict = false;
 +                                                      wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 +                                              }
 +                                      } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 +                                              # Suppress edit conflict with self, except for section edits where merging is required.
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                                $this->isConflict = false;
 -                                              wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
 -                              } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 -                                      # Suppress edit conflict with self, except for section edits where merging is required.
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 -                                      $this->isConflict = false;
                                }
 -                      }
  
 -                      // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 -                      // backwards compatibility with old forms/bots).
 -                      if ( $this->sectiontitle !== '' ) {
 -                              $sectionTitle = $this->sectiontitle;
 -                      } else {
 -                              $sectionTitle = $this->summary;
 -                      }
 -
 -                      if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 -                      } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 -                      }
 -                      if ( is_null( $text ) ) {
 -                              wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 -                              $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 -                      } elseif ( $this->isConflict ) {
 -                              # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 -                                      // Successful merge! Maybe we should tell the user the good news?
 -                                      $this->isConflict = false;
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                              // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 +                              // backwards compatibility with old forms/bots).
 +                              if ( $this->sectiontitle !== '' ) {
 +                                      $sectionTitle = $this->sectiontitle;
                                } else {
 -                                      $this->section = '';
 -                                      $this->textbox1 = $text;
 -                                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      $sectionTitle = $this->summary;
                                }
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              $status->setResult( false, self::AS_CONFLICT_DETECTED );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                              $content = null;
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              if ( $this->isConflict ) {
 +                                      wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
 +                              } else {
 +                                      wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
 +                              }
  
 -                      # Handle the user preference to force summaries here, but not for null edits
 -                      if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 -                      {
 -                              if ( md5( $this->summary ) == $this->autoSumm ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' );
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 +                              if ( is_null( $content ) ) {
 +                                      wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 +                                      $this->isConflict = true;
 +                                      $content = $textbox_content; // do not try to merge here!
 +                              } elseif ( $this->isConflict ) {
 +                                      # Attempt merge
 +                                      if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
 +                                              // Successful merge! Maybe we should tell the user the good news?
 +                                              $this->isConflict = false;
 +                                              $content = $textbox_content;
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                                      } else {
 +                                              $this->section = '';
 +                                              #$this->textbox1 = $text; #redundant, nothing to do here?
 +                                              wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      }
                                }
 -                      }
  
 -                      # And a similar thing for new sections
 -                      if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 -                              if ( trim( $this->summary ) == '' ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 +                              if ( $this->isConflict ) {
 +                                      $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                      }
  
 -                      # All's well
 -                      wfProfileIn( __METHOD__ . '-sectionanchor' );
 -                      $sectionanchor = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->textbox1 == '' ) {
 -                                      $this->missingComment = true;
 -                                      $status->fatal( 'missingcommenttext' );
 -                                      $status->value = self::AS_TEXTBOX_EMPTY;
 -                                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
 +                                              || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                              # Handle the user preference to force summaries here, but not for null edits
 +                              if ( $this->section != 'new' && !$this->allowBlankSummary
 +                                      && !$content->equals( $this->getOriginalContent() )
 +                                      && !$content->isRedirect() ) # check if it's not a redirect
 +                              {
 +                                      if ( md5( $this->summary ) == $this->autoSumm ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' );
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
                                        }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -                                      # This is a new section, so create a link to the new section
 -                                      # in the revision summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
                                }
 -                      } elseif ( $this->section != '' ) {
 -                              # Try to get a section anchor from the section source, redirect to edited section if header found
 -                              # XXX: might be better to integrate this into Article::replaceSection
 -                              # for duplicate heading checking and maybe parsing
 -                              $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 -                              # we can't deal with anchors, includes, html etc in the header for now,
 -                              # headline would need to be parsed to improve this
 -                              if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +
 +                              # And a similar thing for new sections
 +                              if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 +                                      if ( trim( $this->summary ) == '' ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
                                }
 -                      }
 -                      $result['sectionanchor'] = $sectionanchor;
 -                      wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      // Save errors may fall down to the edit form, but we've now
 -                      // merged the section into full text. Clear the section field
 -                      // so that later submission of conflict forms won't try to
 -                      // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 -                      $this->section = '';
 +                              # All's well
 +                              wfProfileIn( __METHOD__ . '-sectionanchor' );
 +                              $sectionanchor = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->textbox1 == '' ) {
 +                                              $this->missingComment = true;
 +                                              $status->fatal( 'missingcommenttext' );
 +                                              $status->value = self::AS_TEXTBOX_EMPTY;
 +                                              wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                                              # This is a new section, so create a link to the new section
 +                                              # in the revision summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
 +                              } elseif ( $this->section != '' ) {
 +                                      # Try to get a section anchor from the section source, redirect to edited section if header found
 +                                      # XXX: might be better to integrate this into Article::replaceSection
 +                                      # for duplicate heading checking and maybe parsing
 +                                      $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 +                                      # we can't deal with anchors, includes, html etc in the header for now,
 +                                      # headline would need to be parsed to improve this
 +                                      if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +                                      }
 +                              }
 +                              $result['sectionanchor'] = $sectionanchor;
 +                              wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      $status->value = self::AS_SUCCESS_UPDATE;
 -              }
 +                              // Save errors may fall down to the edit form, but we've now
 +                              // merged the section into full text. Clear the section field
 +                              // so that later submission of conflict forms won't try to
 +                              // replace that into a duplicated mess.
 +                                      $this->textbox1 = $content->serialize( $this->content_format );
 +                              $this->section = '';
  
 -              // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 -              if ( $this->kblength > $wgMaxArticleSize ) {
 -                      $this->tooBig = true;
 -                      $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              }
 +                              $status->value = self::AS_SUCCESS_UPDATE;
 +                      }
  
 -              $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 -                      ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 -                      ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 -                      ( $bot ? EDIT_FORCE_BOT : 0 );
 +                      // Check for length errors again now that the section is merged in
 +                              $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
 +                      if ( $this->kblength > $wgMaxArticleSize ) {
 +                              $this->tooBig = true;
 +                              $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                      $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 +                              ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 +                              ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 +                              ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 -                      $this->commitWatch();
 +                              $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
 +
 +                      if ( $doEditStatus->isOK() ) {
 +                                      $result['redirect'] = $content->isRedirect();
 +                              $this->commitWatch();
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      } else {
 +                              $this->isConflict = true;
 +                              $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
 +                              wfProfileOut( __METHOD__ );
 +                              return $doEditStatus;
 +                      }
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
                        return $status;
 -              } else {
 -                      $this->isConflict = true;
 -                      $doEditStatus->value = self::AS_END; // Destroys data doEdit() put in $status->value but who cares
 -                      wfProfileOut( __METHOD__ );
 -                      return $doEditStatus;
                }
        }
  
         * @parma $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
 -      function mergeChangesInto( &$editText ) {
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.WD" );
 +
 +              $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
 +       */
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $baseText = $baseRevision->getText();
 +              $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                        }
                }
  
 +              #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $content->serialize( $this->content_format );
  
                        $this->showTextbox1();
                } else {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 +              $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                                      $this->section, $textboxContent,
 +                                                                                                      $this->summary, $this->edittime );
 +
 +              # hanlde legacy text-based hook
 +              $newtext_orig = $newContent->serialize( $this->content_format );
 +              $newtext = $newtext_orig; #clone
                wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
  
 +              if ( $newtext != $newtext_orig ) {
 +                                              #if the hook changed the text, create a new Content object accordingly.
 +                                              $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModel() ); #XXX: handle parse errors ?
 +              }
 +
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 +
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
  
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMsg( 'token_suffix_mismatch' );
 -                      } else {
 -                              $note = wfMsg( 'session_fail_preview' );
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMsg( 'edit_form_incomplete' );
 -              } else {
 -                      $note = wfMsg( 'previewnote' ) .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setTidy( true );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 -
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 +              try {
 +                      $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
 -                                      $class .= " mw-js";
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMsg( 'token_suffix_mismatch' );
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMsg( 'session_fail_preview' );
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMsg( 'edit_form_incomplete' );
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMsg( 'previewnote' ) .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 +                      }
 +
 +                      $parserOptions = ParserOptions::newFromUser( $wgUser );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              # don't parse non-wikitext pages, show message about preview
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
 +                              } else {
 +                                      $note = wfMsg( 'previewnote' );
 +                              }
 +                      } else {
 +                              $note = wfMsg( 'previewnote' );
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 +                      $rt = $content->getRedirectChain();
  
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $toparse_orig = $content->serialize( $this->content_format );
 +                              $toparse = $toparse_orig;
 +                              wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +
 +                              if ( $toparse !== $toparse_orig ) {
 +                                      #hook changed the text, create new Content object
 +                                      $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
 +                              }
 +
 +                              wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) );
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $parserOptions->enableLimitReport();
 +
 +                              #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +
 +                              // TODO: might be a saner way to get a meaningfull context here?
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getContext(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
diff --combined includes/Export.php
@@@ -49,6 -49,15 +49,15 @@@ class WikiExporter 
        const TEXT = 0;
        const STUB = 1;
  
+       var $buffer;
+       var $text;
+       /**
+        * @var DumpOutput
+        */
+       var $sink;
        /**
         * If using WikiExporter::STREAM to stream a large amount of data,
         * provide a database connection which is not managed by
                        ' AND page_title=' . $this->db->addQuotes( $title->getDBkey() ) );
        }
  
+       /**
+        * @param $name string
+        * @throws MWException
+        */
        public function pageByName( $name ) {
                $title = Title::newFromText( $name );
                if ( is_null( $title ) ) {
                }
        }
  
+       /**
+        * @param $names array
+        */
        public function pagesByName( $names ) {
                foreach ( $names as $name ) {
                        $this->pageByName( $name );
                $this->dumpFrom( '' );
        }
  
+       /**
+        * @param $start int
+        * @param $end int
+        */
        public function logsByRange( $start, $end ) {
                $condition = 'log_id >= ' . intval( $start );
                if ( $end ) {
                $this->dumpFrom( $condition );
        }
  
-       # Generates the distinct list of authors of an article
-       # Not called by default (depends on $this->list_authors)
-       # Can be set by Special:Export when not exporting whole history
+       /**
+        * Generates the distinct list of authors of an article
+        * Not called by default (depends on $this->list_authors)
+        * Can be set by Special:Export when not exporting whole history
+        *
+        * @param $cond
+        */
        protected function do_list_authors( $cond ) {
                wfProfileIn( __METHOD__ );
                $this->author_list = "<contributors>";
                wfProfileOut( __METHOD__ );
        }
  
+       /**
+        * @param $cond string
+        * @throws MWException
+        * @throws Exception
+        */
        protected function dumpFrom( $cond = '' ) {
                wfProfileIn( __METHOD__ );
                # For logging dumps...
                }
        }
  
+       /**
+        * @param $resultset array
+        */
        protected function outputLogStream( $resultset ) {
                foreach ( $resultset as $row ) {
                        $output = $this->writer->writeLogItem( $row );
@@@ -464,6 -496,9 +496,9 @@@ class XmlDumpWriter 
                        $this->siteInfo();
        }
  
+       /**
+        * @return string
+        */
        function siteInfo() {
                $info = array(
                        $this->sitename(),
                        "\n  </siteinfo>\n";
        }
  
+       /**
+        * @return string
+        */
        function sitename() {
                global $wgSitename;
                return Xml::element( 'sitename', array(), $wgSitename );
        }
  
+       /**
+        * @return string
+        */
        function generator() {
                global $wgVersion;
                return Xml::element( 'generator', array(), "MediaWiki $wgVersion" );
        }
  
+       /**
+        * @return string
+        */
        function homelink() {
                return Xml::element( 'base', array(), Title::newMainPage()->getCanonicalUrl() );
        }
  
+       /**
+        * @return string
+        */
        function caseSetting() {
                global $wgCapitalLinks;
                // "case-insensitive" option is reserved for future
                return Xml::element( 'case', array(), $sensitivity );
        }
  
+       /**
+        * @return string
+        */
        function namespaces() {
                global $wgContLang;
                $spaces = "<namespaces>\n";
                if ( $row->rev_deleted & Revision::DELETED_COMMENT ) {
                        $out .= "      " . Xml::element( 'comment', array( 'deleted' => 'deleted' ) ) . "\n";
                } elseif ( $row->rev_comment != '' ) {
-                       $out .= "      " . Xml::elementClean( 'comment', null, strval( $row->rev_comment ) ) . "\n";
+                       $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
  
 +              if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
 +                      $name = ContentHandler::getContentModelName( $row->rev_content_model );
 +                      $out .= "      " . Xml::element('model', array( 'name' => $name ), strval( $row->rev_content_model ) ) . "\n";
 +              }
 +
 +              if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
 +                      $mime = ContentHandler::getContentFormatMimeType( $row->rev_content_format );
 +                      $out .= "      " . Xml::element('format', array( 'mime' => $mime ), strval( $row->rev_content_format ) ) . "\n";
 +              }
 +
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                return $out;
        }
  
+       /**
+        * @param $timestamp string
+        * @return string
+        */
        function writeTimestamp( $timestamp ) {
                $ts = wfTimestamp( TS_ISO_8601, $timestamp );
                return "      " . Xml::element( 'timestamp', null, $ts ) . "\n";
        }
  
+       /**
+        * @param $id
+        * @param $text string
+        * @return string
+        */
        function writeContributor( $id, $text ) {
                $out = "      <contributor>\n";
                if ( $id || !IP::isValid( $text ) ) {
  
        /**
         * Warning! This data is potentially inconsistent. :(
+        * @param $row
+        * @param $dumpContents bool
         * @return string
         */
        function writeUploads( $row, $dumpContents = false ) {
   * @ingroup Dump
   */
  class DumpOutput {
+       /**
+        * @param $string string
+        */
        function writeOpenStream( $string ) {
                $this->write( $string );
        }
  
+       /**
+        * @param $string string
+        */
        function writeCloseStream( $string ) {
                $this->write( $string );
        }
  
+       /**
+        * @param $page
+        * @param $string string
+        */
        function writeOpenPage( $page, $string ) {
                $this->write( $string );
        }
  
+       /**
+        * @param $string string
+        */
        function writeClosePage( $string ) {
                $this->write( $string );
        }
  
+       /**
+        * @param $rev
+        * @param $string string
+        */
        function writeRevision( $rev, $string ) {
                $this->write( $string );
        }
  
+       /**
+        * @param $rev
+        * @param $string string
+        */
        function writeLogItem( $rev, $string ) {
                $this->write( $string );
        }
  
        /**
         * Override to write to a different stream type.
+        * @param $string string
         * @return bool
         */
        function write( $string ) {
  class DumpFileOutput extends DumpOutput {
        protected $handle = false, $filename;
  
+       /**
+        * @param $file
+        */
        function __construct( $file ) {
                $this->handle = fopen( $file, "wt" );
                $this->filename = $file;
        }
  
+       /**
+        * @param $string string
+        */
        function writeCloseStream( $string ) {
                parent::writeCloseStream( $string );
                if ( $this->handle ) {
                }
        }
  
+       /**
+        * @param $string string
+        */
        function write( $string ) {
                fputs( $this->handle, $string );
        }
  
+       /**
+        * @param $newname
+        */
        function closeRenameAndReopen( $newname ) {
                $this->closeAndRename( $newname, true );
        }
  
+       /**
+        * @param $newname
+        * @throws MWException
+        */
        function renameOrException( $newname ) {
                        if (! rename( $this->filename, $newname ) ) {
                                throw new MWException( __METHOD__ . ": rename of file {$this->filename} to $newname failed\n" );
                        }
        }
  
+       /**
+        * @param $newname array
+        * @return mixed
+        * @throws MWException
+        */
        function checkRenameArgCount( $newname ) {
                if ( is_array( $newname ) ) {
                        if ( count( $newname ) > 1 ) {
                return $newname;
        }
  
+       /**
+        * @param $newname mixed
+        * @param $open bool
+        */
        function closeAndRename( $newname, $open = false ) {
                $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
                }
        }
  
+       /**
+        * @return string|null
+        */
        function getFilenames() {
                return $this->filename;
        }
@@@ -922,6 -1024,10 +1034,10 @@@ class DumpPipeOutput extends DumpFileOu
        protected $command, $filename;
        protected $procOpenResource = false;
  
+       /**
+        * @param $command
+        * @param $file null
+        */
        function __construct( $command, $file = null ) {
                if ( !is_null( $file ) ) {
                        $command .=  " > " . wfEscapeShellArg( $file );
                $this->filename = $file;
        }
  
+       /**
+        * @param $string string
+        */
        function writeCloseStream( $string ) {
                parent::writeCloseStream( $string );
                if ( $this->procOpenResource ) {
                }
        }
  
+       /**
+        * @param $command
+        */
        function startCommand( $command ) {
                $spec = array(
                        0 => array( "pipe", "r" ),
                $this->handle = $pipes[0];
        }
  
+       /**
+        * @param mixed $newname
+        */
        function closeRenameAndReopen( $newname ) {
                $this->closeAndRename( $newname, true );
        }
  
+       /**
+        * @param $newname mixed
+        * @param $open bool
+        */
        function closeAndRename( $newname, $open = false ) {
                $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
   * @ingroup Dump
   */
  class DumpGZipOutput extends DumpPipeOutput {
+       /**
+        * @param $file string
+        */
        function __construct( $file ) {
                parent::__construct( "gzip", $file );
        }
   * @ingroup Dump
   */
  class DumpBZip2Output extends DumpPipeOutput {
+       /**
+        * @param $file string
+        */
        function __construct( $file ) {
                parent::__construct( "bzip2", $file );
        }
   * @ingroup Dump
   */
  class Dump7ZipOutput extends DumpPipeOutput {
+       /**
+        * @param $file string
+        */
        function __construct( $file ) {
                $command = $this->setup7zCommand( $file );
                parent::__construct( $command );
                $this->filename = $file;
        }
  
+       /**
+        * @param $file string
+        * @return string
+        */
        function setup7zCommand( $file ) {
                $command = "7za a -bd -si " . wfEscapeShellArg( $file );
                // Suppress annoying useless crap from p7zip
                return( $command );
        }
  
+       /**
+        * @param $newname string
+        * @param $open bool
+        */
        function closeAndRename( $newname, $open = false ) {
                $newname = $this->checkRenameArgCount( $newname );
                if ( $newname ) {
        }
  }
  
  /**
   * Dump output filter class.
   * This just does output filtering and streaming; XML formatting is done
   * @ingroup Dump
   */
  class DumpFilter {
+       /**
+        * @var DumpOutput
+        * FIXME will need to be made protected whenever legacy code
+        * is updated.
+        */
+       public $sink;
+       /**
+        * @var bool
+        */
+       protected $sendingThisPage;
+       /**
+        * @param $sink DumpOutput
+        */
        function __construct( &$sink ) {
                $this->sink =& $sink;
        }
  
+       /**
+        * @param $string string
+        */
        function writeOpenStream( $string ) {
                $this->sink->writeOpenStream( $string );
        }
  
+       /**
+        * @param $string string
+        */
        function writeCloseStream( $string ) {
                $this->sink->writeCloseStream( $string );
        }
  
+       /**
+        * @param $page
+        * @param $string string
+        */
        function writeOpenPage( $page, $string ) {
                $this->sendingThisPage = $this->pass( $page, $string );
                if ( $this->sendingThisPage ) {
                }
        }
  
+       /**
+        * @param $string string
+        */
        function writeClosePage( $string ) {
                if ( $this->sendingThisPage ) {
                        $this->sink->writeClosePage( $string );
                }
        }
  
+       /**
+        * @param $rev
+        * @param $string string
+        */
        function writeRevision( $rev, $string ) {
                if ( $this->sendingThisPage ) {
                        $this->sink->writeRevision( $rev, $string );
                }
        }
  
+       /**
+        * @param $rev
+        * @param $string string
+        */
        function writeLogItem( $rev, $string ) {
                $this->sink->writeRevision( $rev, $string );
        }
  
+       /**
+        * @param $newname string
+        */
        function closeRenameAndReopen( $newname ) {
                $this->sink->closeRenameAndReopen( $newname );
        }
  
+       /**
+        * @param $newname string
+        * @param $open bool
+        */
        function closeAndRename( $newname, $open = false ) {
                $this->sink->closeAndRename( $newname, $open );
        }
  
+       /**
+        * @return array
+        */
        function getFilenames() {
                return $this->sink->getFilenames();
        }
  
        /**
         * Override for page-based filter types.
+        * @param $page
         * @return bool
         */
        function pass( $page ) {
   * @ingroup Dump
   */
  class DumpNotalkFilter extends DumpFilter {
+       /**
+        * @param $page
+        * @return bool
+        */
        function pass( $page ) {
                return !MWNamespace::isTalk( $page->page_namespace );
        }
@@@ -1112,6 -1302,10 +1312,10 @@@ class DumpNamespaceFilter extends DumpF
        var $invert = false;
        var $namespaces = array();
  
+       /**
+        * @param $sink DumpOutput
+        * @param $param
+        */
        function __construct( &$sink, $param ) {
                parent::__construct( $sink );
  
                }
        }
  
+       /**
+        * @param $page
+        * @return bool
+        */
        function pass( $page ) {
                $match = isset( $this->namespaces[$page->page_namespace] );
                return $this->invert xor $match;
  class DumpLatestFilter extends DumpFilter {
        var $page, $pageString, $rev, $revString;
  
+       /**
+        * @param $page
+        * @param $string string
+        */
        function writeOpenPage( $page, $string ) {
                $this->page = $page;
                $this->pageString = $string;
        }
  
+       /**
+        * @param $string string
+        */
        function writeClosePage( $string ) {
                if ( $this->rev ) {
                        $this->sink->writeOpenPage( $this->page, $this->pageString );
                $this->pageString = null;
        }
  
+       /**
+        * @param $rev
+        * @param $string string
+        */
        function writeRevision( $rev, $string ) {
                if ( $rev->rev_id == $this->page->page_latest ) {
                        $this->rev = $rev;
   * @ingroup Dump
   */
  class DumpMultiWriter {
+       /**
+        * @param $sinks
+        */
        function __construct( $sinks ) {
                $this->sinks = $sinks;
                $this->count = count( $sinks );
        }
  
+       /**
+        * @param $string string
+        */
        function writeOpenStream( $string ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->writeOpenStream( $string );
                }
        }
  
+       /**
+        * @param $string string
+        */
        function writeCloseStream( $string ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->writeCloseStream( $string );
                }
        }
  
+       /**
+        * @param $page
+        * @param $string string
+        */
        function writeOpenPage( $page, $string ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->writeOpenPage( $page, $string );
                }
        }
  
+       /**
+        * @param $string
+        */
        function writeClosePage( $string ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->writeClosePage( $string );
                }
        }
  
+       /**
+        * @param $rev
+        * @param $string
+        */
        function writeRevision( $rev, $string ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->writeRevision( $rev, $string );
                }
        }
  
+       /**
+        * @param $newnames
+        */
        function closeRenameAndReopen( $newnames ) {
                $this->closeAndRename( $newnames, true );
        }
  
+       /**
+        * @param $newnames array
+        * @param bool $open
+        */
        function closeAndRename( $newnames, $open = false ) {
                for ( $i = 0; $i < $this->count; $i++ ) {
                        $this->sinks[$i]->closeAndRename( $newnames[$i], $open );
                }
        }
  
+       /**
+        * @return array
+        */
        function getFilenames() {
                $filenames = array();
                for ( $i = 0; $i < $this->count; $i++ ) {
  
  }
  
+ /**
+  * @param $string string
+  * @return string
+  */
  function xmlsafe( $string ) {
        wfProfileIn( __FUNCTION__ );
  
diff --combined includes/ImagePage.php
@@@ -1,4 -1,25 +1,25 @@@
  <?php
+ /**
+  * Special handling for file description pages.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
+  * @file
+  */
  /**
   * Class for viewing MediaWiki file description pages
   *
@@@ -30,7 -51,7 +51,7 @@@ class ImagePage extends Article 
        /**
         * Constructor from a page id
         * @param $id Int article ID to load
-        * @returnImagePage|null
+        * @return ImagePage|null
         */
        public static function newFromID( $id ) {
                $t = Title::newFromID( $id );
@@@ -51,7 -72,7 +72,7 @@@
  
        protected function loadFile() {
                if ( $this->fileLoaded ) {
-                       return true;
+                       return;
                }
                $this->fileLoaded = true;
  
         * Include body text only; none of the image extras
         */
        public function render() {
-               global $wgOut;
-               $wgOut->setArticleBodyOnly( true );
+               $this->getContext()->setArticleBodyOnly( true );
                parent::view();
        }
  
        public function view() {
-               global $wgOut, $wgShowEXIF, $wgRequest, $wgUser;
+               global $wgShowEXIF;
  
-               $diff = $wgRequest->getVal( 'diff' );
-               $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
+               $out = $this->getContext()->getOutput();
+               $request = $this->getContext()->getRequest();
+               $diff = $request->getVal( 'diff' );
+               $diffOnly = $request->getBool( 'diffonly', $this->getContext()->getUser()->getOption( 'diffonly' ) );
  
                if ( $this->getTitle()->getNamespace() != NS_FILE || ( isset( $diff ) && $diffOnly ) ) {
                        parent::view();
                        if ( $this->getTitle()->getDBkey() == $this->mPage->getFile()->getName() || isset( $diff ) ) {
                                // mTitle is the same as the redirect target so ask Article
                                // to perform the redirect for us.
-                               $wgRequest->setVal( 'diffonly', 'true' );
+                               $request->setVal( 'diffonly', 'true' );
                                parent::view();
                                return;
                        } else {
                                // mTitle is not the same as the redirect target so it is
                                // probably the redirect page itself. Fake the redirect symbol
-                               $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
-                               $wgOut->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ),
+                               $out->setPageTitle( $this->getTitle()->getPrefixedText() );
+                               $out->addHTML( $this->viewRedirect( Title::makeTitle( NS_FILE, $this->mPage->getFile()->getName() ),
                                        /* $appendSubtitle */ true, /* $forceKnown */ true ) );
                                $this->mPage->doViewUpdates( $this->getContext()->getUser() );
                                return;
                }
  
                if ( !$diff && $this->displayImg->exists() ) {
-                       $wgOut->addHTML( $this->showTOC( $showmeta ) );
+                       $out->addHTML( $this->showTOC( $showmeta ) );
                }
  
                if ( !$diff ) {
                        # NS_FILE is in the user language, but this section (the actual wikitext)
                        # should be in page content language
                        $pageLang = $this->getTitle()->getPageLanguage();
-                       $wgOut->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
+                       $out->addHTML( Xml::openElement( 'div', array( 'id' => 'mw-imagepage-content',
                                'lang' => $pageLang->getCode(), 'dir' => $pageLang->getDir(),
                                'class' => 'mw-content-'.$pageLang->getDir() ) ) );
 -                      parent::view();
 -                      $out->addHTML( Xml::closeElement( 'div' ) );
 +
 +            parent::view(); #FIXME: use ContentHandler::makeArticle() !!
 +
 +                      $wgOut->addHTML( Xml::closeElement( 'div' ) );
                } else {
                        # Just need to set the right headers
-                       $wgOut->setArticleFlag( true );
-                       $wgOut->setPageTitle( $this->getTitle()->getPrefixedText() );
+                       $out->setArticleFlag( true );
+                       $out->setPageTitle( $this->getTitle()->getPrefixedText() );
                        $this->mPage->doViewUpdates( $this->getContext()->getUser() );
                }
  
                if ( $this->mExtraDescription ) {
                        $fol = wfMessage( 'shareddescriptionfollows' );
                        if ( !$fol->isDisabled() ) {
-                               $wgOut->addWikiText( $fol->plain() );
+                               $out->addWikiText( $fol->plain() );
                        }
-                       $wgOut->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
+                       $out->addHTML( '<div id="shared-image-desc">' . $this->mExtraDescription . "</div>\n" );
                }
  
                $this->closeShowImage();
                $this->imageHistory();
                // TODO: Cleanup the following
  
-               $wgOut->addHTML( Xml::element( 'h2',
+               $out->addHTML( Xml::element( 'h2',
                        array( 'id' => 'filelinks' ),
                        wfMsg( 'imagelinks' ) ) . "\n" );
                $this->imageDupes();
                $html = '';
                wfRunHooks( 'ImagePageAfterImageLinks', array( $this, &$html ) );
                if ( $html ) {
-                       $wgOut->addHTML( $html );
+                       $out->addHTML( $html );
                }
  
                if ( $showmeta ) {
-                       $wgOut->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ) . "\n" );
-                       $wgOut->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
-                       $wgOut->addModules( array( 'mediawiki.action.view.metadata' ) );
+                       $out->addHTML( Xml::element( 'h2', array( 'id' => 'metadata' ), wfMsg( 'metadata' ) ) . "\n" );
+                       $out->addWikiText( $this->makeMetadataTable( $formattedMetadata ) );
+                       $out->addModules( array( 'mediawiki.action.view.metadata' ) );
                }
  
                // Add remote Filepage.css
                if( !$this->repo->isLocal() ) {
                        $css = $this->repo->getDescriptionStylesheetUrl();
                        if ( $css ) {
-                               $wgOut->addStyle( $css );
+                               $out->addStyle( $css );
                        }
                }
                // always show the local local Filepage.css, bug 29277
-               $wgOut->addModuleStyles( 'filepage' );
+               $out->addModuleStyles( 'filepage' );
        }
  
        /**
                return $r;
        }
  
 -      /**
 -       * Overloading Article's getContent method.
 -       *
 -       * Omit noarticletext if sharedupload; text will be fetched from the
 -       * shared upload server if possible.
 -       * @return string
 -       */
 -      public function getContent() {
 -              $this->loadFile();
 -              if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 -                      return '';
 -              }
 -              return parent::getContent();
 -      }
 +    /**
 +     * Overloading Article's getContentObject method.
 +     *
 +     * Omit noarticletext if sharedupload; text will be fetched from the
 +     * shared upload server if possible.
 +     * @return string
 +     */
 +    public function getContentObject() {
 +        $this->loadFile();
 +        if ( $this->mPage->getFile() && !$this->mPage->getFile()->isLocal() && 0 == $this->getID() ) {
 +            return null;
 +        }
 +        return parent::getContentObject();
 +    }
  
        protected function openShowImage() {
-               global $wgOut, $wgUser, $wgImageLimits, $wgRequest,
-                       $wgLang, $wgEnableUploads, $wgSend404Code;
+               global $wgImageLimits, $wgEnableUploads, $wgSend404Code;
  
                $this->loadFile();
+               $out = $this->getContext()->getOutput();
+               $user = $this->getContext()->getUser();
+               $lang = $this->getContext()->getLanguage();
+               $dirmark = $lang->getDirMarkEntity();
+               $request = $this->getContext()->getRequest();
  
-               $sizeSel = intval( $wgUser->getOption( 'imagesize' ) );
+               $sizeSel = intval( $user->getOption( 'imagesize' ) );
                if ( !isset( $wgImageLimits[$sizeSel] ) ) {
                        $sizeSel = User::getDefaultOption( 'imagesize' );
  
                $max = $wgImageLimits[$sizeSel];
                $maxWidth = $max[0];
                $maxHeight = $max[1];
-               $dirmark = $wgLang->getDirMarkEntity();
  
                if ( $this->displayImg->exists() ) {
                        # image
-                       $page = $wgRequest->getIntOrNull( 'page' );
+                       $page = $request->getIntOrNull( 'page' );
                        if ( is_null( $page ) ) {
                                $params = array();
                                $page = 1;
  
                        $longDesc = wfMsg( 'parentheses', $this->displayImg->getLongDesc() );
  
-                       wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$wgOut ) );
+                       wfRunHooks( 'ImageOpenShowImageInlineBefore', array( &$this, &$out ) );
  
                        if ( $this->displayImg->allowInlineDisplay() ) {
                                # image
                                        if ( count( $otherSizes ) && $this->displayImg->getRepo()->canTransformVia404() ) {
                                                $msgsmall .= ' ' .
                                                Html::rawElement( 'span', array( 'class' => 'mw-filepage-other-resolutions' ),
-                                                       wfMessage( 'show-big-image-other' )->rawParams( $wgLang->pipeList( $otherSizes ) )->
+                                                       wfMessage( 'show-big-image-other' )->rawParams( $lang->pipeList( $otherSizes ) )->
                                                        params( count( $otherSizes ) )->parse()
                                                );
                                        }
  
                                $isMulti = $this->displayImg->isMultipage() && $this->displayImg->pageCount() > 1;
                                if ( $isMulti ) {
-                                       $wgOut->addHTML( '<table class="multipageimage"><tr><td>' );
+                                       $out->addHTML( '<table class="multipageimage"><tr><td>' );
                                }
  
                                if ( $thumbnail ) {
                                                'alt' => $this->displayImg->getTitle()->getPrefixedText(),
                                                'file-link' => true,
                                        );
-                                       $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+                                       $out->addHTML( '<div class="fullImageLink" id="file">' .
                                                $thumbnail->toHtml( $options ) .
                                                $anchorclose . "</div>\n" );
                                }
                                        $count = $this->displayImg->pageCount();
  
                                        if ( $page > 1 ) {
-                                               $label = $wgOut->parse( wfMsg( 'imgmultipageprev' ), false );
+                                               $label = $out->parse( wfMsg( 'imgmultipageprev' ), false );
                                                $link = Linker::link(
                                                        $this->getTitle(),
                                                        $label,
                                        );
                                        $options = array();
                                        for ( $i = 1; $i <= $count; $i++ ) {
-                                               $options[] = Xml::option( $wgLang->formatNum( $i ), $i, $i == $page );
+                                               $options[] = Xml::option( $lang->formatNum( $i ), $i, $i == $page );
                                        }
                                        $select = Xml::tags( 'select',
                                                array( 'id' => 'pageselector', 'name' => 'page' ),
                                                implode( "\n", $options ) );
  
-                                       $wgOut->addHTML(
+                                       $out->addHTML(
                                                '</td><td><div class="multipageimagenavbox">' .
                                                Xml::openElement( 'form', $formParams ) .
                                                Html::hidden( 'title', $this->getTitle()->getPrefixedDBkey() ) .
                                if ( $this->displayImg->isSafeFile() ) {
                                        $icon = $this->displayImg->iconThumb();
  
-                                       $wgOut->addHTML( '<div class="fullImageLink" id="file">' .
+                                       $out->addHTML( '<div class="fullImageLink" id="file">' .
                                                $icon->toHtml( array( 'file-link' => true ) ) .
                                                "</div>\n" );
                                }
                                        // The dirmark, however, must not be immediately adjacent
                                        // to the filename, because it can get copied with it.
                                        // See bug 25277.
-                                       $wgOut->addWikiText( <<<EOT
+                                       $out->addWikiText( <<<EOT
  <div class="fullMedia"><span class="dangerousLink">{$medialink}</span> $dirmark<span class="fileInfo">$longDesc</span></div>
  <div class="mediaWarning">$warning</div>
  EOT
                                                );
                                } else {
-                                       $wgOut->addWikiText( <<<EOT
+                                       $out->addWikiText( <<<EOT
  <div class="fullMedia">{$medialink} {$dirmark}<span class="fileInfo">$longDesc</span>
  </div>
  EOT
                                # No article exists either
                                # Show deletion log to be consistent with normal articles
                                LogEventsList::showLogExtract(
-                                       $wgOut,
+                                       $out,
                                        array( 'delete', 'move' ),
                                        $this->getTitle()->getPrefixedText(),
                                        '',
                                );
                        }
  
-                       if ( $wgEnableUploads && $wgUser->isAllowed( 'upload' ) ) {
+                       if ( $wgEnableUploads && $user->isAllowed( 'upload' ) ) {
                                // Only show an upload link if the user can upload
                                $uploadTitle = SpecialPage::getTitleFor( 'Upload' );
                                $nofile = array(
                        // Note, if there is an image description page, but
                        // no image, then this setRobotPolicy is overriden
                        // by Article::View().
-                       $wgOut->setRobotPolicy( 'noindex,nofollow' );
-                       $wgOut->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
+                       $out->setRobotPolicy( 'noindex,nofollow' );
+                       $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile );
                        if ( !$this->getID() && $wgSend404Code ) {
                                // If there is no image, no shared image, and no description page,
                                // output a 404, to be consistent with articles.
-                               $wgRequest->response()->header( 'HTTP/1.1 404 Not Found' );
+                               $request->response()->header( 'HTTP/1.1 404 Not Found' );
                        }
                }
-               $wgOut->setFileVersion( $this->displayImg );
+               $out->setFileVersion( $this->displayImg );
        }
  
        /**
         * Show a notice that the file is from a shared repository
         */
        protected function printSharedImageText() {
-               global $wgOut;
+               $out = $this->getContext()->getOutput();
                $this->loadFile();
  
                $descUrl = $this->mPage->getFile()->getDescriptionUrl();
  
                /* Add canonical to head if there is no local page for this shared file */
                if( $descUrl && $this->mPage->getID() == 0 ) {
-                       $wgOut->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) );
+                       $out->addLink( array( 'rel' => 'canonical', 'href' => $descUrl ) );
                }
  
                $wrap = "<div class=\"sharedUploadNotice\">\n$1\n</div>\n";
                $repo = $this->mPage->getFile()->getRepo()->getDisplayName();
  
                if ( $descUrl && $descText && wfMsgNoTrans( 'sharedupload-desc-here' ) !== '-'  ) {
-                       $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) );
+                       $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-here', $repo, $descUrl ) );
                } elseif ( $descUrl && wfMsgNoTrans( 'sharedupload-desc-there' ) !== '-' ) {
-                       $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) );
+                       $out->wrapWikiMsg( $wrap, array( 'sharedupload-desc-there', $repo, $descUrl ) );
                } else {
-                       $wgOut->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ );
+                       $out->wrapWikiMsg( $wrap, array( 'sharedupload', $repo ), ''/*BACKCOMPAT*/ );
                }
  
                if ( $descText ) {
         * external editing (and instructions link) etc.
         */
        protected function uploadLinksBox() {
-               global $wgUser, $wgOut, $wgEnableUploads, $wgUseExternalEditor;
+               global $wgEnableUploads, $wgUseExternalEditor;
  
                if ( !$wgEnableUploads ) {
                        return;
                        return;
                }
  
-               $wgOut->addHTML( "<br /><ul>\n" );
+               $out = $this->getContext()->getOutput();
+               $out->addHTML( "<br /><ul>\n" );
  
                # "Upload a new version of this file" link
-               if ( UploadBase::userCanReUpload( $wgUser, $this->mPage->getFile()->name ) ) {
+               if ( UploadBase::userCanReUpload( $this->getContext()->getUser(), $this->mPage->getFile()->name ) ) {
                        $ulink = Linker::makeExternalLink( $this->getUploadUrl(), wfMsg( 'uploadnewversion-linktext' ) );
-                       $wgOut->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" );
+                       $out->addHTML( "<li id=\"mw-imagepage-reupload-link\"><div class=\"plainlinks\">{$ulink}</div></li>\n" );
                }
  
                # External editing link
                                ),
                                array( 'known', 'noclasses' )
                        );
-                       $wgOut->addHTML(
+                       $out->addHTML(
                                '<li id="mw-imagepage-edit-external">' . $elink . ' <small>' .
                                wfMsgExt( 'edit-externally-help', array( 'parseinline' ) ) .
                                "</small></li>\n"
                        );
                }
  
-               $wgOut->addHTML( "</ul>\n" );
+               $out->addHTML( "</ul>\n" );
        }
  
        protected function closeShowImage() { } # For overloading
         * we follow it with an upload history of the image and its usage.
         */
        protected function imageHistory() {
-               global $wgOut;
                $this->loadFile();
+               $out = $this->getContext()->getOutput();
                $pager = new ImageHistoryPseudoPager( $this );
-               $wgOut->addHTML( $pager->getBody() );
-               $wgOut->preventClickjacking( $pager->getPreventClickjacking() );
+               $out->addHTML( $pager->getBody() );
+               $out->preventClickjacking( $pager->getPreventClickjacking() );
  
                $this->mPage->getFile()->resetHistory(); // free db resources
  
        }
  
        protected function imageLinks() {
-               global $wgOut, $wgLang;
                $limit = 100;
  
+               $out = $this->getContext()->getOutput();
                $res = $this->queryImageLinks( $this->getTitle()->getDbKey(), $limit + 1);
                $rows = array();
                $redirects = array();
                }
  
                if ( $count == 0 ) {
-                       $wgOut->wrapWikiMsg(
+                       $out->wrapWikiMsg(
                                Html::rawElement( 'div',
                                        array( 'id' => 'mw-imagepage-nolinkstoimage' ), "\n$1\n" ),
                                'nolinkstoimage'
                        return;
                }
  
-               $wgOut->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
+               $out->addHTML( "<div id='mw-imagepage-section-linkstoimage'>\n" );
                if ( !$hasMore ) {
-                       $wgOut->addWikiMsg( 'linkstoimage', $count );
+                       $out->addWikiMsg( 'linkstoimage', $count );
                } else {
                        // More links than the limit. Add a link to [[Special:Whatlinkshere]]
-                       $wgOut->addWikiMsg( 'linkstoimage-more',
-                               $wgLang->formatNum( $limit ),
+                       $out->addWikiMsg( 'linkstoimage-more',
+                               $this->getContext()->getLanguage()->formatNum( $limit ),
                                $this->getTitle()->getPrefixedDBkey()
                        );
                }
  
-               $wgOut->addHTML(
+               $out->addHTML(
                        Html::openElement( 'ul',
                                array( 'class' => 'mw-imagepage-linkstoimage' ) ) . "\n"
                );
                                $liContents = wfMessage( 'linkstoimage-redirect' )->rawParams(
                                        $link, $ul )->parse();
                        }
-                       $wgOut->addHTML( Html::rawElement(
+                       $out->addHTML( Html::rawElement(
                                        'li',
                                        array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
                                        $liContents
                        );
  
                };
-               $wgOut->addHTML( Html::closeElement( 'ul' ) . "\n" );
+               $out->addHTML( Html::closeElement( 'ul' ) . "\n" );
                $res->free();
  
                // Add a links to [[Special:Whatlinkshere]]
                if ( $count > $limit ) {
-                       $wgOut->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
+                       $out->addWikiMsg( 'morelinkstoimage', $this->getTitle()->getPrefixedDBkey() );
                }
-               $wgOut->addHTML( Html::closeElement( 'div' ) . "\n" );
+               $out->addHTML( Html::closeElement( 'div' ) . "\n" );
        }
  
        protected function imageDupes() {
-               global $wgOut, $wgLang;
                $this->loadFile();
+               $out = $this->getContext()->getOutput();
  
                $dupes = $this->mPage->getDuplicates();
                if ( count( $dupes ) == 0 ) {
                        return;
                }
  
-               $wgOut->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
-               $wgOut->addWikiMsg( 'duplicatesoffile',
-                       $wgLang->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
+               $out->addHTML( "<div id='mw-imagepage-section-duplicates'>\n" );
+               $out->addWikiMsg( 'duplicatesoffile',
+                       $this->getContext()->getLanguage()->formatNum( count( $dupes ) ), $this->getTitle()->getDBkey()
                );
-               $wgOut->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
+               $out->addHTML( "<ul class='mw-imagepage-duplicates'>\n" );
  
                /**
                 * @var $file File
                                        $file->getTitle()->getPrefixedText() );
                                $fromSrc = wfMsg( 'shared-repo-from', $file->getRepo()->getDisplayName() );
                        }
-                       $wgOut->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
+                       $out->addHTML( "<li>{$link} {$fromSrc}</li>\n" );
                }
-               $wgOut->addHTML( "</ul></div>\n" );
+               $out->addHTML( "</ul></div>\n" );
        }
  
        /**
         * @param $description String
         */
        function showError( $description ) {
-               global $wgOut;
-               $wgOut->setPageTitle( wfMessage( 'internalerror' ) );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-               $wgOut->setArticleRelated( false );
-               $wgOut->enableClientCache( false );
-               $wgOut->addWikiText( $description );
+               $out = $this->getContext()->getOutput();
+               $out->setPageTitle( wfMessage( 'internalerror' ) );
+               $out->setRobotPolicy( 'noindex,nofollow' );
+               $out->setArticleRelated( false );
+               $out->enableClientCache( false );
+               $out->addWikiText( $description );
        }
  
        /**
   *
   * @ingroup Media
   */
- class ImageHistoryList {
+ class ImageHistoryList extends ContextSource {
  
        /**
         * @var Title
                $this->title = $imagePage->getTitle();
                $this->imagePage = $imagePage;
                $this->showThumb = $wgShowArchiveThumbnails && $this->img->canRender();
+               $this->setContext( $imagePage->getContext() );
        }
  
        /**
         * @return string
         */
        public function beginImageHistoryList( $navLinks = '' ) {
-               global $wgOut, $wgUser;
                return Xml::element( 'h2', array( 'id' => 'filehistory' ), wfMsg( 'filehist' ) ) . "\n"
                        . "<div id=\"mw-imagepage-section-filehistory\">\n"
-                       . $wgOut->parse( wfMsgNoTrans( 'filehist-help' ) )
+                       . $this->getOutput()->parse( wfMsgNoTrans( 'filehist-help' ) )
                        . $navLinks . "\n"
                        . Xml::openElement( 'table', array( 'class' => 'wikitable filehistory' ) ) . "\n"
                        . '<tr><td></td>'
-                       . ( $this->current->isLocal() && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' )
+                       . ( $this->current->isLocal() && ( $this->getUser()->isAllowedAny( 'delete', 'deletedhistory' ) ) ? '<td></td>' : '' )
                        . '<th>' . wfMsgHtml( 'filehist-datetime' ) . '</th>'
                        . ( $this->showThumb ? '<th>' . wfMsgHtml( 'filehist-thumb' ) . '</th>' : '' )
                        . '<th>' . wfMsgHtml( 'filehist-dimensions' ) . '</th>'
         * @return string
         */
        public function imageHistoryLine( $iscur, $file ) {
-               global $wgUser, $wgLang, $wgContLang;
+               global $wgContLang;
  
+               $user = $this->getUser();
+               $lang = $this->getLanguage();
                $timestamp = wfTimestamp( TS_MW, $file->getTimestamp() );
                $img = $iscur ? $file->getName() : $file->getArchiveName();
-               $user = $file->getUser( 'id' );
-               $usertext = $file->getUser( 'text' );
+               $userId = $file->getUser( 'id' );
+               $userText = $file->getUser( 'text' );
                $description = $file->getDescription();
  
                $local = $this->current->isLocal();
                $row = $selected = '';
  
                // Deletion link
-               if ( $local && ( $wgUser->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
+               if ( $local && ( $user->isAllowedAny( 'delete', 'deletedhistory' ) ) ) {
                        $row .= '<td>';
                        # Link to remove from history
-                       if ( $wgUser->isAllowed( 'delete' ) ) {
+                       if ( $user->isAllowed( 'delete' ) ) {
                                $q = array( 'action' => 'delete' );
                                if ( !$iscur ) {
                                        $q['oldimage'] = $img;
                                );
                        }
                        # Link to hide content. Don't show useless link to people who cannot hide revisions.
-                       $canHide = $wgUser->isAllowed( 'deleterevision' );
-                       if ( $canHide || ( $wgUser->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) {
-                               if ( $wgUser->isAllowed( 'delete' ) ) {
+                       $canHide = $user->isAllowed( 'deleterevision' );
+                       if ( $canHide || ( $user->isAllowed( 'deletedhistory' ) && $file->getVisibility() ) ) {
+                               if ( $user->isAllowed( 'delete' ) ) {
                                        $row .= '<br />';
                                }
                                // If file is top revision or locked from this user, don't link
                                if ( $iscur || !$file->userCan( File::DELETED_RESTRICTED ) ) {
                                        $del = Linker::revDeleteLinkDisabled( $canHide );
                                } else {
-                                       list( $ts, $name ) = explode( '!', $img, 2 );
+                                       list( $ts, ) = explode( '!', $img, 2 );
                                        $query = array(
                                                'type'   => 'oldimage',
                                                'target' => $this->title->getPrefixedText(),
                $row .= '<td>';
                if ( $iscur ) {
                        $row .= wfMsgHtml( 'filehist-current' );
-               } elseif ( $local && $wgUser->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
+               } elseif ( $local && $user->isLoggedIn() && $this->title->userCan( 'edit' ) ) {
                        if ( $file->isDeleted( File::DELETED_FILE ) ) {
                                $row .= wfMsgHtml( 'filehist-revert' );
                        } else {
                                        array(
                                                'action' => 'revert',
                                                'oldimage' => $img,
-                                               'wpEditToken' => $wgUser->getEditToken( $img )
+                                               'wpEditToken' => $user->getEditToken( $img )
                                        ),
                                        array( 'known', 'noclasses' )
                                );
                $row .= "<td $selected style='white-space: nowrap;'>";
                if ( !$file->userCan( File::DELETED_FILE ) ) {
                        # Don't link to unviewable files
-                       $row .= '<span class="history-deleted">' . $wgLang->timeanddate( $timestamp, true ) . '</span>';
+                       $row .= '<span class="history-deleted">' . $lang->timeanddate( $timestamp, true ) . '</span>';
                } elseif ( $file->isDeleted( File::DELETED_FILE ) ) {
                        if ( $local ) {
                                $this->preventClickjacking();
                                # Make a link to review the image
                                $url = Linker::link(
                                        $revdel,
-                                       $wgLang->timeanddate( $timestamp, true ),
+                                       $lang->timeanddate( $timestamp, true ),
                                        array(),
                                        array(
                                                'target' => $this->title->getPrefixedText(),
                                                'file' => $img,
-                                               'token' => $wgUser->getEditToken( $img )
+                                               'token' => $user->getEditToken( $img )
                                        ),
                                        array( 'known', 'noclasses' )
                                );
                        } else {
-                               $url = $wgLang->timeanddate( $timestamp, true );
+                               $url = $lang->timeanddate( $timestamp, true );
                        }
                        $row .= '<span class="history-deleted">' . $url . '</span>';
                } else {
                        $url = $iscur ? $this->current->getUrl() : $this->current->getArchiveUrl( $img );
-                       $row .= Xml::element( 'a', array( 'href' => $url ), $wgLang->timeanddate( $timestamp, true ) );
+                       $row .= Xml::element( 'a', array( 'href' => $url ), $lang->timeanddate( $timestamp, true ) );
                }
                $row .= "</td>";
  
                        $row .= '<span class="history-deleted">' . wfMsgHtml( 'rev-deleted-user' ) . '</span>';
                } else {
                        if ( $local ) {
-                               $row .= Linker::userLink( $user, $usertext ) . ' <span style="white-space: nowrap;">' .
-                               Linker::userToolLinks( $user, $usertext ) . '</span>';
+                               $row .= Linker::userLink( $userId, $userText ) . ' <span style="white-space: nowrap;">' .
+                               Linker::userToolLinks( $userId, $userText ) . '</span>';
                        } else {
-                               $row .= htmlspecialchars( $usertext );
+                               $row .= htmlspecialchars( $userText );
                        }
                }
                $row .= '</td>';
         * @return string
         */
        protected function getThumbForLine( $file ) {
-               global $wgLang;
+               $lang = $this->getLanguage();
                if ( $file->allowInlineDisplay() && $file->userCan( File::DELETED_FILE ) && !$file->isDeleted( File::DELETED_FILE ) ) {
                        $params = array(
                                'width' => '120',
                        $thumbnail = $file->transform( $params );
                        $options = array(
                                'alt' => wfMsg( 'filehist-thumbtext',
-                                       $wgLang->timeanddate( $timestamp, true ),
-                                       $wgLang->date( $timestamp, true ),
-                                       $wgLang->time( $timestamp, true ) ),
+                                       $lang->timeanddate( $timestamp, true ),
+                                       $lang->date( $timestamp, true ),
+                                       $lang->time( $timestamp, true ) ),
                                'file-link' => true,
                        );
  
@@@ -1190,6 -1211,7 +1213,7 @@@ class ImageHistoryPseudoPager extends R
        }
  
        /**
+        * @param $row object
         * @return string
         */
        function formatRow( $row ) {
diff --combined includes/LinksUpdate.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /**
-  * See docs/deferred.txt
+  * Updater for link tracking tables after a page edit.
   *
   * 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
   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
   * http://www.gnu.org/copyleft/gpl.html
   *
+  * @file
+  */
+ /**
+  * See docs/deferred.txt
+  *
   * @todo document (e.g. one-sentence top-level class description).
   */
 -class LinksUpdate {
 +class LinksUpdate extends SecondaryDBDataUpdate {
  
        /**@{{
         * @private
         */
        var $mId,            //!< Page ID of the article linked from
                $mTitle,         //!< Title object of the article linked from
 -              $mParserOutput,  //!< Parser output
 +              $mParserOutput,  //!< Whether to queue jobs for recursive update
                $mLinks,         //!< Map of title strings to IDs for the links in the document
                $mImages,        //!< DB keys of the images used, in the array key only
                $mTemplates,     //!< Map of title strings to IDs for the template references, including broken ones
@@@ -34,6 -40,8 +40,6 @@@
                $mCategories,    //!< Map of category names to sort keys
                $mInterlangs,    //!< Map of language codes to titles
                $mProperties,    //!< Map of arbitrary name to value
 -              $mDb,            //!< Database connection reference
 -              $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
        /**@}}*/
  
         * @param $recursive Boolean: queue jobs for recursive updates?
         */
        function __construct( $title, $parserOutput, $recursive = true ) {
 -              global $wgAntiLockFlags;
 +              parent::__construct( );
  
 -              if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
 -                      $this->mOptions = array();
 -              } else {
 -                      $this->mOptions = array( 'FOR UPDATE' );
 +              if ( !is_object( $title ) ) {
 +                      throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
 +                              "Please see Article::editUpdates() for an invocation example.\n" );
                }
 -              $this->mDb = wfGetDB( DB_MASTER );
  
 -              if ( !is_object( $title ) ) {
 +              if ( !is_object( $parserOutput ) ) {
                        throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
                                "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
                }
 +
                $this->mTitle = $title;
                $this->mId = $title->getArticleID();
  
                $this->mParserOutput = $parserOutput;
 +
                $this->mLinks = $parserOutput->getLinks();
                $this->mImages = $parserOutput->getImages();
                $this->mTemplates = $parserOutput->getTemplates();
                wfProfileOut( __METHOD__ );
        }
  
 -      /**
 -       * Invalidate the cache of a list of pages from a single namespace
 -       *
 -       * @param $namespace Integer
 -       * @param $dbkeys Array
 -       */
 -      function invalidatePages( $namespace, $dbkeys ) {
 -              if ( !count( $dbkeys ) ) {
 -                      return;
 -              }
 -
 -              /**
 -               * Determine which pages need to be updated
 -               * This is necessary to prevent the job queue from smashing the DB with
 -               * large numbers of concurrent invalidations of the same page
 -               */
 -              $now = $this->mDb->timestamp();
 -              $ids = array();
 -              $res = $this->mDb->select( 'page', array( 'page_id' ),
 -                      array(
 -                              'page_namespace' => $namespace,
 -                              'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
 -                              'page_touched < ' . $this->mDb->addQuotes( $now )
 -                      ), __METHOD__
 -              );
 -              foreach ( $res as $row ) {
 -                      $ids[] = $row->page_id;
 -              }
 -              if ( !count( $ids ) ) {
 -                      return;
 -              }
 -
 -              /**
 -               * Do the update
 -               * We still need the page_touched condition, in case the row has changed since
 -               * the non-locking select above.
 -               */
 -              $this->mDb->update( 'page', array( 'page_touched' => $now ),
 -                      array(
 -                              'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
 -                              'page_touched < ' . $this->mDb->addQuotes( $now )
 -                      ), __METHOD__
 -              );
 -      }
 -
        /**
         * @param $cats
         */
                }
        }
  }
- }
 +
 +/**
 + * Update object handling the cleanup of links tables after a page was deleted.
 + **/
 +class LinksDeletionUpdate extends SecondaryDBDataUpdate {
 +
 +      /**@{{
 +       * @private
 +       */
 +      var $mWikiPage;     //!< WikiPage the wikipage that was deleted
 +      /**@}}*/
 +
 +      /**
 +       * Constructor
 +       *
 +       * @param $title Title of the page we're updating
 +       * @param $parserOutput ParserOutput: output from a full parse of this page
 +       * @param $recursive Boolean: queue jobs for recursive updates?
 +       */
 +      function __construct( WikiPage $page ) {
 +              parent::__construct( );
 +
 +              $this->mPage = $page;
 +      }
 +
 +      /**
 +       * Do some database updates after deletion
 +       */
 +      public function doUpdate() {
 +              $title = $this->mPage->getTitle();
 +              $id = $this->mPage->getId();
 +
 +              # Delete restrictions for it
 +              $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
 +
 +              # Fix category table counts
 +              $cats = array();
 +              $res = $this->mDb->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
 +
 +              foreach ( $res as $row ) {
 +                      $cats [] = $row->cl_to;
 +              }
 +
 +              $this->mPage->updateCategoryCounts( array(), $cats );
 +
 +              # If using cascading deletes, we can skip some explicit deletes
 +              if ( !$this->mDb->cascadingDeletes() ) {
 +                      $this->mDb->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
 +
 +                      # Delete outgoing links
 +                      $this->mDb->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
 +                      $this->mDb->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
 +              }
 +
 +              # If using cleanup triggers, we can skip some manual deletes
 +              if ( !$this->mDb->cleanupTriggers() ) {
 +                      # Clean up recentchanges entries...
 +                      $this->mDb->delete( 'recentchanges',
 +                                    array( 'rc_type != ' . RC_LOG,
 +                                         'rc_namespace' => $title->getNamespace(),
 +                                         'rc_title' => $title->getDBkey() ),
 +                                    __METHOD__ );
 +                      $this->mDb->delete( 'recentchanges',
 +                                    array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
 +                                    __METHOD__ );
 +              }
 +      }
++}
diff --combined includes/Namespace.php
@@@ -1,6 -1,22 +1,22 @@@
  <?php
  /**
-  * Provide things related to namespaces
+  * Provide things related to namespaces.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   */
  
@@@ -14,7 -30,6 +30,6 @@@
   * Users and translators should not change them
   *
   */
  class MWNamespace {
  
        /**
         * Returns array of all defined namespaces with their canonical
         * (English) names.
         *
 +       * @param bool $rebuild rebuild namespace list (default = false). Used for testing.
 +       *
         * @return array
         * @since 1.17
         */
 -      public static function getCanonicalNamespaces() {
 +      public static function getCanonicalNamespaces( $rebuild = false ) {
                static $namespaces = null;
 -              if ( $namespaces === null ) {
 +              if ( $namespaces === null || $rebuild ) {
                        global $wgExtraNamespaces, $wgCanonicalNamespaceNames;
                        $namespaces = array( NS_MAIN => '' ) + $wgCanonicalNamespaceNames;
                        if ( is_array( $wgExtraNamespaces ) ) {
                return $index == NS_USER || $index == NS_USER_TALK;
        }
  
+       /**
+        * It is not possible to use pages from this namespace as template?
+        *
+        * @since 1.20
+        * @param $index int Index to check
+        * @return bool
+        */
+       public static function isNonincludable( $index ) {
+               global $wgNonincludableNamespaces;
+               return $wgNonincludableNamespaces && in_array( $index, $wgNonincludableNamespaces );
+       }
  }
diff --combined includes/Revision.php
@@@ -1,4 -1,24 +1,24 @@@
  <?php
+ /**
+  * Representation of a page version.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
+  * @file
+  */
  
  /**
   * @todo document
@@@ -20,10 -40,6 +40,10 @@@ class Revision 
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
 +      protected $mContentModel;
 +      protected $mContentFormat;
 +      protected $mContent;
 +      protected $mContentHandler;
  
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
 +              global $wgContentHandlerUseDB;
 +
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
 +                      'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
 +                      'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      unset( $attribs['content_model'] );
 +                      unset( $attribs['content_format'] );
 +              }
 +
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
 -                      'rev_sha1'
 +                      'rev_sha1',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_format';
 +                      $fields[] = 'rev_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
                                $this->mTitle = null;
                        }
  
 +                      if( !isset( $row->rev_content_model ) || is_null( $row->rev_content_model ) ) {
 +                              $this->mContentModel = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentModel = intval( $row->rev_content_model );
 +                      }
 +
 +                      if( !isset( $row->rev_content_format ) || is_null( $row->rev_content_format ) ) {
 +                              $this->mContentFormat = null; # determine on demand if needed
 +                      } else {
 +                              $this->mContentFormat = intval( $row->rev_content_format );
 +                      }
 +
                        // Lazy extraction...
                        $this->mText      = null;
                        if( isset( $row->old_text ) ) {
                        // Build a new revision to be saved...
                        global $wgUser; // ugh
  
 +
 +                      # if we have a content object, use it to set the model and type
 +                      if ( !empty( $row['content'] ) ) {
 +                              if ( !empty( $row['text_id'] ) ) { #FIXME: when is that set? test with external store setup! check out insertOn()
 +                                      throw new MWException( "Text already stored in external store (id {$row['text_id']}), can't serialize content object" );
 +                              }
 +
 +                              $row['content_model'] = $row['content']->getModel();
 +                              # note: mContentFormat is initializes later accordingly
 +                              # note: content is serialized later in this method!
 +                              # also set text to null?
 +                      }
 +
                        $this->mId        = isset( $row['id']         ) ? intval( $row['id']         ) : null;
                        $this->mPage      = isset( $row['page']       ) ? intval( $row['page']       ) : null;
                        $this->mTextId    = isset( $row['text_id']    ) ? intval( $row['text_id']    ) : null;
                        $this->mParentId  = isset( $row['parent_id']  ) ? intval( $row['parent_id']  ) : null;
                        $this->mSha1      = isset( $row['sha1']  )      ? strval( $row['sha1']  )      : null;
  
 +                      $this->mContentModel = isset( $row['content_model']  )  ? intval( $row['content_model'] )  : null;
 +                      $this->mContentFormat    = isset( $row['content_format']  ) ? intval( $row['content_format'] ) : null;
 +
                        // Enforce spacing trimming on supplied text
                        $this->mComment   = isset( $row['comment']    ) ?  trim( strval( $row['comment'] ) ) : null;
                        $this->mText      = isset( $row['text']       ) ? rtrim( strval( $row['text']    ) ) : null;
                        $this->mTextRow   = null;
  
 +                      # if we have a content object, override mText and mContentModel
 +                      if ( !empty( $row['content'] ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $row['content'];
 +
 +                              $this->mContentModel = $this->mContent->getModel();
 +                              $this->mContentHandler = null;
 +
 +                              $this->mText = $handler->serializeContent( $row['content'], $this->getContentFormat() );
 +                      } elseif ( !is_null( $this->mText ) ) {
 +                              $handler = $this->getContentHandler();
 +                              $this->mContent = $handler->unserializeContent( $this->mText );
 +                      }
 +
                        $this->mTitle     = null; # Load on demand if needed
 -                      $this->mCurrent   = false;
 +                      $this->mCurrent   = false; #XXX: really? we are about to create a revision. it will usually then be the current one.
 +
                        # If we still have no length, see it we have the text to figure it out
                        if ( !$this->mSize ) {
 -                              $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
 +                              if ( !is_null( $this->mContent ) ) {
 +                                      $this->mSize = $this->mContent->getSize();
 +                              } else {
 +                                      #XXX: my be inconsistent with the notion of "size" use for the present content model
 +                                      #NOTE: should never happen if we have either text or content object!
 +                                      $this->mSize = is_null( $this->mText ) ? null : strlen( $this->mText );
 +                              }
                        }
 +
                        # Same for sha1
                        if ( $this->mSha1 === null ) {
                                $this->mSha1 = is_null( $this->mText ) ? null : self::base36Sha1( $this->mText );
                        }
 +
 +                      $this->getContentModel(); # force lazy init
 +                      $this->getContentFormat();    # force lazy init
                } else {
                        throw new MWException( 'Revision constructor passed invalid row format.' );
                }
                $this->mUnpatrolled = null;
 +
 +              // @TODO: add support for ar_content_format, ar_content_model, rev_content_format, rev_content_model to API
 +              // @TODO: get rid of $mText
        }
  
        /**
         * @param $user User object to check for, only if FOR_THIS_USER is passed
         *              to the $audience parameter
         * @return String
 +       * @deprecated in 1.WD, use getContent() instead
 +       */
 +      public function getText( $audience = self::FOR_PUBLIC, User $user = null ) { #FIXME: deprecated, replace usage! #FIXME: used a LOT!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = $this->getContent();
 +              return ContentHandler::getContentText( $content ); # returns the raw content text, if applicable
 +      }
 +
 +      /**
 +       * Fetch revision content if it's available to the specified audience.
 +       * If the specified audience does not have the ability to view this
 +       * revision, null will be returned.
 +       *
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
 +       * @param $user User object to check for, only if FOR_THIS_USER is passed
 +       *              to the $audience parameter
 +       * @return Content
 +       *
 +       * @since 1.WD
         */
 -      public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
 +      public function getContent( $audience = self::FOR_PUBLIC, User $user = null ) {
                if( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_TEXT ) ) {
 -                      return '';
 +                      return null;
                } elseif( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_TEXT, $user ) ) {
 -                      return '';
 +                      return null;
                } else {
 -                      return $this->getRawText();
 +                      return $this->getContentInternal();
                }
        }
  
         *
         * @return String
         */
 -      public function getRawText() {
 -              if( is_null( $this->mText ) ) {
 -                      // Revision text is immutable. Load on demand:
 -                      $this->mText = $this->loadText();
 +      public function getRawText() { #FIXME: deprecated, replace usage!
 +              return $this->getText( self::RAW );
 +      }
 +
 +      protected function getContentInternal() {
 +              if( is_null( $this->mContent ) ) {
 +                      // Revision is immutable. Load on demand:
 +
 +                      $handler = $this->getContentHandler();
 +                      $format = $this->getContentFormat();
 +                      $title = $this->getTitle();
 +
 +                      if( is_null( $this->mText ) ) {
 +                              // Load text on demand:
 +                              $this->mText = $this->loadText();
 +                      }
 +
 +                      $this->mContent = is_null( $this->mText ) ? null : $handler->unserializeContent( $this->mText, $format );
 +              }
 +
 +              return $this->mContent;
 +      }
 +
 +      /**
 +       * Returns the content model for this revision.
 +       *
 +       * If no content model was stored in the database, $this->getTitle()->getContentModel() is
 +       * used to determine the content model to use. If no title is know, CONTENT_MODEL_WIKITEXT
 +       * is used as a last resort.
 +       *
 +       * @return int the content model id associated with this revision, see the CONTENT_MODEL_XXX constants.
 +       **/
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $title = $this->getTitle();
 +                      $this->mContentModel = ( $title ? $title->getContentModel() : CONTENT_MODEL_WIKITEXT );
 +
 +                      assert( !empty( $this->mContentModel ) );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Returns the content format for this revision.
 +       *
 +       * If no content format was stored in the database, the default format for this
 +       * revision's content model is returned.
 +       *
 +       * @return int the content format id associated with this revision, see the CONTENT_FORMAT_XXX constants.
 +       **/
 +      public function getContentFormat() {
 +              if ( !$this->mContentFormat ) {
 +                      $handler = $this->getContentHandler();
 +                      $this->mContentFormat = $handler->getDefaultFormat();
 +
 +                      assert( !empty( $this->mContentFormat ) );
 +              }
 +
 +              return $this->mContentFormat;
 +      }
 +
 +      /**
 +       * Returns the content handler appropriate for this revision's content model.
 +       *
 +       * @return ContentHandler
 +       */
 +      public function getContentHandler() {
 +              if ( !$this->mContentHandler ) {
 +                      $model = $this->getContentModel();
 +                      $this->mContentHandler = ContentHandler::getForModelID( $model );
 +
 +                      assert( $this->mContentHandler->isSupportedFormat( $this->getContentFormat() ) );
                }
 -              return $this->mText;
 +
 +              return $this->mContentHandler;
        }
  
        /**
         * @return Integer
         */
        public function insertOn( $dbw ) {
 -              global $wgDefaultExternalStore;
 +              global $wgDefaultExternalStore, $wgContentHandlerUseDB;
  
                wfProfileIn( __METHOD__ );
  
                $rev_id = isset( $this->mId )
                        ? $this->mId
                        : $dbw->nextSequenceValue( 'revision_rev_id_seq' );
 -              $dbw->insert( 'revision',
 -                      array(
 -                              'rev_id'         => $rev_id,
 -                              'rev_page'       => $this->mPage,
 -                              'rev_text_id'    => $this->mTextId,
 -                              'rev_comment'    => $this->mComment,
 -                              'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 -                              'rev_user'       => $this->mUser,
 -                              'rev_user_text'  => $this->mUserText,
 -                              'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 -                              'rev_deleted'    => $this->mDeleted,
 -                              'rev_len'        => $this->mSize,
 -                              'rev_parent_id'  => is_null( $this->mParentId )
 -                                      ? $this->getPreviousRevisionId( $dbw )
 -                                      : $this->mParentId,
 -                              'rev_sha1'       => is_null( $this->mSha1 )
 -                                      ? Revision::base36Sha1( $this->mText )
 -                                      : $this->mSha1
 -                      ), __METHOD__
 +
 +              $row = array(
 +                      'rev_id'         => $rev_id,
 +                      'rev_page'       => $this->mPage,
 +                      'rev_text_id'    => $this->mTextId,
 +                      'rev_comment'    => $this->mComment,
 +                      'rev_minor_edit' => $this->mMinorEdit ? 1 : 0,
 +                      'rev_user'       => $this->mUser,
 +                      'rev_user_text'  => $this->mUserText,
 +                      'rev_timestamp'  => $dbw->timestamp( $this->mTimestamp ),
 +                      'rev_deleted'    => $this->mDeleted,
 +                      'rev_len'        => $this->mSize,
 +                      'rev_parent_id'  => is_null( $this->mParentId )
 +                              ? $this->getPreviousRevisionId( $dbw )
 +                              : $this->mParentId,
 +                      'rev_sha1'       => is_null( $this->mSha1 )
 +                              ? Revision::base36Sha1( $this->mText )
 +                              : $this->mSha1,
                );
  
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'rev_content_model' ] = $this->getContentModel();
 +                      $row[ 'rev_content_format' ] = $this->getContentFormat();
 +              }
 +
 +              $dbw->insert( 'revision', $row, __METHOD__ );
 +
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
  
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 +              $fields = array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_model';
 +                      $fields[] = 'rev_content_format';
 +              }
 +
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
 -                      array( 'page_latest', 'rev_text_id', 'rev_len', 'rev_sha1' ),
 +                      $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
                        __METHOD__ );
  
                if( $current ) {
 -                      $revision = new Revision( array(
 +                      $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
                                'text_id'    => $current->rev_text_id,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
 -                              'sha1'       => $current->rev_sha1
 -                              ) );
 +                              'sha1'       => $current->rev_sha1,
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
                } else {
                        $revision = null;
                }
diff --combined includes/Title.php
@@@ -1,5 -1,7 +1,7 @@@
  <?php
  /**
+  * Representation a title within %MediaWiki.
+  *
   * See title.txt
   *
   * This program is free software; you can redistribute it and/or modify
@@@ -63,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;
@@@ -84,6 -85,7 +86,7 @@@
        var $mRedirect = null;            // /< Is the article at this title a redirect?
        var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
        var $mBacklinkCache = null;       // /< Cache of links to this title
+       var $mHasSubpage;                 // /< Whether a page has any subpages
        // @}
  
  
         *   fied by a prefix.  If you want to force a specific namespace even if
         *   $text might begin with a namespace prefix, use makeTitle() or
         *   makeTitleSafe().
-        * @return Title, or null on an error.
+        * @throws MWException
+        * @return Title|null - Title or null on an error.
         */
        public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
                if ( is_object( $text ) ) {
         * @return Title the new object, or NULL on an error
         */
        public static function newFromURL( $url ) {
-               global $wgLegalTitleChars;
                $t = new Title();
  
                # For compatibility with old buggy URLs. "+" is usually not valid in titles,
                # but some URLs used it as a space replacement and they still come
                # from some external search tools.
-               if ( strpos( $wgLegalTitleChars, '+' ) === false ) {
+               if ( strpos( self::legalChars(), '+' ) === false ) {
                        $url = str_replace( '+', ' ', $url );
                }
  
                        if ( isset( $row->page_is_redirect ) )
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
 -                              $this->mLatestID = (int)$row->page_latest;
 +                              $this->mLatestID = (int)$row->page_latest; # FIXME: whene3ver page_latest is updated, also update page_content_model
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = $row->page_content_model;
 +                      else
 +                              $this->mContentModel = null; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = null; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = null; # initialized lazily in getContentModel()
                return $t;
        }
  
                return $this->mNamespace;
        }
  
 +      /**
 +       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 +       *
 +       * @return Integer: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( empty( $this->mContentModel ) ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              assert( !empty( $this->mContentModel ) );
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Conveniance method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         * This is MUCH simpler than individually testing for equivilance
         * against both NS_USER and NS_USER_TALK, and is also forward compatible.
         * @since 1.19
+        * @param $ns int
         * @return bool
         */
        public function hasSubjectNamespace( $ns ) {
         * @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 ) );
        }
  
        /**
         * andthe wfArrayToCGI moved to getLocalURL();
         *
         * @since 1.19 (r105919)
+        * @param $query
+        * @param $query2 bool
         * @return String
         */
        private static function fixUrlQueryArgs( $query, $query2 = false ) {
         * See getLocalURL for the arguments.
         *
         * @see self::getLocalURL
+        * @param $query string
+        * @param $query2 bool|string
         * @return String the URL
         */
        public function escapeLocalURL( $query = '', $query2 = false ) {
        /**
         * Get the expiry time for the restriction against a given action
         *
+        * @param $action
         * @return String|Bool 14-char timestamp, or 'infinity' if the page is protected forever
-        *      or not protected at all, or false if the action is not recognised.
+        *     or not protected at all, or false if the action is not recognised.
         */
        public function getRestrictionExpiry( $action ) {
                if ( !$this->mRestrictionsLoaded ) {
         */
        public function getPreviousRevisionID( $revId, $flags = 0 ) {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
-               return $db->selectField( 'revision', 'rev_id',
+               $revId = $db->selectField( 'revision', 'rev_id',
                        array(
                                'rev_page' => $this->getArticleID( $flags ),
                                'rev_id < ' . intval( $revId )
                        __METHOD__,
                        array( 'ORDER BY' => 'rev_id DESC' )
                );
+               if ( $revId === false ) {
+                       return false;
+               } else {
+                       return intval( $revId );
+               }
        }
  
        /**
         */
        public function getNextRevisionID( $revId, $flags = 0 ) {
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
-               return $db->selectField( 'revision', 'rev_id',
+               $revId = $db->selectField( 'revision', 'rev_id',
                        array(
                                'rev_page' => $this->getArticleID( $flags ),
                                'rev_id > ' . intval( $revId )
                        __METHOD__,
                        array( 'ORDER BY' => 'rev_id' )
                );
+               if ( $revId === false ) {
+                       return false;
+               } else {
+                       return intval( $revId );
+               }
        }
  
        /**
                $dbr = wfGetDB( DB_SLAVE );
                $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
                        'wl_notificationtimestamp',
-                       array( 'wl_namespace' => $this->getNamespace(),
+                       array(
+                               'wl_user' => $user->getId(),
+                               'wl_namespace' => $this->getNamespace(),
                                'wl_title' => $this->getDBkey(),
-                               'wl_user' => $user->getId()
                        ),
                        __METHOD__
                );
         * $wgLang (such as special pages, which are in the user language).
         *
         * @since 1.18
-        * @return object Language
+        * @return Language
         */
        public function getPageLanguage() {
                global $wgLang;
diff --combined includes/WikiPage.php
@@@ -1,4 -1,25 +1,25 @@@
  <?php
+ /**
+  * Base representation for a MediaWiki page.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
+  * @file
+  */
  /**
   * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
   */
@@@ -37,6 -58,28 +58,28 @@@ class WikiPage extends Page 
         */
        const DELETE_NO_REVISIONS = 2;
  
+       // Constants for $mDataLoadedFrom and related
+       /**
+        * Data has not been loaded yet (or the object was cleared)
+        */
+       const DATA_NOT_LOADED = 0;
+       /**
+        * Data has been loaded from a slave database
+        */
+       const DATA_FROM_SLAVE = 1;
+       /**
+        * Data has been loaded from the master database
+        */
+       const DATA_FROM_MASTER = 2;
+       /**
+        * Data has been loaded from the master database using FOR UPDATE
+        */
+       const DATA_FOR_UPDATE = 3;
        /**
         * @var Title
         */
        public $mPreparedEdit = false;       // !< Array
        /**@}}*/
  
+       /**
+        * @var int; one of the DATA_* constants
+        */
+       protected $mDataLoadedFrom = self::DATA_NOT_LOADED;
        /**
         * @var Title
         */
         * Constructor from a page id
         *
         * @param $id Int article ID to load
+        * @param $from string|int one of the following values:
+        *        - "fromdb" or self::DATA_FROM_SLAVE to select from a slave database
+        *        - "fromdbmaster" or self::DATA_FROM_MASTER to select from the master database
         *
         * @return WikiPage|null
         */
-       public static function newFromID( $id ) {
-               $dbr = wfGetDB( DB_SLAVE );
-               $row = $dbr->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+       public static function newFromID( $id, $from = 'fromdb' ) {
+               $from = self::convertSelectType( $from );
+               $db = wfGetDB( $from === self::DATA_FROM_MASTER ? DB_MASTER : DB_SLAVE );
+               $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
                if ( !$row ) {
                        return null;
                }
-               return self::newFromRow( $row );
+               return self::newFromRow( $row, $from );
        }
  
        /**
         * @since 1.20
         * @param $row object: database row containing at least fields returned
         *        by selectFields().
+        * @param $from string|int: source of $data:
+        *        - "fromdb" or self::DATA_FROM_SLAVE: from a slave DB
+        *        - "fromdbmaster" or self::DATA_FROM_MASTER: from the master DB
+        *        - "forupdate" or self::DATA_FOR_UPDATE: from the master DB using SELECT FOR UPDATE
         * @return WikiPage
         */
-       public static function newFromRow( $row ) {
+       public static function newFromRow( $row, $from = 'fromdb' ) {
                $page = self::factory( Title::newFromRow( $row ) );
-               $page->loadFromRow( $row );
+               $page->loadFromRow( $row, $from );
                return $page;
        }
  
+       /**
+        * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to DATA_* constants.
+        *
+        * @param $type object|string|int
+        * @return mixed
+        */
+       private static function convertSelectType( $type ) {
+               switch ( $type ) {
+               case 'fromdb':
+                       return self::DATA_FROM_SLAVE;
+               case 'fromdbmaster':
+                       return self::DATA_FROM_MASTER;
+               case 'forupdate':
+                       return self::DATA_FOR_UPDATE;
+               default:
+                       // It may already be an integer or whatever else
+                       return $type;
+               }
+       }
        /**
         * Returns overrides for action handlers.
         * Classes listed here will be used instead of the default one when
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentHandler() {
 +              return ContentHandler::getForModelID( $this->getContentModel() );
        }
  
        /**
         */
        public function clear() {
                $this->mDataLoaded = false;
+               $this->mDataLoadedFrom = self::DATA_NOT_LOADED;
  
                $this->mCounter = null;
                $this->mRedirectTarget = null; # Title object if set
         * @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;
        }
  
        /**
         * Fetch a page record with the given conditions
         * @param $dbr DatabaseBase object
         * @param $conditions Array
+        * @param $options Array
         * @return mixed Database result resource, or false on failure
         */
-       protected function pageData( $dbr, $conditions ) {
+       protected function pageData( $dbr, $conditions, $options = array() ) {
                $fields = self::selectFields();
  
                wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
  
-               $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__ );
+               $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
  
                wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
  
         *
         * @param $dbr DatabaseBase object
         * @param $title Title object
+        * @param $options Array
         * @return mixed Database result resource, or false on failure
         */
-       public function pageDataFromTitle( $dbr, $title ) {
+       public function pageDataFromTitle( $dbr, $title, $options = array() ) {
                return $this->pageData( $dbr, array(
                        'page_namespace' => $title->getNamespace(),
-                       'page_title'     => $title->getDBkey() ) );
+                       'page_title'     => $title->getDBkey() ), $options );
        }
  
        /**
         *
         * @param $dbr DatabaseBase
         * @param $id Integer
+        * @param $options Array
         * @return mixed Database result resource, or false on failure
         */
-       public function pageDataFromId( $dbr, $id ) {
-               return $this->pageData( $dbr, array( 'page_id' => $id ) );
+       public function pageDataFromId( $dbr, $id, $options = array() ) {
+               return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
        }
  
        /**
         * Set the general counter, title etc data loaded from
         * some source.
         *
-        * @param $data Object|String One of the following:
-        *              A DB query result object or...
-        *              "fromdb" to get from a slave DB or...
-        *              "fromdbmaster" to get from the master DB
+        * @param $from object|string|int One of the following:
+        *        - A DB query result object
+        *        - "fromdb" or self::DATA_FROM_SLAVE to get from a slave DB
+        *        - "fromdbmaster" or self::DATA_FROM_MASTER to get from the master DB
+        *        - "forupdate"  or self::DATA_FOR_UPDATE to get from the master DB using SELECT FOR UPDATE
+        *
         * @return void
         */
-       public function loadPageData( $data = 'fromdb' ) {
-               if ( $data === 'fromdbmaster' ) {
+       public function loadPageData( $from = 'fromdb' ) {
+               $from = self::convertSelectType( $from );
+               if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
+                       // We already have the data from the correct location, no need to load it twice.
+                       return;
+               }
+               if ( $from === self::DATA_FOR_UPDATE ) {
+                       $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) );
+               } elseif ( $from === self::DATA_FROM_MASTER ) {
                        $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
-               } elseif ( $data === 'fromdb' ) { // slave
+               } elseif ( $from === self::DATA_FROM_SLAVE ) {
                        $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
                        # Use a "last rev inserted" timestamp key to dimish the issue of slave lag.
                        # Note that DB also stores the master position in the session and checks it.
                        $touched = $this->getCachedLastEditTime();
                        if ( $touched ) { // key set
                                if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
+                                       $from = self::DATA_FROM_MASTER;
                                        $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
                                }
                        }
+               } else {
+                       // No idea from where the caller got this data, assume slave database.
+                       $data = $from;
+                       $from = self::DATA_FROM_SLAVE;
                }
  
-               $this->loadFromRow( $data );
+               $this->loadFromRow( $data, $from );
        }
  
        /**
         * @since 1.20
         * @param $data object: database row containing at least fields returned
         *        by selectFields()
+        * @param $from string|int One of the following:
+        *        - "fromdb" or self::DATA_FROM_SLAVE if the data comes from a slave DB
+        *        - "fromdbmaster" or self::DATA_FROM_MASTER if the data comes from the master DB
+        *        - "forupdate"  or self::DATA_FOR_UPDATE if the data comes from from
+        *          the master DB using SELECT FOR UPDATE
         */
-       public function loadFromRow( $data ) {
+       public function loadFromRow( $data, $from ) {
                $lc = LinkCache::singleton();
  
                if ( $data ) {
                }
  
                $this->mDataLoaded = true;
+               $this->mDataLoadedFrom = self::convertSelectType( $from );
        }
  
        /**
         * @return bool
         */
        public function isRedirect( $text = false ) {
 -              if ( $text === false ) {
 -                      if ( !$this->mDataLoaded ) {
 -                              $this->loadPageData();
 -                      }
 +              if ( $text === false ) $content = $this->getContent();
 +              else $content = ContentHandler::makeContent( $text, $this->mTitle ); # TODO: allow model and format to be provided; or better, expect a Content object
  
 -                      return (bool)$this->mIsRedirect;
 -              } else {
 -                      return Title::newFromRedirect( $text ) !== null;
 +
 +              if ( empty( $content ) ) return false;
 +              else return $content->isRedirect();
 +      }
 +
 +      /**
 +       * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
 +       *
 +       * Will use the revisions actual content model if the page exists,
 +       * and the page's default if the page doesn't exist yet.
 +       *
 +       * @return int
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentModel() {
 +              if ( $this->exists() ) {
 +                      # look at the revision's actual content model
 +                      $rev = $this->getRevision();
 +
 +                      if ( $rev !== null ) {
 +                              return $rev->getContentModel();
 +                      } else {
 +                              wfWarn( "Page exists but has no revision!" );
 +                      }
                }
 +
 +              # use the default model for this page
 +              return $this->mTitle->getContentModel();
        }
  
        /**
                return (int)$this->mLatest;
        }
  
+       /**
+        * Get the Revision object of the oldest revision
+        * @return Revision|null
+        */
+       public function getOldestRevision() {
+               wfProfileIn( __METHOD__ );
+               // Try using the slave database first, then try the master
+               $continue = 2;
+               $db = wfGetDB( DB_SLAVE );
+               $revSelectFields = Revision::selectFields();
+               while ( $continue ) {
+                       $row = $db->selectRow(
+                               array( 'page', 'revision' ),
+                               $revSelectFields,
+                               array(
+                                       'page_namespace' => $this->mTitle->getNamespace(),
+                                       'page_title' => $this->mTitle->getDBkey(),
+                                       'rev_page = page_id'
+                               ),
+                               __METHOD__,
+                               array(
+                                       'ORDER BY' => 'rev_timestamp ASC'
+                               )
+                       );
+                       if ( $row ) {
+                               $continue = 0;
+                       } else {
+                               $db = wfGetDB( DB_MASTER );
+                               $continue--;
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               if ( $row ) {
+                       return Revision::newFromRow( $row );
+               } else {
+                       return null;
+               }
+       }
        /**
         * Loads everything except the text
         * This isn't necessary for all uses, so it's only done if needed.
        }
  
        /**
 -       * Get the text of the current revision. No side-effects...
 +       * Get the content of the current revision. No side-effects...
         *
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
         *      Revision::FOR_THIS_USER    to be displayed to $wgUser
         *      Revision::RAW              get the text regardless of permissions
 -       * @return String|bool The text of the current revision. False on failure
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.WD
         */
 -      public function getText( $audience = Revision::FOR_PUBLIC ) {
 +      public function getContent( $audience = Revision::FOR_PUBLIC ) {
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getText( $audience );
 +                      return $this->mLastRevision->getContent( $audience );
                }
 -              return false;
 +              return null;
        }
  
        /**
         * Get the text of the current revision. No side-effects...
         *
 -       * @return String|bool The text of the current revision. False on failure
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
 +       * @return String|false The text of the current revision
 +       * @deprecated as of 1.WD, getContent() should be used instead.
         */
 -      public function getRawText() {
 +      public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getRawText();
 +                      return $this->mLastRevision->getText( $audience );
                }
                return false;
        }
  
 +      /**
 +       * Get the text of the current revision. No side-effects...
 +       *
 +       * @return String|bool The text of the current revision. False on failure
 +       * @deprecated as of 1.WD, getContent() should be used instead.
 +       */
 +      public function getRawText() { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              return $this->getText( Revision::RAW );
 +      }
 +
        /**
         * @return string MW timestamp of last article revision
         */
                if ( !$this->mTimestamp ) {
                        $this->loadLastEdit();
                }
 -              
 +
                return wfTimestamp( TS_MW, $this->mTimestamp );
        }
  
                }
        }
  
+       /**
+        * 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::RAW              get the text regardless of permissions
+        * @return User|null
+        */
+       public function getCreator( $audience = Revision::FOR_PUBLIC ) {
+               $revision = $this->getOldestRevision();
+               if ( $revision ) {
+                       $userName = $revision->getUserText( $audience );
+                       return User::newFromName( $userName, false );
+               } else {
+                       return null;
+               }
+       }
        /**
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
                        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)
 +       * @param $context IContextSource context for parsing
 +       *
         * @return ParserOutput or false if the revision was not found
         */
 -      public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
 +      public function getParserOutput( ParserOptions $parserOptions, $oldid = null, IContextSource $context = null ) {
                wfProfileIn( __METHOD__ );
  
                $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
                        $oldid = $this->getLatest();
                }
  
 -              $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
 +              $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache, null, $context );
                $pool->execute();
  
                wfProfileOut( __METHOD__ );
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
                        if ( $this->mTitle->exists() ) {
 -                              $text = $this->getRawText();
 +                              $text = ContentHandler::getContentText( $this->getContent() );
                        } else {
                                $text = false;
                        }
         * @private
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 -              $text = $revision->getText();
 -              $len = strlen( $text );
 -              $rt = Title::newFromRedirectRecurse( $text );
 +              $content = $revision->getContent();
 +              $len = $content->getSize();
 +              $rt = $content->getUltimateRedirectTarget();
  
                $conditions = array( 'page_id' => $this->getId() );
  
                }
  
                $now = wfTimestampNow();
 +              $row = array( /* SET */
 +                      'page_latest'      => $revision->getId(),
 +                      'page_touched'     => $dbw->timestamp( $now ),
 +                      'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 +                      'page_is_redirect' => $rt !== null ? 1 : 0,
 +                      'page_len'         => $len,
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'page_content_model' ] = $revision->getContentModel();
 +              }
 +
                $dbw->update( 'page',
 -                      array( /* SET */
 -                              'page_latest'      => $revision->getId(),
 -                              'page_touched'     => $dbw->timestamp( $now ),
 -                              'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 -                              'page_is_redirect' => $rt !== null ? 1 : 0,
 -                              'page_len'         => $len,
 -                      ),
 +                      $row,
                        $conditions,
                        __METHOD__ );
  
         * @param $undo Revision
         * @param $undoafter Revision Must be an earlier revision than $undo
         * @return mixed string on success, false on failure
 +       * @deprecated since 1.WD: use ContentHandler::getUndoContent() instead.
         */
        public function getUndoText( Revision $undo, Revision $undoafter = null ) {
 -              $cur_text = $this->getRawText();
 -              if ( $cur_text === false ) {
 -                      return false; // no page
 -              }
 -              $undo_text = $undo->getText();
 -              $undoafter_text = $undoafter->getText();
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -              if ( $cur_text == $undo_text ) {
 -                      # No use doing a merge if it's just a straight revert.
 -                      return $undoafter_text;
 -              }
 +              $this->loadLastEdit();
  
 -              $undone_text = '';
 +              if ( $this->mLastRevision ) {
 +                      if ( is_null( $undoafter ) ) {
 +                              $undoafter = $undo->getPrevious();
 +                      }
  
 -              if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
 -                      return false;
 +                      $handler = $this->getContentHandler();
 +                      $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
 +
 +                      if ( !$undone ) {
 +                              return false;
 +                      } else {
 +                              return ContentHandler::getContentText( $undone );
 +                      }
                }
  
 -              return $undone_text;
 +              return false;
        }
  
        /**
         * @param $text String: new text of the section
         * @param $sectionTitle String: new section's subject, only if $section is 'new'
         * @param $edittime String: revision timestamp or null to use the current revision
 -       * @return string Complete article text, or null if error
 +       * @return String new complete article text, or null if error
 +       *
 +       * @deprecated since 1.WD, use replaceSectionContent() instead
         */
        public function replaceSection( $section, $text, $sectionTitle = '', $edittime = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); #XXX: could make section title, but that's not required.
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent ); #XXX: unclear what will happen for non-wikitext!
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.WD
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
 -                              if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 -                                      $text = strlen( trim( $oldtext ) ) > 0
 -                                              ? "{$oldtext}\n\n{$subject}{$text}"
 -                                              : "{$subject}{$text}";
 -                              }
 -                      } else {
 -                              # Replacing an existing section; roll out the big guns
 -                              global $wgParser;
 -
 -                              $text = $wgParser->replaceSection( $oldtext, $section, $text );
 -                      }
 +                      $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
 +       *
 +       * @deprecated since 1.WD: use doEditContent() instead.
         */
 -      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 +      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
 +      }
 +
 +      /**
 +       * Change an existing article or create a new article. Updates RC and all necessary caches,
 +       * optionally via the deferred update array.
 +       *
 +       * @param $content Content: new content
 +       * @param $summary String: edit summary
 +       * @param $flags Integer bitfield:
 +       *      EDIT_NEW
 +       *          Article is known or assumed to be non-existent, create a new one
 +       *      EDIT_UPDATE
 +       *          Article is known or assumed to be pre-existing, update it
 +       *      EDIT_MINOR
 +       *          Mark this edit minor, if the user is allowed to do so
 +       *      EDIT_SUPPRESS_RC
 +       *          Do not log the change in recentchanges
 +       *      EDIT_FORCE_BOT
 +       *          Mark the edit a "bot" edit regardless of user rights
 +       *      EDIT_DEFER_UPDATES
 +       *          Defer some of the updates until the end of index.php
 +       *      EDIT_AUTOSUMMARY
 +       *          Fill in blank summaries with generated text where possible
 +       *
 +       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
 +       * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
 +       * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
 +       * edit-already-exists error will be returned. These two conditions are also possible with
 +       * auto-detection due to MediaWiki's performance-optimised locking strategy.
 +       *
 +       * @param $baseRevId the revision ID this edit was based off, if any
 +       * @param $user User the user doing the edit
 +       * @param $serialisation_format String: format for storing the content in the database
 +       *
 +       * @return Status object. Possible errors:
 +       *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
 +       *     edit-gone-missing:       In update mode, but the article didn't exist
 +       *     edit-conflict:           In update mode, the article changed unexpectedly
 +       *     edit-no-change:          Warning that the text was the same as before
 +       *     edit-already-exists:     In creation mode, but the article already exists
 +       *
 +       *  Extensions may define additional errors.
 +       *
 +       *  $return->value will contain an associative array with members as follows:
 +       *     new:                     Boolean indicating if the function attempted to create a new article
 +       *     revision:                The revision object for the inserted revision, or null
 +       *
 +       * @since 1.WD
 +       */
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
  
                # Low-level sanity check
                $user = is_null( $user ) ? $wgUser : $user;
                $status = Status::newGood( array() );
  
-               # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
+               // Load the data from the master database if needed.
+               // The caller may already loaded it from the master or even loaded it using
+               // SELECT FOR UPDATE, so do not override that using clear().
                $this->loadPageData( 'fromdbmaster' );
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # call legacy hook
 +              $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +              if ( $hook_ok && !empty( $wgHooks['ArticleSave'] ) ) { # avoid serialization overhead if the hook isn't present
 +                      $content_text = $content->serialize();
 +                      $txt = $content_text; # clone
 +
 +                      $hook_ok = wfRunHooks( 'ArticleSave', array( &$this, &$user, &$txt, &$summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +                      if ( $txt !== $content_text ) {
 +                              # if the text changed, unserialize the new version to create an updated Content object.
 +                              $content = $content->getContentHandler()->unserializeContent( $txt );
 +                      }
 +              }
 +
 +              if ( !$hook_ok ) {
 +                      wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
  
                        if ( $status->isOK() ) {
                                $status->fatal( 'edit-hook-aborted' );
                $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
                $bot = $flags & EDIT_FORCE_BOT;
  
 -              $oldtext = $this->getRawText(); // current revision
 -              $oldsize = strlen( $oldtext );
 +              $old_content = $this->getContent( Revision::RAW ); // current revision's content
 +
 +              $oldsize = $old_content ? $old_content->getSize() : 0;
                $oldid = $this->getLatest();
                $oldIsRedirect = $this->isRedirect();
                $oldcountable = $this->isCountable();
  
 +              $handler = $content->getContentHandler();
 +
                # Provide autosummaries if one is not provided and autosummaries are enabled.
                if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
 -                      $summary = self::getAutosummary( $oldtext, $text, $flags );
 +                      if ( !$old_content ) $old_content = null;
 +                      $summary = $handler->getAutosummary( $old_content, $content, $flags );
                }
  
 -              $editInfo = $this->prepareTextForEdit( $text, null, $user );
 -              $text = $editInfo->pst;
 -              $newsize = strlen( $text );
 +              $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
 +              $serialized = $editInfo->pst;
 +              $content = $editInfo->pstContent;
 +              $newsize =  $content->getSize();
  
                $dbw = wfGetDB( DB_MASTER );
                $now = wfTimestampNow();
                                'page'       => $this->getId(),
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'parent_id'  => $oldid,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
                                $dbw->begin( __METHOD__ );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
 -                                      '', strlen( $text ), $revisionId, $patrolled );
 +                                      '', $content->getSize(), $revisionId, $patrolled );
  
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 +                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +
 +                      wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
  
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 +              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +
 +              wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
  
                # Promote user to any groups they meet the criteria for
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.WD: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->prepareContentForEdit( $content, $revid , $user );
 +      }
 +
 +      /**
 +       * Prepare content which is about to be saved.
 +       * Returns a stdclass with source, pst and output members
 +       *
 +       * @param \Content $content
 +       * @param null $revid
 +       * @param null|\User $user
 +       * @param null $serialization_format
 +       *
 +       * @return bool|object
 +       *
 +       * @since 1.WD
 +       */
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $edit = (object)array();
                $edit->revid = $revid;
 -              $edit->newText = $text;
 -              $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
 +
 +              $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 +              $edit->pst = $edit->pstContent->serialize( $serialization_format );
 +              $edit->format = $serialization_format;
 +
                $edit->popts = $this->makeParserOptions( 'canonical' );
 -              $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
 -              $edit->oldText = $this->getRawText();
 +
 +              // TODO: is there no better way to obtain a context here?
 +              $context = RequestContext::getMain();
 +              $context->setTitle( $this->mTitle );
 +              $edit->output = $edit->pstContent->getParserOutput( $context, $revid, $edit->popts );
 +
 +              $edit->newContent = $content;
 +              $edit->oldContent = $this->getContent( Revision::RAW );
 +
 +              $edit->newText = ContentHandler::getContentText( $edit->newContent ); #FIXME: B/C only! don't use this field!
 +              $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : ''; #FIXME: B/C only! don't use this field!
  
                $this->mPreparedEdit = $edit;
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
                wfProfileIn( __METHOD__ );
  
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
 -              $text = $revision->getText();
 +              $content = $revision->getContent();
  
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
 -                      $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
 +                      $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                        $parserCache->save( $editInfo->output, $this, $editInfo->popts );
                }
  
 -              # Update the links tables
 -              $u = new LinksUpdate( $this->mTitle, $editInfo->output );
 -              $u->doUpdate();
 +              # Update the links tables and other secondary data
 +              $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
 +              SecondaryDataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
  
                }
  
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
 +              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) );
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      $msgtext = ContentHandler::getContentText( $content ); #XXX: could skip pseudo-messages like js/css here, based on content model.
 +                      if ( $msgtext === false || $msgtext === null ) $msgtext = '';
 +
 +                      MessageCache::singleton()->replace( $shortTitle, $msgtext );
                }
  
                if( $options['created'] ) {
         * @param $user User The relevant user
         * @param $comment String: comment submitted
         * @param $minor Boolean: whereas it's a minor modification
 +       *
 +       * @deprecated since 1.WD, use doEditContent() instead.
         */
        public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->doQuickEditContent( $content, $user, $comment , $minor );
 +      }
 +
 +      /**
 +       * Edit an article without doing all that other stuff
 +       * The article must already exist; link tables etc
 +       * are not updated, caches are not flushed.
 +       *
 +       * @param $content Content: content submitted
 +       * @param $user User The relevant user
 +       * @param $comment String: comment submitted
 +       * @param $serialisation_format String: format for storing the content in the database
 +       * @param $minor Boolean: whereas it's a minor modification
 +       */
 +      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
                wfProfileIn( __METHOD__ );
  
 +              $serialized = $content->serialize( $serialisation_format );
 +
                $dbw = wfGetDB( DB_MASTER );
                $revision = new Revision( array(
                        'page'       => $this->getId(),
 -                      'text'       => $text,
 +                      'text'       => $serialized,
 +                      'length'     => $content->getSize(),
                        'comment'    => $comment,
                        'minor_edit' => $minor ? 1 : 0,
                ) );
         * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
         * @param $expiry Array: per restriction type expiration
         * @param $user User The user updating the restrictions
-        * @return bool true on success
+        * @return Status
         */
        public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
                global $wgContLang;
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
-               $user = is_null( $user ) ? $wgUser : $user;
  
                wfDebug( __METHOD__ . "\n" );
  
+               if ( $this->mTitle->getDBkey() === '' ) {
+                       return WikiPage::DELETE_NO_PAGE;
+               }
+               $user = is_null( $user ) ? $wgUser : $user;
                if ( ! wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error ) ) ) {
                        return WikiPage::DELETE_HOOK_ABORTED;
                }
-               $dbw = wfGetDB( DB_MASTER );
-               $t = $this->mTitle->getDBkey();
-               $id = $id ? $id : $this->mTitle->getArticleID( Title::GAID_FOR_UPDATE );
  
-               if ( $t === '' || $id == 0 ) {
-                       return WikiPage::DELETE_NO_PAGE;
+               if ( $id == 0 ) {
+                       $this->loadPageData( 'forupdate' );
+                       $id = $this->getID();
+                       if ( $id == 0 ) {
+                               return WikiPage::DELETE_NO_PAGE;
+                       }
                }
  
                // Bitfields to further suppress the content
                        $bitfield = 'rev_deleted';
                }
  
+               $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
                // For now, shunt the revision data into the archive table.
                // Text is *not* removed from the text table; bulk storage
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
 +
 +              $row = array(
 +                      'ar_namespace'  => 'page_namespace',
 +                      'ar_title'      => 'page_title',
 +                      'ar_comment'    => 'rev_comment',
 +                      'ar_user'       => 'rev_user',
 +                      'ar_user_text'  => 'rev_user_text',
 +                      'ar_timestamp'  => 'rev_timestamp',
 +                      'ar_minor_edit' => 'rev_minor_edit',
 +                      'ar_rev_id'     => 'rev_id',
 +                      'ar_parent_id'  => 'rev_parent_id',
 +                      'ar_text_id'    => 'rev_text_id',
 +                      'ar_text'       => '\'\'', // Be explicit to appease
 +                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 +                      'ar_len'        => 'rev_len',
 +                      'ar_page_id'    => 'page_id',
 +                      'ar_deleted'    => $bitfield,
 +                      'ar_sha1'       => 'rev_sha1',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'ar_content_model' ] = 'rev_content_model';
 +                      $row[ 'ar_content_format' ] = 'rev_content_format';
 +              }
 +
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
 +                      $row,
                        array(
 -                              'ar_namespace'  => 'page_namespace',
 -                              'ar_title'      => 'page_title',
 -                              'ar_comment'    => 'rev_comment',
 -                              'ar_user'       => 'rev_user',
 -                              'ar_user_text'  => 'rev_user_text',
 -                              'ar_timestamp'  => 'rev_timestamp',
 -                              'ar_minor_edit' => 'rev_minor_edit',
 -                              'ar_rev_id'     => 'rev_id',
 -                              'ar_parent_id'  => 'rev_parent_id',
 -                              'ar_text_id'    => 'rev_text_id',
 -                              'ar_text'       => '\'\'', // Be explicit to appease
 -                              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                              'ar_len'        => 'rev_len',
 -                              'ar_page_id'    => 'page_id',
 -                              'ar_deleted'    => $bitfield,
 -                              'ar_sha1'       => 'rev_sha1'
 -                      ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
                        return WikiPage::DELETE_NO_REVISIONS;
                }
  
 -              $this->doDeleteUpdates( $id );
 +              # update site status
 +              DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
 +
 +              # remove secondary indexes, etc
 +              $handler = $this->getContentHandler();
 +              $updates = $handler->getDeletionUpdates( $this );
 +              SecondaryDataUpdate::runUpdates( $updates );
 +
 +              # Clear caches
 +              WikiPage::onArticleDelete( $this->mTitle );
 +
 +              # Reset this object
 +              $this->clear();
 +
 +              # Clear the cached article id so the interface doesn't act like we exist
 +              $this->mTitle->resetArticleID( 0 );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
                return WikiPage::DELETE_SUCCESS;
        }
  
 -      /**
 -       * Do some database updates after deletion
 -       *
 -       * @param $id Int: page_id value of the page being deleted
 -       */
 -      public function doDeleteUpdates( $id ) {
 -              DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              # Delete restrictions for it
 -              $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
 -
 -              # Fix category table counts
 -              $cats = array();
 -              $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
 -
 -              foreach ( $res as $row ) {
 -                      $cats [] = $row->cl_to;
 -              }
 -
 -              $this->updateCategoryCounts( array(), $cats );
 -
 -              # If using cascading deletes, we can skip some explicit deletes
 -              if ( !$dbw->cascadingDeletes() ) {
 -                      $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
 -
 -                      # Delete outgoing links
 -                      $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
 -                      $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
 -              }
 -
 -              # If using cleanup triggers, we can skip some manual deletes
 -              if ( !$dbw->cleanupTriggers() ) {
 -                      # Clean up recentchanges entries...
 -                      $dbw->delete( 'recentchanges',
 -                              array( 'rc_type != ' . RC_LOG,
 -                                      'rc_namespace' => $this->mTitle->getNamespace(),
 -                                      'rc_title' => $this->mTitle->getDBkey() ),
 -                              __METHOD__ );
 -                      $dbw->delete( 'recentchanges',
 -                              array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
 -                              __METHOD__ );
 -              }
 -
 -              # Clear caches
 -              self::onArticleDelete( $this->mTitle );
 -
 -              # Reset this object
 -              $this->clear();
 -
 -              # Clear the cached article id so the interface doesn't act like we exist
 -              $this->mTitle->resetArticleID( 0 );
 -      }
 -
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
                }
  
                # Actually store the edit
 -              $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
 +              $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
                if ( !empty( $status->value['revision'] ) ) {
                        $revId = $status->value['revision']->getId();
                } else {
  
        /**
        * Return an applicable autosummary if one exists for the given edit.
 -      * @param $oldtext String: the previous text of the page.
 -      * @param $newtext String: The submitted text of the page.
 +      * @param $oldtext String|null: the previous text of the page.
 +      * @param $newtext String|null: The submitted text of the page.
        * @param $flags Int bitmask: a bitmask of flags submitted for the edit.
        * @return string An appropriate autosummary, or an empty string.
 +      * @deprecated since 1.WD, use ContentHandler::getAutosummary() instead
        */
        public static function getAutosummary( $oldtext, $newtext, $flags ) {
 -              global $wgContLang;
 -
 -              # Decide what kind of autosummary is needed.
 -
 -              # Redirect autosummaries
 -              $ot = Title::newFromRedirect( $oldtext );
 -              $rt = Title::newFromRedirect( $newtext );
 -
 -              if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 250
 -                                      - strlen( wfMsgForContent( 'autoredircomment' ) )
 -                                      - strlen( $rt->getFullText() )
 -                              ) );
 -                      return wfMsgForContent( 'autoredircomment', $rt->getFullText(), $truncatedtext );
 -              }
 -
 -              # New page autosummaries
 -              if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
 -                      # If they're making a new article, give its text, truncated, in the summary.
 -
 -                      $truncatedtext = $wgContLang->truncate(
 -                              str_replace( "\n", ' ', $newtext ),
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
 -
 -                      return wfMsgForContent( 'autosumm-new', $truncatedtext );
 -              }
 -
 -              # Blanking autosummaries
 -              if ( $oldtext != '' && $newtext == '' ) {
 -                      return wfMsgForContent( 'autosumm-blank' );
 -              } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
 -                      # Removing more than 90% of the article
 +              # NOTE: stub for backwards-compatibility. assumes the given text is wikitext. will break horribly if it isn't.
  
 -                      $truncatedtext = $wgContLang->truncate(
 -                              $newtext,
 -                              max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
 +              wfDeprecated( __METHOD__, '1.WD' );
  
 -                      return wfMsgForContent( 'autosumm-replace', $truncatedtext );
 -              }
 +              $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
 +              $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
 +              $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
  
 -              # If we reach this point, there's no applicable autosummary for our case, so our
 -              # autosummary is empty.
 -              return '';
 +              return $handler->getAutosummary( $oldContent, $newContent, $flags );
        }
  
        /**
         * @param &$hasHistory Boolean: whether the page has a history
         * @return mixed String containing deletion reason or empty string, or boolean false
         *    if no revision occurred
 +       * @deprecated since 1.WD, use ContentHandler::getAutoDeleteReason() instead
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 +              #NOTE: stub for backwards-compatibility.
 +
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $handler = ContentHandler::getForTitle( $this->getTitle() );
 +              $handler->getAutoDeleteReason( $this->getTitle(), $hasHistory );
                global $wgContLang;
  
                // Get the last revision
  
                if ( count( $templates_diff ) > 0 ) {
                        # Whee, link updates time.
 +                      # Note: we are only interested in links here. We don't need to get other SecondaryDataUpdate items from the parser output.
                        $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
                        $u->doUpdate();
                }
@@@ -3061,27 -3054,14 +3231,27 @@@ class PoolWorkArticleView extends PoolC
         * @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
 +       * @param $context IContextSource context for parsing
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null, IContextSource $context = 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 );
 +              }
 +
 +              if ( is_null( $context ) ) {
 +                      $context = RequestContext::getMain();
 +                      #XXX: clone and then set title?
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
 +              $this->context = $context;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +                      $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              // TODO: page might not have this method? Hard to tell what page is supposed to be here...
 +              $this->parserOutput = $content->getParserOutput( $this->context, $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
@@@ -46,13 -46,12 +46,12 @@@ class ApiDelete extends ApiBase 
        public function execute() {
                $params = $this->extractRequestParams();
  
-               $titleObj = $this->getTitleOrPageId( $params );
-               $pageObj = WikiPage::factory( $titleObj );
-               $pageObj->loadPageData( 'fromdbmaster' );
+               $pageObj = $this->getTitleOrPageId( $params, 'fromdbmaster' );
                if ( !$pageObj->exists() ) {
                        $this->dieUsageMsg( 'notanarticle' );
                }
  
+               $titleObj = $pageObj->getTitle();
                $reason = ( isset( $params['reason'] ) ? $params['reason'] : null );
                $user = $this->getUser();
  
                        // Need to pass a throwaway variable because generateReason expects
                        // a reference
                        $hasHistory = false;
 -                      $reason = $page->getAutoDeleteReason( $hasHistory );
 +                      $reason = $page->getAutoDeleteReason( $hasHistory ); #FIXME: use ContentHandler::getAutoDeleteReason()
                        if ( $reason === false ) {
                                return array( array( 'cannotdelete', $title->getPrefixedText() ) );
                        }
                return array_merge( parent::getPossibleErrors(),
                        $this->getTitleOrPageIdErrorMessage(),
                        array(
-                               array( 'invalidtitle', 'title' ),
-                               array( 'nosuchpageid', 'pageid' ),
                                array( 'notanarticle' ),
                                array( 'hookaborted', 'error' ),
                                array( 'delete-toobig', 'limit' ),
@@@ -48,7 -48,8 +48,8 @@@ class ApiEditPage extends ApiBase 
                        $this->dieUsageMsg( 'missingtext' );
                }
  
-               $titleObj = $this->getTitleOrPageId( $params );
+               $pageObj = $this->getTitleOrPageId( $params );
+               $titleObj = $pageObj->getTitle();
                if ( $titleObj->isExternal() ) {
                        $this->dieUsageMsg( array( 'invalidtitle', $params['title'] ) );
                }
                        // We do want getContent()'s behavior for non-existent
                        // MediaWiki: pages, though
                        if ( $articleObj->getID() == 0 && $titleObj->getNamespace() != NS_MEDIAWIKI ) {
 -                              $content = '';
 +                              $content = null;
 +                $text = '';
                        } else {
 -                              $content = $articleObj->getContent();
 +                $content = $articleObj->getContentObject();
 +                $text = ContentHandler::getContentText( $content ); #FIXME: serialize?! get format from params?...
                        }
  
                        if ( !is_null( $params['section'] ) ) {
                                // Process the content for section edits
 -                              global $wgParser;
                                $section = intval( $params['section'] );
 -                              $content = $wgParser->getSection( $content, $section, false );
 -                              if ( $content === false ) {
 +                $sectionContent = $content->getSection( $section );
 +                $text = ContentHandler::getContentText( $sectionContent ); #FIXME: serialize?! get format from params?...
 +                              if ( $text === false || $text === null ) {
                                        $this->dieUsage( "There is no section {$section}.", 'nosuchsection' );
                                }
                        }
 -                      $params['text'] = $params['prependtext'] . $content . $params['appendtext'];
 +                      $params['text'] = $params['prependtext'] . $text . $params['appendtext'];
                        $toMD5 = $params['prependtext'] . $params['appendtext'];
                }
  
                // TODO: Make them not or check if they still do
                $wgTitle = $titleObj;
  
 -              $ep = new EditPage( $articleObj );
 +        $handler = ContentHandler::getForTitle( $titleObj );
 +              $ep = $handler->createEditPage( $articleObj );
 +
                $ep->setContextTitle( $titleObj );
                $ep->importFormData( $req );
  
                return array_merge( parent::getPossibleErrors(),
                        $this->getTitleOrPageIdErrorMessage(),
                        array(
-                               array( 'nosuchpageid', 'pageid' ),
                                array( 'missingtext' ),
-                               array( 'invalidtitle', 'title' ),
                                array( 'createonly-exists' ),
                                array( 'nocreate-missing' ),
                                array( 'nosuchrevid', 'undo' ),
diff --combined includes/api/ApiMain.php
@@@ -80,6 -80,7 +80,7 @@@ class ApiMain extends ApiBase 
                'patrol' => 'ApiPatrol',
                'import' => 'ApiImport',
                'userrights' => 'ApiUserrights',
+               'options' => 'ApiOptions',
        );
  
        /**
                'dbgfm' => 'ApiFormatDbg',
                'dump' => 'ApiFormatDump',
                'dumpfm' => 'ApiFormatDump',
 +              'none' => 'ApiFormatNone',
        );
  
        /**
                        $this->executeAction();
                } catch ( Exception $e ) {
                        // Log it
-                       if ( $e instanceof MWException ) {
+                       if ( !( $e instanceof UsageException ) ) {
                                wfDebugLog( 'exception', $e->getLogMessage() );
                        }
  
@@@ -59,13 -59,13 +59,13 @@@ class ApiParse extends ApiBase 
                // The parser needs $wgTitle to be set, apparently the
                // $title parameter in Parser::parse isn't enough *sigh*
                // TODO: Does this still need $wgTitle?
-               global $wgParser, $wgTitle, $wgLang;
+               global $wgParser, $wgTitle;
  
                // Currently unnecessary, code to act as a safeguard against any change in current behaviour of uselang breaks
                $oldLang = null;
-               if ( isset( $params['uselang'] ) && $params['uselang'] != $wgLang->getCode() ) {
-                       $oldLang = $wgLang; // Backup wgLang
-                       $wgLang = Language::factory( $params['uselang'] );
+               if ( isset( $params['uselang'] ) && $params['uselang'] != $this->getContext()->getLanguage()->getCode() ) {
+                       $oldLang = $this->getContext()->getLanguage(); // Backup language
+                       $this->getContext()->setLanguage( Language::factory( $params['uselang'] ) );
                }
  
                $popts = ParserOptions::newFromContext( $this->getContext() );
                                return;
                        }
                        // Not cached (save or load)
 -                      $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts );
 +                      $p_result = $wgParser->parse( $params['pst'] ? $this->pstText : $this->text, $titleObj, $popts ); #FIXME: use Content object¡
                }
  
                $result_array = array();
                                $result->setContent( $result_array['psttext'], $this->pstText );
                        }
                }
+               if ( isset( $prop['properties'] ) ) {
+                       $result_array['properties'] = $this->formatProperties( $p_result->getProperties() );
+               }
  
                $result_mapping = array(
                        'redirects' => 'r',
                        'iwlinks' => 'iw',
                        'sections' => 's',
                        'headitems' => 'hi',
+                       'properties' => 'pp',
                );
                $this->setIndexedTagNames( $result_array, $result_mapping );
                $result->addValue( null, $this->getModuleName(), $result_array );
  
                if ( !is_null( $oldLang ) ) {
-                       $wgLang = $oldLang; // Reset $wgLang to $oldLang
+                       $this->getContext()->setLanguage( $oldLang ); // Reset language to $oldLang
                }
        }
  
  
                $page = WikiPage::factory( $titleObj );
  
 -              if ( $this->section !== false ) {
 +              if ( $this->section !== false ) { #FIXME: get section Content, get parser output, ...
                        $this->text = $this->getSectionText( $page->getRawText(), !is_null( $pageId )
 -                                      ? 'page id ' . $pageId : $titleObj->getText() );
 +                                      ? 'page id ' . $pageId : $titleObj->getText() ); #FIXME: get section...
  
                        // Not cached (save or load)
                        return $wgParser->parse( $this->text, $titleObj, $popts );
                                $this->dieUsage( "There is no revision ID {$page->getLatest()}", 'missingrev' );
                        }
                        if ( $getWikitext ) {
 -                              $this->text = $page->getRawText();
 +                $this->content = $page->getContent( Revision::RAW ); #FIXME: use $this->content everywhere
 +                              $this->text = ContentHandler::getContentText( $this->content ); #FIXME: serialize, get format from params; or use object structure in result?
                        }
                        return $pout;
                }
        }
  
 -      private function getSectionText( $text, $what ) {
 +      private function getSectionText( $text, $what ) { #FIXME: replace with Content::getSection
                global $wgParser;
                // Not cached (save or load)
                $text = $wgParser->getSection( $text, $this->section, false );
                return $result;
        }
  
+       private function formatProperties( $properties ) {
+               $result = array();
+               foreach ( $properties as $name => $value ) {
+                       $entry = array();
+                       $entry['name'] = $name;
+                       $this->getResult()->setContent( $entry, $value );
+                       $result[] = $entry;
+               }
+               return $result;
+       }
        private function formatCss( $css ) {
                $result = array();
                foreach ( $css as $file => $link ) {
                                ApiBase::PARAM_TYPE => 'integer',
                        ),
                        'prop' => array(
-                               ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle',
+                               ApiBase::PARAM_DFLT => 'text|langlinks|categories|links|templates|images|externallinks|sections|revid|displaytitle|iwlinks|properties',
                                ApiBase::PARAM_ISMULTI => true,
                                ApiBase::PARAM_TYPE => array(
                                        'text',
                                        'headhtml',
                                        'iwlinks',
                                        'wikitext',
+                                       'properties',
                                )
                        ),
                        'pst' => false,
                                ' headhtml       - Gives parsed <head> of the page',
                                ' iwlinks        - Gives interwiki links in the parsed wikitext',
                                ' wikitext       - Gives the original wikitext that was parsed',
+                               ' properties     - Gives various properties defined in the parsed wikitext',
                        ),
                        'pst' => array(
                                'Do a pre-save transform on the input before parsing it',
        }
  
        public function getDescription() {
-               return 'Parses wikitext and returns parser output';
+               return array(
+                       'Parses wikitext and returns parser output',
+                       'See the various prop-Modules of action=query to get information from the current version of a page',
+               );
        }
  
        public function getPossibleErrors() {
@@@ -89,12 -89,13 +89,13 @@@ class ApiPurge extends ApiBase 
                                        global $wgParser, $wgEnableParserCache;
  
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
+                                       $popts->setTidy( true );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
 -                                              true, true, $page->getLatest() );
 +                                              true, true, $page->getLatest() ); #FIXME: content!
  
                                        # Update the links tables
 -                                      $u = new LinksUpdate( $title, $p_result );
 -                                      $u->doUpdate();
 +                    $updates = $p_result->getSecondaryDataUpdates( $title );
 +                    SecondaryDataUpdate::runUpdates( $updates );
  
                                        $r['linkupdate'] = '';
  
@@@ -114,7 -114,7 +114,7 @@@ class ApiQueryRevisions extends ApiQuer
                }
  
                if ( !is_null( $params['difftotext'] ) ) {
 -                      $this->difftotext = $params['difftotext'];
 +                      $this->difftotext = $params['difftotext']; #FIXME: handle non-text content!
                } elseif ( !is_null( $params['diffto'] ) ) {
                        if ( $params['diffto'] == 'cur' ) {
                                $params['diffto'] = 0;
                                        "rev_id >= '$revid')"
                                );
                        }
-                       $this->addOption( 'ORDER BY', 'rev_page, rev_id' );
+                       $this->addOption( 'ORDER BY', array(
+                               'rev_page',
+                               'rev_id'
+                       ));
  
                        // assumption testing -- we should never get more then $pageCount rows.
                        $limit = $pageCount;
                                $vals['diff'] = array();
                                $context = new DerivativeContext( $this->getContext() );
                                $context->setTitle( $title );
 +                $handler = ContentHandler::getForTitle( $title );
 +
                                if ( !is_null( $this->difftotext ) ) {
 -                                      $engine = new DifferenceEngine( $context );
 -                                      $engine->setText( $text, $this->difftotext );
 +                                      $engine = $handler->createDifferenceEngine( $context );
 +                                      $engine->setText( $text, $this->difftotext ); #FIXME: use content objects!...
                                } else {
 -                                      $engine = new DifferenceEngine( $context, $revision->getID(), $this->diffto );
 +                                      $engine = $handler->createDifferenceEngine( $context, $revision->getID(), $this->diffto );
                                        $vals['diff']['from'] = $engine->getOldid();
                                        $vals['diff']['to'] = $engine->getNewid();
                                }
@@@ -1,6 -1,21 +1,21 @@@
  <?php
  /**
-  * User interface for the difference engine
+  * User interface for the difference engine.
+  *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
   *
   * @file
   * @ingroup DifferenceEngine
@@@ -23,7 -38,7 +38,7 @@@ class DifferenceEngine extends ContextS
         * @private
         */
        var $mOldid, $mNewid;
 -      var $mOldtext, $mNewtext;
 +      var $mOldContent, $mNewContent;
        protected $mDiffLang;
  
        /**
                # a diff between a version V and its previous version V' AND the version V
                # is the first version of that article. In that case, V' does not exist.
                if ( $this->mOldRev === false ) {
-                       $out->setPageTitle( $this->mNewPage->getPrefixedText() );
-                       $out->addSubtitle( $this->msg( 'difference' ) );
+                       $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
                        $samePage = true;
                        $oldHeader = '';
                } else {
                        }
  
                        if ( $this->mNewPage->equals( $this->mOldPage ) ) {
-                               $out->setPageTitle( $this->mNewPage->getPrefixedText() );
-                               $out->addSubtitle( $this->msg( 'difference' ) );
+                               $out->setPageTitle( $this->msg( 'difference-title', $this->mNewPage->getPrefixedText() ) );
                                $samePage = true;
                        } else {
-                               $out->setPageTitle( $this->mOldPage->getPrefixedText() . ', ' . $this->mNewPage->getPrefixedText() );
+                               $out->setPageTitle( $this->msg( 'difference-title-multipage', $this->mOldPage->getPrefixedText(),
+                                       $this->mNewPage->getPrefixedText() ) );
                                $out->addSubtitle( $this->msg( 'difference-multipage' ) );
                                $samePage = false;
                        }
                        $out->setRevisionTimestamp( $this->mNewRev->getTimestamp() );
                        $out->setArticleFlag( true );
  
 -                      if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) {
 +                      if ( $this->mNewPage->isCssJsSubpage() || $this->mNewPage->isCssOrJsPage() ) { #NOTE: only needed for B/C: custom rendering of JS/CSS via hook
                                // Stolen from Article::view --AG 2007-10-11
                                // Give hooks a chance to customise the output
                                // @TODO: standardize this crap into one function
 -                              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 -                                      // Wrap the whole lot in a <pre> and don't parse
 -                                      $m = array();
 -                                      preg_match( '!\.(css|js)$!u', $this->mNewPage->getText(), $m );
 -                                      $out->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                                      $out->addHTML( htmlspecialchars( $this->mNewtext ) );
 -                                      $out->addHTML( "\n</pre>\n" );
 +                              if ( !Hook::isRegistered( 'ShowRawCssJs' )
 +                    || wfRunHooks( 'ShowRawCssJs', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
 +                    // use the content object's own rendering
 +                    $po = $this->mContentObject->getParserOutput();
 +                    $out->addHTML( $po->getText() );
                                }
 -                      } elseif ( !wfRunHooks( 'ArticleViewCustom', array( $this->mNewtext, $this->mNewPage, $out ) ) ) {
 -                              // Handled by extension
 +            } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->mNewContent, $this->mNewPage, $out ) ) ) {
 +                // Handled by extension
 +            } elseif( Hooks::isRegistered( 'ArticleViewCustom' )
 +                    && !wfRunHooks( 'ArticleViewCustom', array( ContentHandler::getContentText( $this->mNewContent ), $this->mNewPage, $out ) ) ) { #NOTE: deperecated hook, B/C only
 +                // Handled by extension
                        } else {
                                // Normal page
                                if ( $this->getTitle()->equals( $this->mNewPage ) ) {
                        return false;
                }
  
 -              $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
 +        #TODO: make sure both Content objects have the same content model. What do we do if they don't?
 +
 +              $difftext = $this->generateContentDiffBody( $this->mOldContent, $this->mNewContent );
  
                // Save to cache for 7 days
                if ( !wfRunHooks( 'AbortDiffCache', array( &$this ) ) ) {
                }
        }
  
 +    /**
 +     * Generate a diff, no caching.
 +     *
 +     * Subclasses may override this to provide a
 +     *
 +     * @param $old Content: old content
 +     * @param $new Content: new content
 +     *
 +     * @since 1.WD
 +     */
 +    function generateContentDiffBody( Content $old, Content $new ) {
 +        #XXX: generate a warning if $old or $new are not instances of TextContent?
 +        #XXX: fail if $old and $new don't have the same content model? or what?
 +
 +        $otext = $old->serialize();
 +        $ntext = $new->serialize();
 +
 +        #XXX: text should be "already segmented". what does that mean?
 +        return $this->generateTextDiffBody( $otext, $ntext );
 +    }
 +
 +    /**
 +     * Generate a diff, no caching
 +     *
 +     * @param $otext String: old text, must be already segmented
 +     * @param $ntext String: new text, must be already segmented
 +     * @deprecated since 1.WD, use generateContentDiffBody() instead!
 +     */
 +    function generateDiffBody( $otext, $ntext ) {
 +        wfDeprecated( __METHOD__, "1.WD" );
 +
 +        return $this->generateTextDiffBody( $otext, $ntext );
 +    }
 +
        /**
         * Generate a diff, no caching
         *
 +     * @todo move this to TextDifferenceEngine, make DifferenceEngine abstract. At some point.
 +     *
         * @param $otext String: old text, must be already segmented
         * @param $ntext String: new text, must be already segmented
         * @return bool|string
         */
 -      function generateDiffBody( $otext, $ntext ) {
 +      function generateTextDiffBody( $otext, $ntext ) {
                global $wgExternalDiffEngine, $wgContLang;
  
                wfProfileIn( __METHOD__ );
  
        /**
         * Use specified text instead of loading from the database
 +     * @deprecated since 1.WD
         */
 -      function setText( $oldText, $newText ) {
 -              $this->mOldtext = $oldText;
 -              $this->mNewtext = $newText;
 -              $this->mTextLoaded = 2;
 -              $this->mRevisionsLoaded = true;
 -      }
 +      function setText( $oldText, $newText ) { #FIXME: no longer use this, use setContent()!
 +        wfDeprecated( __METHOD__, "1.WD" );
 +
 +        $oldContent = ContentHandler::makeContent( $oldText, $this->getTitle() );
 +        $newContent = ContentHandler::makeContent( $newText, $this->getTitle() );
 +
 +        $this->setContent( $oldContent, $newContent );
 +    }
 +
 +    /**
 +     * Use specified text instead of loading from the database
 +     * @since 1.WD
 +     */
 +    function setContent( Content $oldContent, Content $newContent ) {
 +        $this->mOldContent = $oldContent;
 +        $this->mNewContent = $newContent;
 +
 +        $this->mTextLoaded = 2;
 +        $this->mRevisionsLoaded = true;
 +    }
  
        /**
         * Set the language in which the diff text is written
                        return false;
                }
                if ( $this->mOldRev ) {
 -                      $this->mOldtext = $this->mOldRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mOldtext === false ) {
 +                      $this->mOldContent = $this->mOldRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mOldContent === false ) {
                                return false;
                        }
                }
                if ( $this->mNewRev ) {
 -                      $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 -                      if ( $this->mNewtext === false ) {
 +                      $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
 +                      if ( $this->mNewContent === false ) {
                                return false;
                        }
                }
                if ( !$this->loadRevisionData() ) {
                        return false;
                }
 -              $this->mNewtext = $this->mNewRev->getText( Revision::FOR_THIS_USER );
 +              $this->mNewContent = $this->mNewRev->getContent( Revision::FOR_THIS_USER );
                return true;
        }
  }
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * IBM_DB2-specific updater.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup Deployment
   */
@@@ -72,13 -87,6 +87,13 @@@ class Ibm_db2Updater extends DatabaseUp
  
                        // 1.20
                        array( 'addTable', 'config',                            'patch-config.sql' ),
 +
 +                      // 1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                );
        }
  }
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * MySQL-specific updater.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup Deployment
   */
@@@ -191,19 -206,13 +206,21 @@@ 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( 'addTable', 'config',                            'patch-config.sql' ),
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
+                       array( 'addField', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id.sql' ),
+                       array( 'addIndex', 'ipblocks',      'ipb_parent_block_id',           'patch-ipb-parent-block-id-index.sql' ),
 +
 +                      // 1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                );
        }
  
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * Oracle-specific updater.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup Deployment
   */
@@@ -55,13 -70,6 +70,13 @@@ class OracleUpdater extends DatabaseUpd
                        //1.20
                        array( 'addTable', 'config', 'patch-config.sql' ),
  
 +                      //1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
 +
                        // KEEP THIS AT THE BOTTOM!!
                        array( 'doRebuildDuplicateFunction' ),
  
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * Sqlite-specific updater.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup Deployment
   */
@@@ -70,19 -85,13 +85,21 @@@ 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( 'addTable', 'config',                            'patch-config.sql' ),
                        array( 'addIndex', 'revision', 'page_user_timestamp', 'patch-revision-user-page-index.sql' ),
+                       array( 'addField', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id.sql' ),
+                       array( 'addIndex', 'ipblocks', 'ipb_parent_block_id', 'patch-ipb-parent-block-id-index.sql' ),
 +
 +                      // 1.WD
 +                      array( 'addField',      'revision',     'rev_content_format',           'patch-revision-rev_content_format.sql' ),
 +                      array( 'addField',      'revision',     'rev_content_model',            'patch-revision-rev_content_model.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_format',            'patch-archive-ar_content_format.sql' ),
 +                      array( 'addField',      'archive',      'ar_content_model',                 'patch-archive-ar_content_model.sql' ),
 +                      array( 'addField',      'page',     'page_content_model',               'patch-page-page_content_model.sql' ),
                );
        }
  
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * Job to update links for a given title.
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup JobQueue
   */
@@@ -46,11 -61,9 +61,11 @@@ class RefreshLinksJob extends Job 
                $parserOutput = $wgParser->parse( $revision->getText(), $this->title, $options, true, true, $revision->getId() );
                wfProfileOut( __METHOD__.'-parse' );
                wfProfileIn( __METHOD__.'-update' );
 -              $update = new LinksUpdate( $this->title, $parserOutput, false );
 -              $update->doUpdate();
 -              wfProfileOut( __METHOD__.'-update' );
 +
 +        $updates = $parserOutput->getSecondaryDataUpdates( $this->title, false );
 +        SecondaryDataUpdate::runUpdates( $updates );
 +
 +        wfProfileOut( __METHOD__.'-update' );
                wfProfileOut( __METHOD__ );
                return true;
        }
@@@ -120,10 -133,8 +135,10 @@@ class RefreshLinksJob2 extends Job 
                        $parserOutput = $wgParser->parse( $revision->getText(), $title, $options, true, true, $revision->getId() );
                        wfProfileOut( __METHOD__.'-parse' );
                        wfProfileIn( __METHOD__.'-update' );
 -                      $update = new LinksUpdate( $title, $parserOutput, false );
 -                      $update->doUpdate();
 +
 +            $updates = $parserOutput->getSecondaryDataUpdates( $title, false );
 +            SecondaryDataUpdate::runUpdates( $updates );
 +
                        wfProfileOut( __METHOD__.'-update' );
                        wfWaitForSlaves();
                }
@@@ -2,6 -2,21 +2,21 @@@
  /**
   * Output of the PHP parser
   *
+  * This program is free software; you can redistribute it and/or modify
+  * it under the terms of the GNU General Public License as published by
+  * the Free Software Foundation; either version 2 of the License, or
+  * (at your option) any later version.
+  *
+  * This program is distributed in the hope that it will be useful,
+  * but WITHOUT ANY WARRANTY; without even the implied warranty of
+  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  * GNU General Public License for more details.
+  *
+  * You should have received a copy of the GNU General Public License along
+  * with this program; if not, write to the Free Software Foundation, Inc.,
+  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+  * http://www.gnu.org/copyleft/gpl.html
+  *
   * @file
   * @ingroup Parser
   */
@@@ -10,7 -25,6 +25,6 @@@
   * @todo document
   * @ingroup Parser
   */
  class CacheTime {
        var     $mVersion = Parser::VERSION,  # Compatibility check
                $mCacheTime = '',             # Time when this object was generated, or -1 for uncacheable. Used in ParserCache.
@@@ -30,7 -44,7 +44,7 @@@
         */
        function setCacheTime( $t )          { return wfSetVar( $this->mCacheTime, $t ); }
  
 -      /**
 +      /**abstract
         * Sets the number of seconds after which this object should expire.
         * This value is used with the ParserCache.
         * If called with a value greater than the value provided at any previous call,
@@@ -141,27 -155,8 +155,27 @@@ class ParserOutput extends CacheTime 
                $mProperties = array(),       # Name/value pairs to be cached in the DB
                $mTOCHTML = '',               # HTML of the TOC
                $mTimestamp;                  # Timestamp of the revision
 -      private $mIndexPolicy = '';       # 'index' or 'noindex'?  Any other value will result in no change.
 -      private $mAccessedOptions = array(); # List of ParserOptions (stored in the keys)
 +
 +      /**
 +       * 'index' or 'noindex'?  Any other value will result in no change.
 +       * 
 +       * @var string
 +       */
 +      protected $mIndexPolicy = '';
 +
 +      /**
 +       * List of ParserOptions (stored in the keys)
 +       *
 +       * @var array
 +       */
 +      protected $mAccessedOptions = array();
 +
 +      /**
 +       * List of instances of SecondaryDataObject(), used to cause some information extracted from the page in a custom place.
 +       * @since WD.1
 +       * @var array of SecondaryDataObject
 +       */
 +      protected $mSecondaryDataUpdates = array();
  
        const EDITSECTION_REGEX = '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
  
         function recordOption( $option ) {
                 $this->mAccessedOptions[$option] = true;
         }
 +
 +    /**
 +     * Adds an update job to the output. Any update jobs added to the output will eventually bexecuted in order to
 +     * store any secondary information extracted from the page's content.
 +     *
 +       * @since WD.1
 +       *
 +     * @param SecondaryDataUpdate $update
 +     */
 +    public function addSecondaryDataUpdate( SecondaryDataUpdate $update ) {
 +        $this->mSecondaryDataUpdates[] = $update;
 +    }
 +
 +    /**
 +     * Returns any SecondaryDataUpdate jobs to be executed in order to store secondary information
 +     * extracted from the page's content, includingt a LinksUpdate object for all links stopred in
 +     * this ParserOutput object.
 +     *
 +       * @since WD.1
 +       *
 +     * @param $title Title of the page we're updating. If not given, a title object will be created based on $this->getTitleText()
 +     * @param $recursive Boolean: queue jobs for recursive updates?
 +     *
 +     * @return array an array of instances of SecondaryDataUpdate
 +     */
 +    public function getSecondaryDataUpdates( Title $title = null, $recursive = true ) {
 +        if ( is_null( $title ) ) {
 +            $title = Title::newFromText( $this->getTitleText() );
 +        }
 +
 +        return array_merge(
 +                      $this->mSecondaryDataUpdates,
 +                      array( new LinksUpdate( $title, $this, $recursive ) )
 +              );
 +    }
  }
@@@ -1,5 -1,7 +1,7 @@@
  <?php
  /**
+  * Abstraction for resource loader modules which pull from wiki pages.
+  *
   * 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
@@@ -82,7 -84,7 +84,7 @@@ abstract class ResourceLoaderWikiModul
                if ( !$revision ) {
                        return null;
                }
 -              return $revision->getRawText();
 +              return $revision->getRawText(); #FIXME: get raw data from content object after checking the type;
        }
  
        /* Methods */
@@@ -112,22 -112,12 +112,22 @@@ class PageArchive 
         * @return ResultWrapper
         */
        function listRevisions() {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 +                      'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $res = $dbr->select( 'archive',
 -                      array(
 -                              'ar_minor_edit', 'ar_timestamp', 'ar_user', 'ar_user_text',
 -                              'ar_comment', 'ar_len', 'ar_deleted', 'ar_rev_id', 'ar_sha1'
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                   'ar_title' => $this->title->getDBkey() ),
                        'PageArchive::listRevisions',
         * @return Revision
         */
        function getRevision( $timestamp ) {
 +              global $wgContentHandlerNoDB;
 +
                $dbr = wfGetDB( DB_SLAVE );
 +
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_len',
 +                      'ar_sha1',
 +              );
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                $row = $dbr->selectRow( 'archive',
 -                      array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_len',
 -                              'ar_sha1',
 -                      ),
 +                      $fields,
                        array( 'ar_namespace' => $this->title->getNamespace(),
                                        'ar_title' => $this->title->getDBkey(),
                                        'ar_timestamp' => $dbr->timestamp( $timestamp ) ),
         * @return Mixed: number of revisions restored or false on failure
         */
        private function undeleteRevisions( $timestamps, $unsuppress = false, $comment = '' ) {
 +              global $wgContentHandlerNoDB;
 +
                if ( wfReadOnly() ) {
                        return false;
                }
                        $oldones = "ar_timestamp IN ( {$oldts} )";
                }
  
 +              $fields = array(
 +                      'ar_rev_id',
 +                      'ar_text',
 +                      'ar_comment',
 +                      'ar_user',
 +                      'ar_user_text',
 +                      'ar_timestamp',
 +                      'ar_minor_edit',
 +                      'ar_flags',
 +                      'ar_text_id',
 +                      'ar_deleted',
 +                      'ar_page_id',
 +                      'ar_len',
 +                      'ar_sha1');
 +
 +              if ( !$wgContentHandlerNoDB ) {
 +                      $fields[] = 'ar_content_format';
 +                      $fields[] = 'ar_content_model';
 +              }
 +
                /**
                 * Select each archived revision...
                 */
                $result = $dbw->select( 'archive',
 -                      /* fields */ array(
 -                              'ar_rev_id',
 -                              'ar_text',
 -                              'ar_comment',
 -                              'ar_user',
 -                              'ar_user_text',
 -                              'ar_timestamp',
 -                              'ar_minor_edit',
 -                              'ar_flags',
 -                              'ar_text_id',
 -                              'ar_deleted',
 -                              'ar_page_id',
 -                              'ar_len',
 -                              'ar_sha1' ),
 +                      $fields,
                        /* WHERE */ array(
                                'ar_namespace' => $this->title->getNamespace(),
                                'ar_title'     => $this->title->getDBkey(),
@@@ -921,8 -892,7 +921,8 @@@ class SpecialUndelete extends SpecialPa
         * @return String: HTML
         */
        function showDiff( $previousRev, $currentRev ) {
 -              $diffEngine = new DifferenceEngine( $this->getContext() );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $diffEngine = $contentHandler->createDifferenceEngine( $this->getContext() );
                $diffEngine->showDiffStyle();
                $this->getOutput()->addHTML(
                        "<div>" .
                                $this->diffHeader( $currentRev, 'n' ) .
                                "</td>\n" .
                        "</tr>" .
 -                      $diffEngine->generateDiffBody(
 -                              $previousRev->getText(), $currentRev->getText() ) .
 +                      $diffEngine->generateContentDiffBody(
 +                              $previousRev->getContent(), $currentRev->getContent() ) .
                        "</table>" .
                        "</div>\n"
                );
                }
                $out->wrapWikiMsg(
                        "<div class='mw-undelete-pagetitle'>\n$1\n</div>\n",
-                       array( 'undeletepagetitle', $this->mTargetObj->getPrefixedText() )
+                       array( 'undeletepagetitle', wfEscapeWikiText( $this->mTargetObj->getPrefixedText() ) )
                );
  
                $archive = new PageArchive( $this->mTargetObj );
@@@ -887,7 -887,6 +887,7 @@@ $1'
  'portal-url'           => 'Project:Community portal',
  'privacy'              => 'Privacy policy',
  'privacypage'          => 'Project:Privacy policy',
 +'content-failed-to-parse' => "Failed to parse $2 content for $1 model: $3",
  
  'badaccess'        => 'Permission error',
  'badaccess-group0' => 'You are not allowed to execute the action you have requested.',
@@@ -1040,6 -1039,8 +1040,8 @@@ The reason given is "\'\'$2\'\'".'
  'filereadonlyerror'    => 'Unable to modify the file "$1" because the file repository "$2" is in read-only mode.
  
  The administrator who locked it offered this explanation: "$3".',
+ 'invalidtitle-knownnamespace'   => 'Invalid title with namespace "$2" and text "$3"',
+ 'invalidtitle-unknownnamespace' => 'Invalid title with unknown namespace number $1 and text "$2"',
  
  # Virus scanner
  'virus-badscanner'     => "Bad configuration: Unknown virus scanner: ''$1''",
@@@ -1483,6 -1484,8 +1485,8 @@@ These arguments have been omitted."
  'node-count-exceeded-warning'             => 'Page exceeded the node-count',
  'expansion-depth-exceeded-category'       => 'Pages where expansion depth is exceeded',
  'expansion-depth-exceeded-warning'        => 'Page exceeded the expansion depth',
+ 'parser-unstrip-loop-warning'             => 'Unstrip loop detected',
+ 'parser-unstrip-recursion-limit'          => 'Unstrip recursion limit exceeded ($1)',
  
  # "Undo" feature
  'undo-success' => 'The edit can be undone.
@@@ -1666,15 -1669,16 +1670,16 @@@ Note that using the navigation links wi
  'mergelogpagetext'   => 'Below is a list of the most recent merges of one page history into another.',
  
  # Diffs
- 'history-title'            => 'Revision history of "$1"',
- 'difference'               => '(Difference between revisions)',
- 'difference-multipage'     => '(Difference between pages)',
- 'lineno'                   => 'Line $1:',
- 'compareselectedversions'  => 'Compare selected revisions',
- 'showhideselectedversions' => 'Show/hide selected revisions',
- 'editundo'                 => 'undo',
- 'diff-multi'               => '({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by {{PLURAL:$2|one user|$2 users}} not shown)',
- 'diff-multi-manyusers'     => '({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by more than $2 {{PLURAL:$2|user|users}} not shown)',
+ 'history-title'              => 'Revision history of "$1"',
+ 'difference-title'           => 'Difference between revisions of "$1"',
+ 'difference-title-multipage' => 'Difference between pages "$1" and "$2"',
+ 'difference-multipage'       => '(Difference between pages)',
+ 'lineno'                     => 'Line $1:',
+ 'compareselectedversions'    => 'Compare selected revisions',
+ 'showhideselectedversions'   => 'Show/hide selected revisions',
+ 'editundo'                   => 'undo',
+ 'diff-multi'                 => '({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by {{PLURAL:$2|one user|$2 users}} not shown)',
+ 'diff-multi-manyusers'       => '({{PLURAL:$1|One intermediate revision|$1 intermediate revisions}} by more than $2 {{PLURAL:$2|user|users}} not shown)',
  
  # Search results
  'search-summary'                   => '', # do not translate or duplicate this message to other languages
@@@ -2348,6 -2352,7 +2353,7 @@@ For optimal security, img_auth.php is d
  'http-curl-error'       => 'Error fetching URL: $1',
  'http-host-unreachable' => 'Could not reach URL.',
  'http-bad-status'       => 'There was a problem during the HTTP request: $1 $2',
+ 'http-truncated-body'   => 'The request body was only partially received.',
  
  # Some likely curl errors. More could be added from <http://curl.haxx.se/libcurl/c/libcurl-errors.html>
  'upload-curl-error6'       => 'Could not reach URL',
@@@ -4861,10 -4866,4 +4867,10 @@@ Otherwise, you can use the easy form be
  'duration-centuries' => '$1 {{PLURAL:$1|century|centuries}}',
  'duration-millennia' => '$1 {{PLURAL:$1|millennium|millennia}}',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
 +'content-model-1' => 'wikitext',
 +'content-model-2' => 'JavaScript',
 +'content-model-3' => 'CSS',
 +'content-model-4' => 'plain text',
 +
  );
   * @author Rancher
   * @author Raymond
   * @author Robby
+  * @author Rotemliss
   * @author Ryan Schmidt
   * @author SPQRobin
   * @author Sanbec
   * @author Sborsody
   * @author Seb35
   * @author Sherbrooke
+  * @author Shirayuki
   * @author Shushruth
   * @author Siebrand
   * @author Singularity
@@@ -554,7 -556,7 +556,7 @@@ The format is: "{{int:youhavenewmessage
  
  {{Identical|View source}}',
  'editsectionhint' => "Tool tip shown when hovering the mouse over the link to '[{{MediaWiki:Editsection}}]' a section. Example: Edit section: Heading name",
- 'toc' => 'Ini adalah judul "Daftar isi" yang terlihat pada halaman yang memiliki lebih dari 3 bagian
+ 'toc' => 'This is the title of the table of contents displayed in pages with more than 3 sections
  
  {{Identical|Contents}}',
  'showtoc' => 'This is the link used to show the table of contents
@@@ -682,6 -684,13 +684,13 @@@ $1 is a filename, I think.'
  'editinginterface' => "A message shown when editing pages in the namespace MediaWiki:. In the [http://translatewiki.net/wiki/Main_Page?setlang=en URL], '''change \"setlang=en\" to your own language code.'''",
  '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.
+ * $1 is the namespace number
+ * $2 is the namespace name in content language or {{msg-mw|blanknamespace}} for the main namespace
+ * $3 is the part of the title after the namespace (e.g. SomeName for the page User:SomeName)',
+ 'invalidtitle-unknownnamespace' => 'Displayed when an invalid title was encountered (generally in a list) and the namespace number is unknown.
+ * $1 is the namespace number
+ * $2 is the part of the title after the namespace (e.g. SomeName for the page User:SomeName)',
  
  # Login and logout pages
  'logouttext' => 'Log out message',
  'nocookieslogin' => "This message is displayed when someone tried to login, but the browser doesn't accept cookies.",
  'nocookiesfornew' => "This message is displayed when the user tried to create a new account, but it failed the cross-site request forgery (CSRF) check. It could be blocking an attack, but most likely, the browser isn't  accepting cookies.",
  'nocookiesforlogin' => "{{optional}}
- This message is displayed when someone tried to login and the CSRF failed (most likely, the browser doesn't accept cookies). Defaults to nocookieslogin",
+ This message is displayed when someone tried to login and the CSRF failed (most likely, the browser doesn't accept cookies).
+ Defaults to '''nocookieslogin''' ({{int:nocookieslogin}})",
  'loginsuccesstitle' => 'The title of the page saying that you are logged in. The content of the page is the message "[[MediaWiki:Loginsuccess/{{SUBPAGENAME}}]]".',
  'loginsuccess' => 'The content of the page saying that you are logged in. The title of the page is "[[MediaWiki:Loginsuccesstitle/{{SUBPAGENAME}}|{{int:loginsuccesstitle}}]]". $1 is the name of the logged in user.
  
@@@ -1020,6 -1031,23 +1031,23 @@@ When templates are expanded, there is 
  'language-converter-depth-warning' => 'Error message shown when a page uses too deeply nested language conversion syntax
  
  * <tt>$1</tt> is the value of the depth limit',
+ 'node-count-exceeded-category' => 'This message is used as a category name for a [[mw:Help:Tracking categories|tracking category]] where pages are placed automatically if the node-count of the preprocessor exceeds the limit.',
+ 'node-count-exceeded-warning' => 'Error message shown when a page exceeded the node-count limit of the preprocessor
+ * <tt>$1</tt> is the value of the node-count limit
+ * <tt>$2</tt> is the value of the max node-count limit',
+ 'expansion-depth-exceeded-category' => 'This message is used as a category name for a [[mw:Help:Tracking categories|tracking category]] where pages are placed automatically if the [http://meta.wikimedia.org/wiki/Help:Expansion_depth expansion depth] of the preprocessor exceeds the limit.',
+ 'expansion-depth-exceeded-warning' => 'Error message shown when a page exceeded the [http://meta.wikimedia.org/wiki/Help:Expansion_depth expansion depth limit] of the preprocessor
+ * <tt>$1</tt> is the value of the depth limit
+ * <tt>$2</tt> is the value of the max depth limit',
+ 'parser-unstrip-loop-warning' => 'This error is shown when a parser extension tag such as &lt;pre> includes a reference to itself in its own output.
+ The reference must be to the exact same invocation of the tag at the same location in the source, merely writing &lt;pre>&lt;pre>&lt;/pre>&lt;/pre> will not do it.
+ This is usually impossible and unlikely to happen by accident, so translation is not essential.',
+ 'parser-unstrip-recursion-limit' => 'This message is shown when the recursion limit for nested parser extension tags is exceeded.
+ This warning may be encountered due to input text like &lt;ref>&lt;ref>&lt;ref>...&lt;/ref>&lt;/ref>&lt;/ref>.
+ * <tt>$1</tt> is the depth limit',
  
  # "Undo" feature
  'undo-success' => 'Text on special page to confirm edit revert. You arrive on this page by clicking on the "undo" link on a revision history special page.
@@@ -1227,7 -1255,8 +1255,8 @@@ Please note that the parameters in a lo
  
  # Diffs
  'history-title' => 'Displayed as page title when you click on the "history" tab. The parameter $1 is the normal page title.',
- 'difference' => 'Displayed under the title when viewing the difference between two or more edits.',
+ 'difference-title' => 'Displayed as page title when viewing the difference between two edits of the same page. The parameter $1 is the page title of the two revisions.',
+ 'difference-title-multipage' => 'Displayed as page title when viewing the difference between two edits of different pages. The parameter $1 is the page title of the old revision and $2 is the page title of the new revision.',
  'difference-multipage' => 'Displayed under the title when viewing the difference between two or more pages.
  See also {{msg-mw|difference}}.',
  'lineno' => 'Message used when comparing different versions of a page (diff). $1 is a line number.',
@@@ -2003,6 -2032,7 +2032,7 @@@ Siebrand think this has to do with allo
  
  If \'scheme\' is difficult to translate, then you could use \'prefix\' instead.',
  'http-bad-status' => '$1 is an HTTP error code (e.g. 404), $2 is the HTTP error message (e.g. File Not Found)',
+ 'http-truncated-body' => 'This is a standard HTTP error message. â†’ Seems the connection closed prematurely. The HTTP response contained a content-length greater than the received body.',
  
  'license' => 'This appears in the upload form for the license drop-down. The header in the file description page is now at {{msg-mw|License-header}}.',
  'nolicense' => '{{Identical|None selected}}',
@@@ -4414,8 -4444,9 +4444,9 @@@ This is being used in [[Special:Version
  'version-license-info' => '[[wikipedia:GNU GPL|GNU GPL]] notice shown at [[Special:Version]]. See //www.gnu.org/licenses/old-licenses/gpl-2.0-translations.html for available translations.',
  '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.',
- 'version-entrypoints-header-entrypoint' => 'Header for the first column in the entry points table on [[Special: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' => '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.',
@@@ -4676,10 -4707,4 +4707,10 @@@ $4 is the gender of the target user.'
  'api-error-uploaddisabled' => 'API error message that can be used for client side localisation of API errors.',
  'api-error-verification-error' => 'The word "extension" refers to the part behind the last dot in a file name, that by convention gives a hint about the kind of data format which a files contents are in.',
  
 +# Content model IDs for the ContentHandler facility; used by ContentHander::getContentModel()
 +'content-model-1' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
 +'content-model-2' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
 +'content-model-3' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
 +'content-model-4' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
 +
  );
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_MODEL_XXX constants
 +  ar_content_format int unsigned default NULL
 +
  ) /*$wgDBTableOptions*/;
  
  CREATE INDEX /*i*/name_title_timestamp ON /*_*/archive (ar_namespace,ar_title,ar_timestamp);
@@@ -788,7 -772,13 +788,13 @@@ CREATE TABLE /*_*/ipblocks 
    ipb_block_email bool NOT NULL default 0,
  
    -- Block allows user to edit their own talk page
-   ipb_allow_usertalk bool NOT NULL default 0
+   ipb_allow_usertalk bool NOT NULL default 0,
+   -- ID of the block that caused this block to exist
+   -- Autoblocks set this to the original block
+   -- so that the original block being deleted also
+   -- deletes the autoblocks
+   ipb_parent_block_id int default NULL
  
  ) /*$wgDBTableOptions*/;
  
@@@ -800,6 -790,7 +806,7 @@@ CREATE INDEX /*i*/ipb_user ON /*_*/ipbl
  CREATE INDEX /*i*/ipb_range ON /*_*/ipblocks (ipb_range_start(8), ipb_range_end(8));
  CREATE INDEX /*i*/ipb_timestamp ON /*_*/ipblocks (ipb_timestamp);
  CREATE INDEX /*i*/ipb_expiry ON /*_*/ipblocks (ipb_expiry);
+ CREATE INDEX /*i*/ipb_parent_block_id ON /*_*/ipblocks (ipb_parent_block_id);
  
  
  --
@@@ -3,44 -3,40 +3,65 @@@
  /**
   * Test class for Revision storage.
   *
 + * @group ContentHandler
   * @group Database
   * ^--- important, causes temporary tables to be used instead of the real database
   */
- class RevisionStorageTest extends PHPUnit_Framework_TestCase {
+ class RevisionStorageTest extends MediaWikiTestCase {
  
        var $the_page;
  
+       function  __construct( $name = null, array $data = array(), $dataName = '' ) {
+               parent::__construct( $name, $data, $dataName );
+               $this->tablesUsed = array_merge( $this->tablesUsed,
+                                                array( 'page',
+                                                     'revision',
+                                                     'text',
+                                                     'recentchanges',
+                                                     'logging',
+                                                     'page_props',
+                                                     'pagelinks',
+                                                     'categorylinks',
+                                                     'langlinks',
+                                                     'externallinks',
+                                                     'imagelinks',
+                                                     'templatelinks',
+                                                     'iwlinks' ) );
+       }
        public function setUp() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
 +
 +              $wgExtraNamespaces[ 12312 ] = 'Dummy';
 +              $wgExtraNamespaces[ 12313 ] = 'Dummy_talk';
 +
 +              $wgNamespaceContentModels[ 12312 ] = 'DUMMY';
 +              $wgContentHandlers[ 'DUMMY' ] = 'DummyContentHandlerForTesting';
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +
                if ( !$this->the_page ) {
                        $this->the_page = $this->createPage( 'RevisionStorageTest_the_page', "just a dummy page" );
                }
        }
  
 +      public function tearDown() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
 +
 +              unset( $wgExtraNamespaces[ 12312 ] );
 +              unset( $wgExtraNamespaces[ 12313 ] );
 +
 +              unset( $wgNamespaceContentModels[ 12312 ] );
 +              unset( $wgContentHandlers[ 'DUMMY' ] );
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +      }
 +
        protected function makeRevision( $props = null ) {
                if ( $props === null ) $props = array();
  
@@@ -64,8 -60,7 +85,8 @@@
                        $page->doDeleteArticle( "done" );
                }
  
 -              $page->doEdit( $text, "testing", EDIT_NEW );
 +              $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
 +              $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
@@@ -77,8 -72,6 +98,8 @@@
                $this->assertEquals( $orig->getPage(), $rev->getPage() );
                $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
                $this->assertEquals( $orig->getUser(), $rev->getUser() );
 +              $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
 +              $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
                $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
        }
  
                $this->assertTrue( in_array( 'rev_page', $fields ), 'missing rev_page in list of fields');
                $this->assertTrue( in_array( 'rev_timestamp', $fields ), 'missing rev_timestamp in list of fields');
                $this->assertTrue( in_array( 'rev_user', $fields ), 'missing rev_user in list of fields');
 +
 +              $this->assertTrue( in_array( 'rev_content_model', $fields ), 'missing rev_content_model in list of fields');
 +              $this->assertTrue( in_array( 'rev_content_format', $fields ), 'missing rev_content_format in list of fields');
        }
  
        /**
                $this->assertEquals( 'hello hello.', $rev->getText() );
        }
  
 +      /**
 +       * @covers Revision::getContent
 +       */
 +      public function testGetContent()
 +      {
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.' ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( 'hello hello.', $rev->getContent()->getNativeData() );
 +      }
 +
        /**
         * @covers Revision::revText
         */
  
                $this->assertEquals( 'hello hello raw.', $rev->getRawText() );
        }
 +
 +      /**
 +       * @covers Revision::getContentModel
 +       */
 +      public function testGetContentModel()
 +      {
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $rev->getContentModel() );
 +      }
 +
 +      /**
 +       * @covers Revision::getContentFormat
 +       */
 +      public function testGetContentFormat()
 +      {
 +              $orig = $this->makeRevision( array( 'text' => 'hello hello.', 'content_model' => CONTENT_MODEL_JAVASCRIPT, 'content_format' => CONTENT_FORMAT_JAVASCRIPT ) );
 +              $rev = Revision::newFromId( $orig->getId() );
 +
 +              $this->assertEquals( CONTENT_FORMAT_JAVASCRIPT, $rev->getContentFormat() );
 +      }
 +
        /**
         * @covers Revision::isCurrent
         */
                $rev1x = Revision::newFromId( $rev1->getId() );
                $this->assertTrue( $rev1x->isCurrent() );
  
 -              $page->doEdit( 'Bla bla', 'second rev' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev' );
                $rev2 = $page->getRevision();
  
                # @todo: find out if this should be true
  
                $this->assertNull( $rev1->getPrevious() );
  
 -              $page->doEdit( 'Bla bla', 'second rev testGetPrevious' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev testGetPrevious' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev2->getPrevious() );
  
                $this->assertNull( $rev1->getNext() );
  
 -              $page->doEdit( 'Bla bla', 'second rev testGetNext' );
 +              $page->doEditContent( ContentHandler::makeContent( 'Bla bla', $page->getTitle() ), 'second rev testGetNext' );
                $rev2 = $page->getRevision();
  
                $this->assertNotNull( $rev1->getNext() );
  
                $this->assertNotEquals( $orig->getId(), $rev->getId(), 'new null revision shold have a different id from the original revision' );
                $this->assertEquals( $orig->getTextId(), $rev->getTextId(), 'new null revision shold have the same text id as the original revision' );
 -              $this->assertEquals( 'some testing text', $rev->getText() );
 +              $this->assertEquals( 'some testing text', $rev->getContent()->getNativeData() );
        }
  }
- ?>
@@@ -1,34 -1,7 +1,34 @@@
  <?php
  
 +/**
 + * @group ContentHandler
 + */
  class TitleMethodsTest extends MediaWikiTestCase {
  
 +      public function setup() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
 +
 +              $wgExtraNamespaces[ 12302 ] = 'TEST-JS';
 +              $wgExtraNamespaces[ 12303 ] = 'TEST-JS_TALK';
 +
 +              $wgNamespaceContentModels[ 12302 ] = CONTENT_MODEL_JAVASCRIPT;
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +      }
 +
 +      public function teardown() {
 +              global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContLang;
 +
 +              unset( $wgExtraNamespaces[ 12302 ] );
 +              unset( $wgExtraNamespaces[ 12303 ] );
 +
 +              unset( $wgNamespaceContentModels[ 12302 ] );
 +
 +              MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
 +              $wgContLang->resetNamespaces(); # reset namespace cache
 +      }
 +
        public function dataEquals() {
                return array(
                        array( 'Main Page', 'Main Page', true ),
                $this->assertEquals( $expectedBool, $title->hasSubjectNamespace( $ns ) );
        }
  
 +      public function dataGetContentModel() {
 +              return array(
 +                      array( 'Foo', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'Foo.js', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'Foo/bar.js', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'User:Foo', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'User:Foo.js', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'User:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
 +                      array( 'User:Foo/bar.css', CONTENT_MODEL_CSS ),
 +                      array( 'User talk:Foo/bar.css', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'User:Foo/bar.js.xxx', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'User:Foo/bar.xxx', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'MediaWiki:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
 +                      array( 'MediaWiki:Foo.css', CONTENT_MODEL_CSS ),
 +                      array( 'MediaWiki:Foo/bar.css', CONTENT_MODEL_CSS ),
 +                      array( 'MediaWiki:Foo.JS', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'MediaWiki:Foo.CSS', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'MediaWiki:Foo.css.xxx', CONTENT_MODEL_WIKITEXT ),
 +                      array( 'TEST-JS:Foo', CONTENT_MODEL_JAVASCRIPT ),
 +                      array( 'TEST-JS:Foo.js', CONTENT_MODEL_JAVASCRIPT ),
 +                      array( 'TEST-JS:Foo/bar.js', CONTENT_MODEL_JAVASCRIPT ),
 +                      array( 'TEST-JS_TALK:Foo.js', CONTENT_MODEL_WIKITEXT ),
 +              );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetContentModel
 +       */
 +      public function testGetContentModel( $title, $expectedModelId ) {
 +              $title = Title::newFromText( $title );
 +              $this->assertEquals( $expectedModelId, $title->getContentModel() );
 +      }
 +
 +      /**
 +       * @dataProvider dataGetContentModel
 +       */
 +      public function testHasContentModel( $title, $expectedModelId ) {
 +              $title = Title::newFromText( $title );
 +              $this->assertTrue( $title->hasContentModel( $expectedModelId ) );
 +      }
 +
        public function dataIsCssOrJsPage() {
                return array(
                        array( 'Foo', false ),
                        array( 'MediaWiki:Foo.JS', false ),
                        array( 'MediaWiki:Foo.CSS', false ),
                        array( 'MediaWiki:Foo.css.xxx', false ),
 +                      array( 'TEST-JS:Foo', false ),
 +                      array( 'TEST-JS:Foo.js', false ),
                );
        }
  
                        array( 'MediaWiki:Foo.js', false ),
                        array( 'User:Foo/bar.JS', false ),
                        array( 'User:Foo/bar.CSS', false ),
 +                      array( 'TEST-JS:Foo', false ),
 +                      array( 'TEST-JS:Foo.js', false ),
                );
        }
  
                        array( 'MediaWiki:Foo/bar.css', false ),
                        array( 'User:Foo/bar.JS', true ),
                        array( 'User:Foo/bar.CSS', true ),
 +                      array( 'TEST-JS:Foo', false ),
 +                      array( 'TEST-JS:Foo.js', false ),
 +                      array( 'TEST-JS_TALK:Foo.js', true ),
                );
        }
  
@@@ -1,6 -1,5 +1,6 @@@
  <?php
  /**
 +* @group ContentHandler
  * @group Database
  * ^--- important, causes temporary tables to be used instead of the real database
  **/
@@@ -9,6 -8,27 +9,27 @@@ class WikiPageTest extends MediaWikiTes
  
        var $pages_to_delete;
  
 -
+       function  __construct( $name = null, array $data = array(), $dataName = '' ) {
+               parent::__construct( $name, $data, $dataName );
+               $this->tablesUsed = array_merge ( $this->tablesUsed,
+                                                 array( 'page',
+                                                      'revision',
+                                                      'text',
+                                                      'recentchanges',
+                                                      'logging',
+                                                      'page_props',
+                                                      'pagelinks',
+                                                      'categorylinks',
+                                                      'langlinks',
+                                                      'externallinks',
+                                                      'imagelinks',
+                                                      'templatelinks',
+                                                      'iwlinks' ) );
+       }
++      
        public function setUp() {
                $this->pages_to_delete = array();
        }
                if ( is_string( $page ) ) $page = Title::newFromText( $page );
                if ( $page instanceof Title ) $page = $this->newPage( $page );
  
 -              $page->doEdit( $text, "testing", EDIT_NEW );
 +              $content = ContentHandler::makeContent( $text, $page->getTitle(), $model );
 +              $page->doEditContent( $content, "testing", EDIT_NEW );
  
                return $page;
        }
  
 +      public function testDoEditContent() {
 +              $title = Title::newFromText( "WikiPageTest_testDoEditContent" );
 +
 +              $page = $this->newPage( $title );
 +
 +              $content = ContentHandler::makeContent( "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
 +                                                      . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "testing 1" );
 +
 +              $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
 +              $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
 +
 +              $id = $page->getId();
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $content = ContentHandler::makeContent( "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
 +                                                      . "Stet clita kasd [[gubergren]], no sea takimata sanctus est.",
 +                                                      $title );
 +
 +              $page->doEditContent( $content, "testing 2" );
 +
 +              # ------------------------
 +              $page = new WikiPage( $title );
 +
 +              $retrieved = $page->getContent();
 +              $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
 +
 +              # ------------------------
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'pagelinks', '*', array( 'pl_from' => $id ) );
 +              $n = $res->numRows();
 +              $res->free();
 +
 +              $this->assertEquals( 2, $n, 'pagelinks should contain two links from the page' );
 +      }
++      
        public function testDoEdit() {
                $title = Title::newFromText( "WikiPageTest_testDoEdit" );
  
                $this->assertEquals( $text, $page->getText() );
        }
  
 +      public function testDoQuickEditContent() {
 +              global $wgUser;
 +
 +              $page = $this->createPage( "WikiPageTest_testDoQuickEditContent", "original text" );
 +
 +              $content = ContentHandler::makeContent( "quick text", $page->getTitle() );
 +              $page->doQuickEditContent( $content, $wgUser, "testing q" );
 +
 +              # ---------------------
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertTrue( $content->equals( $page->getContent() ) );
 +      }
++      
        public function testDoDeleteArticle() {
                $page = $this->createPage( "WikiPageTest_testDoDeleteArticle", "[[original text]] foo" );
                $id = $page->getId();
                $page->doDeleteArticle( "testing deletion" );
  
                $this->assertFalse( $page->exists(), "WikiPage::exists should return false after page was deleted" );
 +              $this->assertNull( $page->getContent(), "WikiPage::getContent should return null after page was deleted" );
                $this->assertFalse( $page->getText(), "WikiPage::getText should return false after page was deleted" );
  
                $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
                $rev = $page->getRevision();
  
                $this->assertEquals( $page->getLatest(), $rev->getId() );
 -              $this->assertEquals( "some text", $rev->getText() );
 +              $this->assertEquals( "some text", $rev->getContent()->getNativeData() );
 +      }
 +
 +      public function testGetContent() {
 +              $page = $this->newPage( "WikiPageTest_testGetContent" );
 +
 +              $content = $page->getContent();
 +              $this->assertNull( $content );
 +
 +              # -----------------
 +              $this->createPage( $page, "some text" );
 +
 +              $content = $page->getContent();
 +              $this->assertEquals( "some text", $content->getNativeData() );
        }
  
        public function testGetText() {
                $this->assertEquals( "some text", $text );
        }
  
 -      
 +      public function testGetContentModel() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentModel", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( CONTENT_MODEL_JAVASCRIPT, $page->getContentModel() );
 +      }
 +
 +      public function testGetContentHandler() {
 +              $page = $this->createPage( "WikiPageTest_testGetContentHandler", "some text", CONTENT_MODEL_JAVASCRIPT );
 +
 +              $page = new WikiPage( $page->getTitle() );
 +              $this->assertEquals( 'JavaScriptContentHandler', get_class( $page->getContentHandler() ) );
 +      }
 +
        public function testExists() {
                $page = $this->newPage( "WikiPageTest_testExists" );
                $this->assertFalse( $page->exists() );
        public function testGetRedirectTarget( $title, $text, $target ) {
                $page = $this->createPage( $title, $text );
  
 +              # sanity check, because this test seems to fail for no reason for some people.
 +              $c = $page->getContent();
 +              $this->assertEquals( 'WikitextContent', get_class( $c ) );
++              
                # now, test the actual redirect
                $t = $page->getRedirectTarget();
                $this->assertEquals( $target, is_null( $t ) ? null : $t->getPrefixedText() );
                $wgArticleCountMethod = $mode;
  
                $page = $this->createPage( $title, $text );
 -              $editInfo = $page->prepareTextForEdit( $page->getText() );
 +              $editInfo = $page->prepareContentForEdit( $page->getContent() );
  
                $v = $page->isCountable();
                $w = $page->isCountable( $editInfo );
@@@ -531,18 -461,6 +551,18 @@@ more stuf
                $this->assertEquals( $expected, $text );
        }
  
 +      /**
 +       * @dataProvider dataReplaceSection
 +       */
 +      public function testReplaceSectionContent( $title, $text, $section, $with, $sectionTitle, $expected ) {
 +              $page = $this->createPage( $title, $text );
 +
 +              $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
 +              $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
 +
 +              $this->assertEquals( $expected, is_null( $c ) ? null : trim( $c->getNativeData() ) );
 +      }
++      
        /* @FIXME: fix this!
        public function testGetUndoText() {
                global $wgDiff3;
        */
  
        /**
-        * @group broken
-        *
-        * ^--- marked as broken, because it fails in jenkins, though it passes locally for everyone. or so it seems.
+        * @todo FIXME: this is a better rollback test than the one below, but it keeps failing in jenkins for some reason.
         */
-       public function testDoRollback() {
+       public function broken_testDoRollback() {
                $admin = new User();
                $admin->setName("Admin");
  
                $text = "one";
                $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
  
                $user1 = new User();
                $user1->setName( "127.0.1.11" );
                $text .= "\n\ntwo";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
  
                $user2 = new User();
                $user2->setName( "127.0.2.13" );
                $text .= "\n\nthree";
                $page = new WikiPage( $page->getTitle() );
 -              $page->doEdit( $text, "adding section three", 0, false, $user2 );
 +              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section three", 0, false, $user2 );
  
                # we are having issues with doRollback spuriously failing. apparently the last revision somehow goes missing
                # or not committed under some circumstances. so, make sure the last revision has the right user name.
  
                $page = new WikiPage( $page->getTitle() );
                $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
 -              $this->assertEquals( "one\n\ntwo", $page->getText() );
 +              $this->assertEquals( "one\n\ntwo", $page->getContent()->getNativeData() );
        }
  
 -              $page->doEdit( $text, "section one", EDIT_NEW, false, $admin );
+       /**
+        * @todo FIXME: the above rollback test is better, but it keeps failing in jenkins for some reason.
+        */
+       public function testDoRollback() {
+               $admin = new User();
+               $admin->setName("Admin");
+               $text = "one";
+               $page = $this->newPage( "WikiPageTest_testDoRollback" );
 -              $page->doEdit( $text, "adding section two", 0, false, $user1 );
++              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "section one", EDIT_NEW, false, $admin );
+               $rev1 = $page->getRevision();
+               $user1 = new User();
+               $user1->setName( "127.0.1.11" );
+               $text .= "\n\ntwo";
+               $page = new WikiPage( $page->getTitle() );
 -              $this->assertEquals( "one", $page->getText() );
++              $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), "adding section two", 0, false, $user1 );
+               # now, try the rollback
+               $admin->addGroup( "sysop" ); #XXX: make the test user a sysop...
+               $token = $admin->getEditToken( array( $page->getTitle()->getPrefixedText(), $user1->getName() ), null );
+               $errors = $page->doRollback( $user1->getName(), "testing revert", $token, false, $details, $admin );
+               if ( $errors ) {
+                       $this->fail( "Rollback failed:\n" . print_r( $errors, true ) . ";\n" . print_r( $details, true ) );
+               }
+               $page = new WikiPage( $page->getTitle() );
+               $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(), "rollback did not revert to the correct revision" );
++              $this->assertEquals( "one", $page->getContent()->getNativeData() );
+       }
        public function dataGetAutosummary( ) {
                return array(
                        array(
                        if ( !empty( $edit[1] ) ) $user->setName( $edit[1] );
                        else $user = $wgUser;
  
 -                      $page->doEdit( $edit[0], "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
 +                      $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
 +
 +                      $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
  
                        $c += 1;
                }
@@@ -1,6 -1,6 +1,9 @@@
  <?php
  
  /**
++ * @group medium
++ * ^---- causes phpunit to use a higher timeout threshold
++ * 
   * @group FileRepo
   * @group FileBackend
   */
@@@ -35,6 -35,7 +38,7 @@@ class FileBackendTest extends MediaWiki
                        $this->singleBackend = new FSFileBackend( array(
                                'name'        => 'localtesting',
                                'lockManager' => 'fsLockManager',
+                               #'parallelize' => 'implicit',
                                'containerPaths' => array(
                                        'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
                                        'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
@@@ -43,6 -44,7 +47,7 @@@
                $this->multiBackend = new FileBackendMultiWrite( array(
                        'name'        => 'localtesting',
                        'lockManager' => 'fsLockManager',
+                       'parallelize' => 'implicit',
                        'backends'    => array(
                                array(
                                        'name'          => 'localmutlitesting1',
                $this->tearDownFiles();
        }
  
-       function doTestStore( $op ) {
+       private function doTestStore( $op ) {
                $backendName = $this->backendClass();
  
                $source = $op['src'];
  
                $status = $this->backend->doOperation( $op );
  
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Store from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertEquals( true, $status->isOK(),
                        "Store from $source to $dest succeeded ($backendName)." );
                $this->assertEquals( array( 0 => true ), $status->success,
                        "Store from $source to $dest has proper 'success' field in Status ($backendName)." );
                $this->tearDownFiles();
        }
  
-       function doTestCopy( $op ) {
+       private function doTestCopy( $op ) {
                $backendName = $this->backendClass();
  
                $source = $op['src'];
  
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of file at $source succeeded ($backendName)." );
  
                if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
  
                $status = $this->backend->doOperation( $op );
  
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Copy from $source to $dest succeeded without warnings ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Copy from $source to $dest succeeded ($backendName)." );
  
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of file at $source succeeded ($backendName)." );
  
                if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
                }
  
                $status = $this->backend->doOperation( $op );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Move from $source to $dest succeeded without warnings ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Move from $source to $dest succeeded ($backendName)." );
                if ( $withSource ) {
                        $status = $this->backend->doOperation(
                                array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Creation of file at $source succeeded ($backendName)." );
                }
  
                $status = $this->backend->doOperation( $op );
                if ( $okStatus ) {
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Deletion of file at $source succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Deletion of file at $source succeeded ($backendName)." );
                if ( $alreadyExists ) {
                        $status = $this->backend->doOperation(
                                array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Creation of file at $dest succeeded ($backendName)." );
                }
  
                $status = $this->backend->doOperation( $op );
                if ( $okStatus ) {
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Creation of file at $dest succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Creation of file at $dest succeeded ($backendName)." );
                $this->tearDownFiles();
        }
  
-       public function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+       private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
                $backendName = $this->backendClass();
  
                $expContent = '';
                }
                $status = $this->backend->doOperations( $ops );
  
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of source files succeeded ($backendName)." );
  
                $dest = $params['dst'];
                // Combine the files into one
                $status = $this->backend->concatenate( $params );
                if ( $okStatus ) {
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Creation of concat file at $dest succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Creation of concat file at $dest succeeded ($backendName)." );
                if ( $alreadyExists ) {
                        $this->prepare( array( 'dir' => dirname( $path ) ) );
                        $status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) );
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Creation of file at $path succeeded ($backendName)." );
  
                        $size = $this->backend->getFileSize( array( 'src' => $path ) );
                $this->tearDownFiles();
        }
  
-       public function doTestGetFileContents( $source, $content ) {
+       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->assertEquals( array(), $status->errors,
+               $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)." );
                $this->tearDownFiles();
        }
  
-       public function doTestGetLocalCopy( $source, $content ) {
+       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->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of file at $source succeeded ($backendName)." );
  
                $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
  
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of file at $source succeeded ($backendName)." );
  
                $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
                );
        }
  
-       function doTestPrepareAndClean( $path, $isOK ) {
+       private function doTestPrepareAndClean( $path, $isOK ) {
                $backendName = $this->backendClass();
  
                $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
                if ( $isOK ) {
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Preparing dir $path succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Preparing dir $path succeeded ($backendName)." );
  
                $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
                if ( $isOK ) {
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Cleaning dir $path succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Cleaning dir $path succeeded ($backendName)." );
                );
                foreach ( $dirs as $dir ) {
                        $status = $this->prepare( array( 'dir' => $dir ) );
-                       $this->assertEquals( array(), $status->errors,
+                       $this->assertGoodStatus( $status,
                                "Preparing dir $dir succeeded without warnings ($backendName)." );
                }
  
  
                $status = $this->backend->clean(
                        array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
  
                foreach ( $dirs as $dir ) {
  
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestDoOperationsFailing();
+               $this->doTestDoOperations2();
                $this->tearDownFiles();
  
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
+               $this->doTestDoOperations2();
+               $this->tearDownFiles();
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
                $this->doTestDoOperationsFailing();
                $this->tearDownFiles();
  
-               // @TODO: test some cases where the ops should fail
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestDoOperationsFailing();
+               $this->tearDownFiles();
        }
  
-       function doTestDoOperations() {
+       private function doTestDoOperations() {
                $base = $this->baseStorePath();
  
                $fileA = "$base/unittest-cont1/a/b/fileA.txt";
                $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
                $this->prepare( array( 'dir' => dirname( $fileC ) ) );
                $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileD ) ) );
  
                $status = $this->backend->doOperations( array(
                        array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
                        // Does nothing
                ) );
  
-               $this->assertEquals( array(), $status->errors, "Operation batch succeeded" );
+               $this->assertGoodStatus( $status, "Operation batch succeeded" );
                $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
                $this->assertEquals( 13, count( $status->success ),
                        "Operation batch has correct success array" );
                        "Correct file SHA-1 of $fileC" );
        }
  
+       // concurrency orientated
+       function doTestDoOperations2() {
+               $base = $this->baseStorePath();
+               $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+               $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+               $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+               $tmpNameA = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+               file_put_contents( $tmpNameA, $fileAContents );
+               $tmpNameB = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+               file_put_contents( $tmpNameB, $fileBContents );
+               $tmpNameC = TempFSFile::factory( "unittests_", 'txt' )->getPath();
+               file_put_contents( $tmpNameC, $fileCContents );
+               $this->filesToPrune[] = $tmpNameA; # avoid file leaking
+               $this->filesToPrune[] = $tmpNameB; # avoid file leaking
+               $this->filesToPrune[] = $tmpNameC; # avoid file leaking
+               $fileA = "$base/unittest-cont1/a/b/fileA.txt";
+               $fileB = "$base/unittest-cont1/a/b/fileB.txt";
+               $fileC = "$base/unittest-cont1/a/b/fileC.txt";
+               $fileD = "$base/unittest-cont1/a/b/fileD.txt";
+               $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+               $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+               $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+               $status = $this->backend->doOperations( array(
+                       array( 'op' => 'store', 'src' => $tmpNameA, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       array( 'op' => 'store', 'src' => $tmpNameB, 'dst' => $fileB, 'overwrite' => 1 ),
+                       array( 'op' => 'store', 'src' => $tmpNameC, 'dst' => $fileC, 'overwrite' => 1 ),
+                       array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+                       array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+                       // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+                       array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+                       // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+                       array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+                       // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'null' ),
+                       // Does nothing
+               ) );
+               $this->assertGoodStatus( $status, "Operation batch succeeded" );
+               $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+               $this->assertEquals( 16, count( $status->success ),
+                       "Operation batch has correct success array" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+                       "File does not exist at $fileA" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+                       "File does not exist at $fileB" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+                       "File does not exist at $fileD" );
+               $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+                       "File exists at $fileC" );
+               $this->assertEquals( $fileBContents,
+                       $this->backend->getFileContents( array( 'src' => $fileC ) ),
+                       "Correct file contents of $fileC" );
+               $this->assertEquals( strlen( $fileBContents ),
+                       $this->backend->getFileSize( array( 'src' => $fileC ) ),
+                       "Correct file size of $fileC" );
+               $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+                       $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+                       "Correct file SHA-1 of $fileC" );
+       }
        function doTestDoOperationsFailing() {
                $base = $this->baseStorePath();
  
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
                $status = $this->backend->doOperations( $ops );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Creation of files succeeded with OK status ($backendName)." );
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
                $status = $this->backend->doOperations( $ops );
-               $this->assertEquals( array(), $status->errors,
+               $this->assertGoodStatus( $status,
                        "Creation of files succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Creation of files succeeded with OK status ($backendName)." );
                foreach ( $iter as $iter ) {} // no errors
        }
  
+       public function testLockCalls() {
+               $this->backend = $this->singleBackend;
+               $this->doTestLockCalls();
+       }
+       private function doTestLockCalls() {
+               $backendName = $this->backendClass();
+               for ( $i=0; $i<50; $i++ ) {
+                       $paths = array(
+                               "test1.txt",
+                               "test2.txt",
+                               "test3.txt",
+                               "subdir1",
+                               "subdir1", // duplicate
+                               "subdir1/test1.txt",
+                               "subdir1/test2.txt",
+                               "subdir2",
+                               "subdir2", // duplicate
+                               "subdir2/test3.txt",
+                               "subdir2/test4.txt",
+                               "subdir2/subdir",
+                               "subdir2/subdir/test1.txt",
+                               "subdir2/subdir/test2.txt",
+                               "subdir2/subdir/test3.txt",
+                               "subdir2/subdir/test4.txt",
+                               "subdir2/subdir/test5.txt",
+                               "subdir2/subdir/sub",
+                               "subdir2/subdir/sub/test0.txt",
+                               "subdir2/subdir/sub/120-px-file.txt",
+                       );
+                       $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+                       $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+                       $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+                       $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+               }
+       }
        // test helper wrapper for backend prepare() function
        private function prepare( array $params ) {
                $this->dirsToPrune[] = $params['dir'];
                $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 ) );
+                               $this->backend->delete( array( 'src' => "$base/$container/$file" ),
+                                       array( 'force' => 1, 'nonLocking' => 1 ) );
                        }
                }
        }
  
        private function recursiveClean( $dir ) {
-               do {
-                       if ( !$this->backend->clean( array( 'dir' => $dir ) )->isOK() ) {
-                               break;
-                       }
-               } while ( $dir = FileBackend::parentStoragePath( $dir ) );
+               $this->backend->clean( array( 'dir' => $dir, 'recursive' => 1 ) );
+       }
+       function assertGoodStatus( $status, $msg ) {
+               $this->assertEquals( print_r( array(), 1 ), print_r( $status->errors, 1 ), $msg );
        }
  
        function tearDown() {
@@@ -74,7 -74,7 +74,7 @@@ abstract class DumpTestCase extends Med
         *
         * Clears $wgUser, and reports errors from addDBData to PHPUnit
         */
-       function setUp() {
+       protected function setUp() {
                global $wgUser;
  
                parent::setUp();
                $wgUser = new User();
        }
  
+       /**
+        * Checks for test output consisting only of lines containing ETA announcements
+        */
+       function expectETAOutput() {
+               // Newer PHPUnits require assertion about the output using PHPUnit's own
+               // expectOutput[...] functions. However, the PHPUnit shipped prediactes
+               // do not allow to check /each/ line of the output using /readable/ REs.
+               // So we ...
+               //
+               // 1. ... add a dummy output checking to make PHPUnit not complain
+               //    about unchecked test output
+               $this->expectOutputRegex( '//' );
+               // 2. Do the real output checking on our own.
+               $lines = explode( "\n", $this->getActualOutput() );
+               $this->assertGreaterThan( 1, count( $lines ), "Minimal lines of produced output" );
+               $this->assertEquals( '', array_pop( $lines ), "Output ends in LF" );
+               $timestamp_re = "[0-9]{4}-[01][0-9]-[0-3][0-9] [0-2][0-9]:[0-5][0-9]:[0-6][0-9]";
+               foreach ( $lines as $line ) {
+                       $this->assertRegExp( "/$timestamp_re: .* \(ID [0-9]+\) [0-9]* pages .*, [0-9]* revs .*, ETA/", $line );
+               }
+       }
        /**
         * Step the current XML reader until node end of given name is found.
         *
                $this->skipWhitespace();
  
                $this->assertTextNode( "comment", $summary );
++              $this->skipWhitespace();
++
++              if ( $this->xml->name == "model" ) { // model tag is optional
++                      $this->assertTextNode( "model", CONTENT_MODEL_WIKITEXT ); //@todo: make this a test parameter
++                      $this->skipWhitespace();
++              }
++
++
++              if ( $this->xml->name == "format" ) { // format tag is optional
++                      $this->assertTextNode( "format", CONTENT_FORMAT_WIKITEXT ); //@todo: make this a test parameter
++                      $this->skipWhitespace();
++              }
  
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
index e3f780b,61a20f1..61a20f1
mode 100644,100755..100644
@@@ -94,8 -94,8 +94,8 @@@ require( RUN_MAINTENANCE_IF_MAIN )
  require_once( 'PHPUnit/Runner/Version.php' );
  
  if( PHPUnit_Runner_Version::id() !== '@package_version@'
-    && version_compare( PHPUnit_Runner_Version::id(), '3.5.0', '<' ) ) {
-       die( 'PHPUnit 3.5 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" );
+    && version_compare( PHPUnit_Runner_Version::id(), '3.6.7', '<' ) ) {
+       die( 'PHPUnit 3.6.7 or later required, you have ' . PHPUnit_Runner_Version::id() . ".\n" );
  }
  require_once( 'PHPUnit/Autoload.php' );