merged master
authordaniel <daniel.kinzler@wikimedia.de>
Wed, 20 Jun 2012 17:13:16 +0000 (19:13 +0200)
committerdaniel <daniel.kinzler@wikimedia.de>
Wed, 20 Jun 2012 17:13:16 +0000 (19:13 +0200)
Change-Id: I6cf08c09c7d9b38ecce0b2bbed61431939edd7d4

15 files changed:
1  2 
docs/hooks.txt
includes/Article.php
includes/AutoLoader.php
includes/EditPage.php
includes/Export.php
includes/Revision.php
includes/Title.php
includes/WikiPage.php
includes/parser/Parser.php
languages/Language.php
languages/messages/MessagesQqq.php
maintenance/checkBadRedirects.php
tests/phpunit/maintenance/DumpTestCase.php
tests/phpunit/maintenance/backupPrefetchTest.php
tests/phpunit/maintenance/backupTextPassTest.php

diff --combined docs/hooks.txt
@@@ -406,14 -406,9 +406,14 @@@ token types
  used to retrieve this type of tokens.
  
  'ArticleAfterFetchContent': after fetching content of an article from
 +the database. DEPRECATED, use ArticleAfterFetchContentObject instead.
 +$article: the article (object) being loaded from the database
 +&$content: the content (string) of the article
 +
 +'ArticleAfterFetchContentObject': after fetching content of an article from
  the database
  $article: the article (object) being loaded from the database
 -$content: the content (string) of the article
 +&$content: the content of the article, as a Content object
  
  'ArticleConfirmDelete': before writing the confirmation form for article
        deletion
@@@ -459,7 -454,7 +459,7 @@@ Wiki::articleFromTitle(
  $title: title (object) used to create the article object
  $article: article (object) that will be returned
  
 -'ArticleInsertComplete': After a new article is created
 +'ArticleInsertComplete': After a new article is created. DEPRECATED, use ArticleContentInsertComplete
  $article: WikiPage created
  $user: User creating the article
  $text: New content
@@@ -470,17 -465,6 +470,17 @@@ $section: (No longer used
  $flags: Flags passed to Article::doEdit()
  $revision: New Revision of the article
  
 +'ArticleContentInsertComplete': After a new article is created
 +$article: WikiPage created
 +$user: User creating the article
 +$content: New content as a Content object
 +$summary: Edit summary/comment
 +$isMinor: Whether or not the edit was marked as minor
 +$isWatch: (No longer used)
 +$section: (No longer used)
 +$flags: Flags passed to Article::doEdit()
 +$revision: New Revision of the article
 +
  'ArticleMergeComplete': after merging to article using Special:Mergehistory
  $targetTitle: target title (object)
  $destTitle: destination title (object)
@@@ -529,7 -513,7 +529,7 @@@ $user: the user who did the rollbac
  $revision: the revision the page was reverted back to
  $current: the reverted revision
  
 -'ArticleSave': before an article is saved
 +'ArticleSave': before an article is saved. DEPRECATED, use ArticleContentSave instead
  $article: the WikiPage (object) being saved
  $user: the user (object) saving the article
  $text: the new article text
@@@ -538,16 -522,7 +538,16 @@@ $isminor: minor fla
  $iswatch: watch flag
  $section: section #
  
 -'ArticleSaveComplete': After an article has been updated
 +'ArticleContentSave': before an article is saved.
 +$article: the WikiPage (object) being saved
 +$user: the user (object) saving the article
 +$content: the new article content, as a Content object
 +$summary: the article summary (comment)
 +$isminor: minor flag
 +$iswatch: watch flag
 +$section: section #
 +
 +'ArticleSaveComplete': After an article has been updated. DEPRECATED, use ArticleContentSaveComplete instead.
  $article: WikiPage modified
  $user: User performing the modification
  $text: New content
@@@ -560,19 -535,6 +560,19 @@@ $revision: New Revision of the articl
  $status: Status object about to be returned by doEdit()
  $baseRevId: the rev ID (or false) this edit was based on
  
 +'ArticleContentSaveComplete': After an article has been updated
 +$article: WikiPage modified
 +$user: User performing the modification
 +$content: New content, as a Content object
 +$summary: Edit summary/comment
 +$isMinor: Whether or not the edit was marked as minor
 +$isWatch: (No longer used)
 +$section: (No longer used)
 +$flags: Flags passed to Article::doEdit()
 +$revision: New Revision of the article
 +$status: Status object about to be returned by doEdit()
 +$baseRevId: the rev ID (or false) this edit was based on
 +
  'ArticleUndelete': When one or more revisions of an article are restored
  $title: Title corresponding to the article restored
  $create: Whether or not the restoration caused the page to be created
@@@ -599,19 -561,11 +599,19 @@@ object to both indicate that the outpu
  follwed an redirect
  $article: target article (object)
  
 -'ArticleViewCustom': allows to output the text of the article in a different format than wikitext
 +'ArticleViewCustom': allows to output the text of the article in a different format than wikitext.
 +DEPRECATED, use ArticleContentViewCustom instead.
 +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
  $text: text of the page
  $title: title of the page
  $output: reference to $wgOut
  
 +'ArticleContentViewCustom': allows to output the text of the article in a different format than wikitext.
 +Note that it is preferrable to implement proper handing for a custom data type using the ContentHandler facility.
 +$content: content of the page, as a Content object
 +$title: title of the page
 +$output: reference to $wgOut
 +
  'AuthPluginAutoCreate': Called when creating a local account for an user logged
  in from an external authentication method
  $user: User object created locally
@@@ -723,6 -677,10 +723,10 @@@ $output: OutputPage object in us
  'CategoryPageView': before viewing a categorypage in CategoryPage::view
  $catpage: CategoryPage instance
  
+ 'ChangePasswordForm': For extensions that need to add a field to the ChangePassword form
+ via the Preferences form
+ &$extraFields: An array of arrays that hold fields like would be passed to the pretty function.
  'ChangesListInsertArticleLink': Override or augment link to article in RC list.
  &$changesList: ChangesList instance.
  &$articlelink: HTML of link to article (already filled-in).
@@@ -739,16 -697,6 +743,16 @@@ the collation given in $collationName
  'ConfirmEmailComplete': Called after a user's email has been confirmed successfully
  $user: user (object) whose email is being confirmed
  
 +'ContentHandlerDefaultModelFor': Called when the default content model is determiend
 +for a given title. May be used to assign a different model for that title.
 +$title: the Title in question
 +&$model: the model name. Use with CONTENT_MODEL_XXX constants.
 +
 +'ContentHandlerForModelID': Called when a ContentHandler is requested for a given
 +cointent model name, but no entry for that model exists in $wgContentHandlers.
 +$modeName: the requested content model name
 +&$handler: set this to a ContentHandler object, if desired.
 +
  'ContribsPager::getQueryInfo': Before the contributions query is about to run
  &$pager: Pager object for contributions
  &$queryInfo: The query for the contribs Pager
@@@ -814,19 -762,12 +818,19 @@@ $section: Section being edite
  &$error: Error message to return
  $summary: Edit summary for page
  
 -'EditFilterMerged': Post-section-merge edit filter
 +'EditFilterMerged': Post-section-merge edit filter.
 +DEPRECATED, use EditFilterMergedContent instead.
  $editor: EditPage instance (object)
  $text: content of the edit box
  &$error: error message to return
  $summary: Edit summary for page
  
 +'EditFilterMergedContent': Post-section-merge edit filter
 +$editor: EditPage instance (object)
 +$content: content of the edit box, as a Content object
 +&$error: error message to return
 +$summary: Edit summary for page
 +
  'EditFormPreloadText': Allows population of the edit form when creating
  new pages
  &$text: Text to preload with
@@@ -889,28 -830,14 +893,28 @@@ $title: title of page being edite
  &$msg: localization message name, overridable. Default is either 'copyrightwarning' or 'copyrightwarning2'
  
  'EditPageGetDiffText': Allow modifying the wikitext that will be used in
 -"Show changes"
 +"Show changes". DEPRECATED. Use EditPageGetDiffContent instead.
 +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
  $editPage: EditPage object
  &$newtext: wikitext that will be used as "your version"
  
 -'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed
 +'EditPageGetDiffContent': Allow modifying the wikitext that will be used in
 +"Show changes".
 +Note that it is preferrable to implement diff handling for different data types using the ContentHandler facility.
 +$editPage: EditPage object
 +&$newtext: wikitext that will be used as "your version"
 +
 +'EditPageGetPreviewText': Allow modifying the wikitext that will be previewed.
 +DEPRECATED. Use EditPageGetPreviewContent instead.
 +Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
  $editPage: EditPage object
  &$toparse: wikitext that will be parsed
  
 +'EditPageGetPreviewContent': Allow modifying the wikitext that will be previewed.
 +Note that it is preferrable to implement previews for different data types using the COntentHandler facility.
 +$editPage: EditPage object
 +&$content: Content object to be previewed (may be replaced by hook function)
 +
  'EditPageNoSuchSection': When a section edit request is given for an non-existent section
  &$editpage: The current EditPage object
  &$res: the HTML of the error text
@@@ -1003,6 -930,14 +1007,14 @@@ $fileVersions: array of undeleted versi
  $user: user who performed the undeletion
  $reason: reason
  
+ 'FormatAutocomments': When an autocomment is formatted by the Linker
+  &$comment: Reference to the accumulated comment. Initially null, when set the default code will be skipped.
+  $pre: Initial part of the parsed comment before the call to the hook.
+  $auto: The extracted part of the parsed comment before the call to the hook.
+  $post: The final part of the parsed comment before the call to the hook.
+  $title: An optional title object used to links to sections. Can be null.
+  $local: Boolean indicating whether section links should refer to local page.
  'GetAutoPromoteGroups': When determining which autopromote groups a user
  is entitled to be in.
  &$user: user to promote.
@@@ -1734,8 -1669,7 +1746,8 @@@ $query : Original query
  'ShowMissingArticle': Called when generating the output for a non-existent page
  $article: The article object corresponding to the page
  
 -'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views
 +'ShowRawCssJs': Customise the output of raw CSS and JavaScript in page views.
 +DEPRECATED, use the ContentHandler facility to handle CSS and JavaScript!
  $text: Text being shown
  $title: Title of the custom script/stylesheet page
  $output: Current OutputPage object
@@@ -2346,13 -2280,6 +2358,13 @@@ One, and only one hook should set this
  &$opts: Options to use for the query
  &$join: Join conditions
  
 +'WikiPageDeletionUpdates': manipulate the list of DataUpdates to be applied when
 +      a page is deleted. Called in WikiPage::getDeletionUpdates().
 +      Note that updates specific to a content model should be provided by the
 +      respective ContentHandler's getDeletionUpdates() method.
 +$page: the WikiPage
 +&$updates: the array of DataUpdate objects. Hook function may want to add to it.
 +
  'wfShellWikiCmd': Called when generating a shell-escaped command line
        string to run a MediaWiki cli script.
  &$script: MediaWiki cli script path
diff --combined includes/Article.php
@@@ -57,17 -57,10 +57,17 @@@ class Article extends Page 
        public $mParserOptions;
  
        /**
 -       * Content of the revision we are working on
 +       * Text of the revision we are working on
         * @var string $mContent
         */
 -      var $mContent;                    // !<
 +      var $mContent;                    // !< #BC cruft
 +
 +      /**
 +       * Content of the revision we are working on
 +       * @var Content
 +       * @since 1.WD
 +       */
 +      var $mContentObject;              // !<
  
        /**
         * Is the content ($mContent) already loaded?
         * This function has side effects! Do not use this function if you
         * only want the real revision text if any.
         *
 +       * @deprecated in 1.WD; use getContentObject() instead
 +       *
         * @return string Return the text of this revision
         */
        public function getContent() {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = $this->getContentObject();
 +              return ContentHandler::getContentText( $content );
 +      }
 +
 +      /**
 +       * Returns a Content object representing the pages effective display content,
 +       * not necessarily the revision's content!
 +       *
 +       * Note that getContent/loadContent do not follow redirects anymore.
 +       * If you need to fetch redirectable content easily, try
 +       * the shortcut in WikiPage::getRedirectTarget()
 +       *
 +       * This function has side effects! Do not use this function if you
 +       * only want the real revision text if any.
 +       *
 +       * @return Content Return the content of this revision
 +       *
 +       * @since 1.WD
 +       *
 +       * @todo: FIXME: this should really be protected, all callers should be changed to use WikiPage::getContent() instead.
 +       */
 +      public function getContentObject() {
 +              global $wgUser;
                wfProfileIn( __METHOD__ );
  
                if ( $this->mPage->getID() === 0 ) {
                                if ( $text === false ) {
                                        $text = '';
                                }
 +
 +                              $content = ContentHandler::makeContent( $text, $this->getTitle() );
                        } else {
 -                              $text = wfMsgExt( $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
 +                              $content = new MessageContent( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', null, 'parsemag' );
                        }
                        wfProfileOut( __METHOD__ );
  
 -                      return $text;
 +                      return $content;
                } else {
 -                      $this->fetchContent();
 +                      $this->fetchContentObject();
                        wfProfileOut( __METHOD__ );
  
 -                      return $this->mContent;
 +                      return $this->mContentObject;
                }
        }
  
         * Get text of an article from database
         * Does *NOT* follow redirects.
         *
 +       * @protected
 +       * @note this is really internal functionality that should really NOT be used by other functions. For accessing
 +       *       article content, use the WikiPage class, especially WikiBase::getContent(). However, a lot of legacy code
 +       *       uses this method to retrieve page text from the database, so the function has to remain public for now.
 +       *
         * @return mixed string containing article contents, or false if null
 +       * @deprecated in 1.WD, use WikiPage::getContent() instead
         */
 -      function fetchContent() {
 -              if ( $this->mContentLoaded ) {
 +      function fetchContent() { #BC cruft!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              if ( $this->mContentLoaded && $this->mContent ) {
                        return $this->mContent;
                }
  
                wfProfileIn( __METHOD__ );
  
 +              $content = $this->fetchContentObject();
 +
 +              $this->mContent = ContentHandler::getContentText( $content ); #@todo: get rid of mContent everywhere!
 +              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ); #BC cruft, deprecated!
 +
 +              wfProfileOut( __METHOD__ );
 +
 +              return $this->mContent;
 +      }
 +
 +
 +      /**
 +       * Get text content object
 +       * Does *NOT* follow redirects.
 +       * TODO: when is this null?
 +       *
 +       * @note code that wants to retrieve page content from the database should use WikiPage::getContent().
 +       *
 +       * @return Content|null
 +       *
 +       * @since 1.WD
 +       */
 +      protected function fetchContentObject() {
 +              if ( $this->mContentLoaded ) {
 +                      return $this->mContentObject;
 +              }
 +
 +              wfProfileIn( __METHOD__ );
 +
                $this->mContentLoaded = true;
 +              $this->mContent = null;
  
                $oldid = $this->getOldID();
  
                # fails we'll have something telling us what we intended.
                $t = $this->getTitle()->getPrefixedText();
                $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
 -              $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
 +              $this->mContentObject = new MessageContent( 'missing-article', array($t, $d), array() ) ; // @todo: this isn't page content but a UI message. horrible.
  
                if ( $oldid ) {
                        # $this->mRevision might already be fetched by getOldIDFromRequest()
                        }
  
                        $this->mRevision = $this->mPage->getRevision();
 +
                        if ( !$this->mRevision ) {
                                wfDebug( __METHOD__ . " failed to retrieve current page, rev_id " . $this->mPage->getLatest() . "\n" );
                                wfProfileOut( __METHOD__ );
  
                // @todo FIXME: Horrible, horrible! This content-loading interface just plain sucks.
                // We should instead work with the Revision object when we need it...
 -              $this->mContent = $this->mRevision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
 +              $this->mContentObject = $this->mRevision->getContent( Revision::FOR_THIS_USER ); // Loads if user is allowed
                $this->mRevIdFetched = $this->mRevision->getId();
  
 -              wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) );
 +              wfRunHooks( 'ArticleAfterFetchContentObject', array( &$this, &$this->mContentObject ) );
  
                wfProfileOut( __METHOD__ );
  
 -              return $this->mContent;
 +              return $this->mContentObject;
        }
  
        /**
         * @return Revision|null
         */
        public function getRevisionFetched() {
 -              $this->fetchContent();
 +              $this->fetchContentObject();
  
                return $this->mRevision;
        }
                if ( $outputPage->isPrintable() ) {
                        $parserOptions->setIsPrintable( true );
                        $parserOptions->setEditSection( false );
-               } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit' ) ) {
+               } elseif ( !$this->isCurrent() || !$this->getTitle()->quickUserCan( 'edit', $user ) ) {
                        $parserOptions->setEditSection( false );
                }
  
                                        break;
                                case 3:
                                        # This will set $this->mRevision if needed
 -                                      $this->fetchContent();
 +                                      $this->fetchContentObject();
  
                                        # Are we looking at an old revision
                                        if ( $oldid && $this->mRevision ) {
                                                wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
                                                $this->showCssOrJsPage();
                                                $outputDone = true;
 -                                      } elseif( !wfRunHooks( 'ArticleViewCustom', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 +                                      } elseif( !wfRunHooks( 'ArticleContentViewCustom', array( $this->fetchContentObject(), $this->getTitle(), $outputPage ) ) ) {
 +                                              # Allow extensions do their own custom view for certain pages
 +                                              $outputDone = true;
 +                                      } elseif( Hooks::isRegistered( 'ArticleViewCustom' ) && !wfRunHooks( 'ArticleViewCustom', array( $this->fetchContent(), $this->getTitle(), $outputPage ) ) ) { #FIXME: fetchContent() is deprecated!
                                                # Allow extensions do their own custom view for certain pages
                                                $outputDone = true;
                                        } else {
 -                                              $text = $this->getContent();
 -                                              $rt = Title::newFromRedirectArray( $text );
 +                                              $content = $this->getContentObject();
 +                                              $rt = $content->getRedirectChain();
                                                if ( $rt ) {
                                                        wfDebug( __METHOD__ . ": showing redirect=no page\n" );
                                                        # Viewing a redirect page (e.g. with parameter redirect=no)
                                                        $outputPage->addHTML( $this->viewRedirect( $rt ) );
                                                        # Parse just to get categories, displaytitle, etc.
 -                                                      $this->mParserOutput = $wgParser->parse( $text, $this->getTitle(), $parserOptions );
 +                                                      $this->mParserOutput = $content->getParserOutput( $this->getTitle(), $oldid, $parserOptions, false );
                                                        $outputPage->addParserOutputNoText( $this->mParserOutput );
                                                        $outputDone = true;
                                                }
                                        # Run the parse, protected by a pool counter
                                        wfDebug( __METHOD__ . ": doing uncached parse\n" );
  
 +                                      // @todo: shouldn't we be passing $this->getPage() to PoolWorkArticleView instead of plain $this?
                                        $poolArticleView = new PoolWorkArticleView( $this, $parserOptions,
 -                                              $this->getRevIdFetched(), $useParserCache, $this->getContent() );
 +                                              $this->getRevIdFetched(), $useParserCache, $this->getContentObject(), $this->getContext() );
  
                                        if ( !$poolArticleView->execute() ) {
                                                $error = $poolArticleView->getError();
                $unhide = $request->getInt( 'unhide' ) == 1;
                $oldid = $this->getOldID();
  
 -              $de = new DifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +              $contentHandler = ContentHandler::getForTitle( $this->getTitle() );
 +              $de = $contentHandler->createDifferenceEngine( $this->getContext(), $oldid, $diff, $rcid, $purge, $unhide );
 +
                // DifferenceEngine directly fetched the revision:
                $this->mRevIdFetched = $de->mNewid;
                $de->showDiffPage( $diffOnly );
         * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
         * page views.
         */
 -      protected function showCssOrJsPage() {
 -              $dir = $this->getContext()->getLanguage()->getDir();
 -              $lang = $this->getContext()->getLanguage()->getCode();
 +      protected function showCssOrJsPage( $showCacheHint = true ) {
 +              global $wgOut;
  
 -              $outputPage = $this->getContext()->getOutput();
 -              $outputPage->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 -                      'clearyourcache' );
 +              if ( $showCacheHint ) {
 +                      $dir = $this->getContext()->getLanguage()->getDir();
 +                      $lang = $this->getContext()->getLanguage()->getCode();
 +
 +                      $wgOut->wrapWikiMsg( "<div id='mw-clearyourcache' lang='$lang' dir='$dir' class='mw-content-$dir'>\n$1\n</div>",
 +                              'clearyourcache' );
 +              }
  
                // Give hooks a chance to customise the output
 -              if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->getTitle(), $outputPage ) ) ) {
 -                      // Wrap the whole lot in a <pre> and don't parse
 -                      $m = array();
 -                      preg_match( '!\.(css|js)$!u', $this->getTitle()->getText(), $m );
 -                      $outputPage->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
 -                      $outputPage->addHTML( htmlspecialchars( $this->mContent ) );
 -                      $outputPage->addHTML( "\n</pre>\n" );
 +              if ( !Hooks::isRegistered('ShowRawCssJs') || wfRunHooks( 'ShowRawCssJs', array( $this->fetchContent(), $this->getTitle(), $wgOut ) ) ) { #FIXME: fetchContent() is deprecated
 +                      $po = $this->mContentObject->getParserOutput( $this->getTitle() );
 +                      $wgOut->addHTML( $po->getText() );
                }
        }
  
                $user = $this->getContext()->getUser();
                $rcid = $request->getVal( 'rcid' );
  
-               if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol' ) ) {
+               if ( !$rcid || !$this->getTitle()->quickUserCan( 'patrol', $user ) ) {
                        return;
                }
  
                } elseif ( $this->getTitle()->getNamespace() === NS_MEDIAWIKI ) {
                        // Use the default message text
                        $text = $this->getTitle()->getDefaultMessageText();
+               } elseif ( $this->getTitle()->quickUserCan( 'create', $this->getContext()->getUser() )
+                       && $this->getTitle()->quickUserCan( 'edit', $this->getContext()->getUser() )
+               ) {
+                       $text = wfMsgNoTrans( 'noarticletext' );
                } else {
-                       $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 = wfMsgNoTrans( 'noarticletext' );
-                       } else {
-                               $text = wfMsgNoTrans( 'noarticletext-nopermission' );
-                       }
+                       $text = wfMsgNoTrans( 'noarticletext-nopermission' );
                }
                $text = "<div class='noarticletext'>\n$text\n</div>";
  
  
                $current = ( $oldid == $this->mPage->getLatest() );
                $language = $this->getContext()->getLanguage();
-               $td = $language->timeanddate( $timestamp, true );
-               $tddate = $language->date( $timestamp, true );
-               $tdtime = $language->time( $timestamp, true );
+               $user = $this->getContext()->getUser();
+               $td = $language->userTimeAndDate( $timestamp, $user );
+               $tddate = $language->userDate( $timestamp, $user );
+               $tdtime = $language->userTime( $timestamp, $user );
  
                # Show user links if allowed to see them. If hidden, then show them only if requested...
                $userlinks = Linker::revUserTools( $revision, !$unhide );
                                array( 'known', 'noclasses' )
                        );
  
-               $cdel = Linker::getRevDeleteLink( $this->getContext()->getUser(), $revision, $this->getTitle() );
+               $cdel = Linker::getRevDeleteLink( $user, $revision, $this->getTitle() );
                if ( $cdel !== '' ) {
                        $cdel .= ' ';
                }
                // Generate deletion reason
                $hasHistory = false;
                if ( !$reason ) {
 -                      $reason = $this->generateReason( $hasHistory );
 +                      try {
 +                              $reason = $this->generateReason( $hasHistory );
 +                      } catch (MWException $e) {
 +                              # if a page is horribly broken, we still want to be able to delete it. so be lenient about errors here.
 +                              wfDebug("Error while building auto delete summary: $e");
 +                              $reason = '';
 +                      }
                }
  
                // If the page has a history, insert a warning
                        }
                }
  
-               return $this->confirmDelete( $reason );
+               $this->confirmDelete( $reason );
        }
  
        /**
         * @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
@@@ -176,6 -176,7 +176,7 @@@ $wgAutoloadLocalClasses = array
        'MWException' => 'includes/Exception.php',
        'MWExceptionHandler' => 'includes/Exception.php',
        'MWFunction' => 'includes/MWFunction.php',
+       'MWHookException' => 'includes/Hooks.php',
        'MWHttpRequest' => 'includes/HttpFunctions.php',
        'MWInit' => 'includes/Init.php',
        'MWNamespace' => 'includes/Namespace.php',
        'ReplacementArray' => 'includes/StringUtils.php',
        'Replacer' => 'includes/StringUtils.php',
        'ReverseChronologicalPager' => 'includes/Pager.php',
+       'RevisionItem' => 'includes/RevisionList.php',
        'RevisionItemBase' => 'includes/RevisionList.php',
        'RevisionListBase' => 'includes/RevisionList.php',
        'Revision' => 'includes/Revision.php',
        'TitleArrayFromResult' => 'includes/TitleArray.php',
        'ThrottledError' => 'includes/Exception.php',
        'UnlistedSpecialPage' => 'includes/SpecialPage.php',
+       'UploadSourceAdapter' => 'includes/Import.php',
        'UppercaseCollation' => 'includes/Collation.php',
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
        'Xml' => 'includes/Xml.php',
        'XmlDumpWriter' => 'includes/Export.php',
        'XmlJsCode' => 'includes/Xml.php',
+       'XMLReader2' => 'includes/Import.php',
        'XmlSelect' => 'includes/Xml.php',
        'XmlTypeCheck' => 'includes/XmlTypeCheck.php',
        'ZhClient' => 'includes/ZhClient.php',
        'ZipDirectoryReader' => 'includes/ZipDirectoryReader.php',
+       'ZipDirectoryReaderError' => 'includes/ZipDirectoryReader.php',
  
 +      # content handler
 +      'Content' => 'includes/Content.php',
 +      'AbstractContent' => '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',
        'DatabaseSqlite' => 'includes/db/DatabaseSqlite.php',
        'DatabaseSqliteStandalone' => 'includes/db/DatabaseSqlite.php',
        'DatabaseType' => 'includes/db/Database.php',
+       'DBAccessError' => 'includes/db/LBFactory.php',
        'DBConnectionError' => 'includes/db/DatabaseError.php',
        'DBError' => 'includes/db/DatabaseError.php',
        'DBObject' => 'includes/db/DatabaseUtility.php',
        'Field' => 'includes/db/DatabaseUtility.php',
        'IBM_DB2Blob' => 'includes/db/DatabaseIbm_db2.php',
        'IBM_DB2Field' => 'includes/db/DatabaseIbm_db2.php',
+       'IBM_DB2Helper' => 'includes/db/DatabaseIbm_db2.php',
+       'IBM_DB2Result' => 'includes/db/DatabaseIbm_db2.php',
        'LBFactory' => 'includes/db/LBFactory.php',
+       'LBFactory_Fake' => 'includes/db/LBFactory.php',
        'LBFactory_Multi' => 'includes/db/LBFactory_Multi.php',
        'LBFactory_Simple' => 'includes/db/LBFactory.php',
        'LBFactory_Single' => 'includes/db/LBFactory_Single.php',
        'LoadMonitor' => 'includes/db/LoadMonitor.php',
        'LoadMonitor_MySQL' => 'includes/db/LoadMonitor.php',
        'LoadMonitor_Null' => 'includes/db/LoadMonitor.php',
+       'MssqlField' => 'includes/db/DatabaseMssql.php',
+       'MssqlResult' => 'includes/db/DatabaseMssql.php',
        'MySQLField' => 'includes/db/DatabaseMysql.php',
        'MySQLMasterPos' => 'includes/db/DatabaseMysql.php',
        'ORAField' => 'includes/db/DatabaseOracle.php',
        'ORAResult' => 'includes/db/DatabaseOracle.php',
+       'ORMIterator' => 'includes/db/ORMIterator.php',
        'ORMResult' => 'includes/db/ORMResult.php',
        'ORMRow' => 'includes/db/ORMRow.php',
        'ORMTable' => 'includes/db/ORMTable.php',
        'PostgresField' => 'includes/db/DatabasePostgres.php',
+       'PostgresTransactionState' => 'includes/db/DatabasePostgres.php',
        'ResultWrapper' => 'includes/db/DatabaseUtility.php',
+       'SavepointPostgres' => 'includes/db/DatabasePostgres.php',
        'SQLiteField' => 'includes/db/DatabaseSqlite.php',
  
        # includes/debug
        'LocalRepo' => 'includes/filerepo/LocalRepo.php',
        'NullRepo' => 'includes/filerepo/NullRepo.php',
        'RepoGroup' => 'includes/filerepo/RepoGroup.php',
+       'TempFileRepo' => 'includes/filerepo/FileRepo.php',
  
        # includes/filerepo/file
        'ArchivedFile' => 'includes/filerepo/file/ArchivedFile.php',
  
        # includes/libs
        'CSSJanus' => 'includes/libs/CSSJanus.php',
+       'CSSJanus_Tokenizer' => 'includes/libs/CSSJanus.php',
        'CSSMin' => 'includes/libs/CSSMin.php',
        'HttpStatus' => 'includes/libs/HttpStatus.php',
        'IEContentAnalyzer' => 'includes/libs/IEContentAnalyzer.php',
        'IEUrlExtension' => 'includes/libs/IEUrlExtension.php',
        'JavaScriptMinifier' => 'includes/libs/JavaScriptMinifier.php',
+       'JSCompilerContext' => 'includes/libs/jsminplus.php',
        'JSMinPlus' => 'includes/libs/jsminplus.php',
+       'JSNode' => 'includes/libs/jsminplus.php',
        'JSParser' => 'includes/libs/jsminplus.php',
+       'JSToken' => 'includes/libs/jsminplus.php',
+       'JSTokenizer' => 'includes/libs/jsminplus.php',
  
        # includes/logging
        'DatabaseLogEntry' => 'includes/logging/LogEntry.php',
        'PNGMetadataExtractor' => 'includes/media/PNGMetadataExtractor.php',
        'SvgHandler' => 'includes/media/SVG.php',
        'SVGMetadataExtractor' => 'includes/media/SVGMetadataExtractor.php',
+       'SVGReader' => 'includes/media/SVGMetadataExtractor.php',
        'ThumbnailImage' => 'includes/media/MediaTransformOutput.php',
        'TiffHandler' => 'includes/media/Tiff.php',
        'TransformParameterError' => 'includes/media/MediaTransformOutput.php',
        'LinkHolderArray' => 'includes/parser/LinkHolderArray.php',
        'LinkMarkerReplacer' => 'includes/parser/Parser_LinkHooks.php',
        'MWTidy' => 'includes/parser/Tidy.php',
+       'MWTidyWrapper' => 'includes/parser/Tidy.php',
        'PPCustomFrame_DOM' => 'includes/parser/Preprocessor_DOM.php',
        'PPCustomFrame_Hash' => 'includes/parser/Preprocessor_Hash.php',
        'PPCustomFrame_HipHop' => 'includes/parser/Preprocessor_HipHop.hphp',
        # includes/revisiondelete
        'RevDel_ArchivedFileItem' => 'includes/revisiondelete/RevisionDelete.php',
        'RevDel_ArchivedFileList' => 'includes/revisiondelete/RevisionDelete.php',
+       'RevDel_ArchivedRevisionItem' => 'includes/revisiondelete/RevisionDelete.php',
        'RevDel_ArchiveItem' => 'includes/revisiondelete/RevisionDelete.php',
        'RevDel_ArchiveList' => 'includes/revisiondelete/RevisionDelete.php',
        'RevDel_FileItem' => 'includes/revisiondelete/RevisionDelete.php',
        'RevisionDeleteUser' => 'includes/revisiondelete/RevisionDeleteUser.php',
  
        # includes/search
+       'MssqlSearchResultSet' => 'includes/search/SearchMssql.php',
        'MySQLSearchResultSet' => 'includes/search/SearchMySQL.php',
        'PostgresSearchResult' => 'includes/search/SearchPostgres.php',
        'PostgresSearchResultSet' => 'includes/search/SearchPostgres.php',
        'SearchIBM_DB2' => 'includes/search/SearchIBM_DB2.php',
        'SearchMssql' => 'includes/search/SearchMssql.php',
        'SearchMySQL' => 'includes/search/SearchMySQL.php',
+       'SearchNearMatchResultSet' => 'includes/search/SearchEngine.php',
        'SearchOracle' => 'includes/search/SearchOracle.php',
        'SearchPostgres' => 'includes/search/SearchPostgres.php',
        'SearchResult' => 'includes/search/SearchEngine.php',
        'AncientPagesPage' => 'includes/specials/SpecialAncientpages.php',
        'BlockListPager' => 'includes/specials/SpecialBlockList.php',
        'BrokenRedirectsPage' => 'includes/specials/SpecialBrokenRedirects.php',
+       'CategoryPager' => 'includes/specials/SpecialCategories.php',
        'ContribsPager' => 'includes/specials/SpecialContributions.php',
        'DBLockForm' => 'includes/specials/SpecialLockdb.php',
        'DBUnlockForm' => 'includes/specials/SpecialUnlockdb.php',
        'DeletedContributionsPage' => 'includes/specials/SpecialDeletedContributions.php',
        'DisambiguationsPage' => 'includes/specials/SpecialDisambiguations.php',
        'DoubleRedirectsPage' => 'includes/specials/SpecialDoubleRedirects.php',
+       'EditWatchlistCheckboxSeriesField' => 'includes/specials/SpecialEditWatchlist.php',
+       'EditWatchlistNormalHTMLForm' => 'includes/specials/SpecialEditWatchlist.php',
        'EmailConfirmation' => 'includes/specials/SpecialConfirmemail.php',
        'EmailInvalidation' => 'includes/specials/SpecialConfirmemail.php',
        'FewestrevisionsPage' => 'includes/specials/SpecialFewestrevisions.php',
        'FileDuplicateSearchPage' => 'includes/specials/SpecialFileDuplicateSearch.php',
        'HTMLBlockedUsersItemSelect' => 'includes/specials/SpecialBlockList.php',
+       'ImageListPager' => 'includes/specials/SpecialListfiles.php',
        'ImportReporter' => 'includes/specials/SpecialImport.php',
        'IPBlockForm' => 'includes/specials/SpecialBlock.php',
        'LinkSearchPage' => 'includes/specials/SpecialLinkSearch.php',
        'LoginForm' => 'includes/specials/SpecialUserlogin.php',
        'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php',
        'LongPagesPage' => 'includes/specials/SpecialLongpages.php',
+       'MergeHistoryPager' => 'includes/specials/SpecialMergeHistory.php',
        'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php',
        'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php',
        'MostimagesPage' => 'includes/specials/SpecialMostimages.php',
        'MostlinkedTemplatesPage' => 'includes/specials/SpecialMostlinkedtemplates.php',
        'MostrevisionsPage' => 'includes/specials/SpecialMostrevisions.php',
        'MovePageForm' => 'includes/specials/SpecialMovepage.php',
+       'NewFilesPager' => 'includes/specials/SpecialNewimages.php',
        'NewPagesPager' => 'includes/specials/SpecialNewpages.php',
        'PageArchive' => 'includes/specials/SpecialUndelete.php',
        'PopularPagesPage' => 'includes/specials/SpecialPopularpages.php',
+       'ProtectedPagesPager' => 'includes/specials/SpecialProtectedpages.php',
+       'ProtectedTitlesPager' => 'includes/specials/SpecialProtectedtitles.php',
        'RandomPage' => 'includes/specials/SpecialRandompage.php',
        'ShortPagesPage' => 'includes/specials/SpecialShortpages.php',
        'SpecialActiveUsers' => 'includes/specials/SpecialActiveusers.php',
        'SpecialUnlockdb' => 'includes/specials/SpecialUnlockdb.php',
        'SpecialUpload' => 'includes/specials/SpecialUpload.php',
        'SpecialUploadStash' => 'includes/specials/SpecialUploadStash.php',
+       'SpecialUploadStashTooLargeException' => 'includes/specials/SpecialUploadStash.php',
        'SpecialUserlogout' => 'includes/specials/SpecialUserlogout.php',
        'SpecialVersion' => 'includes/specials/SpecialVersion.php',
        'SpecialWatchlist' => 'includes/specials/SpecialWatchlist.php',
-       'SpecialWhatlinkshere' => 'includes/specials/SpecialWhatlinkshere.php',
+       'SpecialWhatLinksHere' => 'includes/specials/SpecialWhatlinkshere.php',
        'UncategorizedCategoriesPage' => 'includes/specials/SpecialUncategorizedcategories.php',
        'UncategorizedImagesPage' => 'includes/specials/SpecialUncategorizedimages.php',
        'UncategorizedPagesPage' => 'includes/specials/SpecialUncategorizedpages.php',
        'UnusedimagesPage' => 'includes/specials/SpecialUnusedimages.php',
        'UnusedtemplatesPage' => 'includes/specials/SpecialUnusedtemplates.php',
        'UnwatchedpagesPage' => 'includes/specials/SpecialUnwatchedpages.php',
+       'UploadChunkFileException' => 'includes/upload/UploadFromChunks.php',
+       'UploadChunkZeroLengthFileException' => 'includes/upload/UploadFromChunks.php',
        'UploadForm' => 'includes/specials/SpecialUpload.php',
        'UploadSourceField' => 'includes/specials/SpecialUpload.php',
        'UserrightsPage' => 'includes/specials/SpecialUserrights.php',
        'UploadStashNoSuchKeyException' => 'includes/upload/UploadStash.php',
  
        # languages
+       'ConverterRule' => 'languages/LanguageConverter.php',
        'FakeConverter' => 'languages/Language.php',
        'Language' => 'languages/Language.php',
        'LanguageConverter' => 'languages/LanguageConverter.php',
  
        # maintenance/language
        'csvStatsOutput' => 'maintenance/language/StatOutputs.php',
+       'extensionLanguages' => 'maintenance/language/languages.inc',
        'languages' => 'maintenance/language/languages.inc',
        'MessageWriter' => 'maintenance/language/writeMessagesArray.inc',
        'statsOutput' => 'maintenance/language/StatOutputs.php',
  
        # mw-config
        'InstallerOverrides' => 'mw-config/overrides.php',
+       'MyLocalSettingsGenerator' => 'mw-config/overrides.php',
  
        # tests
        'DbTestPreviewer' => 'tests/testHelpers.inc',
        'DbTestRecorder' => 'tests/testHelpers.inc',
+       'DelayedParserTest' => 'tests/testHelpers.inc',
        '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',
-       'ParserTestStaticParserHook' => 'tests/parser/parserTestsStaticParserHook.php',
  
        # tests/selenium
        'Selenium' => 'tests/selenium/Selenium.php',
        'SeleniumTestListener' => 'tests/selenium/SeleniumTestListener.php',
        'SeleniumTestSuite' => 'tests/selenium/SeleniumTestSuite.php',
        'SeleniumConfig' => 'tests/selenium/SeleniumConfig.php',
+       # skins
+       'CologneBlueTemplate' => 'skins/CologneBlue.php',
+       'ModernTemplate' => 'skins/Modern.php',
+       'MonoBookTemplate' => 'skins/MonoBook.php',
+       'NostalgiaTemplate' => 'skins/Nostalgia.php',
+       'SkinChick' => 'skins/Chick.php',
+       'SkinCologneBlue' => 'skins/CologneBlue.php',
+       'SkinModern' => 'skins/Modern.php',
+       'SkinMonoBook' => 'skins/MonoBook.php',
+       'SkinMySkin' => 'skins/MySkin.php',
+       'SkinNostalgia' => 'skins/Nostalgia.php',
+       'SkinSimple' => 'skins/Simple.php',
+       'SkinStandard' => 'skins/Standard.php',
+       'SkinVector' => 'skins/Vector.php',
+       'StandardTemplate' => 'skins/Standard.php',
+       'VectorTemplate' => 'skins/Vector.php',
  );
  
  class AutoLoader {
diff --combined includes/EditPage.php
@@@ -155,11 -155,6 +155,11 @@@ class EditPage 
         */
        const AS_IMAGE_REDIRECT_LOGGED     = 234;
  
 +      /**
 +       * Status: can't parse content
 +       */
 +      const AS_PARSE_ERROR                = 240;
 +
        /**
         * HTML id and name for the beginning of the edit form.
         */
        var $textbox1 = '', $textbox2 = '', $summary = '', $nosummary = false;
        var $edittime = '', $section = '', $sectiontitle = '', $starttime = '';
        var $oldid = 0, $editintro = '', $scrolltop = null, $bot = true;
 +      var $content_model = null, $content_format = null;
  
        # Placeholders for text injection by hooks (must be HTML)
        # extensions should take care to _append_ to the present value
        public $editFormTextBottom = '';
        public $editFormTextAfterContent = '';
        public $previewTextAfterContent = '';
 -      public $mPreloadText = '';
 +      public $mPreloadContent = null;
  
        /* $didSave should be set to true whenever an article was succesfully altered. */
        public $didSave = false;
        public 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'] : '';
                                        '</div>';
                                return true;
                }
-               return false;
        }
  
        /**
  
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
 -                      Title::newFromRedirect( $this->textbox1 ) instanceof Title &&
 +                      Title::newFromRedirect( $this->textbox1 ) instanceof Title && #FIXME: use content handler to check for redirect
                        !$wgUser->isAllowed( 'upload' ) ) {
                                $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
                $this->mArticle->loadPageData( 'forupdate' );
                $new = !$this->mArticle->exists();
  
 -              if ( $new ) {
 -                      // Late check for create permission, just in case *PARANOIA*
 -                      if ( !$this->mTitle->userCan( 'create' ) ) {
 -                              $status->fatal( 'nocreatetext' );
 -                              $status->value = self::AS_NO_CREATE_PERMISSION;
 -                              wfDebug( __METHOD__ . ": no create permission\n" );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +              try {
 +                      if ( $new ) {
 +                              // Late check for create permission, just in case *PARANOIA*
 +                              if ( !$this->mTitle->userCan( 'create' ) ) {
 +                                      $status->fatal( 'nocreatetext' );
 +                                      $status->value = self::AS_NO_CREATE_PERMISSION;
 +                                      wfDebug( __METHOD__ . ": no create permission\n" );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      # Don't save a new article if it's blank.
 -                      if ( $this->textbox1 == '' ) {
 -                              $status->setResult( false, self::AS_BLANK_ARTICLE );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              # Don't save a new article if it's blank.
 +                              if ( $this->textbox1 == '' ) {
 +                                      $status->setResult( false, self::AS_BLANK_ARTICLE );
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $this->textbox1, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              }
  
 -                      $text = $this->textbox1;
 -                      $result['sectionanchor'] = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->sectiontitle ) . "\n\n" . $text;
 -
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 -                                      }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      // Insert the section title above the content.
 -                                      $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $text;
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                                      // Jump to the new section
 -                                      $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                              $result['sectionanchor'] = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
 +
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              // Insert the section title above the content.
 +                                              $content = $content->addSectionHeader( $this->sectiontitle );
  
 -                                      // Create a link to the new section from the edit summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                              // Jump to the new section
 +                                              $result['sectionanchor'] = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +
 +                                              // Create a link to the new section from the edit summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
                                }
 -                      }
  
 -                      $status->value = self::AS_SUCCESS_NEW_ARTICLE;
 +                              $status->value = self::AS_SUCCESS_NEW_ARTICLE;
  
 -              } else {
 +                      } else { # not $new
  
 -                      # Article exists. Check for edit conflict.
 -                      $timestamp = $this->mArticle->getTimestamp();
 -                      wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +                              # Article exists. Check for edit conflict.
  
 -                      if ( $timestamp != $this->edittime ) {
 -                              $this->isConflict = true;
 -                              if ( $this->section == 'new' ) {
 -                                      if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 -                                              $this->mArticle->getComment() == $this->summary ) {
 -                                              // Probably a duplicate submission of a new comment.
 -                                              // This can happen when squid resends a request after
 -                                              // a timeout but the first one actually went through.
 -                                              wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 -                                      } else {
 -                                              // New comment; suppress conflict.
 +                              $this->mArticle->clear(); # Force reload of dates, etc.
 +                              $timestamp = $this->mArticle->getTimestamp();
 +
 +                              wfDebug( "timestamp: {$timestamp}, edittime: {$this->edittime}\n" );
 +
 +                              if ( $timestamp != $this->edittime ) {
 +                                      $this->isConflict = true;
 +                                      if ( $this->section == 'new' ) {
 +                                              if ( $this->mArticle->getUserText() == $wgUser->getName() &&
 +                                                      $this->mArticle->getComment() == $this->summary ) {
 +                                                      // Probably a duplicate submission of a new comment.
 +                                                      // This can happen when squid resends a request after
 +                                                      // a timeout but the first one actually went through.
 +                                                      wfDebug( __METHOD__ . ": duplicate new section submission; trigger edit conflict!\n" );
 +                                              } else {
 +                                                      // New comment; suppress conflict.
 +                                                      $this->isConflict = false;
 +                                                      wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
 +                                              }
 +                                      } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 +                                              # Suppress edit conflict with self, except for section edits where merging is required.
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
                                                $this->isConflict = false;
 -                                              wfDebug( __METHOD__ . ": conflict suppressed; new section\n" );
                                        }
 -                              } elseif ( $this->section == '' && $this->userWasLastToEdit( $wgUser->getId(), $this->edittime ) ) {
 -                                      # Suppress edit conflict with self, except for section edits where merging is required.
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, same user.\n" );
 -                                      $this->isConflict = false;
                                }
 -                      }
 -
 -                      // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 -                      // backwards compatibility with old forms/bots).
 -                      if ( $this->sectiontitle !== '' ) {
 -                              $sectionTitle = $this->sectiontitle;
 -                      } else {
 -                              $sectionTitle = $this->summary;
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle, $this->edittime );
 -                      } else {
 -                              wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 -                              $text = $this->mArticle->replaceSection( $this->section, $this->textbox1, $sectionTitle );
 -                      }
 -                      if ( is_null( $text ) ) {
 -                              wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 -                              $this->isConflict = true;
 -                              $text = $this->textbox1; // do not try to merge here!
 -                      } elseif ( $this->isConflict ) {
 -                              # Attempt merge
 -                              if ( $this->mergeChangesInto( $text ) ) {
 -                                      // Successful merge! Maybe we should tell the user the good news?
 -                                      $this->isConflict = false;
 -                                      wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                              // If sectiontitle is set, use it, otherwise use the summary as the section title (for
 +                              // backwards compatibility with old forms/bots).
 +                              if ( $this->sectiontitle !== '' ) {
 +                                      $sectionTitle = $this->sectiontitle;
                                } else {
 -                                      $this->section = '';
 -                                      $this->textbox1 = $text;
 -                                      wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      $sectionTitle = $this->summary;
                                }
 -                      }
  
 -                      if ( $this->isConflict ) {
 -                              $status->setResult( false, self::AS_CONFLICT_DETECTED );
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              $textbox_content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +                              $content = null;
  
 -                      // Run post-section-merge edit filter
 -                      if ( !wfRunHooks( 'EditFilterMerged', array( $this, $text, &$this->hookError, $this->summary ) ) ) {
 -                              # Error messages etc. could be handled within the hook...
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      } elseif ( $this->hookError != '' ) {
 -                              # ...or the hook could be expecting us to produce an error
 -                              $status->fatal( 'hookaborted' );
 -                              $status->value = self::AS_HOOK_ERROR_EXPECTED;
 -                              wfProfileOut( __METHOD__ );
 -                              return $status;
 -                      }
 +                              if ( $this->isConflict ) {
 +                                      wfDebug( __METHOD__ . ": conflict! getting section '$this->section' for time '$this->edittime' (article time '{$timestamp}')\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle, $this->edittime );
 +                              } else {
 +                                      wfDebug( __METHOD__ . ": getting section '$this->section'\n" );
 +                                      $content = $this->mArticle->replaceSectionContent( $this->section, $textbox_content, $sectionTitle );
 +                              }
  
 -                      # Handle the user preference to force summaries here, but not for null edits
 -                      if ( $this->section != 'new' && !$this->allowBlankSummary
 -                              && $this->getOriginalContent() != $text
 -                              && !Title::newFromRedirect( $text ) ) # check if it's not a redirect
 -                      {
 -                              if ( md5( $this->summary ) == $this->autoSumm ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' );
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 -                                      wfProfileOut( __METHOD__ );
 -                                      return $status;
 +                              if ( is_null( $content ) ) {
 +                                      wfDebug( __METHOD__ . ": activating conflict; section replace failed.\n" );
 +                                      $this->isConflict = true;
 +                                      $content = $textbox_content; // do not try to merge here!
 +                              } elseif ( $this->isConflict ) {
 +                                      # Attempt merge
 +                                      if ( $this->mergeChangesIntoContent( $textbox_content ) ) {
 +                                              // Successful merge! Maybe we should tell the user the good news?
 +                                              $this->isConflict = false;
 +                                              $content = $textbox_content;
 +                                              wfDebug( __METHOD__ . ": Suppressing edit conflict, successful merge.\n" );
 +                                      } else {
 +                                              $this->section = '';
 +                                              #$this->textbox1 = $text; #redundant, nothing to do here?
 +                                              wfDebug( __METHOD__ . ": Keeping edit conflict, failed merge.\n" );
 +                                      }
                                }
 -                      }
  
 -                      # And a similar thing for new sections
 -                      if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 -                              if ( trim( $this->summary ) == '' ) {
 -                                      $this->missingSummary = true;
 -                                      $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 -                                      $status->value = self::AS_SUMMARY_NEEDED;
 +                              if ( $this->isConflict ) {
 +                                      $status->setResult( false, self::AS_CONFLICT_DETECTED );
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                      }
  
 -                      # All's well
 -                      wfProfileIn( __METHOD__ . '-sectionanchor' );
 -                      $sectionanchor = '';
 -                      if ( $this->section == 'new' ) {
 -                              if ( $this->textbox1 == '' ) {
 -                                      $this->missingComment = true;
 -                                      $status->fatal( 'missingcommenttext' );
 -                                      $status->value = self::AS_TEXTBOX_EMPTY;
 -                                      wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                              // Run post-section-merge edit filter
 +                              if ( !wfRunHooks( 'EditFilterMerged', array( $this, $content->serialize( $this->content_format ), &$this->hookError, $this->summary ) )
 +                                              || !wfRunHooks( 'EditFilterMergedContent', array( $this, $content, &$this->hookError, $this->summary ) ) ) {
 +                                      # Error messages etc. could be handled within the hook...
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR;
 +                                      wfProfileOut( __METHOD__ );
 +                                      return $status;
 +                              } elseif ( $this->hookError != '' ) {
 +                                      # ...or the hook could be expecting us to produce an error
 +                                      $status->fatal( 'hookaborted' );
 +                                      $status->value = self::AS_HOOK_ERROR_EXPECTED;
                                        wfProfileOut( __METHOD__ );
                                        return $status;
                                }
 -                              if ( $this->sectiontitle !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 -                                      // If no edit summary was specified, create one automatically from the section
 -                                      // title and have it link to the new section. Otherwise, respect the summary as
 -                                      // passed.
 -                                      if ( $this->summary === '' ) {
 -                                              $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 -                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +
 +                              $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +                              # Handle the user preference to force summaries here, but not for null edits
 +                              if ( $this->section != 'new' && !$this->allowBlankSummary
 +                                      && !$content->equals( $this->getOriginalContent() )
 +                                      && !$content->isRedirect() ) # check if it's not a redirect
 +                              {
 +                                      if ( md5( $this->summary ) == $this->autoSumm ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' );
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
                                        }
 -                              } elseif ( $this->summary !== '' ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 -                                      # This is a new section, so create a link to the new section
 -                                      # in the revision summary.
 -                                      $cleanSummary = $wgParser->stripSectionName( $this->summary );
 -                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
                                }
 -                      } elseif ( $this->section != '' ) {
 -                              # Try to get a section anchor from the section source, redirect to edited section if header found
 -                              # XXX: might be better to integrate this into Article::replaceSection
 -                              # for duplicate heading checking and maybe parsing
 -                              $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 -                              # we can't deal with anchors, includes, html etc in the header for now,
 -                              # headline would need to be parsed to improve this
 -                              if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 -                                      $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +
 +                              # And a similar thing for new sections
 +                              if ( $this->section == 'new' && !$this->allowBlankSummary ) {
 +                                      if ( trim( $this->summary ) == '' ) {
 +                                              $this->missingSummary = true;
 +                                              $status->fatal( 'missingsummary' ); // or 'missingcommentheader' if $section == 'new'. Blegh
 +                                              $status->value = self::AS_SUMMARY_NEEDED;
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
                                }
 -                      }
 -                      $result['sectionanchor'] = $sectionanchor;
 -                      wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      // Save errors may fall down to the edit form, but we've now
 -                      // merged the section into full text. Clear the section field
 -                      // so that later submission of conflict forms won't try to
 -                      // replace that into a duplicated mess.
 -                      $this->textbox1 = $text;
 -                      $this->section = '';
 +                              # All's well
 +                              wfProfileIn( __METHOD__ . '-sectionanchor' );
 +                              $sectionanchor = '';
 +                              if ( $this->section == 'new' ) {
 +                                      if ( $this->textbox1 == '' ) {
 +                                              $this->missingComment = true;
 +                                              $status->fatal( 'missingcommenttext' );
 +                                              $status->value = self::AS_TEXTBOX_EMPTY;
 +                                              wfProfileOut( __METHOD__ . '-sectionanchor' );
 +                                              wfProfileOut( __METHOD__ );
 +                                              return $status;
 +                                      }
 +                                      if ( $this->sectiontitle !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->sectiontitle );
 +                                              // If no edit summary was specified, create one automatically from the section
 +                                              // title and have it link to the new section. Otherwise, respect the summary as
 +                                              // passed.
 +                                              if ( $this->summary === '' ) {
 +                                                      $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
 +                                                      $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSectionTitle );
 +                                              }
 +                                      } elseif ( $this->summary !== '' ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $this->summary );
 +                                              # This is a new section, so create a link to the new section
 +                                              # in the revision summary.
 +                                              $cleanSummary = $wgParser->stripSectionName( $this->summary );
 +                                              $this->summary = wfMsgForContent( 'newsectionsummary', $cleanSummary );
 +                                      }
 +                              } elseif ( $this->section != '' ) {
 +                                      # Try to get a section anchor from the section source, redirect to edited section if header found
 +                                      # XXX: might be better to integrate this into Article::replaceSection
 +                                      # for duplicate heading checking and maybe parsing
 +                                      $hasmatch = preg_match( "/^ *([=]{1,6})(.*?)(\\1) *\\n/i", $this->textbox1, $matches );
 +                                      # we can't deal with anchors, includes, html etc in the header for now,
 +                                      # headline would need to be parsed to improve this
 +                                      if ( $hasmatch && strlen( $matches[2] ) > 0 ) {
 +                                              $sectionanchor = $wgParser->guessLegacySectionNameFromWikiText( $matches[2] );
 +                                      }
 +                              }
 +                              $result['sectionanchor'] = $sectionanchor;
 +                              wfProfileOut( __METHOD__ . '-sectionanchor' );
  
 -                      $status->value = self::AS_SUCCESS_UPDATE;
 -              }
 +                              // Save errors may fall down to the edit form, but we've now
 +                              // merged the section into full text. Clear the section field
 +                              // so that later submission of conflict forms won't try to
 +                              // replace that into a duplicated mess.
 +                                      $this->textbox1 = $content->serialize( $this->content_format );
 +                              $this->section = '';
  
 -              // Check for length errors again now that the section is merged in
 -              $this->kblength = (int)( strlen( $text ) / 1024 );
 -              if ( $this->kblength > $wgMaxArticleSize ) {
 -                      $this->tooBig = true;
 -                      $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              }
 +                              $status->value = self::AS_SUCCESS_UPDATE;
 +                      }
 +
 +                      // Check for length errors again now that the section is merged in
 +                              $this->kblength = (int)( strlen( $content->serialize( $this->content_format ) ) / 1024 );
 +                      if ( $this->kblength > $wgMaxArticleSize ) {
 +                              $this->tooBig = true;
 +                              $status->setResult( false, self::AS_MAX_ARTICLE_SIZE_EXCEEDED );
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      }
  
 -              $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 -                      ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 -                      ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 -                      ( $bot ? EDIT_FORCE_BOT : 0 );
 +                      $flags = EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
 +                              ( $new ? EDIT_NEW : EDIT_UPDATE ) |
 +                              ( ( $this->minoredit && !$this->isNew ) ? EDIT_MINOR : 0 ) |
 +                              ( $bot ? EDIT_FORCE_BOT : 0 );
  
 -              $doEditStatus = $this->mArticle->doEdit( $text, $this->summary, $flags );
 +                              $doEditStatus = $this->mArticle->doEditContent( $content, $this->summary, $flags, false, null, $this->content_format );
  
 -              if ( $doEditStatus->isOK() ) {
 -                      $result['redirect'] = Title::newFromRedirect( $text ) !== null;
 -                      $this->commitWatch();
 -                      wfProfileOut( __METHOD__ );
 -                      return $status;
 -              } else {
 -                      // Failure from doEdit()
 -                      // Show the edit conflict page for certain recognized errors from doEdit(),
 -                      // but don't show it for errors from extension hooks
 -                      $errors = $doEditStatus->getErrorsArray();
 -                      if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
 -                              'edit-already-exists' ) ) )
 -                      {
 -                              $this->isConflict = true;
 -                              // Destroys data doEdit() put in $status->value but who cares
 -                              $doEditStatus->value = self::AS_END;
 +                      if ( $doEditStatus->isOK() ) {
 +                                      $result['redirect'] = $content->isRedirect();
 +                              $this->commitWatch();
 +                              wfProfileOut( __METHOD__ );
 +                              return $status;
 +                      } else {
 +                              // Failure from doEdit()
 +                              // Show the edit conflict page for certain recognized errors from doEdit(),
 +                              // but don't show it for errors from extension hooks
 +                              $errors = $doEditStatus->getErrorsArray();
 +                              if ( in_array( $errors[0][0], array( 'edit-gone-missing', 'edit-conflict',
 +                                      'edit-already-exists' ) ) )
 +                              {
 +                                      $this->isConflict = true;
 +                                      // Destroys data doEdit() put in $status->value but who cares
 +                                      $doEditStatus->value = self::AS_END;
 +                              }
 +                              wfProfileOut( __METHOD__ );
 +                              return $doEditStatus;
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $status->fatal( 'content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $status->value = self::AS_PARSE_ERROR;
                        wfProfileOut( __METHOD__ );
 -                      return $doEditStatus;
 +                      return $status;
                }
        }
  
         * @parma $editText string
         *
         * @return bool
 +       * @deprecated since 1.WD, use mergeChangesIntoContent() instead
         */
 -      function mergeChangesInto( &$editText ) {
 +      function mergeChangesInto( &$editText ){
 +              wfDebug( __METHOD__, "1.WD" );
 +
 +              $editContent = ContentHandler::makeContent( $editText, $this->getTitle(), $this->content_model, $this->content_format );
 +
 +              $ok = $this->mergeChangesIntoContent( $editContent );
 +
 +              if ( $ok ) {
 +                      $editText = $editContent->serialize( $this->content_format ); #XXX: really serialize?!
 +                      return true;
 +              } else {
 +                      return false;
 +              }
 +      }
 +
 +      /**
 +       * @private
 +       * @todo document
 +       *
 +       * @parma $editText string
 +       *
 +       * @return bool
 +       * @since since 1.WD
 +       */
 +      private function mergeChangesIntoContent( &$editContent ){
                wfProfileIn( __METHOD__ );
  
                $db = wfGetDB( DB_MASTER );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $baseText = $baseRevision->getText();
 +              $baseContent = $baseRevision->getContent();
  
                // The current state, we want to merge updates into it
                $currentRevision = Revision::loadFromTitle( $db, $this->mTitle );
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 -              $currentText = $currentRevision->getText();
 +              $currentContent = $currentRevision->getContent();
 +
 +              $handler = ContentHandler::getForModelID( $baseContent->getModel() );
  
 -              $result = '';
 -              if ( wfMerge( $baseText, $editText, $currentText, $result ) ) {
 -                      $editText = $result;
 +              $result = $handler->merge3( $baseContent, $editContent, $currentContent );
 +
 +              if ( $result ) {
 +                      $editContent = $result;
                        wfProfileOut( __METHOD__ );
                        return true;
                } else {
                        }
                }
  
 +              #FIXME: add EditForm plugin interface and use it here! #FIXME: search for textarea1 and textares2, and allow EditForm to override all uses.
                $wgOut->addHTML( Html::openElement( 'form', array( 'id' => self::EDITFORM_ID, 'name' => self::EDITFORM_ID,
                        'method' => 'post', 'action' => $this->getActionURL( $this->getContextTitle() ),
                        'enctype' => 'multipart/form-data' ) ) );
  
                $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
  
 +              $wgOut->addHTML( Html::hidden( 'format', $this->content_format ) );
 +              $wgOut->addHTML( Html::hidden( 'model', $this->content_model ) );
 +
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
                        $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                        // resolved between page source edits and custom ui edits using the
                        // custom edit ui.
                        $this->textbox2 = $this->textbox1;
 -                      $this->textbox1 = $this->getCurrentText();
 +
 +                      $content = $this->getCurrentContent();
 +                      $this->textbox1 = $content->serialize( $this->content_format );
  
                        $this->showTextbox1();
                } else {
                $this->showTextbox( $this->textbox2, 'wpTextbox2', array( 'tabindex' => 6, 'readonly' ) );
        }
  
 -      protected function showTextbox( $content, $name, $customAttribs = array() ) {
 +      protected function showTextbox( $text, $name, $customAttribs = array() ) {
                global $wgOut, $wgUser;
  
 -              $wikitext = $this->safeUnicodeOutput( $content );
 +              $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
                        // is awkward.
                        $oldtext = $this->mTitle->getDefaultMessageText();
                        if( $oldtext !== false ) {
                                $oldtitlemsg = 'defaultmessagetext';
 +                              $oldContent = ContentHandler::makeContent( $oldtext, $this->mTitle );
 +                      } else {
 +                              $oldContent = null;
                        }
                } else {
 -                      $oldtext = $this->mArticle->getRawText();
 +                      $oldContent = $this->getOriginalContent();
                }
 -              $newtext = $this->mArticle->replaceSection(
 -                      $this->section, $this->textbox1, $this->summary, $this->edittime );
  
 +              $textboxContent = ContentHandler::makeContent( $this->textbox1, $this->getTitle(),
 +                                                                                                              $this->content_model, $this->content_format ); #XXX: handle parse errors ?
 +
 +              $newContent = $this->mArticle->replaceSectionContent(
 +                                                                                                      $this->section, $textboxContent,
 +                                                                                                      $this->summary, $this->edittime );
 +
 +              # hanlde legacy text-based hook
 +              $newtext_orig = $newContent->serialize( $this->content_format );
 +              $newtext = $newtext_orig; #clone
                wfRunHooks( 'EditPageGetDiffText', array( $this, &$newtext ) );
  
 +              if ( $newtext != $newtext_orig ) {
 +                                              #if the hook changed the text, create a new Content object accordingly.
 +                                              $newContent = ContentHandler::makeContent( $newtext, $this->getTitle(), $newContent->getModel() ); #XXX: handle parse errors ?
 +              }
 +
 +              wfRunHooks( 'EditPageGetDiffContent', array( $this, &$newContent ) );
 +
                $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
 -              $newtext = $wgParser->preSaveTransform( $newtext, $this->mTitle, $wgUser, $popts );
 +              $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
  
 -              if ( $oldtext !== false  || $newtext != '' ) {
 +              if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
                        $oldtitle = wfMsgExt( $oldtitlemsg, array( 'parseinline' ) );
                        $newtitle = wfMsgExt( 'yourtext', array( 'parseinline' ) );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $oldtext, $newtext );
 +                      $de = $oldContent->getContentHandler()->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $oldContent, $newContent );
 +
                        $difftext = $de->getDiff( $oldtitle, $newtitle );
                        $de->showDiffStyle();
                } else {
                if ( wfRunHooks( 'EditPageBeforeConflictDiff', array( &$this, &$wgOut ) ) ) {
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
  
 -                      $de = new DifferenceEngine( $this->mArticle->getContext() );
 -                      $de->setText( $this->textbox2, $this->textbox1 );
 +                      $content1 = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +                      $content2 = ContentHandler::makeContent( $this->textbox2, $this->getTitle(), $this->content_model, $this->content_format ); #XXX: handle parse errors?
 +
 +                      $handler = ContentHandler::getForModelID( $this->content_model );
 +                      $de = $handler->createDifferenceEngine( $this->mArticle->getContext() );
 +                      $de->setContent( $content2, $content1 );
                        $de->showDiff( wfMsgExt( 'yourtext', 'parseinline' ), wfMsg( 'storedversion' ) );
  
                        $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        return $parsedNote;
                }
  
 -              if ( $this->mTriedSave && !$this->mTokenOk ) {
 -                      if ( $this->mTokenOkExceptSuffix ) {
 -                              $note = wfMsg( 'token_suffix_mismatch' );
 -                      } else {
 -                              $note = wfMsg( 'session_fail_preview' );
 -                      }
 -              } elseif ( $this->incompleteForm ) {
 -                      $note = wfMsg( 'edit_form_incomplete' );
 -              } else {
 -                      $note = wfMsg( 'previewnote' ) .
 -                              ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 -              }
 +              $note = '';
  
 -              $parserOptions = ParserOptions::newFromUser( $wgUser );
 -              $parserOptions->setEditSection( false );
 -              $parserOptions->setTidy( true );
 -              $parserOptions->setIsPreview( true );
 -              $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
 -
 -              # don't parse non-wikitext pages, show message about preview
 -              if ( $this->mTitle->isCssJsSubpage() || !$this->mTitle->isWikitextPage() ) {
 -                      if ( $this->mTitle->isCssJsSubpage() ) {
 -                              $level = 'user';
 -                      } elseif ( $this->mTitle->isCssOrJsPage() ) {
 -                              $level = 'site';
 -                      } else {
 -                              $level = false;
 -                      }
 +              try {
 +                      $content = ContentHandler::makeContent( $this->textbox1, $this->getTitle(), $this->content_model, $this->content_format );
  
 -                      # Used messages to make sure grep find them:
 -                      # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 -                      $class = 'mw-code';
 -                      if ( $level ) {
 -                              if ( preg_match( "/\\.css$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}csspreview'>\n" . wfMsg( "{$level}csspreview" ) . "\n</div>";
 -                                      $class .= " mw-css";
 -                              } elseif ( preg_match( "/\\.js$/", $this->mTitle->getText() ) ) {
 -                                      $previewtext = "<div id='mw-{$level}jspreview'>\n" . wfMsg( "{$level}jspreview" ) . "\n</div>";
 -                                      $class .= " mw-js";
 +                      if ( $this->mTriedSave && !$this->mTokenOk ) {
 +                              if ( $this->mTokenOkExceptSuffix ) {
 +                                      $note = wfMsg( 'token_suffix_mismatch' );
                                } else {
 -                                      throw new MWException( 'A CSS/JS (sub)page but which is not css nor js!' );
 +                                      $note = wfMsg( 'session_fail_preview' );
                                }
 -                              $parserOutput = $wgParser->parse( $previewtext, $this->mTitle, $parserOptions );
 -                              $previewHTML = $parserOutput->getText();
 +                      } elseif ( $this->incompleteForm ) {
 +                              $note = wfMsg( 'edit_form_incomplete' );
                        } else {
 -                              $previewHTML = '';
 -                      }
 +                              $note = wfMsg( 'previewnote' ) .
 +                                      ' [[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' . wfMsg( 'continue-editing' ) . ']]';
 +                      }
 +
 +                      $parserOptions = ParserOptions::newFromUser( $wgUser );
 +                      $parserOptions->setEditSection( false );
 +                      $parserOptions->setTidy( true );
 +                      $parserOptions->setIsPreview( true );
 +                      $parserOptions->setIsSectionPreview( !is_null($this->section) && $this->section !== '' );
 +
 +                      if ( $this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage() ) {
 +                              # don't parse non-wikitext pages, show message about preview
 +                              if( $this->mTitle->isCssJsSubpage() ) {
 +                                      $level = 'user';
 +                              } elseif( $this->mTitle->isCssOrJsPage() ) {
 +                                      $level = 'site';
 +                              } else {
 +                                      $level = false;
 +                              }
  
 -                      $previewHTML .= "<pre class=\"$class\" dir=\"ltr\">\n" . htmlspecialchars( $this->textbox1 ) . "\n</pre>\n";
 -              } else {
 -                      $toparse = $this->textbox1;
 +                              if ( $content->getModel() == CONTENT_MODEL_CSS ) {
 +                                      $format = 'css';
 +                              } elseif ( $content->getModel() == CONTENT_MODEL_JAVASCRIPT ) {
 +                                      $format = 'js';
 +                              } else {
 +                                      $format = false;
 +                              }
  
 -                      # If we're adding a comment, we need to show the
 -                      # summary as the headline
 -                      if ( $this->section == "new" && $this->summary != "" ) {
 -                              $toparse = wfMsgForContent( 'newsectionheaderdefaultlevel', $this->summary ) . "\n\n" . $toparse;
 +                              # Used messages to make sure grep find them:
 +                              # Messages: usercsspreview, userjspreview, sitecsspreview, sitejspreview
 +                              if( $level && $format ) {
 +                                      $note = "<div id='mw-{$level}{$format}preview'>" . wfMsg( "{$level}{$format}preview" ) . "</div>";
 +                              } else {
 +                                      $note = wfMsg( 'previewnote' );
 +                              }
 +                      } else {
 +                              $note = wfMsg( 'previewnote' );
                        }
  
 -                      wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 -
 -                      $parserOptions->enableLimitReport();
 -
 -                      $toparse = $wgParser->preSaveTransform( $toparse, $this->mTitle, $wgUser, $parserOptions );
 -                      $parserOutput = $wgParser->parse( $toparse, $this->mTitle, $parserOptions );
 +                      $rt = $content->getRedirectChain();
  
 -                      $rt = Title::newFromRedirectArray( $this->textbox1 );
                        if ( $rt ) {
                                $previewHTML = $this->mArticle->viewRedirect( $rt, false );
                        } else {
 -                              $previewHTML = $parserOutput->getText();
 -                      }
  
 -                      $this->mParserOutput = $parserOutput;
 -                      $wgOut->addParserOutputNoText( $parserOutput );
 +                              # If we're adding a comment, we need to show the
 +                              # summary as the headline
 +                              if ( $this->section == "new" && $this->summary != "" ) {
 +                                      $content = $content->addSectionHeader( $this->summary );
 +                              }
 +
 +                              $toparse_orig = $content->serialize( $this->content_format );
 +                              $toparse = $toparse_orig;
 +                              wfRunHooks( 'EditPageGetPreviewText', array( $this, &$toparse ) );
 +
 +                              if ( $toparse !== $toparse_orig ) {
 +                                      #hook changed the text, create new Content object
 +                                      $content = ContentHandler::makeContent( $toparse, $this->getTitle(), $this->content_model, $this->content_format );
 +                              }
 +
 +                              wfRunHooks( 'EditPageGetPreviewContent', array( $this, &$content ) );
  
 -                      if ( count( $parserOutput->getWarnings() ) ) {
 -                              $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              $parserOptions->enableLimitReport();
 +
 +                              #XXX: For CSS/JS pages, we should have called the ShowRawCssJs hook here. But it's now deprecated, so never mind
 +                              $content = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
 +
 +                              // TODO: might be a saner way to get a meaningfull context here?
 +                              $parserOutput = $content->getParserOutput( $this->getArticle()->getTitle(), null, $parserOptions );
 +
 +                              $previewHTML = $parserOutput->getText();
 +                              $this->mParserOutput = $parserOutput;
 +                              $wgOut->addParserOutputNoText( $parserOutput );
 +
 +                              if ( count( $parserOutput->getWarnings() ) ) {
 +                                      $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
 +                              }
                        }
 +              } catch (MWContentSerializationException $ex) {
 +                      $note .= "\n\n" . wfMsg('content-failed-to-parse', $this->content_model, $this->content_format, $ex->getMessage() );
 +                      $previewHTML = '';
                }
  
                if ( $this->isConflict ) {
diff --combined includes/Export.php
@@@ -626,6 -626,9 +626,9 @@@ class XmlDumpWriter 
  
                $out  = "    <revision>\n";
                $out .= "      " . Xml::element( 'id', null, strval( $row->rev_id ) ) . "\n";
+               if( $row->rev_parent_id ) {
+                       $out .= "      " . Xml::element( 'parentid', null, strval( $row->rev_parent_id ) ) . "\n";
+               }
  
                $out .= $this->writeTimestamp( $row->rev_timestamp );
  
                        $out .= "      " . Xml::elementClean( 'comment', array(), strval( $row->rev_comment ) ) . "\n";
                }
  
 -              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 -                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 -              } else {
 -                      $out .= "      <sha1/>\n";
 -              }
 -
                $text = '';
                if ( $row->rev_deleted & Revision::DELETED_TEXT ) {
                        $out .= "      " . Xml::element( 'text', array( 'deleted' => 'deleted' ) ) . "\n";
                                "" ) . "\n";
                }
  
 +              if ( $row->rev_sha1 && !( $row->rev_deleted & Revision::DELETED_TEXT ) ) {
 +                      $out .= "      " . Xml::element('sha1', null, strval( $row->rev_sha1 ) ) . "\n";
 +              } else {
 +                      $out .= "      <sha1/>\n";
 +              }
 +
 +              if ( isset( $row->rev_content_model ) && !is_null( $row->rev_content_model )  ) {
 +                      $content_model = intval( $row->rev_content_model );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $title = Title::makeTitle( $row->page_namespace, $row->page_title );
 +                      $content_model = ContentHandler::getDefaultModelFor( $title );
 +              }
 +
 +              $name = ContentHandler::getContentModelName( $content_model );
 +              $out .= "      " . Xml::element('model', array( 'name' => $name ), strval( $content_model ) ) . "\n";
 +
 +              if ( isset( $row->rev_content_format ) && !is_null( $row->rev_content_format ) ) {
 +                      $content_format = intval( $row->rev_content_format );
 +              } else {
 +                      // probably using $wgContentHandlerUseDB = false;
 +                      // @todo: test!
 +                      $content_handler = ContentHandler::getForModelID( $content_model );
 +                      $content_format = $content_handler->getDefaultFormat();
 +              }
 +
 +              $mime = ContentHandler::getContentFormatMimeType( $content_format );
 +              $out .= "      " . Xml::element('format', array( 'mime' => $mime ), strval( $content_format ) ) . "\n";
 +
                wfRunHooks( 'XmlDumpWriterWriteRevision', array( &$this, &$out, $row, $text ) );
  
                $out .= "    </revision>\n";
diff --combined includes/Revision.php
@@@ -40,10 -40,6 +40,10 @@@ class Revision 
        protected $mTextRow;
        protected $mTitle;
        protected $mCurrent;
 +      protected $mContentModel;
 +      protected $mContentFormat;
 +      protected $mContent;
 +      protected $mContentHandler;
  
        const DELETED_TEXT = 1;
        const DELETED_COMMENT = 2;
         * @return Revision
         */
        public static function newFromArchiveRow( $row, $overrides = array() ) {
 +              global $wgContentHandlerUseDB;
 +
                $attribs = $overrides + array(
                        'page'       => isset( $row->ar_page_id ) ? $row->ar_page_id : null,
                        'id'         => isset( $row->ar_rev_id ) ? $row->ar_rev_id : null,
                        'deleted'    => $row->ar_deleted,
                        'len'        => $row->ar_len,
                        'sha1'       => isset( $row->ar_sha1 ) ? $row->ar_sha1 : null,
 +                      'content_model' => isset( $row->ar_content_model ) ? $row->ar_content_model : null,
 +                      'content_format'  => isset( $row->ar_content_format ) ? $row->ar_content_format : null,
                );
 +
 +              if ( !$wgContentHandlerUseDB ) {
 +                      unset( $attribs['content_model'] );
 +                      unset( $attribs['content_format'] );
 +              }
 +
                if ( isset( $row->ar_text ) && !$row->ar_text_id ) {
                        // Pre-1.5 ar_text row
                        $attribs['text'] = self::getRevisionText( $row, 'ar_' );
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'rev_id',
                        'rev_page',
                        'rev_text_id',
                        'rev_deleted',
                        'rev_len',
                        'rev_parent_id',
 -                      'rev_sha1'
 +                      'rev_sha1',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_format';
 +                      $fields[] = 'rev_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
                return array( 'user_name' );
        }
  
+       /**
+        * Do a batched query to get the parent revision lengths
+        * @param $db DatabaseBase
+        * @param $revIds Array
+        * @return array
+        */
+       public static function getParentLengths( $db, array $revIds ) {
+               $revLens = array();
+               if ( !$revIds ) {
+                       return $revLens; // empty
+               }
+               wfProfileIn( __METHOD__ );
+               $res = $db->select( 'revision',
+                       array( 'rev_id', 'rev_len' ),
+                       array( 'rev_id' => $revIds ),
+                       __METHOD__ );
+               foreach ( $res as $row ) {
+                       $revLens[$row->rev_id] = $row->rev_len;
+               }
+               wfProfileOut( __METHOD__ );
+               return $revLens;
+       }
        /**
         * Constructor
         *
                                $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'] ) ) { //@todo: when is that set? test with external store setup! check out insertOn() [dk]
 +                                      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 {
 +                                      #NOTE: this should never happen if we have either text or content object!
 +                                      $this->mSize = null;
 +                              }
                        }
 +
                        # 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.' );
                }
                if( isset( $this->mTitle ) ) {
                        return $this->mTitle;
                }
 -              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL
 +              if( !is_null( $this->mId ) ) { //rev_id is defined as NOT NULL, but this revision may not yet have been inserted.
                        $dbr = wfGetDB( DB_SLAVE );
                        $row = $dbr->selectRow(
                                array( 'page', 'revision' ),
                                $this->mTitle = Title::newFromRow( $row );
                        }
                }
 +
 +              //@todo: as a last resort, perhaps load from page table, if $this->mPage is given?!
                return $this->mTitle;
        }
  
         * @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
 +       * @todo: replace usage in core
         */
        public function getText( $audience = self::FOR_PUBLIC, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = $this->getContent( $audience, $user );
 +              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 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();
                }
        }
  
         * Fetch revision text without regard for view restrictions
         *
         * @return String
 +       *
 +       * @deprecated since 1.WD. Instead, use Revision::getContent( Revision::RAW ) or Revision::getSerializedData() as appropriate.
         */
        public function getRawText() {
 -              if( is_null( $this->mText ) ) {
 -                      // Revision text is immutable. Load on demand:
 -                      $this->mText = $this->loadText();
 -              }
 +              wfDeprecated( __METHOD__, "1.WD" );
 +
 +              return $this->getText( self::RAW );
 +      }
 +
 +      /**
 +       * Fetch original serialized data without regard for view restrictions
 +       *
 +       * @return String
 +       *
 +       * @since 1.WD
 +       */
 +      public function getSerializedData() {
                return $this->mText;
        }
  
 +      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 );
 +
 +                      $format = $this->getContentFormat();
 +
 +                      if ( !$this->mContentHandler->isSupportedFormat( $format ) ) {
 +                              $formatName = ContentHandler::getContentFormatMimeType( $format );
 +                              $modelName = ContentHandler::getContentModelName( $model );
 +
 +                              throw new MWException( "Oops, the content format #$format ($formatName) is not supported for this content model, #$model ($modelName)" );
 +                      }
 +              }
 +
 +              return $this->mContentHandler;
 +      }
 +
        /**
         * @return String
         */
         * @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();
 +              }
 +
 +              $this->checkContentModel();
 +
 +              $dbw->insert( 'revision', $row, __METHOD__ );
 +
                $this->mId = !is_null( $rev_id ) ? $rev_id : $dbw->insertId();
  
                wfRunHooks( 'RevisionInsertComplete', array( &$this, $data, $flags ) );
                return $this->mId;
        }
  
 +      protected function checkContentModel() {
 +              global $wgContentHandlerUseDB;
 +
 +              $title = $this->getTitle(); //note: returns null for revisions that have not yet been inserted.
 +
 +              $model = $this->getContentModel();
 +              $format = $this->getContentFormat();
 +              $handler = $this->getContentHandler();
 +
 +              if ( !$handler->isSupportedFormat( $format ) ) {
 +                      $t = $title->getPrefixedDBkey();
 +                      $modelName = ContentHandler::getContentModelName( $model );
 +                      $formatName = ContentHandler::getContentFormatMimeType( $format );
 +
 +                      throw new MWException( "Can't use format #$format ($formatName) with content model #$model ($modelName) on $t" );
 +              }
 +
 +              if ( !$wgContentHandlerUseDB && $title ) {
 +                      // if $wgContentHandlerUseDB is not set, all revisions must use the default content model and format.
 +
 +                      $defaultModel = ContentHandler::getDefaultModelFor( $title );
 +                      $defaultHandler = ContentHandler::getForModelID( $defaultModel );
 +                      $defaultFormat = $defaultHandler->getDefaultFormat();
 +
 +                      if ( $this->getContentModel() != $defaultModel ) {
 +                              $defaultModelName = ContentHandler::getContentModelName( $defaultModel );
 +                              $modelName = ContentHandler::getContentModelName( $model );
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't save non-default content model with \$wgContentHandlerUseDB disabled: model is #$model ($modelName), default for $t is #$defaultModel ($defaultModelName)" );
 +                      }
 +
 +                      if ( $this->getContentFormat() != $defaultFormat ) {
 +                              $defaultFormatName = ContentHandler::getContentFormatMimeType( $defaultFormat );
 +                              $formatName = ContentHandler::getContentFormatMimeType( $format );
 +                              $t = $title->getPrefixedDBkey();
 +
 +                              throw new MWException( "Can't use non-default content format with \$wgContentHandlerUseDB disabled: format is #$format ($formatName), default for $t is #$defaultFormat ($defaultFormatName)" );
 +                      }
 +              }
 +
 +              $content = $this->getContent( Revision::RAW );
 +
 +              if ( !$content->isValid() ) {
 +                      $t = $title->getPrefixedDBkey();
 +                      $modelName = ContentHandler::getContentModelName( $model );
 +
 +                      throw new MWException( "Content of $t is not valid! Content model is #$model ($modelName)" );
 +              }
 +      }
 +
        /**
         * Get the base 36 SHA-1 value for a string of text
         * @param $text String
         * @return Revision|null on error
         */
        public static function newNullRevision( $dbw, $pageId, $summary, $minor ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 +              $fields = array( 'page_latest', 'page_namespace', 'page_title',
 +                                              'rev_text_id', 'rev_len', 'rev_sha1' );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'rev_content_model';
 +                      $fields[] = 'rev_content_format';
 +              }
 +
                $current = $dbw->selectRow(
                        array( 'page', 'revision' ),
 -                      array( 'page_latest', 'page_namespace', 'page_title',
 -                              'rev_text_id', 'rev_len', 'rev_sha1' ),
 +                      $fields,
                        array(
                                'page_id' => $pageId,
                                'page_latest=rev_id',
                        __METHOD__ );
  
                if( $current ) {
 -                      $revision = new Revision( array(
 +                      $row = array(
                                'page'       => $pageId,
                                'comment'    => $summary,
                                'minor_edit' => $minor,
                                'parent_id'  => $current->page_latest,
                                'len'        => $current->rev_len,
                                'sha1'       => $current->rev_sha1
 -                              ) );
 +                      );
 +
 +                      if ( $wgContentHandlerUseDB ) {
 +                              $row[ 'content_model' ] = $current->rev_content_model;
 +                              $row[ 'content_format' ] = $current->rev_content_format;
 +                      }
 +
 +                      $revision = new Revision( $row );
                        $revision->setTitle( Title::makeTitle( $current->page_namespace, $current->page_title ) );
                } else {
                        $revision = null;
diff --combined includes/Title.php
@@@ -65,7 -65,6 +65,7 @@@ class Title 
        var $mFragment;                   // /< Title fragment (i.e. the bit after the #)
        var $mArticleID = -1;             // /< Article ID, fetched from the link cache on demand
        var $mLatestID = false;           // /< ID of most recent revision
 +      var $mContentModel = false;       // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
        private $mEstimateRevisions;      // /< Estimated number of revisions; null of not loaded
        var $mRestrictions = array();     // /< Array of groups allowed to edit this article
        var $mOldRestrictions = false;
                }
        }
  
 +      /**
 +       * Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
 +       * Uses $wgContentHandlerUseDB to determine whether to include page_content_model.
 +       *
 +       * @return array
 +       */
 +      protected static function getSelectFields() {
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
 +                      'page_namespace', 'page_title', 'page_id',
 +                      'page_len', 'page_is_redirect', 'page_latest',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
 +      }
 +
        /**
         * Create a new Title from an article ID
         *
                $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
                $row = $db->selectRow(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $id ),
                        __METHOD__
                );
  
                $res = $dbr->select(
                        'page',
 -                      array(
 -                              'page_namespace', 'page_title', 'page_id',
 -                              'page_len', 'page_is_redirect', 'page_latest',
 -                      ),
 +                      self::getSelectFields(),
                        array( 'page_id' => $ids ),
                        __METHOD__
                );
                                $this->mRedirect = (bool)$row->page_is_redirect;
                        if ( isset( $row->page_latest ) )
                                $this->mLatestID = (int)$row->page_latest;
 +                      if ( isset( $row->page_content_model ) )
 +                              $this->mContentModel = intval( $row->page_content_model );
 +                      else
 +                              $this->mContentModel = false; # initialized lazily in getContentModel()
                } else { // page not found
                        $this->mArticleID = 0;
                        $this->mLength = 0;
                        $this->mRedirect = false;
                        $this->mLatestID = 0;
 +                      $this->mContentModel = false; # initialized lazily in getContentModel()
                }
        }
  
                $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
                $t->mUrlform = wfUrlencode( $t->mDbkeyform );
                $t->mTextform = str_replace( '_', ' ', $title );
 +              $t->mContentModel = false; # initialized lazily in getContentModel()
                return $t;
        }
  
         *
         * @param $text String: Text with possible redirect
         * @return Title: The corresponding Title
 +       * @deprecated since 1.WD, use Content::getRedirectTarget instead.
         */
        public static function newFromRedirect( $text ) {
 -              return self::newFromRedirectInternal( $text );
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Title
 +       * @deprecated since 1.WD, use Content::getUltimateRedirectTarget instead.
         */
        public static function newFromRedirectRecurse( $text ) {
 -              $titles = self::newFromRedirectArray( $text );
 -              return $titles ? array_pop( $titles ) : null;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getUltimateRedirectTarget();
        }
  
        /**
         *
         * @param $text String Text with possible redirect
         * @return Array of Titles, with the destination last
 +       * @deprecated since 1.WD, use Content::getRedirectChain instead.
         */
        public static function newFromRedirectArray( $text ) {
 -              global $wgMaxRedirects;
 -              $title = self::newFromRedirectInternal( $text );
 -              if ( is_null( $title ) ) {
 -                      return null;
 -              }
 -              // recursive check to follow double redirects
 -              $recurse = $wgMaxRedirects;
 -              $titles = array( $title );
 -              while ( --$recurse > 0 ) {
 -                      if ( $title->isRedirect() ) {
 -                              $page = WikiPage::factory( $title );
 -                              $newtitle = $page->getRedirectTarget();
 -                      } else {
 -                              break;
 -                      }
 -                      // Redirects to some special pages are not permitted
 -                      if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
 -                              // the new title passes the checks, so make that our current title so that further recursion can be checked
 -                              $title = $newtitle;
 -                              $titles[] = $newtitle;
 -                      } else {
 -                              break;
 -                      }
 -              }
 -              return $titles;
 -      }
 -
 -      /**
 -       * Really extract the redirect destination
 -       * Do not call this function directly, use one of the newFromRedirect* functions above
 -       *
 -       * @param $text String Text with possible redirect
 -       * @return Title
 -       */
 -      protected static function newFromRedirectInternal( $text ) {
 -              global $wgMaxRedirects;
 -              if ( $wgMaxRedirects < 1 ) {
 -                      //redirects are disabled, so quit early
 -                      return null;
 -              }
 -              $redir = MagicWord::get( 'redirect' );
 -              $text = trim( $text );
 -              if ( $redir->matchStartAndRemove( $text ) ) {
 -                      // Extract the first link and see if it's usable
 -                      // Ensure that it really does come directly after #REDIRECT
 -                      // Some older redirects included a colon, so don't freak about that!
 -                      $m = array();
 -                      if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
 -                              // Strip preceding colon used to "escape" categories, etc.
 -                              // and URL-decode links
 -                              if ( strpos( $m[1], '%' ) !== false ) {
 -                                      // Match behavior of inline link parsing here;
 -                                      $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
 -                              }
 -                              $title = Title::newFromText( $m[1] );
 -                              // If the title is a redirect to bad special pages or is invalid, return null
 -                              if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
 -                                      return null;
 -                              }
 -                              return $title;
 -                      }
 -              }
 -              return null;
 +              $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 +              return $content->getRedirectChain();
        }
  
        /**
                return $this->mNamespace;
        }
  
 +      /**
 +       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 +       *
 +       * @return Integer: Content model id
 +       */
 +      public function getContentModel() {
 +              if ( !$this->mContentModel ) {
 +                      $linkCache = LinkCache::singleton();
 +                      $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 +              }
 +
 +              if ( !$this->mContentModel ) {
 +                      $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 +              }
 +
 +              if( !$this->mContentModel ) {
 +                      throw new MWException( "failed to determin content model!" );
 +              }
 +
 +              return $this->mContentModel;
 +      }
 +
 +      /**
 +       * Convenience method for checking a title's content model name
 +       *
 +       * @param int $id
 +       * @return Boolean true if $this->getContentModel() == $id
 +       */
 +      public function hasContentModel( $id ) {
 +              return $this->getContentModel() == $id;
 +      }
 +
        /**
         * Get the namespace text
         *
         */
        public function isConversionTable() {
                return $this->getNamespace() == NS_MEDIAWIKI &&
-                       strpos( $this->getText(), 'Conversiontable' ) !== false;
+                       strpos( $this->getText(), 'Conversiontable/' ) === 0;
        }
  
        /**
         * @return Bool
         */
        public function isWikitextPage() {
 -              $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
 -              wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
 -              return $retval;
 +              return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
        }
  
        /**
 -       * Could this page contain custom CSS or JavaScript, based
 -       * on the title?
 +       * Could this page contain custom CSS or JavaScript for the global UI.
 +       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
 +       * or CONTENT_MODEL_JAVASCRIPT.
 +       *
 +       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
 +       *
 +       * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
         *
         * @return Bool
         */
        public function isCssOrJsPage() {
 -              $retval = $this->mNamespace == NS_MEDIAWIKI
 -                      && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
 -              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
 -              return $retval;
 +              $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
 +                      && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                              || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 +
 +              #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
 +              #      hook funktions can force this method to return true even outside the mediawiki namespace.
 +
 +              wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
 +
 +              return $isCssOrJsPage;
        }
  
        /**
         * @return Bool
         */
        public function isCssJsSubpage() {
 -              return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
 +                                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
        }
  
        /**
         * @return Bool
         */
        public function isCssSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_CSS ) );
        }
  
        /**
         * @return Bool
         */
        public function isJsSubpage() {
 -              return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
 +              return ( NS_USER == $this->mNamespace && $this->isSubpage()
 +                      && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
        }
  
        /**
                if ( !$this->getArticleID( $flags ) ) {
                        return $this->mRedirect = false;
                }
 +
                $linkCache = LinkCache::singleton();
 -              $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mRedirect = (bool)$cached;
  
                return $this->mRedirect;
        }
                        return $this->mLength = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLength = intval( $cached );
  
                return $this->mLength;
        }
                        return $this->mLatestID = 0;
                }
                $linkCache = LinkCache::singleton();
 -              $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
 +              $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
 +              if ( $cached === null ) { # check the assumption that the cache actually knows about this title
 +                      # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
 +                      #      as a stop gap, perhaps log this, but don't throw an exception?
 +                      throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
 +              }
 +
 +              $this->mLatestID = intval( $cached );
  
                return $this->mLatestID;
        }
                $this->mRedirect = null;
                $this->mLength = -1;
                $this->mLatestID = false;
 +              $this->mContentModel = false;
                $this->mEstimateRevisions = null;
        }
  
  
                $res = $db->select(
                        array( 'page', $table ),
 -                      array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      self::getSelectFields(),
                        array(
                                "{$prefix}_from=page_id",
                                "{$prefix}_namespace" => $this->getNamespace(),
         * @return Array of Title objects linking here
         */
        public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
 +              global $wgContentHandlerUseDB;
 +
                $id = $this->getArticleID();
  
                # If the page doesn't exist; there can't be any link from this page
                $namespaceFiled = "{$prefix}_namespace";
                $titleField = "{$prefix}_title";
  
 +              $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $res = $db->select(
                        array( $table, 'page' ),
 -                      array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
 +                      $fields,
                        array( "{$prefix}_from" => $id ),
                        __METHOD__,
                        $options,
         * @return Bool
         */
        public function isSingleRevRedirect() {
 +              global $wgContentHandlerUseDB;
 +
                $dbw = wfGetDB( DB_MASTER );
 +
                # Is it a redirect?
 +              $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
 +              if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
 +
                $row = $dbw->selectRow( 'page',
 -                      array( 'page_is_redirect', 'page_latest', 'page_id' ),
 +                      $fields,
                        $this->pageCond(),
                        __METHOD__,
                        array( 'FOR UPDATE' )
                $this->mArticleID = $row ? intval( $row->page_id ) : 0;
                $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
                $this->mLatestID = $row ? intval( $row->page_latest ) : false;
 +              $this->mContentModel = $row && isset( $row->page_content_model ) ? intval( $row->page_content_model ) : false;
                if ( !$this->mRedirect ) {
                        return false;
                }
                if( !is_object( $rev ) ){
                        return false;
                }
 -              $text = $rev->getText();
 +              $content = $rev->getContent();
                # Does the redirect point to the source?
                # Or is it a broken self-redirect, usually caused by namespace collisions?
 -              $m = array();
 -              if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
 -                      $redirTitle = Title::newFromText( $m[1] );
 -                      if ( !is_object( $redirTitle ) ||
 -                              ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 -                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
 +              $redirTitle = $content->getRedirectTarget();
 +
 +              if ( $redirTitle ) {
 +                      if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
 +                              $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
                                wfDebug( __METHOD__ . ": redirect points to other page\n" );
                                return false;
 +                      } else {
 +                              return true;
                        }
                } else {
 -                      # Fail safe
 -                      wfDebug( __METHOD__ . ": failsafe\n" );
 +                      # Fail safe (not a redirect after all. strange.)
 +                      wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() . " is a redirect, but it doesn't contain a valid redirect.\n" );
                        return false;
                }
 -              return true;
        }
  
        /**
diff --combined includes/WikiPage.php
@@@ -230,21 -230,7 +230,21 @@@ class WikiPage extends Page 
         * @return Array
         */
        public function getActionOverrides() {
 -              return array();
 +              $content_handler = $this->getContentHandler();
 +              return $content_handler->getActionOverrides();
 +      }
 +
 +      /**
 +       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 +       *
 +       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 +       *
 +       * @return ContentHandler
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContentHandler() {
 +              return ContentHandler::getForModelID( $this->getContentModel() );
        }
  
        /**
         * @return array
         */
        public static function selectFields() {
 -              return array(
 +              global $wgContentHandlerUseDB;
 +
 +              $fields = array(
                        'page_id',
                        'page_namespace',
                        'page_title',
                        'page_latest',
                        'page_len',
                );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $fields[] = 'page_content_model';
 +              }
 +
 +              return $fields;
        }
  
        /**
        }
  
        /**
 -       * Tests if the article text represents a redirect
 +       * Tests if the article content represents a redirect
         *
 -       * @param $text mixed string containing article contents, or boolean
         * @return bool
         */
 -      public function isRedirect( $text = false ) {
 -              if ( $text === false ) {
 -                      if ( !$this->mDataLoaded ) {
 -                              $this->loadPageData();
 -                      }
 +      public function isRedirect( ) {
 +              $content = $this->getContent();
 +              if ( !$content ) return false;
  
 -                      return (bool)$this->mIsRedirect;
 -              } else {
 -                      return Title::newFromRedirect( $text ) !== null;
 +              return $content->isRedirect();
 +      }
 +
 +      /**
 +       * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
 +       *
 +       * Will use the revisions actual content model if the page exists,
 +       * and the page's default if the page doesn't exist yet.
 +       *
 +       * @return 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 null;
        }
  
 +      /**
 +       * Get the content of the current revision. No side-effects...
 +       *
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
 +       * @return Content|null The content of the current revision
 +       *
 +       * @since 1.WD
 +       */
 +      public function getContent( $audience = Revision::FOR_PUBLIC ) {
 +              $this->loadLastEdit();
 +              if ( $this->mLastRevision ) {
 +                      return $this->mLastRevision->getContent( $audience );
 +              }
 +              return null;
 +      }
 +
        /**
         * Get the text of the current revision. No side-effects...
         *
         *      Revision::FOR_PUBLIC       to be displayed to all users
         *      Revision::FOR_THIS_USER    to be displayed to $wgUser
         *      Revision::RAW              get the text regardless of permissions
 -       * @return String|bool The text of the current revision. False on failure
 +       * @return String|false The text of the current revision
 +       * @deprecated as of 1.WD, getContent() should be used instead.
         */
 -      public function getText( $audience = Revision::FOR_PUBLIC ) {
 +      public function getText( $audience = Revision::FOR_PUBLIC ) { #@todo: deprecated, replace usage!
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
                $this->loadLastEdit();
                if ( $this->mLastRevision ) {
                        return $this->mLastRevision->getText( $audience );
         * 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() {
 -              $this->loadLastEdit();
 -              if ( $this->mLastRevision ) {
 -                      return $this->mLastRevision->getRawText();
 -              }
 -              return false;
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              return $this->getText( Revision::RAW );
        }
  
        /**
                        return false;
                }
  
 -              $text = $editInfo ? $editInfo->pst : false;
 +              if ( $editInfo ) {
 +                      $content = $editInfo->pstContent;
 +              } else {
 +                      $content = $this->getContent();
 +              }
  
 -              if ( $this->isRedirect( $text ) ) {
 +              if ( !$content || $content->isRedirect( ) ) {
                        return false;
                }
  
 -              switch ( $wgArticleCountMethod ) {
 -              case 'any':
 -                      return true;
 -              case 'comma':
 -                      if ( $text === false ) {
 -                              $text = $this->getRawText();
 -                      }
 -                      return strpos( $text,  ',' ) !== false;
 -              case 'link':
 +              $hasLinks = null;
 +
 +              if ( $wgArticleCountMethod === 'link' ) {
 +                      # nasty special case to avoid re-parsing to detect links
 +
                        if ( $editInfo ) {
                                // ParserOutput::getLinks() is a 2D array of page links, so
                                // to be really correct we would need to recurse in the array
                                // but the main array should only have items in it if there are
                                // links.
 -                              return (bool)count( $editInfo->output->getLinks() );
 +                              $hasLinks = (bool)count( $editInfo->output->getLinks() );
                        } else {
 -                              return (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 +                              $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
                                        array( 'pl_from' => $this->getId() ), __METHOD__ );
                        }
                }
 +
 +              return $content->isCountable( $hasLinks );
        }
  
        /**
         */
        public function insertRedirect() {
                // recurse through to only get the final target
 -              $retval = Title::newFromRedirectRecurse( $this->getRawText() );
 +              $content = $this->getContent();
 +              $retval = $content ? $content->getUltimateRedirectTarget() : null;
                if ( !$retval ) {
                        return null;
                }
                        && $parserOptions->getStubThreshold() == 0
                        && $this->mTitle->exists()
                        && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
 -                      && $this->mTitle->isWikitextPage();
 +                      && $this->getContentHandler()->isParserCacheSupported();
        }
  
        /**
         * @param $parserOptions ParserOptions to use for the parse operation
         * @param $oldid Revision ID to get the text from, passing null or 0 will
         *               get the current revision (default value)
 +       *
         * @return ParserOutput or false if the revision was not found
         */
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 +                      //@todo: move this logic to MessageCache
 +
                        if ( $this->mTitle->exists() ) {
 -                              $text = $this->getRawText();
 +                              // NOTE: use transclusion text for messages.
 +                              //       This is consistent with  MessageCache::getMsgFromNamespace()
 +
 +                              $content = $this->getContent();
 +                              $text = $content === null ? null : $content->getWikitextForTransclusion();
 +
 +                              if ( $text === null ) $text = false;
                        } else {
                                $text = false;
                        }
         * @private
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
 +              global $wgContentHandlerUseDB;
 +
                wfProfileIn( __METHOD__ );
  
 -              $text = $revision->getText();
 -              $len = strlen( $text );
 -              $rt = Title::newFromRedirectRecurse( $text );
 +              $content = $revision->getContent();
 +              $len = $content->getSize();
 +              $rt = $content->getUltimateRedirectTarget();
  
                $conditions = array( 'page_id' => $this->getId() );
  
                }
  
                $now = wfTimestampNow();
 +              $row = array( /* SET */
 +                      'page_latest'      => $revision->getId(),
 +                      'page_touched'     => $dbw->timestamp( $now ),
 +                      'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 +                      'page_is_redirect' => $rt !== null ? 1 : 0,
 +                      'page_len'         => $len,
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'page_content_model' ] = $revision->getContentModel();
 +              }
 +
                $dbw->update( 'page',
 -                      array( /* SET */
 -                              'page_latest'      => $revision->getId(),
 -                              'page_touched'     => $dbw->timestamp( $now ),
 -                              'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
 -                              'page_is_redirect' => $rt !== null ? 1 : 0,
 -                              'page_len'         => $len,
 -                      ),
 +                      $row,
                        $conditions,
                        __METHOD__ );
  
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        # Update the LinkCache.
 -                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest );
 +                      LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect, $this->mLatest, $revision->getContentModel() );
                }
  
                wfProfileOut( __METHOD__ );
         * @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();
 +
 +              if ( $this->mLastRevision ) {
 +                      if ( is_null( $undoafter ) ) {
 +                              $undoafter = $undo->getPrevious();
 +                      }
  
 -              $undone_text = '';
 +                      $handler = $this->getContentHandler();
 +                      $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
  
 -              if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) ) {
 -                      return false;
 +                      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' );
 +
 +              if ( !$this->supportsSections() ) {
 +                      return null;
 +              }
 +
 +              $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() ); # could even make section title, but that's not required.
 +
 +              $newContent = $this->replaceSectionContent( $section, $sectionContent, $sectionTitle, $edittime );
 +
 +              return ContentHandler::getContentText( $newContent );
 +      }
 +
 +      /**
 +       * Returns true iff this page's content model supports sections.
 +       *
 +       * @return boolean whether sections are supported.
 +       *
 +       * @todo: the skin should check this and not offer section functionality if sections are not supported.
 +       * @todo: the EditPage should check this and not offer section functionality if sections are not supported.
 +       */
 +      public function supportsSections() {
 +              return $this->getContentHandler()->supportsSections();
 +      }
 +
 +      /**
 +       * @param $section null|bool|int or a section number (0, 1, 2, T1, T2...)
 +       * @param $content Content: new content of the section
 +       * @param $sectionTitle String: new section's subject, only if $section is 'new'
 +       * @param $edittime String: revision timestamp or null to use the current revision
 +       *
 +       * @return Content new complete article content, or null if error
 +       *
 +       * @since 1.WD
 +       */
 +      public function replaceSectionContent( $section, Content $sectionContent, $sectionTitle = '', $edittime = null ) {
                wfProfileIn( __METHOD__ );
  
 +              if ( !$this->supportsSections() ) {
 +                      #XXX: log this?
 +                      return null;
 +              }
 +
                if ( strval( $section ) == '' ) {
                        // Whole-page edit; let the whole text through
 +                      $newContent = $sectionContent;
                } else {
                        // Bug 30711: always use current version when adding a new section
                        if ( is_null( $edittime ) || $section == 'new' ) {
 -                              $oldtext = $this->getRawText();
 -                              if ( $oldtext === false ) {
 +                              $oldContent = $this->getContent();
 +                              if ( ! $oldContent ) {
                                        wfDebug( __METHOD__ . ": no page text\n" );
                                        wfProfileOut( __METHOD__ );
                                        return null;
                                        return null;
                                }
  
 -                              $oldtext = $rev->getText();
 +                              $oldContent = $rev->getContent();
                        }
  
 -                      if ( $section == 'new' ) {
 -                              # Inserting a new section
 -                              $subject = $sectionTitle ? wfMsgForContent( 'newsectionheaderdefaultlevel', $sectionTitle ) . "\n\n" : '';
 -                              if ( wfRunHooks( 'PlaceNewSection', array( $this, $oldtext, $subject, &$text ) ) ) {
 -                                      $text = strlen( trim( $oldtext ) ) > 0
 -                                              ? "{$oldtext}\n\n{$subject}{$text}"
 -                                              : "{$subject}{$text}";
 -                              }
 -                      } else {
 -                              # Replacing an existing section; roll out the big guns
 -                              global $wgParser;
 -
 -                              $text = $wgParser->replaceSection( $oldtext, $section, $text );
 -                      }
 +                      $newContent = $oldContent->replaceSection( $section, $sectionContent, $sectionTitle );
                }
  
                wfProfileOut( __METHOD__ );
 -              return $text;
 +              return $newContent;
        }
  
        /**
         *     revision:                The revision object for the inserted revision, or null
         *
         *  Compatibility note: this function previously returned a boolean value indicating success/failure
 +       *
 +       * @deprecated since 1.WD: use doEditContent() instead.
         */
 -      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
 +      public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) { #@todo: use doEditContent() instead
 +              wfDeprecated( __METHOD__, '1.WD' );
 +
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +
 +              return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
 +      }
 +
 +      /**
 +       * Change an existing article or create a new article. Updates RC and all necessary caches,
 +       * optionally via the deferred update array.
 +       *
 +       * @param $content Content: new content
 +       * @param $summary String: edit summary
 +       * @param $flags Integer bitfield:
 +       *      EDIT_NEW
 +       *          Article is known or assumed to be non-existent, create a new one
 +       *      EDIT_UPDATE
 +       *          Article is known or assumed to be pre-existing, update it
 +       *      EDIT_MINOR
 +       *          Mark this edit minor, if the user is allowed to do so
 +       *      EDIT_SUPPRESS_RC
 +       *          Do not log the change in recentchanges
 +       *      EDIT_FORCE_BOT
 +       *          Mark the edit a "bot" edit regardless of user rights
 +       *      EDIT_DEFER_UPDATES
 +       *          Defer some of the updates until the end of index.php
 +       *      EDIT_AUTOSUMMARY
 +       *          Fill in blank summaries with generated text where possible
 +       *
 +       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
 +       * If EDIT_UPDATE is specified and the article doesn't exist, the function will return an
 +       * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
 +       * edit-already-exists error will be returned. These two conditions are also possible with
 +       * auto-detection due to MediaWiki's performance-optimised locking strategy.
 +       *
 +       * @param $baseRevId the revision ID this edit was based off, if any
 +       * @param $user User the user doing the edit
 +       * @param $serialisation_format String: format for storing the content in the database
 +       *
 +       * @return Status object. Possible errors:
 +       *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
 +       *     edit-gone-missing:       In update mode, but the article didn't exist
 +       *     edit-conflict:           In update mode, the article changed unexpectedly
 +       *     edit-no-change:          Warning that the text was the same as before
 +       *     edit-already-exists:     In creation mode, but the article already exists
 +       *
 +       *  Extensions may define additional errors.
 +       *
 +       *  $return->value will contain an associative array with members as follows:
 +       *     new:                     Boolean indicating if the function attempted to create a new article
 +       *     revision:                The revision object for the inserted revision, or null
 +       *
 +       * @since 1.WD
 +       */
 +      public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 +                                                                 User $user = null, $serialisation_format = null ) {
                global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
  
                # Low-level sanity check
  
                $flags = $this->checkFlags( $flags );
  
 -              if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
 -                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
 -              {
 -                      wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
 +              # call legacy hook
 +              $hook_ok = wfRunHooks( 'ArticleContentSave', array( &$this, &$user, &$content, &$summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, &$status ) );
 +
 +              if ( $hook_ok && Hooks::isRegistered( '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 ) ); #TODO: survey extensions using this hook
 +
 +                      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,
 +                      ) ); #XXX: pass content object?!
  
 -                      $changed = ( strcmp( $text, $oldtext ) != 0 );
 +                      $changed = !$content->equals( $old_content );
  
                        if ( $changed ) {
 +                              if ( !$content->isValid() ) {
 +                                      throw new MWException( "New content failed validity check!" );
 +                              }
 +
                                $dbw->begin( __METHOD__ );
                                $revisionId = $revision->insertOn( $dbw );
  
                        }
  
                        # Update links tables, site stats, etc.
 -                      $this->doEditUpdates( $revision, $user, array( 'changed' => $changed,
 -                              'oldcountable' => $oldcountable ) );
 +                      $this->doEditUpdates(
 +                              $revision,
 +                              $user,
 +                              array(
 +                                      'changed' => $changed,
 +                                      'oldcountable' => $oldcountable
 +                              )
 +                      );
  
                        if ( !$changed ) {
                                $status->warning( 'edit-no-change' );
                                'page'       => $newid,
                                'comment'    => $summary,
                                'minor_edit' => $isminor,
 -                              'text'       => $text,
 +                              'text'       => $serialized,
 +                              'len'        => $newsize,
                                'user'       => $user->getId(),
                                'user_text'  => $user->getName(),
 -                              'timestamp'  => $now
 +                              'timestamp'  => $now,
 +                              'content_model' => $content->getModel(),
 +                              'content_format' => $serialisation_format,
                        ) );
                        $revisionId = $revision->insertOn( $dbw );
  
                                        $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
                                # Add RC row to the DB
                                $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
 -                                      '', strlen( $text ), $revisionId, $patrolled );
 +                                      '', $content->getSize(), $revisionId, $patrolled );
  
                                # Log auto-patrolled edits
                                if ( $patrolled ) {
                        # Update links, etc.
                        $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
  
 -                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
 +                      wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $serialized, $summary,
 +                              $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
 +
 +                      wfRunHooks( 'ArticleContentInsertComplete', array( &$this, &$user, $content, $summary,
                                $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
                }
  
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
  
 -              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
 +              wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $serialized, $summary,
 +                      $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
 +
 +              wfRunHooks( 'ArticleContentSaveComplete', array( &$this, &$user, $content, $summary,
                        $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
  
                # Promote user to any groups they meet the criteria for
        /**
         * Prepare text which is about to be saved.
         * Returns a stdclass with source, pst and output members
 -       * @return bool|object
 +       *
 +       * @deprecated in 1.WD: use prepareContentForEdit instead.
         */
        public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
 +              wfDeprecated( __METHOD__, '1.WD' );
 +              $content = ContentHandler::makeContent( $text, $this->getTitle() );
 +              return $this->prepareContentForEdit( $content, $revid , $user );
 +      }
 +
 +      /**
 +       * Prepare content which is about to be saved.
 +       * Returns a stdclass with source, pst and output members
 +       *
 +       * @param \Content $content
 +       * @param null $revid
 +       * @param null|\User $user
 +       * @param null $serialization_format
 +       *
 +       * @return bool|object
 +       *
 +       * @since 1.WD
 +       */
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
                global $wgParser, $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                // @TODO fixme: check $user->getId() here???
 +
                if ( $this->mPreparedEdit
 -                      && $this->mPreparedEdit->newText == $text
 +                      && $this->mPreparedEdit->newContent
 +                      && $this->mPreparedEdit->newContent->equals( $content )
                        && $this->mPreparedEdit->revid == $revid
 +                      && $this->mPreparedEdit->format == $serialization_format
 +                      #XXX: also check $user here?
                ) {
                        // Already prepared
                        return $this->mPreparedEdit;
  
                $edit = (object)array();
                $edit->revid = $revid;
 -              $edit->newText = $text;
 -              $edit->pst = $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
 +
 +              $edit->pstContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 +              $edit->pst = $edit->pstContent->serialize( $serialization_format ); #XXX: do we need this??
 +              $edit->format = $serialization_format;
 +
                $edit->popts = $this->makeParserOptions( 'canonical' );
 -              $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $edit->popts, true, true, $revid );
 -              $edit->oldText = $this->getRawText();
 +
 +              $edit->output = $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts );
 +
 +              $edit->newContent = $content;
 +              $edit->oldContent = $this->getContent( Revision::RAW );
 +
 +              #NOTE: B/C for hooks! don't use these fields!
 +              $edit->newText = ContentHandler::getContentText( $edit->newContent );
 +              $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
  
                $this->mPreparedEdit = $edit;
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @private
         * @param $revision Revision object
         * @param $user User object that did the revision
         * @param $options Array of options, following indexes are used:
                wfProfileIn( __METHOD__ );
  
                $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
 -              $text = $revision->getText();
 +              $content = $revision->getContent();
  
                # Parse the text
                # Be careful not to double-PST: $text is usually already PST-ed once
                if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
                        wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
 -                      $editInfo = $this->prepareTextForEdit( $text, $revision->getId(), $user );
 +                      $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
                } else {
                        wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
                        $editInfo = $this->mPreparedEdit;
                }
  
                # Update the links tables and other secondary data
 -              $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
 +              $contentHandler = $revision->getContentHandler();
 +              $updates = $contentHandler->getSecondaryDataUpdates( $content, $this->getTitle(), null, true, $editInfo->output );
                DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
                }
  
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, $good, $total ) );
 -              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $text ) );
 +              DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content->getTextForSearchIndex() ) ); #TODO: let the search engine decide what to do with the content object
  
                # If this is another user's talk page, update newtalk.
                # Don't do this if $options['changed'] = false (null-edits) nor if
                }
  
                if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
 -                      MessageCache::singleton()->replace( $shortTitle, $text );
 +                      $msgtext = $content->getWikitextForTransclusion(); #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,
 -              ) );
 +              ) ); #XXX: set the content object?
                $revision->insertOn( $dbw );
                $this->updateRevisionOn( $dbw, $revision );
  
        public function doDeleteArticleReal(
                $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
        ) {
 -              global $wgUser;
 +              global $wgUser, $wgContentHandlerUseDB;
  
                wfDebug( __METHOD__ . "\n" );
  
                        $bitfield = 'rev_deleted';
                }
  
 +              // we need to remember the old content so we can use it to generate all deletion updates.
 +              $content = $this->getContent( Revision::RAW );
 +
                $dbw = wfGetDB( DB_MASTER );
                $dbw->begin( __METHOD__ );
                // For now, shunt the revision data into the archive table.
                //
                // In the future, we may keep revisions and mark them with
                // the rev_deleted field, which is reserved for this purpose.
 +
 +              $row = array(
 +                      'ar_namespace'  => 'page_namespace',
 +                      'ar_title'      => 'page_title',
 +                      'ar_comment'    => 'rev_comment',
 +                      'ar_user'       => 'rev_user',
 +                      'ar_user_text'  => 'rev_user_text',
 +                      'ar_timestamp'  => 'rev_timestamp',
 +                      'ar_minor_edit' => 'rev_minor_edit',
 +                      'ar_rev_id'     => 'rev_id',
 +                      'ar_parent_id'  => 'rev_parent_id',
 +                      'ar_text_id'    => 'rev_text_id',
 +                      'ar_text'       => '\'\'', // Be explicit to appease
 +                      'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 +                      'ar_len'        => 'rev_len',
 +                      'ar_page_id'    => 'page_id',
 +                      'ar_deleted'    => $bitfield,
 +                      'ar_sha1'       => 'rev_sha1',
 +              );
 +
 +              if ( $wgContentHandlerUseDB ) {
 +                      $row[ 'ar_content_model' ] = 'rev_content_model';
 +                      $row[ 'ar_content_format' ] = 'rev_content_format';
 +              }
 +
                $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
 +                      $row,
                        array(
 -                              'ar_namespace'  => 'page_namespace',
 -                              'ar_title'      => 'page_title',
 -                              'ar_comment'    => 'rev_comment',
 -                              'ar_user'       => 'rev_user',
 -                              'ar_user_text'  => 'rev_user_text',
 -                              'ar_timestamp'  => 'rev_timestamp',
 -                              'ar_minor_edit' => 'rev_minor_edit',
 -                              'ar_rev_id'     => 'rev_id',
 -                              'ar_parent_id'  => 'rev_parent_id',
 -                              'ar_text_id'    => 'rev_text_id',
 -                              'ar_text'       => '\'\'', // Be explicit to appease
 -                              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
 -                              'ar_len'        => 'rev_len',
 -                              'ar_page_id'    => 'page_id',
 -                              'ar_deleted'    => $bitfield,
 -                              'ar_sha1'       => 'rev_sha1'
 -                      ), array(
                                'page_id' => $id,
                                'page_id = rev_page'
                        ), __METHOD__
                        return WikiPage::DELETE_NO_REVISIONS;
                }
  
 -              $this->doDeleteUpdates( $id );
 +              $this->doDeleteUpdates( $id, $content );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
         * Do some database updates after deletion
         *
         * @param $id Int: page_id value of the page being deleted (B/C, currently unused)
 +       * @param $content Content: optional page content to be used when determining the required updates.
 +       *        This may be needed because $this->getContent() may already return null when the page proper was deleted.
         */
 -      public function doDeleteUpdates( $id ) {
 +      public function doDeleteUpdates( $id, Content $content = null ) {
                # update site status
                DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
  
                # remove secondary indexes, etc
 -              $updates = $this->getDeletionUpdates( );
 +              $updates = $this->getDeletionUpdates( $content );
                DataUpdate::runUpdates( $updates );
  
                # Clear caches
                $this->mTitle->resetArticleID( 0 );
        }
  
 -      public function getDeletionUpdates() {
 -              $updates = array(
 -                      new LinksDeletionUpdate( $this ),
 -              );
 -
 -              //@todo: make a hook to add update objects
 -              //NOTE: deletion updates will be determined by the ContentHandler in the future
 -              return $updates;
 -      }
 -
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
                }
  
                # Actually store the edit
 -              $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId(), $guser );
 +              $status = $this->doEditContent( $target->getContent(), $summary, $flags, $target->getId(), $guser );
                if ( !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 );
        }
  
        /**
         *    if no revision occurred
         */
        public function getAutoDeleteReason( &$hasHistory ) {
 -              global $wgContLang;
 -
 -              // Get the last revision
 -              $rev = $this->getRevision();
 -
 -              if ( is_null( $rev ) ) {
 -                      return false;
 -              }
 -
 -              // Get the article's contents
 -              $contents = $rev->getText();
 -              $blank = false;
 -
 -              // If the page is blank, use the text from the previous revision,
 -              // which can only be blank if there's a move/import/protect dummy revision involved
 -              if ( $contents == '' ) {
 -                      $prev = $rev->getPrevious();
 -
 -                      if ( $prev )    {
 -                              $contents = $prev->getText();
 -                              $blank = true;
 -                      }
 -              }
 -
 -              $dbw = wfGetDB( DB_MASTER );
 -
 -              // Find out if there was only one contributor
 -              // Only scan the last 20 revisions
 -              $res = $dbw->select( 'revision', 'rev_user_text',
 -                      array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
 -                      __METHOD__,
 -                      array( 'LIMIT' => 20 )
 -              );
 -
 -              if ( $res === false ) {
 -                      // This page has no revisions, which is very weird
 -                      return false;
 -              }
 -
 -              $hasHistory = ( $res->numRows() > 1 );
 -              $row = $dbw->fetchObject( $res );
 -
 -              if ( $row ) { // $row is false if the only contributor is hidden
 -                      $onlyAuthor = $row->rev_user_text;
 -                      // Try to find a second contributor
 -                      foreach ( $res as $row ) {
 -                              if ( $row->rev_user_text != $onlyAuthor ) { // Bug 22999
 -                                      $onlyAuthor = false;
 -                                      break;
 -                              }
 -                      }
 -              } else {
 -                      $onlyAuthor = false;
 -              }
 -
 -              // Generate the summary with a '$1' placeholder
 -              if ( $blank ) {
 -                      // The current revision is blank and the one before is also
 -                      // blank. It's just not our lucky day
 -                      $reason = wfMsgForContent( 'exbeforeblank', '$1' );
 -              } else {
 -                      if ( $onlyAuthor ) {
 -                              $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
 -                      } else {
 -                              $reason = wfMsgForContent( 'excontent', '$1' );
 -                      }
 -              }
 -
 -              if ( $reason == '-' ) {
 -                      // Allow these UI messages to be blanked out cleanly
 -                      return '';
 -              }
 -
 -              // Replace newlines with spaces to prevent uglyness
 -              $contents = preg_replace( "/[\n\r]/", ' ', $contents );
 -              // Calculate the maximum amount of chars to get
 -              // Max content length = max comment length - length of the comment (excl. $1)
 -              $maxLength = 255 - ( strlen( $reason ) - 2 );
 -              $contents = $wgContLang->truncate( $contents, $maxLength );
 -              // Remove possible unfinished links
 -              $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
 -              // Now replace the '$1' placeholder
 -              $reason = str_replace( '$1', $contents, $reason );
 -
 -              return $reason;
 +              return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
        }
  
        /**
        public function quickEdit( $text, $comment = '', $minor = 0 ) {
                wfDeprecated( __METHOD__, '1.18' );
                global $wgUser;
-               return $this->doQuickEdit( $text, $wgUser, $comment, $minor );
+               $this->doQuickEdit( $text, $wgUser, $comment, $minor );
        }
  
        /**
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
 +
 +      /**
 +       * Returns a list of updates to be performed when this page is deleted. The updates should remove any infomration
 +       * about this page from secondary data stores such as links tables.
 +       *
 +       * @param Content|null $content optional Content object for determining the necessary updates
 +       * @return Array an array of DataUpdates objects
 +       */
 +      public function getDeletionUpdates( Content $content = null ) {
 +              if ( !$content ) {
 +                      // load content object, which may be used to determine the necessary updates
 +                      // XXX: the content may not be needed to determine the updates, then this would be overhead.
 +                      $content = $this->getContent( Revision::RAW );
 +              }
 +
 +              $updates = $this->getContentHandler()->getDeletionUpdates( $content, $this->mTitle );
 +
 +              wfRunHooks( 'WikiPageDeletionUpdates', array( $this, &$updates ) );
 +              return $updates;
 +      }
 +
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
        private $parserOptions;
  
        /**
 -       * @var string|null
 +       * @var Content|null
         */
 -      private $text;
 +      private $content = null;
  
        /**
         * @var ParserOutput|bool
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
 -       * @param $text String: text to parse or null to load it
 +       * @param $content Content|String: content to parse or null to load it; may also be given as a wikitext string, for BC
         */
 -      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $text = null ) {
 +      function __construct( Page $page, ParserOptions $parserOptions, $revid, $useParserCache, $content = null ) {
 +              if ( is_string($content) ) { #BC: old style call
 +                      $modelId = $page->getRevision()->getContentModel();
 +                      $format = $page->getRevision()->getContentFormat();
 +                      $content = ContentHandler::makeContent( $content, $page->getTitle(), $modelId, $format );
 +              }
 +
                $this->page = $page;
                $this->revid = $revid;
                $this->cacheable = $useParserCache;
                $this->parserOptions = $parserOptions;
 -              $this->text = $text;
 +              $this->content = $content;
                $this->cacheKey = ParserCache::singleton()->getKey( $page, $parserOptions );
                parent::__construct( 'ArticleView', $this->cacheKey . ':revid:' . $revid );
        }
         * @return bool
         */
        function doWork() {
 -              global $wgParser, $wgUseFileCache;
 +              global $wgUseFileCache;
 +
 +              // @todo: several of the methods called on $this->page are not declared in Page, but present in WikiPage and delegated by Article.
  
                $isCurrent = $this->revid === $this->page->getLatest();
  
 -              if ( $this->text !== null ) {
 -                      $text = $this->text;
 +              if ( $this->content !== null ) {
 +                      $content = $this->content;
                } elseif ( $isCurrent ) {
 -                      $text = $this->page->getRawText();
 +                      $content = $this->page->getContent( Revision::RAW ); #XXX: why use RAW audience here, and PUBLIC (default) below?
                } else {
                        $rev = Revision::newFromTitle( $this->page->getTitle(), $this->revid );
                        if ( $rev === null ) {
                                return false;
                        }
 -                      $text = $rev->getText();
 +                      $content = $rev->getContent(); #XXX: why use PUBLIC audience here (default), and RAW above?
                }
  
                $time = - microtime( true );
 -              $this->parserOutput = $wgParser->parse( $text, $this->page->getTitle(),
 -                      $this->parserOptions, true, true, $this->revid );
 +              $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
  
                # Timing hack
                return false;
        }
  }
 +
@@@ -3553,13 -3553,7 +3553,13 @@@ class Parser 
                        }
  
                        if ( $rev ) {
 -                              $text = $rev->getText();
 +                              $content = $rev->getContent();
 +                              $text = $content->getWikitextForTransclusion();
 +
 +                              if ( $text === false || $text === null ) {
 +                                      $text = false;
 +                                      break;
 +                              }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
                                        $text = false;
                                        break;
                                }
 +                              $content = $message->content();
                                $text = $message->plain();
                        } else {
                                break;
                        }
 -                      if ( $text === false ) {
 +                      if ( !$content ) {
                                break;
                        }
                        # Redirect?
                        $finalTitle = $title;
 -                      $title = Title::newFromRedirect( $text );
 +                      $title = $content->getRedirectTarget();
                }
                return array(
                        'text' => $text,
                        # Don't number the heading if it is the only one (looks silly)
                        if ( count( $matches[3] ) > 1 && $this->mOptions->getNumberHeadings() ) {
                                # the two are different if the line contains a link
-                               $headline = $numbering . ' ' . $headline;
+                               $headline = Html::element( 'span', array( 'class' => 'mw-headline-number' ), $numbering ) . ' ' . $headline;
                        }
  
                        # Create the anchor for linking from the TOC to the section
  
                $p1 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\))\\|]]/";           # [[ns:page (context)|]]
                $p4 = "/\[\[(:?$nc+:|:|)($tc+?)( ?($tc+))\\|]]/";           # [[ns:page(context)|]]
-               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)($tc+|)\\|]]/"; # [[ns:page (context), context|]]
+               $p3 = "/\[\[(:?$nc+:|:|)($tc+?)( ?\\($tc+\\)|)((?:, |,)$tc+|)\\|]]/"; # [[ns:page (context), context|]]
                $p2 = "/\[\[\\|($tc+)]]/";                                      # [[|page]]
  
                # try $p1 first, to turn "[[A, B (C)|]]" into "[[A, B (C)|A, B]]"
diff --combined languages/Language.php
@@@ -256,6 -256,7 +256,7 @@@ class Language 
         *
         * @param $code string
         *
+        * @throws MWException
         * @since 1.18
         * @return bool
         */
         */
        public function setNamespaces( array $namespaces ) {
                $this->namespaceNames = $namespaces;
 +              $this->mNamespaceIds = null;
 +      }
 +
 +      /**
 +       * Resets all of the namespace caches. Mainly used for testing
 +       */
 +      public function resetNamespaces( ) {
 +              $this->namespaceNames = null;
 +              $this->mNamespaceIds = null;
 +              $this->namespaceAliases = null;
        }
  
        /**
         * @param $title Title object to link
         * @param $offset Integer offset parameter
         * @param $limit Integer limit parameter
-        * @param $query String optional URL query parameter string
+        * @param $query array|String optional URL query parameter string
         * @param $atend Bool optional param for specified if this is the last page
         * @return String
         */
@@@ -2376,7 -2376,7 +2376,7 @@@ The title is {{msg-mw|nopagetitle}}.'
  'booksources-go' => 'Name of button in [[Special:BookSources]]
  
  {{Identical|Go}}',
- 'booksources-invalid-isbn' => 'This message is displayed after an invalid ISBN is entered on Special:Booksources.',
+ 'booksources-invalid-isbn' => 'This message is displayed after an invalid ISBN is entered on [[Special:Booksources]].',
  
  # Special:Log
  'specialloguserlabel' => 'Used in [[Special:Log]] as a label for an input field with which the log can be filtered for entries describing actions \'\'performed\'\' by the specified user.  "Carried out" and "done" are possible alternatives for "performed".',
@@@ -4738,10 -4738,4 +4738,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 ContentHandler::getContentModel()
 +'content-model-wikitext' => 'Name for the wikitext content model, used when decribing what type of content a page contains.',
 +'content-model-javascript' => 'Name for the JavaScript content model, used when decribing what type of content a page contains.',
 +'content-model-css' => 'Name for the CSS content model, used when decribing what type of content a page contains.',
 +'content-model-text' => 'Name for the plain text content model, used when decribing what type of content a page contains.',
 +
  );
@@@ -1,7 -1,6 +1,6 @@@
  <?php
  /**
-  * CheckBadRedirects - See if pages marked as being redirects
-  * really are.
+  * Check that pages marked as being redirects really are.
   *
   * 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
  
  require_once( dirname( __FILE__ ) . '/Maintenance.php' );
  
+ /**
+  * Maintenance script to check that pages marked as being redirects really are.
+  *
+  * @ingroup Maintenance
+  */
  class CheckBadRedirects extends Maintenance {
        public function __construct() {
                parent::__construct();
@@@ -46,7 -50,7 +50,7 @@@
                        $title = Title::makeTitle( $row->page_namespace, $row->page_title );
                        $rev = Revision::newFromId( $row->page_latest );
                        if ( $rev ) {
 -                              $target = Title::newFromRedirect( $rev->getText() );
 +                              $target = $rev->getContent()->getRedirectTarget();
                                if ( !$target ) {
                                        $this->output( $title->getPrefixedText() . "\n" );
                                }
@@@ -295,16 -295,17 +295,21 @@@ abstract class DumpTestCase extends Med
         * @param $text_sha1 string: the base36 SHA-1 of the revision's text
         * @param $text string|false: (optional) The revision's string, or false to check for a
         *            revision stub
 +       * @param $model int: the expected content model id (default: CONTENT_MODEL_WIKITEXT)
 +       * @param $format int: the expected format model id (default: CONTENT_FORMAT_WIKITEXT)
+        * @param $parentid int|false: (optional) id of the parent revision
         */
-       protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false,
-                                                                               $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT ) {
 -      protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, $parentid = false ) {
++      protected function assertRevision( $id, $summary, $text_id, $text_bytes, $text_sha1, $text = false, 
++                                              $model = CONTENT_MODEL_WIKITEXT, $format = CONTENT_FORMAT_WIKITEXT,
++                                              $parentid = false ) {
  
                $this->assertNodeStart( "revision" );
                $this->skipWhitespace();
  
                $this->assertTextNode( "id", $id );
+               if( $parentid ) {
+                       $this->assertTextNode( "parentid", $parentid );
+               }
                $this->assertTextNode( "timestamp", false );
  
                $this->assertNodeStart( "contributor" );
                $this->skipWhitespace();
  
                $this->assertTextNode( "comment", $summary );
 +              $this->skipWhitespace();
 +
 +              if ( $this->xml->name == "text" ) {
 +                      // note: <text> tag may occur here or at the very end.
 +                      $text_found = true;
 +                      $this->assertText( $id, $text_id, $text_bytes, $text );
 +              } else {
 +                      $text_found = false;
 +              }
  
                $this->assertTextNode( "sha1", $text_sha1 );
  
 +              $this->assertTextNode( "model", $model );
 +              $this->skipWhitespace();
 +
 +              $this->assertTextNode( "format", $format );
 +              $this->skipWhitespace();
 +
 +              if ( !$text_found ) {
 +                      $this->assertText( $id, $text_id, $text_bytes, $text );
 +              }
 +
 +              $this->assertNodeEnd( "revision" );
 +              $this->skipWhitespace();
 +      }
 +
 +      protected function assertText( $id, $text_id, $text_bytes, $text ) {
                $this->assertNodeStart( "text", false );
                if ( $text_bytes !== false ) {
                        $this->assertEquals( $this->xml->getAttribute( "bytes" ), $text_bytes,
                        $this->assertNodeEnd( "text" );
                        $this->skipWhitespace();
                }
+               $this->assertNodeEnd( "revision" );
+               $this->skipWhitespace();
        }
- }
 -
+ }
@@@ -21,6 -21,8 +21,8 @@@ class BaseDumpTest extends MediaWikiTes
                        $this->dump->close();
                }
  
+               // Bug 37458, parent teardown need to be done after closing the
+               // dump or it might cause some permissions errors.
                parent::tearDown();
        }
  
                $fname = $this->getNewTempFile();
  
                // The header of every prefetch file
-               $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.6/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.6/ http://www.mediawiki.org/xml/export-0.6.xsd" version="0.6" xml:lang="en">
+               $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en">
    <siteinfo>
      <sitename>wikisvn</sitename>
      <base>http://localhost/wiki-svn/index.php/Main_Page</base>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP1Summary1</comment>
-       <text xml:space="preserve">BackupDumperTestP1Text1</text>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
+       <text xml:space="preserve">BackupDumperTestP1Text1</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
      <id>2</id>
      <revision>
        <id>2</id>
+       <parentid>5</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary1</comment>
-       <text xml:space="preserve">BackupDumperTestP2Text1</text>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
+       <text xml:space="preserve">BackupDumperTestP2Text1</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
      <revision>
        <id>5</id>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary4 extra</comment>
-       <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
+       <text xml:space="preserve">BackupDumperTestP2Text4 some additional Text</text>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
      </revision>
    </page>
  ';
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
-       <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
+       <text xml:space="preserve">Talk about BackupDumperTestP1 Text1</text>
      </revision>
    </page>
  ';
@@@ -12,12 -12,14 +12,14 @@@ class TextPassDumperTest extends DumpTe
  
        // We'll add several pages, revision and texts. The following variables hold the
        // corresponding ids.
-       private $pageId1, $pageId2, $pageId3, $pageId4, $pageId5;
+       private $pageId1, $pageId2, $pageId3, $pageId4;
+       private static $numOfPages = 4;
        private $revId1_1, $textId1_1;
        private $revId2_1, $textId2_1, $revId2_2, $textId2_2;
        private $revId2_3, $textId2_3, $revId2_4, $textId2_4;
        private $revId3_1, $textId3_1, $revId3_2, $textId3_2;
        private $revId4_1, $textId4_1;
+       private static $numOfRevs = 8;
  
        function addDBData() {
                $this->tablesUsed[] = 'page';
                $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
                $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                       "BackupDumperTestP2Text1" );
+                       "BackupDumperTestP2Text1", $this->revId2_2 );
                $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                       "BackupDumperTestP2Text2" );
+                       "BackupDumperTestP2Text2", $this->revId2_3 );
                $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                       "BackupDumperTestP2Text3" );
+                       "BackupDumperTestP2Text3", $this->revId2_4 );
                $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
                        "BackupDumperTestP2Text4 some additional Text" );
                $this->assertPageStart( $this->pageId2, NS_MAIN, "BackupDumperTestP2" );
                $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                       "BackupDumperTestP2Text1" );
+                       "BackupDumperTestP2Text1", $this->revId2_2 );
                $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                       "BackupDumperTestP2Text2" );
+                       "BackupDumperTestP2Text2", $this->revId2_3 );
                // Prefetch kicks in. This is still the SHA-1 of the original text,
                // But the actual text (with different SHA-1) comes from prefetch.
                $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                       "Prefetch_________2Text3" );
+                       "Prefetch_________2Text3", $this->revId2_4 );
                $this->assertRevision( $this->revId2_4, "BackupDumperTestP2Summary4 extra",
                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
                        "BackupDumperTestP2Text4 some additional Text" );
                        switch ( $lookingForPage ) {
                        case 1:
                                // Page 1
-                               $this->assertPageStart( $this->pageId1 + $i * 4, NS_MAIN,
+                               $this->assertPageStart( $this->pageId1 + $i * self::$numOfPages, NS_MAIN,
                                        "BackupDumperTestP1" );
-                               $this->assertRevision( $this->revId1_1, "BackupDumperTestP1Summary1",
+                               $this->assertRevision( $this->revId1_1 + $i * self::$numOfRevs, "BackupDumperTestP1Summary1",
                                        $this->textId1_1, false, "0bolhl6ol7i6x0e7yq91gxgaan39j87",
                                        "BackupDumperTestP1Text1" );
                                $this->assertPageEnd();
  
                        case 2:
                                // Page 2
-                               $this->assertPageStart( $this->pageId2 + $i * 4, NS_MAIN,
+                               $this->assertPageStart( $this->pageId2 + $i * self::$numOfPages, NS_MAIN,
                                        "BackupDumperTestP2" );
-                               $this->assertRevision( $this->revId2_1, "BackupDumperTestP2Summary1",
+                               $this->assertRevision( $this->revId2_1 + $i * self::$numOfRevs, "BackupDumperTestP2Summary1",
                                        $this->textId2_1, false, "jprywrymfhysqllua29tj3sc7z39dl2",
-                                       "BackupDumperTestP2Text1" );
-                               $this->assertRevision( $this->revId2_2, "BackupDumperTestP2Summary2",
+                                       "BackupDumperTestP2Text1", $this->revId2_2 + $i * self::$numOfRevs );
+                               $this->assertRevision( $this->revId2_2 + $i * self::$numOfRevs, "BackupDumperTestP2Summary2",
                                        $this->textId2_2, false, "b7vj5ks32po5m1z1t1br4o7scdwwy95",
-                                       "BackupDumperTestP2Text2" );
-                               $this->assertRevision( $this->revId2_3, "BackupDumperTestP2Summary3",
+                                       "BackupDumperTestP2Text2", $this->revId2_3 + $i * self::$numOfRevs );
+                               $this->assertRevision( $this->revId2_3 + $i * self::$numOfRevs, "BackupDumperTestP2Summary3",
                                        $this->textId2_3, false, "jfunqmh1ssfb8rs43r19w98k28gg56r",
-                                       "BackupDumperTestP2Text3" );
-                               $this->assertRevision( $this->revId2_4,
+                                       "BackupDumperTestP2Text3", $this->revId2_4 + $i * self::$numOfRevs );
+                               $this->assertRevision( $this->revId2_4 + $i * self::$numOfRevs,
                                        "BackupDumperTestP2Summary4 extra",
                                        $this->textId2_4, false, "6o1ciaxa6pybnqprmungwofc4lv00wv",
                                        "BackupDumperTestP2Text4 some additional Text" );
  
                        case 4:
                                // Page 4
-                               $this->assertPageStart( $this->pageId4 + $i * 4, NS_TALK,
+                               $this->assertPageStart( $this->pageId4 + $i * self::$numOfPages, NS_TALK,
                                        "Talk:BackupDumperTestP1" );
-                               $this->assertRevision( $this->revId4_1,
+                               $this->assertRevision( $this->revId4_1 + $i * self::$numOfRevs,
                                        "Talk BackupDumperTestP1 Summary1",
                                        $this->textId4_1, false, "nktofwzd0tl192k3zfepmlzxoax1lpe",
                                        "Talk about BackupDumperTestP1 Text1" );
                if ( $fname === null ) {
                        $fname = $this->getNewTempFile();
                }
-               $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.6/" '
+               $header = '<mediawiki xmlns="http://www.mediawiki.org/xml/export-0.7/" '
                        . 'xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" '
-                       . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.6/ '
-                       . 'http://www.mediawiki.org/xml/export-0.6.xsd" version="0.6" xml:lang="en">
+                       . 'xsi:schemaLocation="http://www.mediawiki.org/xml/export-0.7/ '
+                       . 'http://www.mediawiki.org/xml/export-0.7.xsd" version="0.7" xml:lang="en">
    <siteinfo>
      <sitename>wikisvn</sitename>
      <base>http://localhost/wiki-svn/index.php/Main_Page</base>
                        $page1 = '  <page>
      <title>BackupDumperTestP1</title>
      <ns>0</ns>
-     <id>' . ( $this->pageId1 + $i * 4 ) . '</id>
+     <id>' . ( $this->pageId1 + $i * self::$numOfPages ) . '</id>
      <revision>
-       <id>' . $this->revId1_1 . '</id>
+       <id>' . ( $this->revId1_1 + $i * self::$numOfRevs ) . '</id>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP1Summary1</comment>
        <sha1>0bolhl6ol7i6x0e7yq91gxgaan39j87</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId1_1 . '" bytes="23" />
      </revision>
    </page>
                        $page2 = '  <page>
      <title>BackupDumperTestP2</title>
      <ns>0</ns>
-     <id>' . ( $this->pageId2 + $i * 4 ) . '</id>
+     <id>' . ( $this->pageId2 + $i * self::$numOfPages ) . '</id>
      <revision>
-       <id>' . $this->revId2_1 . '</id>
+       <id>' . ( $this->revId2_1 + $i * self::$numOfRevs ) . '</id>
+       <parentid>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary1</comment>
        <sha1>jprywrymfhysqllua29tj3sc7z39dl2</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId2_1 . '" bytes="23" />
      </revision>
      <revision>
-       <id>' . $this->revId2_2 . '</id>
+       <id>' . ( $this->revId2_2 + $i * self::$numOfRevs ) . '</id>
+       <parentid>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary2</comment>
        <sha1>b7vj5ks32po5m1z1t1br4o7scdwwy95</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId2_2 . '" bytes="23" />
      </revision>
      <revision>
-       <id>' . $this->revId2_3 . '</id>
+       <id>' . ( $this->revId2_3 + $i * self::$numOfRevs ) . '</id>
+       <parentid>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</parentid>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary3</comment>
        <sha1>jfunqmh1ssfb8rs43r19w98k28gg56r</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId2_3 . '" bytes="23" />
      </revision>
      <revision>
-       <id>' . $this->revId2_4 . '</id>
+       <id>' . ( $this->revId2_4 + $i * self::$numOfRevs ) . '</id>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>BackupDumperTestP2Summary4 extra</comment>
        <sha1>6o1ciaxa6pybnqprmungwofc4lv00wv</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId2_4 . '" bytes="44" />
      </revision>
    </page>
                        $page4 = '  <page>
      <title>Talk:BackupDumperTestP1</title>
      <ns>1</ns>
-     <id>' . ( $this->pageId4 + $i * 4 ) . '</id>
+     <id>' . ( $this->pageId4 + $i * self::$numOfPages ) . '</id>
      <revision>
-       <id>' . $this->revId4_1 . '</id>
+       <id>' . ( $this->revId4_1 + $i * self::$numOfRevs ) . '</id>
        <timestamp>2012-04-01T16:46:05Z</timestamp>
        <contributor>
          <ip>127.0.0.1</ip>
        </contributor>
        <comment>Talk BackupDumperTestP1 Summary1</comment>
        <sha1>nktofwzd0tl192k3zfepmlzxoax1lpe</sha1>
 +      <model name="wikitext">1</model>
 +      <format mime="text/x-wiki">1</format>
        <text id="' . $this->textId4_1 . '" bytes="35" />
      </revision>
    </page>